diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..c0564c2 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,4 @@ +service_name: travis-ci +src_dir: framework/yii +coverage_clover: tests/unit/runtime/coveralls/clover.xml +json_path: tests/unit/runtime/coveralls/coveralls-upload.json \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..818cb6a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,23 @@ +# Autodetect text files +* text=auto + +# ...Unless the name matches the following overriding patterns + +# Definitively text files +*.php text +*.css text +*.js text +*.txt text +*.md text +*.xml text +*.json text +*.bat text +*.sql text +*.xml text +*.yml text + +# Ensure those won't be messed up with +*.png binary +*.jpg binary +*.gif binary +*.ttf binary diff --git a/.gitignore b/.gitignore index 832a890..f2915a9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,10 @@ nbproject Thumbs.db # composer vendor dir -/yii/vendor \ No newline at end of file +/yii/vendor + +# composer itself is not needed +composer.phar + +# Mac DS_Store Files +.DS_Store diff --git a/.travis.yml b/.travis.yml index e4b8278..a905b36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,24 @@ language: php php: - - 5.3 - 5.4 - 5.5 -env: - - DB=mysql +services: + - redis-server + - memcached before_script: - - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS yiitest;'; fi" + - composer self-update && composer --version + - composer require satooshi/php-coveralls 0.6.* + - mysql -e 'CREATE DATABASE yiitest;'; + - psql -U postgres -c 'CREATE DATABASE yiitest;'; + - tests/unit/data/travis/apc-setup.sh + - tests/unit/data/travis/memcache-setup.sh + - tests/unit/data/travis/cubrid-setup.sh -script: phpunit \ No newline at end of file +script: + - phpunit --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata + +after_script: + - php vendor/bin/coveralls diff --git a/license.md b/LICENSE.md similarity index 96% rename from license.md rename to LICENSE.md index 92a3254..6edcc4f 100644 --- a/license.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ The Yii framework is free software. It is released under the terms of the following BSD License. -Copyright © 2008-2012 by Yii Software LLC (http://www.yiisoft.com) +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/readme.md b/README.md similarity index 76% rename from readme.md rename to README.md index 178acd4..dc03ac7 100644 --- a/readme.md +++ b/README.md @@ -9,15 +9,24 @@ If you are looking for a production-ready PHP framework, please use Yii 2.0 is still under heavy development. We may make significant changes without prior notices. **Yii 2.0 is not ready for production use yet.** +[](https://packagist.org/packages/yiisoft/yii2) +[](https://packagist.org/packages/yiisoft/yii2) +[](http://travis-ci.org/yiisoft/yii2) +[](https://www.versioneye.com/php/yiisoft:yii2/dev-master) + DIRECTORY STRUCTURE ------------------- apps/ ready-to-use Web apps built on Yii 2 - bootstrap/ a simple app supporting user login and contact page + advanced/ advanced app template with complex features + basic/ a simple app supporting user login and contact page + benchmark/ app demonstrating the minimal overhead introduced by the framework build/ internally used build tools docs/ documentation - framework/ framework source files + extensions/ extensions + framework/ framework files + yii/ framework source files tests/ tests of the core framework code @@ -33,7 +42,7 @@ DOCUMENTATION For 1.1 users, you may refer to [Upgrading from Yii 1.1](docs/guide/upgrade-from-v1.md) to have a general idea of what has changed in 2.0. -We are writing more documentation to get you started and learn more in depth. +[Definitive Guide draft](docs/guide/index.md) is available. It's not complete yet but main parts are already OK. HOW TO PARTICIPATE diff --git a/apps/advanced/.gitignore b/apps/advanced/.gitignore new file mode 100644 index 0000000..19dfc06 --- /dev/null +++ b/apps/advanced/.gitignore @@ -0,0 +1,2 @@ +/yii +/composer.lock \ No newline at end of file diff --git a/apps/advanced/LICENSE.md b/apps/advanced/LICENSE.md new file mode 100644 index 0000000..6edcc4f --- /dev/null +++ b/apps/advanced/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/apps/advanced/README.md b/apps/advanced/README.md new file mode 100644 index 0000000..3903532 --- /dev/null +++ b/apps/advanced/README.md @@ -0,0 +1,118 @@ +Yii 2 Advanced Application Template +=================================== + +**NOTE** Yii 2 and the relevant applications and extensions are still under heavy +development. We may make significant changes without prior notices. Please do not +use them for production. Please consider using [Yii v1.1](https://github.com/yiisoft/yii) +if you have a project to be deployed for production soon. + + +Thank you for using Yii 2 Advanced Application Template - an application template +that works out-of-box and can be easily customized to fit for your needs. + +Yii 2 Advanced Application Template is best suitable for large projects requiring frontend and backend separation, +deployment in different environments, configuration nesting etc. + + +DIRECTORY STRUCTURE +------------------- + +``` +common + config/ contains shared configurations + models/ contains model classes used in both backend and frontend +console + config/ contains console configurations + controllers/ contains console controllers (commands) + migrations/ contains database migrations + models/ contains console-specific model classes + runtime/ contains files generated during runtime +backend + assets/ contains application assets such as JavaScript and CSS + config/ contains backend configurations + controllers/ contains Web controller classes + models/ contains backend-specific model classes + runtime/ contains files generated during runtime + views/ contains view files for the Web application + web/ contains the entry script and Web resources +frontend + assets/ contains application assets such as JavaScript and CSS + config/ contains frontend configurations + controllers/ contains Web controller classes + models/ contains frontend-specific model classes + runtime/ contains files generated during runtime + views/ contains view files for the Web application + web/ contains the entry script and Web resources +vendor/ contains dependent 3rd-party packages +environments/ contains environment-based overrides +``` + + + +REQUIREMENTS +------------ + +The minimum requirement by Yii is that your Web server supports PHP 5.3.?. + +In order for captcha to work you need either GD2 extension or ImageMagick PHP extension. + +INSTALLATION +------------ + +### Install via Composer + +If you do not have [Composer](http://getcomposer.org/), you may download it from +[http://getcomposer.org/](http://getcomposer.org/) or run the following command on Linux/Unix/MacOS: + +~~~ +curl -s http://getcomposer.org/installer | php +~~~ + +You can then install the application using the following command: + +~~~ +php composer.phar create-project --stability=dev yiisoft/yii2-app-advanced yii-advanced +~~~ + +Note that in order to install some dependencies you must have `php_openssl` extension enabled. + + +### Install from an Archive File + +This is not currently available. We will provide it when Yii 2 is formally released. + + +### Install from development repository + +If you've cloned the [Yii 2 framework main development repository](https://github.com/yiisoft/yii2) you +can bootstrap your application with: + +~~~ +cd yii2/apps/advanced +php composer.phar create-project +~~~ + +*Note: If the above command fails with `[RuntimeException] Not enough arguments.` run +`php composer.phar self-update` to obtain an updated version of composer which supports creating projects +from local packages.* + + +GETTING STARTED +--------------- + +After you install the application, you have to conduct the following steps to initialize +the installed application. You only need to do these once for all. + +1. Execute the `init` command and select `dev` as environment. Alternatively you can execute it as `init --env=Development` +or `init --env=Production`. +2. Create a new database. It is assumed that MySQL InnoDB is used. If not, adjust `console/migrations/m130524_201442_init.php`. +3. In `common/config/params.php` set your database details in `components.db` values. +4. Apply migrations with `yii migrate`. + +Now you should be able to access: + +- the frontend using the URL `http://localhost/yii-advanced/frontend/web/` +- the backend using the URL `http://localhost/yii-advanced/backend/web/` + +assuming `yii-advanced` is directly under the document root of your Web server. + diff --git a/apps/bootstrap/assets/.gitignore b/apps/advanced/backend/assets/.gitkeep similarity index 100% rename from apps/bootstrap/assets/.gitignore rename to apps/advanced/backend/assets/.gitkeep diff --git a/apps/advanced/backend/config/.gitignore b/apps/advanced/backend/config/.gitignore new file mode 100644 index 0000000..20da318 --- /dev/null +++ b/apps/advanced/backend/config/.gitignore @@ -0,0 +1,2 @@ +main-local.php +params-local.php \ No newline at end of file diff --git a/apps/advanced/backend/config/AppAsset.php b/apps/advanced/backend/config/AppAsset.php new file mode 100644 index 0000000..267e48c --- /dev/null +++ b/apps/advanced/backend/config/AppAsset.php @@ -0,0 +1,29 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace backend\config; + +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class AppAsset extends AssetBundle +{ + public $basePath = '@webroot'; + public $baseUrl = '@web'; + public $css = array( + 'css/site.css', + ); + public $js = array( + ); + public $depends = array( + 'yii\web\YiiAsset', + 'yii\bootstrap\BootstrapAsset', + ); +} diff --git a/apps/advanced/backend/config/main.php b/apps/advanced/backend/config/main.php new file mode 100644 index 0000000..30c1825 --- /dev/null +++ b/apps/advanced/backend/config/main.php @@ -0,0 +1,42 @@ +<?php +$rootDir = __DIR__ . '/../..'; + +$params = array_merge( + require($rootDir . '/common/config/params.php'), + require($rootDir . '/common/config/params-local.php'), + require(__DIR__ . '/params.php'), + require(__DIR__ . '/params-local.php') +); + +return array( + 'id' => 'app-backend', + 'basePath' => dirname(__DIR__), + 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', + 'preload' => array('log'), + 'controllerNamespace' => 'backend\controllers', + 'modules' => array( + ), + 'components' => array( + 'request' => array( + 'enableCsrfValidation' => true, + ), + 'db' => $params['components.db'], + 'cache' => $params['components.cache'], + 'user' => array( + 'identityClass' => 'common\models\User', + ), + 'log' => array( + 'traceLevel' => YII_DEBUG ? 3 : 0, + 'targets' => array( + array( + 'class' => 'yii\log\FileTarget', + 'levels' => array('error', 'warning'), + ), + ), + ), + 'errorHandler' => array( + 'errorAction' => 'site/error', + ), + ), + 'params' => $params, +); diff --git a/apps/advanced/backend/config/params.php b/apps/advanced/backend/config/params.php new file mode 100644 index 0000000..1643a70 --- /dev/null +++ b/apps/advanced/backend/config/params.php @@ -0,0 +1,4 @@ +<?php +return array( + 'adminEmail' => 'admin@example.com', +); diff --git a/apps/advanced/backend/controllers/SiteController.php b/apps/advanced/backend/controllers/SiteController.php new file mode 100644 index 0000000..28f2310 --- /dev/null +++ b/apps/advanced/backend/controllers/SiteController.php @@ -0,0 +1,63 @@ +<?php + +namespace backend\controllers; + +use Yii; +use yii\web\Controller; +use common\models\LoginForm; + +class SiteController extends Controller +{ + public function behaviors() + { + return array( + 'access' => array( + 'class' => \yii\web\AccessControl::className(), + 'rules' => array( + array( + 'actions' => array('login'), + 'allow' => true, + 'roles' => array('?'), + ), + array( + 'actions' => array('logout', 'index'), + 'allow' => true, + 'roles' => array('@'), + ), + ), + ), + ); + } + + public function actions() + { + return array( + 'error' => array( + 'class' => 'yii\web\ErrorAction', + ), + ); + } + + public function actionIndex() + { + return $this->render('index'); + } + + public function actionLogin() + { + $model = new LoginForm(); + if ($model->load($_POST) && $model->login()) { + return $this->goHome(); + } else { + return $this->render('login', array( + 'model' => $model, + )); + } + } + + public function actionLogout() + { + Yii::$app->user->logout(); + return $this->goHome(); + } +} diff --git a/apps/bootstrap/protected/runtime/.gitignore b/apps/advanced/backend/models/.gitkeep similarity index 100% rename from apps/bootstrap/protected/runtime/.gitignore rename to apps/advanced/backend/models/.gitkeep diff --git a/apps/advanced/backend/runtime/.gitignore b/apps/advanced/backend/runtime/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/apps/advanced/backend/runtime/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/apps/advanced/backend/views/layouts/main.php b/apps/advanced/backend/views/layouts/main.php new file mode 100644 index 0000000..928f990 --- /dev/null +++ b/apps/advanced/backend/views/layouts/main.php @@ -0,0 +1,64 @@ +<?php +use backend\config\AppAsset; +use yii\helpers\Html; +use yii\bootstrap\Nav; +use yii\bootstrap\NavBar; +use yii\widgets\Breadcrumbs; + +/** + * @var $this \yii\base\View + * @var $content string + */ +AppAsset::register($this); +?> +<?php $this->beginPage(); ?> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="<?php echo Yii::$app->charset; ?>"/> + <title><?php echo Html::encode($this->title); ?></title> + <?php $this->head(); ?> +</head> +<body> + <?php $this->beginBody(); ?> + <?php + NavBar::begin(array( + 'brandLabel' => 'My Company', + 'brandUrl' => Yii::$app->homeUrl, + 'options' => array( + 'class' => 'navbar-inverse navbar-fixed-top', + ), + )); + $menuItems = array( + array('label' => 'Home', 'url' => array('/site/index')), + ); + if (Yii::$app->user->isGuest) { + $menuItems[] = array('label' => 'Login', 'url' => array('/site/login')); + } else { + $menuItems[] = array('label' => 'Logout (' . Yii::$app->user->identity->username .')' , 'url' => array('/site/logout')); + } + echo Nav::widget(array( + 'options' => array('class' => 'navbar-nav pull-right'), + 'items' => $menuItems, + )); + NavBar::end(); + ?> + + <div class="container"> + <?php echo Breadcrumbs::widget(array( + 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : array(), + )); ?> + <?php echo $content; ?> + </div> + + <footer class="footer"> + <div class="container"> + <p class="pull-left">© My Company <?php echo date('Y'); ?></p> + <p class="pull-right"><?php echo Yii::powered(); ?></p> + </div> + </footer> + + <?php $this->endBody(); ?> +</body> +</html> +<?php $this->endPage(); ?> diff --git a/apps/advanced/backend/views/site/error.php b/apps/advanced/backend/views/site/error.php new file mode 100644 index 0000000..024e27d --- /dev/null +++ b/apps/advanced/backend/views/site/error.php @@ -0,0 +1,29 @@ +<?php + +use yii\helpers\Html; + +/** + * @var yii\base\View $this + * @var string $name + * @var string $message + * @var Exception $exception + */ + +$this->title = $name; +?> +<div class="site-error"> + + <h1><?php echo Html::encode($this->title); ?></h1> + + <div class="alert alert-danger"> + <?php echo nl2br(Html::encode($message)); ?> + </div> + + <p> + The above error occurred while the Web server was processing your request. + </p> + <p> + Please contact us if you think this is a server error. Thank you. + </p> + +</div> diff --git a/apps/advanced/backend/views/site/index.php b/apps/advanced/backend/views/site/index.php new file mode 100644 index 0000000..f2e6d5e --- /dev/null +++ b/apps/advanced/backend/views/site/index.php @@ -0,0 +1,53 @@ +<?php +/** + * @var yii\base\View $this + */ +$this->title = 'My Yii Application'; +?> +<div class="site-index"> + + <div class="jumbotron"> + <h1>Congratulations!</h1> + + <p class="lead">You have successfully created your Yii-powered application.</p> + + <p><a class="btn btn-lg btn-success" href="http://www.yiiframework.com">Get started with Yii</a></p> + </div> + + <div class="body-content"> + + <div class="row"> + <div class="col-lg-4"> + <h2>Heading</h2> + + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.</p> + + <p><a class="btn btn-default" href="http://www.yiiframework.com/doc/">Yii Documentation »</a></p> + </div> + <div class="col-lg-4"> + <h2>Heading</h2> + + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.</p> + + <p><a class="btn btn-default" href="http://www.yiiframework.com/forum/">Yii Forum »</a></p> + </div> + <div class="col-lg-4"> + <h2>Heading</h2> + + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.</p> + + <p><a class="btn btn-default" href="http://www.yiiframework.com/extensions/">Yii Extensions »</a></p> + </div> + </div> + + </div> +</div> diff --git a/apps/bootstrap/protected/views/site/login.php b/apps/advanced/backend/views/site/login.php similarity index 64% rename from apps/bootstrap/protected/views/site/login.php rename to apps/advanced/backend/views/site/login.php index 65dc7d1..0c16570 100644 --- a/apps/bootstrap/protected/views/site/login.php +++ b/apps/advanced/backend/views/site/login.php @@ -10,15 +10,21 @@ use yii\widgets\ActiveForm; $this->title = 'Login'; $this->params['breadcrumbs'][] = $this->title; ?> -<h1><?php echo Html::encode($this->title); ?></h1> +<div class="site-login"> + <h1><?php echo Html::encode($this->title); ?></h1> -<p>Please fill out the following fields to login:</p> + <p>Please fill out the following fields to login:</p> -<?php $form = $this->beginWidget(ActiveForm::className(), array('options' => array('class' => 'form-horizontal'))); ?> - <?php echo $form->field($model, 'username')->textInput(); ?> - <?php echo $form->field($model, 'password')->passwordInput(); ?> - <?php echo $form->field($model, 'rememberMe')->checkbox(); ?> - <div class="form-actions"> - <?php echo Html::submitButton('Login', null, null, array('class' => 'btn btn-primary')); ?> + <div class="row"> + <div class="col-lg-5"> + <?php $form = ActiveForm::begin(array('id' => 'login-form')); ?> + <?php echo $form->field($model, 'username'); ?> + <?php echo $form->field($model, 'password')->passwordInput(); ?> + <?php echo $form->field($model, 'rememberMe')->checkbox(); ?> + <div class="form-group"> + <?php echo Html::submitButton('Login', array('class' => 'btn btn-primary')); ?> + </div> + <?php ActiveForm::end(); ?> + </div> </div> -<?php $this->endWidget(); ?> +</div> diff --git a/apps/advanced/backend/web/.gitignore b/apps/advanced/backend/web/.gitignore new file mode 100644 index 0000000..148f2b0 --- /dev/null +++ b/apps/advanced/backend/web/.gitignore @@ -0,0 +1 @@ +/index.php \ No newline at end of file diff --git a/apps/advanced/backend/web/assets/.gitignore b/apps/advanced/backend/web/assets/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/apps/advanced/backend/web/assets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/apps/advanced/backend/web/css/site.css b/apps/advanced/backend/web/css/site.css new file mode 100644 index 0000000..e6db73c --- /dev/null +++ b/apps/advanced/backend/web/css/site.css @@ -0,0 +1,19 @@ +body { + padding-top: 70px; +} + +.footer { + border-top: 1px solid #ddd; + margin-top: 30px; + padding-top: 15px; + padding-bottom: 30px; +} + +.jumbotron { + text-align: center; + background-color: transparent; +} +.jumbotron .btn { + font-size: 21px; + padding: 14px 24px; +} diff --git a/apps/advanced/common/config/.gitignore b/apps/advanced/common/config/.gitignore new file mode 100644 index 0000000..46f6eb4 --- /dev/null +++ b/apps/advanced/common/config/.gitignore @@ -0,0 +1 @@ +params-local.php \ No newline at end of file diff --git a/apps/advanced/common/config/params.php b/apps/advanced/common/config/params.php new file mode 100644 index 0000000..2dff87b --- /dev/null +++ b/apps/advanced/common/config/params.php @@ -0,0 +1,22 @@ +<?php + +Yii::setAlias('common', __DIR__ . '/../'); +Yii::setAlias('frontend', __DIR__ . '/../../frontend'); +Yii::setAlias('backend', __DIR__ . '/../../backend'); + +return array( + 'adminEmail' => 'admin@example.com', + 'supportEmail' => 'support@example.com', + + 'components.cache' => array( + 'class' => 'yii\caching\FileCache', + ), + + 'components.db' => array( + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=yii2advanced', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8', + ), +); diff --git a/apps/advanced/common/models/LoginForm.php b/apps/advanced/common/models/LoginForm.php new file mode 100644 index 0000000..4631dbd --- /dev/null +++ b/apps/advanced/common/models/LoginForm.php @@ -0,0 +1,58 @@ +<?php + +namespace common\models; + +use Yii; +use yii\base\Model; + +/** + * LoginForm is the model behind the login form. + */ +class LoginForm extends Model +{ + public $username; + public $password; + public $rememberMe = true; + + /** + * @return array the validation rules. + */ + public function rules() + { + return array( + // username and password are both required + array('username, password', 'required'), + // password is validated by validatePassword() + array('password', 'validatePassword'), + // rememberMe must be a boolean value + array('rememberMe', 'boolean'), + ); + } + + /** + * Validates the password. + * This method serves as the inline validation for password. + */ + public function validatePassword() + { + $user = User::findByUsername($this->username); + if (!$user || !$user->validatePassword($this->password)) { + $this->addError('password', 'Incorrect username or password.'); + } + } + + /** + * Logs in a user using the provided username and password. + * @return boolean whether the user is logged in successfully + */ + public function login() + { + if ($this->validate()) { + $user = User::findByUsername($this->username); + Yii::$app->user->login($user, $this->rememberMe ? 3600*24*30 : 0); + return true; + } else { + return false; + } + } +} diff --git a/apps/advanced/common/models/User.php b/apps/advanced/common/models/User.php new file mode 100644 index 0000000..62baf48 --- /dev/null +++ b/apps/advanced/common/models/User.php @@ -0,0 +1,144 @@ +<?php +namespace common\models; + +use yii\db\ActiveRecord; +use yii\helpers\Security; +use yii\web\IdentityInterface; + +/** + * Class User + * @package common\models + * + * @property integer $id + * @property string $username + * @property string $password_hash + * @property string $password_reset_token + * @property string $email + * @property string $auth_key + * @property integer $role + * @property integer $status + * @property integer $create_time + * @property integer $update_time + */ +class User extends ActiveRecord implements IdentityInterface +{ + /** + * @var string the raw password. Used to collect password input and isn't saved in database + */ + public $password; + + const STATUS_DELETED = 0; + const STATUS_ACTIVE = 10; + + const ROLE_USER = 10; + + public function behaviors() + { + return array( + 'timestamp' => array( + 'class' => 'yii\behaviors\AutoTimestamp', + 'attributes' => array( + ActiveRecord::EVENT_BEFORE_INSERT => array('create_time', 'update_time'), + ActiveRecord::EVENT_BEFORE_UPDATE => 'update_time', + ), + ), + ); + } + + /** + * Finds an identity by the given ID. + * + * @param string|integer $id the ID to be looked for + * @return IdentityInterface|null the identity object that matches the given ID. + */ + public static function findIdentity($id) + { + return static::find($id); + } + + /** + * Finds user by username + * + * @param string $username + * @return null|User + */ + public static function findByUsername($username) + { + return static::find(array('username' => $username, 'status' => static::STATUS_ACTIVE)); + } + + /** + * @return int|string current user ID + */ + public function getId() + { + return $this->id; + } + + /** + * @return string current user auth key + */ + public function getAuthKey() + { + return $this->auth_key; + } + + /** + * @param string $authKey + * @return boolean if auth key is valid for current user + */ + public function validateAuthKey($authKey) + { + return $this->getAuthKey() === $authKey; + } + + /** + * @param string $password password to validate + * @return bool if password provided is valid for current user + */ + public function validatePassword($password) + { + return Security::validatePassword($password, $this->password_hash); + } + + public function rules() + { + return array( + array('username', 'filter', 'filter' => 'trim'), + array('username', 'required'), + array('username', 'string', 'min' => 2, 'max' => 255), + + array('email', 'filter', 'filter' => 'trim'), + array('email', 'required'), + array('email', 'email'), + array('email', 'unique', 'message' => 'This email address has already been taken.', 'on' => 'signup'), + array('email', 'exist', 'message' => 'There is no user with such email.', 'on' => 'requestPasswordResetToken'), + + array('password', 'required'), + array('password', 'string', 'min' => 6), + ); + } + + public function scenarios() + { + return array( + 'signup' => array('username', 'email', 'password'), + 'resetPassword' => array('password'), + 'requestPasswordResetToken' => array('email'), + ); + } + + public function beforeSave($insert) + { + if (parent::beforeSave($insert)) { + if (($this->isNewRecord || $this->getScenario() === 'resetPassword') && !empty($this->password)) { + $this->password_hash = Security::generatePasswordHash($this->password); + } + if ($this->isNewRecord) { + $this->auth_key = Security::generateRandomKey(); + } + return true; + } + return false; + } +} diff --git a/apps/advanced/composer.json b/apps/advanced/composer.json new file mode 100644 index 0000000..4c0fced --- /dev/null +++ b/apps/advanced/composer.json @@ -0,0 +1,39 @@ +{ + "name": "yiisoft/yii2-app-advanced", + "description": "Yii 2 Advanced Application Template", + "keywords": ["yii", "framework", "advanced", "application template"], + "homepage": "http://www.yiiframework.com/", + "type": "project", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "minimum-stability": "dev", + "require": { + "php": ">=5.3.0", + "yiisoft/yii2": "dev-master", + "yiisoft/yii2-composer": "dev-master" + }, + "scripts": { + "post-create-project-cmd": [ + "yii\\composer\\InstallHandler::setPermissions", + "./init" + ] + }, + "extra": { + "yii-install-writable": [ + "backend/runtime", + "backend/web/assets", + + "console/runtime", + "console/migrations", + + "frontend/runtime", + "frontend/web/assets" + ] + } +} diff --git a/apps/advanced/console/config/.gitignore b/apps/advanced/console/config/.gitignore new file mode 100644 index 0000000..20da318 --- /dev/null +++ b/apps/advanced/console/config/.gitignore @@ -0,0 +1,2 @@ +main-local.php +params-local.php \ No newline at end of file diff --git a/apps/advanced/console/config/main.php b/apps/advanced/console/config/main.php new file mode 100644 index 0000000..7a223c3 --- /dev/null +++ b/apps/advanced/console/config/main.php @@ -0,0 +1,31 @@ +<?php +$rootDir = __DIR__ . '/../..'; + +$params = array_merge( + require($rootDir . '/common/config/params.php'), + require($rootDir . '/common/config/params-local.php'), + require(__DIR__ . '/params.php'), + require(__DIR__ . '/params-local.php') +); + +return array( + 'id' => 'app-console', + 'basePath' => dirname(__DIR__), + 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', + 'controllerNamespace' => 'console\controllers', + 'modules' => array( + ), + 'components' => array( + 'db' => $params['components.db'], + 'cache' => $params['components.cache'], + 'log' => array( + 'targets' => array( + array( + 'class' => 'yii\log\FileTarget', + 'levels' => array('error', 'warning'), + ), + ), + ), + ), + 'params' => $params, +); diff --git a/apps/advanced/console/config/params.php b/apps/advanced/console/config/params.php new file mode 100644 index 0000000..1643a70 --- /dev/null +++ b/apps/advanced/console/config/params.php @@ -0,0 +1,4 @@ +<?php +return array( + 'adminEmail' => 'admin@example.com', +); diff --git a/docs/guide/dao.md b/apps/advanced/console/controllers/.gitkeep similarity index 100% rename from docs/guide/dao.md rename to apps/advanced/console/controllers/.gitkeep diff --git a/apps/advanced/console/migrations/m130524_201442_init.php b/apps/advanced/console/migrations/m130524_201442_init.php new file mode 100644 index 0000000..e7b9e84 --- /dev/null +++ b/apps/advanced/console/migrations/m130524_201442_init.php @@ -0,0 +1,31 @@ +<?php + +use yii\db\Schema; + +class m130524_201442_init extends \yii\db\Migration +{ + public function up() + { + // MySQL-specific table options. Adjust if you plan working with another DBMS + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB'; + + $this->createTable('tbl_user', array( + 'id' => Schema::TYPE_PK, + 'username' => Schema::TYPE_STRING.' NOT NULL', + 'auth_key' => Schema::TYPE_STRING.'(32) NOT NULL', + 'password_hash' => Schema::TYPE_STRING.' NOT NULL', + 'password_reset_token' => Schema::TYPE_STRING.'(32)', + 'email' => Schema::TYPE_STRING.' NOT NULL', + 'role' => 'tinyint NOT NULL DEFAULT 10', + + 'status' => 'tinyint NOT NULL DEFAULT 10', + 'create_time' => Schema::TYPE_INTEGER.' NOT NULL', + 'update_time' => Schema::TYPE_INTEGER.' NOT NULL', + ), $tableOptions); + } + + public function down() + { + $this->dropTable('tbl_user'); + } +} diff --git a/yii/console/runtime/.gitignore b/apps/advanced/console/models/.gitkeep similarity index 100% rename from yii/console/runtime/.gitignore rename to apps/advanced/console/models/.gitkeep index f59ec20..72e8ffc 100644 --- a/yii/console/runtime/.gitignore +++ b/apps/advanced/console/models/.gitkeep @@ -1 +1 @@ -* \ No newline at end of file +* diff --git a/apps/advanced/console/runtime/.gitignore b/apps/advanced/console/runtime/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/apps/advanced/console/runtime/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/apps/advanced/environments/dev/backend/config/main-local.php b/apps/advanced/environments/dev/backend/config/main-local.php new file mode 100644 index 0000000..2689ed1 --- /dev/null +++ b/apps/advanced/environments/dev/backend/config/main-local.php @@ -0,0 +1,11 @@ +<?php +return array( + 'preload' => array( + //'debug', + ), + 'modules' => array( +// 'debug' => array( +// 'class' => 'yii\debug\Module', +// ), + ), +); diff --git a/apps/advanced/environments/dev/backend/config/params-local.php b/apps/advanced/environments/dev/backend/config/params-local.php new file mode 100644 index 0000000..5b61b0e --- /dev/null +++ b/apps/advanced/environments/dev/backend/config/params-local.php @@ -0,0 +1,3 @@ +<?php +return array( +); diff --git a/apps/advanced/environments/dev/backend/web/index.php b/apps/advanced/environments/dev/backend/web/index.php new file mode 100644 index 0000000..2113419 --- /dev/null +++ b/apps/advanced/environments/dev/backend/web/index.php @@ -0,0 +1,14 @@ +<?php +defined('YII_DEBUG') or define('YII_DEBUG', true); +defined('YII_ENV') or define('YII_ENV', 'dev'); + +require(__DIR__ . '/../../vendor/autoload.php'); +require(__DIR__ . '/../../vendor/yiisoft/yii2/yii/Yii.php'); + +$config = yii\helpers\ArrayHelper::merge( + require(__DIR__ . '/../config/main.php'), + require(__DIR__ . '/../config/main-local.php') +); + +$application = new yii\web\Application($config); +$application->run(); diff --git a/apps/advanced/environments/dev/common/config/params-local.php b/apps/advanced/environments/dev/common/config/params-local.php new file mode 100644 index 0000000..5b61b0e --- /dev/null +++ b/apps/advanced/environments/dev/common/config/params-local.php @@ -0,0 +1,3 @@ +<?php +return array( +); diff --git a/apps/advanced/environments/dev/console/config/main-local.php b/apps/advanced/environments/dev/console/config/main-local.php new file mode 100644 index 0000000..5b61b0e --- /dev/null +++ b/apps/advanced/environments/dev/console/config/main-local.php @@ -0,0 +1,3 @@ +<?php +return array( +); diff --git a/apps/advanced/environments/dev/console/config/params-local.php b/apps/advanced/environments/dev/console/config/params-local.php new file mode 100644 index 0000000..5b61b0e --- /dev/null +++ b/apps/advanced/environments/dev/console/config/params-local.php @@ -0,0 +1,3 @@ +<?php +return array( +); diff --git a/apps/advanced/environments/dev/frontend/config/main-local.php b/apps/advanced/environments/dev/frontend/config/main-local.php new file mode 100644 index 0000000..35d10ed --- /dev/null +++ b/apps/advanced/environments/dev/frontend/config/main-local.php @@ -0,0 +1,11 @@ +<?php +return array( + 'preload' => array( + //'debug', + ), + 'modules' => array( +// 'debug' => array( +// 'class' => 'yii\debug\Module', +// ), + ), +); diff --git a/apps/advanced/environments/dev/frontend/config/params-local.php b/apps/advanced/environments/dev/frontend/config/params-local.php new file mode 100644 index 0000000..5b61b0e --- /dev/null +++ b/apps/advanced/environments/dev/frontend/config/params-local.php @@ -0,0 +1,3 @@ +<?php +return array( +); diff --git a/apps/advanced/environments/dev/frontend/web/index.php b/apps/advanced/environments/dev/frontend/web/index.php new file mode 100644 index 0000000..9a7cbae --- /dev/null +++ b/apps/advanced/environments/dev/frontend/web/index.php @@ -0,0 +1,15 @@ +<?php +defined('YII_DEBUG') or define('YII_DEBUG', true); +defined('YII_ENV') or define('YII_ENV', 'dev'); + +require(__DIR__ . '/../../vendor/autoload.php'); +require(__DIR__ . '/../../vendor/yiisoft/yii2/yii/Yii.php'); +Yii::importNamespaces(require(__DIR__ . '/../../vendor/composer/autoload_namespaces.php')); + +$config = yii\helpers\ArrayHelper::merge( + require(__DIR__ . '/../config/main.php'), + require(__DIR__ . '/../config/main-local.php') +); + +$application = new yii\web\Application($config); +$application->run(); diff --git a/apps/advanced/environments/dev/yii b/apps/advanced/environments/dev/yii new file mode 100644 index 0000000..e7d5f6c --- /dev/null +++ b/apps/advanced/environments/dev/yii @@ -0,0 +1,28 @@ +#!/usr/bin/env php +<?php +/** + * Yii console bootstrap file. + * + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +defined('YII_DEBUG') or define('YII_DEBUG', true); +defined('YII_ENV') or define('YII_ENV', 'dev'); + +// fcgi doesn't have STDIN defined by default +defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); + +require(__DIR__ . '/vendor/autoload.php'); +require(__DIR__ . '/vendor/yiisoft/yii2/yii/Yii.php'); +Yii::importNamespaces(require(__DIR__ . '/vendor/composer/autoload_namespaces.php')); + +$config = yii\helpers\ArrayHelper::merge( + require(__DIR__ . '/console/config/main.php'), + require(__DIR__ . '/console/config/main-local.php') +); + +$application = new yii\console\Application($config); +$exitCode = $application->run(); +exit($exitCode); diff --git a/apps/advanced/environments/index.php b/apps/advanced/environments/index.php new file mode 100644 index 0000000..78d221a --- /dev/null +++ b/apps/advanced/environments/index.php @@ -0,0 +1,38 @@ +<?php +/** + * The manifest of files that are local to specific environment. + * This file returns a list of environments that the application + * may be installed under. The returned data must be in the following + * format: + * + * ```php + * return array( + * 'environment name' => array( + * 'path' => 'directory storing the local files', + * 'writable' => array( + * // list of directories that should be set writable + * ), + * ), + * ); + * ``` + */ +return array( + 'Development' => array( + 'path' => 'dev', + 'writable' => array( + // handled by composer.json already + ), + 'executable' => array( + 'yii', + ), + ), + 'Production' => array( + 'path' => 'prod', + 'writable' => array( + // handled by composer.json already + ), + 'executable' => array( + 'yii', + ), + ), +); diff --git a/apps/advanced/environments/prod/backend/config/main-local.php b/apps/advanced/environments/prod/backend/config/main-local.php new file mode 100644 index 0000000..5b61b0e --- /dev/null +++ b/apps/advanced/environments/prod/backend/config/main-local.php @@ -0,0 +1,3 @@ +<?php +return array( +); diff --git a/apps/advanced/environments/prod/backend/config/params-local.php b/apps/advanced/environments/prod/backend/config/params-local.php new file mode 100644 index 0000000..5b61b0e --- /dev/null +++ b/apps/advanced/environments/prod/backend/config/params-local.php @@ -0,0 +1,3 @@ +<?php +return array( +); diff --git a/apps/advanced/environments/prod/backend/web/index.php b/apps/advanced/environments/prod/backend/web/index.php new file mode 100644 index 0000000..fc62a78 --- /dev/null +++ b/apps/advanced/environments/prod/backend/web/index.php @@ -0,0 +1,14 @@ +<?php +defined('YII_DEBUG') or define('YII_DEBUG', false); +defined('YII_ENV') or define('YII_ENV', 'prod'); + +require(__DIR__ . '/../../vendor/autoload.php'); +require(__DIR__ . '/../../vendor/yiisoft/yii2/yii/Yii.php'); + +$config = yii\helpers\ArrayHelper::merge( + require(__DIR__ . '/../config/main.php'), + require(__DIR__ . '/../config/main-local.php') +); + +$application = new yii\web\Application($config); +$application->run(); diff --git a/apps/advanced/environments/prod/common/config/params-local.php b/apps/advanced/environments/prod/common/config/params-local.php new file mode 100644 index 0000000..5b61b0e --- /dev/null +++ b/apps/advanced/environments/prod/common/config/params-local.php @@ -0,0 +1,3 @@ +<?php +return array( +); diff --git a/apps/advanced/environments/prod/console/config/main-local.php b/apps/advanced/environments/prod/console/config/main-local.php new file mode 100644 index 0000000..5b61b0e --- /dev/null +++ b/apps/advanced/environments/prod/console/config/main-local.php @@ -0,0 +1,3 @@ +<?php +return array( +); diff --git a/apps/advanced/environments/prod/console/config/params-local.php b/apps/advanced/environments/prod/console/config/params-local.php new file mode 100644 index 0000000..5b61b0e --- /dev/null +++ b/apps/advanced/environments/prod/console/config/params-local.php @@ -0,0 +1,3 @@ +<?php +return array( +); diff --git a/apps/advanced/environments/prod/frontend/config/main-local.php b/apps/advanced/environments/prod/frontend/config/main-local.php new file mode 100644 index 0000000..5b61b0e --- /dev/null +++ b/apps/advanced/environments/prod/frontend/config/main-local.php @@ -0,0 +1,3 @@ +<?php +return array( +); diff --git a/apps/advanced/environments/prod/frontend/config/params-local.php b/apps/advanced/environments/prod/frontend/config/params-local.php new file mode 100644 index 0000000..5b61b0e --- /dev/null +++ b/apps/advanced/environments/prod/frontend/config/params-local.php @@ -0,0 +1,3 @@ +<?php +return array( +); diff --git a/apps/advanced/environments/prod/frontend/web/index.php b/apps/advanced/environments/prod/frontend/web/index.php new file mode 100644 index 0000000..cbd096b --- /dev/null +++ b/apps/advanced/environments/prod/frontend/web/index.php @@ -0,0 +1,15 @@ +<?php +defined('YII_DEBUG') or define('YII_DEBUG', false); +defined('YII_ENV') or define('YII_ENV', 'prod'); + +require(__DIR__ . '/../../vendor/autoload.php'); +require(__DIR__ . '/../../vendor/yiisoft/yii2/yii/Yii.php'); +Yii::importNamespaces(require(__DIR__ . '/../../vendor/composer/autoload_namespaces.php')); + +$config = yii\helpers\ArrayHelper::merge( + require(__DIR__ . '/../config/main.php'), + require(__DIR__ . '/../config/main-local.php') +); + +$application = new yii\web\Application($config); +$application->run(); diff --git a/apps/advanced/environments/prod/yii b/apps/advanced/environments/prod/yii new file mode 100644 index 0000000..9e13eb2 --- /dev/null +++ b/apps/advanced/environments/prod/yii @@ -0,0 +1,28 @@ +#!/usr/bin/env php +<?php +/** + * Yii console bootstrap file. + * + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +defined('YII_DEBUG') or define('YII_DEBUG', false); +defined('YII_ENV') or define('YII_ENV', 'prod'); + +// fcgi doesn't have STDIN defined by default +defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); + +require(__DIR__ . '/vendor/autoload.php'); +require(__DIR__ . '/vendor/yiisoft/yii2/yii/Yii.php'); +Yii::importNamespaces(require(__DIR__ . '/vendor/composer/autoload_namespaces.php')); + +$config = yii\helpers\ArrayHelper::merge( + require(__DIR__ . '/console/config/main.php'), + require(__DIR__ . '/console/config/main-local.php') +); + +$application = new yii\console\Application($config); +$exitCode = $application->run(); +exit($exitCode); diff --git a/apps/advanced/frontend/assets/.gitkeep b/apps/advanced/frontend/assets/.gitkeep new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/apps/advanced/frontend/assets/.gitkeep @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/apps/advanced/frontend/config/.gitignore b/apps/advanced/frontend/config/.gitignore new file mode 100644 index 0000000..20da318 --- /dev/null +++ b/apps/advanced/frontend/config/.gitignore @@ -0,0 +1,2 @@ +main-local.php +params-local.php \ No newline at end of file diff --git a/apps/advanced/frontend/config/AppAsset.php b/apps/advanced/frontend/config/AppAsset.php new file mode 100644 index 0000000..4df653c --- /dev/null +++ b/apps/advanced/frontend/config/AppAsset.php @@ -0,0 +1,29 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace frontend\config; + +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class AppAsset extends AssetBundle +{ + public $basePath = '@webroot'; + public $baseUrl = '@web'; + public $css = array( + 'css/site.css', + ); + public $js = array( + ); + public $depends = array( + 'yii\web\YiiAsset', + 'yii\bootstrap\BootstrapAsset', + ); +} diff --git a/apps/advanced/frontend/config/main.php b/apps/advanced/frontend/config/main.php new file mode 100644 index 0000000..975a3b4 --- /dev/null +++ b/apps/advanced/frontend/config/main.php @@ -0,0 +1,42 @@ +<?php +$rootDir = __DIR__ . '/../..'; + +$params = array_merge( + require($rootDir . '/common/config/params.php'), + require($rootDir . '/common/config/params-local.php'), + require(__DIR__ . '/params.php'), + require(__DIR__ . '/params-local.php') +); + +return array( + 'id' => 'app-frontend', + 'basePath' => dirname(__DIR__), + 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', + 'controllerNamespace' => 'frontend\controllers', + 'modules' => array( + 'gii' => 'yii\gii\Module' + ), + 'components' => array( + 'request' => array( + 'enableCsrfValidation' => true, + ), + 'db' => $params['components.db'], + 'cache' => $params['components.cache'], + 'user' => array( + 'identityClass' => 'common\models\User', + ), + 'log' => array( + 'traceLevel' => YII_DEBUG ? 3 : 0, + 'targets' => array( + array( + 'class' => 'yii\log\FileTarget', + 'levels' => array('error', 'warning'), + ), + ), + ), + 'errorHandler' => array( + 'errorAction' => 'site/error', + ), + ), + 'params' => $params, +); diff --git a/apps/advanced/frontend/config/params.php b/apps/advanced/frontend/config/params.php new file mode 100644 index 0000000..1643a70 --- /dev/null +++ b/apps/advanced/frontend/config/params.php @@ -0,0 +1,4 @@ +<?php +return array( + 'adminEmail' => 'admin@example.com', +); diff --git a/apps/advanced/frontend/controllers/SiteController.php b/apps/advanced/frontend/controllers/SiteController.php new file mode 100644 index 0000000..a9413de --- /dev/null +++ b/apps/advanced/frontend/controllers/SiteController.php @@ -0,0 +1,172 @@ +<?php + +namespace frontend\controllers; + +use Yii; +use yii\web\Controller; +use common\models\LoginForm; +use frontend\models\ContactForm; +use common\models\User; +use yii\web\HttpException; +use yii\helpers\Security; + +class SiteController extends Controller +{ + public function behaviors() + { + return array( + 'access' => array( + 'class' => \yii\web\AccessControl::className(), + 'only' => array('login', 'logout', 'signup'), + 'rules' => array( + array( + 'actions' => array('login', 'signup'), + 'allow' => true, + 'roles' => array('?'), + ), + array( + 'actions' => array('logout'), + 'allow' => true, + 'roles' => array('@'), + ), + ), + ), + ); + } + + public function actions() + { + return array( + 'error' => array( + 'class' => 'yii\web\ErrorAction', + ), + 'captcha' => array( + 'class' => 'yii\captcha\CaptchaAction', + 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, + ), + ); + } + + public function actionIndex() + { + return $this->render('index'); + } + + public function actionLogin() + { + $model = new LoginForm(); + if ($model->load($_POST) && $model->login()) { + return $this->goHome(); + } else { + return $this->render('login', array( + 'model' => $model, + )); + } + } + + public function actionLogout() + { + Yii::$app->user->logout(); + return $this->goHome(); + } + + public function actionContact() + { + $model = new ContactForm; + if ($model->load($_POST) && $model->contact(Yii::$app->params['adminEmail'])) { + Yii::$app->session->setFlash('success', 'Thank you for contacting us. We will respond to you as soon as possible.'); + return $this->refresh(); + } else { + return $this->render('contact', array( + 'model' => $model, + )); + } + } + + public function actionAbout() + { + return $this->render('about'); + } + + public function actionSignup() + { + $model = new User(); + $model->setScenario('signup'); + if ($model->load($_POST) && $model->save()) { + if (Yii::$app->getUser()->login($model)) { + return $this->goHome(); + } + } + + return $this->render('signup', array( + 'model' => $model, + )); + } + + public function actionRequestPasswordReset() + { + $model = new User(); + $model->scenario = 'requestPasswordResetToken'; + if ($model->load($_POST) && $model->validate()) { + if ($this->sendPasswordResetEmail($model->email)) { + Yii::$app->getSession()->setFlash('success', 'Check your email for further instructions.'); + return $this->goHome(); + } else { + Yii::$app->getSession()->setFlash('error', 'There was an error sending email.'); + } + } + return $this->render('requestPasswordResetToken', array( + 'model' => $model, + )); + } + + public function actionResetPassword($token) + { + $model = User::find(array( + 'password_reset_token' => $token, + 'status' => User::STATUS_ACTIVE, + )); + + if (!$model) { + throw new HttpException(400, 'Wrong password reset token.'); + } + + $model->scenario = 'resetPassword'; + if ($model->load($_POST) && $model->save()) { + Yii::$app->getSession()->setFlash('success', 'New password was saved.'); + return $this->goHome(); + } + + return $this->render('resetPassword', array( + 'model' => $model, + )); + } + + private function sendPasswordResetEmail($email) + { + $user = User::find(array( + 'status' => User::STATUS_ACTIVE, + 'email' => $email, + )); + + if (!$user) { + return false; + } + + $user->password_reset_token = Security::generateRandomKey(); + if ($user->save(false)) { + $fromEmail = \Yii::$app->params['supportEmail']; + $name = '=?UTF-8?B?' . base64_encode(\Yii::$app->name . ' robot') . '?='; + $subject = '=?UTF-8?B?' . base64_encode('Password reset for ' . \Yii::$app->name) . '?='; + $body = $this->renderPartial('/emails/passwordResetToken', array( + 'user' => $user, + )); + $headers = "From: $name <{$fromEmail}>\r\n" . + "MIME-Version: 1.0\r\n" . + "Content-type: text/plain; charset=UTF-8"; + return mail($email, $subject, $body, $headers); + } + + return false; + } +} diff --git a/apps/advanced/frontend/models/ContactForm.php b/apps/advanced/frontend/models/ContactForm.php new file mode 100644 index 0000000..b3d8682 --- /dev/null +++ b/apps/advanced/frontend/models/ContactForm.php @@ -0,0 +1,63 @@ +<?php + +namespace frontend\models; + +use yii\base\Model; + +/** + * ContactForm is the model behind the contact form. + */ +class ContactForm extends Model +{ + public $name; + public $email; + public $subject; + public $body; + public $verifyCode; + + /** + * @return array the validation rules. + */ + public function rules() + { + return array( + // name, email, subject and body are required + array('name, email, subject, body', 'required'), + // email has to be a valid email address + array('email', 'email'), + // verifyCode needs to be entered correctly + array('verifyCode', 'captcha'), + ); + } + + /** + * @return array customized attribute labels + */ + public function attributeLabels() + { + return array( + 'verifyCode' => 'Verification Code', + ); + } + + /** + * Sends an email to the specified email address using the information collected by this model. + * @param string $email the target email address + * @return boolean whether the model passes validation + */ + public function contact($email) + { + if ($this->validate()) { + $name = '=?UTF-8?B?' . base64_encode($this->name) . '?='; + $subject = '=?UTF-8?B?' . base64_encode($this->subject) . '?='; + $headers = "From: $name <{$this->email}>\r\n" . + "Reply-To: {$this->email}\r\n" . + "MIME-Version: 1.0\r\n" . + "Content-type: text/plain; charset=UTF-8"; + mail($email, $subject, $this->body, $headers); + return true; + } else { + return false; + } + } +} diff --git a/apps/advanced/frontend/runtime/.gitignore b/apps/advanced/frontend/runtime/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/apps/advanced/frontend/runtime/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/apps/advanced/frontend/views/emails/passwordResetToken.php b/apps/advanced/frontend/views/emails/passwordResetToken.php new file mode 100644 index 0000000..1e7a855 --- /dev/null +++ b/apps/advanced/frontend/views/emails/passwordResetToken.php @@ -0,0 +1,16 @@ +<?php +use yii\helpers\Html; + +/** + * @var yii\base\View $this + * @var common\models\User $user; + */ + +$resetLink = Yii::$app->urlManager->createAbsoluteUrl('site/reset-password', array('token' => $user->password_reset_token)); +?> + +Hello <?php echo Html::encode($user->username)?>, + +Follow the link below to reset your password: + +<?php echo Html::a(Html::encode($resetLink), $resetLink)?> diff --git a/apps/advanced/frontend/views/layouts/main.php b/apps/advanced/frontend/views/layouts/main.php new file mode 100644 index 0000000..0165ba0 --- /dev/null +++ b/apps/advanced/frontend/views/layouts/main.php @@ -0,0 +1,69 @@ +<?php +use frontend\config\AppAsset; +use yii\helpers\Html; +use yii\bootstrap\Nav; +use yii\bootstrap\NavBar; +use yii\widgets\Breadcrumbs; +use frontend\widgets\Alert; + +/** + * @var $this \yii\base\View + * @var $content string + */ +AppAsset::register($this); +?> +<?php $this->beginPage(); ?> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="<?php echo Yii::$app->charset; ?>"/> + <title><?php echo Html::encode($this->title); ?></title> + <?php $this->head(); ?> +</head> +<body> + <?php $this->beginBody(); ?> + <?php + NavBar::begin(array( + 'brandLabel' => 'My Company', + 'brandUrl' => Yii::$app->homeUrl, + 'options' => array( + 'class' => 'navbar-inverse navbar-fixed-top', + ), + )); + $menuItems = array( + array('label' => 'Home', 'url' => array('/site/index')), + array('label' => 'About', 'url' => array('/site/about')), + array('label' => 'Contact', 'url' => array('/site/contact')), + ); + if (Yii::$app->user->isGuest) { + $menuItems[] = array('label' => 'Signup', 'url' => array('/site/signup')); + $menuItems[] = array('label' => 'Login', 'url' => array('/site/login')); + } else { + $menuItems[] = array('label' => 'Logout (' . Yii::$app->user->identity->username .')' , 'url' => array('/site/logout')); + } + echo Nav::widget(array( + 'options' => array('class' => 'navbar-nav pull-right'), + 'items' => $menuItems, + )); + NavBar::end(); + ?> + + <div class="container"> + <?php echo Breadcrumbs::widget(array( + 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : array(), + )); ?> + <?php echo Alert::widget()?> + <?php echo $content; ?> + </div> + + <footer class="footer"> + <div class="container"> + <p class="pull-left">© My Company <?php echo date('Y'); ?></p> + <p class="pull-right"><?php echo Yii::powered(); ?></p> + </div> + </footer> + + <?php $this->endBody(); ?> +</body> +</html> +<?php $this->endPage(); ?> diff --git a/apps/advanced/frontend/views/site/about.php b/apps/advanced/frontend/views/site/about.php new file mode 100644 index 0000000..a372e69 --- /dev/null +++ b/apps/advanced/frontend/views/site/about.php @@ -0,0 +1,16 @@ +<?php +use yii\helpers\Html; + +/** + * @var yii\base\View $this + */ +$this->title = 'About'; +$this->params['breadcrumbs'][] = $this->title; +?> +<div class="site-about"> + <h1><?php echo Html::encode($this->title); ?></h1> + + <p>This is the About page. You may modify the following file to customize its content:</p> + + <code><?php echo __FILE__; ?></code> +</div> diff --git a/apps/advanced/frontend/views/site/contact.php b/apps/advanced/frontend/views/site/contact.php new file mode 100644 index 0000000..851deda --- /dev/null +++ b/apps/advanced/frontend/views/site/contact.php @@ -0,0 +1,39 @@ +<?php +use yii\helpers\Html; +use yii\widgets\ActiveForm; +use yii\captcha\Captcha; + +/** + * @var yii\base\View $this + * @var yii\widgets\ActiveForm $form + * @var app\models\ContactForm $model + */ +$this->title = 'Contact'; +$this->params['breadcrumbs'][] = $this->title; +?> +<div class="site-contact"> + <h1><?php echo Html::encode($this->title); ?></h1> + + <p> + If you have business inquiries or other questions, please fill out the following form to contact us. Thank you. + </p> + + <div class="row"> + <div class="col-lg-5"> + <?php $form = ActiveForm::begin(array('id' => 'contact-form')); ?> + <?php echo $form->field($model, 'name'); ?> + <?php echo $form->field($model, 'email'); ?> + <?php echo $form->field($model, 'subject'); ?> + <?php echo $form->field($model, 'body')->textArea(array('rows' => 6)); ?> + <?php echo $form->field($model, 'verifyCode')->widget(Captcha::className(), array( + 'options' => array('class' => 'form-control'), + 'template' => '<div class="row"><div class="col-lg-3">{image}</div><div class="col-lg-6">{input}</div></div>', + )); ?> + <div class="form-group"> + <?php echo Html::submitButton('Submit', array('class' => 'btn btn-primary')); ?> + </div> + <?php ActiveForm::end(); ?> + </div> + </div> + +</div> diff --git a/apps/advanced/frontend/views/site/error.php b/apps/advanced/frontend/views/site/error.php new file mode 100644 index 0000000..024e27d --- /dev/null +++ b/apps/advanced/frontend/views/site/error.php @@ -0,0 +1,29 @@ +<?php + +use yii\helpers\Html; + +/** + * @var yii\base\View $this + * @var string $name + * @var string $message + * @var Exception $exception + */ + +$this->title = $name; +?> +<div class="site-error"> + + <h1><?php echo Html::encode($this->title); ?></h1> + + <div class="alert alert-danger"> + <?php echo nl2br(Html::encode($message)); ?> + </div> + + <p> + The above error occurred while the Web server was processing your request. + </p> + <p> + Please contact us if you think this is a server error. Thank you. + </p> + +</div> diff --git a/apps/advanced/frontend/views/site/index.php b/apps/advanced/frontend/views/site/index.php new file mode 100644 index 0000000..f2e6d5e --- /dev/null +++ b/apps/advanced/frontend/views/site/index.php @@ -0,0 +1,53 @@ +<?php +/** + * @var yii\base\View $this + */ +$this->title = 'My Yii Application'; +?> +<div class="site-index"> + + <div class="jumbotron"> + <h1>Congratulations!</h1> + + <p class="lead">You have successfully created your Yii-powered application.</p> + + <p><a class="btn btn-lg btn-success" href="http://www.yiiframework.com">Get started with Yii</a></p> + </div> + + <div class="body-content"> + + <div class="row"> + <div class="col-lg-4"> + <h2>Heading</h2> + + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.</p> + + <p><a class="btn btn-default" href="http://www.yiiframework.com/doc/">Yii Documentation »</a></p> + </div> + <div class="col-lg-4"> + <h2>Heading</h2> + + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.</p> + + <p><a class="btn btn-default" href="http://www.yiiframework.com/forum/">Yii Forum »</a></p> + </div> + <div class="col-lg-4"> + <h2>Heading</h2> + + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.</p> + + <p><a class="btn btn-default" href="http://www.yiiframework.com/extensions/">Yii Extensions »</a></p> + </div> + </div> + + </div> +</div> diff --git a/apps/advanced/frontend/views/site/login.php b/apps/advanced/frontend/views/site/login.php new file mode 100644 index 0000000..5e7f6f6 --- /dev/null +++ b/apps/advanced/frontend/views/site/login.php @@ -0,0 +1,33 @@ +<?php +use yii\helpers\Html; +use yii\widgets\ActiveForm; + +/** + * @var yii\base\View $this + * @var yii\widgets\ActiveForm $form + * @var app\models\LoginForm $model + */ +$this->title = 'Login'; +$this->params['breadcrumbs'][] = $this->title; +?> +<div class="site-login"> + <h1><?php echo Html::encode($this->title); ?></h1> + + <p>Please fill out the following fields to login:</p> + + <div class="row"> + <div class="col-lg-5"> + <?php $form = ActiveForm::begin(array('id' => 'login-form')); ?> + <?php echo $form->field($model, 'username'); ?> + <?php echo $form->field($model, 'password')->passwordInput(); ?> + <?php echo $form->field($model, 'rememberMe')->checkbox(); ?> + <div style="color:#999;margin:1em 0"> + If you forgot your password you can <?php echo Html::a('reset it', array('site/request-password-reset'))?>. + </div> + <div class="form-group"> + <?php echo Html::submitButton('Login', array('class' => 'btn btn-primary')); ?> + </div> + <?php ActiveForm::end(); ?> + </div> + </div> +</div> diff --git a/apps/advanced/frontend/views/site/requestPasswordResetToken.php b/apps/advanced/frontend/views/site/requestPasswordResetToken.php new file mode 100644 index 0000000..c754948 --- /dev/null +++ b/apps/advanced/frontend/views/site/requestPasswordResetToken.php @@ -0,0 +1,28 @@ +<?php +use yii\helpers\Html; +use yii\widgets\ActiveForm; + +/** + * @var yii\base\View $this + * @var yii\widgets\ActiveForm $form + * @var common\models\User $model + */ +$this->title = 'Request password reset'; +$this->params['breadcrumbs'][] = $this->title; +?> +<div class="site-request-password-reset"> + <h1><?php echo Html::encode($this->title); ?></h1> + + <p>Please fill out your email. A link to reset password will be sent there.</p> + + <div class="row"> + <div class="col-lg-5"> + <?php $form = ActiveForm::begin(array('id' => 'request-password-reset-form')); ?> + <?php echo $form->field($model, 'email'); ?> + <div class="form-group"> + <?php echo Html::submitButton('Send', array('class' => 'btn btn-primary')); ?> + </div> + <?php ActiveForm::end(); ?> + </div> + </div> +</div> diff --git a/apps/advanced/frontend/views/site/resetPassword.php b/apps/advanced/frontend/views/site/resetPassword.php new file mode 100644 index 0000000..2c38028 --- /dev/null +++ b/apps/advanced/frontend/views/site/resetPassword.php @@ -0,0 +1,28 @@ +<?php +use yii\helpers\Html; +use yii\widgets\ActiveForm; + +/** + * @var yii\base\View $this + * @var yii\widgets\ActiveForm $form + * @var common\models\User $model + */ +$this->title = 'Reset password'; +$this->params['breadcrumbs'][] = $this->title; +?> +<div class="site-reset-password"> + <h1><?php echo Html::encode($this->title); ?></h1> + + <p>Please choose your new password:</p> + + <div class="row"> + <div class="col-lg-5"> + <?php $form = ActiveForm::begin(array('id' => 'reset-password-form')); ?> + <?php echo $form->field($model, 'password')->passwordInput(); ?> + <div class="form-group"> + <?php echo Html::submitButton('Save', array('class' => 'btn btn-primary')); ?> + </div> + <?php ActiveForm::end(); ?> + </div> + </div> +</div> diff --git a/apps/advanced/frontend/views/site/signup.php b/apps/advanced/frontend/views/site/signup.php new file mode 100644 index 0000000..92525bf --- /dev/null +++ b/apps/advanced/frontend/views/site/signup.php @@ -0,0 +1,30 @@ +<?php +use yii\helpers\Html; +use yii\widgets\ActiveForm; + +/** + * @var yii\base\View $this + * @var yii\widgets\ActiveForm $form + * @var common\models\User $model + */ +$this->title = 'Signup'; +$this->params['breadcrumbs'][] = $this->title; +?> +<div class="site-signup"> + <h1><?php echo Html::encode($this->title); ?></h1> + + <p>Please fill out the following fields to signup:</p> + + <div class="row"> + <div class="col-lg-5"> + <?php $form = ActiveForm::begin(array('id' => 'form-signup')); ?> + <?php echo $form->field($model, 'username'); ?> + <?php echo $form->field($model, 'email'); ?> + <?php echo $form->field($model, 'password')->passwordInput(); ?> + <div class="form-group"> + <?php echo Html::submitButton('Signup', array('class' => 'btn btn-primary')); ?> + </div> + <?php ActiveForm::end(); ?> + </div> + </div> +</div> diff --git a/apps/advanced/frontend/web/.gitignore b/apps/advanced/frontend/web/.gitignore new file mode 100644 index 0000000..148f2b0 --- /dev/null +++ b/apps/advanced/frontend/web/.gitignore @@ -0,0 +1 @@ +/index.php \ No newline at end of file diff --git a/apps/advanced/frontend/web/assets/.gitignore b/apps/advanced/frontend/web/assets/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/apps/advanced/frontend/web/assets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/apps/advanced/frontend/web/css/site.css b/apps/advanced/frontend/web/css/site.css new file mode 100644 index 0000000..e6db73c --- /dev/null +++ b/apps/advanced/frontend/web/css/site.css @@ -0,0 +1,19 @@ +body { + padding-top: 70px; +} + +.footer { + border-top: 1px solid #ddd; + margin-top: 30px; + padding-top: 15px; + padding-bottom: 30px; +} + +.jumbotron { + text-align: center; + background-color: transparent; +} +.jumbotron .btn { + font-size: 21px; + padding: 14px 24px; +} diff --git a/apps/advanced/frontend/widgets/Alert.php b/apps/advanced/frontend/widgets/Alert.php new file mode 100644 index 0000000..b68bfb0 --- /dev/null +++ b/apps/advanced/frontend/widgets/Alert.php @@ -0,0 +1,48 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace frontend\widgets; + +use yii\helpers\Html; + +/** + * Alert widget renders a message from session flash. You can set message as following: + * + * - \Yii::$app->getSession()->setFlash('error', 'This is the message'); + * - \Yii::$app->getSession()->setFlash('success', 'This is the message'); + * - \Yii::$app->getSession()->setFlash('info', 'This is the message'); + * + * @author Alexander Makarov <sam@rmcerative.ru> + */ +class Alert extends \yii\bootstrap\Alert +{ + private $_doNotRender = false; + public function init() + { + if ($this->body = \Yii::$app->getSession()->getFlash('error', null, true)) { + Html::addCssClass($this->options, 'alert-danger'); + } elseif ($this->body = \Yii::$app->getSession()->getFlash('success', null, true)) { + Html::addCssClass($this->options, 'alert-success'); + } elseif ($this->body = \Yii::$app->getSession()->getFlash('info', null, true)) { + Html::addCssClass($this->options, 'alert-info'); + } elseif ($this->body = \Yii::$app->getSession()->getFlash('warning', null, true)) { + Html::addCssClass($this->options, 'alert-warning'); + } else { + $this->_doNotRender = true; + return; + } + + parent::init(); + } + + public function run() + { + if (!$this->_doNotRender) { + parent::run(); + } + } +} diff --git a/apps/advanced/init b/apps/advanced/init new file mode 100755 index 0000000..3a8f6a6 --- /dev/null +++ b/apps/advanced/init @@ -0,0 +1,154 @@ +#!/usr/bin/env php +<?php +$params = getParams(); +$root = str_replace('\\', '/', __DIR__); +$envs = require("$root/environments/index.php"); +$envNames = array_keys($envs); + +echo "Yii Application Initialization Tool v1.0\n\n"; + +$envName = null; +if (empty($params['env'])) { + echo "Which environment do you want the application to be initialized in?\n\n"; + foreach ($envNames as $i => $name) { + echo " [$i] $name\n"; + } + echo "\n Your choice [0-" . (count($envs) - 1) . ', or "q" to quit] '; + $answer = trim(fgets(STDIN)); + + if (!ctype_digit($answer) || !in_array($answer, range(0, count($envs) - 1))) { + echo "\n Quit initialization.\n"; + exit(1); + } + + if(isset($envNames[$answer])) { + $envName = $envNames[$answer]; + } +} +else { + $envName = $params['env']; +} + +if (!in_array($envName, $envNames)) { + $envsList = implode(', ', $envNames); + echo "\n $envName is not a valid environment. Try one of the following: $envsList. \n"; + exit(2); +} + +$env = $envs[$envName]; + +if (empty($params['env'])) { + echo "\n Initialize the application under '{$envNames[$answer]}' environment? [yes|no] "; + $answer = trim(fgets(STDIN)); + if (strncasecmp($answer, 'y', 1)) { + echo "\n Quit initialization.\n"; + exit(1); + } +} + +echo "\n Start initialization ...\n\n"; +$files = getFileList("$root/environments/{$env['path']}"); +$all = false; +foreach ($files as $file) { + if (!copyFile($root, "environments/{$env['path']}/$file", $file, $all)) { + break; + } +} + +if (isset($env['writable'])) { + foreach ($env['writable'] as $writable) { + echo " chmod 0777 $writable\n"; + @chmod("$root/$writable", 0777); + } +} + +if (isset($env['executable'])) { + foreach ($env['executable'] as $executable) { + echo " chmod 0755 $executable\n"; + @chmod("$root/$executable", 0755); + } +} + +echo "\n ... initialization completed.\n\n"; + +function getFileList($root, $basePath = '') +{ + $files = array(); + $handle = opendir($root); + while (($path = readdir($handle)) !== false) { + if ($path === '.svn' || $path === '.' || $path === '..') { + continue; + } + $fullPath = "$root/$path"; + $relativePath = $basePath === '' ? $path : "$basePath/$path"; + if (is_dir($fullPath)) { + $files = array_merge($files, getFileList($fullPath, $relativePath)); + } else { + $files[] = $relativePath; + } + } + closedir($handle); + return $files; +} + +function copyFile($root, $source, $target, &$all) +{ + if (!is_file($root . '/' . $source)) { + echo " skip $target ($source not exist)\n"; + return true; + } + if (is_file($root . '/' . $target)) { + if (file_get_contents($root . '/' . $source) === file_get_contents($root . '/' . $target)) { + echo " unchanged $target\n"; + return true; + } + if ($all) { + echo " overwrite $target\n"; + } else { + echo " exist $target\n"; + echo " ...overwrite? [Yes|No|All|Quit] "; + $answer = trim(fgets(STDIN)); + if (!strncasecmp($answer, 'q', 1)) { + return false; + } else { + if (!strncasecmp($answer, 'y', 1)) { + echo " overwrite $target\n"; + } else { + if (!strncasecmp($answer, 'a', 1)) { + echo " overwrite $target\n"; + $all = true; + } else { + echo " skip $target\n"; + return true; + } + } + } + } + file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source)); + return true; + } + echo " generate $target\n"; + @mkdir(dirname($root . '/' . $target), 0777, true); + file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source)); + return true; +} + +function getParams() +{ + $rawParams = array(); + if (isset($_SERVER['argv'])) { + $rawParams = $_SERVER['argv']; + array_shift($rawParams); + } + + $params = array(); + foreach ($rawParams as $param) { + if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) { + $name = $matches[1]; + $params[$name] = isset($matches[3]) ? $matches[3] : true; + } else { + $params[] = $param; + } + } + return $params; +} diff --git a/yii/yiic.bat b/apps/advanced/init.bat similarity index 80% rename from yii/yiic.bat rename to apps/advanced/init.bat index c63fc81..4fc52f7 100644 --- a/yii/yiic.bat +++ b/apps/advanced/init.bat @@ -1,9 +1,7 @@ @echo off rem ------------------------------------------------------------- -rem Yii command line script for Windows. -rem -rem This is the bootstrap script for running yiic on Windows. +rem Yii command line init script for Windows. rem rem @author Qiang Xue <qiang.xue@gmail.com> rem @link http://www.yiiframework.com/ @@ -17,6 +15,6 @@ set YII_PATH=%~dp0 if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe -"%PHP_COMMAND%" "%YII_PATH%yiic" %* +"%PHP_COMMAND%" "%YII_PATH%init" %* -@endlocal \ No newline at end of file +@endlocal diff --git a/apps/advanced/requirements.php b/apps/advanced/requirements.php new file mode 100644 index 0000000..c9e6493 --- /dev/null +++ b/apps/advanced/requirements.php @@ -0,0 +1,103 @@ +<?php +/** + * Application requirement checker script. + * + * In order to run this script use the following console command: + * php requirements.php + * + * In order to run this script from the web, you should copy it to the web root. + * If you are using Linux you can create a hard link instead, using the following command: + * ln requirements.php ../requirements.php + */ + +// you may need to adjust this path to the correct Yii framework path +$frameworkPath = dirname(__FILE__) . '/vendor/yiisoft/yii2/yii'; + +if (!is_dir($frameworkPath)) { + echo '<h1>Error</h1>'; + echo '<p><strong>The path to yii framework seems to be incorrect.</strong></p>'; + echo '<p>You need to install Yii framework via composer or adjust the framework path in file <abbr title="' . __FILE__ . '">' . basename(__FILE__) .'</abbr>.</p>'; + echo '<p>Please refer to the <abbr title="' . dirname(__FILE__) . '/README.md">README</abbr> on how to install Yii.</p>'; +} + +require_once($frameworkPath . '/requirements/YiiRequirementChecker.php'); +$requirementsChecker = new YiiRequirementChecker(); + +/** + * Adjust requirements according to your application specifics. + */ +$requirements = array( + // Database : + array( + 'name' => 'PDO extension', + 'mandatory' => true, + 'condition' => extension_loaded('pdo'), + 'by' => 'All <a href="http://www.yiiframework.com/doc/api/#system.db">DB-related classes</a>', + ), + array( + 'name' => 'PDO SQLite extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_sqlite'), + 'by' => 'All <a href="http://www.yiiframework.com/doc/api/#system.db">DB-related classes</a>', + 'memo' => 'Required for SQLite database.', + ), + array( + 'name' => 'PDO MySQL extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_mysql'), + 'by' => 'All <a href="http://www.yiiframework.com/doc/api/#system.db">DB-related classes</a>', + 'memo' => 'Required for MySQL database.', + ), + // Cache : + array( + 'name' => 'Memcache extension', + 'mandatory' => false, + 'condition' => extension_loaded('memcache') || extension_loaded('memcached'), + 'by' => '<a href="http://www.yiiframework.com/doc/api/CMemCache">CMemCache</a>', + 'memo' => extension_loaded('memcached') ? 'To use memcached set <a href="http://www.yiiframework.com/doc/api/CMemCache#useMemcached-detail">CMemCache::useMemcached</a> to <code>true</code>.' : '' + ), + array( + 'name' => 'APC extension', + 'mandatory' => false, + 'condition' => extension_loaded('apc') || extension_loaded('apc'), + 'by' => '<a href="http://www.yiiframework.com/doc/api/CApcCache">CApcCache</a>', + ), + // Additional PHP extensions : + array( + 'name' => 'Mcrypt extension', + 'mandatory' => false, + 'condition' => extension_loaded('mcrypt'), + 'by' => '<a href="http://www.yiiframework.com/doc/api/CSecurityManager">CSecurityManager</a>', + 'memo' => 'Required by encrypt and decrypt methods.' + ), + // PHP ini : + 'phpSafeMode' => array( + 'name' => 'PHP safe mode', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("safe_mode"), + 'by' => 'File uploading and console command execution', + 'memo' => '"safe_mode" should be disabled at php.ini', + ), + 'phpExposePhp' => array( + 'name' => 'Expose PHP', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"), + 'by' => 'Security reasons', + 'memo' => '"expose_php" should be disabled at php.ini', + ), + 'phpAllowUrlInclude' => array( + 'name' => 'PHP allow url include', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"), + 'by' => 'Security reasons', + 'memo' => '"allow_url_include" should be disabled at php.ini', + ), + 'phpSmtp' => array( + 'name' => 'PHP mail SMTP', + 'mandatory' => false, + 'condition' => strlen(ini_get('SMTP'))>0, + 'by' => 'Email sending', + 'memo' => 'PHP mail SMTP server required', + ), +); +$requirementsChecker->checkYii()->check($requirements)->render(); diff --git a/apps/advanced/vendor/.gitignore b/apps/advanced/vendor/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/apps/advanced/vendor/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/apps/advanced/yii.bat b/apps/advanced/yii.bat new file mode 100644 index 0000000..5e21e2e --- /dev/null +++ b/apps/advanced/yii.bat @@ -0,0 +1,20 @@ +@echo off + +rem ------------------------------------------------------------- +rem Yii command line bootstrap script for Windows. +rem +rem @author Qiang Xue <qiang.xue@gmail.com> +rem @link http://www.yiiframework.com/ +rem @copyright Copyright © 2012 Yii Software LLC +rem @license http://www.yiiframework.com/license/ +rem ------------------------------------------------------------- + +@setlocal + +set YII_PATH=%~dp0 + +if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe + +"%PHP_COMMAND%" "%YII_PATH%yii" %* + +@endlocal diff --git a/apps/basic/.gitignore b/apps/basic/.gitignore new file mode 100644 index 0000000..9dce4ff --- /dev/null +++ b/apps/basic/.gitignore @@ -0,0 +1 @@ +/composer.lock \ No newline at end of file diff --git a/apps/basic/LICENSE.md b/apps/basic/LICENSE.md new file mode 100644 index 0000000..6edcc4f --- /dev/null +++ b/apps/basic/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/apps/basic/README.md b/apps/basic/README.md new file mode 100644 index 0000000..aaa7fba --- /dev/null +++ b/apps/basic/README.md @@ -0,0 +1,79 @@ +Yii 2 Basic Application Template +================================ + +**NOTE** Yii 2 and the relevant applications and extensions are still under heavy +development. We may make significant changes without prior notices. Please do not +use them for production. Please consider using [Yii v1.1](https://github.com/yiisoft/yii) +if you have a project to be deployed for production soon. + + +Thank you for using Yii 2 Basic Application Template - an application template +that works out-of-box and can be easily customized to fit for your needs. + +Yii 2 Basic Application Template is best suitable for small Websites which mainly contain +a few informational pages. + + +DIRECTORY STRUCTURE +------------------- + + commands/ contains console commands (controllers) + config/ contains application configurations + controllers/ contains Web controller classes + models/ contains model classes + runtime/ contains files generated during runtime + vendor/ contains dependent 3rd-party packages + views/ contains view files for the Web application + web/ contains the entry script and Web resources + + + +REQUIREMENTS +------------ + +The minimum requirement by Yii is that your Web server supports PHP 5.3.?. + +In order for captcha to work you need either GD2 extension or ImageMagick PHP extension. + +INSTALLATION +------------ + +### Install via Composer + +If you do not have [Composer](http://getcomposer.org/), you may download it from +[http://getcomposer.org/](http://getcomposer.org/) or run the following command on Linux/Unix/MacOS: + +~~~ +curl -s http://getcomposer.org/installer | php +~~~ + +You can then install the Bootstrap Application using the following command: + +~~~ +php composer.phar create-project --stability=dev yiisoft/yii2-app-basic yii-basic +~~~ + +Now you should be able to access the application using the URL `http://localhost/yii-basic/web/`, +assuming `yii-basic` is directly under the document root of your Web server. + +Note that in order to install some dependencies you must have `php_openssl` extension enabled. + + +### Install from an Archive File + +This is not currently available. We will provide it when Yii 2 is formally released. + + +### Install from development repository + +If you've cloned the [Yii 2 framework main development repository](https://github.com/yiisoft/yii2) you +can bootstrap your application with: + +~~~ +cd yii2/apps/basic +php composer.phar create-project +~~~ + +*Note: If the above command fails with `[RuntimeException] Not enough arguments.` run +`php composer.phar self-update` to obtain an updated version of composer which supports creating projects +from local packages.* diff --git a/apps/basic/codeception.yml b/apps/basic/codeception.yml new file mode 100644 index 0000000..5b1f441 --- /dev/null +++ b/apps/basic/codeception.yml @@ -0,0 +1,18 @@ +paths: + tests: tests + log: tests/_log + data: tests/_data + helpers: tests/_helpers +settings: + bootstrap: _bootstrap.php + suite_class: \PHPUnit_Framework_TestSuite + colors: true + memory_limit: 1024M + log: true +modules: + config: + Db: + dsn: '' + user: '' + password: '' + dump: tests/_data/dump.sql diff --git a/apps/basic/commands/HelloController.php b/apps/basic/commands/HelloController.php new file mode 100644 index 0000000..29394cd --- /dev/null +++ b/apps/basic/commands/HelloController.php @@ -0,0 +1,30 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace app\commands; + +use yii\console\Controller; + +/** + * This command echoes what the first argument that you have entered. + * + * This command is provided as an example for you to learn how to create console commands. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class HelloController extends Controller +{ + /** + * This command echoes what you have entered as the message. + * @param string $message the message to be echoed. + */ + public function actionIndex($message = 'hello world') + { + echo $message."\n"; + } +} diff --git a/apps/basic/composer.json b/apps/basic/composer.json new file mode 100644 index 0000000..dd50b69 --- /dev/null +++ b/apps/basic/composer.json @@ -0,0 +1,35 @@ +{ + "name": "yiisoft/yii2-app-basic", + "description": "Yii 2 Basic Application Template", + "keywords": ["yii", "framework", "basic", "application template"], + "homepage": "http://www.yiiframework.com/", + "type": "project", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "minimum-stability": "dev", + "require": { + "php": ">=5.3.0", + "yiisoft/yii2": "dev-master", + "yiisoft/yii2-composer": "dev-master" + }, + "scripts": { + "post-create-project-cmd": [ + "yii\\composer\\InstallHandler::setPermissions" + ] + }, + "extra": { + "yii-install-writable": [ + "runtime", + "web/assets" + ], + "yii-install-executable": [ + "yii" + ] + } +} diff --git a/apps/basic/config/AppAsset.php b/apps/basic/config/AppAsset.php new file mode 100644 index 0000000..3e22b9b --- /dev/null +++ b/apps/basic/config/AppAsset.php @@ -0,0 +1,29 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace app\config; + +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class AppAsset extends AssetBundle +{ + public $basePath = '@webroot'; + public $baseUrl = '@web'; + public $css = array( + 'css/site.css', + ); + public $js = array( + ); + public $depends = array( + 'yii\web\YiiAsset', + 'yii\bootstrap\BootstrapAsset', + ); +} diff --git a/apps/bootstrap/protected/config/main.php b/apps/basic/config/console.php similarity index 60% rename from apps/bootstrap/protected/config/main.php rename to apps/basic/config/console.php index f19dead..12f13cd 100644 --- a/apps/bootstrap/protected/config/main.php +++ b/apps/basic/config/console.php @@ -1,36 +1,25 @@ <?php - +$params = require(__DIR__ . '/params.php'); return array( - 'id' => 'hello', + 'id' => 'bootstrap-console', 'basePath' => dirname(__DIR__), 'preload' => array('log'), + 'controllerPath' => dirname(__DIR__) . '/commands', + 'controllerNamespace' => 'app\commands', 'modules' => array( - 'debug' => array( - 'class' => 'yii\debug\Module', - ) ), 'components' => array( 'cache' => array( 'class' => 'yii\caching\FileCache', ), - 'user' => array( - 'class' => 'yii\web\User', - 'identityClass' => 'app\models\User', - ), - 'assetManager' => array( - 'bundles' => require(__DIR__ . '/assets.php'), - ), 'log' => array( - 'class' => 'yii\logging\Router', 'targets' => array( - 'file' => array( - 'class' => 'yii\logging\FileTarget', + array( + 'class' => 'yii\log\FileTarget', 'levels' => array('error', 'warning'), ), ), ), ), - 'params' => array( - 'adminEmail' => 'admin@example.com', - ), + 'params' => $params, ); diff --git a/apps/basic/config/params.php b/apps/basic/config/params.php new file mode 100644 index 0000000..398a1ce --- /dev/null +++ b/apps/basic/config/params.php @@ -0,0 +1,5 @@ +<?php + +return array( + 'adminEmail' => 'admin@example.com', +); diff --git a/apps/basic/config/web-test.php b/apps/basic/config/web-test.php new file mode 100644 index 0000000..ca0be86 --- /dev/null +++ b/apps/basic/config/web-test.php @@ -0,0 +1,7 @@ +<?php + +$config = require(__DIR__ . '/web.php'); + +// ... customize $config for the "test" environment here... + +return $config; diff --git a/apps/basic/config/web.php b/apps/basic/config/web.php new file mode 100644 index 0000000..e7d9420 --- /dev/null +++ b/apps/basic/config/web.php @@ -0,0 +1,38 @@ +<?php +$params = require(__DIR__ . '/params.php'); +$config = array( + 'id' => 'bootstrap', + 'basePath' => dirname(__DIR__), + 'components' => array( + 'request' => array( + 'enableCsrfValidation' => true, + ), + 'cache' => array( + 'class' => 'yii\caching\FileCache', + ), + 'user' => array( + 'identityClass' => 'app\models\User', + ), + 'errorHandler' => array( + 'errorAction' => 'site/error', + ), + 'log' => array( + 'traceLevel' => YII_DEBUG ? 3 : 0, + 'targets' => array( + array( + 'class' => 'yii\log\FileTarget', + 'levels' => array('error', 'warning'), + ), + ), + ), + ), + 'params' => $params, +); + +if (YII_ENV_DEV) { + $config['preload'][] = 'debug'; + $config['modules']['debug'] = 'yii\debug\Module'; + $config['modules']['gii'] = 'yii\gii\Module'; +} + +return $config; diff --git a/apps/bootstrap/protected/controllers/SiteController.php b/apps/basic/controllers/SiteController.php similarity index 57% rename from apps/bootstrap/protected/controllers/SiteController.php rename to apps/basic/controllers/SiteController.php index b06ed06..e243223 100644 --- a/apps/bootstrap/protected/controllers/SiteController.php +++ b/apps/basic/controllers/SiteController.php @@ -1,32 +1,69 @@ <?php +namespace app\controllers; + +use Yii; +use yii\web\AccessControl; use yii\web\Controller; +use yii\web\VerbFilter; use app\models\LoginForm; use app\models\ContactForm; class SiteController extends Controller { + public function behaviors() + { + return array( + 'access' => array( + 'class' => AccessControl::className(), + 'only' => array('login', 'logout'), + 'rules' => array( + array( + 'actions' => array('login'), + 'allow' => true, + 'roles' => array('?'), + ), + array( + 'actions' => array('logout'), + 'allow' => true, + 'roles' => array('@'), + ), + ), + ), + 'verbs' => array( + 'class' => VerbFilter::className(), + 'actions' => array( + 'logout' => array('post'), + ), + ), + ); + } + public function actions() { return array( + 'error' => array( + 'class' => 'yii\web\ErrorAction', + ), 'captcha' => array( - 'class' => 'yii\web\CaptchaAction', + 'class' => 'yii\captcha\CaptchaAction', + 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, ), ); } public function actionIndex() { - echo $this->render('index'); + return $this->render('index'); } public function actionLogin() { $model = new LoginForm(); - if ($this->populate($_POST, $model) && $model->login()) { - Yii::$app->response->redirect(array('site/index')); + if ($model->load($_POST) && $model->login()) { + return $this->goBack(); } else { - echo $this->render('login', array( + return $this->render('login', array( 'model' => $model, )); } @@ -34,18 +71,18 @@ class SiteController extends Controller public function actionLogout() { - Yii::$app->getUser()->logout(); - Yii::$app->getResponse()->redirect(array('site/index')); + Yii::$app->user->logout(); + return $this->goHome(); } public function actionContact() { $model = new ContactForm; - if ($this->populate($_POST, $model) && $model->contact(Yii::$app->params['adminEmail'])) { + if ($model->load($_POST) && $model->contact(Yii::$app->params['adminEmail'])) { Yii::$app->session->setFlash('contactFormSubmitted'); - Yii::$app->response->refresh(); + return $this->refresh(); } else { - echo $this->render('contact', array( + return $this->render('contact', array( 'model' => $model, )); } @@ -53,6 +90,6 @@ class SiteController extends Controller public function actionAbout() { - echo $this->render('about'); + return $this->render('about'); } } diff --git a/apps/bootstrap/protected/models/ContactForm.php b/apps/basic/models/ContactForm.php similarity index 100% rename from apps/bootstrap/protected/models/ContactForm.php rename to apps/basic/models/ContactForm.php diff --git a/apps/bootstrap/protected/models/LoginForm.php b/apps/basic/models/LoginForm.php similarity index 100% rename from apps/bootstrap/protected/models/LoginForm.php rename to apps/basic/models/LoginForm.php diff --git a/apps/bootstrap/protected/models/User.php b/apps/basic/models/User.php similarity index 98% rename from apps/bootstrap/protected/models/User.php rename to apps/basic/models/User.php index afbf9f8..e1088a0 100644 --- a/apps/bootstrap/protected/models/User.php +++ b/apps/basic/models/User.php @@ -2,7 +2,7 @@ namespace app\models; -class User extends \yii\base\Object implements \yii\web\Identity +class User extends \yii\base\Object implements \yii\web\IdentityInterface { public $id; public $username; diff --git a/apps/basic/requirements.php b/apps/basic/requirements.php new file mode 100644 index 0000000..c9e6493 --- /dev/null +++ b/apps/basic/requirements.php @@ -0,0 +1,103 @@ +<?php +/** + * Application requirement checker script. + * + * In order to run this script use the following console command: + * php requirements.php + * + * In order to run this script from the web, you should copy it to the web root. + * If you are using Linux you can create a hard link instead, using the following command: + * ln requirements.php ../requirements.php + */ + +// you may need to adjust this path to the correct Yii framework path +$frameworkPath = dirname(__FILE__) . '/vendor/yiisoft/yii2/yii'; + +if (!is_dir($frameworkPath)) { + echo '<h1>Error</h1>'; + echo '<p><strong>The path to yii framework seems to be incorrect.</strong></p>'; + echo '<p>You need to install Yii framework via composer or adjust the framework path in file <abbr title="' . __FILE__ . '">' . basename(__FILE__) .'</abbr>.</p>'; + echo '<p>Please refer to the <abbr title="' . dirname(__FILE__) . '/README.md">README</abbr> on how to install Yii.</p>'; +} + +require_once($frameworkPath . '/requirements/YiiRequirementChecker.php'); +$requirementsChecker = new YiiRequirementChecker(); + +/** + * Adjust requirements according to your application specifics. + */ +$requirements = array( + // Database : + array( + 'name' => 'PDO extension', + 'mandatory' => true, + 'condition' => extension_loaded('pdo'), + 'by' => 'All <a href="http://www.yiiframework.com/doc/api/#system.db">DB-related classes</a>', + ), + array( + 'name' => 'PDO SQLite extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_sqlite'), + 'by' => 'All <a href="http://www.yiiframework.com/doc/api/#system.db">DB-related classes</a>', + 'memo' => 'Required for SQLite database.', + ), + array( + 'name' => 'PDO MySQL extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_mysql'), + 'by' => 'All <a href="http://www.yiiframework.com/doc/api/#system.db">DB-related classes</a>', + 'memo' => 'Required for MySQL database.', + ), + // Cache : + array( + 'name' => 'Memcache extension', + 'mandatory' => false, + 'condition' => extension_loaded('memcache') || extension_loaded('memcached'), + 'by' => '<a href="http://www.yiiframework.com/doc/api/CMemCache">CMemCache</a>', + 'memo' => extension_loaded('memcached') ? 'To use memcached set <a href="http://www.yiiframework.com/doc/api/CMemCache#useMemcached-detail">CMemCache::useMemcached</a> to <code>true</code>.' : '' + ), + array( + 'name' => 'APC extension', + 'mandatory' => false, + 'condition' => extension_loaded('apc') || extension_loaded('apc'), + 'by' => '<a href="http://www.yiiframework.com/doc/api/CApcCache">CApcCache</a>', + ), + // Additional PHP extensions : + array( + 'name' => 'Mcrypt extension', + 'mandatory' => false, + 'condition' => extension_loaded('mcrypt'), + 'by' => '<a href="http://www.yiiframework.com/doc/api/CSecurityManager">CSecurityManager</a>', + 'memo' => 'Required by encrypt and decrypt methods.' + ), + // PHP ini : + 'phpSafeMode' => array( + 'name' => 'PHP safe mode', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("safe_mode"), + 'by' => 'File uploading and console command execution', + 'memo' => '"safe_mode" should be disabled at php.ini', + ), + 'phpExposePhp' => array( + 'name' => 'Expose PHP', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"), + 'by' => 'Security reasons', + 'memo' => '"expose_php" should be disabled at php.ini', + ), + 'phpAllowUrlInclude' => array( + 'name' => 'PHP allow url include', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"), + 'by' => 'Security reasons', + 'memo' => '"allow_url_include" should be disabled at php.ini', + ), + 'phpSmtp' => array( + 'name' => 'PHP mail SMTP', + 'mandatory' => false, + 'condition' => strlen(ini_get('SMTP'))>0, + 'by' => 'Email sending', + 'memo' => 'PHP mail SMTP server required', + ), +); +$requirementsChecker->checkYii()->check($requirements)->render(); diff --git a/apps/basic/runtime/.gitignore b/apps/basic/runtime/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/apps/basic/runtime/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/apps/basic/tests/_data/dump.sql b/apps/basic/tests/_data/dump.sql new file mode 100644 index 0000000..4bc742c --- /dev/null +++ b/apps/basic/tests/_data/dump.sql @@ -0,0 +1 @@ +/* Replace this file with actual dump of your database */ \ No newline at end of file diff --git a/apps/basic/tests/_helpers/CodeHelper.php b/apps/basic/tests/_helpers/CodeHelper.php new file mode 100644 index 0000000..972c8f3 --- /dev/null +++ b/apps/basic/tests/_helpers/CodeHelper.php @@ -0,0 +1,8 @@ +<?php +namespace Codeception\Module; + +// here you can define custom functions for CodeGuy + +class CodeHelper extends \Codeception\Module +{ +} diff --git a/apps/basic/tests/_helpers/TestHelper.php b/apps/basic/tests/_helpers/TestHelper.php new file mode 100644 index 0000000..37737cd --- /dev/null +++ b/apps/basic/tests/_helpers/TestHelper.php @@ -0,0 +1,8 @@ +<?php +namespace Codeception\Module; + +// here you can define custom functions for TestGuy + +class TestHelper extends \Codeception\Module +{ +} diff --git a/apps/basic/tests/_helpers/WebHelper.php b/apps/basic/tests/_helpers/WebHelper.php new file mode 100644 index 0000000..66b070e --- /dev/null +++ b/apps/basic/tests/_helpers/WebHelper.php @@ -0,0 +1,8 @@ +<?php +namespace Codeception\Module; + +// here you can define custom functions for WebGuy + +class WebHelper extends \Codeception\Module +{ +} diff --git a/apps/basic/tests/_log/.gitignore b/apps/basic/tests/_log/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/apps/basic/tests/_log/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/apps/basic/tests/acceptance.suite.dist.yml b/apps/basic/tests/acceptance.suite.dist.yml new file mode 100644 index 0000000..4965d89 --- /dev/null +++ b/apps/basic/tests/acceptance.suite.dist.yml @@ -0,0 +1,18 @@ +# Codeception Test Suite Configuration + +# suite for acceptance tests. +# perform tests in browser using the Selenium-like tools. +# powered by Mink (http://mink.behat.org). +# (tip: that's what your customer will see). +# (tip: test your ajax and javascript by one of Mink drivers). + +# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. + +class_name: WebGuy +modules: + enabled: + - PhpBrowser + - WebHelper + config: + PhpBrowser: + url: 'http://localhost/index-test.php' diff --git a/apps/basic/tests/acceptance/AboutCept.php b/apps/basic/tests/acceptance/AboutCept.php new file mode 100644 index 0000000..65102c7 --- /dev/null +++ b/apps/basic/tests/acceptance/AboutCept.php @@ -0,0 +1,5 @@ +<?php +$I = new WebGuy($scenario); +$I->wantTo('ensure that about works'); +$I->amOnPage('?r=site/about'); +$I->see('About', 'h1'); diff --git a/apps/basic/tests/acceptance/ContactCept.php b/apps/basic/tests/acceptance/ContactCept.php new file mode 100644 index 0000000..73527ab --- /dev/null +++ b/apps/basic/tests/acceptance/ContactCept.php @@ -0,0 +1,36 @@ +<?php +$I = new WebGuy($scenario); +$I->wantTo('ensure that contact works'); +$I->amOnPage('?r=site/contact'); +$I->see('Contact', 'h1'); + +$I->submitForm('#contact-form', array()); +$I->see('Contact', 'h1'); +$I->see('Name cannot be blank'); +$I->see('Email cannot be blank'); +$I->see('Subject cannot be blank'); +$I->see('Body cannot be blank'); +$I->see('The verification code is incorrect'); + +$I->submitForm('#contact-form', array( + 'ContactForm[name]' => 'tester', + 'ContactForm[email]' => 'tester.email', + 'ContactForm[subject]' => 'test subject', + 'ContactForm[body]' => 'test content', + 'ContactForm[verifyCode]' => 'testme', +)); +$I->dontSee('Name cannot be blank', '.help-inline'); +$I->see('Email is not a valid email address.'); +$I->dontSee('Subject cannot be blank', '.help-inline'); +$I->dontSee('Body cannot be blank', '.help-inline'); +$I->dontSee('The verification code is incorrect', '.help-inline'); + +$I->submitForm('#contact-form', array( + 'ContactForm[name]' => 'tester', + 'ContactForm[email]' => 'tester@example.com', + 'ContactForm[subject]' => 'test subject', + 'ContactForm[body]' => 'test content', + 'ContactForm[verifyCode]' => 'testme', +)); +$I->dontSeeElement('#contact-form'); +$I->see('Thank you for contacting us. We will respond to you as soon as possible.'); diff --git a/apps/basic/tests/acceptance/HomeCept.php b/apps/basic/tests/acceptance/HomeCept.php new file mode 100644 index 0000000..03dc4d5 --- /dev/null +++ b/apps/basic/tests/acceptance/HomeCept.php @@ -0,0 +1,8 @@ +<?php +$I = new WebGuy($scenario); +$I->wantTo('ensure that home page works'); +$I->amOnPage(''); +$I->see('My Company'); +$I->seeLink('About'); +$I->click('About'); +$I->see('This is the About page.'); diff --git a/apps/basic/tests/acceptance/LoginCept.php b/apps/basic/tests/acceptance/LoginCept.php new file mode 100644 index 0000000..77c4a07 --- /dev/null +++ b/apps/basic/tests/acceptance/LoginCept.php @@ -0,0 +1,23 @@ +<?php +$I = new WebGuy($scenario); +$I->wantTo('ensure that login works'); +$I->amOnPage('?r=site/login'); +$I->see('Login', 'h1'); + +$I->submitForm('#login-form', array()); +$I->dontSee('Logout (admin)'); +$I->see('Username cannot be blank'); +$I->see('Password cannot be blank'); + +$I->submitForm('#login-form', array( + 'LoginForm[username]' => 'admin', + 'LoginForm[password]' => 'wrong', +)); +$I->dontSee('Logout (admin)'); +$I->see('Incorrect username or password'); + +$I->submitForm('#login-form', array( + 'LoginForm[username]' => 'admin', + 'LoginForm[password]' => 'admin', +)); +$I->see('Logout (admin)'); diff --git a/apps/basic/tests/acceptance/WebGuy.php b/apps/basic/tests/acceptance/WebGuy.php new file mode 100644 index 0000000..397761c --- /dev/null +++ b/apps/basic/tests/acceptance/WebGuy.php @@ -0,0 +1,1205 @@ +<?php +// This class was automatically generated by build task +// You should not change it manually as it will be overwritten on next build +// @codingStandardsIgnoreFile + + +use \Codeception\Maybe; +use Codeception\Module\PhpBrowser; +use Codeception\Module\WebHelper; + +/** + * Inherited methods + * @method void execute($callable) + * @method void wantToTest($text) + * @method void wantTo($text) + * @method void expectTo($prediction) + * @method void expect($prediction) + * @method void amGoingTo($argumentation) + * @method void am($role) + * @method void lookForwardTo($role) +*/ + +class WebGuy extends \Codeception\AbstractGuy +{ + + /** + * Submits a form located on page. + * Specify the form by it's css or xpath selector. + * Fill the form fields values as array. + * + * Skipped fields will be filled by their values from page. + * You don't need to click the 'Submit' button afterwards. + * This command itself triggers the request to form's action. + * + * Examples: + * + * ``` php + * <?php + * $I->submitForm('#login', array('login' => 'davert', 'password' => '123456')); + * + * ``` + * + * For sample Sign Up form: + * + * ``` html + * <form action="/sign_up"> + * Login: <input type="text" name="user[login]" /><br/> + * Password: <input type="password" name="user[password]" /><br/> + * Do you agree to out terms? <input type="checkbox" name="user[agree]" /><br/> + * Select pricing plan <select name="plan"><option value="1">Free</option><option value="2" selected="selected">Paid</option></select> + * <input type="submit" value="Submit" /> + * </form> + * ``` + * I can write this: + * + * ``` php + * <?php + * $I->submitForm('#userForm', array('user' => array('login' => 'Davert', 'password' => '123456', 'agree' => true))); + * + * ``` + * Note, that pricing plan will be set to Paid, as it's selected on page. + * + * @param $selector + * @param $params + * @see PhpBrowser::submitForm() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function submitForm($selector, $params) { + $this->scenario->action('submitForm', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * If your page triggers an ajax request, you can perform it manually. + * This action sends a POST ajax request with specified params. + * Additional params can be passed as array. + * + * Example: + * + * Imagine that by clicking checkbox you trigger ajax request which updates user settings. + * We emulate that click by running this ajax request manually. + * + * ``` php + * <?php + * $I->sendAjaxPostRequest('/updateSettings', array('notifications' => true); // POST + * $I->sendAjaxGetRequest('/updateSettings', array('notifications' => true); // GET + * + * ``` + * + * @param $uri + * @param $params + * @see PhpBrowser::sendAjaxPostRequest() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function sendAjaxPostRequest($uri, $params = null) { + $this->scenario->action('sendAjaxPostRequest', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * If your page triggers an ajax request, you can perform it manually. + * This action sends a GET ajax request with specified params. + * + * See ->sendAjaxPostRequest for examples. + * + * @param $uri + * @param $params + * @see PhpBrowser::sendAjaxGetRequest() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function sendAjaxGetRequest($uri, $params = null) { + $this->scenario->action('sendAjaxGetRequest', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Asserts that current page has 404 response status code. + * @see PhpBrowser::seePageNotFound() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seePageNotFound() { + $this->scenario->assertion('seePageNotFound', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that response code is equal to value provided. + * + * @param $code + * @return mixed + * @see PhpBrowser::seeResponseCodeIs() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeResponseCodeIs($code) { + $this->scenario->assertion('seeResponseCodeIs', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Adds HTTP authentication via username/password. + * + * @param $username + * @param $password + * @see PhpBrowser::amHttpAuthenticated() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function amHttpAuthenticated($username, $password) { + $this->scenario->condition('amHttpAuthenticated', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Low-level API method. + * If Codeception commands are not enough, use [Guzzle HTTP Client](http://guzzlephp.org/) methods directly + * + * Example: + * + * ``` php + * <?php + * // from the official Guzzle manual + * $I->amGoingTo('Sign all requests with OAuth'); + * $I->executeInGuzzle(function (\Guzzle\Http\Client $client) { + * $client->addSubscriber(new Guzzle\Plugin\Oauth\OauthPlugin(array( + * 'consumer_key' => '***', + * 'consumer_secret' => '***', + * 'token' => '***', + * 'token_secret' => '***' + * ))); + * }); + * ?> + * ``` + * + * Not recommended this command too be used on regular basis. + * If Codeception lacks important Guzzle Client methods implement then and submit patches. + * + * @param callable $function + * @see PhpBrowser::executeInGuzzle() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function executeInGuzzle($function) { + $this->scenario->action('executeInGuzzle', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Opens the page. + * + * @param $page + * @see Mink::amOnPage() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function amOnPage($page) { + $this->scenario->condition('amOnPage', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Sets 'url' configuration parameter to hosts subdomain. + * It does not open a page on subdomain. Use `amOnPage` for that + * + * ``` php + * <?php + * // If config is: 'http://mysite.com' + * // or config is: 'http://www.mysite.com' + * // or config is: 'http://company.mysite.com' + * + * $I->amOnSubdomain('user'); + * $I->amOnPage('/'); + * // moves to http://user.mysite.com/ + * ?> + * ``` + * @param $subdomain + * @return mixed + * @see Mink::amOnSubdomain() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function amOnSubdomain($subdomain) { + $this->scenario->condition('amOnSubdomain', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Check if current page doesn't contain the text specified. + * Specify the css selector to match only specific region. + * + * Examples: + * + * ```php + * <?php + * $I->dontSee('Login'); // I can suppose user is already logged in + * $I->dontSee('Sign Up','h1'); // I can suppose it's not a signup page + * $I->dontSee('Sign Up','//body/h1'); // with XPath + * ``` + * + * @param $text + * @param null $selector + * @see Mink::dontSee() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSee($text, $selector = null) { + $this->scenario->action('dontSee', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Check if current page contains the text specified. + * Specify the css selector to match only specific region. + * + * Examples: + * + * ``` php + * <?php + * $I->see('Logout'); // I can suppose user is logged in + * $I->see('Sign Up','h1'); // I can suppose it's a signup page + * $I->see('Sign Up','//body/h1'); // with XPath + * + * ``` + * + * @param $text + * @param null $selector + * @see Mink::see() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function see($text, $selector = null) { + $this->scenario->assertion('see', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if there is a link with text specified. + * Specify url to match link with exact this url. + * + * Examples: + * + * ``` php + * <?php + * $I->seeLink('Logout'); // matches <a href="#">Logout</a> + * $I->seeLink('Logout','/logout'); // matches <a href="/logout">Logout</a> + * + * ``` + * + * @param $text + * @param null $url + * @see Mink::seeLink() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeLink($text, $url = null) { + $this->scenario->assertion('seeLink', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if page doesn't contain the link with text specified. + * Specify url to narrow the results. + * + * Examples: + * + * ``` php + * <?php + * $I->dontSeeLink('Logout'); // I suppose user is not logged in + * + * ``` + * + * @param $text + * @param null $url + * @see Mink::dontSeeLink() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeLink($text, $url = null) { + $this->scenario->action('dontSeeLink', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Perform a click on link or button. + * Link or button are found by their names or CSS selector. + * Submits a form if button is a submit type. + * + * If link is an image it's found by alt attribute value of image. + * If button is image button is found by it's value + * If link or button can't be found by name they are searched by CSS selector. + * + * The second parameter is a context: CSS or XPath locator to narrow the search. + * + * Examples: + * + * ``` php + * <?php + * // simple link + * $I->click('Logout'); + * // button of form + * $I->click('Submit'); + * // CSS button + * $I->click('#form input[type=submit]'); + * // XPath + * $I->click('//form/*[@type=submit]') + * // link in context + * $I->click('Logout', '#nav'); + * ?> + * ``` + * @param $link + * @param $context + * @see Mink::click() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function click($link, $context = null) { + $this->scenario->action('click', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if element exists on a page, matching it by CSS or XPath + * + * ``` php + * <?php + * $I->seeElement('.error'); + * $I->seeElement(//form/input[1]); + * ?> + * ``` + * @param $selector + * @see Mink::seeElement() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeElement($selector) { + $this->scenario->assertion('seeElement', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if element does not exist (or is visible) on a page, matching it by CSS or XPath + * + * ``` php + * <?php + * $I->dontSeeElement('.error'); + * $I->dontSeeElement(//form/input[1]); + * ?> + * ``` + * @param $selector + * @see Mink::dontSeeElement() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeElement($selector) { + $this->scenario->action('dontSeeElement', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Reloads current page + * @see Mink::reloadPage() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function reloadPage() { + $this->scenario->action('reloadPage', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Moves back in history + * @see Mink::moveBack() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function moveBack() { + $this->scenario->action('moveBack', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Moves forward in history + * @see Mink::moveForward() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function moveForward() { + $this->scenario->action('moveForward', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Fills a text field or textarea with value. + * + * @param $field + * @param $value + * @see Mink::fillField() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function fillField($field, $value) { + $this->scenario->action('fillField', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Selects an option in select tag or in radio button group. + * + * Example: + * + * ``` php + * <?php + * $I->selectOption('form select[name=account]', 'Premium'); + * $I->selectOption('form input[name=payment]', 'Monthly'); + * $I->selectOption('//form/select[@name=account]', 'Monthly'); + * ?> + * ``` + * + * @param $select + * @param $option + * @see Mink::selectOption() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function selectOption($select, $option) { + $this->scenario->action('selectOption', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Ticks a checkbox. + * For radio buttons use `selectOption` method. + * + * Example: + * + * ``` php + * <?php + * $I->checkOption('#agree'); + * ?> + * ``` + * + * @param $option + * @see Mink::checkOption() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function checkOption($option) { + $this->scenario->action('checkOption', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Unticks a checkbox. + * + * Example: + * + * ``` php + * <?php + * $I->uncheckOption('#notify'); + * ?> + * ``` + * + * @param $option + * @see Mink::uncheckOption() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function uncheckOption($option) { + $this->scenario->action('uncheckOption', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current uri contains a value + * + * ``` php + * <?php + * // to match: /home/dashboard + * $I->seeInCurrentUrl('home'); + * // to match: /users/1 + * $I->seeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param $uri + * @see Mink::seeInCurrentUrl() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeInCurrentUrl($uri) { + $this->scenario->assertion('seeInCurrentUrl', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current uri does not contain a value + * + * ``` php + * <?php + * $I->dontSeeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param $uri + * @see Mink::dontSeeInCurrentUrl() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeInCurrentUrl($uri) { + $this->scenario->action('dontSeeInCurrentUrl', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current url is equal to value. + * Unlike `seeInCurrentUrl` performs a strict check. + * + * <?php + * // to match root url + * $I->seeCurrentUrlEquals('/'); + * ?> + * + * @param $uri + * @see Mink::seeCurrentUrlEquals() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeCurrentUrlEquals($uri) { + $this->scenario->assertion('seeCurrentUrlEquals', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current url is not equal to value. + * Unlike `dontSeeInCurrentUrl` performs a strict check. + * + * <?php + * // current url is not root + * $I->dontSeeCurrentUrlEquals('/'); + * ?> + * + * @param $uri + * @see Mink::dontSeeCurrentUrlEquals() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeCurrentUrlEquals($uri) { + $this->scenario->action('dontSeeCurrentUrlEquals', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current url is matches a RegEx value + * + * <?php + * // to match root url + * $I->seeCurrentUrlMatches('~$/users/(\d+)~'); + * ?> + * + * @param $uri + * @see Mink::seeCurrentUrlMatches() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeCurrentUrlMatches($uri) { + $this->scenario->assertion('seeCurrentUrlMatches', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current url does not match a RegEx value + * + * <?php + * // to match root url + * $I->dontSeeCurrentUrlMatches('~$/users/(\d+)~'); + * ?> + * + * @param $uri + * @see Mink::dontSeeCurrentUrlMatches() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeCurrentUrlMatches($uri) { + $this->scenario->action('dontSeeCurrentUrlMatches', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * + * @see Mink::seeCookie() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeCookie($cookie) { + $this->scenario->assertion('seeCookie', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * + * @see Mink::dontSeeCookie() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeCookie($cookie) { + $this->scenario->action('dontSeeCookie', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * + * @see Mink::setCookie() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function setCookie($cookie, $value) { + $this->scenario->action('setCookie', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * + * @see Mink::resetCookie() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function resetCookie($cookie) { + $this->scenario->action('resetCookie', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * + * @see Mink::grabCookie() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function grabCookie($cookie) { + $this->scenario->action('grabCookie', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Takes a parameters from current URI by RegEx. + * If no url provided returns full URI. + * + * ``` php + * <?php + * $user_id = $I->grabFromCurrentUrl('~$/user/(\d+)/~'); + * $uri = $I->grabFromCurrentUrl(); + * ?> + * ``` + * + * @param null $uri + * @internal param $url + * @return mixed + * @see Mink::grabFromCurrentUrl() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function grabFromCurrentUrl($uri = null) { + $this->scenario->action('grabFromCurrentUrl', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Attaches file from Codeception data directory to upload field. + * + * Example: + * + * ``` php + * <?php + * // file is stored in 'tests/data/tests.xls' + * $I->attachFile('prices.xls'); + * ?> + * ``` + * + * @param $field + * @param $filename + * @see Mink::attachFile() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function attachFile($field, $filename) { + $this->scenario->action('attachFile', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if option is selected in select field. + * + * ``` php + * <?php + * $I->seeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * @return mixed + * @see Mink::seeOptionIsSelected() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeOptionIsSelected($select, $text) { + $this->scenario->assertion('seeOptionIsSelected', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if option is not selected in select field. + * + * ``` php + * <?php + * $I->dontSeeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * @return mixed + * @see Mink::dontSeeOptionIsSelected() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeOptionIsSelected($select, $text) { + $this->scenario->action('dontSeeOptionIsSelected', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Assert if the specified checkbox is checked. + * Use css selector or xpath to match. + * + * Example: + * + * ``` php + * <?php + * $I->seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user agreed to terms, If there is only one checkbox in form. + * $I->seeCheckboxIsChecked('//form/input[@type=checkbox and @name=agree]'); + * + * ``` + * + * @param $checkbox + * @see Mink::seeCheckboxIsChecked() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeCheckboxIsChecked($checkbox) { + $this->scenario->assertion('seeCheckboxIsChecked', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Assert if the specified checkbox is unchecked. + * Use css selector or xpath to match. + * + * Example: + * + * ``` php + * <?php + * $I->dontSeeCheckboxIsChecked('#agree'); // I suppose user didn't agree to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user didn't check the first checkbox in form. + * + * ``` + * + * @param $checkbox + * @see Mink::dontSeeCheckboxIsChecked() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeCheckboxIsChecked($checkbox) { + $this->scenario->action('dontSeeCheckboxIsChecked', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that an input field or textarea contains value. + * Field is matched either by label or CSS or Xpath + * + * Example: + * + * ``` php + * <?php + * $I->seeInField('Body','Type your comment here'); + * $I->seeInField('form textarea[name=body]','Type your comment here'); + * $I->seeInField('form input[type=hidden]','hidden_value'); + * $I->seeInField('#searchform input','Search'); + * $I->seeInField('//form/*[@name=search]','Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see Mink::seeInField() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeInField($field, $value) { + $this->scenario->assertion('seeInField', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that an input field or textarea doesn't contain value. + * Field is matched either by label or CSS or Xpath + * Example: + * + * ``` php + * <?php + * $I->dontSeeInField('Body','Type your comment here'); + * $I->dontSeeInField('form textarea[name=body]','Type your comment here'); + * $I->dontSeeInField('form input[type=hidden]','hidden_value'); + * $I->dontSeeInField('#searchform input','Search'); + * $I->dontSeeInField('//form/*[@name=search]','Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see Mink::dontSeeInField() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeInField($field, $value) { + $this->scenario->action('dontSeeInField', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Finds and returns text contents of element. + * Element is searched by CSS selector, XPath or matcher by regex. + * + * Example: + * + * ``` php + * <?php + * $heading = $I->grabTextFrom('h1'); + * $heading = $I->grabTextFrom('descendant-or-self::h1'); + * $value = $I->grabTextFrom('~<input value=(.*?)]~sgi'); + * ?> + * ``` + * + * @param $cssOrXPathOrRegex + * @return mixed + * @see Mink::grabTextFrom() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function grabTextFrom($cssOrXPathOrRegex) { + $this->scenario->action('grabTextFrom', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Finds and returns field and returns it's value. + * Searches by field name, then by CSS, then by XPath + * + * Example: + * + * ``` php + * <?php + * $name = $I->grabValueFrom('Name'); + * $name = $I->grabValueFrom('input[name=username]'); + * $name = $I->grabValueFrom('descendant-or-self::form/descendant::input[@name = 'username']'); + * ?> + * ``` + * + * @param $field + * @return mixed + * @see Mink::grabValueFrom() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function grabValueFrom($field) { + $this->scenario->action('grabValueFrom', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * + * @see Mink::grabAttribute() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function grabAttribute() { + $this->scenario->action('grabAttribute', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } +} + diff --git a/apps/basic/tests/acceptance/_bootstrap.php b/apps/basic/tests/acceptance/_bootstrap.php new file mode 100644 index 0000000..7dfa7c3 --- /dev/null +++ b/apps/basic/tests/acceptance/_bootstrap.php @@ -0,0 +1,2 @@ +<?php +// Here you can initialize variables that will for your tests diff --git a/apps/basic/tests/functional.suite.dist.yml b/apps/basic/tests/functional.suite.dist.yml new file mode 100644 index 0000000..6dd9193 --- /dev/null +++ b/apps/basic/tests/functional.suite.dist.yml @@ -0,0 +1,15 @@ +# Codeception Test Suite Configuration + +# suite for functional (integration) tests. +# emulate web requests and make application process them. +# (tip: better to use with frameworks). + +# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. + +class_name: TestGuy +modules: + enabled: [Filesystem, TestHelper, Yii2] + config: + Yii2: + entryScript: 'web/index-test.php' + url: 'http://localhost/' diff --git a/apps/basic/tests/functional/AboutCept.php b/apps/basic/tests/functional/AboutCept.php new file mode 100644 index 0000000..3b92b2e --- /dev/null +++ b/apps/basic/tests/functional/AboutCept.php @@ -0,0 +1,5 @@ +<?php +$I = new TestGuy($scenario); +$I->wantTo('ensure that about works'); +$I->amOnPage('?r=site/about'); +$I->see('About', 'h1'); diff --git a/apps/basic/tests/functional/ContactCept.php b/apps/basic/tests/functional/ContactCept.php new file mode 100644 index 0000000..6feafd9 --- /dev/null +++ b/apps/basic/tests/functional/ContactCept.php @@ -0,0 +1,36 @@ +<?php +$I = new TestGuy($scenario); +$I->wantTo('ensure that contact works'); +$I->amOnPage('?r=site/contact'); +$I->see('Contact', 'h1'); + +$I->submitForm('#contact-form', array()); +$I->see('Contact', 'h1'); +$I->see('Name cannot be blank'); +$I->see('Email cannot be blank'); +$I->see('Subject cannot be blank'); +$I->see('Body cannot be blank'); +$I->see('The verification code is incorrect'); + +$I->submitForm('#contact-form', array( + 'ContactForm[name]' => 'tester', + 'ContactForm[email]' => 'tester.email', + 'ContactForm[subject]' => 'test subject', + 'ContactForm[body]' => 'test content', + 'ContactForm[verifyCode]' => 'testme', +)); +$I->dontSee('Name cannot be blank', '.help-inline'); +$I->see('Email is not a valid email address.'); +$I->dontSee('Subject cannot be blank', '.help-inline'); +$I->dontSee('Body cannot be blank', '.help-inline'); +$I->dontSee('The verification code is incorrect', '.help-inline'); + +$I->submitForm('#contact-form', array( + 'ContactForm[name]' => 'tester', + 'ContactForm[email]' => 'tester@example.com', + 'ContactForm[subject]' => 'test subject', + 'ContactForm[body]' => 'test content', + 'ContactForm[verifyCode]' => 'testme', +)); +$I->dontSeeElement('#contact-form'); +$I->see('Thank you for contacting us. We will respond to you as soon as possible.'); diff --git a/apps/basic/tests/functional/HomeCept.php b/apps/basic/tests/functional/HomeCept.php new file mode 100644 index 0000000..1d24af6 --- /dev/null +++ b/apps/basic/tests/functional/HomeCept.php @@ -0,0 +1,8 @@ +<?php +$I = new TestGuy($scenario); +$I->wantTo('ensure that home page works'); +$I->amOnPage(''); +$I->see('My Company'); +$I->seeLink('About'); +$I->click('About'); +$I->see('This is the About page.'); diff --git a/apps/basic/tests/functional/LoginCept.php b/apps/basic/tests/functional/LoginCept.php new file mode 100644 index 0000000..11f8f6b --- /dev/null +++ b/apps/basic/tests/functional/LoginCept.php @@ -0,0 +1,23 @@ +<?php +$I = new TestGuy($scenario); +$I->wantTo('ensure that login works'); +$I->amOnPage('?r=site/login'); +$I->see('Login', 'h1'); + +$I->submitForm('#login-form', array()); +$I->dontSee('Logout (admin)'); +$I->see('Username cannot be blank'); +$I->see('Password cannot be blank'); + +$I->submitForm('#login-form', array( + 'LoginForm[username]' => 'admin', + 'LoginForm[password]' => 'wrong', +)); +$I->dontSee('Logout (admin)'); +$I->see('Incorrect username or password'); + +$I->submitForm('#login-form', array( + 'LoginForm[username]' => 'admin', + 'LoginForm[password]' => 'admin', +)); +$I->see('Logout (admin)'); diff --git a/apps/basic/tests/functional/TestGuy.php b/apps/basic/tests/functional/TestGuy.php new file mode 100644 index 0000000..767d564 --- /dev/null +++ b/apps/basic/tests/functional/TestGuy.php @@ -0,0 +1,1286 @@ +<?php +// This class was automatically generated by build task +// You should not change it manually as it will be overwritten on next build +// @codingStandardsIgnoreFile + + +use \Codeception\Maybe; +use Codeception\Module\Filesystem; +use Codeception\Module\TestHelper; +use Codeception\Module\Yii2; + +/** + * Inherited methods + * @method void execute($callable) + * @method void wantToTest($text) + * @method void wantTo($text) + * @method void expectTo($prediction) + * @method void expect($prediction) + * @method void amGoingTo($argumentation) + * @method void am($role) + * @method void lookForwardTo($role) +*/ + +class TestGuy extends \Codeception\AbstractGuy +{ + + /** + * Enters a directory In local filesystem. + * Project root directory is used by default + * + * @param $path + * @see Filesystem::amInPath() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function amInPath($path) { + $this->scenario->condition('amInPath', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Opens a file and stores it's content. + * + * Usage: + * + * ``` php + * <?php + * $I->openFile('composer.json'); + * $I->seeInThisFile('codeception/codeception'); + * ?> + * ``` + * + * @param $filename + * @see Filesystem::openFile() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function openFile($filename) { + $this->scenario->action('openFile', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Deletes a file + * + * ``` php + * <?php + * $I->deleteFile('composer.lock'); + * ?> + * ``` + * + * @param $filename + * @see Filesystem::deleteFile() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function deleteFile($filename) { + $this->scenario->action('deleteFile', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Deletes directory with all subdirectories + * + * ``` php + * <?php + * $I->deleteDir('vendor'); + * ?> + * ``` + * + * @param $dirname + * @see Filesystem::deleteDir() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function deleteDir($dirname) { + $this->scenario->action('deleteDir', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Copies directory with all contents + * + * ``` php + * <?php + * $I->copyDir('vendor','old_vendor'); + * ?> + * ``` + * + * @param $src + * @param $dst + * @see Filesystem::copyDir() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function copyDir($src, $dst) { + $this->scenario->action('copyDir', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks If opened file has `text` in it. + * + * Usage: + * + * ``` php + * <?php + * $I->openFile('composer.json'); + * $I->seeInThisFile('codeception/codeception'); + * ?> + * ``` + * + * @param $text + * @see Filesystem::seeInThisFile() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeInThisFile($text) { + $this->scenario->assertion('seeInThisFile', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks the strict matching of file contents. + * Unlike `seeInThisFile` will fail if file has something more then expected lines. + * Better to use with HEREDOC strings. + * Matching is done after removing "\r" chars from file content. + * + * ``` php + * <?php + * $I->openFile('process.pid'); + * $I->seeFileContentsEqual('3192'); + * ?> + * ``` + * + * @param $text + * @see Filesystem::seeFileContentsEqual() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeFileContentsEqual($text) { + $this->scenario->assertion('seeFileContentsEqual', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks If opened file doesn't contain `text` in it + * + * ``` php + * <?php + * $I->openFile('composer.json'); + * $I->seeInThisFile('codeception/codeception'); + * ?> + * ``` + * + * @param $text + * @see Filesystem::dontSeeInThisFile() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeInThisFile($text) { + $this->scenario->action('dontSeeInThisFile', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Deletes a file + * @see Filesystem::deleteThisFile() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function deleteThisFile() { + $this->scenario->action('deleteThisFile', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if file exists in path. + * Opens a file when it's exists + * + * ``` php + * <?php + * $I->seeFileFound('UserModel.php','app/models'); + * ?> + * ``` + * + * @param $filename + * @param string $path + * @see Filesystem::seeFileFound() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeFileFound($filename, $path = null) { + $this->scenario->assertion('seeFileFound', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Erases directory contents + * + * ``` php + * <?php + * $I->cleanDir('logs'); + * ?> + * ``` + * + * @param $dirname + * @see Filesystem::cleanDir() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function cleanDir($dirname) { + $this->scenario->action('cleanDir', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Adds HTTP authentication via username/password. + * + * @param $username + * @param $password + * @see Framework::amHttpAuthenticated() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function amHttpAuthenticated($username, $password) { + $this->scenario->condition('amHttpAuthenticated', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Opens the page. + * Requires relative uri as parameter + * + * Example: + * + * ``` php + * <?php + * // opens front page + * $I->amOnPage('/'); + * // opens /register page + * $I->amOnPage('/register'); + * ?> + * ``` + * + * @param $page + * @see Framework::amOnPage() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function amOnPage($page) { + $this->scenario->condition('amOnPage', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Perform a click on link or button. + * Link or button are found by their names or CSS selector. + * Submits a form if button is a submit type. + * + * If link is an image it's found by alt attribute value of image. + * If button is image button is found by it's value + * If link or button can't be found by name they are searched by CSS selector. + * + * The second parameter is a context: CSS or XPath locator to narrow the search. + * + * Examples: + * + * ``` php + * <?php + * // simple link + * $I->click('Logout'); + * // button of form + * $I->click('Submit'); + * // CSS button + * $I->click('#form input[type=submit]'); + * // XPath + * $I->click('//form/*[@type=submit]') + * // link in context + * $I->click('Logout', '#nav'); + * ?> + * ``` + * @param $link + * @param $context + * @see Framework::click() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function click($link, $context = null) { + $this->scenario->action('click', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Check if current page contains the text specified. + * Specify the css selector to match only specific region. + * + * Examples: + * + * ``` php + * <?php + * $I->see('Logout'); // I can suppose user is logged in + * $I->see('Sign Up','h1'); // I can suppose it's a signup page + * $I->see('Sign Up','//body/h1'); // with XPath + * + * ``` + * + * @param $text + * @param null $selector + * @see Framework::see() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function see($text, $selector = null) { + $this->scenario->assertion('see', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Check if current page doesn't contain the text specified. + * Specify the css selector to match only specific region. + * + * Examples: + * + * ```php + * <?php + * $I->dontSee('Login'); // I can suppose user is already logged in + * $I->dontSee('Sign Up','h1'); // I can suppose it's not a signup page + * $I->dontSee('Sign Up','//body/h1'); // with XPath + * ``` + * + * @param $text + * @param null $selector + * @see Framework::dontSee() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSee($text, $selector = null) { + $this->scenario->action('dontSee', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if there is a link with text specified. + * Specify url to match link with exact this url. + * + * Examples: + * + * ``` php + * <?php + * $I->seeLink('Logout'); // matches <a href="#">Logout</a> + * $I->seeLink('Logout','/logout'); // matches <a href="/logout">Logout</a> + * + * ``` + * + * @param $text + * @param null $url + * @see Framework::seeLink() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeLink($text, $url = null) { + $this->scenario->assertion('seeLink', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if page doesn't contain the link with text specified. + * Specify url to narrow the results. + * + * Examples: + * + * ``` php + * <?php + * $I->dontSeeLink('Logout'); // I suppose user is not logged in + * + * ``` + * + * @param $text + * @param null $url + * @see Framework::dontSeeLink() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeLink($text, $url = null) { + $this->scenario->action('dontSeeLink', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current uri contains a value + * + * ``` php + * <?php + * // to match: /home/dashboard + * $I->seeInCurrentUrl('home'); + * // to match: /users/1 + * $I->seeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param $uri + * @see Framework::seeInCurrentUrl() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeInCurrentUrl($uri) { + $this->scenario->assertion('seeInCurrentUrl', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current uri does not contain a value + * + * ``` php + * <?php + * $I->dontSeeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param $uri + * @see Framework::dontSeeInCurrentUrl() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeInCurrentUrl($uri) { + $this->scenario->action('dontSeeInCurrentUrl', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current url is equal to value. + * Unlike `seeInCurrentUrl` performs a strict check. + * + * <?php + * // to match root url + * $I->seeCurrentUrlEquals('/'); + * ?> + * + * @param $uri + * @see Framework::seeCurrentUrlEquals() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeCurrentUrlEquals($uri) { + $this->scenario->assertion('seeCurrentUrlEquals', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current url is not equal to value. + * Unlike `dontSeeInCurrentUrl` performs a strict check. + * + * <?php + * // current url is not root + * $I->dontSeeCurrentUrlEquals('/'); + * ?> + * + * @param $uri + * @see Framework::dontSeeCurrentUrlEquals() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeCurrentUrlEquals($uri) { + $this->scenario->action('dontSeeCurrentUrlEquals', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current url is matches a RegEx value + * + * <?php + * // to match root url + * $I->seeCurrentUrlMatches('~$/users/(\d+)~'); + * ?> + * + * @param $uri + * @see Framework::seeCurrentUrlMatches() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeCurrentUrlMatches($uri) { + $this->scenario->assertion('seeCurrentUrlMatches', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current url does not match a RegEx value + * + * <?php + * // to match root url + * $I->dontSeeCurrentUrlMatches('~$/users/(\d+)~'); + * ?> + * + * @param $uri + * @see Framework::dontSeeCurrentUrlMatches() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeCurrentUrlMatches($uri) { + $this->scenario->action('dontSeeCurrentUrlMatches', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Takes a parameters from current URI by RegEx. + * If no url provided returns full URI. + * + * ``` php + * <?php + * $user_id = $I->grabFromCurrentUrl('~$/user/(\d+)/~'); + * $uri = $I->grabFromCurrentUrl(); + * ?> + * ``` + * + * @param null $uri + * @internal param $url + * @return mixed + * @see Framework::grabFromCurrentUrl() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function grabFromCurrentUrl($uri = null) { + $this->scenario->action('grabFromCurrentUrl', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Assert if the specified checkbox is checked. + * Use css selector or xpath to match. + * + * Example: + * + * ``` php + * <?php + * $I->seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user agreed to terms, If there is only one checkbox in form. + * $I->seeCheckboxIsChecked('//form/input[@type=checkbox and @name=agree]'); + * + * ``` + * + * @param $checkbox + * @see Framework::seeCheckboxIsChecked() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeCheckboxIsChecked($checkbox) { + $this->scenario->assertion('seeCheckboxIsChecked', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Assert if the specified checkbox is unchecked. + * Use css selector or xpath to match. + * + * Example: + * + * ``` php + * <?php + * $I->dontSeeCheckboxIsChecked('#agree'); // I suppose user didn't agree to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user didn't check the first checkbox in form. + * + * ``` + * + * @param $checkbox + * @see Framework::dontSeeCheckboxIsChecked() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeCheckboxIsChecked($checkbox) { + $this->scenario->action('dontSeeCheckboxIsChecked', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that an input field or textarea contains value. + * Field is matched either by label or CSS or Xpath + * + * Example: + * + * ``` php + * <?php + * $I->seeInField('Body','Type your comment here'); + * $I->seeInField('form textarea[name=body]','Type your comment here'); + * $I->seeInField('form input[type=hidden]','hidden_value'); + * $I->seeInField('#searchform input','Search'); + * $I->seeInField('//form/*[@name=search]','Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see Framework::seeInField() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeInField($field, $value) { + $this->scenario->assertion('seeInField', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that an input field or textarea doesn't contain value. + * Field is matched either by label or CSS or Xpath + * Example: + * + * ``` php + * <?php + * $I->dontSeeInField('Body','Type your comment here'); + * $I->dontSeeInField('form textarea[name=body]','Type your comment here'); + * $I->dontSeeInField('form input[type=hidden]','hidden_value'); + * $I->dontSeeInField('#searchform input','Search'); + * $I->dontSeeInField('//form/*[@name=search]','Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see Framework::dontSeeInField() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeInField($field, $value) { + $this->scenario->action('dontSeeInField', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Submits a form located on page. + * Specify the form by it's css or xpath selector. + * Fill the form fields values as array. + * + * Skipped fields will be filled by their values from page. + * You don't need to click the 'Submit' button afterwards. + * This command itself triggers the request to form's action. + * + * Examples: + * + * ``` php + * <?php + * $I->submitForm('#login', array('login' => 'davert', 'password' => '123456')); + * + * ``` + * + * For sample Sign Up form: + * + * ``` html + * <form action="/sign_up"> + * Login: <input type="text" name="user[login]" /><br/> + * Password: <input type="password" name="user[password]" /><br/> + * Do you agree to out terms? <input type="checkbox" name="user[agree]" /><br/> + * Select pricing plan <select name="plan"><option value="1">Free</option><option value="2" selected="selected">Paid</option></select> + * <input type="submit" value="Submit" /> + * </form> + * ``` + * I can write this: + * + * ``` php + * <?php + * $I->submitForm('#userForm', array('user' => array('login' => 'Davert', 'password' => '123456', 'agree' => true))); + * + * ``` + * Note, that pricing plan will be set to Paid, as it's selected on page. + * + * @param $selector + * @param $params + * @see Framework::submitForm() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function submitForm($selector, $params) { + $this->scenario->action('submitForm', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Fills a text field or textarea with value. + * + * @param $field + * @param $value + * @see Framework::fillField() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function fillField($field, $value) { + $this->scenario->action('fillField', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Selects an option in select tag or in radio button group. + * + * Example: + * + * ``` php + * <?php + * $I->selectOption('form select[name=account]', 'Premium'); + * $I->selectOption('form input[name=payment]', 'Monthly'); + * $I->selectOption('//form/select[@name=account]', 'Monthly'); + * ?> + * ``` + * + * @param $select + * @param $option + * @see Framework::selectOption() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function selectOption($select, $option) { + $this->scenario->action('selectOption', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Ticks a checkbox. + * For radio buttons use `selectOption` method. + * + * Example: + * + * ``` php + * <?php + * $I->checkOption('#agree'); + * ?> + * ``` + * + * @param $option + * @see Framework::checkOption() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function checkOption($option) { + $this->scenario->action('checkOption', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Unticks a checkbox. + * + * Example: + * + * ``` php + * <?php + * $I->uncheckOption('#notify'); + * ?> + * ``` + * + * @param $option + * @see Framework::uncheckOption() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function uncheckOption($option) { + $this->scenario->action('uncheckOption', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Attaches file from Codeception data directory to upload field. + * + * Example: + * + * ``` php + * <?php + * // file is stored in 'tests/data/tests.xls' + * $I->attachFile('prices.xls'); + * ?> + * ``` + * + * @param $field + * @param $filename + * @see Framework::attachFile() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function attachFile($field, $filename) { + $this->scenario->action('attachFile', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * If your page triggers an ajax request, you can perform it manually. + * This action sends a GET ajax request with specified params. + * + * See ->sendAjaxPostRequest for examples. + * + * @param $uri + * @param $params + * @see Framework::sendAjaxGetRequest() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function sendAjaxGetRequest($uri, $params = null) { + $this->scenario->action('sendAjaxGetRequest', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * If your page triggers an ajax request, you can perform it manually. + * This action sends a POST ajax request with specified params. + * Additional params can be passed as array. + * + * Example: + * + * Imagine that by clicking checkbox you trigger ajax request which updates user settings. + * We emulate that click by running this ajax request manually. + * + * ``` php + * <?php + * $I->sendAjaxPostRequest('/updateSettings', array('notifications' => true); // POST + * $I->sendAjaxGetRequest('/updateSettings', array('notifications' => true); // GET + * + * ``` + * + * @param $uri + * @param $params + * @see Framework::sendAjaxPostRequest() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function sendAjaxPostRequest($uri, $params = null) { + $this->scenario->action('sendAjaxPostRequest', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * + * @see Framework::formatResponse() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function formatResponse($response) { + $this->scenario->action('formatResponse', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Finds and returns text contents of element. + * Element is searched by CSS selector, XPath or matcher by regex. + * + * Example: + * + * ``` php + * <?php + * $heading = $I->grabTextFrom('h1'); + * $heading = $I->grabTextFrom('descendant-or-self::h1'); + * $value = $I->grabTextFrom('~<input value=(.*?)]~sgi'); + * ?> + * ``` + * + * @param $cssOrXPathOrRegex + * @return mixed + * @see Framework::grabTextFrom() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function grabTextFrom($cssOrXPathOrRegex) { + $this->scenario->action('grabTextFrom', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Finds and returns field and returns it's value. + * Searches by field name, then by CSS, then by XPath + * + * Example: + * + * ``` php + * <?php + * $name = $I->grabValueFrom('Name'); + * $name = $I->grabValueFrom('input[name=username]'); + * $name = $I->grabValueFrom('descendant-or-self::form/descendant::input[@name = 'username']'); + * ?> + * ``` + * + * @param $field + * @return mixed + * @see Framework::grabValueFrom() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function grabValueFrom($field) { + $this->scenario->action('grabValueFrom', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if element exists on a page, matching it by CSS or XPath + * + * ``` php + * <?php + * $I->seeElement('.error'); + * $I->seeElement(//form/input[1]); + * ?> + * ``` + * @param $selector + * @see Framework::seeElement() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeElement($selector) { + $this->scenario->assertion('seeElement', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if element does not exist (or is visible) on a page, matching it by CSS or XPath + * + * ``` php + * <?php + * $I->dontSeeElement('.error'); + * $I->dontSeeElement(//form/input[1]); + * ?> + * ``` + * @param $selector + * @see Framework::dontSeeElement() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeElement($selector) { + $this->scenario->action('dontSeeElement', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if option is selected in select field. + * + * ``` php + * <?php + * $I->seeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * @return mixed + * @see Framework::seeOptionIsSelected() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeOptionIsSelected($select, $optionText) { + $this->scenario->assertion('seeOptionIsSelected', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if option is not selected in select field. + * + * ``` php + * <?php + * $I->dontSeeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * @return mixed + * @see Framework::dontSeeOptionIsSelected() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeOptionIsSelected($select, $optionText) { + $this->scenario->action('dontSeeOptionIsSelected', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Asserts that current page has 404 response status code. + * @see Framework::seePageNotFound() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seePageNotFound() { + $this->scenario->assertion('seePageNotFound', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that response code is equal to value provided. + * + * @param $code + * @return mixed + * @see Framework::seeResponseCodeIs() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeResponseCodeIs($code) { + $this->scenario->assertion('seeResponseCodeIs', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } +} + diff --git a/apps/basic/tests/functional/_bootstrap.php b/apps/basic/tests/functional/_bootstrap.php new file mode 100644 index 0000000..7dfa7c3 --- /dev/null +++ b/apps/basic/tests/functional/_bootstrap.php @@ -0,0 +1,2 @@ +<?php +// Here you can initialize variables that will for your tests diff --git a/apps/basic/tests/unit.suite.dist.yml b/apps/basic/tests/unit.suite.dist.yml new file mode 100644 index 0000000..04873a8 --- /dev/null +++ b/apps/basic/tests/unit.suite.dist.yml @@ -0,0 +1,8 @@ +# Codeception Test Suite Configuration + +# suite for unit (internal) tests. +# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. + +class_name: CodeGuy +modules: + enabled: [CodeHelper] diff --git a/apps/basic/tests/unit/CodeGuy.php b/apps/basic/tests/unit/CodeGuy.php new file mode 100644 index 0000000..adcd618 --- /dev/null +++ b/apps/basic/tests/unit/CodeGuy.php @@ -0,0 +1,27 @@ +<?php +// This class was automatically generated by build task +// You can change it manually, but it will be overwritten on next build +// @codingStandardsIgnoreFile + +use Codeception\Maybe; +use Codeception\Module\CodeHelper; + +/** + * Inherited methods + * @method void wantToTest($text) + * @method void wantTo($text) + * @method void amTesting($method) + * @method void amTestingMethod($method) + * @method void testMethod($signature) + * @method void expectTo($prediction) + * @method void expect($prediction) + * @method void amGoingTo($argumentation) + * @method void am($role) + * @method void lookForwardTo($role) +*/ + +class CodeGuy extends \Codeception\AbstractGuy +{ + +} + diff --git a/apps/basic/tests/unit/_bootstrap.php b/apps/basic/tests/unit/_bootstrap.php new file mode 100644 index 0000000..7dfa7c3 --- /dev/null +++ b/apps/basic/tests/unit/_bootstrap.php @@ -0,0 +1,2 @@ +<?php +// Here you can initialize variables that will for your tests diff --git a/apps/basic/vendor/.gitignore b/apps/basic/vendor/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/apps/basic/vendor/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/apps/bootstrap/protected/views/layouts/main.php b/apps/basic/views/layouts/main.php similarity index 57% rename from apps/bootstrap/protected/views/layouts/main.php rename to apps/basic/views/layouts/main.php index a81f983..1b7083d 100644 --- a/apps/bootstrap/protected/views/layouts/main.php +++ b/apps/basic/views/layouts/main.php @@ -1,64 +1,64 @@ <?php use yii\helpers\Html; -use yii\widgets\Menu; +use yii\bootstrap\Nav; +use yii\bootstrap\NavBar; +use yii\widgets\Breadcrumbs; /** * @var $this \yii\base\View * @var $content string */ -$this->registerAssetBundle('app'); +app\config\AppAsset::register($this); ?> <?php $this->beginPage(); ?> <!DOCTYPE html> <html lang="en"> <head> - <meta charset="utf-8"/> + <meta charset="<?php echo Yii::$app->charset; ?>"/> <title><?php echo Html::encode($this->title); ?></title> <?php $this->head(); ?> </head> <body> -<div class="container"> - <?php $this->beginBody(); ?> - <div class="masthead"> - <h3 class="muted">My Company</h3> +<?php $this->beginBody(); ?> + <?php + NavBar::begin(array( + 'brandLabel' => 'My Company', + 'brandUrl' => Yii::$app->homeUrl, + 'options' => array( + 'class' => 'navbar-inverse navbar-fixed-top', + ), + )); + echo Nav::widget(array( + 'options' => array('class' => 'navbar-nav pull-right'), + 'items' => array( + array('label' => 'Home', 'url' => array('/site/index')), + array('label' => 'About', 'url' => array('/site/about')), + array('label' => 'Contact', 'url' => array('/site/contact')), + Yii::$app->user->isGuest ? + array('label' => 'Login', 'url' => array('/site/login')) : + array('label' => 'Logout (' . Yii::$app->user->identity->username .')' , + 'url' => array('/site/logout'), + 'linkOptions' => array('data-method' => 'post')), + ), + )); + NavBar::end(); + ?> - <div class="navbar"> - <div class="navbar-inner"> - <div class="container"> - <?php $this->widget(Menu::className(), array( - 'options' => array('class' => 'nav'), - 'items' => array( - array('label' => 'Home', 'url' => array('/site/index')), - array('label' => 'About', 'url' => array('/site/about')), - array('label' => 'Contact', 'url' => array('/site/contact')), - Yii::$app->user->isGuest ? - array('label' => 'Login', 'url' => array('/site/login')) : - array('label' => 'Logout (' . Yii::$app->user->identity->username .')' , 'url' => array('/site/logout')), - ), - )); ?> - </div> - </div> - </div> - <!-- /.navbar --> + <div class="container"> + <?php echo Breadcrumbs::widget(array( + 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : array(), + )); ?> + <?php echo $content; ?> </div> - <?php $this->widget('yii\widgets\Breadcrumbs', array( - 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : array(), - )); ?> - <?php echo $content; ?> - - <hr> + <footer class="footer"> + <div class="container"> + <p class="pull-left">© My Company <?php echo date('Y'); ?></p> + <p class="pull-right"><?php echo Yii::powered(); ?></p> + </div> + </footer> - <div class="footer"> - <p>© My Company <?php echo date('Y'); ?></p> - <p> - <?php echo Yii::powered(); ?> - Template by <a href="http://twitter.github.io/bootstrap/">Twitter Bootstrap</a> - </p> - </div> - <?php $this->endBody(); ?> -</div> -<?php $this->widget('yii\debug\Toolbar'); ?> +<?php $this->endBody(); ?> </body> </html> <?php $this->endPage(); ?> diff --git a/apps/bootstrap/protected/views/site/about.php b/apps/basic/views/site/about.php similarity index 93% rename from apps/bootstrap/protected/views/site/about.php rename to apps/basic/views/site/about.php index 86e19e1..967b8fe 100644 --- a/apps/bootstrap/protected/views/site/about.php +++ b/apps/basic/views/site/about.php @@ -1,16 +1,18 @@ <?php use yii\helpers\Html; + /** * @var yii\base\View $this */ $this->title = 'About'; $this->params['breadcrumbs'][] = $this->title; ?> -<h1><?php echo Html::encode($this->title); ?></h1> - -<p> - This is the About page. You may modify the following file to customize its content: -</p> +<div class="site-about"> + <h1><?php echo Html::encode($this->title); ?></h1> -<code><?php echo __FILE__; ?></code> + <p> + This is the About page. You may modify the following file to customize its content: + </p> + <code><?php echo __FILE__; ?></code> +</div> diff --git a/apps/basic/views/site/contact.php b/apps/basic/views/site/contact.php new file mode 100644 index 0000000..d1411c0 --- /dev/null +++ b/apps/basic/views/site/contact.php @@ -0,0 +1,48 @@ +<?php +use yii\helpers\Html; +use yii\widgets\ActiveForm; +use yii\captcha\Captcha; + +/** + * @var yii\base\View $this + * @var yii\widgets\ActiveForm $form + * @var app\models\ContactForm $model + */ +$this->title = 'Contact'; +$this->params['breadcrumbs'][] = $this->title; +?> +<div class="site-contact"> + <h1><?php echo Html::encode($this->title); ?></h1> + + <?php if (Yii::$app->session->hasFlash('contactFormSubmitted')): ?> + + <div class="alert alert-success"> + Thank you for contacting us. We will respond to you as soon as possible. + </div> + + <?php else: ?> + + <p> + If you have business inquiries or other questions, please fill out the following form to contact us. Thank you. + </p> + + <div class="row"> + <div class="col-lg-5"> + <?php $form = ActiveForm::begin(array('id' => 'contact-form')); ?> + <?php echo $form->field($model, 'name'); ?> + <?php echo $form->field($model, 'email'); ?> + <?php echo $form->field($model, 'subject'); ?> + <?php echo $form->field($model, 'body')->textArea(array('rows' => 6)); ?> + <?php echo $form->field($model, 'verifyCode')->widget(Captcha::className(), array( + 'options' => array('class' => 'form-control'), + 'template' => '<div class="row"><div class="col-lg-3">{image}</div><div class="col-lg-6">{input}</div></div>', + )); ?> + <div class="form-group"> + <?php echo Html::submitButton('Submit', array('class' => 'btn btn-primary')); ?> + </div> + <?php ActiveForm::end(); ?> + </div> + </div> + + <?php endif; ?> +</div> diff --git a/apps/basic/views/site/error.php b/apps/basic/views/site/error.php new file mode 100644 index 0000000..024e27d --- /dev/null +++ b/apps/basic/views/site/error.php @@ -0,0 +1,29 @@ +<?php + +use yii\helpers\Html; + +/** + * @var yii\base\View $this + * @var string $name + * @var string $message + * @var Exception $exception + */ + +$this->title = $name; +?> +<div class="site-error"> + + <h1><?php echo Html::encode($this->title); ?></h1> + + <div class="alert alert-danger"> + <?php echo nl2br(Html::encode($message)); ?> + </div> + + <p> + The above error occurred while the Web server was processing your request. + </p> + <p> + Please contact us if you think this is a server error. Thank you. + </p> + +</div> diff --git a/apps/basic/views/site/index.php b/apps/basic/views/site/index.php new file mode 100644 index 0000000..f2e6d5e --- /dev/null +++ b/apps/basic/views/site/index.php @@ -0,0 +1,53 @@ +<?php +/** + * @var yii\base\View $this + */ +$this->title = 'My Yii Application'; +?> +<div class="site-index"> + + <div class="jumbotron"> + <h1>Congratulations!</h1> + + <p class="lead">You have successfully created your Yii-powered application.</p> + + <p><a class="btn btn-lg btn-success" href="http://www.yiiframework.com">Get started with Yii</a></p> + </div> + + <div class="body-content"> + + <div class="row"> + <div class="col-lg-4"> + <h2>Heading</h2> + + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.</p> + + <p><a class="btn btn-default" href="http://www.yiiframework.com/doc/">Yii Documentation »</a></p> + </div> + <div class="col-lg-4"> + <h2>Heading</h2> + + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.</p> + + <p><a class="btn btn-default" href="http://www.yiiframework.com/forum/">Yii Forum »</a></p> + </div> + <div class="col-lg-4"> + <h2>Heading</h2> + + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.</p> + + <p><a class="btn btn-default" href="http://www.yiiframework.com/extensions/">Yii Extensions »</a></p> + </div> + </div> + + </div> +</div> diff --git a/apps/basic/views/site/login.php b/apps/basic/views/site/login.php new file mode 100644 index 0000000..f61d9d7 --- /dev/null +++ b/apps/basic/views/site/login.php @@ -0,0 +1,47 @@ +<?php +use yii\helpers\Html; +use yii\widgets\ActiveForm; + +/** + * @var yii\base\View $this + * @var yii\widgets\ActiveForm $form + * @var app\models\LoginForm $model + */ +$this->title = 'Login'; +$this->params['breadcrumbs'][] = $this->title; +?> +<div class="site-login"> + <h1><?php echo Html::encode($this->title); ?></h1> + + <p>Please fill out the following fields to login:</p> + + <?php $form = ActiveForm::begin(array( + 'id' => 'login-form', + 'options' => array('class' => 'form-horizontal'), + 'fieldConfig' => array( + 'template' => "{label}\n<div class=\"col-lg-3\">{input}</div>\n<div class=\"col-lg-8\">{error}</div>", + 'labelOptions' => array('class' => 'col-lg-1 control-label'), + ), + )); ?> + + <?php echo $form->field($model, 'username'); ?> + + <?php echo $form->field($model, 'password')->passwordInput(); ?> + + <?php echo $form->field($model, 'rememberMe', array( + 'template' => "<div class=\"col-lg-offset-1 col-lg-3\">{input}</div>\n<div class=\"col-lg-8\">{error}</div>", + ))->checkbox(); ?> + + <div class="form-group"> + <div class="col-lg-offset-1 col-lg-11"> + <?php echo Html::submitButton('Login', array('class' => 'btn btn-primary')); ?> + </div> + </div> + + <?php ActiveForm::end(); ?> + + <div class="col-lg-offset-1" style="color:#999;"> + You may login with <strong>admin/admin</strong> or <strong>demo/demo</strong>.<br> + To modify the username/password, please check out the code <code>app\models\User::$users</code>. + </div> +</div> diff --git a/apps/basic/web/assets/.gitignore b/apps/basic/web/assets/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/apps/basic/web/assets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/apps/basic/web/css/site.css b/apps/basic/web/css/site.css new file mode 100644 index 0000000..84c46e8 --- /dev/null +++ b/apps/basic/web/css/site.css @@ -0,0 +1,20 @@ +body { + padding-top: 70px; +} + +.footer { + border-top: 1px solid #ddd; + margin-top: 30px; + padding-top: 15px; + padding-bottom: 30px; +} + +.jumbotron { + text-align: center; + background-color: transparent; +} + +.jumbotron .btn { + font-size: 21px; + padding: 14px 24px; +} diff --git a/apps/basic/web/index-test.php b/apps/basic/web/index-test.php new file mode 100644 index 0000000..79273ae --- /dev/null +++ b/apps/basic/web/index-test.php @@ -0,0 +1,18 @@ +<?php + +if (!in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1'))) { + die('You are not allowed to access this file.'); +} + +defined('YII_DEBUG') or define('YII_DEBUG', true); + +defined('YII_ENV') or define('YII_ENV', 'test'); + +require_once(__DIR__ . '/../vendor/autoload.php'); +require_once(__DIR__ . '/../vendor/yiisoft/yii2/yii/Yii.php'); +Yii::importNamespaces(require(__DIR__ . '/../vendor/composer/autoload_namespaces.php')); + +$config = require(__DIR__ . '/../config/web-test.php'); + +$application = new yii\web\Application($config); +$application->run(); diff --git a/apps/basic/web/index.php b/apps/basic/web/index.php new file mode 100644 index 0000000..51673d3 --- /dev/null +++ b/apps/basic/web/index.php @@ -0,0 +1,14 @@ +<?php + +// comment out the following two lines when deployed to production +defined('YII_DEBUG') or define('YII_DEBUG', true); +defined('YII_ENV') or define('YII_ENV', 'dev'); + +require(__DIR__ . '/../vendor/autoload.php'); +require(__DIR__ . '/../vendor/yiisoft/yii2/yii/Yii.php'); +Yii::importNamespaces(require(__DIR__ . '/../vendor/composer/autoload_namespaces.php')); + +$config = require(__DIR__ . '/../config/web.php'); + +$application = new yii\web\Application($config); +$application->run(); diff --git a/yii/yiic b/apps/basic/yii similarity index 50% rename from yii/yiic rename to apps/basic/yii index d35d262..cd979ca 100755 --- a/yii/yiic +++ b/apps/basic/yii @@ -1,13 +1,24 @@ #!/usr/bin/env php <?php /** - * Yii command line script for Unix/Linux. - * - * This is the bootstrap script for running yiic on Unix/Linux. + * Yii console bootstrap file. * * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ -require_once(__DIR__ . '/yiic.php'); +defined('YII_DEBUG') or define('YII_DEBUG', true); + +// fcgi doesn't have STDIN defined by default +defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); + +require(__DIR__ . '/vendor/autoload.php'); +require(__DIR__ . '/vendor/yiisoft/yii2/yii/Yii.php'); +Yii::importNamespaces(require(__DIR__ . '/vendor/composer/autoload_namespaces.php')); + +$config = require(__DIR__ . '/config/console.php'); + +$application = new yii\console\Application($config); +$exitCode = $application->run(); +exit($exitCode); diff --git a/apps/basic/yii.bat b/apps/basic/yii.bat new file mode 100644 index 0000000..5e21e2e --- /dev/null +++ b/apps/basic/yii.bat @@ -0,0 +1,20 @@ +@echo off + +rem ------------------------------------------------------------- +rem Yii command line bootstrap script for Windows. +rem +rem @author Qiang Xue <qiang.xue@gmail.com> +rem @link http://www.yiiframework.com/ +rem @copyright Copyright © 2012 Yii Software LLC +rem @license http://www.yiiframework.com/license/ +rem ------------------------------------------------------------- + +@setlocal + +set YII_PATH=%~dp0 + +if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe + +"%PHP_COMMAND%" "%YII_PATH%yii" %* + +@endlocal diff --git a/apps/benchmark/.gitignore b/apps/benchmark/.gitignore new file mode 100644 index 0000000..9dce4ff --- /dev/null +++ b/apps/benchmark/.gitignore @@ -0,0 +1 @@ +/composer.lock \ No newline at end of file diff --git a/apps/benchmark/LICENSE.md b/apps/benchmark/LICENSE.md new file mode 100644 index 0000000..6edcc4f --- /dev/null +++ b/apps/benchmark/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md new file mode 100644 index 0000000..2d5871a --- /dev/null +++ b/apps/benchmark/README.md @@ -0,0 +1,58 @@ +Yii 2 Benchmark Application +=========================== + +**NOTE** Yii 2 and the relevant applications and extensions are still under heavy +development. We may make significant changes without prior notices. Please do not +use them for production. Please consider using [Yii v1.1](https://github.com/yiisoft/yii) +if you have a project to be deployed for production soon. + + +Yii 2 Benchmark Application is an application built to demonstrate the minimal overhead +introduced by the Yii framework. The application contains a single page which only renders +the "hello world" string. + +The application attempts to simulate the scenario in which you can achieve the best performance +when using Yii. It does so by assuming that both of the main application configuration and the page +content are cached in memory, and the application enables pretty URLs. + + +DIRECTORY STRUCTURE +------------------- + + protected/ contains application source code + controllers/ contains Web controller classes + index.php the entry script + + +REQUIREMENTS +------------ + +The minimum requirement by Yii is that your Web server supports PHP 5.3.?. + + +INSTALLATION +------------ + +If you do not have [Composer](http://getcomposer.org/), you may download it from +[http://getcomposer.org/](http://getcomposer.org/) or run the following command on Linux/Unix/MacOS: + +~~~ +curl -s http://getcomposer.org/installer | php +~~~ + +You can then install the Bootstrap Application using the following command: + +~~~ +php composer.phar create-project --stability=dev yiisoft/yii2-app-benchmark yii-benchmark +~~~ + +Now you should be able to access the benchmark page using the URL + +~~~ +http://localhost/yii-benchmark/index.php/site/hello +~~~ + +In the above, we assume `yii-benchmark` is directly under the document root of your Web server. + +Note that in order to install some dependencies you must have `php_openssl` extension enabled. + diff --git a/apps/benchmark/composer.json b/apps/benchmark/composer.json new file mode 100644 index 0000000..2b077e0 --- /dev/null +++ b/apps/benchmark/composer.json @@ -0,0 +1,23 @@ +{ + "name": "yiisoft/yii2-app-benchmark", + "description": "Yii 2 Benchmark Application", + "keywords": ["yii", "framework", "benchmark", "application"], + "homepage": "http://www.yiiframework.com/", + "type": "project", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "config": { + "vendor-dir": "protected/vendor" + }, + "minimum-stability": "dev", + "require": { + "php": ">=5.3.0", + "yiisoft/yii2": "dev-master" + } +} diff --git a/apps/benchmark/index.php b/apps/benchmark/index.php new file mode 100644 index 0000000..ddf6081 --- /dev/null +++ b/apps/benchmark/index.php @@ -0,0 +1,18 @@ +<?php + +defined('YII_DEBUG') or define('YII_DEBUG', false); + +require(__DIR__ . '/protected/vendor/yiisoft/yii2/yii/Yii.php'); + +$config = array( + 'id' => 'benchmark', + 'basePath' => __DIR__ . '/protected', + 'components' => array( + 'urlManager' => array( + 'enablePrettyUrl' => true, + ), + ) +); + +$application = new yii\web\Application($config); +$application->run(); diff --git a/apps/bootstrap/protected/.htaccess b/apps/benchmark/protected/.htaccess similarity index 100% rename from apps/bootstrap/protected/.htaccess rename to apps/benchmark/protected/.htaccess index e019832..8d2f256 100644 --- a/apps/bootstrap/protected/.htaccess +++ b/apps/benchmark/protected/.htaccess @@ -1 +1 @@ -deny from all +deny from all diff --git a/apps/benchmark/protected/controllers/SiteController.php b/apps/benchmark/protected/controllers/SiteController.php new file mode 100644 index 0000000..9abc164 --- /dev/null +++ b/apps/benchmark/protected/controllers/SiteController.php @@ -0,0 +1,15 @@ +<?php + +namespace app\controllers; + +use yii\web\Controller; + +class SiteController extends Controller +{ + public $defaultAction = 'hello'; + + public function actionHello() + { + return 'hello world'; + } +} diff --git a/apps/benchmark/protected/vendor/.gitignore b/apps/benchmark/protected/vendor/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/apps/benchmark/protected/vendor/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/apps/bootstrap/css/bootstrap-responsive.css b/apps/bootstrap/css/bootstrap-responsive.css deleted file mode 100644 index fcd72f7..0000000 --- a/apps/bootstrap/css/bootstrap-responsive.css +++ /dev/null @@ -1,1109 +0,0 @@ -/*! - * Bootstrap Responsive v2.3.1 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */ - -.clearfix { - *zoom: 1; -} - -.clearfix:before, -.clearfix:after { - display: table; - line-height: 0; - content: ""; -} - -.clearfix:after { - clear: both; -} - -.hide-text { - font: 0/0 a; - color: transparent; - text-shadow: none; - background-color: transparent; - border: 0; -} - -.input-block-level { - display: block; - width: 100%; - min-height: 30px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -@-ms-viewport { - width: device-width; -} - -.hidden { - display: none; - visibility: hidden; -} - -.visible-phone { - display: none !important; -} - -.visible-tablet { - display: none !important; -} - -.hidden-desktop { - display: none !important; -} - -.visible-desktop { - display: inherit !important; -} - -@media (min-width: 768px) and (max-width: 979px) { - .hidden-desktop { - display: inherit !important; - } - .visible-desktop { - display: none !important ; - } - .visible-tablet { - display: inherit !important; - } - .hidden-tablet { - display: none !important; - } -} - -@media (max-width: 767px) { - .hidden-desktop { - display: inherit !important; - } - .visible-desktop { - display: none !important; - } - .visible-phone { - display: inherit !important; - } - .hidden-phone { - display: none !important; - } -} - -.visible-print { - display: none !important; -} - -@media print { - .visible-print { - display: inherit !important; - } - .hidden-print { - display: none !important; - } -} - -@media (min-width: 1200px) { - .row { - margin-left: -30px; - *zoom: 1; - } - .row:before, - .row:after { - display: table; - line-height: 0; - content: ""; - } - .row:after { - clear: both; - } - [class*="span"] { - float: left; - min-height: 1px; - margin-left: 30px; - } - .container, - .navbar-static-top .container, - .navbar-fixed-top .container, - .navbar-fixed-bottom .container { - width: 1170px; - } - .span12 { - width: 1170px; - } - .span11 { - width: 1070px; - } - .span10 { - width: 970px; - } - .span9 { - width: 870px; - } - .span8 { - width: 770px; - } - .span7 { - width: 670px; - } - .span6 { - width: 570px; - } - .span5 { - width: 470px; - } - .span4 { - width: 370px; - } - .span3 { - width: 270px; - } - .span2 { - width: 170px; - } - .span1 { - width: 70px; - } - .offset12 { - margin-left: 1230px; - } - .offset11 { - margin-left: 1130px; - } - .offset10 { - margin-left: 1030px; - } - .offset9 { - margin-left: 930px; - } - .offset8 { - margin-left: 830px; - } - .offset7 { - margin-left: 730px; - } - .offset6 { - margin-left: 630px; - } - .offset5 { - margin-left: 530px; - } - .offset4 { - margin-left: 430px; - } - .offset3 { - margin-left: 330px; - } - .offset2 { - margin-left: 230px; - } - .offset1 { - margin-left: 130px; - } - .row-fluid { - width: 100%; - *zoom: 1; - } - .row-fluid:before, - .row-fluid:after { - display: table; - line-height: 0; - content: ""; - } - .row-fluid:after { - clear: both; - } - .row-fluid [class*="span"] { - display: block; - float: left; - width: 100%; - min-height: 30px; - margin-left: 2.564102564102564%; - *margin-left: 2.5109110747408616%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - } - .row-fluid [class*="span"]:first-child { - margin-left: 0; - } - .row-fluid .controls-row [class*="span"] + [class*="span"] { - margin-left: 2.564102564102564%; - } - .row-fluid .span12 { - width: 100%; - *width: 99.94680851063829%; - } - .row-fluid .span11 { - width: 91.45299145299145%; - *width: 91.39979996362975%; - } - .row-fluid .span10 { - width: 82.90598290598291%; - *width: 82.8527914166212%; - } - .row-fluid .span9 { - width: 74.35897435897436%; - *width: 74.30578286961266%; - } - .row-fluid .span8 { - width: 65.81196581196582%; - *width: 65.75877432260411%; - } - .row-fluid .span7 { - width: 57.26495726495726%; - *width: 57.21176577559556%; - } - .row-fluid .span6 { - width: 48.717948717948715%; - *width: 48.664757228587014%; - } - .row-fluid .span5 { - width: 40.17094017094017%; - *width: 40.11774868157847%; - } - .row-fluid .span4 { - width: 31.623931623931625%; - *width: 31.570740134569924%; - } - .row-fluid .span3 { - width: 23.076923076923077%; - *width: 23.023731587561375%; - } - .row-fluid .span2 { - width: 14.52991452991453%; - *width: 14.476723040552828%; - } - .row-fluid .span1 { - width: 5.982905982905983%; - *width: 5.929714493544281%; - } - .row-fluid .offset12 { - margin-left: 105.12820512820512%; - *margin-left: 105.02182214948171%; - } - .row-fluid .offset12:first-child { - margin-left: 102.56410256410257%; - *margin-left: 102.45771958537915%; - } - .row-fluid .offset11 { - margin-left: 96.58119658119658%; - *margin-left: 96.47481360247316%; - } - .row-fluid .offset11:first-child { - margin-left: 94.01709401709402%; - *margin-left: 93.91071103837061%; - } - .row-fluid .offset10 { - margin-left: 88.03418803418803%; - *margin-left: 87.92780505546462%; - } - .row-fluid .offset10:first-child { - margin-left: 85.47008547008548%; - *margin-left: 85.36370249136206%; - } - .row-fluid .offset9 { - margin-left: 79.48717948717949%; - *margin-left: 79.38079650845607%; - } - .row-fluid .offset9:first-child { - margin-left: 76.92307692307693%; - *margin-left: 76.81669394435352%; - } - .row-fluid .offset8 { - margin-left: 70.94017094017094%; - *margin-left: 70.83378796144753%; - } - .row-fluid .offset8:first-child { - margin-left: 68.37606837606839%; - *margin-left: 68.26968539734497%; - } - .row-fluid .offset7 { - margin-left: 62.393162393162385%; - *margin-left: 62.28677941443899%; - } - .row-fluid .offset7:first-child { - margin-left: 59.82905982905982%; - *margin-left: 59.72267685033642%; - } - .row-fluid .offset6 { - margin-left: 53.84615384615384%; - *margin-left: 53.739770867430444%; - } - .row-fluid .offset6:first-child { - margin-left: 51.28205128205128%; - *margin-left: 51.175668303327875%; - } - .row-fluid .offset5 { - margin-left: 45.299145299145295%; - *margin-left: 45.1927623204219%; - } - .row-fluid .offset5:first-child { - margin-left: 42.73504273504273%; - *margin-left: 42.62865975631933%; - } - .row-fluid .offset4 { - margin-left: 36.75213675213675%; - *margin-left: 36.645753773413354%; - } - .row-fluid .offset4:first-child { - margin-left: 34.18803418803419%; - *margin-left: 34.081651209310785%; - } - .row-fluid .offset3 { - margin-left: 28.205128205128204%; - *margin-left: 28.0987452264048%; - } - .row-fluid .offset3:first-child { - margin-left: 25.641025641025642%; - *margin-left: 25.53464266230224%; - } - .row-fluid .offset2 { - margin-left: 19.65811965811966%; - *margin-left: 19.551736679396257%; - } - .row-fluid .offset2:first-child { - margin-left: 17.094017094017094%; - *margin-left: 16.98763411529369%; - } - .row-fluid .offset1 { - margin-left: 11.11111111111111%; - *margin-left: 11.004728132387708%; - } - .row-fluid .offset1:first-child { - margin-left: 8.547008547008547%; - *margin-left: 8.440625568285142%; - } - input, - textarea, - .uneditable-input { - margin-left: 0; - } - .controls-row [class*="span"] + [class*="span"] { - margin-left: 30px; - } - input.span12, - textarea.span12, - .uneditable-input.span12 { - width: 1156px; - } - input.span11, - textarea.span11, - .uneditable-input.span11 { - width: 1056px; - } - input.span10, - textarea.span10, - .uneditable-input.span10 { - width: 956px; - } - input.span9, - textarea.span9, - .uneditable-input.span9 { - width: 856px; - } - input.span8, - textarea.span8, - .uneditable-input.span8 { - width: 756px; - } - input.span7, - textarea.span7, - .uneditable-input.span7 { - width: 656px; - } - input.span6, - textarea.span6, - .uneditable-input.span6 { - width: 556px; - } - input.span5, - textarea.span5, - .uneditable-input.span5 { - width: 456px; - } - input.span4, - textarea.span4, - .uneditable-input.span4 { - width: 356px; - } - input.span3, - textarea.span3, - .uneditable-input.span3 { - width: 256px; - } - input.span2, - textarea.span2, - .uneditable-input.span2 { - width: 156px; - } - input.span1, - textarea.span1, - .uneditable-input.span1 { - width: 56px; - } - .thumbnails { - margin-left: -30px; - } - .thumbnails > li { - margin-left: 30px; - } - .row-fluid .thumbnails { - margin-left: 0; - } -} - -@media (min-width: 768px) and (max-width: 979px) { - .row { - margin-left: -20px; - *zoom: 1; - } - .row:before, - .row:after { - display: table; - line-height: 0; - content: ""; - } - .row:after { - clear: both; - } - [class*="span"] { - float: left; - min-height: 1px; - margin-left: 20px; - } - .container, - .navbar-static-top .container, - .navbar-fixed-top .container, - .navbar-fixed-bottom .container { - width: 724px; - } - .span12 { - width: 724px; - } - .span11 { - width: 662px; - } - .span10 { - width: 600px; - } - .span9 { - width: 538px; - } - .span8 { - width: 476px; - } - .span7 { - width: 414px; - } - .span6 { - width: 352px; - } - .span5 { - width: 290px; - } - .span4 { - width: 228px; - } - .span3 { - width: 166px; - } - .span2 { - width: 104px; - } - .span1 { - width: 42px; - } - .offset12 { - margin-left: 764px; - } - .offset11 { - margin-left: 702px; - } - .offset10 { - margin-left: 640px; - } - .offset9 { - margin-left: 578px; - } - .offset8 { - margin-left: 516px; - } - .offset7 { - margin-left: 454px; - } - .offset6 { - margin-left: 392px; - } - .offset5 { - margin-left: 330px; - } - .offset4 { - margin-left: 268px; - } - .offset3 { - margin-left: 206px; - } - .offset2 { - margin-left: 144px; - } - .offset1 { - margin-left: 82px; - } - .row-fluid { - width: 100%; - *zoom: 1; - } - .row-fluid:before, - .row-fluid:after { - display: table; - line-height: 0; - content: ""; - } - .row-fluid:after { - clear: both; - } - .row-fluid [class*="span"] { - display: block; - float: left; - width: 100%; - min-height: 30px; - margin-left: 2.7624309392265194%; - *margin-left: 2.709239449864817%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - } - .row-fluid [class*="span"]:first-child { - margin-left: 0; - } - .row-fluid .controls-row [class*="span"] + [class*="span"] { - margin-left: 2.7624309392265194%; - } - .row-fluid .span12 { - width: 100%; - *width: 99.94680851063829%; - } - .row-fluid .span11 { - width: 91.43646408839778%; - *width: 91.38327259903608%; - } - .row-fluid .span10 { - width: 82.87292817679558%; - *width: 82.81973668743387%; - } - .row-fluid .span9 { - width: 74.30939226519337%; - *width: 74.25620077583166%; - } - .row-fluid .span8 { - width: 65.74585635359117%; - *width: 65.69266486422946%; - } - .row-fluid .span7 { - width: 57.18232044198895%; - *width: 57.12912895262725%; - } - .row-fluid .span6 { - width: 48.61878453038674%; - *width: 48.56559304102504%; - } - .row-fluid .span5 { - width: 40.05524861878453%; - *width: 40.00205712942283%; - } - .row-fluid .span4 { - width: 31.491712707182323%; - *width: 31.43852121782062%; - } - .row-fluid .span3 { - width: 22.92817679558011%; - *width: 22.87498530621841%; - } - .row-fluid .span2 { - width: 14.3646408839779%; - *width: 14.311449394616199%; - } - .row-fluid .span1 { - width: 5.801104972375691%; - *width: 5.747913483013988%; - } - .row-fluid .offset12 { - margin-left: 105.52486187845304%; - *margin-left: 105.41847889972962%; - } - .row-fluid .offset12:first-child { - margin-left: 102.76243093922652%; - *margin-left: 102.6560479605031%; - } - .row-fluid .offset11 { - margin-left: 96.96132596685082%; - *margin-left: 96.8549429881274%; - } - .row-fluid .offset11:first-child { - margin-left: 94.1988950276243%; - *margin-left: 94.09251204890089%; - } - .row-fluid .offset10 { - margin-left: 88.39779005524862%; - *margin-left: 88.2914070765252%; - } - .row-fluid .offset10:first-child { - margin-left: 85.6353591160221%; - *margin-left: 85.52897613729868%; - } - .row-fluid .offset9 { - margin-left: 79.8342541436464%; - *margin-left: 79.72787116492299%; - } - .row-fluid .offset9:first-child { - margin-left: 77.07182320441989%; - *margin-left: 76.96544022569647%; - } - .row-fluid .offset8 { - margin-left: 71.2707182320442%; - *margin-left: 71.16433525332079%; - } - .row-fluid .offset8:first-child { - margin-left: 68.50828729281768%; - *margin-left: 68.40190431409427%; - } - .row-fluid .offset7 { - margin-left: 62.70718232044199%; - *margin-left: 62.600799341718584%; - } - .row-fluid .offset7:first-child { - margin-left: 59.94475138121547%; - *margin-left: 59.838368402492065%; - } - .row-fluid .offset6 { - margin-left: 54.14364640883978%; - *margin-left: 54.037263430116376%; - } - .row-fluid .offset6:first-child { - margin-left: 51.38121546961326%; - *margin-left: 51.27483249088986%; - } - .row-fluid .offset5 { - margin-left: 45.58011049723757%; - *margin-left: 45.47372751851417%; - } - .row-fluid .offset5:first-child { - margin-left: 42.81767955801105%; - *margin-left: 42.71129657928765%; - } - .row-fluid .offset4 { - margin-left: 37.01657458563536%; - *margin-left: 36.91019160691196%; - } - .row-fluid .offset4:first-child { - margin-left: 34.25414364640884%; - *margin-left: 34.14776066768544%; - } - .row-fluid .offset3 { - margin-left: 28.45303867403315%; - *margin-left: 28.346655695309746%; - } - .row-fluid .offset3:first-child { - margin-left: 25.69060773480663%; - *margin-left: 25.584224756083227%; - } - .row-fluid .offset2 { - margin-left: 19.88950276243094%; - *margin-left: 19.783119783707537%; - } - .row-fluid .offset2:first-child { - margin-left: 17.12707182320442%; - *margin-left: 17.02068884448102%; - } - .row-fluid .offset1 { - margin-left: 11.32596685082873%; - *margin-left: 11.219583872105325%; - } - .row-fluid .offset1:first-child { - margin-left: 8.56353591160221%; - *margin-left: 8.457152932878806%; - } - input, - textarea, - .uneditable-input { - margin-left: 0; - } - .controls-row [class*="span"] + [class*="span"] { - margin-left: 20px; - } - input.span12, - textarea.span12, - .uneditable-input.span12 { - width: 710px; - } - input.span11, - textarea.span11, - .uneditable-input.span11 { - width: 648px; - } - input.span10, - textarea.span10, - .uneditable-input.span10 { - width: 586px; - } - input.span9, - textarea.span9, - .uneditable-input.span9 { - width: 524px; - } - input.span8, - textarea.span8, - .uneditable-input.span8 { - width: 462px; - } - input.span7, - textarea.span7, - .uneditable-input.span7 { - width: 400px; - } - input.span6, - textarea.span6, - .uneditable-input.span6 { - width: 338px; - } - input.span5, - textarea.span5, - .uneditable-input.span5 { - width: 276px; - } - input.span4, - textarea.span4, - .uneditable-input.span4 { - width: 214px; - } - input.span3, - textarea.span3, - .uneditable-input.span3 { - width: 152px; - } - input.span2, - textarea.span2, - .uneditable-input.span2 { - width: 90px; - } - input.span1, - textarea.span1, - .uneditable-input.span1 { - width: 28px; - } -} - -@media (max-width: 767px) { - body { - padding-right: 20px; - padding-left: 20px; - } - .navbar-fixed-top, - .navbar-fixed-bottom, - .navbar-static-top { - margin-right: -20px; - margin-left: -20px; - } - .container-fluid { - padding: 0; - } - .dl-horizontal dt { - float: none; - width: auto; - clear: none; - text-align: left; - } - .dl-horizontal dd { - margin-left: 0; - } - .container { - width: auto; - } - .row-fluid { - width: 100%; - } - .row, - .thumbnails { - margin-left: 0; - } - .thumbnails > li { - float: none; - margin-left: 0; - } - [class*="span"], - .uneditable-input[class*="span"], - .row-fluid [class*="span"] { - display: block; - float: none; - width: 100%; - margin-left: 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - } - .span12, - .row-fluid .span12 { - width: 100%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - } - .row-fluid [class*="offset"]:first-child { - margin-left: 0; - } - .input-large, - .input-xlarge, - .input-xxlarge, - input[class*="span"], - select[class*="span"], - textarea[class*="span"], - .uneditable-input { - display: block; - width: 100%; - min-height: 30px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - } - .input-prepend input, - .input-append input, - .input-prepend input[class*="span"], - .input-append input[class*="span"] { - display: inline-block; - width: auto; - } - .controls-row [class*="span"] + [class*="span"] { - margin-left: 0; - } - .modal { - position: fixed; - top: 20px; - right: 20px; - left: 20px; - width: auto; - margin: 0; - } - .modal.fade { - top: -100px; - } - .modal.fade.in { - top: 20px; - } -} - -@media (max-width: 480px) { - .nav-collapse { - -webkit-transform: translate3d(0, 0, 0); - } - .page-header h1 small { - display: block; - line-height: 20px; - } - input[type="checkbox"], - input[type="radio"] { - border: 1px solid #ccc; - } - .form-horizontal .control-label { - float: none; - width: auto; - padding-top: 0; - text-align: left; - } - .form-horizontal .controls { - margin-left: 0; - } - .form-horizontal .control-list { - padding-top: 0; - } - .form-horizontal .form-actions { - padding-right: 10px; - padding-left: 10px; - } - .media .pull-left, - .media .pull-right { - display: block; - float: none; - margin-bottom: 10px; - } - .media-object { - margin-right: 0; - margin-left: 0; - } - .modal { - top: 10px; - right: 10px; - left: 10px; - } - .modal-header .close { - padding: 10px; - margin: -10px; - } - .carousel-caption { - position: static; - } -} - -@media (max-width: 979px) { - body { - padding-top: 0; - } - .navbar-fixed-top, - .navbar-fixed-bottom { - position: static; - } - .navbar-fixed-top { - margin-bottom: 20px; - } - .navbar-fixed-bottom { - margin-top: 20px; - } - .navbar-fixed-top .navbar-inner, - .navbar-fixed-bottom .navbar-inner { - padding: 5px; - } - .navbar .container { - width: auto; - padding: 0; - } - .navbar .brand { - padding-right: 10px; - padding-left: 10px; - margin: 0 0 0 -5px; - } - .nav-collapse { - clear: both; - } - .nav-collapse .nav { - float: none; - margin: 0 0 10px; - } - .nav-collapse .nav > li { - float: none; - } - .nav-collapse .nav > li > a { - margin-bottom: 2px; - } - .nav-collapse .nav > .divider-vertical { - display: none; - } - .nav-collapse .nav .nav-header { - color: #777777; - text-shadow: none; - } - .nav-collapse .nav > li > a, - .nav-collapse .dropdown-menu a { - padding: 9px 15px; - font-weight: bold; - color: #777777; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - } - .nav-collapse .btn { - padding: 4px 10px 4px; - font-weight: normal; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - } - .nav-collapse .dropdown-menu li + li a { - margin-bottom: 2px; - } - .nav-collapse .nav > li > a:hover, - .nav-collapse .nav > li > a:focus, - .nav-collapse .dropdown-menu a:hover, - .nav-collapse .dropdown-menu a:focus { - background-color: #f2f2f2; - } - .navbar-inverse .nav-collapse .nav > li > a, - .navbar-inverse .nav-collapse .dropdown-menu a { - color: #999999; - } - .navbar-inverse .nav-collapse .nav > li > a:hover, - .navbar-inverse .nav-collapse .nav > li > a:focus, - .navbar-inverse .nav-collapse .dropdown-menu a:hover, - .navbar-inverse .nav-collapse .dropdown-menu a:focus { - background-color: #111111; - } - .nav-collapse.in .btn-group { - padding: 0; - margin-top: 5px; - } - .nav-collapse .dropdown-menu { - position: static; - top: auto; - left: auto; - display: none; - float: none; - max-width: none; - padding: 0; - margin: 0 15px; - background-color: transparent; - border: none; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - } - .nav-collapse .open > .dropdown-menu { - display: block; - } - .nav-collapse .dropdown-menu:before, - .nav-collapse .dropdown-menu:after { - display: none; - } - .nav-collapse .dropdown-menu .divider { - display: none; - } - .nav-collapse .nav > li > .dropdown-menu:before, - .nav-collapse .nav > li > .dropdown-menu:after { - display: none; - } - .nav-collapse .navbar-form, - .nav-collapse .navbar-search { - float: none; - padding: 10px 15px; - margin: 10px 0; - border-top: 1px solid #f2f2f2; - border-bottom: 1px solid #f2f2f2; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); - } - .navbar-inverse .nav-collapse .navbar-form, - .navbar-inverse .nav-collapse .navbar-search { - border-top-color: #111111; - border-bottom-color: #111111; - } - .navbar .nav-collapse .nav.pull-right { - float: none; - margin-left: 0; - } - .nav-collapse, - .nav-collapse.collapse { - height: 0; - overflow: hidden; - } - .navbar .btn-navbar { - display: block; - } - .navbar-static .navbar-inner { - padding-right: 10px; - padding-left: 10px; - } -} - -@media (min-width: 980px) { - .nav-collapse.collapse { - height: auto !important; - overflow: visible !important; - } -} diff --git a/apps/bootstrap/css/bootstrap-responsive.min.css b/apps/bootstrap/css/bootstrap-responsive.min.css deleted file mode 100644 index d1b7f4b..0000000 --- a/apps/bootstrap/css/bootstrap-responsive.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Bootstrap Responsive v2.3.1 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} diff --git a/apps/bootstrap/css/bootstrap.css b/apps/bootstrap/css/bootstrap.css deleted file mode 100644 index 2f56af3..0000000 --- a/apps/bootstrap/css/bootstrap.css +++ /dev/null @@ -1,6158 +0,0 @@ -/*! - * Bootstrap v2.3.1 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */ - -.clearfix { - *zoom: 1; -} - -.clearfix:before, -.clearfix:after { - display: table; - line-height: 0; - content: ""; -} - -.clearfix:after { - clear: both; -} - -.hide-text { - font: 0/0 a; - color: transparent; - text-shadow: none; - background-color: transparent; - border: 0; -} - -.input-block-level { - display: block; - width: 100%; - min-height: 30px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -nav, -section { - display: block; -} - -audio, -canvas, -video { - display: inline-block; - *display: inline; - *zoom: 1; -} - -audio:not([controls]) { - display: none; -} - -html { - font-size: 100%; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; -} - -a:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} - -a:hover, -a:active { - outline: 0; -} - -sub, -sup { - position: relative; - font-size: 75%; - line-height: 0; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -img { - width: auto\9; - height: auto; - max-width: 100%; - vertical-align: middle; - border: 0; - -ms-interpolation-mode: bicubic; -} - -#map_canvas img, -.google-maps img { - max-width: none; -} - -button, -input, -select, -textarea { - margin: 0; - font-size: 100%; - vertical-align: middle; -} - -button, -input { - *overflow: visible; - line-height: normal; -} - -button::-moz-focus-inner, -input::-moz-focus-inner { - padding: 0; - border: 0; -} - -button, -html input[type="button"], -input[type="reset"], -input[type="submit"] { - cursor: pointer; - -webkit-appearance: button; -} - -label, -select, -button, -input[type="button"], -input[type="reset"], -input[type="submit"], -input[type="radio"], -input[type="checkbox"] { - cursor: pointer; -} - -input[type="search"] { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; - -webkit-appearance: textfield; -} - -input[type="search"]::-webkit-search-decoration, -input[type="search"]::-webkit-search-cancel-button { - -webkit-appearance: none; -} - -textarea { - overflow: auto; - vertical-align: top; -} - -@media print { - * { - color: #000 !important; - text-shadow: none !important; - background: transparent !important; - box-shadow: none !important; - } - a, - a:visited { - text-decoration: underline; - } - a[href]:after { - content: " (" attr(href) ")"; - } - abbr[title]:after { - content: " (" attr(title) ")"; - } - .ir a:after, - a[href^="javascript:"]:after, - a[href^="#"]:after { - content: ""; - } - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - thead { - display: table-header-group; - } - tr, - img { - page-break-inside: avoid; - } - img { - max-width: 100% !important; - } - @page { - margin: 0.5cm; - } - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - h2, - h3 { - page-break-after: avoid; - } -} - -body { - margin: 0; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 20px; - color: #333333; - background-color: #ffffff; -} - -a { - color: #0088cc; - text-decoration: none; -} - -a:hover, -a:focus { - color: #005580; - text-decoration: underline; -} - -.img-rounded { - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.img-polaroid { - padding: 4px; - background-color: #fff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.img-circle { - -webkit-border-radius: 500px; - -moz-border-radius: 500px; - border-radius: 500px; -} - -.row { - margin-left: -20px; - *zoom: 1; -} - -.row:before, -.row:after { - display: table; - line-height: 0; - content: ""; -} - -.row:after { - clear: both; -} - -[class*="span"] { - float: left; - min-height: 1px; - margin-left: 20px; -} - -.container, -.navbar-static-top .container, -.navbar-fixed-top .container, -.navbar-fixed-bottom .container { - width: 940px; -} - -.span12 { - width: 940px; -} - -.span11 { - width: 860px; -} - -.span10 { - width: 780px; -} - -.span9 { - width: 700px; -} - -.span8 { - width: 620px; -} - -.span7 { - width: 540px; -} - -.span6 { - width: 460px; -} - -.span5 { - width: 380px; -} - -.span4 { - width: 300px; -} - -.span3 { - width: 220px; -} - -.span2 { - width: 140px; -} - -.span1 { - width: 60px; -} - -.offset12 { - margin-left: 980px; -} - -.offset11 { - margin-left: 900px; -} - -.offset10 { - margin-left: 820px; -} - -.offset9 { - margin-left: 740px; -} - -.offset8 { - margin-left: 660px; -} - -.offset7 { - margin-left: 580px; -} - -.offset6 { - margin-left: 500px; -} - -.offset5 { - margin-left: 420px; -} - -.offset4 { - margin-left: 340px; -} - -.offset3 { - margin-left: 260px; -} - -.offset2 { - margin-left: 180px; -} - -.offset1 { - margin-left: 100px; -} - -.row-fluid { - width: 100%; - *zoom: 1; -} - -.row-fluid:before, -.row-fluid:after { - display: table; - line-height: 0; - content: ""; -} - -.row-fluid:after { - clear: both; -} - -.row-fluid [class*="span"] { - display: block; - float: left; - width: 100%; - min-height: 30px; - margin-left: 2.127659574468085%; - *margin-left: 2.074468085106383%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.row-fluid [class*="span"]:first-child { - margin-left: 0; -} - -.row-fluid .controls-row [class*="span"] + [class*="span"] { - margin-left: 2.127659574468085%; -} - -.row-fluid .span12 { - width: 100%; - *width: 99.94680851063829%; -} - -.row-fluid .span11 { - width: 91.48936170212765%; - *width: 91.43617021276594%; -} - -.row-fluid .span10 { - width: 82.97872340425532%; - *width: 82.92553191489361%; -} - -.row-fluid .span9 { - width: 74.46808510638297%; - *width: 74.41489361702126%; -} - -.row-fluid .span8 { - width: 65.95744680851064%; - *width: 65.90425531914893%; -} - -.row-fluid .span7 { - width: 57.44680851063829%; - *width: 57.39361702127659%; -} - -.row-fluid .span6 { - width: 48.93617021276595%; - *width: 48.88297872340425%; -} - -.row-fluid .span5 { - width: 40.42553191489362%; - *width: 40.37234042553192%; -} - -.row-fluid .span4 { - width: 31.914893617021278%; - *width: 31.861702127659576%; -} - -.row-fluid .span3 { - width: 23.404255319148934%; - *width: 23.351063829787233%; -} - -.row-fluid .span2 { - width: 14.893617021276595%; - *width: 14.840425531914894%; -} - -.row-fluid .span1 { - width: 6.382978723404255%; - *width: 6.329787234042553%; -} - -.row-fluid .offset12 { - margin-left: 104.25531914893617%; - *margin-left: 104.14893617021275%; -} - -.row-fluid .offset12:first-child { - margin-left: 102.12765957446808%; - *margin-left: 102.02127659574467%; -} - -.row-fluid .offset11 { - margin-left: 95.74468085106382%; - *margin-left: 95.6382978723404%; -} - -.row-fluid .offset11:first-child { - margin-left: 93.61702127659574%; - *margin-left: 93.51063829787232%; -} - -.row-fluid .offset10 { - margin-left: 87.23404255319149%; - *margin-left: 87.12765957446807%; -} - -.row-fluid .offset10:first-child { - margin-left: 85.1063829787234%; - *margin-left: 84.99999999999999%; -} - -.row-fluid .offset9 { - margin-left: 78.72340425531914%; - *margin-left: 78.61702127659572%; -} - -.row-fluid .offset9:first-child { - margin-left: 76.59574468085106%; - *margin-left: 76.48936170212764%; -} - -.row-fluid .offset8 { - margin-left: 70.2127659574468%; - *margin-left: 70.10638297872339%; -} - -.row-fluid .offset8:first-child { - margin-left: 68.08510638297872%; - *margin-left: 67.9787234042553%; -} - -.row-fluid .offset7 { - margin-left: 61.70212765957446%; - *margin-left: 61.59574468085106%; -} - -.row-fluid .offset7:first-child { - margin-left: 59.574468085106375%; - *margin-left: 59.46808510638297%; -} - -.row-fluid .offset6 { - margin-left: 53.191489361702125%; - *margin-left: 53.085106382978715%; -} - -.row-fluid .offset6:first-child { - margin-left: 51.063829787234035%; - *margin-left: 50.95744680851063%; -} - -.row-fluid .offset5 { - margin-left: 44.68085106382979%; - *margin-left: 44.57446808510638%; -} - -.row-fluid .offset5:first-child { - margin-left: 42.5531914893617%; - *margin-left: 42.4468085106383%; -} - -.row-fluid .offset4 { - margin-left: 36.170212765957444%; - *margin-left: 36.06382978723405%; -} - -.row-fluid .offset4:first-child { - margin-left: 34.04255319148936%; - *margin-left: 33.93617021276596%; -} - -.row-fluid .offset3 { - margin-left: 27.659574468085104%; - *margin-left: 27.5531914893617%; -} - -.row-fluid .offset3:first-child { - margin-left: 25.53191489361702%; - *margin-left: 25.425531914893618%; -} - -.row-fluid .offset2 { - margin-left: 19.148936170212764%; - *margin-left: 19.04255319148936%; -} - -.row-fluid .offset2:first-child { - margin-left: 17.02127659574468%; - *margin-left: 16.914893617021278%; -} - -.row-fluid .offset1 { - margin-left: 10.638297872340425%; - *margin-left: 10.53191489361702%; -} - -.row-fluid .offset1:first-child { - margin-left: 8.51063829787234%; - *margin-left: 8.404255319148938%; -} - -[class*="span"].hide, -.row-fluid [class*="span"].hide { - display: none; -} - -[class*="span"].pull-right, -.row-fluid [class*="span"].pull-right { - float: right; -} - -.container { - margin-right: auto; - margin-left: auto; - *zoom: 1; -} - -.container:before, -.container:after { - display: table; - line-height: 0; - content: ""; -} - -.container:after { - clear: both; -} - -.container-fluid { - padding-right: 20px; - padding-left: 20px; - *zoom: 1; -} - -.container-fluid:before, -.container-fluid:after { - display: table; - line-height: 0; - content: ""; -} - -.container-fluid:after { - clear: both; -} - -p { - margin: 0 0 10px; -} - -.lead { - margin-bottom: 20px; - font-size: 21px; - font-weight: 200; - line-height: 30px; -} - -small { - font-size: 85%; -} - -strong { - font-weight: bold; -} - -em { - font-style: italic; -} - -cite { - font-style: normal; -} - -.muted { - color: #999999; -} - -a.muted:hover, -a.muted:focus { - color: #808080; -} - -.text-warning { - color: #c09853; -} - -a.text-warning:hover, -a.text-warning:focus { - color: #a47e3c; -} - -.text-error { - color: #b94a48; -} - -a.text-error:hover, -a.text-error:focus { - color: #953b39; -} - -.text-info { - color: #3a87ad; -} - -a.text-info:hover, -a.text-info:focus { - color: #2d6987; -} - -.text-success { - color: #468847; -} - -a.text-success:hover, -a.text-success:focus { - color: #356635; -} - -.text-left { - text-align: left; -} - -.text-right { - text-align: right; -} - -.text-center { - text-align: center; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - margin: 10px 0; - font-family: inherit; - font-weight: bold; - line-height: 20px; - color: inherit; - text-rendering: optimizelegibility; -} - -h1 small, -h2 small, -h3 small, -h4 small, -h5 small, -h6 small { - font-weight: normal; - line-height: 1; - color: #999999; -} - -h1, -h2, -h3 { - line-height: 40px; -} - -h1 { - font-size: 38.5px; -} - -h2 { - font-size: 31.5px; -} - -h3 { - font-size: 24.5px; -} - -h4 { - font-size: 17.5px; -} - -h5 { - font-size: 14px; -} - -h6 { - font-size: 11.9px; -} - -h1 small { - font-size: 24.5px; -} - -h2 small { - font-size: 17.5px; -} - -h3 small { - font-size: 14px; -} - -h4 small { - font-size: 14px; -} - -.page-header { - padding-bottom: 9px; - margin: 20px 0 30px; - border-bottom: 1px solid #eeeeee; -} - -ul, -ol { - padding: 0; - margin: 0 0 10px 25px; -} - -ul ul, -ul ol, -ol ol, -ol ul { - margin-bottom: 0; -} - -li { - line-height: 20px; -} - -ul.unstyled, -ol.unstyled { - margin-left: 0; - list-style: none; -} - -ul.inline, -ol.inline { - margin-left: 0; - list-style: none; -} - -ul.inline > li, -ol.inline > li { - display: inline-block; - *display: inline; - padding-right: 5px; - padding-left: 5px; - *zoom: 1; -} - -dl { - margin-bottom: 20px; -} - -dt, -dd { - line-height: 20px; -} - -dt { - font-weight: bold; -} - -dd { - margin-left: 10px; -} - -.dl-horizontal { - *zoom: 1; -} - -.dl-horizontal:before, -.dl-horizontal:after { - display: table; - line-height: 0; - content: ""; -} - -.dl-horizontal:after { - clear: both; -} - -.dl-horizontal dt { - float: left; - width: 160px; - overflow: hidden; - clear: left; - text-align: right; - text-overflow: ellipsis; - white-space: nowrap; -} - -.dl-horizontal dd { - margin-left: 180px; -} - -hr { - margin: 20px 0; - border: 0; - border-top: 1px solid #eeeeee; - border-bottom: 1px solid #ffffff; -} - -abbr[title], -abbr[data-original-title] { - cursor: help; - border-bottom: 1px dotted #999999; -} - -abbr.initialism { - font-size: 90%; - text-transform: uppercase; -} - -blockquote { - padding: 0 0 0 15px; - margin: 0 0 20px; - border-left: 5px solid #eeeeee; -} - -blockquote p { - margin-bottom: 0; - font-size: 17.5px; - font-weight: 300; - line-height: 1.25; -} - -blockquote small { - display: block; - line-height: 20px; - color: #999999; -} - -blockquote small:before { - content: '\2014 \00A0'; -} - -blockquote.pull-right { - float: right; - padding-right: 15px; - padding-left: 0; - border-right: 5px solid #eeeeee; - border-left: 0; -} - -blockquote.pull-right p, -blockquote.pull-right small { - text-align: right; -} - -blockquote.pull-right small:before { - content: ''; -} - -blockquote.pull-right small:after { - content: '\00A0 \2014'; -} - -q:before, -q:after, -blockquote:before, -blockquote:after { - content: ""; -} - -address { - display: block; - margin-bottom: 20px; - font-style: normal; - line-height: 20px; -} - -code, -pre { - padding: 0 3px 2px; - font-family: Monaco, Menlo, Consolas, "Courier New", monospace; - font-size: 12px; - color: #333333; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -code { - padding: 2px 4px; - color: #d14; - white-space: nowrap; - background-color: #f7f7f9; - border: 1px solid #e1e1e8; -} - -pre { - display: block; - padding: 9.5px; - margin: 0 0 10px; - font-size: 13px; - line-height: 20px; - word-break: break-all; - word-wrap: break-word; - white-space: pre; - white-space: pre-wrap; - background-color: #f5f5f5; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.15); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -pre.prettyprint { - margin-bottom: 20px; -} - -pre code { - padding: 0; - color: inherit; - white-space: pre; - white-space: pre-wrap; - background-color: transparent; - border: 0; -} - -.pre-scrollable { - max-height: 340px; - overflow-y: scroll; -} - -form { - margin: 0 0 20px; -} - -fieldset { - padding: 0; - margin: 0; - border: 0; -} - -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: 20px; - font-size: 21px; - line-height: 40px; - color: #333333; - border: 0; - border-bottom: 1px solid #e5e5e5; -} - -legend small { - font-size: 15px; - color: #999999; -} - -label, -input, -button, -select, -textarea { - font-size: 14px; - font-weight: normal; - line-height: 20px; -} - -input, -button, -select, -textarea { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; -} - -label { - display: block; - margin-bottom: 5px; -} - -select, -textarea, -input[type="text"], -input[type="password"], -input[type="datetime"], -input[type="datetime-local"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="week"], -input[type="number"], -input[type="email"], -input[type="url"], -input[type="search"], -input[type="tel"], -input[type="color"], -.uneditable-input { - display: inline-block; - height: 20px; - padding: 4px 6px; - margin-bottom: 10px; - font-size: 14px; - line-height: 20px; - color: #555555; - vertical-align: middle; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -input, -textarea, -.uneditable-input { - width: 206px; -} - -textarea { - height: auto; -} - -textarea, -input[type="text"], -input[type="password"], -input[type="datetime"], -input[type="datetime-local"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="week"], -input[type="number"], -input[type="email"], -input[type="url"], -input[type="search"], -input[type="tel"], -input[type="color"], -.uneditable-input { - background-color: #ffffff; - border: 1px solid #cccccc; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; - -moz-transition: border linear 0.2s, box-shadow linear 0.2s; - -o-transition: border linear 0.2s, box-shadow linear 0.2s; - transition: border linear 0.2s, box-shadow linear 0.2s; -} - -textarea:focus, -input[type="text"]:focus, -input[type="password"]:focus, -input[type="datetime"]:focus, -input[type="datetime-local"]:focus, -input[type="date"]:focus, -input[type="month"]:focus, -input[type="time"]:focus, -input[type="week"]:focus, -input[type="number"]:focus, -input[type="email"]:focus, -input[type="url"]:focus, -input[type="search"]:focus, -input[type="tel"]:focus, -input[type="color"]:focus, -.uneditable-input:focus { - border-color: rgba(82, 168, 236, 0.8); - outline: 0; - outline: thin dotted \9; - /* IE6-9 */ - - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); -} - -input[type="radio"], -input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px \9; - *margin-top: 0; - line-height: normal; -} - -input[type="file"], -input[type="image"], -input[type="submit"], -input[type="reset"], -input[type="button"], -input[type="radio"], -input[type="checkbox"] { - width: auto; -} - -select, -input[type="file"] { - height: 30px; - /* In IE7, the height of the select element cannot be changed by height, only font-size */ - - *margin-top: 4px; - /* For IE7, add top margin to align select with labels */ - - line-height: 30px; -} - -select { - width: 220px; - background-color: #ffffff; - border: 1px solid #cccccc; -} - -select[multiple], -select[size] { - height: auto; -} - -select:focus, -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} - -.uneditable-input, -.uneditable-textarea { - color: #999999; - cursor: not-allowed; - background-color: #fcfcfc; - border-color: #cccccc; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); -} - -.uneditable-input { - overflow: hidden; - white-space: nowrap; -} - -.uneditable-textarea { - width: auto; - height: auto; -} - -input:-moz-placeholder, -textarea:-moz-placeholder { - color: #999999; -} - -input:-ms-input-placeholder, -textarea:-ms-input-placeholder { - color: #999999; -} - -input::-webkit-input-placeholder, -textarea::-webkit-input-placeholder { - color: #999999; -} - -.radio, -.checkbox { - min-height: 20px; - padding-left: 20px; -} - -.radio input[type="radio"], -.checkbox input[type="checkbox"] { - float: left; - margin-left: -20px; -} - -.controls > .radio:first-child, -.controls > .checkbox:first-child { - padding-top: 5px; -} - -.radio.inline, -.checkbox.inline { - display: inline-block; - padding-top: 5px; - margin-bottom: 0; - vertical-align: middle; -} - -.radio.inline + .radio.inline, -.checkbox.inline + .checkbox.inline { - margin-left: 10px; -} - -.input-mini { - width: 60px; -} - -.input-small { - width: 90px; -} - -.input-medium { - width: 150px; -} - -.input-large { - width: 210px; -} - -.input-xlarge { - width: 270px; -} - -.input-xxlarge { - width: 530px; -} - -input[class*="span"], -select[class*="span"], -textarea[class*="span"], -.uneditable-input[class*="span"], -.row-fluid input[class*="span"], -.row-fluid select[class*="span"], -.row-fluid textarea[class*="span"], -.row-fluid .uneditable-input[class*="span"] { - float: none; - margin-left: 0; -} - -.input-append input[class*="span"], -.input-append .uneditable-input[class*="span"], -.input-prepend input[class*="span"], -.input-prepend .uneditable-input[class*="span"], -.row-fluid input[class*="span"], -.row-fluid select[class*="span"], -.row-fluid textarea[class*="span"], -.row-fluid .uneditable-input[class*="span"], -.row-fluid .input-prepend [class*="span"], -.row-fluid .input-append [class*="span"] { - display: inline-block; -} - -input, -textarea, -.uneditable-input { - margin-left: 0; -} - -.controls-row [class*="span"] + [class*="span"] { - margin-left: 20px; -} - -input.span12, -textarea.span12, -.uneditable-input.span12 { - width: 926px; -} - -input.span11, -textarea.span11, -.uneditable-input.span11 { - width: 846px; -} - -input.span10, -textarea.span10, -.uneditable-input.span10 { - width: 766px; -} - -input.span9, -textarea.span9, -.uneditable-input.span9 { - width: 686px; -} - -input.span8, -textarea.span8, -.uneditable-input.span8 { - width: 606px; -} - -input.span7, -textarea.span7, -.uneditable-input.span7 { - width: 526px; -} - -input.span6, -textarea.span6, -.uneditable-input.span6 { - width: 446px; -} - -input.span5, -textarea.span5, -.uneditable-input.span5 { - width: 366px; -} - -input.span4, -textarea.span4, -.uneditable-input.span4 { - width: 286px; -} - -input.span3, -textarea.span3, -.uneditable-input.span3 { - width: 206px; -} - -input.span2, -textarea.span2, -.uneditable-input.span2 { - width: 126px; -} - -input.span1, -textarea.span1, -.uneditable-input.span1 { - width: 46px; -} - -.controls-row { - *zoom: 1; -} - -.controls-row:before, -.controls-row:after { - display: table; - line-height: 0; - content: ""; -} - -.controls-row:after { - clear: both; -} - -.controls-row [class*="span"], -.row-fluid .controls-row [class*="span"] { - float: left; -} - -.controls-row .checkbox[class*="span"], -.controls-row .radio[class*="span"] { - padding-top: 5px; -} - -input[disabled], -select[disabled], -textarea[disabled], -input[readonly], -select[readonly], -textarea[readonly] { - cursor: not-allowed; - background-color: #eeeeee; -} - -input[type="radio"][disabled], -input[type="checkbox"][disabled], -input[type="radio"][readonly], -input[type="checkbox"][readonly] { - background-color: transparent; -} - -.control-group.warning .control-label, -.control-group.warning .help-block, -.control-group.warning .help-inline { - color: #c09853; -} - -.control-group.warning .checkbox, -.control-group.warning .radio, -.control-group.warning input, -.control-group.warning select, -.control-group.warning textarea { - color: #c09853; -} - -.control-group.warning input, -.control-group.warning select, -.control-group.warning textarea { - border-color: #c09853; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.control-group.warning input:focus, -.control-group.warning select:focus, -.control-group.warning textarea:focus { - border-color: #a47e3c; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; -} - -.control-group.warning .input-prepend .add-on, -.control-group.warning .input-append .add-on { - color: #c09853; - background-color: #fcf8e3; - border-color: #c09853; -} - -.control-group.error .control-label, -.control-group.error .help-block, -.control-group.error .help-inline { - color: #b94a48; -} - -.control-group.error .checkbox, -.control-group.error .radio, -.control-group.error input, -.control-group.error select, -.control-group.error textarea { - color: #b94a48; -} - -.control-group.error input, -.control-group.error select, -.control-group.error textarea { - border-color: #b94a48; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.control-group.error input:focus, -.control-group.error select:focus, -.control-group.error textarea:focus { - border-color: #953b39; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; -} - -.control-group.error .input-prepend .add-on, -.control-group.error .input-append .add-on { - color: #b94a48; - background-color: #f2dede; - border-color: #b94a48; -} - -.control-group.success .control-label, -.control-group.success .help-block, -.control-group.success .help-inline { - color: #468847; -} - -.control-group.success .checkbox, -.control-group.success .radio, -.control-group.success input, -.control-group.success select, -.control-group.success textarea { - color: #468847; -} - -.control-group.success input, -.control-group.success select, -.control-group.success textarea { - border-color: #468847; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.control-group.success input:focus, -.control-group.success select:focus, -.control-group.success textarea:focus { - border-color: #356635; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; -} - -.control-group.success .input-prepend .add-on, -.control-group.success .input-append .add-on { - color: #468847; - background-color: #dff0d8; - border-color: #468847; -} - -.control-group.info .control-label, -.control-group.info .help-block, -.control-group.info .help-inline { - color: #3a87ad; -} - -.control-group.info .checkbox, -.control-group.info .radio, -.control-group.info input, -.control-group.info select, -.control-group.info textarea { - color: #3a87ad; -} - -.control-group.info input, -.control-group.info select, -.control-group.info textarea { - border-color: #3a87ad; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.control-group.info input:focus, -.control-group.info select:focus, -.control-group.info textarea:focus { - border-color: #2d6987; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; -} - -.control-group.info .input-prepend .add-on, -.control-group.info .input-append .add-on { - color: #3a87ad; - background-color: #d9edf7; - border-color: #3a87ad; -} - -input:focus:invalid, -textarea:focus:invalid, -select:focus:invalid { - color: #b94a48; - border-color: #ee5f5b; -} - -input:focus:invalid:focus, -textarea:focus:invalid:focus, -select:focus:invalid:focus { - border-color: #e9322d; - -webkit-box-shadow: 0 0 6px #f8b9b7; - -moz-box-shadow: 0 0 6px #f8b9b7; - box-shadow: 0 0 6px #f8b9b7; -} - -.form-actions { - padding: 19px 20px 20px; - margin-top: 20px; - margin-bottom: 20px; - background-color: #f5f5f5; - border-top: 1px solid #e5e5e5; - *zoom: 1; -} - -.form-actions:before, -.form-actions:after { - display: table; - line-height: 0; - content: ""; -} - -.form-actions:after { - clear: both; -} - -.help-block, -.help-inline { - color: #595959; -} - -.help-block { - display: block; - margin-bottom: 10px; -} - -.help-inline { - display: inline-block; - *display: inline; - padding-left: 5px; - vertical-align: middle; - *zoom: 1; -} - -.input-append, -.input-prepend { - display: inline-block; - margin-bottom: 10px; - font-size: 0; - white-space: nowrap; - vertical-align: middle; -} - -.input-append input, -.input-prepend input, -.input-append select, -.input-prepend select, -.input-append .uneditable-input, -.input-prepend .uneditable-input, -.input-append .dropdown-menu, -.input-prepend .dropdown-menu, -.input-append .popover, -.input-prepend .popover { - font-size: 14px; -} - -.input-append input, -.input-prepend input, -.input-append select, -.input-prepend select, -.input-append .uneditable-input, -.input-prepend .uneditable-input { - position: relative; - margin-bottom: 0; - *margin-left: 0; - vertical-align: top; - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-append input:focus, -.input-prepend input:focus, -.input-append select:focus, -.input-prepend select:focus, -.input-append .uneditable-input:focus, -.input-prepend .uneditable-input:focus { - z-index: 2; -} - -.input-append .add-on, -.input-prepend .add-on { - display: inline-block; - width: auto; - height: 20px; - min-width: 16px; - padding: 4px 5px; - font-size: 14px; - font-weight: normal; - line-height: 20px; - text-align: center; - text-shadow: 0 1px 0 #ffffff; - background-color: #eeeeee; - border: 1px solid #ccc; -} - -.input-append .add-on, -.input-prepend .add-on, -.input-append .btn, -.input-prepend .btn, -.input-append .btn-group > .dropdown-toggle, -.input-prepend .btn-group > .dropdown-toggle { - vertical-align: top; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.input-append .active, -.input-prepend .active { - background-color: #a9dba9; - border-color: #46a546; -} - -.input-prepend .add-on, -.input-prepend .btn { - margin-right: -1px; -} - -.input-prepend .add-on:first-child, -.input-prepend .btn:first-child { - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.input-append input, -.input-append select, -.input-append .uneditable-input { - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.input-append input + .btn-group .btn:last-child, -.input-append select + .btn-group .btn:last-child, -.input-append .uneditable-input + .btn-group .btn:last-child { - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-append .add-on, -.input-append .btn, -.input-append .btn-group { - margin-left: -1px; -} - -.input-append .add-on:last-child, -.input-append .btn:last-child, -.input-append .btn-group:last-child > .dropdown-toggle { - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-prepend.input-append input, -.input-prepend.input-append select, -.input-prepend.input-append .uneditable-input { - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.input-prepend.input-append input + .btn-group .btn, -.input-prepend.input-append select + .btn-group .btn, -.input-prepend.input-append .uneditable-input + .btn-group .btn { - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-prepend.input-append .add-on:first-child, -.input-prepend.input-append .btn:first-child { - margin-right: -1px; - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.input-prepend.input-append .add-on:last-child, -.input-prepend.input-append .btn:last-child { - margin-left: -1px; - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-prepend.input-append .btn-group:first-child { - margin-left: 0; -} - -input.search-query { - padding-right: 14px; - padding-right: 4px \9; - padding-left: 14px; - padding-left: 4px \9; - /* IE7-8 doesn't have border-radius, so don't indent the padding */ - - margin-bottom: 0; - -webkit-border-radius: 15px; - -moz-border-radius: 15px; - border-radius: 15px; -} - -/* Allow for input prepend/append in search forms */ - -.form-search .input-append .search-query, -.form-search .input-prepend .search-query { - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.form-search .input-append .search-query { - -webkit-border-radius: 14px 0 0 14px; - -moz-border-radius: 14px 0 0 14px; - border-radius: 14px 0 0 14px; -} - -.form-search .input-append .btn { - -webkit-border-radius: 0 14px 14px 0; - -moz-border-radius: 0 14px 14px 0; - border-radius: 0 14px 14px 0; -} - -.form-search .input-prepend .search-query { - -webkit-border-radius: 0 14px 14px 0; - -moz-border-radius: 0 14px 14px 0; - border-radius: 0 14px 14px 0; -} - -.form-search .input-prepend .btn { - -webkit-border-radius: 14px 0 0 14px; - -moz-border-radius: 14px 0 0 14px; - border-radius: 14px 0 0 14px; -} - -.form-search input, -.form-inline input, -.form-horizontal input, -.form-search textarea, -.form-inline textarea, -.form-horizontal textarea, -.form-search select, -.form-inline select, -.form-horizontal select, -.form-search .help-inline, -.form-inline .help-inline, -.form-horizontal .help-inline, -.form-search .uneditable-input, -.form-inline .uneditable-input, -.form-horizontal .uneditable-input, -.form-search .input-prepend, -.form-inline .input-prepend, -.form-horizontal .input-prepend, -.form-search .input-append, -.form-inline .input-append, -.form-horizontal .input-append { - display: inline-block; - *display: inline; - margin-bottom: 0; - vertical-align: middle; - *zoom: 1; -} - -.form-search .hide, -.form-inline .hide, -.form-horizontal .hide { - display: none; -} - -.form-search label, -.form-inline label, -.form-search .btn-group, -.form-inline .btn-group { - display: inline-block; -} - -.form-search .input-append, -.form-inline .input-append, -.form-search .input-prepend, -.form-inline .input-prepend { - margin-bottom: 0; -} - -.form-search .radio, -.form-search .checkbox, -.form-inline .radio, -.form-inline .checkbox { - padding-left: 0; - margin-bottom: 0; - vertical-align: middle; -} - -.form-search .radio input[type="radio"], -.form-search .checkbox input[type="checkbox"], -.form-inline .radio input[type="radio"], -.form-inline .checkbox input[type="checkbox"] { - float: left; - margin-right: 3px; - margin-left: 0; -} - -.control-group { - margin-bottom: 10px; -} - -legend + .control-group { - margin-top: 20px; - -webkit-margin-top-collapse: separate; -} - -.form-horizontal .control-group { - margin-bottom: 20px; - *zoom: 1; -} - -.form-horizontal .control-group:before, -.form-horizontal .control-group:after { - display: table; - line-height: 0; - content: ""; -} - -.form-horizontal .control-group:after { - clear: both; -} - -.form-horizontal .control-label { - float: left; - width: 160px; - padding-top: 5px; - text-align: right; -} - -.form-horizontal .controls { - *display: inline-block; - *padding-left: 20px; - margin-left: 180px; - *margin-left: 0; -} - -.form-horizontal .controls:first-child { - *padding-left: 180px; -} - -.form-horizontal .help-block { - margin-bottom: 0; -} - -.form-horizontal input + .help-block, -.form-horizontal select + .help-block, -.form-horizontal textarea + .help-block, -.form-horizontal .uneditable-input + .help-block, -.form-horizontal .input-prepend + .help-block, -.form-horizontal .input-append + .help-block { - margin-top: 10px; -} - -.form-horizontal .form-actions { - padding-left: 180px; -} - -table { - max-width: 100%; - background-color: transparent; - border-collapse: collapse; - border-spacing: 0; -} - -.table { - width: 100%; - margin-bottom: 20px; -} - -.table th, -.table td { - padding: 8px; - line-height: 20px; - text-align: left; - vertical-align: top; - border-top: 1px solid #dddddd; -} - -.table th { - font-weight: bold; -} - -.table thead th { - vertical-align: bottom; -} - -.table caption + thead tr:first-child th, -.table caption + thead tr:first-child td, -.table colgroup + thead tr:first-child th, -.table colgroup + thead tr:first-child td, -.table thead:first-child tr:first-child th, -.table thead:first-child tr:first-child td { - border-top: 0; -} - -.table tbody + tbody { - border-top: 2px solid #dddddd; -} - -.table .table { - background-color: #ffffff; -} - -.table-condensed th, -.table-condensed td { - padding: 4px 5px; -} - -.table-bordered { - border: 1px solid #dddddd; - border-collapse: separate; - *border-collapse: collapse; - border-left: 0; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.table-bordered th, -.table-bordered td { - border-left: 1px solid #dddddd; -} - -.table-bordered caption + thead tr:first-child th, -.table-bordered caption + tbody tr:first-child th, -.table-bordered caption + tbody tr:first-child td, -.table-bordered colgroup + thead tr:first-child th, -.table-bordered colgroup + tbody tr:first-child th, -.table-bordered colgroup + tbody tr:first-child td, -.table-bordered thead:first-child tr:first-child th, -.table-bordered tbody:first-child tr:first-child th, -.table-bordered tbody:first-child tr:first-child td { - border-top: 0; -} - -.table-bordered thead:first-child tr:first-child > th:first-child, -.table-bordered tbody:first-child tr:first-child > td:first-child, -.table-bordered tbody:first-child tr:first-child > th:first-child { - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-topleft: 4px; -} - -.table-bordered thead:first-child tr:first-child > th:last-child, -.table-bordered tbody:first-child tr:first-child > td:last-child, -.table-bordered tbody:first-child tr:first-child > th:last-child { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -moz-border-radius-topright: 4px; -} - -.table-bordered thead:last-child tr:last-child > th:first-child, -.table-bordered tbody:last-child tr:last-child > td:first-child, -.table-bordered tbody:last-child tr:last-child > th:first-child, -.table-bordered tfoot:last-child tr:last-child > td:first-child, -.table-bordered tfoot:last-child tr:last-child > th:first-child { - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; -} - -.table-bordered thead:last-child tr:last-child > th:last-child, -.table-bordered tbody:last-child tr:last-child > td:last-child, -.table-bordered tbody:last-child tr:last-child > th:last-child, -.table-bordered tfoot:last-child tr:last-child > td:last-child, -.table-bordered tfoot:last-child tr:last-child > th:last-child { - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -moz-border-radius-bottomright: 4px; -} - -.table-bordered tfoot + tbody:last-child tr:last-child td:first-child { - -webkit-border-bottom-left-radius: 0; - border-bottom-left-radius: 0; - -moz-border-radius-bottomleft: 0; -} - -.table-bordered tfoot + tbody:last-child tr:last-child td:last-child { - -webkit-border-bottom-right-radius: 0; - border-bottom-right-radius: 0; - -moz-border-radius-bottomright: 0; -} - -.table-bordered caption + thead tr:first-child th:first-child, -.table-bordered caption + tbody tr:first-child td:first-child, -.table-bordered colgroup + thead tr:first-child th:first-child, -.table-bordered colgroup + tbody tr:first-child td:first-child { - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-topleft: 4px; -} - -.table-bordered caption + thead tr:first-child th:last-child, -.table-bordered caption + tbody tr:first-child td:last-child, -.table-bordered colgroup + thead tr:first-child th:last-child, -.table-bordered colgroup + tbody tr:first-child td:last-child { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -moz-border-radius-topright: 4px; -} - -.table-striped tbody > tr:nth-child(odd) > td, -.table-striped tbody > tr:nth-child(odd) > th { - background-color: #f9f9f9; -} - -.table-hover tbody tr:hover > td, -.table-hover tbody tr:hover > th { - background-color: #f5f5f5; -} - -table td[class*="span"], -table th[class*="span"], -.row-fluid table td[class*="span"], -.row-fluid table th[class*="span"] { - display: table-cell; - float: none; - margin-left: 0; -} - -.table td.span1, -.table th.span1 { - float: none; - width: 44px; - margin-left: 0; -} - -.table td.span2, -.table th.span2 { - float: none; - width: 124px; - margin-left: 0; -} - -.table td.span3, -.table th.span3 { - float: none; - width: 204px; - margin-left: 0; -} - -.table td.span4, -.table th.span4 { - float: none; - width: 284px; - margin-left: 0; -} - -.table td.span5, -.table th.span5 { - float: none; - width: 364px; - margin-left: 0; -} - -.table td.span6, -.table th.span6 { - float: none; - width: 444px; - margin-left: 0; -} - -.table td.span7, -.table th.span7 { - float: none; - width: 524px; - margin-left: 0; -} - -.table td.span8, -.table th.span8 { - float: none; - width: 604px; - margin-left: 0; -} - -.table td.span9, -.table th.span9 { - float: none; - width: 684px; - margin-left: 0; -} - -.table td.span10, -.table th.span10 { - float: none; - width: 764px; - margin-left: 0; -} - -.table td.span11, -.table th.span11 { - float: none; - width: 844px; - margin-left: 0; -} - -.table td.span12, -.table th.span12 { - float: none; - width: 924px; - margin-left: 0; -} - -.table tbody tr.success > td { - background-color: #dff0d8; -} - -.table tbody tr.error > td { - background-color: #f2dede; -} - -.table tbody tr.warning > td { - background-color: #fcf8e3; -} - -.table tbody tr.info > td { - background-color: #d9edf7; -} - -.table-hover tbody tr.success:hover > td { - background-color: #d0e9c6; -} - -.table-hover tbody tr.error:hover > td { - background-color: #ebcccc; -} - -.table-hover tbody tr.warning:hover > td { - background-color: #faf2cc; -} - -.table-hover tbody tr.info:hover > td { - background-color: #c4e3f3; -} - -[class^="icon-"], -[class*=" icon-"] { - display: inline-block; - width: 14px; - height: 14px; - margin-top: 1px; - *margin-right: .3em; - line-height: 14px; - vertical-align: text-top; - background-image: url("../img/glyphicons-halflings.png"); - background-position: 14px 14px; - background-repeat: no-repeat; -} - -/* White icons with optional class, or on hover/focus/active states of certain elements */ - -.icon-white, -.nav-pills > .active > a > [class^="icon-"], -.nav-pills > .active > a > [class*=" icon-"], -.nav-list > .active > a > [class^="icon-"], -.nav-list > .active > a > [class*=" icon-"], -.navbar-inverse .nav > .active > a > [class^="icon-"], -.navbar-inverse .nav > .active > a > [class*=" icon-"], -.dropdown-menu > li > a:hover > [class^="icon-"], -.dropdown-menu > li > a:focus > [class^="icon-"], -.dropdown-menu > li > a:hover > [class*=" icon-"], -.dropdown-menu > li > a:focus > [class*=" icon-"], -.dropdown-menu > .active > a > [class^="icon-"], -.dropdown-menu > .active > a > [class*=" icon-"], -.dropdown-submenu:hover > a > [class^="icon-"], -.dropdown-submenu:focus > a > [class^="icon-"], -.dropdown-submenu:hover > a > [class*=" icon-"], -.dropdown-submenu:focus > a > [class*=" icon-"] { - background-image: url("../img/glyphicons-halflings-white.png"); -} - -.icon-glass { - background-position: 0 0; -} - -.icon-music { - background-position: -24px 0; -} - -.icon-search { - background-position: -48px 0; -} - -.icon-envelope { - background-position: -72px 0; -} - -.icon-heart { - background-position: -96px 0; -} - -.icon-star { - background-position: -120px 0; -} - -.icon-star-empty { - background-position: -144px 0; -} - -.icon-user { - background-position: -168px 0; -} - -.icon-film { - background-position: -192px 0; -} - -.icon-th-large { - background-position: -216px 0; -} - -.icon-th { - background-position: -240px 0; -} - -.icon-th-list { - background-position: -264px 0; -} - -.icon-ok { - background-position: -288px 0; -} - -.icon-remove { - background-position: -312px 0; -} - -.icon-zoom-in { - background-position: -336px 0; -} - -.icon-zoom-out { - background-position: -360px 0; -} - -.icon-off { - background-position: -384px 0; -} - -.icon-signal { - background-position: -408px 0; -} - -.icon-cog { - background-position: -432px 0; -} - -.icon-trash { - background-position: -456px 0; -} - -.icon-home { - background-position: 0 -24px; -} - -.icon-file { - background-position: -24px -24px; -} - -.icon-time { - background-position: -48px -24px; -} - -.icon-road { - background-position: -72px -24px; -} - -.icon-download-alt { - background-position: -96px -24px; -} - -.icon-download { - background-position: -120px -24px; -} - -.icon-upload { - background-position: -144px -24px; -} - -.icon-inbox { - background-position: -168px -24px; -} - -.icon-play-circle { - background-position: -192px -24px; -} - -.icon-repeat { - background-position: -216px -24px; -} - -.icon-refresh { - background-position: -240px -24px; -} - -.icon-list-alt { - background-position: -264px -24px; -} - -.icon-lock { - background-position: -287px -24px; -} - -.icon-flag { - background-position: -312px -24px; -} - -.icon-headphones { - background-position: -336px -24px; -} - -.icon-volume-off { - background-position: -360px -24px; -} - -.icon-volume-down { - background-position: -384px -24px; -} - -.icon-volume-up { - background-position: -408px -24px; -} - -.icon-qrcode { - background-position: -432px -24px; -} - -.icon-barcode { - background-position: -456px -24px; -} - -.icon-tag { - background-position: 0 -48px; -} - -.icon-tags { - background-position: -25px -48px; -} - -.icon-book { - background-position: -48px -48px; -} - -.icon-bookmark { - background-position: -72px -48px; -} - -.icon-print { - background-position: -96px -48px; -} - -.icon-camera { - background-position: -120px -48px; -} - -.icon-font { - background-position: -144px -48px; -} - -.icon-bold { - background-position: -167px -48px; -} - -.icon-italic { - background-position: -192px -48px; -} - -.icon-text-height { - background-position: -216px -48px; -} - -.icon-text-width { - background-position: -240px -48px; -} - -.icon-align-left { - background-position: -264px -48px; -} - -.icon-align-center { - background-position: -288px -48px; -} - -.icon-align-right { - background-position: -312px -48px; -} - -.icon-align-justify { - background-position: -336px -48px; -} - -.icon-list { - background-position: -360px -48px; -} - -.icon-indent-left { - background-position: -384px -48px; -} - -.icon-indent-right { - background-position: -408px -48px; -} - -.icon-facetime-video { - background-position: -432px -48px; -} - -.icon-picture { - background-position: -456px -48px; -} - -.icon-pencil { - background-position: 0 -72px; -} - -.icon-map-marker { - background-position: -24px -72px; -} - -.icon-adjust { - background-position: -48px -72px; -} - -.icon-tint { - background-position: -72px -72px; -} - -.icon-edit { - background-position: -96px -72px; -} - -.icon-share { - background-position: -120px -72px; -} - -.icon-check { - background-position: -144px -72px; -} - -.icon-move { - background-position: -168px -72px; -} - -.icon-step-backward { - background-position: -192px -72px; -} - -.icon-fast-backward { - background-position: -216px -72px; -} - -.icon-backward { - background-position: -240px -72px; -} - -.icon-play { - background-position: -264px -72px; -} - -.icon-pause { - background-position: -288px -72px; -} - -.icon-stop { - background-position: -312px -72px; -} - -.icon-forward { - background-position: -336px -72px; -} - -.icon-fast-forward { - background-position: -360px -72px; -} - -.icon-step-forward { - background-position: -384px -72px; -} - -.icon-eject { - background-position: -408px -72px; -} - -.icon-chevron-left { - background-position: -432px -72px; -} - -.icon-chevron-right { - background-position: -456px -72px; -} - -.icon-plus-sign { - background-position: 0 -96px; -} - -.icon-minus-sign { - background-position: -24px -96px; -} - -.icon-remove-sign { - background-position: -48px -96px; -} - -.icon-ok-sign { - background-position: -72px -96px; -} - -.icon-question-sign { - background-position: -96px -96px; -} - -.icon-info-sign { - background-position: -120px -96px; -} - -.icon-screenshot { - background-position: -144px -96px; -} - -.icon-remove-circle { - background-position: -168px -96px; -} - -.icon-ok-circle { - background-position: -192px -96px; -} - -.icon-ban-circle { - background-position: -216px -96px; -} - -.icon-arrow-left { - background-position: -240px -96px; -} - -.icon-arrow-right { - background-position: -264px -96px; -} - -.icon-arrow-up { - background-position: -289px -96px; -} - -.icon-arrow-down { - background-position: -312px -96px; -} - -.icon-share-alt { - background-position: -336px -96px; -} - -.icon-resize-full { - background-position: -360px -96px; -} - -.icon-resize-small { - background-position: -384px -96px; -} - -.icon-plus { - background-position: -408px -96px; -} - -.icon-minus { - background-position: -433px -96px; -} - -.icon-asterisk { - background-position: -456px -96px; -} - -.icon-exclamation-sign { - background-position: 0 -120px; -} - -.icon-gift { - background-position: -24px -120px; -} - -.icon-leaf { - background-position: -48px -120px; -} - -.icon-fire { - background-position: -72px -120px; -} - -.icon-eye-open { - background-position: -96px -120px; -} - -.icon-eye-close { - background-position: -120px -120px; -} - -.icon-warning-sign { - background-position: -144px -120px; -} - -.icon-plane { - background-position: -168px -120px; -} - -.icon-calendar { - background-position: -192px -120px; -} - -.icon-random { - width: 16px; - background-position: -216px -120px; -} - -.icon-comment { - background-position: -240px -120px; -} - -.icon-magnet { - background-position: -264px -120px; -} - -.icon-chevron-up { - background-position: -288px -120px; -} - -.icon-chevron-down { - background-position: -313px -119px; -} - -.icon-retweet { - background-position: -336px -120px; -} - -.icon-shopping-cart { - background-position: -360px -120px; -} - -.icon-folder-close { - width: 16px; - background-position: -384px -120px; -} - -.icon-folder-open { - width: 16px; - background-position: -408px -120px; -} - -.icon-resize-vertical { - background-position: -432px -119px; -} - -.icon-resize-horizontal { - background-position: -456px -118px; -} - -.icon-hdd { - background-position: 0 -144px; -} - -.icon-bullhorn { - background-position: -24px -144px; -} - -.icon-bell { - background-position: -48px -144px; -} - -.icon-certificate { - background-position: -72px -144px; -} - -.icon-thumbs-up { - background-position: -96px -144px; -} - -.icon-thumbs-down { - background-position: -120px -144px; -} - -.icon-hand-right { - background-position: -144px -144px; -} - -.icon-hand-left { - background-position: -168px -144px; -} - -.icon-hand-up { - background-position: -192px -144px; -} - -.icon-hand-down { - background-position: -216px -144px; -} - -.icon-circle-arrow-right { - background-position: -240px -144px; -} - -.icon-circle-arrow-left { - background-position: -264px -144px; -} - -.icon-circle-arrow-up { - background-position: -288px -144px; -} - -.icon-circle-arrow-down { - background-position: -312px -144px; -} - -.icon-globe { - background-position: -336px -144px; -} - -.icon-wrench { - background-position: -360px -144px; -} - -.icon-tasks { - background-position: -384px -144px; -} - -.icon-filter { - background-position: -408px -144px; -} - -.icon-briefcase { - background-position: -432px -144px; -} - -.icon-fullscreen { - background-position: -456px -144px; -} - -.dropup, -.dropdown { - position: relative; -} - -.dropdown-toggle { - *margin-bottom: -3px; -} - -.dropdown-toggle:active, -.open .dropdown-toggle { - outline: 0; -} - -.caret { - display: inline-block; - width: 0; - height: 0; - vertical-align: top; - border-top: 4px solid #000000; - border-right: 4px solid transparent; - border-left: 4px solid transparent; - content: ""; -} - -.dropdown .caret { - margin-top: 8px; - margin-left: 2px; -} - -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - display: none; - float: left; - min-width: 160px; - padding: 5px 0; - margin: 2px 0 0; - list-style: none; - background-color: #ffffff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - *border-right-width: 2px; - *border-bottom-width: 2px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; -} - -.dropdown-menu.pull-right { - right: 0; - left: auto; -} - -.dropdown-menu .divider { - *width: 100%; - height: 1px; - margin: 9px 1px; - *margin: -5px 0 5px; - overflow: hidden; - background-color: #e5e5e5; - border-bottom: 1px solid #ffffff; -} - -.dropdown-menu > li > a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 20px; - color: #333333; - white-space: nowrap; -} - -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus, -.dropdown-submenu:hover > a, -.dropdown-submenu:focus > a { - color: #ffffff; - text-decoration: none; - background-color: #0081c2; - background-image: -moz-linear-gradient(top, #0088cc, #0077b3); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); - background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); - background-image: -o-linear-gradient(top, #0088cc, #0077b3); - background-image: linear-gradient(to bottom, #0088cc, #0077b3); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); -} - -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - color: #ffffff; - text-decoration: none; - background-color: #0081c2; - background-image: -moz-linear-gradient(top, #0088cc, #0077b3); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); - background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); - background-image: -o-linear-gradient(top, #0088cc, #0077b3); - background-image: linear-gradient(to bottom, #0088cc, #0077b3); - background-repeat: repeat-x; - outline: 0; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); -} - -.dropdown-menu > .disabled > a, -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - color: #999999; -} - -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - text-decoration: none; - cursor: default; - background-color: transparent; - background-image: none; - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.open { - *z-index: 1000; -} - -.open > .dropdown-menu { - display: block; -} - -.pull-right > .dropdown-menu { - right: 0; - left: auto; -} - -.dropup .caret, -.navbar-fixed-bottom .dropdown .caret { - border-top: 0; - border-bottom: 4px solid #000000; - content: ""; -} - -.dropup .dropdown-menu, -.navbar-fixed-bottom .dropdown .dropdown-menu { - top: auto; - bottom: 100%; - margin-bottom: 1px; -} - -.dropdown-submenu { - position: relative; -} - -.dropdown-submenu > .dropdown-menu { - top: 0; - left: 100%; - margin-top: -6px; - margin-left: -1px; - -webkit-border-radius: 0 6px 6px 6px; - -moz-border-radius: 0 6px 6px 6px; - border-radius: 0 6px 6px 6px; -} - -.dropdown-submenu:hover > .dropdown-menu { - display: block; -} - -.dropup .dropdown-submenu > .dropdown-menu { - top: auto; - bottom: 0; - margin-top: 0; - margin-bottom: -2px; - -webkit-border-radius: 5px 5px 5px 0; - -moz-border-radius: 5px 5px 5px 0; - border-radius: 5px 5px 5px 0; -} - -.dropdown-submenu > a:after { - display: block; - float: right; - width: 0; - height: 0; - margin-top: 5px; - margin-right: -10px; - border-color: transparent; - border-left-color: #cccccc; - border-style: solid; - border-width: 5px 0 5px 5px; - content: " "; -} - -.dropdown-submenu:hover > a:after { - border-left-color: #ffffff; -} - -.dropdown-submenu.pull-left { - float: none; -} - -.dropdown-submenu.pull-left > .dropdown-menu { - left: -100%; - margin-left: 10px; - -webkit-border-radius: 6px 0 6px 6px; - -moz-border-radius: 6px 0 6px 6px; - border-radius: 6px 0 6px 6px; -} - -.dropdown .dropdown-menu .nav-header { - padding-right: 20px; - padding-left: 20px; -} - -.typeahead { - z-index: 1051; - margin-top: 2px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: #f5f5f5; - border: 1px solid #e3e3e3; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -} - -.well blockquote { - border-color: #ddd; - border-color: rgba(0, 0, 0, 0.15); -} - -.well-large { - padding: 24px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.well-small { - padding: 9px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.fade { - opacity: 0; - -webkit-transition: opacity 0.15s linear; - -moz-transition: opacity 0.15s linear; - -o-transition: opacity 0.15s linear; - transition: opacity 0.15s linear; -} - -.fade.in { - opacity: 1; -} - -.collapse { - position: relative; - height: 0; - overflow: hidden; - -webkit-transition: height 0.35s ease; - -moz-transition: height 0.35s ease; - -o-transition: height 0.35s ease; - transition: height 0.35s ease; -} - -.collapse.in { - height: auto; -} - -.close { - float: right; - font-size: 20px; - font-weight: bold; - line-height: 20px; - color: #000000; - text-shadow: 0 1px 0 #ffffff; - opacity: 0.2; - filter: alpha(opacity=20); -} - -.close:hover, -.close:focus { - color: #000000; - text-decoration: none; - cursor: pointer; - opacity: 0.4; - filter: alpha(opacity=40); -} - -button.close { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} - -.btn { - display: inline-block; - *display: inline; - padding: 4px 12px; - margin-bottom: 0; - *margin-left: .3em; - font-size: 14px; - line-height: 20px; - color: #333333; - text-align: center; - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - vertical-align: middle; - cursor: pointer; - background-color: #f5f5f5; - *background-color: #e6e6e6; - background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); - background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); - background-repeat: repeat-x; - border: 1px solid #cccccc; - *border: 0; - border-color: #e6e6e6 #e6e6e6 #bfbfbf; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - border-bottom-color: #b3b3b3; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); - *zoom: 1; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn:hover, -.btn:focus, -.btn:active, -.btn.active, -.btn.disabled, -.btn[disabled] { - color: #333333; - background-color: #e6e6e6; - *background-color: #d9d9d9; -} - -.btn:active, -.btn.active { - background-color: #cccccc \9; -} - -.btn:first-child { - *margin-left: 0; -} - -.btn:hover, -.btn:focus { - color: #333333; - text-decoration: none; - background-position: 0 -15px; - -webkit-transition: background-position 0.1s linear; - -moz-transition: background-position 0.1s linear; - -o-transition: background-position 0.1s linear; - transition: background-position 0.1s linear; -} - -.btn:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} - -.btn.active, -.btn:active { - background-image: none; - outline: 0; - -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn.disabled, -.btn[disabled] { - cursor: default; - background-image: none; - opacity: 0.65; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -.btn-large { - padding: 11px 19px; - font-size: 17.5px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.btn-large [class^="icon-"], -.btn-large [class*=" icon-"] { - margin-top: 4px; -} - -.btn-small { - padding: 2px 10px; - font-size: 11.9px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.btn-small [class^="icon-"], -.btn-small [class*=" icon-"] { - margin-top: 0; -} - -.btn-mini [class^="icon-"], -.btn-mini [class*=" icon-"] { - margin-top: -1px; -} - -.btn-mini { - padding: 0 6px; - font-size: 10.5px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.btn-block { - display: block; - width: 100%; - padding-right: 0; - padding-left: 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.btn-block + .btn-block { - margin-top: 5px; -} - -input[type="submit"].btn-block, -input[type="reset"].btn-block, -input[type="button"].btn-block { - width: 100%; -} - -.btn-primary.active, -.btn-warning.active, -.btn-danger.active, -.btn-success.active, -.btn-info.active, -.btn-inverse.active { - color: rgba(255, 255, 255, 0.75); -} - -.btn-primary { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #006dcc; - *background-color: #0044cc; - background-image: -moz-linear-gradient(top, #0088cc, #0044cc); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); - background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); - background-image: -o-linear-gradient(top, #0088cc, #0044cc); - background-image: linear-gradient(to bottom, #0088cc, #0044cc); - background-repeat: repeat-x; - border-color: #0044cc #0044cc #002a80; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-primary:hover, -.btn-primary:focus, -.btn-primary:active, -.btn-primary.active, -.btn-primary.disabled, -.btn-primary[disabled] { - color: #ffffff; - background-color: #0044cc; - *background-color: #003bb3; -} - -.btn-primary:active, -.btn-primary.active { - background-color: #003399 \9; -} - -.btn-warning { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #faa732; - *background-color: #f89406; - background-image: -moz-linear-gradient(top, #fbb450, #f89406); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); - background-image: -webkit-linear-gradient(top, #fbb450, #f89406); - background-image: -o-linear-gradient(top, #fbb450, #f89406); - background-image: linear-gradient(to bottom, #fbb450, #f89406); - background-repeat: repeat-x; - border-color: #f89406 #f89406 #ad6704; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-warning:hover, -.btn-warning:focus, -.btn-warning:active, -.btn-warning.active, -.btn-warning.disabled, -.btn-warning[disabled] { - color: #ffffff; - background-color: #f89406; - *background-color: #df8505; -} - -.btn-warning:active, -.btn-warning.active { - background-color: #c67605 \9; -} - -.btn-danger { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #da4f49; - *background-color: #bd362f; - background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); - background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); - background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); - background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); - background-repeat: repeat-x; - border-color: #bd362f #bd362f #802420; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-danger:hover, -.btn-danger:focus, -.btn-danger:active, -.btn-danger.active, -.btn-danger.disabled, -.btn-danger[disabled] { - color: #ffffff; - background-color: #bd362f; - *background-color: #a9302a; -} - -.btn-danger:active, -.btn-danger.active { - background-color: #942a25 \9; -} - -.btn-success { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #5bb75b; - *background-color: #51a351; - background-image: -moz-linear-gradient(top, #62c462, #51a351); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); - background-image: -webkit-linear-gradient(top, #62c462, #51a351); - background-image: -o-linear-gradient(top, #62c462, #51a351); - background-image: linear-gradient(to bottom, #62c462, #51a351); - background-repeat: repeat-x; - border-color: #51a351 #51a351 #387038; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-success:hover, -.btn-success:focus, -.btn-success:active, -.btn-success.active, -.btn-success.disabled, -.btn-success[disabled] { - color: #ffffff; - background-color: #51a351; - *background-color: #499249; -} - -.btn-success:active, -.btn-success.active { - background-color: #408140 \9; -} - -.btn-info { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #49afcd; - *background-color: #2f96b4; - background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); - background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); - background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); - background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); - background-repeat: repeat-x; - border-color: #2f96b4 #2f96b4 #1f6377; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-info:hover, -.btn-info:focus, -.btn-info:active, -.btn-info.active, -.btn-info.disabled, -.btn-info[disabled] { - color: #ffffff; - background-color: #2f96b4; - *background-color: #2a85a0; -} - -.btn-info:active, -.btn-info.active { - background-color: #24748c \9; -} - -.btn-inverse { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #363636; - *background-color: #222222; - background-image: -moz-linear-gradient(top, #444444, #222222); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); - background-image: -webkit-linear-gradient(top, #444444, #222222); - background-image: -o-linear-gradient(top, #444444, #222222); - background-image: linear-gradient(to bottom, #444444, #222222); - background-repeat: repeat-x; - border-color: #222222 #222222 #000000; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-inverse:hover, -.btn-inverse:focus, -.btn-inverse:active, -.btn-inverse.active, -.btn-inverse.disabled, -.btn-inverse[disabled] { - color: #ffffff; - background-color: #222222; - *background-color: #151515; -} - -.btn-inverse:active, -.btn-inverse.active { - background-color: #080808 \9; -} - -button.btn, -input[type="submit"].btn { - *padding-top: 3px; - *padding-bottom: 3px; -} - -button.btn::-moz-focus-inner, -input[type="submit"].btn::-moz-focus-inner { - padding: 0; - border: 0; -} - -button.btn.btn-large, -input[type="submit"].btn.btn-large { - *padding-top: 7px; - *padding-bottom: 7px; -} - -button.btn.btn-small, -input[type="submit"].btn.btn-small { - *padding-top: 3px; - *padding-bottom: 3px; -} - -button.btn.btn-mini, -input[type="submit"].btn.btn-mini { - *padding-top: 1px; - *padding-bottom: 1px; -} - -.btn-link, -.btn-link:active, -.btn-link[disabled] { - background-color: transparent; - background-image: none; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -.btn-link { - color: #0088cc; - cursor: pointer; - border-color: transparent; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.btn-link:hover, -.btn-link:focus { - color: #005580; - text-decoration: underline; - background-color: transparent; -} - -.btn-link[disabled]:hover, -.btn-link[disabled]:focus { - color: #333333; - text-decoration: none; -} - -.btn-group { - position: relative; - display: inline-block; - *display: inline; - *margin-left: .3em; - font-size: 0; - white-space: nowrap; - vertical-align: middle; - *zoom: 1; -} - -.btn-group:first-child { - *margin-left: 0; -} - -.btn-group + .btn-group { - margin-left: 5px; -} - -.btn-toolbar { - margin-top: 10px; - margin-bottom: 10px; - font-size: 0; -} - -.btn-toolbar > .btn + .btn, -.btn-toolbar > .btn-group + .btn, -.btn-toolbar > .btn + .btn-group { - margin-left: 5px; -} - -.btn-group > .btn { - position: relative; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.btn-group > .btn + .btn { - margin-left: -1px; -} - -.btn-group > .btn, -.btn-group > .dropdown-menu, -.btn-group > .popover { - font-size: 14px; -} - -.btn-group > .btn-mini { - font-size: 10.5px; -} - -.btn-group > .btn-small { - font-size: 11.9px; -} - -.btn-group > .btn-large { - font-size: 17.5px; -} - -.btn-group > .btn:first-child { - margin-left: 0; - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; - -moz-border-radius-topleft: 4px; -} - -.btn-group > .btn:last-child, -.btn-group > .dropdown-toggle { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -moz-border-radius-topright: 4px; - -moz-border-radius-bottomright: 4px; -} - -.btn-group > .btn.large:first-child { - margin-left: 0; - -webkit-border-bottom-left-radius: 6px; - border-bottom-left-radius: 6px; - -webkit-border-top-left-radius: 6px; - border-top-left-radius: 6px; - -moz-border-radius-bottomleft: 6px; - -moz-border-radius-topleft: 6px; -} - -.btn-group > .btn.large:last-child, -.btn-group > .large.dropdown-toggle { - -webkit-border-top-right-radius: 6px; - border-top-right-radius: 6px; - -webkit-border-bottom-right-radius: 6px; - border-bottom-right-radius: 6px; - -moz-border-radius-topright: 6px; - -moz-border-radius-bottomright: 6px; -} - -.btn-group > .btn:hover, -.btn-group > .btn:focus, -.btn-group > .btn:active, -.btn-group > .btn.active { - z-index: 2; -} - -.btn-group .dropdown-toggle:active, -.btn-group.open .dropdown-toggle { - outline: 0; -} - -.btn-group > .btn + .dropdown-toggle { - *padding-top: 5px; - padding-right: 8px; - *padding-bottom: 5px; - padding-left: 8px; - -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn-group > .btn-mini + .dropdown-toggle { - *padding-top: 2px; - padding-right: 5px; - *padding-bottom: 2px; - padding-left: 5px; -} - -.btn-group > .btn-small + .dropdown-toggle { - *padding-top: 5px; - *padding-bottom: 4px; -} - -.btn-group > .btn-large + .dropdown-toggle { - *padding-top: 7px; - padding-right: 12px; - *padding-bottom: 7px; - padding-left: 12px; -} - -.btn-group.open .dropdown-toggle { - background-image: none; - -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn-group.open .btn.dropdown-toggle { - background-color: #e6e6e6; -} - -.btn-group.open .btn-primary.dropdown-toggle { - background-color: #0044cc; -} - -.btn-group.open .btn-warning.dropdown-toggle { - background-color: #f89406; -} - -.btn-group.open .btn-danger.dropdown-toggle { - background-color: #bd362f; -} - -.btn-group.open .btn-success.dropdown-toggle { - background-color: #51a351; -} - -.btn-group.open .btn-info.dropdown-toggle { - background-color: #2f96b4; -} - -.btn-group.open .btn-inverse.dropdown-toggle { - background-color: #222222; -} - -.btn .caret { - margin-top: 8px; - margin-left: 0; -} - -.btn-large .caret { - margin-top: 6px; -} - -.btn-large .caret { - border-top-width: 5px; - border-right-width: 5px; - border-left-width: 5px; -} - -.btn-mini .caret, -.btn-small .caret { - margin-top: 8px; -} - -.dropup .btn-large .caret { - border-bottom-width: 5px; -} - -.btn-primary .caret, -.btn-warning .caret, -.btn-danger .caret, -.btn-info .caret, -.btn-success .caret, -.btn-inverse .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; -} - -.btn-group-vertical { - display: inline-block; - *display: inline; - /* IE7 inline-block hack */ - - *zoom: 1; -} - -.btn-group-vertical > .btn { - display: block; - float: none; - max-width: 100%; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.btn-group-vertical > .btn + .btn { - margin-top: -1px; - margin-left: 0; -} - -.btn-group-vertical > .btn:first-child { - -webkit-border-radius: 4px 4px 0 0; - -moz-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; -} - -.btn-group-vertical > .btn:last-child { - -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius: 0 0 4px 4px; - border-radius: 0 0 4px 4px; -} - -.btn-group-vertical > .btn-large:first-child { - -webkit-border-radius: 6px 6px 0 0; - -moz-border-radius: 6px 6px 0 0; - border-radius: 6px 6px 0 0; -} - -.btn-group-vertical > .btn-large:last-child { - -webkit-border-radius: 0 0 6px 6px; - -moz-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; -} - -.alert { - padding: 8px 35px 8px 14px; - margin-bottom: 20px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - background-color: #fcf8e3; - border: 1px solid #fbeed5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.alert, -.alert h4 { - color: #c09853; -} - -.alert h4 { - margin: 0; -} - -.alert .close { - position: relative; - top: -2px; - right: -21px; - line-height: 20px; -} - -.alert-success { - color: #468847; - background-color: #dff0d8; - border-color: #d6e9c6; -} - -.alert-success h4 { - color: #468847; -} - -.alert-danger, -.alert-error { - color: #b94a48; - background-color: #f2dede; - border-color: #eed3d7; -} - -.alert-danger h4, -.alert-error h4 { - color: #b94a48; -} - -.alert-info { - color: #3a87ad; - background-color: #d9edf7; - border-color: #bce8f1; -} - -.alert-info h4 { - color: #3a87ad; -} - -.alert-block { - padding-top: 14px; - padding-bottom: 14px; -} - -.alert-block > p, -.alert-block > ul { - margin-bottom: 0; -} - -.alert-block p + p { - margin-top: 5px; -} - -.nav { - margin-bottom: 20px; - margin-left: 0; - list-style: none; -} - -.nav > li > a { - display: block; -} - -.nav > li > a:hover, -.nav > li > a:focus { - text-decoration: none; - background-color: #eeeeee; -} - -.nav > li > a > img { - max-width: none; -} - -.nav > .pull-right { - float: right; -} - -.nav-header { - display: block; - padding: 3px 15px; - font-size: 11px; - font-weight: bold; - line-height: 20px; - color: #999999; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - text-transform: uppercase; -} - -.nav li + .nav-header { - margin-top: 9px; -} - -.nav-list { - padding-right: 15px; - padding-left: 15px; - margin-bottom: 0; -} - -.nav-list > li > a, -.nav-list .nav-header { - margin-right: -15px; - margin-left: -15px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); -} - -.nav-list > li > a { - padding: 3px 15px; -} - -.nav-list > .active > a, -.nav-list > .active > a:hover, -.nav-list > .active > a:focus { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); - background-color: #0088cc; -} - -.nav-list [class^="icon-"], -.nav-list [class*=" icon-"] { - margin-right: 2px; -} - -.nav-list .divider { - *width: 100%; - height: 1px; - margin: 9px 1px; - *margin: -5px 0 5px; - overflow: hidden; - background-color: #e5e5e5; - border-bottom: 1px solid #ffffff; -} - -.nav-tabs, -.nav-pills { - *zoom: 1; -} - -.nav-tabs:before, -.nav-pills:before, -.nav-tabs:after, -.nav-pills:after { - display: table; - line-height: 0; - content: ""; -} - -.nav-tabs:after, -.nav-pills:after { - clear: both; -} - -.nav-tabs > li, -.nav-pills > li { - float: left; -} - -.nav-tabs > li > a, -.nav-pills > li > a { - padding-right: 12px; - padding-left: 12px; - margin-right: 2px; - line-height: 14px; -} - -.nav-tabs { - border-bottom: 1px solid #ddd; -} - -.nav-tabs > li { - margin-bottom: -1px; -} - -.nav-tabs > li > a { - padding-top: 8px; - padding-bottom: 8px; - line-height: 20px; - border: 1px solid transparent; - -webkit-border-radius: 4px 4px 0 0; - -moz-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; -} - -.nav-tabs > li > a:hover, -.nav-tabs > li > a:focus { - border-color: #eeeeee #eeeeee #dddddd; -} - -.nav-tabs > .active > a, -.nav-tabs > .active > a:hover, -.nav-tabs > .active > a:focus { - color: #555555; - cursor: default; - background-color: #ffffff; - border: 1px solid #ddd; - border-bottom-color: transparent; -} - -.nav-pills > li > a { - padding-top: 8px; - padding-bottom: 8px; - margin-top: 2px; - margin-bottom: 2px; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; -} - -.nav-pills > .active > a, -.nav-pills > .active > a:hover, -.nav-pills > .active > a:focus { - color: #ffffff; - background-color: #0088cc; -} - -.nav-stacked > li { - float: none; -} - -.nav-stacked > li > a { - margin-right: 0; -} - -.nav-tabs.nav-stacked { - border-bottom: 0; -} - -.nav-tabs.nav-stacked > li > a { - border: 1px solid #ddd; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.nav-tabs.nav-stacked > li:first-child > a { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-topright: 4px; - -moz-border-radius-topleft: 4px; -} - -.nav-tabs.nav-stacked > li:last-child > a { - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -moz-border-radius-bottomright: 4px; - -moz-border-radius-bottomleft: 4px; -} - -.nav-tabs.nav-stacked > li > a:hover, -.nav-tabs.nav-stacked > li > a:focus { - z-index: 2; - border-color: #ddd; -} - -.nav-pills.nav-stacked > li > a { - margin-bottom: 3px; -} - -.nav-pills.nav-stacked > li:last-child > a { - margin-bottom: 1px; -} - -.nav-tabs .dropdown-menu { - -webkit-border-radius: 0 0 6px 6px; - -moz-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; -} - -.nav-pills .dropdown-menu { - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.nav .dropdown-toggle .caret { - margin-top: 6px; - border-top-color: #0088cc; - border-bottom-color: #0088cc; -} - -.nav .dropdown-toggle:hover .caret, -.nav .dropdown-toggle:focus .caret { - border-top-color: #005580; - border-bottom-color: #005580; -} - -/* move down carets for tabs */ - -.nav-tabs .dropdown-toggle .caret { - margin-top: 8px; -} - -.nav .active .dropdown-toggle .caret { - border-top-color: #fff; - border-bottom-color: #fff; -} - -.nav-tabs .active .dropdown-toggle .caret { - border-top-color: #555555; - border-bottom-color: #555555; -} - -.nav > .dropdown.active > a:hover, -.nav > .dropdown.active > a:focus { - cursor: pointer; -} - -.nav-tabs .open .dropdown-toggle, -.nav-pills .open .dropdown-toggle, -.nav > li.dropdown.open.active > a:hover, -.nav > li.dropdown.open.active > a:focus { - color: #ffffff; - background-color: #999999; - border-color: #999999; -} - -.nav li.dropdown.open .caret, -.nav li.dropdown.open.active .caret, -.nav li.dropdown.open a:hover .caret, -.nav li.dropdown.open a:focus .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; - opacity: 1; - filter: alpha(opacity=100); -} - -.tabs-stacked .open > a:hover, -.tabs-stacked .open > a:focus { - border-color: #999999; -} - -.tabbable { - *zoom: 1; -} - -.tabbable:before, -.tabbable:after { - display: table; - line-height: 0; - content: ""; -} - -.tabbable:after { - clear: both; -} - -.tab-content { - overflow: auto; -} - -.tabs-below > .nav-tabs, -.tabs-right > .nav-tabs, -.tabs-left > .nav-tabs { - border-bottom: 0; -} - -.tab-content > .tab-pane, -.pill-content > .pill-pane { - display: none; -} - -.tab-content > .active, -.pill-content > .active { - display: block; -} - -.tabs-below > .nav-tabs { - border-top: 1px solid #ddd; -} - -.tabs-below > .nav-tabs > li { - margin-top: -1px; - margin-bottom: 0; -} - -.tabs-below > .nav-tabs > li > a { - -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius: 0 0 4px 4px; - border-radius: 0 0 4px 4px; -} - -.tabs-below > .nav-tabs > li > a:hover, -.tabs-below > .nav-tabs > li > a:focus { - border-top-color: #ddd; - border-bottom-color: transparent; -} - -.tabs-below > .nav-tabs > .active > a, -.tabs-below > .nav-tabs > .active > a:hover, -.tabs-below > .nav-tabs > .active > a:focus { - border-color: transparent #ddd #ddd #ddd; -} - -.tabs-left > .nav-tabs > li, -.tabs-right > .nav-tabs > li { - float: none; -} - -.tabs-left > .nav-tabs > li > a, -.tabs-right > .nav-tabs > li > a { - min-width: 74px; - margin-right: 0; - margin-bottom: 3px; -} - -.tabs-left > .nav-tabs { - float: left; - margin-right: 19px; - border-right: 1px solid #ddd; -} - -.tabs-left > .nav-tabs > li > a { - margin-right: -1px; - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.tabs-left > .nav-tabs > li > a:hover, -.tabs-left > .nav-tabs > li > a:focus { - border-color: #eeeeee #dddddd #eeeeee #eeeeee; -} - -.tabs-left > .nav-tabs .active > a, -.tabs-left > .nav-tabs .active > a:hover, -.tabs-left > .nav-tabs .active > a:focus { - border-color: #ddd transparent #ddd #ddd; - *border-right-color: #ffffff; -} - -.tabs-right > .nav-tabs { - float: right; - margin-left: 19px; - border-left: 1px solid #ddd; -} - -.tabs-right > .nav-tabs > li > a { - margin-left: -1px; - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.tabs-right > .nav-tabs > li > a:hover, -.tabs-right > .nav-tabs > li > a:focus { - border-color: #eeeeee #eeeeee #eeeeee #dddddd; -} - -.tabs-right > .nav-tabs .active > a, -.tabs-right > .nav-tabs .active > a:hover, -.tabs-right > .nav-tabs .active > a:focus { - border-color: #ddd #ddd #ddd transparent; - *border-left-color: #ffffff; -} - -.nav > .disabled > a { - color: #999999; -} - -.nav > .disabled > a:hover, -.nav > .disabled > a:focus { - text-decoration: none; - cursor: default; - background-color: transparent; -} - -.navbar { - *position: relative; - *z-index: 2; - margin-bottom: 20px; - overflow: visible; -} - -.navbar-inner { - min-height: 40px; - padding-right: 20px; - padding-left: 20px; - background-color: #fafafa; - background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); - background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); - background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); - background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); - background-repeat: repeat-x; - border: 1px solid #d4d4d4; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); - *zoom: 1; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); -} - -.navbar-inner:before, -.navbar-inner:after { - display: table; - line-height: 0; - content: ""; -} - -.navbar-inner:after { - clear: both; -} - -.navbar .container { - width: auto; -} - -.nav-collapse.collapse { - height: auto; - overflow: visible; -} - -.navbar .brand { - display: block; - float: left; - padding: 10px 20px 10px; - margin-left: -20px; - font-size: 20px; - font-weight: 200; - color: #777777; - text-shadow: 0 1px 0 #ffffff; -} - -.navbar .brand:hover, -.navbar .brand:focus { - text-decoration: none; -} - -.navbar-text { - margin-bottom: 0; - line-height: 40px; - color: #777777; -} - -.navbar-link { - color: #777777; -} - -.navbar-link:hover, -.navbar-link:focus { - color: #333333; -} - -.navbar .divider-vertical { - height: 40px; - margin: 0 9px; - border-right: 1px solid #ffffff; - border-left: 1px solid #f2f2f2; -} - -.navbar .btn, -.navbar .btn-group { - margin-top: 5px; -} - -.navbar .btn-group .btn, -.navbar .input-prepend .btn, -.navbar .input-append .btn, -.navbar .input-prepend .btn-group, -.navbar .input-append .btn-group { - margin-top: 0; -} - -.navbar-form { - margin-bottom: 0; - *zoom: 1; -} - -.navbar-form:before, -.navbar-form:after { - display: table; - line-height: 0; - content: ""; -} - -.navbar-form:after { - clear: both; -} - -.navbar-form input, -.navbar-form select, -.navbar-form .radio, -.navbar-form .checkbox { - margin-top: 5px; -} - -.navbar-form input, -.navbar-form select, -.navbar-form .btn { - display: inline-block; - margin-bottom: 0; -} - -.navbar-form input[type="image"], -.navbar-form input[type="checkbox"], -.navbar-form input[type="radio"] { - margin-top: 3px; -} - -.navbar-form .input-append, -.navbar-form .input-prepend { - margin-top: 5px; - white-space: nowrap; -} - -.navbar-form .input-append input, -.navbar-form .input-prepend input { - margin-top: 0; -} - -.navbar-search { - position: relative; - float: left; - margin-top: 5px; - margin-bottom: 0; -} - -.navbar-search .search-query { - padding: 4px 14px; - margin-bottom: 0; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - font-weight: normal; - line-height: 1; - -webkit-border-radius: 15px; - -moz-border-radius: 15px; - border-radius: 15px; -} - -.navbar-static-top { - position: static; - margin-bottom: 0; -} - -.navbar-static-top .navbar-inner { - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.navbar-fixed-top, -.navbar-fixed-bottom { - position: fixed; - right: 0; - left: 0; - z-index: 1030; - margin-bottom: 0; -} - -.navbar-fixed-top .navbar-inner, -.navbar-static-top .navbar-inner { - border-width: 0 0 1px; -} - -.navbar-fixed-bottom .navbar-inner { - border-width: 1px 0 0; -} - -.navbar-fixed-top .navbar-inner, -.navbar-fixed-bottom .navbar-inner { - padding-right: 0; - padding-left: 0; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.navbar-static-top .container, -.navbar-fixed-top .container, -.navbar-fixed-bottom .container { - width: 940px; -} - -.navbar-fixed-top { - top: 0; -} - -.navbar-fixed-top .navbar-inner, -.navbar-static-top .navbar-inner { - -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); -} - -.navbar-fixed-bottom { - bottom: 0; -} - -.navbar-fixed-bottom .navbar-inner { - -webkit-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); - box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); -} - -.navbar .nav { - position: relative; - left: 0; - display: block; - float: left; - margin: 0 10px 0 0; -} - -.navbar .nav.pull-right { - float: right; - margin-right: 0; -} - -.navbar .nav > li { - float: left; -} - -.navbar .nav > li > a { - float: none; - padding: 10px 15px 10px; - color: #777777; - text-decoration: none; - text-shadow: 0 1px 0 #ffffff; -} - -.navbar .nav .dropdown-toggle .caret { - margin-top: 8px; -} - -.navbar .nav > li > a:focus, -.navbar .nav > li > a:hover { - color: #333333; - text-decoration: none; - background-color: transparent; -} - -.navbar .nav > .active > a, -.navbar .nav > .active > a:hover, -.navbar .nav > .active > a:focus { - color: #555555; - text-decoration: none; - background-color: #e5e5e5; - -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); - -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); - box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); -} - -.navbar .btn-navbar { - display: none; - float: right; - padding: 7px 10px; - margin-right: 5px; - margin-left: 5px; - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #ededed; - *background-color: #e5e5e5; - background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5)); - background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5); - background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5); - background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5); - background-repeat: repeat-x; - border-color: #e5e5e5 #e5e5e5 #bfbfbf; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); -} - -.navbar .btn-navbar:hover, -.navbar .btn-navbar:focus, -.navbar .btn-navbar:active, -.navbar .btn-navbar.active, -.navbar .btn-navbar.disabled, -.navbar .btn-navbar[disabled] { - color: #ffffff; - background-color: #e5e5e5; - *background-color: #d9d9d9; -} - -.navbar .btn-navbar:active, -.navbar .btn-navbar.active { - background-color: #cccccc \9; -} - -.navbar .btn-navbar .icon-bar { - display: block; - width: 18px; - height: 2px; - background-color: #f5f5f5; - -webkit-border-radius: 1px; - -moz-border-radius: 1px; - border-radius: 1px; - -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); - -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); -} - -.btn-navbar .icon-bar + .icon-bar { - margin-top: 3px; -} - -.navbar .nav > li > .dropdown-menu:before { - position: absolute; - top: -7px; - left: 9px; - display: inline-block; - border-right: 7px solid transparent; - border-bottom: 7px solid #ccc; - border-left: 7px solid transparent; - border-bottom-color: rgba(0, 0, 0, 0.2); - content: ''; -} - -.navbar .nav > li > .dropdown-menu:after { - position: absolute; - top: -6px; - left: 10px; - display: inline-block; - border-right: 6px solid transparent; - border-bottom: 6px solid #ffffff; - border-left: 6px solid transparent; - content: ''; -} - -.navbar-fixed-bottom .nav > li > .dropdown-menu:before { - top: auto; - bottom: -7px; - border-top: 7px solid #ccc; - border-bottom: 0; - border-top-color: rgba(0, 0, 0, 0.2); -} - -.navbar-fixed-bottom .nav > li > .dropdown-menu:after { - top: auto; - bottom: -6px; - border-top: 6px solid #ffffff; - border-bottom: 0; -} - -.navbar .nav li.dropdown > a:hover .caret, -.navbar .nav li.dropdown > a:focus .caret { - border-top-color: #333333; - border-bottom-color: #333333; -} - -.navbar .nav li.dropdown.open > .dropdown-toggle, -.navbar .nav li.dropdown.active > .dropdown-toggle, -.navbar .nav li.dropdown.open.active > .dropdown-toggle { - color: #555555; - background-color: #e5e5e5; -} - -.navbar .nav li.dropdown > .dropdown-toggle .caret { - border-top-color: #777777; - border-bottom-color: #777777; -} - -.navbar .nav li.dropdown.open > .dropdown-toggle .caret, -.navbar .nav li.dropdown.active > .dropdown-toggle .caret, -.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { - border-top-color: #555555; - border-bottom-color: #555555; -} - -.navbar .pull-right > li > .dropdown-menu, -.navbar .nav > li > .dropdown-menu.pull-right { - right: 0; - left: auto; -} - -.navbar .pull-right > li > .dropdown-menu:before, -.navbar .nav > li > .dropdown-menu.pull-right:before { - right: 12px; - left: auto; -} - -.navbar .pull-right > li > .dropdown-menu:after, -.navbar .nav > li > .dropdown-menu.pull-right:after { - right: 13px; - left: auto; -} - -.navbar .pull-right > li > .dropdown-menu .dropdown-menu, -.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu { - right: 100%; - left: auto; - margin-right: -1px; - margin-left: 0; - -webkit-border-radius: 6px 0 6px 6px; - -moz-border-radius: 6px 0 6px 6px; - border-radius: 6px 0 6px 6px; -} - -.navbar-inverse .navbar-inner { - background-color: #1b1b1b; - background-image: -moz-linear-gradient(top, #222222, #111111); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); - background-image: -webkit-linear-gradient(top, #222222, #111111); - background-image: -o-linear-gradient(top, #222222, #111111); - background-image: linear-gradient(to bottom, #222222, #111111); - background-repeat: repeat-x; - border-color: #252525; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); -} - -.navbar-inverse .brand, -.navbar-inverse .nav > li > a { - color: #999999; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); -} - -.navbar-inverse .brand:hover, -.navbar-inverse .nav > li > a:hover, -.navbar-inverse .brand:focus, -.navbar-inverse .nav > li > a:focus { - color: #ffffff; -} - -.navbar-inverse .brand { - color: #999999; -} - -.navbar-inverse .navbar-text { - color: #999999; -} - -.navbar-inverse .nav > li > a:focus, -.navbar-inverse .nav > li > a:hover { - color: #ffffff; - background-color: transparent; -} - -.navbar-inverse .nav .active > a, -.navbar-inverse .nav .active > a:hover, -.navbar-inverse .nav .active > a:focus { - color: #ffffff; - background-color: #111111; -} - -.navbar-inverse .navbar-link { - color: #999999; -} - -.navbar-inverse .navbar-link:hover, -.navbar-inverse .navbar-link:focus { - color: #ffffff; -} - -.navbar-inverse .divider-vertical { - border-right-color: #222222; - border-left-color: #111111; -} - -.navbar-inverse .nav li.dropdown.open > .dropdown-toggle, -.navbar-inverse .nav li.dropdown.active > .dropdown-toggle, -.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle { - color: #ffffff; - background-color: #111111; -} - -.navbar-inverse .nav li.dropdown > a:hover .caret, -.navbar-inverse .nav li.dropdown > a:focus .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; -} - -.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret { - border-top-color: #999999; - border-bottom-color: #999999; -} - -.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret, -.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret, -.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; -} - -.navbar-inverse .navbar-search .search-query { - color: #ffffff; - background-color: #515151; - border-color: #111111; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); - -webkit-transition: none; - -moz-transition: none; - -o-transition: none; - transition: none; -} - -.navbar-inverse .navbar-search .search-query:-moz-placeholder { - color: #cccccc; -} - -.navbar-inverse .navbar-search .search-query:-ms-input-placeholder { - color: #cccccc; -} - -.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder { - color: #cccccc; -} - -.navbar-inverse .navbar-search .search-query:focus, -.navbar-inverse .navbar-search .search-query.focused { - padding: 5px 15px; - color: #333333; - text-shadow: 0 1px 0 #ffffff; - background-color: #ffffff; - border: 0; - outline: 0; - -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); - -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); - box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); -} - -.navbar-inverse .btn-navbar { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #0e0e0e; - *background-color: #040404; - background-image: -moz-linear-gradient(top, #151515, #040404); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404)); - background-image: -webkit-linear-gradient(top, #151515, #040404); - background-image: -o-linear-gradient(top, #151515, #040404); - background-image: linear-gradient(to bottom, #151515, #040404); - background-repeat: repeat-x; - border-color: #040404 #040404 #000000; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.navbar-inverse .btn-navbar:hover, -.navbar-inverse .btn-navbar:focus, -.navbar-inverse .btn-navbar:active, -.navbar-inverse .btn-navbar.active, -.navbar-inverse .btn-navbar.disabled, -.navbar-inverse .btn-navbar[disabled] { - color: #ffffff; - background-color: #040404; - *background-color: #000000; -} - -.navbar-inverse .btn-navbar:active, -.navbar-inverse .btn-navbar.active { - background-color: #000000 \9; -} - -.breadcrumb { - padding: 8px 15px; - margin: 0 0 20px; - list-style: none; - background-color: #f5f5f5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.breadcrumb > li { - display: inline-block; - *display: inline; - text-shadow: 0 1px 0 #ffffff; - *zoom: 1; -} - -.breadcrumb > li > .divider { - padding: 0 5px; - color: #ccc; -} - -.breadcrumb > .active { - color: #999999; -} - -.pagination { - margin: 20px 0; -} - -.pagination ul { - display: inline-block; - *display: inline; - margin-bottom: 0; - margin-left: 0; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - *zoom: 1; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.pagination ul > li { - display: inline; -} - -.pagination ul > li > a, -.pagination ul > li > span { - float: left; - padding: 4px 12px; - line-height: 20px; - text-decoration: none; - background-color: #ffffff; - border: 1px solid #dddddd; - border-left-width: 0; -} - -.pagination ul > li > a:hover, -.pagination ul > li > a:focus, -.pagination ul > .active > a, -.pagination ul > .active > span { - background-color: #f5f5f5; -} - -.pagination ul > .active > a, -.pagination ul > .active > span { - color: #999999; - cursor: default; -} - -.pagination ul > .disabled > span, -.pagination ul > .disabled > a, -.pagination ul > .disabled > a:hover, -.pagination ul > .disabled > a:focus { - color: #999999; - cursor: default; - background-color: transparent; -} - -.pagination ul > li:first-child > a, -.pagination ul > li:first-child > span { - border-left-width: 1px; - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; - -moz-border-radius-topleft: 4px; -} - -.pagination ul > li:last-child > a, -.pagination ul > li:last-child > span { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -moz-border-radius-topright: 4px; - -moz-border-radius-bottomright: 4px; -} - -.pagination-centered { - text-align: center; -} - -.pagination-right { - text-align: right; -} - -.pagination-large ul > li > a, -.pagination-large ul > li > span { - padding: 11px 19px; - font-size: 17.5px; -} - -.pagination-large ul > li:first-child > a, -.pagination-large ul > li:first-child > span { - -webkit-border-bottom-left-radius: 6px; - border-bottom-left-radius: 6px; - -webkit-border-top-left-radius: 6px; - border-top-left-radius: 6px; - -moz-border-radius-bottomleft: 6px; - -moz-border-radius-topleft: 6px; -} - -.pagination-large ul > li:last-child > a, -.pagination-large ul > li:last-child > span { - -webkit-border-top-right-radius: 6px; - border-top-right-radius: 6px; - -webkit-border-bottom-right-radius: 6px; - border-bottom-right-radius: 6px; - -moz-border-radius-topright: 6px; - -moz-border-radius-bottomright: 6px; -} - -.pagination-mini ul > li:first-child > a, -.pagination-small ul > li:first-child > a, -.pagination-mini ul > li:first-child > span, -.pagination-small ul > li:first-child > span { - -webkit-border-bottom-left-radius: 3px; - border-bottom-left-radius: 3px; - -webkit-border-top-left-radius: 3px; - border-top-left-radius: 3px; - -moz-border-radius-bottomleft: 3px; - -moz-border-radius-topleft: 3px; -} - -.pagination-mini ul > li:last-child > a, -.pagination-small ul > li:last-child > a, -.pagination-mini ul > li:last-child > span, -.pagination-small ul > li:last-child > span { - -webkit-border-top-right-radius: 3px; - border-top-right-radius: 3px; - -webkit-border-bottom-right-radius: 3px; - border-bottom-right-radius: 3px; - -moz-border-radius-topright: 3px; - -moz-border-radius-bottomright: 3px; -} - -.pagination-small ul > li > a, -.pagination-small ul > li > span { - padding: 2px 10px; - font-size: 11.9px; -} - -.pagination-mini ul > li > a, -.pagination-mini ul > li > span { - padding: 0 6px; - font-size: 10.5px; -} - -.pager { - margin: 20px 0; - text-align: center; - list-style: none; - *zoom: 1; -} - -.pager:before, -.pager:after { - display: table; - line-height: 0; - content: ""; -} - -.pager:after { - clear: both; -} - -.pager li { - display: inline; -} - -.pager li > a, -.pager li > span { - display: inline-block; - padding: 5px 14px; - background-color: #fff; - border: 1px solid #ddd; - -webkit-border-radius: 15px; - -moz-border-radius: 15px; - border-radius: 15px; -} - -.pager li > a:hover, -.pager li > a:focus { - text-decoration: none; - background-color: #f5f5f5; -} - -.pager .next > a, -.pager .next > span { - float: right; -} - -.pager .previous > a, -.pager .previous > span { - float: left; -} - -.pager .disabled > a, -.pager .disabled > a:hover, -.pager .disabled > a:focus, -.pager .disabled > span { - color: #999999; - cursor: default; - background-color: #fff; -} - -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - background-color: #000000; -} - -.modal-backdrop.fade { - opacity: 0; -} - -.modal-backdrop, -.modal-backdrop.fade.in { - opacity: 0.8; - filter: alpha(opacity=80); -} - -.modal { - position: fixed; - top: 10%; - left: 50%; - z-index: 1050; - width: 560px; - margin-left: -280px; - background-color: #ffffff; - border: 1px solid #999; - border: 1px solid rgba(0, 0, 0, 0.3); - *border: 1px solid #999; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - outline: none; - -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - -webkit-background-clip: padding-box; - -moz-background-clip: padding-box; - background-clip: padding-box; -} - -.modal.fade { - top: -25%; - -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; - -moz-transition: opacity 0.3s linear, top 0.3s ease-out; - -o-transition: opacity 0.3s linear, top 0.3s ease-out; - transition: opacity 0.3s linear, top 0.3s ease-out; -} - -.modal.fade.in { - top: 10%; -} - -.modal-header { - padding: 9px 15px; - border-bottom: 1px solid #eee; -} - -.modal-header .close { - margin-top: 2px; -} - -.modal-header h3 { - margin: 0; - line-height: 30px; -} - -.modal-body { - position: relative; - max-height: 400px; - padding: 15px; - overflow-y: auto; -} - -.modal-form { - margin-bottom: 0; -} - -.modal-footer { - padding: 14px 15px 15px; - margin-bottom: 0; - text-align: right; - background-color: #f5f5f5; - border-top: 1px solid #ddd; - -webkit-border-radius: 0 0 6px 6px; - -moz-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; - *zoom: 1; - -webkit-box-shadow: inset 0 1px 0 #ffffff; - -moz-box-shadow: inset 0 1px 0 #ffffff; - box-shadow: inset 0 1px 0 #ffffff; -} - -.modal-footer:before, -.modal-footer:after { - display: table; - line-height: 0; - content: ""; -} - -.modal-footer:after { - clear: both; -} - -.modal-footer .btn + .btn { - margin-bottom: 0; - margin-left: 5px; -} - -.modal-footer .btn-group .btn + .btn { - margin-left: -1px; -} - -.modal-footer .btn-block + .btn-block { - margin-left: 0; -} - -.tooltip { - position: absolute; - z-index: 1030; - display: block; - font-size: 11px; - line-height: 1.4; - opacity: 0; - filter: alpha(opacity=0); - visibility: visible; -} - -.tooltip.in { - opacity: 0.8; - filter: alpha(opacity=80); -} - -.tooltip.top { - padding: 5px 0; - margin-top: -3px; -} - -.tooltip.right { - padding: 0 5px; - margin-left: 3px; -} - -.tooltip.bottom { - padding: 5px 0; - margin-top: 3px; -} - -.tooltip.left { - padding: 0 5px; - margin-left: -3px; -} - -.tooltip-inner { - max-width: 200px; - padding: 8px; - color: #ffffff; - text-align: center; - text-decoration: none; - background-color: #000000; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} - -.tooltip.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-top-color: #000000; - border-width: 5px 5px 0; -} - -.tooltip.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-right-color: #000000; - border-width: 5px 5px 5px 0; -} - -.tooltip.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-left-color: #000000; - border-width: 5px 0 5px 5px; -} - -.tooltip.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-bottom-color: #000000; - border-width: 0 5px 5px; -} - -.popover { - position: absolute; - top: 0; - left: 0; - z-index: 1010; - display: none; - max-width: 276px; - padding: 1px; - text-align: left; - white-space: normal; - background-color: #ffffff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; -} - -.popover.top { - margin-top: -10px; -} - -.popover.right { - margin-left: 10px; -} - -.popover.bottom { - margin-top: 10px; -} - -.popover.left { - margin-left: -10px; -} - -.popover-title { - padding: 8px 14px; - margin: 0; - font-size: 14px; - font-weight: normal; - line-height: 18px; - background-color: #f7f7f7; - border-bottom: 1px solid #ebebeb; - -webkit-border-radius: 5px 5px 0 0; - -moz-border-radius: 5px 5px 0 0; - border-radius: 5px 5px 0 0; -} - -.popover-title:empty { - display: none; -} - -.popover-content { - padding: 9px 14px; -} - -.popover .arrow, -.popover .arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} - -.popover .arrow { - border-width: 11px; -} - -.popover .arrow:after { - border-width: 10px; - content: ""; -} - -.popover.top .arrow { - bottom: -11px; - left: 50%; - margin-left: -11px; - border-top-color: #999; - border-top-color: rgba(0, 0, 0, 0.25); - border-bottom-width: 0; -} - -.popover.top .arrow:after { - bottom: 1px; - margin-left: -10px; - border-top-color: #ffffff; - border-bottom-width: 0; -} - -.popover.right .arrow { - top: 50%; - left: -11px; - margin-top: -11px; - border-right-color: #999; - border-right-color: rgba(0, 0, 0, 0.25); - border-left-width: 0; -} - -.popover.right .arrow:after { - bottom: -10px; - left: 1px; - border-right-color: #ffffff; - border-left-width: 0; -} - -.popover.bottom .arrow { - top: -11px; - left: 50%; - margin-left: -11px; - border-bottom-color: #999; - border-bottom-color: rgba(0, 0, 0, 0.25); - border-top-width: 0; -} - -.popover.bottom .arrow:after { - top: 1px; - margin-left: -10px; - border-bottom-color: #ffffff; - border-top-width: 0; -} - -.popover.left .arrow { - top: 50%; - right: -11px; - margin-top: -11px; - border-left-color: #999; - border-left-color: rgba(0, 0, 0, 0.25); - border-right-width: 0; -} - -.popover.left .arrow:after { - right: 1px; - bottom: -10px; - border-left-color: #ffffff; - border-right-width: 0; -} - -.thumbnails { - margin-left: -20px; - list-style: none; - *zoom: 1; -} - -.thumbnails:before, -.thumbnails:after { - display: table; - line-height: 0; - content: ""; -} - -.thumbnails:after { - clear: both; -} - -.row-fluid .thumbnails { - margin-left: 0; -} - -.thumbnails > li { - float: left; - margin-bottom: 20px; - margin-left: 20px; -} - -.thumbnail { - display: block; - padding: 4px; - line-height: 20px; - border: 1px solid #ddd; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); - -webkit-transition: all 0.2s ease-in-out; - -moz-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; -} - -a.thumbnail:hover, -a.thumbnail:focus { - border-color: #0088cc; - -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); - -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); - box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); -} - -.thumbnail > img { - display: block; - max-width: 100%; - margin-right: auto; - margin-left: auto; -} - -.thumbnail .caption { - padding: 9px; - color: #555555; -} - -.media, -.media-body { - overflow: hidden; - *overflow: visible; - zoom: 1; -} - -.media, -.media .media { - margin-top: 15px; -} - -.media:first-child { - margin-top: 0; -} - -.media-object { - display: block; -} - -.media-heading { - margin: 0 0 5px; -} - -.media > .pull-left { - margin-right: 10px; -} - -.media > .pull-right { - margin-left: 10px; -} - -.media-list { - margin-left: 0; - list-style: none; -} - -.label, -.badge { - display: inline-block; - padding: 2px 4px; - font-size: 11.844px; - font-weight: bold; - line-height: 14px; - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - white-space: nowrap; - vertical-align: baseline; - background-color: #999999; -} - -.label { - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.badge { - padding-right: 9px; - padding-left: 9px; - -webkit-border-radius: 9px; - -moz-border-radius: 9px; - border-radius: 9px; -} - -.label:empty, -.badge:empty { - display: none; -} - -a.label:hover, -a.label:focus, -a.badge:hover, -a.badge:focus { - color: #ffffff; - text-decoration: none; - cursor: pointer; -} - -.label-important, -.badge-important { - background-color: #b94a48; -} - -.label-important[href], -.badge-important[href] { - background-color: #953b39; -} - -.label-warning, -.badge-warning { - background-color: #f89406; -} - -.label-warning[href], -.badge-warning[href] { - background-color: #c67605; -} - -.label-success, -.badge-success { - background-color: #468847; -} - -.label-success[href], -.badge-success[href] { - background-color: #356635; -} - -.label-info, -.badge-info { - background-color: #3a87ad; -} - -.label-info[href], -.badge-info[href] { - background-color: #2d6987; -} - -.label-inverse, -.badge-inverse { - background-color: #333333; -} - -.label-inverse[href], -.badge-inverse[href] { - background-color: #1a1a1a; -} - -.btn .label, -.btn .badge { - position: relative; - top: -1px; -} - -.btn-mini .label, -.btn-mini .badge { - top: 0; -} - -@-webkit-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -@-moz-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -@-ms-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -@-o-keyframes progress-bar-stripes { - from { - background-position: 0 0; - } - to { - background-position: 40px 0; - } -} - -@keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -.progress { - height: 20px; - margin-bottom: 20px; - overflow: hidden; - background-color: #f7f7f7; - background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); - background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); - background-repeat: repeat-x; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); -} - -.progress .bar { - float: left; - width: 0; - height: 100%; - font-size: 12px; - color: #ffffff; - text-align: center; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #0e90d2; - background-image: -moz-linear-gradient(top, #149bdf, #0480be); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); - background-image: -webkit-linear-gradient(top, #149bdf, #0480be); - background-image: -o-linear-gradient(top, #149bdf, #0480be); - background-image: linear-gradient(to bottom, #149bdf, #0480be); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-transition: width 0.6s ease; - -moz-transition: width 0.6s ease; - -o-transition: width 0.6s ease; - transition: width 0.6s ease; -} - -.progress .bar + .bar { - -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); - box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); -} - -.progress-striped .bar { - background-color: #149bdf; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - -moz-background-size: 40px 40px; - -o-background-size: 40px 40px; - background-size: 40px 40px; -} - -.progress.active .bar { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -moz-animation: progress-bar-stripes 2s linear infinite; - -ms-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; -} - -.progress-danger .bar, -.progress .bar-danger { - background-color: #dd514c; - background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); - background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); - background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0); -} - -.progress-danger.progress-striped .bar, -.progress-striped .bar-danger { - background-color: #ee5f5b; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.progress-success .bar, -.progress .bar-success { - background-color: #5eb95e; - background-image: -moz-linear-gradient(top, #62c462, #57a957); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); - background-image: -webkit-linear-gradient(top, #62c462, #57a957); - background-image: -o-linear-gradient(top, #62c462, #57a957); - background-image: linear-gradient(to bottom, #62c462, #57a957); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0); -} - -.progress-success.progress-striped .bar, -.progress-striped .bar-success { - background-color: #62c462; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.progress-info .bar, -.progress .bar-info { - background-color: #4bb1cf; - background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); - background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); - background-image: -o-linear-gradient(top, #5bc0de, #339bb9); - background-image: linear-gradient(to bottom, #5bc0de, #339bb9); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0); -} - -.progress-info.progress-striped .bar, -.progress-striped .bar-info { - background-color: #5bc0de; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.progress-warning .bar, -.progress .bar-warning { - background-color: #faa732; - background-image: -moz-linear-gradient(top, #fbb450, #f89406); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); - background-image: -webkit-linear-gradient(top, #fbb450, #f89406); - background-image: -o-linear-gradient(top, #fbb450, #f89406); - background-image: linear-gradient(to bottom, #fbb450, #f89406); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); -} - -.progress-warning.progress-striped .bar, -.progress-striped .bar-warning { - background-color: #fbb450; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.accordion { - margin-bottom: 20px; -} - -.accordion-group { - margin-bottom: 2px; - border: 1px solid #e5e5e5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.accordion-heading { - border-bottom: 0; -} - -.accordion-heading .accordion-toggle { - display: block; - padding: 8px 15px; -} - -.accordion-toggle { - cursor: pointer; -} - -.accordion-inner { - padding: 9px 15px; - border-top: 1px solid #e5e5e5; -} - -.carousel { - position: relative; - margin-bottom: 20px; - line-height: 1; -} - -.carousel-inner { - position: relative; - width: 100%; - overflow: hidden; -} - -.carousel-inner > .item { - position: relative; - display: none; - -webkit-transition: 0.6s ease-in-out left; - -moz-transition: 0.6s ease-in-out left; - -o-transition: 0.6s ease-in-out left; - transition: 0.6s ease-in-out left; -} - -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - display: block; - line-height: 1; -} - -.carousel-inner > .active, -.carousel-inner > .next, -.carousel-inner > .prev { - display: block; -} - -.carousel-inner > .active { - left: 0; -} - -.carousel-inner > .next, -.carousel-inner > .prev { - position: absolute; - top: 0; - width: 100%; -} - -.carousel-inner > .next { - left: 100%; -} - -.carousel-inner > .prev { - left: -100%; -} - -.carousel-inner > .next.left, -.carousel-inner > .prev.right { - left: 0; -} - -.carousel-inner > .active.left { - left: -100%; -} - -.carousel-inner > .active.right { - left: 100%; -} - -.carousel-control { - position: absolute; - top: 40%; - left: 15px; - width: 40px; - height: 40px; - margin-top: -20px; - font-size: 60px; - font-weight: 100; - line-height: 30px; - color: #ffffff; - text-align: center; - background: #222222; - border: 3px solid #ffffff; - -webkit-border-radius: 23px; - -moz-border-radius: 23px; - border-radius: 23px; - opacity: 0.5; - filter: alpha(opacity=50); -} - -.carousel-control.right { - right: 15px; - left: auto; -} - -.carousel-control:hover, -.carousel-control:focus { - color: #ffffff; - text-decoration: none; - opacity: 0.9; - filter: alpha(opacity=90); -} - -.carousel-indicators { - position: absolute; - top: 15px; - right: 15px; - z-index: 5; - margin: 0; - list-style: none; -} - -.carousel-indicators li { - display: block; - float: left; - width: 10px; - height: 10px; - margin-left: 5px; - text-indent: -999px; - background-color: #ccc; - background-color: rgba(255, 255, 255, 0.25); - border-radius: 5px; -} - -.carousel-indicators .active { - background-color: #fff; -} - -.carousel-caption { - position: absolute; - right: 0; - bottom: 0; - left: 0; - padding: 15px; - background: #333333; - background: rgba(0, 0, 0, 0.75); -} - -.carousel-caption h4, -.carousel-caption p { - line-height: 20px; - color: #ffffff; -} - -.carousel-caption h4 { - margin: 0 0 5px; -} - -.carousel-caption p { - margin-bottom: 0; -} - -.hero-unit { - padding: 60px; - margin-bottom: 30px; - font-size: 18px; - font-weight: 200; - line-height: 30px; - color: inherit; - background-color: #eeeeee; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.hero-unit h1 { - margin-bottom: 0; - font-size: 60px; - line-height: 1; - letter-spacing: -1px; - color: inherit; -} - -.hero-unit li { - line-height: 30px; -} - -.pull-right { - float: right; -} - -.pull-left { - float: left; -} - -.hide { - display: none; -} - -.show { - display: block; -} - -.invisible { - visibility: hidden; -} - -.affix { - position: fixed; -} diff --git a/apps/bootstrap/css/bootstrap.min.css b/apps/bootstrap/css/bootstrap.min.css deleted file mode 100644 index c10c7f4..0000000 --- a/apps/bootstrap/css/bootstrap.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Bootstrap v2.3.1 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover,a:focus{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}a.muted:hover,a.muted:focus{color:#808080}.text-warning{color:#c09853}a.text-warning:hover,a.text-warning:focus{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover,a.text-error:focus{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover,a.text-info:focus{color:#2d6987}.text-success{color:#468847}a.text-success:hover,a.text-success:focus{color:#356635}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;padding-right:5px;padding-left:5px;*zoom:1}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:17.5px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#555;vertical-align:middle;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{display:inline-block;margin-bottom:10px;font-size:0;white-space:nowrap;vertical-align:middle}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu,.input-append .popover,.input-prepend .popover{font-size:14px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:#f5f5f5}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#dff0d8}.table tbody tr.error>td{background-color:#f2dede}.table tbody tr.warning>td{background-color:#fcf8e3}.table tbody tr.info>td{background-color:#d9edf7}.table-hover tbody tr.success:hover>td{background-color:#d0e9c6}.table-hover tbody tr.error:hover>td{background-color:#ebcccc}.table-hover tbody tr.warning:hover>td{background-color:#faf2cc}.table-hover tbody tr.info:hover>td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{width:16px;background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success h4{color:#468847}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777}.navbar-link{color:#777}.navbar-link:hover,.navbar-link:focus{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:focus{color:#fff}.navbar-inverse .brand{color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-moz-linear-gradient(top,#151515,#040404);background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px;-moz-border-radius-bottomleft:3px;-moz-border-radius-topleft:3px}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px;-moz-border-radius-topright:3px;-moz-border-radius-bottomright:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0 6px;font-size:10.5px}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#f5f5f5}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover,a.thumbnail:focus{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-indicators{position:absolute;top:15px;right:15px;z-index:5;margin:0;list-style:none}.carousel-indicators li{display:block;float:left;width:10px;height:10px;margin-left:5px;text-indent:-999px;background-color:#ccc;background-color:rgba(255,255,255,0.25);border-radius:5px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:30px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:30px}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/apps/bootstrap/css/site.css b/apps/bootstrap/css/site.css deleted file mode 100644 index 890a953..0000000 --- a/apps/bootstrap/css/site.css +++ /dev/null @@ -1,78 +0,0 @@ -body { - padding-top: 20px; - padding-bottom: 60px; -} - -/* Custom container */ -.container { - margin: 0 auto; - max-width: 1000px; -} - -.container > hr { - margin: 60px 0; -} - -/* Main marketing message and sign up button */ -.jumbotron { - margin: 80px 0; - text-align: center; -} - -.jumbotron h1 { - font-size: 100px; - line-height: 1; -} - -.jumbotron .lead { - font-size: 24px; - line-height: 1.25; -} - -.jumbotron .btn { - font-size: 21px; - padding: 14px 24px; -} - -/* Supporting marketing content */ -.marketing { - margin: 60px 0; -} - -.marketing p + h4 { - margin-top: 28px; -} - -/* Customize the navbar links to be fill the entire space of the .navbar */ -.navbar .navbar-inner { - padding: 0; -} - -.navbar .nav { - margin: 0; - display: table; - width: 100%; -} - -.navbar .nav li { - display: table-cell; - width: 1%; - float: none; -} - -.navbar .nav li a { - font-weight: bold; - text-align: center; - border-left: 1px solid rgba(255, 255, 255, .75); - border-right: 1px solid rgba(0, 0, 0, .1); -} - -.navbar .nav li:first-child a { - border-left: 0; - border-radius: 3px 0 0 3px; -} - -.navbar .nav li:last-child a { - border-right: 0; - border-radius: 0 3px 3px 0; -} diff --git a/apps/bootstrap/img/glyphicons-halflings-white.png b/apps/bootstrap/img/glyphicons-halflings-white.png deleted file mode 100644 index 3bf6484..0000000 Binary files a/apps/bootstrap/img/glyphicons-halflings-white.png and /dev/null differ diff --git a/apps/bootstrap/img/glyphicons-halflings.png b/apps/bootstrap/img/glyphicons-halflings.png deleted file mode 100644 index a996999..0000000 Binary files a/apps/bootstrap/img/glyphicons-halflings.png and /dev/null differ diff --git a/apps/bootstrap/index.php b/apps/bootstrap/index.php deleted file mode 100644 index a0488ca..0000000 --- a/apps/bootstrap/index.php +++ /dev/null @@ -1,14 +0,0 @@ -<?php - -// comment out the following line to disable debug mode -defined('YII_DEBUG') or define('YII_DEBUG', true); - -$frameworkPath = __DIR__ . '/../../yii'; - -require($frameworkPath . '/Yii.php'); -// Register Composer autoloader -@include($frameworkPath . '/vendor/autoload.php'); - -$config = require(__DIR__ . '/protected/config/main.php'); -$application = new yii\web\Application($config); -$application->run(); diff --git a/apps/bootstrap/js/bootstrap.js b/apps/bootstrap/js/bootstrap.js deleted file mode 100644 index c298ee4..0000000 --- a/apps/bootstrap/js/bootstrap.js +++ /dev/null @@ -1,2276 +0,0 @@ -/* =================================================== - * bootstrap-transition.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#transitions - * =================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) - * ======================================================= */ - - $(function () { - - $.support.transition = (function () { - - var transitionEnd = (function () { - - var el = document.createElement('bootstrap') - , transEndEventNames = { - 'WebkitTransition' : 'webkitTransitionEnd' - , 'MozTransition' : 'transitionend' - , 'OTransition' : 'oTransitionEnd otransitionend' - , 'transition' : 'transitionend' - } - , name - - for (name in transEndEventNames){ - if (el.style[name] !== undefined) { - return transEndEventNames[name] - } - } - - }()) - - return transitionEnd && { - end: transitionEnd - } - - })() - - }) - -}(window.jQuery);/* ========================================================== - * bootstrap-alert.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#alerts - * ========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* ALERT CLASS DEFINITION - * ====================== */ - - var dismiss = '[data-dismiss="alert"]' - , Alert = function (el) { - $(el).on('click', dismiss, this.close) - } - - Alert.prototype.close = function (e) { - var $this = $(this) - , selector = $this.attr('data-target') - , $parent - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } - - $parent = $(selector) - - e && e.preventDefault() - - $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) - - $parent.trigger(e = $.Event('close')) - - if (e.isDefaultPrevented()) return - - $parent.removeClass('in') - - function removeElement() { - $parent - .trigger('closed') - .remove() - } - - $.support.transition && $parent.hasClass('fade') ? - $parent.on($.support.transition.end, removeElement) : - removeElement() - } - - - /* ALERT PLUGIN DEFINITION - * ======================= */ - - var old = $.fn.alert - - $.fn.alert = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('alert') - if (!data) $this.data('alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - $.fn.alert.Constructor = Alert - - - /* ALERT NO CONFLICT - * ================= */ - - $.fn.alert.noConflict = function () { - $.fn.alert = old - return this - } - - - /* ALERT DATA-API - * ============== */ - - $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) - -}(window.jQuery);/* ============================================================ - * bootstrap-button.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#buttons - * ============================================================ - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* BUTTON PUBLIC CLASS DEFINITION - * ============================== */ - - var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.button.defaults, options) - } - - Button.prototype.setState = function (state) { - var d = 'disabled' - , $el = this.$element - , data = $el.data() - , val = $el.is('input') ? 'val' : 'html' - - state = state + 'Text' - data.resetText || $el.data('resetText', $el[val]()) - - $el[val](data[state] || this.options[state]) - - // push to event loop to allow forms to submit - setTimeout(function () { - state == 'loadingText' ? - $el.addClass(d).attr(d, d) : - $el.removeClass(d).removeAttr(d) - }, 0) - } - - Button.prototype.toggle = function () { - var $parent = this.$element.closest('[data-toggle="buttons-radio"]') - - $parent && $parent - .find('.active') - .removeClass('active') - - this.$element.toggleClass('active') - } - - - /* BUTTON PLUGIN DEFINITION - * ======================== */ - - var old = $.fn.button - - $.fn.button = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('button') - , options = typeof option == 'object' && option - if (!data) $this.data('button', (data = new Button(this, options))) - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } - - $.fn.button.defaults = { - loadingText: 'loading...' - } - - $.fn.button.Constructor = Button - - - /* BUTTON NO CONFLICT - * ================== */ - - $.fn.button.noConflict = function () { - $.fn.button = old - return this - } - - - /* BUTTON DATA-API - * =============== */ - - $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - $btn.button('toggle') - }) - -}(window.jQuery);/* ========================================================== - * bootstrap-carousel.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#carousel - * ========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* CAROUSEL CLASS DEFINITION - * ========================= */ - - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.options.pause == 'hover' && this.$element - .on('mouseenter', $.proxy(this.pause, this)) - .on('mouseleave', $.proxy(this.cycle, this)) - } - - Carousel.prototype = { - - cycle: function (e) { - if (!e) this.paused = false - if (this.interval) clearInterval(this.interval); - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - return this - } - - , getActiveIndex: function () { - this.$active = this.$element.find('.item.active') - this.$items = this.$active.parent().children() - return this.$items.index(this.$active) - } - - , to: function (pos) { - var activeIndex = this.getActiveIndex() - , that = this - - if (pos > (this.$items.length - 1) || pos < 0) return - - if (this.sliding) { - return this.$element.one('slid', function () { - that.to(pos) - }) - } - - if (activeIndex == pos) { - return this.pause().cycle() - } - - return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) - } - - , pause: function (e) { - if (!e) this.paused = true - if (this.$element.find('.next, .prev').length && $.support.transition.end) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - clearInterval(this.interval) - this.interval = null - return this - } - - , next: function () { - if (this.sliding) return - return this.slide('next') - } - - , prev: function () { - if (this.sliding) return - return this.slide('prev') - } - - , slide: function (type, next) { - var $active = this.$element.find('.item.active') - , $next = next || $active[type]() - , isCycling = this.interval - , direction = type == 'next' ? 'left' : 'right' - , fallback = type == 'next' ? 'first' : 'last' - , that = this - , e - - this.sliding = true - - isCycling && this.pause() - - $next = $next.length ? $next : this.$element.find('.item')[fallback]() - - e = $.Event('slide', { - relatedTarget: $next[0] - , direction: direction - }) - - if ($next.hasClass('active')) return - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - this.$element.one('slid', function () { - var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) - $nextIndicator && $nextIndicator.addClass('active') - }) - } - - if ($.support.transition && this.$element.hasClass('slide')) { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - this.$element.one($.support.transition.end, function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { that.$element.trigger('slid') }, 0) - }) - } else { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger('slid') - } - - isCycling && this.cycle() - - return this - } - - } - - - /* CAROUSEL PLUGIN DEFINITION - * ========================== */ - - var old = $.fn.carousel - - $.fn.carousel = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('carousel') - , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) - , action = typeof option == 'string' ? option : options.slide - if (!data) $this.data('carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) - } - - $.fn.carousel.defaults = { - interval: 5000 - , pause: 'hover' - } - - $.fn.carousel.Constructor = Carousel - - - /* CAROUSEL NO CONFLICT - * ==================== */ - - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this - } - - /* CAROUSEL DATA-API - * ================= */ - - $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { - var $this = $(this), href - , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 - , options = $.extend({}, $target.data(), $this.data()) - , slideIndex - - $target.carousel(options) - - if (slideIndex = $this.attr('data-slide-to')) { - $target.data('carousel').pause().to(slideIndex).cycle() - } - - e.preventDefault() - }) - -}(window.jQuery);/* ============================================================= - * bootstrap-collapse.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#collapse - * ============================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* COLLAPSE PUBLIC CLASS DEFINITION - * ================================ */ - - var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.collapse.defaults, options) - - if (this.options.parent) { - this.$parent = $(this.options.parent) - } - - this.options.toggle && this.toggle() - } - - Collapse.prototype = { - - constructor: Collapse - - , dimension: function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - , show: function () { - var dimension - , scroll - , actives - , hasData - - if (this.transitioning || this.$element.hasClass('in')) return - - dimension = this.dimension() - scroll = $.camelCase(['scroll', dimension].join('-')) - actives = this.$parent && this.$parent.find('> .accordion-group > .in') - - if (actives && actives.length) { - hasData = actives.data('collapse') - if (hasData && hasData.transitioning) return - actives.collapse('hide') - hasData || actives.data('collapse', null) - } - - this.$element[dimension](0) - this.transition('addClass', $.Event('show'), 'shown') - $.support.transition && this.$element[dimension](this.$element[0][scroll]) - } - - , hide: function () { - var dimension - if (this.transitioning || !this.$element.hasClass('in')) return - dimension = this.dimension() - this.reset(this.$element[dimension]()) - this.transition('removeClass', $.Event('hide'), 'hidden') - this.$element[dimension](0) - } - - , reset: function (size) { - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - [dimension](size || 'auto') - [0].offsetWidth - - this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') - - return this - } - - , transition: function (method, startEvent, completeEvent) { - var that = this - , complete = function () { - if (startEvent.type == 'show') that.reset() - that.transitioning = 0 - that.$element.trigger(completeEvent) - } - - this.$element.trigger(startEvent) - - if (startEvent.isDefaultPrevented()) return - - this.transitioning = 1 - - this.$element[method]('in') - - $.support.transition && this.$element.hasClass('collapse') ? - this.$element.one($.support.transition.end, complete) : - complete() - } - - , toggle: function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - } - - - /* COLLAPSE PLUGIN DEFINITION - * ========================== */ - - var old = $.fn.collapse - - $.fn.collapse = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('collapse') - , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option) - if (!data) $this.data('collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.collapse.defaults = { - toggle: true - } - - $.fn.collapse.Constructor = Collapse - - - /* COLLAPSE NO CONFLICT - * ==================== */ - - $.fn.collapse.noConflict = function () { - $.fn.collapse = old - return this - } - - - /* COLLAPSE DATA-API - * ================= */ - - $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { - var $this = $(this), href - , target = $this.attr('data-target') - || e.preventDefault() - || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 - , option = $(target).data('collapse') ? 'toggle' : $this.data() - $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') - $(target).collapse(option) - }) - -}(window.jQuery);/* ============================================================ - * bootstrap-dropdown.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#dropdowns - * ============================================================ - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* DROPDOWN CLASS DEFINITION - * ========================= */ - - var toggle = '[data-toggle=dropdown]' - , Dropdown = function (element) { - var $el = $(element).on('click.dropdown.data-api', this.toggle) - $('html').on('click.dropdown.data-api', function () { - $el.parent().removeClass('open') - }) - } - - Dropdown.prototype = { - - constructor: Dropdown - - , toggle: function (e) { - var $this = $(this) - , $parent - , isActive - - if ($this.is('.disabled, :disabled')) return - - $parent = getParent($this) - - isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - $parent.toggleClass('open') - } - - $this.focus() - - return false - } - - , keydown: function (e) { - var $this - , $items - , $active - , $parent - , isActive - , index - - if (!/(38|40|27)/.test(e.keyCode)) return - - $this = $(this) - - e.preventDefault() - e.stopPropagation() - - if ($this.is('.disabled, :disabled')) return - - $parent = getParent($this) - - isActive = $parent.hasClass('open') - - if (!isActive || (isActive && e.keyCode == 27)) { - if (e.which == 27) $parent.find(toggle).focus() - return $this.click() - } - - $items = $('[role=menu] li:not(.divider):visible a', $parent) - - if (!$items.length) return - - index = $items.index($items.filter(':focus')) - - if (e.keyCode == 38 && index > 0) index-- // up - if (e.keyCode == 40 && index < $items.length - 1) index++ // down - if (!~index) index = 0 - - $items - .eq(index) - .focus() - } - - } - - function clearMenus() { - $(toggle).each(function () { - getParent($(this)).removeClass('open') - }) - } - - function getParent($this) { - var selector = $this.attr('data-target') - , $parent - - if (!selector) { - selector = $this.attr('href') - selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } - - $parent = selector && $(selector) - - if (!$parent || !$parent.length) $parent = $this.parent() - - return $parent - } - - - /* DROPDOWN PLUGIN DEFINITION - * ========================== */ - - var old = $.fn.dropdown - - $.fn.dropdown = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('dropdown') - if (!data) $this.data('dropdown', (data = new Dropdown(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - $.fn.dropdown.Constructor = Dropdown - - - /* DROPDOWN NO CONFLICT - * ==================== */ - - $.fn.dropdown.noConflict = function () { - $.fn.dropdown = old - return this - } - - - /* APPLY TO STANDARD DROPDOWN ELEMENTS - * =================================== */ - - $(document) - .on('click.dropdown.data-api', clearMenus) - .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) - .on('click.dropdown-menu', function (e) { e.stopPropagation() }) - .on('click.dropdown.data-api' , toggle, Dropdown.prototype.toggle) - .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) - -}(window.jQuery); -/* ========================================================= - * bootstrap-modal.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#modals - * ========================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================= */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* MODAL CLASS DEFINITION - * ====================== */ - - var Modal = function (element, options) { - this.options = options - this.$element = $(element) - .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) - this.options.remote && this.$element.find('.modal-body').load(this.options.remote) - } - - Modal.prototype = { - - constructor: Modal - - , toggle: function () { - return this[!this.isShown ? 'show' : 'hide']() - } - - , show: function () { - var that = this - , e = $.Event('show') - - this.$element.trigger(e) - - if (this.isShown || e.isDefaultPrevented()) return - - this.isShown = true - - this.escape() - - this.backdrop(function () { - var transition = $.support.transition && that.$element.hasClass('fade') - - if (!that.$element.parent().length) { - that.$element.appendTo(document.body) //don't move modals dom position - } - - that.$element.show() - - if (transition) { - that.$element[0].offsetWidth // force reflow - } - - that.$element - .addClass('in') - .attr('aria-hidden', false) - - that.enforceFocus() - - transition ? - that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) : - that.$element.focus().trigger('shown') - - }) - } - - , hide: function (e) { - e && e.preventDefault() - - var that = this - - e = $.Event('hide') - - this.$element.trigger(e) - - if (!this.isShown || e.isDefaultPrevented()) return - - this.isShown = false - - this.escape() - - $(document).off('focusin.modal') - - this.$element - .removeClass('in') - .attr('aria-hidden', true) - - $.support.transition && this.$element.hasClass('fade') ? - this.hideWithTransition() : - this.hideModal() - } - - , enforceFocus: function () { - var that = this - $(document).on('focusin.modal', function (e) { - if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { - that.$element.focus() - } - }) - } - - , escape: function () { - var that = this - if (this.isShown && this.options.keyboard) { - this.$element.on('keyup.dismiss.modal', function ( e ) { - e.which == 27 && that.hide() - }) - } else if (!this.isShown) { - this.$element.off('keyup.dismiss.modal') - } - } - - , hideWithTransition: function () { - var that = this - , timeout = setTimeout(function () { - that.$element.off($.support.transition.end) - that.hideModal() - }, 500) - - this.$element.one($.support.transition.end, function () { - clearTimeout(timeout) - that.hideModal() - }) - } - - , hideModal: function () { - var that = this - this.$element.hide() - this.backdrop(function () { - that.removeBackdrop() - that.$element.trigger('hidden') - }) - } - - , removeBackdrop: function () { - this.$backdrop && this.$backdrop.remove() - this.$backdrop = null - } - - , backdrop: function (callback) { - var that = this - , animate = this.$element.hasClass('fade') ? 'fade' : '' - - if (this.isShown && this.options.backdrop) { - var doAnimate = $.support.transition && animate - - this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') - .appendTo(document.body) - - this.$backdrop.click( - this.options.backdrop == 'static' ? - $.proxy(this.$element[0].focus, this.$element[0]) - : $.proxy(this.hide, this) - ) - - if (doAnimate) this.$backdrop[0].offsetWidth // force reflow - - this.$backdrop.addClass('in') - - if (!callback) return - - doAnimate ? - this.$backdrop.one($.support.transition.end, callback) : - callback() - - } else if (!this.isShown && this.$backdrop) { - this.$backdrop.removeClass('in') - - $.support.transition && this.$element.hasClass('fade')? - this.$backdrop.one($.support.transition.end, callback) : - callback() - - } else if (callback) { - callback() - } - } - } - - - /* MODAL PLUGIN DEFINITION - * ======================= */ - - var old = $.fn.modal - - $.fn.modal = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('modal') - , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option) - if (!data) $this.data('modal', (data = new Modal(this, options))) - if (typeof option == 'string') data[option]() - else if (options.show) data.show() - }) - } - - $.fn.modal.defaults = { - backdrop: true - , keyboard: true - , show: true - } - - $.fn.modal.Constructor = Modal - - - /* MODAL NO CONFLICT - * ================= */ - - $.fn.modal.noConflict = function () { - $.fn.modal = old - return this - } - - - /* MODAL DATA-API - * ============== */ - - $(document).on('click.modal.data-api', '[data-toggle="modal"]', function (e) { - var $this = $(this) - , href = $this.attr('href') - , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7 - , option = $target.data('modal') ? 'toggle' : $.extend({ remote:!/#/.test(href) && href }, $target.data(), $this.data()) - - e.preventDefault() - - $target - .modal(option) - .one('hide', function () { - $this.focus() - }) - }) - -}(window.jQuery); -/* =========================================================== - * bootstrap-tooltip.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#tooltips - * Inspired by the original jQuery.tipsy by Jason Frame - * =========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* TOOLTIP PUBLIC CLASS DEFINITION - * =============================== */ - - var Tooltip = function (element, options) { - this.init('tooltip', element, options) - } - - Tooltip.prototype = { - - constructor: Tooltip - - , init: function (type, element, options) { - var eventIn - , eventOut - , triggers - , trigger - , i - - this.type = type - this.$element = $(element) - this.options = this.getOptions(options) - this.enabled = true - - triggers = this.options.trigger.split(' ') - - for (i = triggers.length; i--;) { - trigger = triggers[i] - if (trigger == 'click') { - this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) - } else if (trigger != 'manual') { - eventIn = trigger == 'hover' ? 'mouseenter' : 'focus' - eventOut = trigger == 'hover' ? 'mouseleave' : 'blur' - this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) - this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) - } - } - - this.options.selector ? - (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : - this.fixTitle() - } - - , getOptions: function (options) { - options = $.extend({}, $.fn[this.type].defaults, this.$element.data(), options) - - if (options.delay && typeof options.delay == 'number') { - options.delay = { - show: options.delay - , hide: options.delay - } - } - - return options - } - - , enter: function (e) { - var defaults = $.fn[this.type].defaults - , options = {} - , self - - this._options && $.each(this._options, function (key, value) { - if (defaults[key] != value) options[key] = value - }, this) - - self = $(e.currentTarget)[this.type](options).data(this.type) - - if (!self.options.delay || !self.options.delay.show) return self.show() - - clearTimeout(this.timeout) - self.hoverState = 'in' - this.timeout = setTimeout(function() { - if (self.hoverState == 'in') self.show() - }, self.options.delay.show) - } - - , leave: function (e) { - var self = $(e.currentTarget)[this.type](this._options).data(this.type) - - if (this.timeout) clearTimeout(this.timeout) - if (!self.options.delay || !self.options.delay.hide) return self.hide() - - self.hoverState = 'out' - this.timeout = setTimeout(function() { - if (self.hoverState == 'out') self.hide() - }, self.options.delay.hide) - } - - , show: function () { - var $tip - , pos - , actualWidth - , actualHeight - , placement - , tp - , e = $.Event('show') - - if (this.hasContent() && this.enabled) { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $tip = this.tip() - this.setContent() - - if (this.options.animation) { - $tip.addClass('fade') - } - - placement = typeof this.options.placement == 'function' ? - this.options.placement.call(this, $tip[0], this.$element[0]) : - this.options.placement - - $tip - .detach() - .css({ top: 0, left: 0, display: 'block' }) - - this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) - - pos = this.getPosition() - - actualWidth = $tip[0].offsetWidth - actualHeight = $tip[0].offsetHeight - - switch (placement) { - case 'bottom': - tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} - break - case 'top': - tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} - break - case 'left': - tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} - break - case 'right': - tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} - break - } - - this.applyPlacement(tp, placement) - this.$element.trigger('shown') - } - } - - , applyPlacement: function(offset, placement){ - var $tip = this.tip() - , width = $tip[0].offsetWidth - , height = $tip[0].offsetHeight - , actualWidth - , actualHeight - , delta - , replace - - $tip - .offset(offset) - .addClass(placement) - .addClass('in') - - actualWidth = $tip[0].offsetWidth - actualHeight = $tip[0].offsetHeight - - if (placement == 'top' && actualHeight != height) { - offset.top = offset.top + height - actualHeight - replace = true - } - - if (placement == 'bottom' || placement == 'top') { - delta = 0 - - if (offset.left < 0){ - delta = offset.left * -2 - offset.left = 0 - $tip.offset(offset) - actualWidth = $tip[0].offsetWidth - actualHeight = $tip[0].offsetHeight - } - - this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') - } else { - this.replaceArrow(actualHeight - height, actualHeight, 'top') - } - - if (replace) $tip.offset(offset) - } - - , replaceArrow: function(delta, dimension, position){ - this - .arrow() - .css(position, delta ? (50 * (1 - delta / dimension) + "%") : '') - } - - , setContent: function () { - var $tip = this.tip() - , title = this.getTitle() - - $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) - $tip.removeClass('fade in top bottom left right') - } - - , hide: function () { - var that = this - , $tip = this.tip() - , e = $.Event('hide') - - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - - $tip.removeClass('in') - - function removeWithAnimation() { - var timeout = setTimeout(function () { - $tip.off($.support.transition.end).detach() - }, 500) - - $tip.one($.support.transition.end, function () { - clearTimeout(timeout) - $tip.detach() - }) - } - - $.support.transition && this.$tip.hasClass('fade') ? - removeWithAnimation() : - $tip.detach() - - this.$element.trigger('hidden') - - return this - } - - , fixTitle: function () { - var $e = this.$element - if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { - $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') - } - } - - , hasContent: function () { - return this.getTitle() - } - - , getPosition: function () { - var el = this.$element[0] - return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { - width: el.offsetWidth - , height: el.offsetHeight - }, this.$element.offset()) - } - - , getTitle: function () { - var title - , $e = this.$element - , o = this.options - - title = $e.attr('data-original-title') - || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) - - return title - } - - , tip: function () { - return this.$tip = this.$tip || $(this.options.template) - } - - , arrow: function(){ - return this.$arrow = this.$arrow || this.tip().find(".tooltip-arrow") - } - - , validate: function () { - if (!this.$element[0].parentNode) { - this.hide() - this.$element = null - this.options = null - } - } - - , enable: function () { - this.enabled = true - } - - , disable: function () { - this.enabled = false - } - - , toggleEnabled: function () { - this.enabled = !this.enabled - } - - , toggle: function (e) { - var self = e ? $(e.currentTarget)[this.type](this._options).data(this.type) : this - self.tip().hasClass('in') ? self.hide() : self.show() - } - - , destroy: function () { - this.hide().$element.off('.' + this.type).removeData(this.type) - } - - } - - - /* TOOLTIP PLUGIN DEFINITION - * ========================= */ - - var old = $.fn.tooltip - - $.fn.tooltip = function ( option ) { - return this.each(function () { - var $this = $(this) - , data = $this.data('tooltip') - , options = typeof option == 'object' && option - if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.tooltip.Constructor = Tooltip - - $.fn.tooltip.defaults = { - animation: true - , placement: 'top' - , selector: false - , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' - , trigger: 'hover focus' - , title: '' - , delay: 0 - , html: false - , container: false - } - - - /* TOOLTIP NO CONFLICT - * =================== */ - - $.fn.tooltip.noConflict = function () { - $.fn.tooltip = old - return this - } - -}(window.jQuery); -/* =========================================================== - * bootstrap-popover.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#popovers - * =========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * =========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* POPOVER PUBLIC CLASS DEFINITION - * =============================== */ - - var Popover = function (element, options) { - this.init('popover', element, options) - } - - - /* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js - ========================================== */ - - Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, { - - constructor: Popover - - , setContent: function () { - var $tip = this.tip() - , title = this.getTitle() - , content = this.getContent() - - $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) - $tip.find('.popover-content')[this.options.html ? 'html' : 'text'](content) - - $tip.removeClass('fade top bottom left right in') - } - - , hasContent: function () { - return this.getTitle() || this.getContent() - } - - , getContent: function () { - var content - , $e = this.$element - , o = this.options - - content = (typeof o.content == 'function' ? o.content.call($e[0]) : o.content) - || $e.attr('data-content') - - return content - } - - , tip: function () { - if (!this.$tip) { - this.$tip = $(this.options.template) - } - return this.$tip - } - - , destroy: function () { - this.hide().$element.off('.' + this.type).removeData(this.type) - } - - }) - - - /* POPOVER PLUGIN DEFINITION - * ======================= */ - - var old = $.fn.popover - - $.fn.popover = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('popover') - , options = typeof option == 'object' && option - if (!data) $this.data('popover', (data = new Popover(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.popover.Constructor = Popover - - $.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, { - placement: 'right' - , trigger: 'click' - , content: '' - , template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' - }) - - - /* POPOVER NO CONFLICT - * =================== */ - - $.fn.popover.noConflict = function () { - $.fn.popover = old - return this - } - -}(window.jQuery); -/* ============================================================= - * bootstrap-scrollspy.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#scrollspy - * ============================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* SCROLLSPY CLASS DEFINITION - * ========================== */ - - function ScrollSpy(element, options) { - var process = $.proxy(this.process, this) - , $element = $(element).is('body') ? $(window) : $(element) - , href - this.options = $.extend({}, $.fn.scrollspy.defaults, options) - this.$scrollElement = $element.on('scroll.scroll-spy.data-api', process) - this.selector = (this.options.target - || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 - || '') + ' .nav li > a' - this.$body = $('body') - this.refresh() - this.process() - } - - ScrollSpy.prototype = { - - constructor: ScrollSpy - - , refresh: function () { - var self = this - , $targets - - this.offsets = $([]) - this.targets = $([]) - - $targets = this.$body - .find(this.selector) - .map(function () { - var $el = $(this) - , href = $el.data('target') || $el.attr('href') - , $href = /^#\w/.test(href) && $(href) - return ( $href - && $href.length - && [[ $href.position().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]] ) || null - }) - .sort(function (a, b) { return a[0] - b[0] }) - .each(function () { - self.offsets.push(this[0]) - self.targets.push(this[1]) - }) - } - - , process: function () { - var scrollTop = this.$scrollElement.scrollTop() + this.options.offset - , scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight - , maxScroll = scrollHeight - this.$scrollElement.height() - , offsets = this.offsets - , targets = this.targets - , activeTarget = this.activeTarget - , i - - if (scrollTop >= maxScroll) { - return activeTarget != (i = targets.last()[0]) - && this.activate ( i ) - } - - for (i = offsets.length; i--;) { - activeTarget != targets[i] - && scrollTop >= offsets[i] - && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) - && this.activate( targets[i] ) - } - } - - , activate: function (target) { - var active - , selector - - this.activeTarget = target - - $(this.selector) - .parent('.active') - .removeClass('active') - - selector = this.selector - + '[data-target="' + target + '"],' - + this.selector + '[href="' + target + '"]' - - active = $(selector) - .parent('li') - .addClass('active') - - if (active.parent('.dropdown-menu').length) { - active = active.closest('li.dropdown').addClass('active') - } - - active.trigger('activate') - } - - } - - - /* SCROLLSPY PLUGIN DEFINITION - * =========================== */ - - var old = $.fn.scrollspy - - $.fn.scrollspy = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('scrollspy') - , options = typeof option == 'object' && option - if (!data) $this.data('scrollspy', (data = new ScrollSpy(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.scrollspy.Constructor = ScrollSpy - - $.fn.scrollspy.defaults = { - offset: 10 - } - - - /* SCROLLSPY NO CONFLICT - * ===================== */ - - $.fn.scrollspy.noConflict = function () { - $.fn.scrollspy = old - return this - } - - - /* SCROLLSPY DATA-API - * ================== */ - - $(window).on('load', function () { - $('[data-spy="scroll"]').each(function () { - var $spy = $(this) - $spy.scrollspy($spy.data()) - }) - }) - -}(window.jQuery);/* ======================================================== - * bootstrap-tab.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#tabs - * ======================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* TAB CLASS DEFINITION - * ==================== */ - - var Tab = function (element) { - this.element = $(element) - } - - Tab.prototype = { - - constructor: Tab - - , show: function () { - var $this = this.element - , $ul = $this.closest('ul:not(.dropdown-menu)') - , selector = $this.attr('data-target') - , previous - , $target - , e - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } - - if ( $this.parent('li').hasClass('active') ) return - - previous = $ul.find('.active:last a')[0] - - e = $.Event('show', { - relatedTarget: previous - }) - - $this.trigger(e) - - if (e.isDefaultPrevented()) return - - $target = $(selector) - - this.activate($this.parent('li'), $ul) - this.activate($target, $target.parent(), function () { - $this.trigger({ - type: 'shown' - , relatedTarget: previous - }) - }) - } - - , activate: function ( element, container, callback) { - var $active = container.find('> .active') - , transition = callback - && $.support.transition - && $active.hasClass('fade') - - function next() { - $active - .removeClass('active') - .find('> .dropdown-menu > .active') - .removeClass('active') - - element.addClass('active') - - if (transition) { - element[0].offsetWidth // reflow for transition - element.addClass('in') - } else { - element.removeClass('fade') - } - - if ( element.parent('.dropdown-menu') ) { - element.closest('li.dropdown').addClass('active') - } - - callback && callback() - } - - transition ? - $active.one($.support.transition.end, next) : - next() - - $active.removeClass('in') - } - } - - - /* TAB PLUGIN DEFINITION - * ===================== */ - - var old = $.fn.tab - - $.fn.tab = function ( option ) { - return this.each(function () { - var $this = $(this) - , data = $this.data('tab') - if (!data) $this.data('tab', (data = new Tab(this))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.tab.Constructor = Tab - - - /* TAB NO CONFLICT - * =============== */ - - $.fn.tab.noConflict = function () { - $.fn.tab = old - return this - } - - - /* TAB DATA-API - * ============ */ - - $(document).on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { - e.preventDefault() - $(this).tab('show') - }) - -}(window.jQuery);/* ============================================================= - * bootstrap-typeahead.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#typeahead - * ============================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function($){ - - "use strict"; // jshint ;_; - - - /* TYPEAHEAD PUBLIC CLASS DEFINITION - * ================================= */ - - var Typeahead = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.typeahead.defaults, options) - this.matcher = this.options.matcher || this.matcher - this.sorter = this.options.sorter || this.sorter - this.highlighter = this.options.highlighter || this.highlighter - this.updater = this.options.updater || this.updater - this.source = this.options.source - this.$menu = $(this.options.menu) - this.shown = false - this.listen() - } - - Typeahead.prototype = { - - constructor: Typeahead - - , select: function () { - var val = this.$menu.find('.active').attr('data-value') - this.$element - .val(this.updater(val)) - .change() - return this.hide() - } - - , updater: function (item) { - return item - } - - , show: function () { - var pos = $.extend({}, this.$element.position(), { - height: this.$element[0].offsetHeight - }) - - this.$menu - .insertAfter(this.$element) - .css({ - top: pos.top + pos.height - , left: pos.left - }) - .show() - - this.shown = true - return this - } - - , hide: function () { - this.$menu.hide() - this.shown = false - return this - } - - , lookup: function (event) { - var items - - this.query = this.$element.val() - - if (!this.query || this.query.length < this.options.minLength) { - return this.shown ? this.hide() : this - } - - items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source - - return items ? this.process(items) : this - } - - , process: function (items) { - var that = this - - items = $.grep(items, function (item) { - return that.matcher(item) - }) - - items = this.sorter(items) - - if (!items.length) { - return this.shown ? this.hide() : this - } - - return this.render(items.slice(0, this.options.items)).show() - } - - , matcher: function (item) { - return ~item.toLowerCase().indexOf(this.query.toLowerCase()) - } - - , sorter: function (items) { - var beginswith = [] - , caseSensitive = [] - , caseInsensitive = [] - , item - - while (item = items.shift()) { - if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) - else if (~item.indexOf(this.query)) caseSensitive.push(item) - else caseInsensitive.push(item) - } - - return beginswith.concat(caseSensitive, caseInsensitive) - } - - , highlighter: function (item) { - var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') - return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { - return '<strong>' + match + '</strong>' - }) - } - - , render: function (items) { - var that = this - - items = $(items).map(function (i, item) { - i = $(that.options.item).attr('data-value', item) - i.find('a').html(that.highlighter(item)) - return i[0] - }) - - items.first().addClass('active') - this.$menu.html(items) - return this - } - - , next: function (event) { - var active = this.$menu.find('.active').removeClass('active') - , next = active.next() - - if (!next.length) { - next = $(this.$menu.find('li')[0]) - } - - next.addClass('active') - } - - , prev: function (event) { - var active = this.$menu.find('.active').removeClass('active') - , prev = active.prev() - - if (!prev.length) { - prev = this.$menu.find('li').last() - } - - prev.addClass('active') - } - - , listen: function () { - this.$element - .on('focus', $.proxy(this.focus, this)) - .on('blur', $.proxy(this.blur, this)) - .on('keypress', $.proxy(this.keypress, this)) - .on('keyup', $.proxy(this.keyup, this)) - - if (this.eventSupported('keydown')) { - this.$element.on('keydown', $.proxy(this.keydown, this)) - } - - this.$menu - .on('click', $.proxy(this.click, this)) - .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) - .on('mouseleave', 'li', $.proxy(this.mouseleave, this)) - } - - , eventSupported: function(eventName) { - var isSupported = eventName in this.$element - if (!isSupported) { - this.$element.setAttribute(eventName, 'return;') - isSupported = typeof this.$element[eventName] === 'function' - } - return isSupported - } - - , move: function (e) { - if (!this.shown) return - - switch(e.keyCode) { - case 9: // tab - case 13: // enter - case 27: // escape - e.preventDefault() - break - - case 38: // up arrow - e.preventDefault() - this.prev() - break - - case 40: // down arrow - e.preventDefault() - this.next() - break - } - - e.stopPropagation() - } - - , keydown: function (e) { - this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27]) - this.move(e) - } - - , keypress: function (e) { - if (this.suppressKeyPressRepeat) return - this.move(e) - } - - , keyup: function (e) { - switch(e.keyCode) { - case 40: // down arrow - case 38: // up arrow - case 16: // shift - case 17: // ctrl - case 18: // alt - break - - case 9: // tab - case 13: // enter - if (!this.shown) return - this.select() - break - - case 27: // escape - if (!this.shown) return - this.hide() - break - - default: - this.lookup() - } - - e.stopPropagation() - e.preventDefault() - } - - , focus: function (e) { - this.focused = true - } - - , blur: function (e) { - this.focused = false - if (!this.mousedover && this.shown) this.hide() - } - - , click: function (e) { - e.stopPropagation() - e.preventDefault() - this.select() - this.$element.focus() - } - - , mouseenter: function (e) { - this.mousedover = true - this.$menu.find('.active').removeClass('active') - $(e.currentTarget).addClass('active') - } - - , mouseleave: function (e) { - this.mousedover = false - if (!this.focused && this.shown) this.hide() - } - - } - - - /* TYPEAHEAD PLUGIN DEFINITION - * =========================== */ - - var old = $.fn.typeahead - - $.fn.typeahead = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('typeahead') - , options = typeof option == 'object' && option - if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.typeahead.defaults = { - source: [] - , items: 8 - , menu: '<ul class="typeahead dropdown-menu"></ul>' - , item: '<li><a href="#"></a></li>' - , minLength: 1 - } - - $.fn.typeahead.Constructor = Typeahead - - - /* TYPEAHEAD NO CONFLICT - * =================== */ - - $.fn.typeahead.noConflict = function () { - $.fn.typeahead = old - return this - } - - - /* TYPEAHEAD DATA-API - * ================== */ - - $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { - var $this = $(this) - if ($this.data('typeahead')) return - $this.typeahead($this.data()) - }) - -}(window.jQuery); -/* ========================================================== - * bootstrap-affix.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#affix - * ========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* AFFIX CLASS DEFINITION - * ====================== */ - - var Affix = function (element, options) { - this.options = $.extend({}, $.fn.affix.defaults, options) - this.$window = $(window) - .on('scroll.affix.data-api', $.proxy(this.checkPosition, this)) - .on('click.affix.data-api', $.proxy(function () { setTimeout($.proxy(this.checkPosition, this), 1) }, this)) - this.$element = $(element) - this.checkPosition() - } - - Affix.prototype.checkPosition = function () { - if (!this.$element.is(':visible')) return - - var scrollHeight = $(document).height() - , scrollTop = this.$window.scrollTop() - , position = this.$element.offset() - , offset = this.options.offset - , offsetBottom = offset.bottom - , offsetTop = offset.top - , reset = 'affix affix-top affix-bottom' - , affix - - if (typeof offset != 'object') offsetBottom = offsetTop = offset - if (typeof offsetTop == 'function') offsetTop = offset.top() - if (typeof offsetBottom == 'function') offsetBottom = offset.bottom() - - affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? - false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? - 'bottom' : offsetTop != null && scrollTop <= offsetTop ? - 'top' : false - - if (this.affixed === affix) return - - this.affixed = affix - this.unpin = affix == 'bottom' ? position.top - scrollTop : null - - this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : '')) - } - - - /* AFFIX PLUGIN DEFINITION - * ======================= */ - - var old = $.fn.affix - - $.fn.affix = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('affix') - , options = typeof option == 'object' && option - if (!data) $this.data('affix', (data = new Affix(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.affix.Constructor = Affix - - $.fn.affix.defaults = { - offset: 0 - } - - - /* AFFIX NO CONFLICT - * ================= */ - - $.fn.affix.noConflict = function () { - $.fn.affix = old - return this - } - - - /* AFFIX DATA-API - * ============== */ - - $(window).on('load', function () { - $('[data-spy="affix"]').each(function () { - var $spy = $(this) - , data = $spy.data() - - data.offset = data.offset || {} - - data.offsetBottom && (data.offset.bottom = data.offsetBottom) - data.offsetTop && (data.offset.top = data.offsetTop) - - $spy.affix(data) - }) - }) - - -}(window.jQuery); \ No newline at end of file diff --git a/apps/bootstrap/js/bootstrap.min.js b/apps/bootstrap/js/bootstrap.min.js deleted file mode 100644 index 95c5ac5..0000000 --- a/apps/bootstrap/js/bootstrap.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! -* Bootstrap.js by @fat & @mdo -* Copyright 2012 Twitter, Inc. -* http://www.apache.org/licenses/LICENSE-2.0.txt -*/ -!function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||s.toggleClass("open"),n.focus(),!1},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this),n.preventDefault(),n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r),a=u.hasClass("open");if(!a||a&&n.keyCode==27)return n.which==27&&u.find(t).focus(),r.click();s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus")),n.keyCode==38&&f>0&&f--,n.keyCode==40&&f<s.length-1&&f++,~f||(f=0),s.eq(f).focus()}};var s=e.fn.dropdown;e.fn.dropdown=function(t){return this.each(function(){var r=e(this),i=r.data("dropdown");i||r.data("dropdown",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.dropdown.Constructor=n,e.fn.dropdown.noConflict=function(){return e.fn.dropdown=s,this},e(document).on("click.dropdown.data-api",r).on("click.dropdown.data-api",".dropdown form",function(e){e.stopPropagation()}).on("click.dropdown-menu",function(e){e.stopPropagation()}).on("click.dropdown.data-api",t,n.prototype.toggle).on("keydown.dropdown.data-api",t+", [role=menu]",n.prototype.keydown)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=n,this.$element=e(t).delegate('[data-dismiss="modal"]',"click.dismiss.modal",e.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};t.prototype={constructor:t,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var t=this,n=e.Event("show");this.$element.trigger(n);if(this.isShown||n.isDefaultPrevented())return;this.isShown=!0,this.escape(),this.backdrop(function(){var n=e.support.transition&&t.$element.hasClass("fade");t.$element.parent().length||t.$element.appendTo(document.body),t.$element.show(),n&&t.$element[0].offsetWidth,t.$element.addClass("in").attr("aria-hidden",!1),t.enforceFocus(),n?t.$element.one(e.support.transition.end,function(){t.$element.focus().trigger("shown")}):t.$element.focus().trigger("shown")})},hide:function(t){t&&t.preventDefault();var n=this;t=e.Event("hide"),this.$element.trigger(t);if(!this.isShown||t.isDefaultPrevented())return;this.isShown=!1,this.escape(),e(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),e.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var t=this;e(document).on("focusin.modal",function(e){t.$element[0]!==e.target&&!t.$element.has(e.target).length&&t.$element.focus()})},escape:function(){var e=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(t){t.which==27&&e.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var t=this,n=setTimeout(function(){t.$element.off(e.support.transition.end),t.hideModal()},500);this.$element.one(e.support.transition.end,function(){clearTimeout(n),t.hideModal()})},hideModal:function(){var e=this;this.$element.hide(),this.backdrop(function(){e.removeBackdrop(),e.$element.trigger("hidden")})},removeBackdrop:function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},backdrop:function(t){var n=this,r=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var i=e.support.transition&&r;this.$backdrop=e('<div class="modal-backdrop '+r+'" />').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?e.proxy(this.$element[0].focus,this.$element[0]):e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!t)return;i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,t):t()):t&&t()}};var n=e.fn.modal;e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e.fn.modal.noConflict=function(){return e.fn.modal=n,this},e(document).on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s,o,u,a;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,o=this.options.trigger.split(" ");for(a=o.length;a--;)u=o[a],u=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):u!="manual"&&(i=u=="hover"?"mouseenter":"focus",s=u=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this)));this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,this.$element.data(),t),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e.fn[this.type].defaults,r={},i;this._options&&e.each(this._options,function(e,t){n[e]!=t&&(r[e]=t)},this),i=e(t.currentTarget)[this.type](r).data(this.type);if(!i.options.delay||!i.options.delay.show)return i.show();clearTimeout(this.timeout),i.hoverState="in",this.timeout=setTimeout(function(){i.hoverState=="in"&&i.show()},i.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var t,n,r,i,s,o,u=e.Event("show");if(this.hasContent()&&this.enabled){this.$element.trigger(u);if(u.isDefaultPrevented())return;t=this.tip(),this.setContent(),this.options.animation&&t.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,t[0],this.$element[0]):this.options.placement,t.detach().css({top:0,left:0,display:"block"}),this.options.container?t.appendTo(this.options.container):t.insertAfter(this.$element),n=this.getPosition(),r=t[0].offsetWidth,i=t[0].offsetHeight;switch(s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}this.applyPlacement(o,s),this.$element.trigger("shown")}},applyPlacement:function(e,t){var n=this.tip(),r=n[0].offsetWidth,i=n[0].offsetHeight,s,o,u,a;n.offset(e).addClass(t).addClass("in"),s=n[0].offsetWidth,o=n[0].offsetHeight,t=="top"&&o!=i&&(e.top=e.top+i-o,a=!0),t=="bottom"||t=="top"?(u=0,e.left<0&&(u=e.left*-2,e.left=0,n.offset(e),s=n[0].offsetWidth,o=n[0].offsetHeight),this.replaceArrow(u-r+s,s,"left")):this.replaceArrow(o-i,o,"top"),a&&n.offset(e)},replaceArrow:function(e,t,n){this.arrow().css(n,e?50*(1-e/t)+"%":"")},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function i(){var t=setTimeout(function(){n.off(e.support.transition.end).detach()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.detach()})}var t=this,n=this.tip(),r=e.Event("hide");this.$element.trigger(r);if(r.isDefaultPrevented())return;return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?i():n.detach(),this.$element.trigger("hidden"),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var t=this.$element[0];return e.extend({},typeof t.getBoundingClientRect=="function"?t.getBoundingClientRect():{width:t.offsetWidth,height:t.offsetHeight},this.$element.offset())},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(t){var n=t?e(t.currentTarget)[this.type](this._options).data(this.type):this;n.tip().hasClass("in")?n.hide():n.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var n=e.fn.tooltip;e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},e.fn.tooltip.noConflict=function(){return e.fn.tooltip=n,this}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=(typeof n.content=="function"?n.content.call(t[0]):n.content)||t.attr("data-content"),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var n=e.fn.popover;e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),e.fn.popover.noConflict=function(){return e.fn.popover=n,this}}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var n=e(this),r=n.data("target")||n.attr("href"),i=/^#\w/.test(r)&&e(r);return i&&i.length&&[[i.position().top+(!e.isWindow(t.$scrollElement.get(0))&&t.$scrollElement.scrollTop()),r]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}};var n=e.fn.scrollspy;e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e.fn.scrollspy.noConflict=function(){return e.fn.scrollspy=n,this},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active:last a")[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}};var n=e.fn.tab;e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e.fn.tab.noConflict=function(){return e.fn.tab=n,this},e(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=e(this.options.menu),this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:t.top+t.height,left:t.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length<this.options.minLength?this.shown?this.hide():this:(n=e.isFunction(this.source)?this.source(this.query,e.proxy(this.process,this)):this.source,n?this.process(n):this)},process:function(t){var n=this;return t=e.grep(t,function(e){return n.matcher(e)}),t=this.sorter(t),t.length?this.render(t.slice(0,this.options.items)).show():this.shown?this.hide():this},matcher:function(e){return~e.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(e){var t=[],n=[],r=[],i;while(i=e.shift())i.toLowerCase().indexOf(this.query.toLowerCase())?~i.indexOf(this.query)?n.push(i):r.push(i):t.push(i);return t.concat(n,r)},highlighter:function(e){var t=this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&");return e.replace(new RegExp("("+t+")","ig"),function(e,t){return"<strong>"+t+"</strong>"})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("focus",e.proxy(this.focus,this)).on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this)).on("mouseleave","li",e.proxy(this.mouseleave,this))},eventSupported:function(e){var t=e in this.$element;return t||(this.$element.setAttribute(e,"return;"),t=typeof this.$element[e]=="function"),t},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},focus:function(e){this.focused=!0},blur:function(e){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(e){e.stopPropagation(),e.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(t){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")},mouseleave:function(e){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var n=e.fn.typeahead;e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'<ul class="typeahead dropdown-menu"></ul>',item:'<li><a href="#"></a></li>',minLength:1},e.fn.typeahead.Constructor=t,e.fn.typeahead.noConflict=function(){return e.fn.typeahead=n,this},e(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;n.typeahead(n.data())})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)).on("click.affix.data-api",e.proxy(function(){setTimeout(e.proxy(this.checkPosition,this),1)},this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))};var n=e.fn.affix;e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e.fn.affix.noConflict=function(){return e.fn.affix=n,this},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); \ No newline at end of file diff --git a/apps/bootstrap/protected/config/assets.php b/apps/bootstrap/protected/config/assets.php deleted file mode 100644 index a3ba847..0000000 --- a/apps/bootstrap/protected/config/assets.php +++ /dev/null @@ -1,19 +0,0 @@ -<?php - -return array( - 'app' => array( - 'basePath' => '@wwwroot', - 'baseUrl' => '@www', - 'css' => array( - 'css/bootstrap.min.css', - 'css/bootstrap-responsive.min.css', - 'css/site.css', - ), - 'js' => array( - - ), - 'depends' => array( - 'yii', - ), - ), -); diff --git a/apps/bootstrap/protected/views/site/contact.php b/apps/bootstrap/protected/views/site/contact.php deleted file mode 100644 index bee1ede..0000000 --- a/apps/bootstrap/protected/views/site/contact.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -use yii\helpers\Html; -use yii\widgets\ActiveForm; -use yii\widgets\Captcha; - -/** - * @var yii\base\View $this - * @var yii\widgets\ActiveForm $form - * @var app\models\ContactForm $model - */ -$this->title = 'Contact'; -$this->params['breadcrumbs'][] = $this->title; -?> -<h1><?php echo Html::encode($this->title); ?></h1> - -<?php if(Yii::$app->session->hasFlash('contactFormSubmitted')): ?> -<div class="alert alert-success"> - Thank you for contacting us. We will respond to you as soon as possible. -</div> -<?php return; endif; ?> - -<p> - If you have business inquiries or other questions, please fill out the following form to contact us. Thank you. -</p> - -<?php $form = $this->beginWidget(ActiveForm::className(), array( - 'options' => array('class' => 'form-horizontal'), - 'fieldConfig' => array('inputOptions' => array('class' => 'input-xlarge')), -)); ?> - <?php echo $form->field($model, 'name')->textInput(); ?> - <?php echo $form->field($model, 'email')->textInput(); ?> - <?php echo $form->field($model, 'subject')->textInput(); ?> - <?php echo $form->field($model, 'body')->textArea(array('rows' => 6)); ?> - <?php - $field = $form->field($model, 'verifyCode'); - echo $field->begin(); - echo $field->label(); - $this->widget(Captcha::className()); - echo Html::activeTextInput($model, 'verifyCode', array('class' => 'input-medium')); - echo $field->error(); - echo $field->end(); - ?> - <div class="form-actions"> - <?php echo Html::submitButton('Submit', null, null, array('class' => 'btn btn-primary')); ?> - </div> -<?php $this->endWidget(); ?> diff --git a/apps/bootstrap/protected/views/site/index.php b/apps/bootstrap/protected/views/site/index.php deleted file mode 100644 index 158b61c..0000000 --- a/apps/bootstrap/protected/views/site/index.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php -/** - * @var yii\base\View $this - */ -$this->title = 'Welcome'; -?> -<div class="jumbotron"> - <h1>Welcome!</h1> - - <p class="lead">Cras justo odio, dapibus ac facilisis in, egestas eget quam. Fusce dapibus, tellus ac cursus - commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p> - <a class="btn btn-large btn-success" href="http://www.yiiframework.com">Get started with Yii</a> -</div> - -<hr> - -<!-- Example row of columns --> -<div class="row-fluid"> - <div class="span4"> - <h2>Heading</h2> - - <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris - condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. - Donec sed odio dui. </p> - - <p><a class="btn" href="#">View details »</a></p> - </div> - <div class="span4"> - <h2>Heading</h2> - - <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris - condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. - Donec sed odio dui. </p> - - <p><a class="btn" href="#">View details »</a></p> - </div> - <div class="span4"> - <h2>Heading</h2> - - <p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta - felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum - massa.</p> - - <p><a class="btn" href="#">View details »</a></p> - </div> -</div> - diff --git a/build/.htaccess b/build/.htaccess index e019832..8d2f256 100644 --- a/build/.htaccess +++ b/build/.htaccess @@ -1 +1 @@ -deny from all +deny from all diff --git a/build/build b/build/build index 95b51e4..a6c7cc8 100755 --- a/build/build +++ b/build/build @@ -11,10 +11,11 @@ // fcgi doesn't have STDIN defined by default defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); -require(__DIR__ . '/../framework/Yii.php'); +require(__DIR__ . '/../framework/yii/Yii.php'); -$id = 'yiic-build'; -$basePath = __DIR__; - -$application = new yii\console\Application(array('id' => $id, 'basePath' => $basePath)); +$application = new yii\console\Application(array( + 'id' => 'yii-build', + 'basePath' => __DIR__, + 'controllerNamespace' => 'yii\build\controllers', +)); $application->run(); diff --git a/build/build.bat b/build/build.bat index a1ae41f..e659199 100644 --- a/build/build.bat +++ b/build/build.bat @@ -1,23 +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 <qiang.xue@gmail.com> -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" %* - +@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 <qiang.xue@gmail.com> +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 index 18a420d..b0975dc 100644 --- a/build/build.xml +++ b/build/build.xml @@ -94,7 +94,7 @@ </fileset> <fileset dir="." id="executables"> - <include name="${build.src.dir}/**/yiic" /> + <include name="${build.src.dir}/**/yii" /> </fileset> <target name="src" depends="sync"> @@ -265,7 +265,7 @@ Please update yiisite/common/data/versions.php file with the following code: where <target name> can be one of the following: - - sync : synchronize yiilite.php and YiiBase.php + - sync : synchronize yiilite.php and BaseYii.php - message : extract i18n messages of the framework - src : build source release - doc : build documentation release (Windows only) diff --git a/build/controllers/ClassmapController.php b/build/controllers/ClassmapController.php new file mode 100644 index 0000000..6a5ac8c --- /dev/null +++ b/build/controllers/ClassmapController.php @@ -0,0 +1,91 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\build\controllers; + +use yii\console\Controller; +use yii\helpers\FileHelper; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ClassmapController extends Controller +{ + public $defaultAction = 'create'; + + /** + * Creates a class map for the core Yii classes. + * @param string $root the root path of Yii framework. Defaults to YII_PATH. + * @param string $mapFile the file to contain the class map. Defaults to YII_PATH . '/classes.php'. + */ + public function actionCreate($root = null, $mapFile = null) + { + if ($root === null) { + $root = YII_PATH; + } + $root = FileHelper::normalizePath($root); + if ($mapFile === null) { + $mapFile = YII_PATH . '/classes.php'; + } + $options = array( + 'filter' => function ($path) { + if (is_file($path)) { + $file = basename($path); + if ($file[0] < 'A' || $file[0] > 'Z') { + return false; + } + } + return null; + }, + 'only' => array('.php'), + 'except' => array( + 'Yii.php', + 'BaseYii.php', + '/debug/', + '/console/', + '/test/', + '/gii/', + ), + ); + $files = FileHelper::findFiles($root, $options); + $map = array(); + foreach ($files as $file) { + if (($pos = strpos($file, $root)) !== 0) { + die("Something wrong: $file\n"); + } + $path = str_replace('\\', '/', substr($file, strlen($root))); + $map[$path] = "\t'yii" . substr(str_replace('/', '\\', $path), 0, -4) . "' => YII_PATH . '$path',"; + } + ksort($map); + $map = implode("\n", $map); + $output = <<<EOD +<?php +/** + * Yii core class map. + * + * This file is automatically generated by the "build classmap" command under the "build" folder. + * Do not modify it directly. + * + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +return array( +$map +); + +EOD; + if (is_file($mapFile) && file_get_contents($mapFile) === $output) { + echo "Nothing changed.\n"; + } else { + file_put_contents($mapFile, $output); + echo "Class map saved in $mapFile\n"; + } + } +} diff --git a/build/controllers/LocaleController.php b/build/controllers/LocaleController.php deleted file mode 100644 index d471c0d..0000000 --- a/build/controllers/LocaleController.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -use yii\console\Controller; -use yii\helpers\FileHelper; - -/** - * http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html - * @author Qiang Xue <qiang.xue@gmail.com> - * @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\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 - ); - 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); - } - $expr = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); - $rules[] = preg_replace_callback('/range\((\d+),(\d+)\)/', function ($matches) { - if ($matches[2] - $matches[1] <= 5) { - return 'array(' . implode(',', range($matches[1], $matches[2])) . ')'; - } else { - return $matches[0]; - } - }, $expr); - - } - 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 "<?php\n"; - echo <<<EOD -/** - * Plural rules. - * - * This file is automatically generated by the "yiic locale/plural" command under the "build" folder. - * Do not modify it directly. - * - * The original plural rule data used for generating this file has the following copyright terms: - * - * Copyright © 1991-2007 Unicode, Inc. All rights reserved. - * Distributed under the Terms of Use in http://www.unicode.org/copyright.html. - * - * @revision $revision (of the original plural file) - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ -EOD; - echo "\nreturn " . var_export($allRules, true) . ';'; - } -} diff --git a/build/controllers/PhpDocController.php b/build/controllers/PhpDocController.php new file mode 100644 index 0000000..cb574ff --- /dev/null +++ b/build/controllers/PhpDocController.php @@ -0,0 +1,328 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\build\controllers; + +use yii\console\Controller; +use yii\helpers\Console; +use yii\helpers\FileHelper; + +/** + * PhpDocController is there to help maintaining PHPDoc annotation in class files + * + * @author Carsten Brandt <mail@cebe.cc> + * @author Alexander Makarov <sam@rmcreative.ru> + * @since 2.0 + */ +class PhpDocController extends Controller +{ + public $defaultAction = 'property'; + + /** + * @var bool whether to update class docs directly. Setting this to false will just output docs + * for copy and paste. + */ + public $updateFiles = true; + + /** + * Generates @property annotations in class files from getters and setters + * + * Property description will be taken from getter or setter or from an @property annotation + * in the getters docblock if there is one defined. + * + * See https://github.com/yiisoft/yii2/wiki/Core-framework-code-style#documentation for details. + * + * @param null $root the directory to parse files from. Defaults to YII_PATH. + */ + public function actionProperty($root=null) + { + if ($root === null) { + $root = YII_PATH; + } + $root = FileHelper::normalizePath($root); + $options = array( + 'filter' => function ($path) { + if (is_file($path)) { + $file = basename($path); + if ($file[0] < 'A' || $file[0] > 'Z') { + return false; + } + } + return null; + }, + 'only' => array('.php'), + 'except' => array( + 'BaseYii.php', + 'Yii.php', + '/debug/views/', + '/requirements/', + '/gii/views/', + '/gii/generators/', + ), + ); + $files = FileHelper::findFiles($root, $options); + $nFilesTotal = 0; + $nFilesUpdated = 0; + foreach ($files as $file) { + $result = $this->generateClassPropertyDocs($file); + if ($result !== false) { + list($className, $phpdoc) = $result; + if ($this->updateFiles) { + if ($this->updateClassPropertyDocs($file, $className, $phpdoc)) { + $nFilesUpdated++; + } + } elseif (!empty($phpdoc)) { + $this->stdout("\n[ " . $file . " ]\n\n", Console::BOLD); + $this->stdout($phpdoc); + } + } + $nFilesTotal++; + } + + $this->stdout("\nParsed $nFilesTotal files.\n"); + $this->stdout("Updated $nFilesUpdated files.\n"); + } + + public function globalOptions() + { + return array_merge(parent::globalOptions(), array('updateFiles')); + } + + protected function updateClassPropertyDocs($file, $className, $propertyDoc) + { + $ref = new \ReflectionClass($className); + if ($ref->getFileName() != $file) { + $this->stderr("[ERR] Unable to create ReflectionClass for class: $className loaded class is not from file: $file\n", Console::FG_RED); + } + + if (!$ref->isSubclassOf('yii\base\Object') && $className != 'yii\base\Object') { + $this->stderr("[INFO] Skipping class $className as it is not a subclass of yii\\base\\Object.\n", Console::FG_BLUE, Console::BOLD); + return false; + } + + $oldDoc = $ref->getDocComment(); + $newDoc = $this->cleanDocComment($this->updateDocComment($oldDoc, $propertyDoc)); + + $seenSince = false; + $seenAuthor = false; + + // TODO move these checks to different action + $lines = explode("\n", $newDoc); + if (trim($lines[1]) == '*' || substr(trim($lines[1]), 0, 3) == '* @') { + $this->stderr("[WARN] Class $className has no short description.\n", Console::FG_YELLOW, Console::BOLD); + } + foreach($lines as $line) { + if (substr(trim($line), 0, 9) == '* @since ') { + $seenSince = true; + } elseif (substr(trim($line), 0, 10) == '* @author ') { + $seenAuthor = true; + } + } + + if (!$seenSince) { + $this->stderr("[ERR] No @since found in class doc in file: $file\n", Console::FG_RED); + } + if (!$seenAuthor) { + $this->stderr("[ERR] No @author found in class doc in file: $file\n", Console::FG_RED); + } + + if (trim($oldDoc) != trim($newDoc)) { + + $fileContent = explode("\n", file_get_contents($file)); + $start = $ref->getStartLine() - 2; + $docStart = $start - count(explode("\n", $oldDoc)) + 1; + + $newFileContent = array(); + $n = count($fileContent); + for($i = 0; $i < $n; $i++) { + if ($i > $start || $i < $docStart) { + $newFileContent[] = $fileContent[$i]; + } else { + $newFileContent[] = trim($newDoc); + $i = $start; + } + } + + file_put_contents($file, implode("\n", $newFileContent)); + + return true; + } + return false; + } + + /** + * remove multi empty lines and trim trailing whitespace + * + * @param $doc + * @return string + */ + protected function cleanDocComment($doc) + { + $lines = explode("\n", $doc); + $n = count($lines); + for($i = 0; $i < $n; $i++) { + $lines[$i] = rtrim($lines[$i]); + if (trim($lines[$i]) == '*' && trim($lines[$i + 1]) == '*') { + unset($lines[$i]); + } + } + return implode("\n", $lines); + } + + /** + * replace property annotations in doc comment + * @param $doc + * @param $properties + * @return string + */ + protected function updateDocComment($doc, $properties) + { + $lines = explode("\n", $doc); + $propertyPart = false; + $propertyPosition = false; + foreach($lines as $i => $line) { + if (substr(trim($line), 0, 12) == '* @property ') { + $propertyPart = true; + } elseif ($propertyPart && trim($line) == '*') { + $propertyPosition = $i; + $propertyPart = false; + } + if (substr(trim($line), 0, 10) == '* @author ' && $propertyPosition === false) { + $propertyPosition = $i - 1; + $propertyPart = false; + } + if ($propertyPart) { + unset($lines[$i]); + } + } + $finalDoc = ''; + foreach($lines as $i => $line) { + $finalDoc .= $line . "\n"; + if ($i == $propertyPosition) { + $finalDoc .= $properties; + } + } + return $finalDoc; + } + + protected function generateClassPropertyDocs($fileName) + { + $phpdoc = ""; + $file = str_replace("\r", "", str_replace("\t", " ", file_get_contents($fileName, true))); + $ns = $this->match('#\nnamespace (?<name>[\w\\\\]+);\n#', $file); + $namespace = reset($ns); + $namespace = $namespace['name']; + $classes = $this->match('#\n(?:abstract )?class (?<name>\w+)( |\n)(extends )?.+\{(?<content>.*)\n\}(\n|$)#', $file); + + if (count($classes) > 1) { + $this->stderr("[ERR] There should be only one class in a file: $fileName\n", Console::FG_RED); + return false; + } + if (count($classes) < 1) { + $interfaces = $this->match('#\ninterface (?<name>\w+)\n\{(?<content>.+)\n\}(\n|$)#', $file); + if (count($interfaces) == 1) { + return false; + } elseif (count($interfaces) > 1) { + $this->stderr("[ERR] There should be only one interface in a file: $fileName\n", Console::FG_RED); + } else { + $this->stderr("[ERR] No class in file: $fileName\n", Console::FG_RED); + } + return false; + } + + $className = null; + foreach ($classes as &$class) { + + $className = $namespace . '\\' . $class['name']; + + $gets = $this->match( + '#\* @return (?<type>[\w\\|\\\\\\[\\]]+)(?: (?<comment>(?:(?!\*/|\* @).)+?)(?:(?!\*/).)+|[\s\n]*)\*/' . + '[\s\n]{2,}public function (?<kind>get)(?<name>\w+)\((?:,? ?\$\w+ ?= ?[^,]+)*\)#', + $class['content']); + $sets = $this->match( + '#\* @param (?<type>[\w\\|\\\\\\[\\]]+) \$\w+(?: (?<comment>(?:(?!\*/|\* @).)+?)(?:(?!\*/).)+|[\s\n]*)\*/' . + '[\s\n]{2,}public function (?<kind>set)(?<name>\w+)\(\$\w+(?:, ?\$\w+ ?= ?[^,]+)*\)#', + $class['content']); + // check for @property annotations in getter and setter + $properties = $this->match( + '#\* @(?<kind>property) (?<type>[\w\\|\\\\\\[\\]]+)(?: (?<comment>(?:(?!\*/|\* @).)+?)(?:(?!\*/).)+|[\s\n]*)\*/' . + '[\s\n]{2,}public function [g|s]et(?<name>\w+)\(((?:,? ?\$\w+ ?= ?[^,]+)*|\$\w+(?:, ?\$\w+ ?= ?[^,]+)*)\)#', + $class['content']); + $acrs = array_merge($properties, $gets, $sets); + //print_r($acrs); continue; + + $props = array(); + foreach ($acrs as &$acr) { + $acr['name'] = lcfirst($acr['name']); + $acr['comment'] = trim(preg_replace('#(^|\n)\s+\*\s?#', '$1 * ', $acr['comment'])); + $props[$acr['name']][$acr['kind']] = array( + 'type' => $acr['type'], + 'comment' => $this->fixSentence($acr['comment']), + ); + } + + ksort($props); + + if (count($props) > 0) { + $phpdoc .= " *\n"; + foreach ($props as $propName => &$prop) { + $docline = ' * @'; + $docline .= 'property'; // Do not use property-read and property-write as few IDEs support complex syntax. + $note = ''; + if (isset($prop['get']) && isset($prop['set'])) { + if ($prop['get']['type'] != $prop['set']['type']) { + $note = ' Note that the type of this property differs in getter and setter.' + . ' See [[get'.ucfirst($propName).'()]] and [[set'.ucfirst($propName).'()]] for details.'; + } + } elseif (isset($prop['get'])) { + $note = ' This property is read-only.'; +// $docline .= '-read'; + } elseif (isset($prop['set'])) { + $note = ' This property is write-only.'; +// $docline .= '-write'; + } else { + continue; + } + $docline .= ' ' . $this->getPropParam($prop, 'type') . " $$propName "; + $comment = explode("\n", $this->getPropParam($prop, 'comment') . $note); + foreach ($comment as &$cline) { + $cline = ltrim($cline, '* '); + } + $docline = wordwrap($docline . implode(' ', $comment), 110, "\n * ") . "\n"; + + $phpdoc .= $docline; + } + $phpdoc .= " *\n"; + } + } + return array($className, $phpdoc); + } + + protected function match($pattern, $subject) + { + $sets = array(); + preg_match_all($pattern . 'suU', $subject, $sets, PREG_SET_ORDER); + foreach ($sets as &$set) + foreach ($set as $i => $match) + if (is_numeric($i) /*&& $i != 0*/) + unset($set[$i]); + return $sets; + } + + protected function fixSentence($str) + { + // TODO fix word wrap + if ($str == '') + return ''; + return strtoupper(substr($str, 0, 1)) . substr($str, 1) . ($str[strlen($str) - 1] != '.' ? '.' : ''); + } + + protected function getPropParam($prop, $param) + { + return isset($prop['property']) ? $prop['property'][$param] : (isset($prop['get']) ? $prop['get'][$param] : $prop['set'][$param]); + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 1cae3d4..0000000 --- a/composer.lock +++ /dev/null @@ -1,212 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" - ], - "hash": "7d46ce9c4d8d5f4ecae1611ea8f0b49c", - "packages": [ - { - "name": "ezyang/htmlpurifier", - "version": "v4.5.0", - "source": { - "type": "git", - "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "v4.5.0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/v4.5.0", - "reference": "v4.5.0", - "shasum": "" - }, - "require": { - "php": ">=5.2" - }, - "type": "library", - "autoload": { - "psr-0": { - "HTMLPurifier": "library/" - }, - "files": [ - "library/HTMLPurifier.composer.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL" - ], - "authors": [ - { - "name": "Edward Z. Yang", - "email": "admin@htmlpurifier.org", - "homepage": "http://ezyang.com" - } - ], - "description": "Standards compliant HTML filter written in PHP", - "homepage": "http://htmlpurifier.org/", - "keywords": [ - "html" - ], - "time": "2013-02-18 00:04:08" - }, - { - "name": "michelf/php-markdown", - "version": "1.3", - "source": { - "type": "git", - "url": "https://github.com/michelf/php-markdown.git", - "reference": "1.3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/michelf/php-markdown/zipball/1.3", - "reference": "1.3", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-lib": "1.3.x-dev" - } - }, - "autoload": { - "psr-0": { - "Michelf": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Michel Fortin", - "email": "michel.fortin@michelf.ca", - "homepage": "http://michelf.ca/", - "role": "Developer" - }, - { - "name": "John Gruber", - "homepage": "http://daringfireball.net/" - } - ], - "description": "PHP Markdown", - "homepage": "http://michelf.ca/projects/php-markdown/", - "keywords": [ - "markdown" - ], - "time": "2013-04-11 18:53:11" - }, - { - "name": "smarty/smarty", - "version": "v3.1.13", - "source": { - "type": "svn", - "url": "http://smarty-php.googlecode.com/svn", - "reference": "/tags/v3.1.13/@4699" - }, - "require": { - "php": ">=5.2" - }, - "type": "library", - "autoload": { - "classmap": [ - "distribution/libs/Smarty.class.php", - "distribution/libs/SmartyBC.class.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0" - ], - "authors": [ - { - "name": "Monte Ohrt", - "email": "monte@ohrt.com" - }, - { - "name": "Uwe Tews", - "email": "uwe.tews@googlemail.com" - }, - { - "name": "Rodney Rehm", - "email": "rodney.rehm@medialize.de" - } - ], - "description": "Smarty - the compiling PHP template engine", - "homepage": "http://www.smarty.net", - "keywords": [ - "templating" - ], - "time": "2013-01-26 12:03:52" - }, - { - "name": "twig/twig", - "version": "v1.12.3", - "source": { - "type": "git", - "url": "https://github.com/fabpot/Twig.git", - "reference": "v1.12.3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/fabpot/Twig/zipball/v1.12.3", - "reference": "v1.12.3", - "shasum": "" - }, - "require": { - "php": ">=5.2.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.12-dev" - } - }, - "autoload": { - "psr-0": { - "Twig_": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Armin Ronacher", - "email": "armin.ronacher@active-4.com" - } - ], - "description": "Twig, the flexible, fast, and secure template language for PHP", - "homepage": "http://twig.sensiolabs.org", - "keywords": [ - "templating" - ], - "time": "2013-04-08 12:40:11" - } - ], - "packages-dev": [ - - ], - "aliases": [ - - ], - "minimum-stability": "stable", - "stability-flags": [ - - ], - "platform": { - "php": ">=5.3.0" - }, - "platform-dev": [ - - ] -} diff --git a/docs/api/db/ActiveRecord.md b/docs/api/db/ActiveRecord.md index 4e82793..d8bedb4 100644 --- a/docs/api/db/ActiveRecord.md +++ b/docs/api/db/ActiveRecord.md @@ -446,3 +446,7 @@ $customers = Customer::find()->olderThan(50)->all(); The parameters should follow after the `$query` parameter when defining the scope method, and they can take default values like shown above. + +### Atomic operations and scenarios + +TBD diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index e69de29..7d0c744 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -0,0 +1,644 @@ +Active Record +============= + +Active Record implements the [Active Record design pattern](http://en.wikipedia.org/wiki/Active_record). +The premise behind Active Record is that an individual [[ActiveRecord]] object is associated with a specific row in a database table. The object's attributes are mapped to the columns of the corresponding table. Referencing an Active Record attribute is equivalent to accessing +the corresponding table column for that record. + +As an example, say that the `Customer` ActiveRecord class is associated with the +`tbl_customer` table. This would mean that the class's `name` attribute is automatically mapped to the `name` column in `tbl_customer`. +Thanks to Active Record, assuming the variable `$customer` is an object of type `Customer`, to get the value of the `name` column for the table row, you can use the expression `$customer->name`. In this example, Active Record is providing an object-oriented interface for accessing data stored in the database. But Active Record provides much more functionality than this. + +With Active Record, instead of writing raw SQL statements to perform database queries, you can call intuitive methods to achieve the same goals. For example, calling [[ActiveRecord::save()|save()]] would perform an INSERT or UPDATE query, creating or updating a row in the associated table of the ActiveRecord class: + +```php +$customer = new Customer(); +$customer->name = 'Qiang'; +$customer->save(); // a new row is inserted into tbl_customer +``` + + +Declaring ActiveRecord Classes +------------------------------ + +To declare an ActiveRecord class you need to extend [[\yii\db\ActiveRecord]] and +implement the `tableName` method: + +```php +use yii\db\ActiveRecord; + +class Customer extends ActiveRecord +{ + /** + * @return string the name of the table associated with this ActiveRecord class. + */ + public static function tableName() + { + return 'tbl_customer'; + } +} +``` + +The `tableName` method only has to return the name of the database table associated with the class. + +Class instances are obtained in one of two ways: + +* Using the `new` operator to create a new, empty object +* Using a method to fetch an existing record (or records) from the database + +Connecting to the Database +---------------------- + +ActiveRecord relies on a [[Connection|DB connection]] to perform the underlying DB operations. +By default, ActiveRecord assumes that there is an application component named `db` which provides the needed +[[Connection]] instance. Usually this component is configured in application configuration file: + +```php +return array( + 'components' => array( + 'db' => array( + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=testdb', + 'username' => 'demo', + 'password' => 'demo', + ), + ), +); +``` + +Please read the [Database basics](database-basics.md) section to learn more on how to configure and use database connections. + +Querying Data from the Database +--------------------------- + +There are two ActiveRecord methods for querying data from database: + + - [[ActiveRecord::find()]] + - [[ActiveRecord::findBySql()]] + +Both methods return an [[ActiveQuery]] instance, which extends [[Query]], and thus supports +the same set of flexible and powerful DB query methods. The following examples demonstrate some of the possibilities. + +```php +// to retrieve all *active* customers and order them by their ID: +$customers = Customer::find() + ->where(array('status' => $active)) + ->orderBy('id') + ->all(); + +// to return a single customer whose ID is 1: +$customer = Customer::find(1); + +// the above code is equivalent to the following: +$customer = Customer::find() + ->where(array('id' => 1)) + ->one(); + +// to retrieve customers using a raw SQL statement: +$sql = 'SELECT * FROM tbl_customer'; +$customers = Customer::findBySql($sql)->all(); + +// to return the number of *active* customers: +$count = Customer::find() + ->where(array('status' => $active)) + ->count(); + +// to return customers in terms of arrays rather than `Customer` objects: +$customers = Customer::find() + ->asArray() + ->all(); +// each element of $customers is an array of name-value pairs + +// to index the result by customer IDs: +$customers = Customer::find()->indexBy('id')->all(); +// $customers array is indexed by customer IDs +``` + + +Accessing Column Data +--------------------- + +ActiveRecord maps each column of the corresponding database table row to an attribute in the ActiveRecord +object. The attribute behaves like any regular object public property. The attribute's name will be the same as the corresponding column +name, and is case-sensitive. + +To read the value of a column, you can use the following syntax: + +```php +// "id" and "email" are the names of columns in the table associated with $customer ActiveRecord object +$id = $customer->id; +$email = $customer->email; +``` + +To change the value of a column, assign a new value to the associated property and save the object: + +``` +$customer->email = 'jane@example.com'; +$customer->save(); +``` + +Manipulating Data in the Database +----------------------------- + +ActiveRecord provides the following methods to insert, update and delete data in the database: + +- [[ActiveRecord::save()|save()]] +- [[ActiveRecord::insert()|insert()]] +- [[ActiveRecord::update()|update()]] +- [[ActiveRecord::delete()|delete()]] +- [[ActiveRecord::updateCounters()|updateCounters()]] +- [[ActiveRecord::updateAll()|updateAll()]] +- [[ActiveRecord::updateAllCounters()|updateAllCounters()]] +- [[ActiveRecord::deleteAll()|deleteAll()]] + +Note that [[ActiveRecord::updateAll()|updateAll()]], [[ActiveRecord::updateAllCounters()|updateAllCounters()]] +and [[ActiveRecord::deleteAll()|deleteAll()]] are static methods that apply to the whole database +table. The other methods only apply to the row associated with the ActiveRecord object through which the method is being called. + +```php +// to insert a new customer record +$customer = new Customer; +$customer->name = 'James'; +$customer->email = 'james@example.com'; +$customer->save(); // equivalent to $customer->insert(); + +// to update an existing customer record +$customer = Customer::find($id); +$customer->email = 'james@example.com'; +$customer->save(); // equivalent to $customer->update(); + +// to delete an existing customer record +$customer = Customer::find($id); +$customer->delete(); + +// to increment the age of ALL customers by 1 +Customer::updateAllCounters(array('age' => 1)); +``` + +> Info: The `save()` method will either perform an `INSERT` or `UPDATE` SQL statement, depending + on whether the ActiveRecord being saved is new or not by checking `ActiveRecord::isNewRecord`. + + +Data Input and Validation +------------------------- + +ActiveRecord inherits data validation and data input features from [[\yii\base\Model]]. Data validation is called +automatically when `save()` is performed. If data validation fails, the saving operation will be cancelled. + +For more details refer to the [Model](model.md) section of this guide. + +Querying Relational Data +------------------------ + +You can use ActiveRecord to also query a table's relational data (i.e., selection of data from Table A can also pull in related data from Table B). Thanks to ActiveRecord, the relational data returned can be accessed like a property of the ActiveRecord object associated with the primary table. + +For example, with an appropriate relation declaration, by accessing `$customer->orders` you may obtain +an array of `Order` objects which represent the orders placed by the specified customer. + +To declare a relation, define a getter method which returns an [[ActiveRelation]] object. For example, + +```php +class Customer extends \yii\db\ActiveRecord +{ + public function getOrders() + { + return $this->hasMany('Order', array('customer_id' => 'id')); + } +} + +class Order extends \yii\db\ActiveRecord +{ + public function getCustomer() + { + return $this->hasOne('Customer', array('id' => 'customer_id')); + } +} +``` + +The methods [[ActiveRecord::hasMany()]] and [[ActiveRecord::hasOne()]] used in the above +are used to model the many-one relationship and one-one relationship in a relational database. +For example, a customer has many orders, and an order has one customer. +Both methods take two parameters and return an [[ActiveRelation]] object: + + - `$class`: the name of the class of the related model(s). If specified without + a namespace, the namespace of the related model class will be taken from the declaring class. + - `$link`: the association between columns from the two tables. This should be given as an array. + The keys of the array are the names of the columns from the table associated with `$class`, + while the values of the array are the names of the columns from the declaring class. + It is a good practice to define relationships based on table foreign keys. + +After declaring relations, getting relational data is as easy as accessing a component property +that is defined by the corresponding getter method: + +```php +// get the orders of a customer +$customer = Customer::find(1); +$orders = $customer->orders; // $orders is an array of Order objects +``` + +Behind the scene, the above code executes the following two SQL queries, one for each line of code: + +```sql +SELECT * FROM tbl_customer WHERE id=1; +SELECT * FROM tbl_order WHERE customer_id=1; +``` + +> Tip: If you access the expression `$customer->orders` again, will it perform the second SQL query again? +Nope. The SQL query is only performed the first time when this expression is accessed. Any further +accesses will only return the previously fetched results that are cached internally. If you want to re-query +the relational data, simply unset the existing one first: `unset($customer->orders);`. + +Sometimes, you may want to pass parameters to a relational query. For example, instead of returning +all orders of a customer, you may want to return only big orders whose subtotal exceeds a specified amount. +To do so, declare a `bigOrders` relation with the following getter method: + +```php +class Customer extends \yii\db\ActiveRecord +{ + public function getBigOrders($threshold = 100) + { + return $this->hasMany('Order', array('customer_id' => 'id')) + ->where('subtotal > :threshold', array(':threshold' => $threshold)) + ->orderBy('id'); + } +} +``` + +Remember that `hasMany()` returns an [[ActiveRelation]] object which extends from [[ActiveQuery]] +and thus supports the same set of querying methods as [[ActiveQuery]]. + +With the above declaration, if you access `$customer->bigOrders`, it will only return the orders +whose subtotal is greater than 100. To specify a different threshold value, use the following code: + +```php +$orders = $customer->getBigOrders(200)->all(); +``` + + +Relations with Pivot Table +-------------------------- + +Sometimes, two tables are related together via an intermediary table called +[pivot table](http://en.wikipedia.org/wiki/Pivot_table). To declare such relations, we can customize +the [[ActiveRelation]] object by calling its [[ActiveRelation::via()]] or [[ActiveRelation::viaTable()]] +method. + +For example, if table `tbl_order` and table `tbl_item` are related via pivot table `tbl_order_item`, +we can declare the `items` relation in the `Order` class like the following: + +```php +class Order extends \yii\db\ActiveRecord +{ + public function getItems() + { + return $this->hasMany('Item', array('id' => 'item_id')) + ->viaTable('tbl_order_item', array('order_id' => 'id')); + } +} +``` + +[[ActiveRelation::via()]] method is similar to [[ActiveRelation::viaTable()]] except that +the first parameter of [[ActiveRelation::via()]] takes a relation name declared in the ActiveRecord class +instead of the pivot table name. For example, the above `items` relation can be equivalently declared as follows: + +```php +class Order extends \yii\db\ActiveRecord +{ + public function getOrderItems() + { + return $this->hasMany('OrderItem', array('order_id' => 'id')); + } + + public function getItems() + { + return $this->hasMany('Item', array('id' => 'item_id')) + ->via('orderItems'); + } +} +``` + + +Lazy and Eager Loading +---------------------- + +As described earlier, when you access the related objects the first time, ActiveRecord will perform a DB query +to retrieve the corresponding data and populate it into the related objects. No query will be performed +if you access the same related objects again. We call this *lazy loading*. For example, + +```php +// SQL executed: SELECT * FROM tbl_customer WHERE id=1 +$customer = Customer::find(1); +// SQL executed: SELECT * FROM tbl_order WHERE customer_id=1 +$orders = $customer->orders; +// no SQL executed +$orders2 = $customer->orders; +``` + +Lazy loading is very convenient to use. However, it may suffer from a performance issue in the following scenario: + +```php +// SQL executed: SELECT * FROM tbl_customer LIMIT 100 +$customers = Customer::find()->limit(100)->all(); + +foreach ($customers as $customer) { + // SQL executed: SELECT * FROM tbl_order WHERE customer_id=... + $orders = $customer->orders; + // ...handle $orders... +} +``` + +How many SQL queries will be performed in the above code, assuming there are more than 100 customers in +the database? 101! The first SQL query brings back 100 customers. Then for each customer, a SQL query +is performed to bring back the orders of that customer. + +To solve the above performance problem, you can use the so-called *eager loading* approach by calling [[ActiveQuery::with()]]: + +```php +// SQL executed: SELECT * FROM tbl_customer LIMIT 100; +// SELECT * FROM tbl_orders WHERE customer_id IN (1,2,...) +$customers = Customer::find()->limit(100) + ->with('orders')->all(); + +foreach ($customers as $customer) { + // no SQL executed + $orders = $customer->orders; + // ...handle $orders... +} +``` + +As you can see, only two SQL queries are needed for the same task. + + +Sometimes, you may want to customize the relational queries on the fly. This can be +done for both lazy loading and eager loading. For example, + +```php +$customer = Customer::find(1); +// lazy loading: SELECT * FROM tbl_order WHERE customer_id=1 AND subtotal>100 +$orders = $customer->getOrders()->where('subtotal>100')->all(); + +// eager loading: SELECT * FROM tbl_customer LIMIT 10 + SELECT * FROM tbl_order WHERE customer_id IN (1,2,...) AND subtotal>100 +$customers = Customer::find()->limit(100)->with(array( + 'orders' => function($query) { + $query->andWhere('subtotal>100'); + }, +))->all(); +``` + + +Working with Relationships +-------------------------- + +ActiveRecord provides the following two methods for establishing and breaking a +relationship between two ActiveRecord objects: + +- [[ActiveRecord::link()|link()]] +- [[ActiveRecord::unlink()|unlink()]] + +For example, given a customer and a new order, we can use the following code to make the +order owned by the customer: + +```php +$customer = Customer::find(1); +$order = new Order; +$order->subtotal = 100; +$customer->link('orders', $order); +``` + +The [[link()]] call above will set the `customer_id` of the order to be the primary key +value of `$customer` and then call [[save()]] to save the order into database. + + +Life Cycles of an ActiveRecord Object +------------------------------------- + +An ActiveRecord object undergoes different life cycles when it is used in different cases. +Subclasses or ActiveRecord behaviors may "inject" custom code in these life cycles through +method overriding and event handling mechanisms. + +When instantiating a new ActiveRecord instance, we will have the following life cycles: + +1. constructor +2. [[init()]]: will trigger an [[EVENT_INIT]] event + +When getting an ActiveRecord instance through the [[find()]] method, we will have the following life cycles: + +1. constructor +2. [[init()]]: will trigger an [[EVENT_INIT]] event +3. [[afterFind()]]: will trigger an [[EVENT_AFTER_FIND]] event + +When calling [[save()]] to insert or update an ActiveRecord, we will have the following life cycles: + +1. [[beforeValidate()]]: will trigger an [[EVENT_BEFORE_VALIDATE]] event +2. [[afterValidate()]]: will trigger an [[EVENT_AFTER_VALIDATE]] event +3. [[beforeSave()]]: will trigger an [[EVENT_BEFORE_INSERT]] or [[EVENT_BEFORE_UPDATE]] event +4. perform the actual data insertion or updating +5. [[afterSave()]]: will trigger an [[EVENT_AFTER_INSERT]] or [[EVENT_AFTER_UPDATE]] event + +Finally when calling [[delete()]] to delete an ActiveRecord, we will have the following life cycles: + +1. [[beforeDelete()]]: will trigger an [[EVENT_BEFORE_DELETE]] event +2. perform the actual data deletion +3. [[afterDelete()]]: will trigger an [[EVENT_AFTER_DELETE]] event + + +Scopes +------ + +A scope is a method that customizes a given [[ActiveQuery]] object. Scope methods are static and are defined +in the ActiveRecord classes. They can be invoked through the [[ActiveQuery]] object that is created +via [[find()]] or [[findBySql()]]. The following is an example: + +```php +class Customer extends \yii\db\ActiveRecord +{ + // ... + + /** + * @param ActiveQuery $query + */ + public static function active($query) + { + $query->andWhere('status = 1'); + } +} + +$customers = Customer::find()->active()->all(); +``` + +In the above, the `active()` method is defined in `Customer` while we are calling it +through `ActiveQuery` returned by `Customer::find()`. + +Scopes can be parameterized. For example, we can define and use the following `olderThan` scope: + +```php +class Customer extends \yii\db\ActiveRecord +{ + // ... + + /** + * @param ActiveQuery $query + * @param integer $age + */ + public static function olderThan($query, $age = 30) + { + $query->andWhere('age > :age', array(':age' => $age)); + } +} + +$customers = Customer::find()->olderThan(50)->all(); +``` + +The parameters should follow after the `$query` parameter when defining the scope method, and they +can take default values like shown above. + + +Transactional operations +------------------------ + + +When a few DB operations are related and are executed + +TODO: FIXME: WIP, TBD, https://github.com/yiisoft/yii2/issues/226 + +, +[[afterSave()]], [[beforeDelete()]] and/or [[afterDelete()]] life cycle methods. Developer may come +to the solution of overriding ActiveRecord [[save()]] method with database transaction wrapping or +even using transaction in controller action, which is strictly speaking doesn't seems to be a good +practice (recall skinny-controller fat-model fundamental rule). + +Here these ways are (**DO NOT** use them unless you're sure what are you actually doing). Models: + +```php +class Feature extends \yii\db\ActiveRecord +{ + // ... + + public function getProduct() + { + return $this->hasOne('Product', array('product_id' => 'id')); + } +} + +class Product extends \yii\db\ActiveRecord +{ + // ... + + public function getFeatures() + { + return $this->hasMany('Feature', array('id' => 'product_id')); + } +} +``` + +Overriding [[save()]] method: + +```php + +class ProductController extends \yii\web\Controller +{ + public function actionCreate() + { + // FIXME: TODO: WIP, TBD + } +} +``` + +Using transactions within controller layer: + +```php +class ProductController extends \yii\web\Controller +{ + public function actionCreate() + { + // FIXME: TODO: WIP, TBD + } +} +``` + +Instead of using these fragile methods you should consider using atomic scenarios and operations feature. + +```php +class Feature extends \yii\db\ActiveRecord +{ + // ... + + public function getProduct() + { + return $this->hasOne('Product', array('product_id' => 'id')); + } + + public function scenarios() + { + return array( + 'userCreates' => array( + 'attributes' => array('name', 'value'), + 'atomic' => array(self::OP_INSERT), + ), + ); + } +} + +class Product extends \yii\db\ActiveRecord +{ + // ... + + public function getFeatures() + { + return $this->hasMany('Feature', array('id' => 'product_id')); + } + + public function scenarios() + { + return array( + 'userCreates' => array( + 'attributes' => array('title', 'price'), + 'atomic' => array(self::OP_INSERT), + ), + ); + } + + public function afterValidate() + { + parent::afterValidate(); + // FIXME: TODO: WIP, TBD + } + + public function afterSave($insert) + { + parent::afterSave(); + if ($this->getScenario() === 'userCreates') { + // FIXME: TODO: WIP, TBD + } + } +} +``` + +Controller is very thin and neat: + +```php +class ProductController extends \yii\web\Controller +{ + public function actionCreate() + { + // FIXME: TODO: WIP, TBD + } +} +``` + +Optimistic Locks +---------------- + +TODO + +Dirty Attributes +---------------- + +TODO + +See also +-------- + +- [Model](model.md) +- [[\yii\db\ActiveRecord]] diff --git a/docs/guide/apps-advanced.md b/docs/guide/apps-advanced.md new file mode 100644 index 0000000..61e2489 --- /dev/null +++ b/docs/guide/apps-advanced.md @@ -0,0 +1,174 @@ +Advanced application template +============================= + +This template is for large projects developed in teams where backend is divided from frontend, application is deployed +to multiple servers etc. This application template also goes a bit further regarding features and provides essential +database, signup and password restore out of the box. + +Installation +------------ + +### Install via Composer + +If you do not have [Composer](http://getcomposer.org/), you may download it from +[http://getcomposer.org/](http://getcomposer.org/) or run the following command on Linux/Unix/MacOS: + +~~~ +curl -s http://getcomposer.org/installer | php +~~~ + +You can then install the application using the following command: + +~~~ +php composer.phar create-project --stability=dev yiisoft/yii2-app-advanced /path/to/yii-application +~~~ + +Getting started +--------------- + +After you install the application, you have to conduct the following steps to initialize +the installed application. You only need to do these once for all. + +1. Execute the `init` command and select `dev` as environment. +--- +php /path/to/yii-application/init +--- +2. Create a new database. It is assumed that MySQL InnoDB is used. If not, adjust `console/migrations/m130524_201442_init.php`. +3. In `common/config/params.php` set your database details in `components.db` values. + +4. Set document roots of your Web server: + +- for frontend `/path/to/yii-application/frontend/web/` and using the URL `http://frontend/` +- for backend `/path/to/yii-application/backend/web/` and using the URL `http://backend/` + +Directory structure +------------------- + +The root directory contains the following subdirectories: + +- `backend` - backend web application. +- `common` - files common to all applications. +- `console` - console application. +- `environments` - environment configs. +- `frontend` - frontend web application. + +Root directory contains a set of files. + +- `.gitignore` contains a list of directories ignored by git version system. If you need something never get to your source + code repository, add it there. +- `composer.json` - Composer config described in detail below. +- `init` - initialization script described in "Composer config described in detail below". +- `init.bat` - same for Windows. +- `LICENSE.md` - license info. Put your project license there. Especially when opensourcing. +- `README.md` - basic info about installing template. Consider replacing it with information about your project and its + installation. +- `requirements.php` - Yii requirements checker. +- `yii` - console application bootstrap. +- `yii.bat` - same for Windows. + +Applications +------------ + +There are three applications in advanced template: frontend, backend and console. Frontend is typically what is presented +to end user, the project itself. Backend is admin panel, analytics and such functionality. Console is typically used for +cron jobs and low-level server management. Also it's used during application deployment and handles migrations and assets. + +There's also a `common` directory that contains files used by more than one application. For example, `User` model. + +frontend and backend are both web applications and both contain `web` directory. That's the webroot you should point your +webserver to. + +Each application has its own namespace and alias corresponding to its name. Same applies to common directory. + +Configuration and environments +------------------------------ + +There are multiple problems with straightforward approach to configuration: + +- Each team member has its own configuration options. Committing such config will affect other team members. +- Production database password and API keys should not end up in repository. +- There are multiple servers: development, testing, production. Each should have its own configuration. +- Defining all configuration options for each case is very repetitive and takes too much time to maintain. + +In order to solve these issues Yii introduces environments concept that is very simple. Each environment is represented +by a set of files under `environments` directory. `init` command is used to switch between these. What is really does is +just copying everything from environment directory over the root directory where all applications are. + +Typically environment contains application bootstrap files such as `index.php` and config files suffixed with +`-local.php`. These are added to `.gitignore` and never added to source code repository. + +In order to avoid duplication configurations are overriding each other. For example, frontend reads configuration in the +following order: + +- `frontend/config/main.php` +- `frontend/config/main-local.php` + +Parameters are read in the following order: + +- `common/config/params.php` +- `common/config/params-local.php` +- `frontend/config/params.php` +- `frontend/config/params-local.php` + +The later config file overrides the former. + +Another difference is that most application component configurations are moved to params. Since params are read from +`common` as well it allows you to specify database connection in one file and it will be then used for all applications. + +Configuring Composer +-------------------- + +After application template is installed it's a good idea to adjust default `composer.json` that can be found in the root +directory: + +```javascript +{ + "name": "yiisoft/yii2-app-advanced", + "description": "Yii 2 Advanced Application Template", + "keywords": ["yii", "framework", "advanced", "application template"], + "homepage": "http://www.yiiframework.com/", + "type": "project", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "minimum-stability": "dev", + "require": { + "php": ">=5.3.0", + "yiisoft/yii2": "dev-master", + "yiisoft/yii2-composer": "dev-master" + }, + "scripts": { + "post-create-project-cmd": [ + "yii\\composer\\InstallHandler::setPermissions" + ] + }, + "extra": { + "yii-install-writable": [ + "backend/runtime", + "backend/web/assets", + + "console/runtime", + "console/migrations", + + "frontend/runtime", + "frontend/web/assets" + ] + } +} + +``` + +First we're updating basic information. Change `name`, `description`, `keywords`, `homepage` and `support` to match +your project. + +Now the interesting part. You can add more packages your application needs to `require` section. +For example, to use markdown helper you need to add `michelf/php-markdown`. All these packages are coming from +[packagist.org](https://packagist.org/) so feel free to browse the website for useful code. + +After your `composer.json` is changed you can run `php composer.phar update`, wait till packages are downloaded and +installed and then just use them. Autoloading of classes will be handled automatically. diff --git a/docs/guide/apps-basic.md b/docs/guide/apps-basic.md new file mode 100644 index 0000000..e61ab55 --- /dev/null +++ b/docs/guide/apps-basic.md @@ -0,0 +1,160 @@ +Basic application template +========================== + +This template is a perfect fit for small projects or learning Yii2. + +The application has four pages: the homepage, the about page, the contact page and the login page. +The contact page displays a contact form that users can fill in to submit their inquiries to the webmaster, +and the login page allows users to be authenticated before accessing privileged contents. + +Installation +------------ + +If you do not have [Composer](http://getcomposer.org/), you may download it from +[http://getcomposer.org/](http://getcomposer.org/) or run the following command on Linux/Unix/MacOS: + +~~~ +curl -s http://getcomposer.org/installer | php +~~~ + +You can then install the Bootstrap Application using the following command: + +~~~ +php composer.phar create-project --stability=dev yiisoft/yii2-app-basic /path/to/yii-application +~~~ + +Now set document root directory of your Web server to /path/to/yii-application/web and you should be able to access the application using the URL `http://localhost/`. + +Directory structure +------------------- + +The basic application does not divide application directories much. Here's the basic structure: + +- `commands` - console controllers. +- `config` - configuration. +- `controllers` - web controllers. +- `models` - application models. +- `runtime` - logs, states, file cache. +- `views` - view templates. +- `web` - webroot. + +Root directory contains a set of files. + +- `.gitignore` contains a list of directories ignored by git version system. If you need something never get to your source +code repository, add it there. +- `codeception.yml` - Codeception config. +- `composer.json` - Composer config described in detail below. +- `LICENSE.md` - license info. Put your project license there. Especially when opensourcing. +- `README.md` - basic info about installing template. Consider replacing it with information about your project and its + installation. +- `requirements.php` - Yii requirements checker. +- `yii` - console application bootstrap. +- `yii.bat` - same for Windows. + + +### config + +This directory contains configuration files: + +- `AppAsset.php` - definition of application assets such as CSS, JavaScript etc. Check [Managing assets](assets.md) for + details. +- `console.php` - console application configuration. +- `params.php` - common application parameters. +- `web.php` - web application configuration. +- `web-test.php` - web application configuration used when running functional tests. + +All these files except `AppAsset.php` are returning arrays used to configure corresponding application properties. Check +[Configuration](configuration.md) guide section for details. + +### views + +Views directory contains templates your application is using. In the basic template there are: + +``` +layouts + main.php +site + about.php + contact.php + error.php + index.php + login.php +``` + +`layouts` contains HTML layouts i.e. page markup except content: doctype, head section, main menu, footer etc. +The rest are typically controller views. By convention these are located in subdirectories matching controller id. For +`SiteController` views are under `site`. Names of the views themselves are typically match controller action names. +Partials are often named starting with underscore. + +### web + +Directory is a webroot. Typically a webserver is pointed into it. + +``` +assets +css +index.php +index-test.php +``` + +`assets` contains published asset files such as CSS, JavaScript etc. Publishing process is automatic so you don't need +to do anything with this directory other than making sure Yii has enough permissions to write to it. + +`css` contains plain CSS files and is useful for global CSS that isn't going to be compressed or merged by assets manager. + +`index.php` is the main web application bootstrap and is the central entry point for it. `index-test.php` is the entry +point for functional testing. + +Configuring Composer +-------------------- + +After application template is installed it's a good idea to adjust default `composer.json` that can be found in the root +directory: + +```javascript +{ + "name": "yiisoft/yii2-app-basic", + "description": "Yii 2 Basic Application Template", + "keywords": ["yii", "framework", "basic", "application template"], + "homepage": "http://www.yiiframework.com/", + "type": "project", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "minimum-stability": "dev", + "require": { + "php": ">=5.3.0", + "yiisoft/yii2": "dev-master", + "yiisoft/yii2-composer": "dev-master" + }, + "scripts": { + "post-create-project-cmd": [ + "yii\\composer\\InstallHandler::setPermissions" + ] + }, + "extra": { + "yii-install-writable": [ + "runtime", + "web/assets" + ], + "yii-install-executable": [ + "yii" + ] + } +} +``` + +First we're updating basic information. Change `name`, `description`, `keywords`, `homepage` and `support` to match +your project. + +Now the interesting part. You can add more packages your application needs to `require` section. +For example, to use markdown helper you need to add `michelf/php-markdown`. All these packages are coming from +[packagist.org](https://packagist.org/) so feel free to browse the website for useful code. + +After your `composer.json` is changed you can run `php composer.phar update`, wait till packages are downloaded and +installed and then just use them. Autoloading of classes will be handled automatically. diff --git a/docs/guide/bootstrap-widgets.md b/docs/guide/bootstrap-widgets.md new file mode 100644 index 0000000..432dcd8 --- /dev/null +++ b/docs/guide/bootstrap-widgets.md @@ -0,0 +1,47 @@ +Bootstrap widgets +================= + +Out of the box, Yii includes support for the [Bootstrap 3](http://getbootstrap.com/) markup and components framework (also known as "Twitter Bootstrap"). Bootstrap is an excellent, responsive framework that can greatly speed up the client-side of your development process. + +The core of Bootstrap is represented by two parts: + +- CSS basics, such as a grid layout system, typography, helper classes, and responsive utilities. +- Ready to use components, such as menus, pagination, modal boxes, tabs etc. + +Basics +------ + +Yii doesn't wrap the bootstrap basics into PHP code since HTML is very simple by itself in this case. You can find details +about using the basics at [bootstrap documentation website](http://getbootstrap.com/css/). Still Yii provides a +convenient way to include bootstrap assets in your pages with a single line added to `AppAsset.php` located in your +`config` directory: + +```php +public $depends = array( + 'yii\web\YiiAsset', + 'yii\bootstrap\BootstrapAsset', // this line + // 'yii\bootstrap\BootstrapThemeAsset' // uncomment to apply bootstrap 2 style to bootstrap 3 +); +``` + +Using bootstrap through Yii asset manager allows you to minimize its resources and combine with your own resources when +needed. + +Yii widgets +----------- + +Most complex bootstrap components are wrapped into Yii widgets to allow more robust syntax and integrate with +framework features. All widgets belong to `\yii\bootstrap` namespace: + +- Alert +- Button +- ButtonDropdown +- ButtonGroup +- Carousel +- Collapse +- Dropdown +- Modal +- Nav +- NavBar +- Progress +- Tabs diff --git a/docs/guide/bootstrap.md b/docs/guide/bootstrap.md deleted file mode 100644 index 1bc3fe6..0000000 --- a/docs/guide/bootstrap.md +++ /dev/null @@ -1,63 +0,0 @@ -Bootstrap with Yii -================== - -A ready-to-use Web application is distributed together with Yii. You may find -its source code under the `app` folder after you expand the Yii release file. -If you have installed Yii under a Web-accessible folder, you should be able to -access this application through the following URL: - -~~~ -http://localhost/yii/apps/bootstrap/index.php -~~~ - - -As you can see, the application has four pages: the homepage, the about page, -the contact page and the login page. The contact page displays a contact -form that users can fill in to submit their inquiries to the webmaster, -and the login page allows users to be authenticated before accessing privileged contents. - - -The following diagram shows the directory structure of this application. - -~~~ -app/ - index.php Web application entry script file - index-test.php entry script file for the functional tests - assets/ containing published resource files - css/ containing CSS files - img/ containing image files - themes/ containing application themes - protected/ containing protected application files - yiic yiic command line script for Unix/Linux - yiic.bat yiic command line script for Windows - yiic.php yiic command line PHP script - commands/ containing customized 'yiic' commands - components/ containing reusable user components - config/ containing configuration files - console.php the console application configuration - main.php the Web application configuration - controllers/ containing controller class files - SiteController.php the default controller class - data/ containing the sample database - schema.mysql.sql the DB schema for the sample MySQL database - schema.sqlite.sql the DB schema for the sample SQLite database - bootstrap.db the sample SQLite database file - vendor/ containing third-party extensions and libraries - messages/ containing translated messages - models/ containing model class files - User.php the User model - LoginForm.php the form model for 'login' action - ContactForm.php the form model for 'contact' action - runtime/ containing temporarily generated files - views/ containing controller view and layout files - layouts/ containing layout view files - main.php the base layout shared by all pages - site/ containing view files for the 'site' controller - about.php the view for the 'about' action - contact.php the view for the 'contact' action - index.php the view for the 'index' action - login.php the view for the 'login' action -~~~ - - -TBD \ No newline at end of file diff --git a/docs/guide/caching.md b/docs/guide/caching.md index cd945e7..64c7e8d 100644 --- a/docs/guide/caching.md +++ b/docs/guide/caching.md @@ -1,3 +1,189 @@ Caching ======= +Overview and Base Concepts +-------------------------- + +Caching is a cheap and effective way to improve the performance of a web application. By storing relatively +static data in cache and serving it from cache when requested, we save the time needed to generate the data. + +Using cache in Yii mainly involves configuring and accessing a cache application component. The following +application configuration specifies a cache component that uses [memcached](http://memcached.org/) with +two cache servers. Note, this configuration should be done in file located at `@app/config/web.php` alias +in case you're using basic sample application. + +```php +'components' => array( + 'cache' => array( + 'class' => '\yii\caching\MemCache', + 'servers' => array( + array( + 'host' => 'server1', + 'port' => 11211, + 'weight' => 100, + ), + array( + 'host' => 'server2', + 'port' => 11211, + 'weight' => 50, + ), + ), + ), +), +``` + +When the application is running, the cache component can be accessed through `Yii::$app->cache` call. + +Yii provides various cache components that can store cached data in different media. The following +is a summary of the available cache components: + +* [[\yii\caching\ApcCache]]: uses PHP [APC](http://php.net/manual/en/book.apc.php) extension. This option can be + considered as the fastest one when dealing with cache for a centralized thick application (e.g. one + server, no dedicated load balancers, etc.). + +* [[\yii\caching\DbCache]]: uses a database table to store cached data. By default, it will create and use a + [SQLite3](http://sqlite.org/) database under the runtime directory. You can explicitly specify a database for + it to use by setting its `db` property. + +* [[\yii\caching\DummyCache]]: presents dummy cache that does no caching at all. The purpose of this component + is to simplify the code that needs to check the availability of cache. For example, during development or if + the server doesn't have actual cache support, we can use this cache component. When an actual cache support + is enabled, we can switch to use the corresponding cache component. In both cases, we can use the same + code `Yii::$app->cache->get($key)` to attempt retrieving a piece of data without worrying that + `Yii::$app->cache` might be `null`. + +* [[\yii\caching\FileCache]]: uses standard files to store cached data. This is particular suitable + to cache large chunk of data (such as pages). + +* [[\yii\caching\MemCache]]: uses PHP [memcache](http://php.net/manual/en/book.memcache.php) + and [memcached](http://php.net/manual/en/book.memcached.php) extensions. This option can be considered as + the fastest one when dealing with cache in a distributed applications (e.g. with several servers, load + balancers, etc.) + +* [[\yii\caching\RedisCache]]: implements a cache component based on [Redis](http://redis.io/) key-value store + (redis version 2.6 or higher is required). + +* [[\yii\caching\WinCache]]: uses PHP [WinCache](http://iis.net/downloads/microsoft/wincache-extension) + ([see also](http://php.net/manual/en/book.wincache.php)) extension. + +* [[\yii\caching\XCache]]: uses PHP [XCache](http://xcache.lighttpd.net/) extension. + +* [[\yii\caching\ZendDataCache]]: uses + [Zend Data Cache](http://files.zend.com/help/Zend-Server-6/zend-server.htm#data_cache_component.htm) + as the underlying caching medium. + +Tip: because all these cache components extend from the same base class [[Cache]], one can switch to use +a different type of cache without modifying the code that uses cache. + +Caching can be used at different levels. At the lowest level, we use cache to store a single piece of data, +such as a variable, and we call this data caching. At the next level, we store in cache a page fragment which +is generated by a portion of a view script. And at the highest level, we store a whole page in cache and serve +it from cache as needed. + +In the next few subsections, we elaborate how to use cache at these levels. + +Note, by definition, cache is a volatile storage medium. It does not ensure the existence of the cached +data even if it does not expire. Therefore, do not use cache as a persistent storage (e.g. do not use cache +to store session data or other valuable information). + +Data Caching +------------ + +Data caching is about storing some PHP variable in cache and retrieving it later from cache. For this purpose, +the cache component base class [[\yii\caching\Cache]] provides two methods that are used most of the time: +[[set()]] and [[get()]]. Note, only serializable variables and objects could be cached successfully. + +To store a variable `$value` in cache, we choose a unique `$key` and call [[set()]] to store it: + +```php +Yii::$app->cache->set($key, $value); +``` + +The cached data will remain in the cache forever unless it is removed because of some caching policy +(e.g. caching space is full and the oldest data are removed). To change this behavior, we can also supply +an expiration parameter when calling [[set()]] so that the data will be removed from the cache after +a certain period of time: + +```php +// keep the value in cache for at most 45 seconds +Yii::$app->cache->set($key, $value, 45); +``` + +Later when we need to access this variable (in either the same or a different web request), we call [[get()]] +with the key to retrieve it from cache. If the value returned is `false`, it means the value is not available +in cache and we should regenerate it: + +```php +public function getCachedData() +{ + $key = /* generate unique key here */; + $value = Yii::$app->getCache()->get($key); + if ($value === false) { + $value = /* regenerate value because it is not found in cache and then save it in cache for later use */; + Yii::$app->cache->set($key, $value); + } + return $value; +} +``` + +This is the common pattern of arbitrary data caching for general use. + +When choosing the key for a variable to be cached, make sure the key is unique among all other variables that +may be cached in the application. It is **NOT** required that the key is unique across applications because +the cache component is intelligent enough to differentiate keys for different applications. + +Some cache storages, such as MemCache, APC, support retrieving multiple cached values in a batch mode, +which may reduce the overhead involved in retrieving cached data. A method named [[mget()]] is provided +to exploit this feature. In case the underlying cache storage does not support this feature, +[[mget()]] will still simulate it. + +To remove a cached value from cache, call [[delete()]]; and to remove everything from cache, call [[flush()]]. +Be very careful when calling [[flush()]] because it also removes cached data that are from other applications. + +Note, because [[Cache]] implements `ArrayAccess`, a cache component can be used liked an array. The followings +are some examples: + +```php +$cache = Yii::$app->getComponent('cache'); +$cache['var1'] = $value1; // equivalent to: $cache->set('var1', $value1); +$value2 = $cache['var2']; // equivalent to: $value2 = $cache->get('var2'); +``` + +### Cache Dependency + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.data#cache-dependency + +### Query Caching + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.data#query-caching + +Fragment Caching +---------------- + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.fragment + +### Caching Options + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.fragment#caching-options + +### Nested Caching + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.fragment#nested-caching + +Dynamic Content +--------------- + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.dynamic + +Page Caching +------------ + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.page + +### Output Caching + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.page#output-caching + +### HTTP Caching + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.page#http-caching diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md new file mode 100644 index 0000000..8325855 --- /dev/null +++ b/docs/guide/configuration.md @@ -0,0 +1,98 @@ +Configuration +============= + +Yii applications rely upon components to perform most of the common tasks, such as connecting to a database, routing browser requests, and handling sessions. How these stock components behave can be adjusted by *configuring* your Yii application. The majority of components have sensible defaults, so it's unlikely that you'll spend a lot of time configuring +them. Still there are some mandatory settings, such as the database connection, that you will have to establish. + +How application is configured depends on application template but there are some general principles applying in any case. + +Configuring options in bootstrap file +------------------------------------- + +For each application in Yii there is at least one bootstrap file. For web applications it's typically `index.php`, for +console applications it's `yii`. Both are doing nearly the same job: + +1. Setting common constants. +2. Including Yii itself. +3. Including Composer autoloader. +4. Reading config file into `$config`. +5. Creating new application instance using `$config` and running it. + +Bootstrap file is not the part of framework but your application so it's OK to adjust it to fit your application. Typical +adjustments are the value of `YII_DEBUG` that should never be `true` on production and the way config is read. + +Configuring application instance +-------------------------------- + +It was mentioned above that application is configured in bootstrap file when its instance is created. Config is typically +stored in a PHP file in `/config` directory of the application and looks like the following: + +```php +<?php +return array( + 'id' => 'applicationId', + 'basePath' => dirname(__DIR__), + 'components' => array( + // ... + ), + 'params' => require(__DIR__ . '/params.php'), +); +``` + +In the above array keys are names of application properties. Depending on application type you can check properties of +either `\yii\web\Application` or `\yii\console\Application`. Both are extended from `\yii\base\Application`. + +> Note that you can configure not only public class properties but anything accessible via setter. For example, to + configure runtime path you can use key named `runtimePath`. There's no such property in the application class but + since there's a corresponding setter named `setRuntimePath` it will be properly configured. + +Configuring application components +---------------------------------- + +Majority of Yii functionality are application components. These are attached to application via its `components` property: + +```php +<?php +return array( + 'id' => 'applicationId', + 'basePath' => dirname(__DIR__), + 'components' => array( + 'cache' => array( + 'class' => 'yii\caching\FileCache', + ), + 'user' => array( + 'identityClass' => 'app\models\User', + ), + 'errorHandler' => array( + 'errorAction' => 'site/error', + ), + 'log' => array( + 'traceLevel' => YII_DEBUG ? 3 : 0, + 'targets' => array( + array( + 'class' => 'yii\log\FileTarget', + 'levels' => array('error', 'warning'), + ), + ), + ), + ), + // ... +); +``` + +In the above four components are configured: `cache`, `user`, `errorHandler`, `log`. Each entry key is a component ID +and the value is the configuration array. ID is used to access the component like `\Yii::$app->myComponent`. +Configuration array has one special key named `class` that sets component class. The rest of the keys and values are used +to configure component properties in the same way as top-level keys are used to configure application properties. + +Each application has predefined set of the components. In case of configuring one of these `class` key is omitted and +application default class is used instead. You can check `registerCoreComponents` method of the application you are using +to get a list of component IDs and corresponding classes. + +Note that Yii is smart enough to configure the component when it's actually used i.e. if `cache` is never used it will +not be instantiated and configured at all. + +Setting component defaults classwide +------------------------------------ + +TBD diff --git a/docs/guide/controller.md b/docs/guide/controller.md index e69de29..c665037 100644 --- a/docs/guide/controller.md +++ b/docs/guide/controller.md @@ -0,0 +1,192 @@ +Controller +========== + +Controller is one of the key parts of the application. It determines how to handle incoming request and creates a response. + +Most often a controller takes HTTP request data and returns HTML, JSON or XML as a response. + +Basics +------ + +Controller resides in application's `controllers` directory is is named like `SiteController.php` where `Site` +part could be anything describing a set of actions it contains. + +The basic web controller is a class that extends [[\yii\web\Controller]] and could be very simple: + +```php +namespace app\controllers; + +use yii\web\Controller; + +class SiteController extends Controller +{ + public function actionIndex() + { + // will render view from "views/site/index.php" + return $this->render('index'); + } + + public function actionTest() + { + // will just print "test" to the browser + return 'test'; + } +} +``` + +As you can see, typical controller contains actions that are public class methods named as `actionSomething`. +The output of an action is what the method returns. The return value will be handled by the `response` application +component which can convert the output to differnet formats such as JSON for example. The default behavior +is to output the value unchanged though. + +Routes +------ + +Each controller action has a corresponding internal route. In our example above `actionIndex` has `site/index` route +and `actionTest` has `site/test` route. In this route `site` is referred to as controller ID while `test` is referred to +as action ID. + +By default you can access specific controller and action using the `http://example.com/?r=controller/action` URL. This +behavior is fully customizable. For details refer to [URL Management](url.md). + +If controller is located inside a module its action internal route will be `module/controller/action`. + +In case module, controller or action specified isn't found Yii will return "not found" page and HTTP status code 404. + +> Note: If controller name or action name contains camelCased words, internal route will use dashes i.e. for +`DateTimeController::actionFastForward` route will be `date-time/fast-forward`. + +### Defaults + +If user isn't specifying any route i.e. using URL like `http://example.com/`, Yii assumes that default route should be +used. It is determined by [[\yii\web\Application::defaultRoute]] method and is `site` by default meaning that `SiteController` +will be loaded. + +A controller has a default action. When the user request does not specify which action to execute by using an URL such as +`http://example.com/?r=site`, the default action will be executed. By default, the default action is named as `index`. +It can be changed by setting the [[\yii\base\Controller::defaultAction]] property. + +Action parameters +----------------- + +It was already mentioned that a simple action is just a public method named as `actionSomething`. Now we'll review +ways that an action can get parameters from HTTP. + +### Action parameters + +You can define named arguments for an action and these will be automatically populated from corresponding values from +`$_GET`. This is very convenient both because of the short syntax and an ability to specify defaults: + +```php +namespace app\controllers; + +use yii\web\Controller; + +class BlogController extends Controller +{ + public function actionView($id, $version = null) + { + $post = Post::find($id); + $text = $post->text; + + if($version) { + $text = $post->getHistory($version); + } + + return $this->render('view', array( + 'post' => $post, + 'text' => $text, + )); + } +} +``` + +The action above can be accessed using either `http://example.com/?r=blog/view&id=42` or +`http://example.com/?r=blog/view&id=42&version=3`. In the first case `version` isn't specified and default parameter +value is used instead. + +### Getting data from request + +If your action is working with data from HTTP POST or has too many GET parameters you can rely on request object that +is accessible via `\Yii::$app->request`: + +```php +namespace app\controllers; + +use yii\web\Controller; +use yii\web\HttpException; + +class BlogController extends Controller +{ + public function actionUpdate($id) + { + $post = Post::find($id); + if(!$post) { + throw new HttpException(404); + } + + if(\Yii::$app->request->isPost)) { + $post->load($_POST); + if($post->save()) { + $this->redirect(array('view', 'id' => $post->id)); + } + } + + return $this->render('update', array( + 'post' => $post, + )); + } +} +``` + +Standalone actions +------------------ + +If action is generic enough it makes sense to implement it in a separate class to be able to reuse it. +Create `actions/Page.php` + +```php +namespace \app\actions; + +class Page extends \yii\base\Action +{ + public $view = 'index'; + + public function run() + { + $this->controller->render($view); + } +} +``` + +The following code is too simple to implement as a separate action but gives an idea of how it works. Action implemented +can be used in your controller as following: + +```php +public SiteController extends \yii\web\Controller +{ + public function actions() + { + return array( + 'about' => array( + 'class' => '@app/actions/Page', + 'view' => 'about', + ), + ), + ); + } +} +``` + +After doing so you can access your action as `http://example.com/?r=site/about`. + +Filters +------- + +Catching all incoming requests +------------------------------ + +See also +-------- + +- [Console](console.md) diff --git a/docs/guide/database-basics.md b/docs/guide/database-basics.md new file mode 100644 index 0000000..85bc042 --- /dev/null +++ b/docs/guide/database-basics.md @@ -0,0 +1,246 @@ +Database basics +=============== + +Yii has a database access layer built on top of PHP's [PDO](http://www.php.net/manual/en/ref.pdo.php). It provides +uniform API and solves some inconsistencies between different DBMS. By default Yii supports the following DBMS: + +- [MySQL](http://www.mysql.com/) +- [SQLite](http://sqlite.org/) +- [PostgreSQL](http://www.postgresql.org/) +- [CUBRID](http://www.cubrid.org/) (version 9.1.0 and higher). +- Oracle +- MSSQL + + +Configuration +------------- + +In order to start using database you need to configure database connection component first by adding `db` component +to application configuration (for "basic" web application it's `config/web.php`) like the following: + +```php +return array( + // ... + 'components' => array( + // ... + 'db' => array( + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=mydatabase', // MySQL, MariaDB + //'dsn' => 'sqlite:/path/to/database/file', // SQLite + //'dsn' => 'pgsql:host=localhost;port=5432;dbname=mydatabase', // PostgreSQL + //'dsn' => 'cubrid:dbname=demodb;host=localhost;port=33000', // CUBRID + //'dsn' => 'sqlsrv:Server=localhost;Database=mydatabase', // MS SQL Server, sqlsrv driver + //'dsn' => 'dblib:host=localhost;dbname=mydatabase', // MS SQL Server, dblib driver + //'dsn' => 'mssql:host=localhost;dbname=mydatabase', // MS SQL Server, mssql driver + //'dsn' => 'oci:dbname=//localhost:1521/mydatabase', // Oracle + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8', + ), + ), + // ... +); +``` +Please refer to the [PHP manual](http://www.php.net/manual/en/function.PDO-construct.php) for more details +on the format of the DSN string. + +After the connection component is configured you can access it using the following syntax: + +```php +$connection = \Yii::$app->db; +``` + +You can refer to [[\yii\db\Connection]] for a list of properties you can configure. Also note that you can define more +than one connection component and use both at the same time if needed: + +```php +$primaryConnection = \Yii::$app->db; +$secondaryConnection = \Yii::$app->secondDb; +``` + +If you don't want to define the connection as an application component you can instantiate it directly: + +```php +$connection = new \yii\db\Connection(array( + 'dsn' => $dsn, + 'username' => $username, + 'password' => $password, +)); +$connection->open(); +``` + + +Basic SQL queries +----------------- + +Once you have a connection instance you can execute SQL queries using [[\yii\db\Command]]. + +### SELECT + +When query returns a set of rows: + +```php +$command = $connection->createCommand('SELECT * FROM tbl_post'); +$posts = $command->queryAll(); +``` + +When only a single row is returned: + +```php +$command = $connection->createCommand('SELECT * FROM tbl_post WHERE id=1'); +$post = $command->queryOne(); +``` + +When there are multiple values from the same column: + +```php +$command = $connection->createCommand('SELECT title FROM tbl_post'); +$titles = $command->queryColumn(); +``` + +When there's a scalar value: + +```php +$command = $connection->createCommand('SELECT COUNT(*) FROM tbl_post'); +$postCount = $command->queryScalar(); +``` + +### UPDATE, INSERT, DELETE etc. + +If SQL executed doesn't return any data you can use command's `execute` method: + +```php +$command = $connection->createCommand('UPDATE tbl_post SET status=1 WHERE id=1'); +$command->execute(); +``` + +Alternatively the following syntax that takes care of proper table and column names quoting is possible: + +```php +// INSERT +$connection->createCommand()->insert('tbl_user', array( + 'name' => 'Sam', + 'age' => 30, +))->execute(); + +// INSERT multiple rows at once +$connection->createCommand()->batchInsert('tbl_user', array('name', 'age'), array( + array('Tom', 30), + array('Jane', 20), + array('Linda', 25), +))->execute(); + +// UPDATE +$connection->createCommand()->update('tbl_user', array( + 'status' => 1, +), 'age > 30')->execute(); + +// DELETE +$connection->createCommand()->delete('tbl_user', 'status = 0')->execute(); +``` + +Quoting table and column names +------------------------------ + +Most of the time you would use the following syntax for quoting table and column names: + +```php +$sql = "SELECT COUNT([[$column]]) FROM {{$table}}"; +$rowCount = $connection->createCommand($sql)->queryScalar(); +``` + +In the code above `[[X]]` will be converted to properly quoted column name while `{{Y}}` will be converted to properly +quoted table name. + +The alternative is to quote table and column names manually using [[\yii\db\Connection::quoteTableName()]] and +[[\yii\db\Connection::quoteColumnName()]]: + +```php +$column = $connection->quoteColumnName($column); +$table = $connection->quoteTableName($table); +$sql = "SELECT COUNT($column) FROM $table"; +$rowCount = $connection->createCommand($sql)->queryScalar(); +``` + +Prepared statements +------------------- + +In order to securely pass query parameters you can use prepared statements: + +```php +$command = $connection->createCommand('SELECT * FROM tbl_post WHERE id=:id'); +$command->bindValue(':id', $_GET['id']); +$post = $command->query(); +``` + +Another usage is performing a query multiple times while preparing it only once: + +```php +$command = $connection->createCommand('DELETE FROM tbl_post WHERE id=:id'); +$command->bindParam(':id', $id); + +$id = 1; +$command->execute(); + +$id = 2; +$command->execute(); +``` + +Transactions +------------ + +If the underlying DBMS supports transactions, you can perform transactional SQL queries like the following: + +```php +$transaction = $connection->beginTransaction(); +try { + $connection->createCommand($sql1)->execute(); + $connection->createCommand($sql2)->execute(); + // ... executing other SQL statements ... + $transaction->commit(); +} catch(Exception $e) { + $transaction->rollback(); +} +``` + +Working with database schema +---------------------------- + +### Getting schema information + +You can get a [[\yii\db\Schema]] instance like the following: + +```php +$schema = $connection->getSchema(); +``` + +It contains a set of methods allowing you to retrieve various information about the database: + +```php +$tables = $schema->getTableNames(); +``` + +For the full reference check [[\yii\db\Schema]]. + +### Modifying schema + +Aside from basic SQL queries [[\yii\db\Command]] contains a set of methods allowing to modify database schema: + +- createTable, renameTable, dropTable, truncateTable +- addColumn, renameColumn, dropColumn, alterColumn +- addPrimaryKey, dropPrimaryKey +- addForeignKey, dropForeignKey +- createIndex, dropIndex + +These can be used as follows: + +```php +// CREATE TABLE +$connection->createCommand()->createTable('tbl_post', array( + 'id' => 'pk', + 'title' => 'string', + 'text' => 'text', +); +``` + +For the full reference check [[\yii\db\Command]]. diff --git a/docs/guide/debugger.md b/docs/guide/debugger.md new file mode 100644 index 0000000..768f25d --- /dev/null +++ b/docs/guide/debugger.md @@ -0,0 +1,15 @@ +Debug toolbar and debugger +========================== + +Yii2 includes a handy toolbar to aid faster development and debugging as well as debugger. Toolbar displays information +about currently opened page while using debugger you can analyze data collected before. + +Installing and configuring +-------------------------- + +How to use it +------------- + +Creating your own panels +------------------------ + diff --git a/docs/guide/error.md b/docs/guide/error.md index c97fada..20bb23d 100644 --- a/docs/guide/error.md +++ b/docs/guide/error.md @@ -1,3 +1,7 @@ Error Handling ============== +Error handling in Yii is different from plain PHP. First of all, all non-fatal errors are converted to exceptions so +you can use `try`-`catch` to work with these. Second, even fatal errors are rendered in a nice way. In debug mode that +means you have a trace and a piece of code where it happened so it takes less time to analyze and fix it. + diff --git a/docs/guide/form.md b/docs/guide/form.md index e69de29..c1f1ba3 100644 --- a/docs/guide/form.md +++ b/docs/guide/form.md @@ -0,0 +1,3 @@ +Working with forms +================== + diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md index e69de29..57f08f7 100644 --- a/docs/guide/i18n.md +++ b/docs/guide/i18n.md @@ -0,0 +1,267 @@ +Internationalization +==================== + +Internationalization (I18N) refers to the process of designing a software application so that it can be adapted to +various languages and regions without engineering changes. For Web applications, this is of particular importance +because the potential users may be worldwide. + +Locale and Language +------------------- + +There are two languages defined in Yii application: [[\yii\base\Application::$sourceLanguage|source language]] and +[[\yii\base\Application::$language|target language]]. + +Source language is the language original application messages are written in such as: + +```php +echo \Yii::t('app', 'I am a message!'); +``` + +> **Tip**: Default is English and it's not recommended to change it. The reason is that it's easier to find people translating from +> English to any language than from non-English to non-English. + +Target language is what's currently used. It's defined in application configuration like the following: + +```php +// ... +return array( + 'id' => 'applicationID', + 'basePath' => dirname(__DIR__), + 'language' => 'ru_RU' // ← here! +``` + +Later you can easily change it in runtime: + +```php +\Yii::$app->language = 'zh_CN'; +``` + +Basic message translation +------------------------- + +Yii basic message translation in its basic variant works without additional PHP extension. What it does is finding a +translation of the message from source language into target language. Message itself is specified as the second +`\Yii::t` method parameter: + +```php +echo \Yii::t('app', 'This is a string to translate!'); +``` + +Yii tries to load approprite translation from one of the message sources defined via `i18n` component configuration: + +```php +'components' => array( + // ... + 'i18n' => array( + 'translations' => array( + 'app*' => array( + 'class' => 'yii\i18n\PhpMessageSource', + //'basePath' => '@app/messages', + //'sourceLanguage' => 'en_US', + 'fileMap' => array( + 'app' => 'app.php', + 'app/error' => 'error.php', + ), + ), + ), + ), +), +``` + +In the above `app*` is a pattern that specifies which categories are handled by the message source. In this case we're +handling everything that begins with `app`. + +`class` defines which message source is used. The following message sources are available: + +- PhpMessageSource that uses PHP files. +- GettextMessageSource that uses GNU Gettext MO or PO files. +- DbMessageSource that uses database. + +`basePath` defines where to store messages for the currently used message source. In this case it's `messages` directory + in your application directory. In case of using database this option should be skipped. + +`sourceLanguage` defines which language is used in `\Yii::t` second argument. If not specified, application's source +language is used. + +`fileMap` specifies how message categories specified in the first argument of `\Yii::t()` are mapped to files when +`PhpMessageSource` is used. In the example we're defining two categories `app` and `app/error`. + +Instead of configuring `fileMap` you can rely on convention which is `messages/BasePath/LanguageID/CategoryName.php`. + +### Named placeholders + +You can add parameters to a translation message that will be substituted with the corresponding value after translation. +The format for this is to use curly brackets around the parameter name as you can see in the following example: + +```php +$username = 'Alexander'; +echo \Yii::t('app', 'Hello, {username}!', array( + 'username' => $username, +)); +``` + +Note that the parameter assignment is without the brackets. + +### Positional placeholders + +```php +$sum = 42; +echo \Yii::t('app', 'Balance: {0}', $sum); +``` + +> **Tip**: Try keep message strings meaningful and avoid using too many positional parameters. Remember that +> translator has source string only so it should be obvious about what will replace each placeholder. + +Advanced placeholder formatting +------------------------------- + +In order to use advanced features you need to install and enable [intl](http://www.php.net/manual/en/intro.intl.php) PHP +extension. After installing and enabling it you will be able to use extended syntax for placeholders. Either short form +`{placeholderName, argumentType}` that means default setting or full form `{placeholderName, argumentType, argumentStyle}` +that allows you to specify formatting style. + +Full reference is [available at ICU website](http://icu-project.org/apiref/icu4c/classMessageFormat.html) but since it's +a bit crypric we have our own reference below. + +### Numbers + +```php +$sum = 42; +echo \Yii::t('app', 'Balance: {0, number}', $sum); +``` + +You can specify one of the built-in styles (`integer`, `currency`, `percent`): + +```php +$sum = 42; +echo \Yii::t('app', 'Balance: {0, number, currency}', $sum); +``` + +Or specify custom pattern: + +```php +$sum = 42; +echo \Yii::t('app', 'Balance: {0, number, ,000,000000}', $sum); +``` + +[Formatting reference](http://icu-project.org/apiref/icu4c/classicu_1_1DecimalFormat.html). + +### Dates + +```php +echo \Yii::t('app', 'Today is {0, date}', time()); +``` + +Built in formats (`short`, `medium`, `long`, `full`): + +```php +echo \Yii::t('app', 'Today is {0, date, short}', time()); +``` + +Custom pattern: + +```php +echo \Yii::t('app', 'Today is {0, date, YYYY-MM-dd}', time()); +``` + +[Formatting reference](http://icu-project.org/apiref/icu4c/classicu_1_1SimpleDateFormat.html). + +### Time + +```php +echo \Yii::t('app', 'It is {0, time}', time()); +``` + +Built in formats (`short`, `medium`, `long`, `full`): + +```php +echo \Yii::t('app', 'It is {0, time, short}', time()); +``` + +Custom pattern: + +```php +echo \Yii::t('app', 'It is {0, date, HH:mm}', time()); +``` + +[Formatting reference](http://icu-project.org/apiref/icu4c/classicu_1_1SimpleDateFormat.html). + + +### Spellout + +```php +echo \Yii::t('app', '{n,number} is spelled as {n, spellout}', array( + 'n' => 42, +)); +``` + +### Ordinal + +```php +echo \Yii::t('app', 'You are {n, ordinal} visitor here!', array( + 'n' => 42, +)); +``` + +Will produce "You are 42nd visitor here!". + +### Duration + + +```php +echo \Yii::t('app', 'You are here for {n, duration} already!', array( + 'n' => 42, +)); +``` + +Will produce "You are here for 47 sec. already!". + +### Plurals + +Different languages have different ways to inflect plurals. Some rules are very complex so it's very handy that this +functionality is provided without the need to specify inflection rule. Instead it only requires your input of inflected +word in certain situations. + +```php +echo \Yii::t('app', 'There {n, plural, =0{are no cats} =1{is one cat} other{are # cats}}!', array( + 'n' => 0, +)); +``` + +Will give us "There are no cats!". + +In the plural rule arguments above `=0` means exactly zero, `=1` stands for exactly one `other` is for any other number. +`#` is replaced with the `n` argument value. It's not that simple for languages other than English. Here's an example +for Russian: + +``` +Здесь {n, plural, =0{котов нет} =1{есть один кот} one{# кот} few{# кота} many{# котов} other{# кота}}! +``` + +In the above it worth mentioning that `=1` matches exactly `n = 1` while `one` matches `21` or `101`. + +To learn which inflection forms you should specify for your language you can referer to +[rules reference at unicode.org](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). + +### Selections + +You can select phrases based on keywords. The pattern in this case specifies how to map keywords to phrases and +provides a default phrase. + +```php +echo \Yii::t('app', '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', array( + 'name' => 'Snoopy', + 'gender' => 'dog', +)); +``` + +Will produce "Snoopy is dog and it loves Yii!". + +In the expression `female` and `male` are possible values. `other` handler values that do not match. Strings inside +brackets are sub-expressions so could be just a string or a string with more placeholders. + +Formatters +---------- + +In order to use formatters you need to install and enable [intl](http://www.php.net/manual/en/intro.intl.php) PHP +extension. diff --git a/docs/guide/index.md b/docs/guide/index.md index dd72ca3..601323a 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -1,30 +1,80 @@ -* [Overview](overview.md) -* [Installation](installation.md) -* [Bootstrap with Yii](bootstrap.md) -* [MVC Overview](mvc.md) -* [Controller](controller.md) -* [Model](model.md) -* [View](view.md) -* [Application](application.md) -* [Form](form.md) -* [Data Validation](validation.md) -* [Database Access Objects](dao.md) -* [Query Builder](query-builder.md) -* [ActiveRecord](active-record.md) -* [Database Migration](migration.md) -* [Caching](caching.md) -* [Internationalization](i18n.md) -* [Extending Yii](extension.md) -* [Authentication](authentication.md) -* [Authorization](authorization.md) -* [Logging](logging.md) -* [URL Management](url.md) -* [Theming](theming.md) -* [Error Handling](error.md) -* [Template](template.md) -* [Console Application](console.md) -* [Security](security.md) -* [Performance Tuning](performance.md) -* [Testing](testing.md) -* [Automatic Code Generation](gii.md) -* [Upgrading from 1.1 to 2.0](upgrade-from-v1.md) +Introduction +============ + +- [Overview](overview.md) + +Getting started +=============== + +- [Installation](installation.md) +- [Configuration](configuration.md) + +Application templates +===================== + +- [Basic](apps-basic.md) +- [Advanced](apps-advanced.md) +- [Creating your own application template](apps-own.md) + +Base concepts +============= + +- [MVC Overview](mvc.md) +- [Model](model.md) +- [View](view.md) +- [Controller](controller.md) +- [Application](application.md) + +Database +======== + +- [Basics](database-basics.md) +- [Query Builder](query-builder.md) +- [ActiveRecord](active-record.md) +- [Database Migration](migration.md) + +Extensions +========== + +- [Extending Yii](extension.md) +- [Using template engines](template.md) + +Security and access control +=========================== + +- [Authentication](authentication.md) +- [Authorization](authorization.md) +- [Security](security.md) +- Role based access control + +Data providers, lists and grids +=============================== + +- Overview +- Data providers +- Grids +- Lists + +Toolbox +======= + +- [Automatic Code Generation](gii.md) +- [Debug toolbar and debugger](debugger.md) +- [Error Handling](error.md) +- [Logging](logging.md) + +More +==== + +- [Bootstrap widgets](bootstrap-widgets.md) +- [Working with forms](form.md) +- [Model validation reference](validation.md) +- [Caching](caching.md) +- [Internationalization](i18n.md) +- [URL Management](url.md) +- [Theming](theming.md) +- [Console Application](console.md) +- [Performance Tuning](performance.md) +- [Managing assets](assets.md) +- [Testing](testing.md) +- [Upgrading from 1.1 to 2.0](upgrade-from-v1.md) diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 3f9a803..16a496c 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -1,7 +1,32 @@ Installation ============ -Installation of Yii mainly involves the following two steps: +Installing via Composer +----------------------- + +The recommended way of installing Yii is by using [Composer](http://getcomposer.org/) package manager. If you do not +have it, you may download it from [http://getcomposer.org/](http://getcomposer.org/) or run the following command: + +``` +curl -s http://getcomposer.org/installer | php +``` + +Yii provides a few ready-to-use application templates. Based on your needs, you may choose one of them to bootstrap +your project. + +There are two application templates available: + +- [basic](https://github.com/yiisoft/yii2-app-basic) that is just a basic frontend application template. +- [advanced](https://github.com/yiisoft/yii2-app-advanced) that is a set of frontend, backend, console, common + (shared code) and environments support. + +Please refer to installation instructions on these pages. To read more about ideas behing these application templates and +proposed usage refer to [basic application template](apps-basic.md) and [advanced application template](apps-advanced.md). + +Installing from zip +------------------- + +Installation from zip mainly involves the following two steps: 1. Download Yii Framework from [yiiframework.com](http://www.yiiframework.com/). 2. Unpack the Yii release file to a Web-accessible directory. @@ -12,7 +37,6 @@ needs to be exposed to Web users. Other PHP scripts, including those from Yii, should be protected from Web access; otherwise they might be exploited by hackers. - Requirements ------------ @@ -24,7 +48,7 @@ script via the following URL in a Web browser: http://hostname/path/to/yii/requirements/index.php ~~~ -Yii requires PHP 5.3, so the server must have PHP 5.3 or above installed and +Yii requires PHP 5.3.7, so the server must have PHP 5.3.7 or above installed and available to the web server. Yii has been tested with [Apache HTTP server](http://httpd.apache.org/) on Windows and Linux. It may also run on other Web servers and platforms, provided PHP 5.3 is supported. @@ -93,7 +117,10 @@ server { set $fsn $fastcgi_script_name; } + #for php-cgi fastcgi_pass 127.0.0.1:9000; + #for php-fpm + #fastcgi_pass unix:/var/run/php5-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fsn; diff --git a/docs/guide/migration.md b/docs/guide/migration.md index bafd293..d6c7165 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -19,7 +19,7 @@ The following steps show how we can use database migration during development: 4. Doug applies the migration to his local development database -Yii supports database migration via the `yiic migrate` command line tool. This +Yii supports database migration via the `yii migrate` command line tool. This tool supports creating new migrations, applying/reverting/redoing migrations, and showing migration history and new migrations. @@ -28,25 +28,24 @@ Creating Migrations To create a new migration (e.g. create a news table), we run the following command: -~~~ -yiic migrate/create <name> -~~~ +``` +yii migrate/create <name> +``` The required `name` parameter specifies a very brief description of the migration (e.g. `create_news_table`). As we will show in the following, the `name` parameter is used as part of a PHP class name. Therefore, it should only contain letters, digits and/or underscore characters. -~~~ -yiic migrate/create create_news_table -~~~ +``` +yii migrate/create create_news_table +``` The above command will create under the `protected/migrations` directory a new file named `m101129_185401_create_news_table.php` which contains the following initial code: -~~~ -[php] +```php class m101129_185401_create_news_table extends \yii\db\Migration { public function up() @@ -59,7 +58,7 @@ class m101129_185401_create_news_table extends \yii\db\Migration return false; } } -~~~ +``` Notice that the class name is the same as the file name which is of the pattern `m<timestamp>_<name>`, where `<timestamp>` refers to the UTC timestamp (in the @@ -78,15 +77,14 @@ method returns `false` to indicate that the migration cannot be reverted. As an example, let's show the migration about creating a news table. -~~~ -[php] +```php class m101129_185401_create_news_table extends \yii\db\Migration { public function up() { - $this->db->createCommand()->createTable('tbl_news, array( + $this->db->createCommand()->createTable('tbl_news', array( 'id' => 'pk', - 'title' => 'string NOT NULL', + 'title' => 'string(128) NOT NULL', 'content' => 'text', ))->execute(); } @@ -96,11 +94,21 @@ class m101129_185401_create_news_table extends \yii\db\Migration $this->db->createCommand()->dropTable('tbl_news')->execute(); } } -~~~ +``` The base class [\yii\db\Migration] exposes a database connection via `db` property. You can use it for manipulating data and schema of a database. +The column types used in this example are abstract types that will be replaced +by Yii with the corresponding types depended on your database management system. +You can use them to write database independent migrations. +For example `pk` will be replaced by `int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY` +for MySQL and `integer PRIMARY KEY AUTOINCREMENT NOT NULL` for sqlite. +See documentation of [[QueryBuilder::getColumnType()]] for more details and a list +of available types. You may also use the constants defined in [[\yii\db\Schema]] to +define column types. + + Transactional Migrations ------------------------ @@ -112,8 +120,7 @@ DB transactions. We could explicitly start a DB transaction and enclose the rest of the DB-related code within the transaction, like the following: -~~~ -[php] +```php class m101129_185401_create_news_table extends \yii\db\Migration { public function up() @@ -121,7 +128,7 @@ class m101129_185401_create_news_table extends \yii\db\Migration $transaction=$this->getDbConnection()->beginTransaction(); try { - $this->db->createCommand()->createTable('tbl_news, array( + $this->db->createCommand()->createTable('tbl_news', array( 'id' => 'pk', 'title' => 'string NOT NULL', 'content' => 'text', @@ -138,7 +145,7 @@ class m101129_185401_create_news_table extends \yii\db\Migration // ...similar code for down() } -~~~ +``` > Note: Not all DBMS support transactions. And some DB queries cannot be put > into a transaction. In this case, you will have to implement `up()` and @@ -152,9 +159,9 @@ Applying Migrations To apply all available new migrations (i.e., make the local database up-to-date), run the following command: -~~~ -yiic migrate -~~~ +``` +yii migrate +``` The command will show the list of all new migrations. If you confirm to apply the migrations, it will run the `up()` method in every new migration class, one @@ -169,18 +176,18 @@ application component. Sometimes, we may only want to apply one or a few new migrations. We can use the following command: -~~~ -yiic migrate/up 3 -~~~ +``` +yii migrate/up 3 +``` This command will apply the 3 new migrations. Changing the value 3 will allow us to change the number of migrations to be applied. We can also migrate the database to a specific version with the following command: -~~~ -yiic migrate/to 101129_185401 -~~~ +``` +yii migrate/to 101129_185401 +``` That is, we use the timestamp part of a migration name to specify the version that we want to migrate the database to. If there are multiple migrations between @@ -195,9 +202,9 @@ Reverting Migrations To revert the last one or several applied migrations, we can use the following command: -~~~ -yiic migrate/down [step] -~~~ +``` +yii migrate/down [step] +``` where the optional `step` parameter specifies how many migrations to be reverted back. It defaults to 1, meaning reverting back the last applied migration. @@ -212,9 +219,9 @@ Redoing Migrations Redoing migrations means first reverting and then applying the specified migrations. This can be done with the following command: -~~~ -yiic migrate/redo [step] -~~~ +``` +yii migrate/redo [step] +``` where the optional `step` parameter specifies how many migrations to be redone. It defaults to 1, meaning redoing the last migration. @@ -226,10 +233,10 @@ Showing Migration Information Besides applying and reverting migrations, the migration tool can also display the migration history and the new migrations to be applied. -~~~ -yiic migrate/history [limit] -yiic migrate/new [limit] -~~~ +``` +yii migrate/history [limit] +yii migrate/new [limit] +``` where the optional parameter `limit` specifies the number of migrations to be displayed. If `limit` is not specified, all available migrations will be displayed. @@ -246,11 +253,11 @@ version without actually applying or reverting the relevant migrations. This often happens when developing a new migration. We can use the following command to achieve this goal. -~~~ -yiic migrate/mark 101129_185401 -~~~ +``` +yii migrate/mark 101129_185401 +``` -This command is very similar to `yiic migrate/to` command, except that it only +This command is very similar to `yii migrate/to` command, except that it only modifies the migration history table to the specified version without applying or reverting the migrations. @@ -290,17 +297,17 @@ line: To specify these options, execute the migrate command using the following format -~~~ -yiic migrate/up --option1=value1 --option2=value2 ... -~~~ +``` +yii migrate/up --option1=value1 --option2=value2 ... +``` For example, if we want to migrate for a `forum` module whose migration files are located within the module's `migrations` directory, we can use the following command: -~~~ -yiic migrate/up --migrationPath=ext.forum.migrations -~~~ +``` +yii migrate/up --migrationPath=ext.forum.migrations +``` ### Configure Command Globally diff --git a/docs/guide/model.md b/docs/guide/model.md index e69de29..b93fb7a 100644 --- a/docs/guide/model.md +++ b/docs/guide/model.md @@ -0,0 +1,269 @@ +Model +===== + +In keeping with the MVC approach, a model in Yii is intended for storing or temporarily representing application data. Yii models have the following basic features: + +- Attribute declaration: a model defines what is considered an attribute. +- Attribute labels: each attribute may be associated with a label for display purpose. +- Massive attribute assignment: the ability to populate multiple model attributes in one step. +- Scenario-based data validation. + +Models in Yii extend from the [[\yii\base\Model]] class. Models are typically used to both hold data and define the validation rules for that data. The validation rules greatly simply the generation of models from complex web forms. +The Model class is also the base for more advanced models with additional functionality such as [Active Record](active-record.md). + +Attributes +---------- + +Attributes store the actual data represented by a model and can +be accessed like object member variables. For example, a `Post` model +may contain a `title` attribute and a `content` attribute which may be +accessed as follows: + +```php +$post = new Post; +$post->title = 'Hello, world'; +$post->content = 'Something interesting is happening'; +echo $post->title; +echo $post->content; +``` + +Since [[\yii\base\Model|Model]] implements the [ArrayAccess](http://php.net/manual/en/class.arrayaccess.php) interface, +you can also access the attributes like accessing array elements: + +```php +$post = new Post; +$post['title'] = 'Hello, world'; +$post['content'] = 'Something interesting is happening'; +echo $post['title']; +echo $post['content']; +``` + +By default, [[\yii\base\Model|Model]] requires that attributes be declared as *public* and *non-static* +class member variables. In the following example, the `LoginForm` model class declares two attributes: +`username` and `password`. + +```php +// LoginForm has two attributes: username and password +class LoginForm extends \yii\base\Model +{ + public $username; + public $password; +} +``` + +Derived model classes may use different ways to declare attributes by overriding the [[\yii\base\Model::attributes()|attributes()]] +method. For example, [[\yii\db\ActiveRecord]] defines attributes as the column names of the database table +that is associated with the class. + + +Attribute Labels +---------------- + +Attribute labels are mainly used for display purpose. For example, given an attribute `firstName`, we can declare +a label `First Name` which is more user-friendly and can be displayed to end users in places such as form labels, +error messages. Given an attribute name, you can obtain its label by calling [[\yii\base\Model::getAttributeLabel()]]. + +To declare attribute labels, you should override the [[\yii\base\Model::attributeLabels()]] method and return +a mapping from attribute names to attribute labels, like shown in the example below. If an attribute is not found +in this mapping, its label will be generated using the [[\yii\base\Model::generateAttributeLabel()]] method, which +in many cases, will generate reasonable labels (e.g. `username` to `Username`, `orderNumber` to `Order Number`). + +```php +// LoginForm has two attributes: username and password +class LoginForm extends \yii\base\Model +{ + public $username; + public $password; + + public function attributeLabels() + { + return array( + 'username' => 'Your name', + 'password' => 'Your password', + ); + } +} +``` + +Scenarios +--------- + +A model may be used in different scenarios. For example, a `User` model may be used to collect user login inputs, +and it may also be used for user registration purpose. For this reason, each model has a property named `scenario` +which stores the name of the scenario that the model is currently being used in. As we will explain in the next +few sections, the concept of scenario is mainly used for data validation and massive attribute assignment. + +Associated with each scenario is a list of attributes that are *active* in that particular scenario. For example, +in the `login` scenario, only the `username` and `password` attributes are active; while in the `register` scenario, +additional attributes such as `email` are *active*. + +Possible scenarios should be listed in the `scenarios()` method which returns an array whose keys are the scenario +names and whose values are the corresponding active attribute lists. Below is an example: + +```php +class User extends \yii\db\ActiveRecord +{ + public function scenarios() + { + return array( + 'login' => array('username', 'password'), + 'register' => array('username', 'email', 'password'), + ); + } +} +``` + +Sometimes, we want to mark an attribute as not safe for massive assignment (but we still want it to be validated). +We may do so by prefixing an exclamation character to the attribute name when declaring it in `scenarios()`. For example, + +```php +array('username', 'password', '!secret') +``` + +Active model scenario could be set using one of the following ways: + +```php +class EmployeeController extends \yii\web\Controller +{ + public function actionCreate($id = null) + { + // first way + $employee = new Employee(array('scenario' => 'managementPanel')); + + // second way + $employee = new Employee; + $employee->scenario = 'managementPanel'; + + // third way + $employee = Employee::find()->where('id = :id', array(':id' => $id))->one(); + if ($employee !== null) { + $employee->setScenario('managementPanel'); + } + } +} +``` + +In the example above we are using [Active Record](active-record.md). For basic form models it's rarely needed to +use scenarios since form model is typically used for a single form. + +Validation +---------- + +When a model is used to collect user input data via its attributes, it usually needs to validate the affected attributes +to make sure they satisfy certain requirements, such as an attribute cannot be empty, an attribute must contain letters +only, etc. If errors are found in validation, they may be presented to the user to help him fix the errors. +The following example shows how the validation is performed: + +```php +$model = new LoginForm; +$model->username = $_POST['username']; +$model->password = $_POST['password']; +if ($model->validate()) { + // ... login the user ... +} else { + $errors = $model->getErrors(); + // ... display the errors to the end user ... +} +``` + +The possible validation rules for a model should be listed in its `rules()` method. Each validation rule applies to one +or several attributes and is effective in one or several scenarios. A rule can be specified using a validator object - an +instance of a [[\yii\validators\Validator]] child class, or an array with the following format: + +```php +array( + 'attribute1, attribute2, ...', + 'validator class or alias', + // specifies in which scenario(s) this rule is active. + // if not given, it means it is active in all scenarios + 'on' => 'scenario1, scenario2, ...', + // the following name-value pairs will be used + // to initialize the validator properties + 'property1' => 'value1', + 'property2' => 'value2', + // ... +) +``` + +When `validate()` is called, the actual validation rules executed are determined using both of the following criteria: + +- the rule must be associated with at least one active attribute; +- the rule must be active for the current scenario. + + +### Active Attributes + +An attribute is *active* if it is subject to some validations in the current scenario. + + +### Safe Attributes + +An attribute is *safe* if it can be massively assigned in the current scenario. + + +Massive Attribute Retrieval and Assignment +------------------------------------------ + +Attributes can be massively retrieved via the `attributes` property. +The following code will return *all* attributes in the `$post` model +as an array of name-value pairs. + +```php +$attributes = $post->attributes; +var_dump($attributes); +``` + +Using the same `attributes` property you can massively assign data from associative array to model attributes: + +```php +$attributes = array( + 'title' => 'Model attributes', + 'create_time' => time(), +); +$post->attributes = $attributes; +``` + +In the code above we're assigning corresponding data to model attributes named as array keys. The key difference from mass +retrieval that always works for all attributes is that in order to be assigned an attribute should be **safe** else +it will be ignored. + + +Validation rules and mass assignment +------------------------------------ + +In Yii2 unlike Yii 1.x validation rules are separated from mass assignment. Validation +rules are described in `rules()` method of the model while what's safe for mass +assignment is described in `scenarios` method: + +```php +function rules() +{ + return array( + // rule applied when corresponding field is "safe" + array('username', 'string', 'length' => array(4, 32)), + array('first_name', 'string', 'max' => 128), + array('password', 'required'), + + // rule applied when scenario is "signup" no matter if field is "safe" or not + array('hashcode', 'check', 'on' => 'signup'), + ); +} + +function scenarios() +{ + return array( + // on signup allow mass assignment of username + 'signup' => array('username', 'password'), + 'update' => array('username', 'first_name'), + ); +} +``` + +Note that everything is unsafe by default and you can't make field "safe" without specifying scenario. + + +See also +-------- + +- [Model validation reference](validation.md) +- [[\yii\base\Model]] diff --git a/docs/guide/mvc.md b/docs/guide/mvc.md index a99d043..f67ba4d 100644 --- a/docs/guide/mvc.md +++ b/docs/guide/mvc.md @@ -10,7 +10,7 @@ of the user interface such as text, form inputs; and the controller manages the communication between the model and the view. Besides implementing MVC, Yii also introduces a front-controller, called -`Application`, which encapsulates the execution context for the processing +`application`, which encapsulates the execution context for the processing of a request. Application collects information about a user request and then dispatches it to an appropriate controller for further handling. diff --git a/docs/guide/overview.md b/docs/guide/overview.md index 9e54fd4..835c511 100644 --- a/docs/guide/overview.md +++ b/docs/guide/overview.md @@ -1,18 +1,17 @@ What is Yii =========== -Yii is a high-performance, component-based PHP framework for developing -large-scale Web applications rapidly. It enables maximum reusability in Web +Yii is a high-performance, component-based PHP framework for rapidly developing large-scale Web applications. Yii enables maximum reusability in Web programming and can significantly accelerate your Web application development process. The name Yii (pronounced `Yee` or `[ji:]`) is an acronym for -"**Yes It Is!**". +**Yes It Is!**. Requirements ------------ To run a Yii-powered Web application, you need a Web server that supports -PHP 5.3.?. +PHP 5.3.? or greater. For developers who want to use Yii, understanding object-oriented programming (OOP) is very helpful, because Yii is a pure OOP framework. @@ -25,12 +24,15 @@ Yii is a generic Web programming framework that can be used for developing virtually any type of Web application. Because it is light-weight and equipped with sophisticated caching mechanisms, it is especially suited to high-traffic applications, such as portals, forums, content -management systems (CMS), e-commerce systems, etc. +management systems (CMS), e-commerce projects, etc. How does Yii Compare with Other Frameworks? ------------------------------------------- -Like most PHP frameworks, Yii is an MVC (Model-View-Controller) framework. - -TBD \ No newline at end of file +- Like most PHP frameworks, Yii is uses the MVC (Model-View-Controller) design approach. +- Yii is a fullstack framework providing many solutions and components, such as logging, session management, caching etc. +- Yii strikes a good balance between simplicity and features. +- Syntax and overall development usability are taken seriously by the Yii development team. +- Performance is one of the key goals for the Yii framework. +- The Yii development team is constantly watching what other Web frameworks are doing to see what best practices and features should be incorporated into Yii. The initial Yii release was heavily influenced by Ruby on Rails. Still, no framework or feature is being blindly copied into Yii; all decisions are based upon what's best for Web developers and in keeping with Yii's philosophy. diff --git a/docs/guide/performance.md b/docs/guide/performance.md index 9a871dc..3d89af9 100644 --- a/docs/guide/performance.md +++ b/docs/guide/performance.md @@ -97,9 +97,11 @@ return array( ``` You can use `CacheSession` to store sessions using cache. Note that some -cache storages such as memcached has no guaranteee that session data will not +cache storage such as memcached has no guarantee that session data will not be lost leading to unexpected logouts. +If you have [Redis](http://redis.io/) on your server, it's highly recommended as session storage. + Improving application --------------------- @@ -114,7 +116,7 @@ If a whole page remains relative static, we can use the page caching approach to save the rendering cost for the whole page. -### Leveraging HTTP to save procesing time and bandwidth +### Leveraging HTTP to save processing time and bandwidth TBD @@ -132,7 +134,7 @@ but it may slow down INSERT, UPDATE or DELETE queries. For complex queries, it is recommended to create a database view for it instead of issuing the queries inside the PHP code and asking DBMS to parse them repetitively. -Do not overuse Active Record. Although Active Record is good at modelling data +Do not overuse Active Record. Although Active Record is good at modeling data in an OOP fashion, it actually degrades performance due to the fact that it needs to create one or several objects to represent each row of query result. For data intensive applications, using DAO or database APIs at lower level could be @@ -152,17 +154,17 @@ class PostController extends Controller public function actionIndex() { $posts = Post::find()->orderBy('id DESC')->limit(100)->asArray()->all(); - echo $this->render('index', array( + return $this->render('index', array( 'posts' => $posts, )); } } ``` -In the view you should access fields of each invidual record from `$posts` as array: +In the view you should access fields of each individual record from `$posts` as array: ```php -foreach($posts as $post) { +foreach ($posts as $post) { echo $post['title']."<br>"; } ``` @@ -178,4 +180,4 @@ request later if there's no need for immediate response. - Cron jobs + console. - queues + handlers. -TBD \ No newline at end of file +TBD diff --git a/docs/guide/query-builder.md b/docs/guide/query-builder.md index e69de29..2ec2581 100644 --- a/docs/guide/query-builder.md +++ b/docs/guide/query-builder.md @@ -0,0 +1,235 @@ +Query Builder and Query +======================= + +Yii provides a basic database access layer as was described in [Database basics](database-basics.md) section. Still it's +a bit too much to use SQL directly all the time. To solve the issue Yii provides a query builder that allows you to +work with the database in object-oriented style. + +Basic query builder usage is the following: + +```php +$query = new Query; + +// Define query +$query->select('id, name') + ->from('tbl_user') + ->limit(10); + +// Create a command. You can get the actual SQL using $command->sql +$command = $query->createCommand(); +// Execute command +$rows = $command->queryAll(); +``` + +Basic selects and joins +----------------------- + +In order to form a `SELECT` query you need to specify what to select and where to select it from. + +```php +$query->select('id, name') + ->from('tbl_user'); +``` + +If you want to get IDs of all users with posts you can use `DISTINCT`. With query builder it will look like the following: + +```php +$query->select('user_id')->distinct()->from('tbl_post'); +``` + +Select options can be specified as array. It's especially useful when these are formed dynamically. + +```php +$query->select(array('tbl_user.name AS author', 'tbl_post.title as title')) // <-- specified as array + ->from('tbl_user') + ->leftJoin('tbl_post', 'tbl_post.user_id = tbl_user.id'); // <-- join with another table +``` + +In the code above we've used `leftJoin` method to select from two related tables at the same time. Firsrt parameter +specifies table name and the second is the join condition. Query builder has the following methods to join tables: + +- `innerJoin` +- `leftJoin` +- `rightJoin` + +If your data storage supports more types you can use generic `join` method: + +```php +$query->join('FULL OUTER JOIN', 'tbl_post', 'tbl_post.user_id = tbl_user.id'); +``` + +Specifying conditions +--------------------- + +Usually you need data that matches some conditions. There are some useful methods to specify these and the most powerful +is `where`. There are multiple ways to use it. + +The simplest is to specify condition in a string: + +```php +$query->where('status=:status', array( + ':status' => $status, +)); +``` + +When using this format make sure you're binding parameters and not creating a query by string concatenation. + +Instead of binding status value immediately you can do it using `params` or `addParams`: + +```php +$query->where('status=:status'); + +$query->addParams(array( + ':status' => $status, +)); +``` + +There is another convenient way to use the method called hash format: + +```php +$query->where(array( + 'status' => 10, + 'type' => 2, + 'id' => array(4, 8, 15, 16, 23, 42), +)); +``` + +It will generate the following SQL: + +```sql +WHERE (`status` = 10) AND (`type` = 2) AND (`id` IN (4, 8, 15, 16, 23, 42)) +``` + +If you'll specify value as `null` such as the following: + +```php +$query->where(array( + 'status' => null, +)); +``` + +SQL generated will be: + +```sql +WHERE (`status` IS NULL) +``` + +Another way to use the method is the operand format which is `array(operator, operand1, operand2, ...)`. + +Operator can be one of the following: + +- `and`: the operands should be concatenated together using `AND`. For example, + `array('and', 'id=1', 'id=2')` will generate `id=1 AND id=2`. If an operand is an array, + it will be converted into a string using the rules described here. For example, + `array('and', 'type=1', array('or', 'id=1', 'id=2'))` will generate `type=1 AND (id=1 OR id=2)`. + The method will NOT do any quoting or escaping. +- `or`: similar to the `and` operator except that the operands are concatenated using `OR`. +- `between`: operand 1 should be the column name, and operand 2 and 3 should be the + starting and ending values of the range that the column is in. + For example, `array('between', 'id', 1, 10)` will generate `id BETWEEN 1 AND 10`. +- `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` + in the generated condition. +- `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing + the range of the values that the column or DB expression should be in. For example, + `array('in', 'id', array(1, 2, 3))` will generate `id IN (1, 2, 3)`. + The method will properly quote the column name and escape values in the range. +- `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. +- `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing + the values that the column or DB expression should be like. + For example, `array('like', 'name', '%tester%')` will generate `name LIKE '%tester%'`. + When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated + using `AND`. For example, `array('like', 'name', array('%test%', '%sample%'))` will generate + `name LIKE '%test%' AND name LIKE '%sample%'`. + The method will properly quote the column name and escape values in the range. +- `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` + predicates when operand 2 is an array. +- `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` + in the generated condition. +- `or not like`: similar to the `not like` operator except that `OR` is used to concatenate + the `NOT LIKE` predicates. + +If you are building parts of condition dynamically it's very convenient to use `andWhere` and `orWhere`: + +```php +$status = 10; +$search = 'yii'; + +$query->where(array('status' => $status)); +if (!empty($search)) { + $query->addWhere('like', 'title', $search); +} +``` + +In case `$search` isn't empty the following SQL will be generated: + +```sql +WHERE (`status` = 10) AND (`title` LIKE '%yii%') +``` + +Order +----- + +For ordering results `orderBy` and `addOrderBy` could be used: + +```php +$query->orderBy(array( + 'id' => Query::SORT_ASC, + 'name' => Query::SORT_DESC, +)); +``` + +Here we are ordering by `id` ascending and then by `name` descending. + +Group and Having +---------------- + +In order to add `GROUP BY` to generated SQL you can use the following: + +```php +$query->groupBy('id, status'); +``` + +If you want to add another field after using `groupBy`: + +```php +$query->addGroupBy(array('created_at', 'updated_at')); +``` + +To add a `HAVING` condition the corresponding `having` method and its `andHaving` and `orHaving` can be used. Parameters +for these are similar to the ones for `where` methods group: + +```php +$query->having(array('status' => $status)); +``` + +Limit and offset +---------------- + +To limit result to 10 rows `limit` can be used: + +```php +$query->limit(10); +``` + +To skip 100 fist rows use: + +```php +$query->offset(100); +``` + +Union +----- + +`UNION` in SQL adds results of one query to results of another query. Columns returned by both queries should match. +In Yii in order to build it you can first form two query objects and then use `union` method: + +```php +$query = new Query; +$query->select("id, 'post' as type, name")->from('tbl_post')->limit(10); + +$anotherQuery = new Query; +$query->select('id, 'user' as type, name')->from('tbl_user')->limit(10); + +$query->union($anotherQuery); +``` + diff --git a/docs/guide/security.md b/docs/guide/security.md index e69de29..a7f4054 100644 --- a/docs/guide/security.md +++ b/docs/guide/security.md @@ -0,0 +1,83 @@ +Security +======== + +Hashing and verifying passwords +------------------------------ + +Most developers know that you cannot store passwords in plain text, but many believe it's safe to hash passwords using `md5` or `sha1`. There was a time when those hashing algorithms were sufficient, but modern hardware makes it possible to break those hashes very quickly using a brute force attack. + +In order to truly secure user passwords, even in the worst case scenario (your database is broken into), you need to use a hashing algorithm that is resistant to brute force attacks. The best current choice is `bcrypt`. In PHP, you can create a `bcrypt` hash by using the [crypt function](http://php.net/manual/en/function.crypt.php). However, this function is not easy to use properly, so Yii provides two helper functions to make securely generating and verifying hashes easier. + +When a user provides a password for the first time (e.g., upon registration), the password needs to be hashed: + +```php +$hash = \yii\helpers\Security::generatePasswordHash($password); +``` + +The hash would then be associated with the model, so that it will be stored in the database for later use. + +When user attempts to log in, the submitted log in password must be verified against the previously hashed and stored password: + +```php +use \yii\helpers; +if (Security::validatePassword($password, $hash)) { + // all good, logging user in +} else { + // wrong password +} +``` + + +Random data +----------- + +Random data is useful in many cases. For example, when resetting a password via email you need to generate a token, +save it to database and send it via email to end user so he's able to prove that email belongs to him. It is very +important for this token to be truly unique else there will be a possibility to predict a value and reset another user's +password. + +Yii security helper makes it as simple as: + +```php +$key = \yii\helpers\Security::generateRandomKey(); +``` + +Encryption and decryption +------------------------- + +In order to encrypt data so only person knowing a secret passphrase or having a secret key will be able to decrypt it. +For example, we need to store some information in our database but we need to make sure only user knowing a secret code +can view it (even if database is leaked): + + +```php +// $data and $secretWord are from the form +$encryptedData = \yii\helpers\Security::encrypt($data, $secretWord); +// store $encryptedData to database +``` + +Then when user want to read it: + +```php +// $secretWord is from the form, $encryptedData is from database +$data = \yii\helpers\Security::decrypt($encryptedData, $secretWord); +``` + +Making sure data wasn't modified +-------------------------------- + +hashData() +validateData() + + +Securing Cookies +---------------- + +- validation +- httpOnly + +See also +-------- + +- [Views security](view.md#security) + diff --git a/docs/guide/template.md b/docs/guide/template.md index dc83d15..f9405ff 100644 --- a/docs/guide/template.md +++ b/docs/guide/template.md @@ -1,3 +1,85 @@ -Template -======== +Using template engines +====================== + +By default Yii uses PHP as template language, but you can configure it to support other rendering engines, such as [Twig](http://twig.sensiolabs.org/) or [Smarty](http://www.smarty.net/). + +The `view` component is responsible for rendering views. You can add +a custom template engines by reconfiguring this component's behavior: + +```php +array( + 'components' => array( + 'view' => array( + 'class' => 'yii\base\View', + 'renderers' => array( + 'tpl' => array( + 'class' => 'yii\renderers\SmartyViewRenderer', + ), + 'twig' => array( + 'class' => 'yii\renderers\TwigViewRenderer', + 'twigPath' => '@app/vendors/Twig', + ), + // ... + ), + ), + ), +) +``` + +Note that the Smarty and Twig packages themselves are not bundled with Yii. You must download them yourself. Then unpack the packages and place the resulting files in a logical location, such as the application's `protected/vendor` folder. Finally, specify the correct `smartyPath` or `twigPath`, as in the code above (for Twig). + +Twig +---- + +To use Twig, you need to create templates in files with the `.twig` extension (or use another file extension but configure the component accordingly). +Unlike standard view files, when using Twig, you must include the extension when calling `$this->render()` +or `$this->renderPartial()` from your controller: + +```php +echo $this->render('renderer.twig', array('username' => 'Alex')); +``` + +### Additional functions + +Yii adds the following construct to the standard Twig syntax: + +```php +<a href="{{ path('blog/view', {'alias' : post.alias}) }}">{{ post.title }}</a> +``` + +Internally, the `path()` function calls Yii's `Html::url()` method. + +### Additional variables + +Within Twig templates, you can also make use of these variables: + +- `app`, which equates to `\Yii::$app` +- `this`, which equates to the current `View` object + +Smarty +------ + +To use Smarty, you need to create templates in files with the `.tpl` extension (or use another file extension but configure the component accordingly). Unlike standard view files, when using Smarty, you must include the extension when calling `$this->render()` +or `$this->renderPartial()` from your controller: + +```php +echo $this->render('renderer.tpl', array('username' => 'Alex')); +``` + +### Additional functions + +Yii adds the following construct to the standard Smarty syntax: + +```php +<a href="{path route='blog/view' alias=$post.alias}">{$post.title}</a> +``` + +Internally, the `path()` function calls Yii's `Html::url()` method. + +### Additional variables + +Within Smarty templates, you can also make use of these variables: + +- `$app`, which equates to `\Yii::$app` +- `$this`, which equates to the current `View` object diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md index d35e6e0..f174864 100644 --- a/docs/guide/upgrade-from-v1.md +++ b/docs/guide/upgrade-from-v1.md @@ -13,7 +13,7 @@ The most obvious change in Yii 2.0 is the use of namespaces. Almost every core c is namespaced, e.g., `yii\web\Request`. The "C" prefix is no longer used in class names. The naming of the namespaces follows the directory structure. For example, `yii\web\Request` indicates the corresponding class file is `web/Request.php` under the Yii framework folder. -You can use any core class without explicitly include that class file, thanks to the Yii +You can use any core class without explicitly including that class file, thanks to the Yii class loader. @@ -117,17 +117,17 @@ supported in most places in the Yii core code. For example, `FileCache::cachePat both a path alias and a normal directory path. Path alias is also closely related with class namespaces. It is recommended that a path -alias defined for each root namespace so that you can use Yii class autoloader without +alias be defined for each root namespace so that you can use Yii the class autoloader without any further configuration. For example, because `@yii` refers to the Yii installation directory, a class like `yii\web\Request` can be autoloaded by Yii. If you use a third party library -such as Zend Framework, you may define a path alias `@Zend` which refers to its installation directory. -And Yii will be able to autoload any class in this library. +such as Zend Framework, you may define a path alias `@Zend` which refers to its installation +directory and Yii will be able to autoload any class in this library. View ---- -Yii 2.0 introduces a `View` class to represent the view part in the MVC pattern. +Yii 2.0 introduces a `View` class to represent the view part of the MVC pattern. It can be configured globally through the "view" application component. It is also accessible in any view file via `$this`. This is one of the biggest changes compared to 1.1: **`$this` in a view file no longer refers to the controller or widget object.** @@ -149,20 +149,41 @@ $content = Yii::$app->view->renderFile($viewFile, $params); Also, there is no more `CClientScript` in Yii 2.0. The `View` class has taken over its role with significant improvements. For more details, please see the "assets" subsection. -While Yii 2.0 continues to use PHP as its main template language, it comes with built-in -support for two popular template engines: Smarty and Twig. The Prado template engine is +While Yii 2.0 continues to use PHP as its main template language, it comes with two official extensions +adding support for two popular template engines: Smarty and Twig. The Prado template engine is no longer supported. To use these template engines, you just need to use `tpl` as the file -extension for your Smarty views, or `twig` for Twig views. You may also configure the -`View::renderers` property to use other template engines. +extension for your Smarty views, or `twig` for Twig views. You may also configure the +`View::renderers` property to use other template engines. See [Using template engines](template.md) section +of the guide for more details. Models ------ -A model is now associated with a form name returned its `formName()` method. This is +A model is now associated with a form name returned by its `formName()` method. This is mainly used when using HTML forms to collect user inputs for a model. Previously in 1.1, this is usually hardcoded as the class name of the model. +A new methods called `load()` and `Model::loadMultiple()` is introduced to simplify the data population from user inputs +to a model. For example, + +```php +$model = new Post; +if ($model->load($_POST)) {...} +// which is equivalent to: +if (isset($_POST['Post'])) { + $model->attributes = $_POST['Post']; +} + +$model->save(); + +$postTags = array(); +$tagsCount = count($_POST['PostTag']); +while($tagsCount-- > 0){ + $postTags[] = new PostTag(array('post_id' => $model->id)); +} +Model::loadMultiple($postTags, $_POST); +``` Yii 2.0 introduces a new method called `scenarios()` to declare which attributes require validation under which scenario. Child classes should overwrite `scenarios()` to return @@ -189,6 +210,8 @@ Because of the above change, Yii 2.0 no longer has "safe" and "unsafe" validator If your model only has one scenario (very common), you do not have to overwrite `scenarios()`, and everything will still work like the 1.1 way. +To learn more about Yii 2.0 models refer to [Models](model.md) section of the guide. + Controllers ----------- @@ -196,27 +219,38 @@ Controllers The `render()` and `renderPartial()` methods now return the rendering results instead of directly sending them out. You have to `echo` them explicitly, e.g., `echo $this->render(...);`. -A new method called `populate()` is introduced to simplify the data population from user inputs -to a model. For example, +To learn more about Yii 2.0 controllers refer to [Controller](controller.md) section of the guide. + +Widgets +------- + +Using a widget is more straightforward in 2.0. You mainly use the `begin()`, `end()` and `widget()` +methods of the `Widget` class. For example, ```php -$model = new Post; -if ($this->populate($_POST, $model)) {...} -// which is equivalent to: -if (isset($_POST['Post'])) { - $model->attributes = $_POST['Post']; -} +// Note that you have to "echo" the result to display it +echo \yii\widgets\Menu::widget(array('items' => $items)); + +// Passing an array to initialize the object properties +$form = \yii\widgets\ActiveForm::begin(array( + 'options' => array('class' => 'form-horizontal'), + 'fieldConfig' => array('inputOptions' => array('class' => 'input-xlarge')), +)); +... form inputs here ... +\yii\widgets\ActiveForm::end(); ``` +Previously in 1.1, you would have to enter the widget class names as strings via the `beginWidget()`, +`endWidget()` and `widget()` methods of `CBaseController`. The approach above gets better IDE support. Themes ------ -Theme works completely different in 2.0. It is now based on a path map to "translate" a source +Themes work completely different in 2.0. They are now based on a path map to "translate" a source view into a themed view. For example, if the path map for a theme is -`array('/www/views' => '/www/themes/basic')`, then the themed version for a view file -`/www/views/site/index.php` will be `/www/themes/basic/site/index.php`. +`array('/web/views' => '/web/themes/basic')`, then the themed version for a view file +`/web/views/site/index.php` will be `/web/themes/basic/site/index.php`. For this reason, theme can now be applied to any view file, even if a view rendered outside of the context of a controller or a widget. @@ -228,11 +262,11 @@ application component. Console Applications -------------------- -Console applications are now composed by controllers, too, like Web applications. In fact, +Console applications are now composed by controllers, like Web applications. In fact, console controllers and Web controllers share the same base controller class. Each console controller is like `CConsoleCommand` in 1.1. It consists of one or several -actions. You use the `yiic <route>` command to execute a console command, where `<route>` +actions. You use the `yii <route>` command to execute a console command, where `<route>` stands for a controller route (e.g. `sitemap/index`). Additional anonymous arguments are passed as the parameters to the corresponding controller action method, and named arguments are treated as global options declared in `globalOptions()`. @@ -249,10 +283,6 @@ Message translation is still supported, but managed via the "i18n" application c The component manages a set of message sources, which allows you to use different message sources based on message categories. For more information, see the class documentation for `I18N`. -The message translation method is changed by merging the message category into the message being -translated. For example, `Yii::t('yii|message to be translated')`. - - Action Filters -------------- @@ -270,7 +300,6 @@ public function behaviors() 'class' => 'yii\web\AccessControl', 'rules' => array( array('allow' => true, 'actions' => array('admin'), 'roles' => array('@')), - array('allow' => false), ), ), ); @@ -282,13 +311,14 @@ public function behaviors() Assets ------ -Yii 2.0 introduces a new concept called *asset bundle*. It is a bit similar to script +Yii 2.0 introduces a new concept called *asset bundle*. It is similar to script packages (managed by `CClientScript`) in 1.1, but with better support. An asset bundle is a collection of asset files (e.g. JavaScript files, CSS files, image files, etc.) -under a directory. By registering an asset bundle via `View::registerAssetBundle()`, you -will be able to make the assets in that bundle accessible via Web, and the current page -will automatically contain references to the JavaScript and CSS files in that bundle. +under a directory. Each asset bundle is represented as a class extending `AssetBundle`. +By registering an asset bundle via `AssetBundle::register()`, you will be able to make +the assets in that bundle accessible via Web, and the current page will automatically +contain the references to the JavaScript and CSS files specified in that bundle. @@ -297,7 +327,7 @@ Static Helpers Yii 2.0 introduces many commonly used static helper classes, such as `Html`, `ArrayHelper`, `StringHelper`. These classes are designed to be easily extended. Note that static classes -are usually hard to be extended because of the fixed class name references. But Yii 2.0 +are usually hard to extend because of the fixed class name references. But Yii 2.0 introduces the class map (via `Yii::$classMap`) to overcome this difficulty. @@ -305,17 +335,17 @@ introduces the class map (via `Yii::$classMap`) to overcome this difficulty. ------------ Yii 2.0 introduces the *field* concept for building a form using `ActiveForm`. A field -is a container consisting of a label, an input, and an error message. It is represented -as an `ActiveField` object. Using fields, you can build a form more cleanly than before: +is a container consisting of a label, an input, an error message, and/or a hint text. +It is represented as an `ActiveField` object. Using fields, you can build a form more cleanly than before: ```php -<?php $form = $this->beginWidget('yii\widgets\ActiveForm'); ?> - <?php echo $form->field($model, 'username')->textInput(); ?> +<?php $form = yii\widgets\ActiveForm::begin(); ?> + <?php echo $form->field($model, 'username'); ?> <?php echo $form->field($model, 'password')->passwordInput(); ?> - <div class="form-actions"> + <div class="form-group"> <?php echo Html::submitButton('Login'); ?> </div> -<?php $this->endWidget(); ?> +<?php yii\widgets\ActiveForm::end(); ?> ``` @@ -325,7 +355,7 @@ Query Builder In 1.1, query building is scattered among several classes, including `CDbCommand`, `CDbCriteria`, and `CDbCommandBuilder`. Yii 2.0 uses `Query` to represent a DB query -and `QueryBuilder` to generate SQL statements from query objects. For example, +and `QueryBuilder` to generate SQL statements from query objects. For example: ```php $query = new \yii\db\Query; @@ -347,7 +377,7 @@ ActiveRecord ------------ ActiveRecord has undergone significant changes in Yii 2.0. The most important one -is about relational ActiveRecord query. In 1.1, you have to declare the relations +is the relational ActiveRecord query. In 1.1, you have to declare the relations in the `relations()` method. In 2.0, this is done via getter methods that return an `ActiveQuery` object. For example, the following method declares an "orders" relation: @@ -374,7 +404,7 @@ by filtering with the primary keys of the primary records. Yii 2.0 no longer uses the `model()` method when performing queries. Instead, you -use the `find()` method like the following: +use the `find()` method: ```php // to retrieve all *active* customers and order them by their ID: @@ -392,14 +422,14 @@ Therefore, you can use all query methods of `Query`. Instead of returning ActiveRecord objects, you may call `ActiveQuery::asArray()` to return results in terms of arrays. This is more efficient and is especially useful -when you need to return large number of records. For example, +when you need to return a large number of records: ```php $customers = Customer::find()->asArray()->all(); ``` By default, ActiveRecord now only saves dirty attributes. In 1.1, all attributes -would be saved to database when you call `save()`, regardless they are changed or not, +are saved to database when you call `save()`, regardless of having changed or not, unless you explicitly list the attributes to save. @@ -409,7 +439,7 @@ Auto-quoting Table and Column Names Yii 2.0 supports automatic quoting of database table and column names. A name enclosed within double curly brackets is treated as a table name, and a name enclosed within double square brackets is treated as a column name. They will be quoted according to -the database driver being used. For example, +the database driver being used: ```php $command = $connection->createCommand('SELECT [[id]] FROM {{posts}}'); @@ -420,11 +450,11 @@ This feature is especially useful if you are developing an application that supp different DBMS. -User and Identity ------------------ +User and IdentityInterface +-------------------------- The `CWebUser` class in 1.1 is now replaced by `\yii\Web\User`, and there is no more -`CUserIdentity` class. Instead, you should implement the `Identity` interface which +`CUserIdentity` class. Instead, you should implement the `IdentityInterface` which is much more straightforward to implement. The bootstrap application provides such an example. diff --git a/docs/guide/validation.md b/docs/guide/validation.md index e69de29..c555913 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -0,0 +1,182 @@ +Model validation reference +========================== + +This guide section doesn't describe how validation works but instead describes all Yii validators and their parameters. +In order to learn model validation basics please refer to [Model, Validation subsection](model.md#Validation). + +Standard Yii validators +----------------------- + +Standard Yii validators could be specified using aliases instead of referring to class names. Here's the list of all +validators budled with Yii with their most useful properties: + +### `boolean`: [[BooleanValidator]] + +Checks if the attribute value is a boolean value. + +- `trueValue`, the value representing true status. _(1)_ +- `falseValue`, the value representing false status. _(0)_ +- `strict`, whether to compare the type of the value and `trueValue`/`falseValue`. _(false)_ + +### `captcha`: [[CaptchaValidator]] + +Validates that the attribute value is the same as the verification code displayed in the CAPTCHA. Should be used together +with [[CaptchaAction]]. + +- `caseSensitive` whether the comparison is case sensitive. _(false)_ +- `captchaAction` the route of the controller action that renders the CAPTCHA image. _('site/captcha')_ + +### `compare`: [[CompareValidator]] + +Compares the specified attribute value with another value and validates if they are equal. + +- `compareAttribute` the name of the attribute to be compared with. _(currentAttribute_repeat)_ +- `compareValue` the constant value to be compared with. +- `operator` the operator for comparison. _('==')_ + +### `date`: [[DateValidator]] + +Verifies if the attribute represents a date, time or datetime in a proper format. + +- `format` the date format that the value being validated should follow according to [[http://www.php.net/manual/en/datetime.createfromformat.php]]. _('Y-m-d')_ +- `timestampAttribute` the name of the attribute to receive the parsing result. + +### `default`: [[DefaultValueValidator]] + +Sets the attribute to be the specified default value. + +- `value` the default value to be set to the specified attributes. + +### `double`: [[NumberValidator]] + +Validates that the attribute value is a number. + +- `max` limit of the number. _(null)_ +- `min` lower limit of the number. _(null)_ + +### `email`: [[EmailValidator]] + +Validates that the attribute value is a valid email address. + +- `allowName` whether to allow name in the email address (e.g. `John Smith <john.smith@example.com>`). _(false)_. +- `checkMX` whether to check the MX record for the email address. _(false)_ +- `checkPort` whether to check port 25 for the email address. _(false)_ +- `enableIDN` whether validation process should take into account IDN (internationalized domain names). _(false)_ + +### `exist`: [[ExistValidator]] + +Validates that the attribute value exists in a table. + +- `className` the ActiveRecord class name or alias of the class that should be used to look for the attribute value being + validated. _(ActiveRecord class of the attribute being validated)_ +- `attributeName` the ActiveRecord attribute name that should be used to look for the attribute value being validated. + _(name of the attribute being validated)_ + +### `file`: [[FileValidator]] + +Verifies if an attribute is receiving a valid uploaded file. + +- `types` a list of file name extensions that are allowed to be uploaded. _(any)_ +- `minSize` the minimum number of bytes required for the uploaded file. +- `maxSize` the maximum number of bytes required for the uploaded file. +- `maxFiles` the maximum file count the given attribute can hold. _(1)_ + +### `filter`: [[FilterValidator]] + +Converts the attribute value according to a filter. + +- `filter` PHP callback that defines a filter. + +Typically a callback is either the name of PHP function: + +```php +array('password', 'filter', 'filter' => 'trim'), +``` + +Or an anonymous function: + +```php +array('text', 'filter', 'filter' => function ($value) { + // here we are removing all swear words from text + return $newValue; +}), +``` + +### `in`: [[RangeValidator]] + +Validates that the attribute value is among a list of values. + +- `range` list of valid values that the attribute value should be among. +- `strict` whether the comparison is strict (both type and value must be the same). _(false)_ +- `not` whether to invert the validation logic. _(false)_ + +### `integer`: [[NumberValidator]] + +Validates that the attribute value is an integer number. + +- `max` limit of the number. _(null)_ +- `min` lower limit of the number. _(null)_ + +### `match`: [[RegularExpressionValidator]] + +Validates that the attribute value matches the specified pattern defined by regular expression. + +- `pattern` the regular expression to be matched with. +- `not` whether to invert the validation logic. _(false)_ + +### `required`: [[RequiredValidator]] + +Validates that the specified attribute does not have null or empty value. + +- `requiredValue` the desired value that the attribute must have. _(any)_ +- `strict` whether the comparison between the attribute value and [[requiredValue]] is strict. _(false)_ + +### `safe`: [[SafeValidator]] + +Serves as a dummy validator whose main purpose is to mark the attributes to be safe for massive assignment. + +### `string`: [[StringValidator]] + +Validates that the attribute value is of certain length. + +- `length` specifies the length limit of the value to be validated. Can be `exactly X`, `array(min X)`, `array(min X, max Y)`. +- `max` maximum length. If not set, it means no maximum length limit. +- `min` minimum length. If not set, it means no minimum length limit. +- `encoding` the encoding of the string value to be validated. _([[\yii\base\Application::charset]])_ + +### `unique`: [[UniqueValidator]] + +Validates that the attribute value is unique in the corresponding database table. + +- `className` the ActiveRecord class name or alias of the class that should be used to look for the attribute value being + validated. _(ActiveRecord class of the attribute being validated)_ +- `attributeName` the ActiveRecord attribute name that should be used to look for the attribute value being validated. + _(name of the attribute being validated)_ + +### `url`: [[UrlValidator]] + +Validates that the attribute value is a valid http or https URL. + +- `validSchemes` list of URI schemes which should be considered valid. _array('http', 'https')_ +- `defaultScheme` the default URI scheme. If the input doesn't contain the scheme part, the default scheme will be + prepended to it. _(null)_ +- `enableIDN` whether validation process should take into account IDN (internationalized domain names). _(false)_ + +Validating values out of model context +-------------------------------------- + +Sometimes you need to validate a value that is not bound to any model such as email. In Yii `Validator` class has +`validateValue` method that can help you with it. Not all validator classes have it implemented but the ones that can +operate without model do. In our case to validate an email we can do the following: + +```php +$email = 'test@example.com'; +$validator = new yii\validators\EmailValidator(); +if ($validator->validateValue($email)) { + echo 'Email is valid.'; +} else { + echo 'Email is not valid.' +} +``` + +TBD: refer to http://www.yiiframework.com/wiki/56/ for the format diff --git a/docs/guide/view.md b/docs/guide/view.md index e69de29..6ca2368 100644 --- a/docs/guide/view.md +++ b/docs/guide/view.md @@ -0,0 +1,359 @@ +View +==== + +View is an important part of MVC and is responsible for presenting data to end users. + + +Basics +------ + +Yii uses PHP in view templates by default so in a web application a view typically contains some HTML, `echo`, `foreach` +and such basic constructs. It may also contain widget calls. Using complex code in views is considered a bad practice. +Such code should be moved to controller or widgets. + +View is typically called from controller action like the following: + +```php +public function actionIndex() +{ + return $this->render('index', array( + 'username' => 'samdark', + )); +} +``` + +First argument is the view name. In context of the controller Yii will search for its views in `views/site/` where `site` +is controller ID. For details on how view name is resolved please refer to [yii\base\Controller::render] method. +Second argument is data array that contains key-value pairs. Value is available in the view as a variable named the same +as the corresponding key. + +So the view for the action above should be in `views/site/index.php` and can be something like: + +```php +<p>Hello, <?php echo $username?>!</p> +``` + +Instead of just scalar values you can pass anything else such as arrays or objects. + +Widgets +------- + +Widgets are a self-contained building blocks for your views. A widget may contain advanced logic, typically takes some +configuration and data and returns HTML. There is a good number of widgets bundled with Yii such as [active form](form.md), +breadcrumbs, menu or [wrappers around bootstrap component framework](boostrap-widgets.md). Additionally there are +extensions providing additional widgets such as official one for jQueryUI components. + +In order to use widget you need to do the following: + +```php +// Note that you have to "echo" the result to display it +echo \yii\widgets\Menu::widget(array('items' => $items)); + +// Passing an array to initialize the object properties +$form = \yii\widgets\ActiveForm::begin(array( + 'options' => array('class' => 'form-horizontal'), + 'fieldConfig' => array('inputOptions' => array('class' => 'input-xlarge')), +)); +... form inputs here ... +\yii\widgets\ActiveForm::end(); +``` + +In the code above `widget` method is used for a widget that just outputs content while `begin` and `end` are used for a +widget that wraps content between method calls with its own output. In case of the form this output is the `<form>` tag +with some properties set. + +Security +-------- + +One of the main security principles is to always escape output. If violated it leads to script execution and, +most probably, to cross-site scripting known as XSS leading to leaking of admin passwords, making a user to automatically +perform actions etc. + +Yii provides a good toolset in order help you escaping your output. The very basic thing to escape is a text without any +markup. You can deal with it like the following: + +```php +<?php +use yii\helpers\Html; +?> + +<div class="username"> + <?php echo Html::encode($user->name); ?> +</div> +``` + +When you want to render HTML it becomes complex so we're delegating the task to excellent +[HTMLPurifier](http://htmlpurifier.org/) library. In order to use it you need to modify your `composer.json` first by +adding the following to `require`: + +```javascript +"ezyang/htmlpurifier": "v4.5.0" +``` + +After it's done run `php composer.phar install` and wait till package is downloaded. Now everything is prepared to use +Yii's HtmlPurifier helper: + +```php +<?php +use yii\helpers\HtmlPurifier; +?> + +<div class="post"> + <?php echo HtmlPurifier::process($post->text); ?> +</div> +``` + +Note that besides HTMLPurifier does excellent job making output safe it's not very fast so consider +[caching result](caching.md). + +Alternative template languages +------------------------------ + +There are official extensions for [Smarty](http://www.smarty.net/) and [Twig](http://twig.sensiolabs.org/). In order +to learn more refer to [Using template engines](template.md) section of the guide. + +Using View object in templates +------------------------------ + +An instance of `yii\base\View` component is available in view templates as `$this` variable. Using it in templates you +can do many useful things including setting page title and meta, registering scripts and accessing the context. + +### Setting page title + +A common place to set page title are view templates. Since we can access view object with `$this`, setting a title +becomes as easy as: + +```php +$this->title = 'My page title'; +``` + +### Adding meta tags + +Adding meta tags such as encoding, description, keywords is easy with view object as well: + +```php +$this->registerMetaTag(array('encoding' => 'utf-8')); +``` + +The first argument is an map of `<meta>` tag option names and values. The code above will produce: + +```html +<meta encoding="utf-8"> +``` + +Sometimes there's a need to have only a single tag of a type. In this case you need to specify the second argument: + +```html +$this->registerMetaTag(array('description' => 'This is my cool website made with Yii!'), 'meta-description'); +$this->registerMetaTag(array('description' => 'This website is about funny raccoons.'), 'meta-description'); +``` + +If there are multiple calls with the same value of the second argument (`meta-description` in this case), the latter will +override the former and only a single tag will be rendered: + +```html +<meta description="This website is about funny raccoons."> +``` + +### Registering link tags + +`<link>` tag is useful in many cases such as customizing favicon, pointing to RSS feed or delegating OpenID to another +server. Yii view object has a method to work with these: + +```php +$this->registerLinkTag(array( + 'title' => 'Lives News for Yii Framework', + 'rel' => 'alternate', + 'type' => 'application/rss+xml', + 'href' => 'http://www.yiiframework.com/rss.xml/', +)); +``` + +The code above will result in + +```html +<link title="Lives News for Yii Framework" rel="alternate" type="application/rss+xml" href="http://www.yiiframework.com/rss.xml/" /> +``` + +Same as with meta tags you can specify additional argument to make sure there's only one link of a type registered. + +### Registering CSS + +You can register CSS using `registerCss` or `registerCssFile`. Former is for outputting code in `<style>` tags directly +to the page which is not recommended in most cases (but still valid). Latter is for registering CSS file. In Yii it's +much better to [use asset manager](assets.md) to deal with these since it provides extra features so `registerCssFile` +is manly useful for external CSS files. + +```php +$this->registerCss("body { background: #f00; }"); +``` + +The code above will result in adding the following to the head section of the page: + +```html +<style> +body { background: #f00; } +</style> +``` + +If you want to specify additional properties of the style tag, pass array of name-values to the second argument. If you +need to make sure there's only a single style tag use third argument as was mentioned in meta tags description. + +```php +$this->registerCssFile("http://example.com/css/themes/black-and-white.css", array('media' => 'print'), 'css-print-theme'); +``` + +The code above will add a link to CSS file to the head section of the page. The CSS will be used only when printing the +page. We're using third argument so one of the views could override it. + +### Registering scripts + +With View object you can register scripts. There are two dedicated methods for it: `registerJs` for inline scripts +and `registerJsFile` for external scripts. Inline scripts are useful for configuration and dynamically generated code. +The method for adding these can be used as follows: + +```php +$this->registerJs("var options = ".json_encode($options).";", View::POS_END, 'my-options'); +``` + +First argument is the actual code where we're converting a PHP array of options to JavaScript one. Second argument +determines where script should be in the page. Possible values are: + +- `View::POS_HEAD` for head section. +- `View::POS_BEGIN` for right after opening `<body>`. +- `View::POS_END` for right before closing `</body>`. +- `View::POS_READY` for executing code on document `ready` event. This one registers jQuery automatically. + +The last argument is unique script ID that is used to identify code block and replace existing one with the same ID +instead of adding a new one. + +External script can be added like the following: + +```php +$this->registerJsFile('http://example.com/js/main.js'); +``` + +Same as with external CSS it's preferred to use asset bundles for external scripts. + +### Registering asset bundles + +As was mentioned earlier it's preferred to use asset bundles instead of using CSS and JavaScript directly. You can get +details on how to define asset bundles in [asset manager](assets.md) section of the guide. As for using already defined +asset bundle, it's very straightforward: + +```php +frontend\config\AppAsset::register($this); +``` + +### Layout + +A layout is a very convenient way to represent the part of the page that is common for all or at least for most pages +generated by your application. Typically it includes `<head>` section, footer, main menu and alike elements. +You can fine a fine example of the layout in a [basic application template](apps-basic.md). Here we'll review the very +basic one without any widgets or extra markup. + +```php +<?php +use yii\helpers\Html; +?> +<?php $this->beginPage(); ?> +<!DOCTYPE html> +<html lang="<?php echo Yii::$app->charset; ?>"> +<head> + <meta charset="<?php echo Yii::$app->charset; ?>"/> + <title><?php echo Html::encode($this->title); ?></title> + <?php $this->head(); ?> +</head> +<body> +<?php $this->beginBody(); ?> + <div class="container"> + <?php echo $content; ?> + </div> + <footer class="footer">© 2013 me :)</footer> +<?php $this->endBody(); ?> +</body> +</html> +<?php $this->endPage(); ?> +``` + +In the markup above there's some code. First of all, `$content` is a variable that will contain result of views rendered +with controller's `$this->render()` method. + +TBD + +### Partials + +Often you need to reuse some HTML markup in many views and often it's too simple to create a full-featured widget for it. +In this case you may use partials. + +Partial is a view as well. It resides in one of directories under `views` and by convention is often started with `_`. +For example, we need to render a list of user profiles and, at the same time, display individual profile elsewhere. + +First we need to define a partial for user profile in `_profile.php`: + +```php +<?php +use yii\helpers\Html; +?> + +<div class="profile"> + <h2><?php echo Html::encode($username); ?></h2> + <p><?php echo Html::encode($tagline); ?></p> +</div> +``` + +Then we're using it in `index.php` view where we display a list of users: + +```php +<div class="user-index"> + <?php + foreach($users as $user) { + echo $this->render('_profile', array( + 'username' => $user->name, + 'tagline' => $user->tagline, + )); + } + ?> +</div> +``` + +Same way we can reuse it in another view displaying a single user profile: + +```php +echo $this->render('_profile', array( + 'username' => $user->name, + 'tagline' => $user->tagline, +)); +``` + +### Accessing context + +Views are generally used either by controller or by widget. In both cases the object that called view rendering is +available in the view as `$this->context`. For example if we need to print out the current internal request route in a +view rendered by controller we can use the following: + +```php +echo $this->context->getRoute(); +``` + +### Caching blocks + +To learn about caching of view fragments please refer to [caching](caching.md) section of the guide. + +Customizing View component +-------------------------- + +Since view is also an application component named `view` you can replace it with your own component that extends +from `yii\base\View`. It can be done via application configuration file such as `config/web.php`: + +```php +return array( + // ... + 'components' => array( + 'view' => array( + 'class' => 'app\components\View', + ), + // ... + ), +); +``` diff --git a/docs/internals/ar.md b/docs/internals/ar.md index c493269..d59ba6a 100644 --- a/docs/internals/ar.md +++ b/docs/internals/ar.md @@ -1,15 +1,32 @@ ActiveRecord ============ +Scenarios +--------- + +Possible scenario formats supported by ActiveRecord: + +```php +public function scenarios() +{ + return array( + // attributes array, all operations won't be wrapped with transaction + 'scenario1' => array('attribute1', 'attribute2'), + + // insert and update operations will be wrapped with transaction, delete won't be wrapped + 'scenario2' => array( + 'attributes' => array('attribute1', 'attribute2'), + 'atomic' => array(self::OP_INSERT, self::OP_UPDATE), + ), + ); +} +``` + Query ----- ### Basic Queries - - ### Relational Queries ### Scopes - - diff --git a/docs/autoloader.md b/docs/internals/autoloader.md similarity index 92% rename from docs/autoloader.md rename to docs/internals/autoloader.md index b7696d7..76a545b 100644 --- a/docs/autoloader.md +++ b/docs/internals/autoloader.md @@ -16,4 +16,4 @@ PEAR-style libraries References ---------- -- YiiBase::autoload \ No newline at end of file +- BaseYii::autoload \ No newline at end of file diff --git a/docs/model.md b/docs/model.md deleted file mode 100644 index 4c39a5b..0000000 --- a/docs/model.md +++ /dev/null @@ -1,214 +0,0 @@ -Model -===== - -Attributes ----------- - -Attributes store the actual data represented by a model and can -be accessed like object member variables. For example, a `Post` model -may contain a `title` attribute and a `content` attribute which may be -accessed as follows: - -~~~php -$post->title = 'Hello, world'; -$post->content = 'Something interesting is happening'; -echo $post->title; -echo $post->content; -~~~ - -A model should list all its available attributes in the `attributes()` method. - -Attributes may be implemented in various ways. The [[\yii\base\Model]] class -implements attributes as public member variables of the class, while the -[[\yii\db\ActiveRecord]] class implements them as DB table columns. For example, - -~~~php -// LoginForm has two attributes: username and password -class LoginForm extends \yii\base\Model -{ - public $username; - public $password; -} - -// Post is associated with the tbl_post DB table. -// Its attributes correspond to the columns in tbl_post -class Post extends \yii\db\ActiveRecord -{ - public function table() - { - return 'tbl_post'; - } -} -~~~ - - -### Attribute Labels - - -Scenarios ---------- - -A model may be used in different scenarios. For example, a `User` model may be -used to collect user login inputs, and it may also be used for user registration -purpose. For this reason, each model has a property named `scenario` which stores -the name of the scenario that the model is currently being used in. As we will explain -in the next few sections, the concept of scenario is mainly used in validation and -massive attribute assignment. - -Associated with each scenario is a list of attributes that are *active* in that -particular scenario. For example, in the `login` scenario, only the `username` -and `password` attributes are active; while in the `register` scenario, -additional attributes such as `email` are *active*. - -Possible scenarios should be listed in the `scenarios()` method which returns an array -whose keys are the scenario names and whose values are the corresponding -active attribute lists. Below is an example: - -~~~php -class User extends \yii\db\ActiveRecord -{ - public function table() - { - return 'tbl_user'; - } - - public function scenarios() - { - return array( - 'login' => array('username', 'password'), - 'register' => array('username', 'email', 'password'), - ); - } -} -~~~ - -Sometimes, we want to mark that an attribute is not safe for massive assignment -(but we still want it to be validated). We may do so by prefixing an exclamation -character to the attribute name when declaring it in `scenarios()`. For example, - -~~~php -array('username', 'password', '!secret') -~~~ - - -Validation ----------- - -When a model is used to collect user input data via its attributes, -it usually needs to validate the affected attributes to make sure they -satisfy certain requirements, such as an attribute cannot be empty, -an attribute must contain letters only, etc. If errors are found in -validation, they may be presented to the user to help him fix the errors. -The following example shows how the validation is performed: - -~~~php -$model = new LoginForm; -$model->username = $_POST['username']; -$model->password = $_POST['password']; -if ($model->validate()) { - // ...login the user... -} else { - $errors = $model->getErrors(); - // ...display the errors to the end user... -} -~~~ - -The possible validation rules for a model should be listed in its -`rules()` method. Each validation rule applies to one or several attributes -and is effective in one or several scenarios. A rule can be specified -using a validator object - an instance of a [[\yii\validators\Validator]] -child class, or an array with the following format: - -~~~php -array( - 'attribute1, attribute2, ...', - 'validator class or alias', - // specifies in which scenario(s) this rule is active. - // if not given, it means it is active in all scenarios - 'on' => 'scenario1, scenario2, ...', - // the following name-value pairs will be used - // to initialize the validator properties... - 'name1' => 'value1', - 'name2' => 'value2', - .... -) -~~~ - -When `validate()` is called, the actual validation rules executed are -determined using both of the following criteria: - -* the rules must be associated with at least one active attribute; -* the rules must be active for the current scenario. - - -### Active Attributes - -An attribute is *active* if it is subject to some validations in the current scenario. - - -### Safe Attributes - -An attribute is *safe* if it can be massively assigned in the current scenario. - - -Massive Access of Attributes ----------------------------- - - -Massive Attribute Retrieval ---------------------------- - -Attributes can be massively retrieved via the `attributes` property. -The following code will return *all* attributes in the `$post` model -as an array of name-value pairs. - -~~~php -$attributes = $post->attributes; -var_dump($attributes); -~~~ - - -Massive Attribute Assignment ----------------------------- - - - - -Safe Attributes ---------------- - -Safe attributes are those that can be massively assigned. For example, - -Validation rules and mass assignment ------------------------------------- - -In Yii2 unlike Yii 1.x validation rules are separated from mass assignment. Validation -rules are described in `rules()` method of the model while what's safe for mass -assignment is described in `scenarios` method: - -```php - -function rules() { - return array( - // rule applied when corresponding field is "safe" - array('username', 'length', 'min' => 2), - array('first_name', 'length', 'min' => 2), - array('password', 'required'), - - // rule applied when scenario is "signup" no matter if field is "safe" or not - array('hashcode', 'check', 'on' => 'signup'), - ); -} - -function scenarios() { - return array( - // on signup allow mass assignment of username - 'signup' => array('username', 'password'), - 'update' => array('username', 'first_name'), - ); -} - -``` - -Note that everything is unsafe by default and you can't make field "safe" -without specifying scenario. \ No newline at end of file diff --git a/docs/view_renderers.md b/docs/view_renderers.md deleted file mode 100644 index e26fe83..0000000 --- a/docs/view_renderers.md +++ /dev/null @@ -1,85 +0,0 @@ -Yii2 view renderers -=================== - -By default Yii uses PHP as template language but you can configure it to be able -to render templates with special engines such as Twig or Smarty. - -The component responsible for rendering a view is called `view`. You can add -a custom template engines as follows: - -```php -array( - 'components' => array( - 'view' => array( - 'class' => 'yii\base\View', - 'renderers' => array( - 'tpl' => array( - 'class' => 'yii\renderers\SmartyViewRenderer', - ), - 'twig' => array( - 'class' => 'yii\renderers\TwigViewRenderer', - 'twigPath' => '@app/vendors/Twig', - ), - // ... - ), - ), - ), -) -``` - -Note that Smarty and Twig are not bundled with Yii and you have to download and -unpack these yourself and then specify `twigPath` and `smartyPath` respectively. - -Twig ----- - -In order to use Twig you need to put you templates in files with extension `.twig` -(or another one if configured differently). -Also you need to specify this extension explicitly when calling `$this->render()` -or `$this->renderPartial()` from your controller: - -```php -echo $this->render('renderer.twig', array('username' => 'Alex')); -``` - -### Additional functions - -Additionally to regular Twig syntax the following is available in Yii: - -```php -<a href="{{ path('blog/view', {'alias' : post.alias}) }}">{{ post.title }}</a> -``` - -path function calls `Html::url()` internally. - -### Additional variables - -- `app` = `\Yii::$app` -- `this` = current `View` object - -Smarty ------- - -In order to use Smarty you need to put you templates in files with extension `.tpl` -(or another one if configured differently). -Also you need to specify this extension explicitly when calling `$this->render()` -or `$this->renderPartial()` from your controller: - -```php -echo $this->render('renderer.tpl', array('username' => 'Alex')); -``` - -### Additional functions - -Additionally to regular Smarty syntax the following is available in Yii: - -```php -<a href="{path route='blog/view' alias=$post.alias}">{$post.title}</a> -``` - -path function calls `Html::url()` internally. - -### Additional variables - -- `$app` = `\Yii::$app` -- `$this` = current `View` object \ No newline at end of file diff --git a/extensions/composer/LICENSE.md b/extensions/composer/LICENSE.md new file mode 100644 index 0000000..0bb1a8d --- /dev/null +++ b/extensions/composer/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/extensions/composer/README.md b/extensions/composer/README.md new file mode 100644 index 0000000..853d3c3 --- /dev/null +++ b/extensions/composer/README.md @@ -0,0 +1,44 @@ +Yii 2.0 Public Preview - Composer Installer +=========================================== + +Thank you for choosing Yii - a high-performance component-based PHP framework. + +If you are looking for a production-ready PHP framework, please use +[Yii v1.1](https://github.com/yiisoft/yii). + +Yii 2.0 is still under heavy development. We may make significant changes +without prior notices. **Yii 2.0 is not ready for production use yet.** + +[](http://travis-ci.org/yiisoft/yii2) + +This is the yii2 composer installer. + + +Installation +------------ + +This extension offers you enhanced Composer handling for your yii2-project. It will therefore require you to use Composer. + +``` +php composer.phar require yiisoft/yii2-composer "*" +``` + +*Note: You might have to run `php composer.phar selfupdate` before using this extension.* + + +Usage & Documentation +--------------------- + +This extension allows you to hook to certain composer events and automate preparing your Yii2 application for further usage. + +After the package is installed, the `composer.json` file has to be modified to enable this extension. + +To see it in action take a look at the example apps in the repository: + +- [Basic](https://github.com/suralc/yii2/blob/master/apps/basic/composer.json#L27) +- [Advanced](https://github.com/suralc/yii2/blob/extensions-readme/apps/advanced/composer.json) + +However it might be useful to read through the official composer [documentation](http://getcomposer.org/doc/articles/scripts.md) +to understand what this extension can do for you and what it can't. + +You can also use this as a template to create your own composer additions to ease development and deployment of your app. diff --git a/extensions/composer/composer.json b/extensions/composer/composer.json new file mode 100644 index 0000000..db430a2 --- /dev/null +++ b/extensions/composer/composer.json @@ -0,0 +1,27 @@ +{ + "name": "yiisoft/yii2-composer", + "description": "The composer integration for the Yii framework", + "keywords": ["yii", "composer", "install", "update"], + "type": "library", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com" + } + ], + "minimum-stability": "dev", + "require": { + "yiisoft/yii2": "*" + }, + "autoload": { + "psr-0": { "yii\\composer\\": "" } + } +} diff --git a/extensions/composer/yii/composer/InstallHandler.php b/extensions/composer/yii/composer/InstallHandler.php new file mode 100644 index 0000000..be4037b --- /dev/null +++ b/extensions/composer/yii/composer/InstallHandler.php @@ -0,0 +1,97 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\composer; + +use Composer\Script\CommandEvent; +use yii\console\Application; +use yii\console\Exception; + +defined('YII_DEBUG') or define('YII_DEBUG', true); + +// fcgi doesn't have STDIN defined by default +defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); + +/** + * InstallHandler is called by Composer after it installs/updates the current package. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Tobias Munk <schmunk@usrbin.de> + * @since 2.0 + */ +class InstallHandler +{ + const PARAM_WRITABLE = 'yii-install-writable'; + const PARAM_EXECUTABLE = 'yii-install-executable'; + const PARAM_CONFIG = 'yii-install-config'; + const PARAM_COMMANDS = 'yii-install-commands'; + + /** + * Sets the correct permissions of files and directories. + * @param CommandEvent $event + */ + public static function setPermissions($event) + { + $options = array_merge(array( + self::PARAM_WRITABLE => array(), + self::PARAM_EXECUTABLE => array(), + ), $event->getComposer()->getPackage()->getExtra()); + + foreach ((array)$options[self::PARAM_WRITABLE] as $path) { + echo "Setting writable: $path ..."; + if (is_dir($path)) { + chmod($path, 0777); + echo "done\n"; + } else { + echo "The directory was not found: " . getcwd() . DIRECTORY_SEPARATOR . $path; + return; + } + } + + foreach ((array)$options[self::PARAM_EXECUTABLE] as $path) { + echo "Setting executable: $path ..."; + if (is_file($path)) { + chmod($path, 0755); + echo "done\n"; + } else { + echo "\n\tThe file was not found: " . getcwd() . DIRECTORY_SEPARATOR . $path . "\n"; + return; + } + } + } + + /** + * Executes a yii command. + * @param CommandEvent $event + */ + public static function run($event) + { + $options = array_merge(array( + self::PARAM_COMMANDS => array(), + ), $event->getComposer()->getPackage()->getExtra()); + + if (!isset($options[self::PARAM_CONFIG])) { + throw new Exception('Please specify the "' . self::PARAM_CONFIG . '" parameter in composer.json.'); + } + $configFile = getcwd() . '/' . $options[self::PARAM_CONFIG]; + if (!is_file($configFile)) { + throw new Exception("Config file does not exist: $configFile"); + } + + require_once(__DIR__ . '/../../../yii2/yii/Yii.php'); + $application = new Application(require($configFile)); + $request = $application->getRequest(); + + foreach ((array)$options[self::PARAM_COMMANDS] as $command) { + $params = str_getcsv($command, ' '); // see http://stackoverflow.com/a/6609509/291573 + $request->setParams($params); + list($route, $params) = $request->resolve(); + echo "Running command: yii {$command}\n"; + $application->runAction($route, $params); + } + } +} diff --git a/extensions/composer/yii/composer/Installer.php b/extensions/composer/yii/composer/Installer.php new file mode 100644 index 0000000..8b5b9ea --- /dev/null +++ b/extensions/composer/yii/composer/Installer.php @@ -0,0 +1,208 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\composer; + +use Composer\Package\PackageInterface; +use Composer\Installer\LibraryInstaller; +use Composer\Repository\InstalledRepositoryInterface; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Installer extends LibraryInstaller +{ + const EXTRA_WRITABLES = 'yii-writables'; + const EXTRA_EXECUTABLES = 'yii-executables'; + const EXTRA_CONFIG = 'yii-config'; + const EXTRA_COMMANDS = 'yii-commands'; + const EXTRA_ALIASES = 'yii-aliases'; + const EXTRA_PREINIT = 'yii-preinit'; + const EXTRA_INIT = 'yii-init'; + + /** + * @inheritdoc + */ + public function supports($packageType) + { + return $packageType === 'yii2-extension'; + } + + /** + * @inheritdoc + */ + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + parent::install($repo, $package); + $this->addPackage($package); + } + + /** + * @inheritdoc + */ + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + parent::update($repo, $initial, $target); + $this->removePackage($initial); + $this->addPackage($target); + } + + /** + * @inheritdoc + */ + public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) + { + parent::uninstall($repo, $package); + $this->removePackage($package); + } + + protected function addPackage(PackageInterface $package) + { + $extension = array('name' => $package->getPrettyName()); + + $root = $package->getPrettyName(); + if ($targetDir = $package->getTargetDir()) { + $root .= '/' . trim($targetDir, '/'); + } + $root = trim($root, '/'); + + $extra = $package->getExtra(); + + if (isset($extra[self::EXTRA_PREINIT]) && is_string($extra[self::EXTRA_PREINIT])) { + $extension[self::EXTRA_PREINIT] = "<vendor-dir>/$root/" . ltrim(str_replace('\\', '/', $extra[self::EXTRA_PREINIT]), '/'); + } + if (isset($extra[self::EXTRA_INIT]) && is_string($extra[self::EXTRA_INIT])) { + $extension[self::EXTRA_INIT] = "<vendor-dir>/$root/" . ltrim(str_replace('\\', '/', $extra[self::EXTRA_INIT]), '/'); + } + + if (isset($extra['aliases']) && is_array($extra['aliases'])) { + foreach ($extra['aliases'] as $alias => $path) { + $extension['aliases']['@' . ltrim($alias, '@')] = "<vendor-dir>/$root/" . ltrim(str_replace('\\', '/', $path), '/'); + } + } + + if (!empty($aliases)) { + foreach ($aliases as $alias => $path) { + if (strncmp($alias, '@', 1) !== 0) { + $alias = '@' . $alias; + } + $path = trim(str_replace('\\', '/', $path), '/'); + $extension['aliases'][$alias] = $root . '/' . $path; + } + } + + $extensions = $this->loadExtensions(); + $extensions[$package->getId()] = $extension; + $this->saveExtensions($extensions); + } + + protected function removePackage(PackageInterface $package) + { + $packages = $this->loadExtensions(); + unset($packages[$package->getId()]); + $this->saveExtensions($packages); + } + + protected function loadExtensions() + { + $file = $this->vendorDir . '/yii-extensions.php'; + if (!is_file($file)) { + return array(); + } + $extensions = require($file); + /** @var string $vendorDir defined in yii-extensions.php */ + $n = strlen($vendorDir); + foreach ($extensions as &$extension) { + if (isset($extension['aliases'])) { + foreach ($extension['aliases'] as $alias => $path) { + $extension['aliases'][$alias] = '<vendor-dir>' . substr($path, $n); + } + } + if (isset($extension[self::EXTRA_PREINIT])) { + $extension[self::EXTRA_PREINIT] = '<vendor-dir>' . substr($extension[self::EXTRA_PREINIT], $n); + } + if (isset($extension[self::EXTRA_INIT])) { + $extension[self::EXTRA_INIT] = '<vendor-dir>' . substr($extension[self::EXTRA_INIT], $n); + } + } + return $extensions; + } + + protected function saveExtensions(array $extensions) + { + $file = $this->vendorDir . '/yii-extensions.php'; + $array = str_replace("'<vendor-dir>", '$vendorDir . \'', var_export($extensions, true)); + file_put_contents($file, "<?php\n\$vendorDir = __DIR__;\n\nreturn $array;\n"); + } + + + /** + * Sets the correct permissions of files and directories. + * @param CommandEvent $event + */ + public static function setPermissions($event) + { + $options = array_merge(array( + self::EXTRA_WRITABLES => array(), + self::EXTRA_EXECUTABLES => array(), + ), $event->getComposer()->getPackage()->getExtra()); + + foreach ((array)$options[self::EXTRA_WRITABLES] as $path) { + echo "Setting writable: $path ..."; + if (is_dir($path)) { + chmod($path, 0777); + echo "done\n"; + } else { + echo "The directory was not found: " . getcwd() . DIRECTORY_SEPARATOR . $path; + return; + } + } + + foreach ((array)$options[self::EXTRA_EXECUTABLES] as $path) { + echo "Setting executable: $path ..."; + if (is_file($path)) { + chmod($path, 0755); + echo "done\n"; + } else { + echo "\n\tThe file was not found: " . getcwd() . DIRECTORY_SEPARATOR . $path . "\n"; + return; + } + } + } + + /** + * Executes a yii command. + * @param CommandEvent $event + */ + public static function run($event) + { + $options = array_merge(array( + self::EXTRA_COMMANDS => array(), + ), $event->getComposer()->getPackage()->getExtra()); + + if (!isset($options[self::EXTRA_CONFIG])) { + throw new Exception('Please specify the "' . self::EXTRA_CONFIG . '" parameter in composer.json.'); + } + $configFile = getcwd() . '/' . $options[self::EXTRA_CONFIG]; + if (!is_file($configFile)) { + throw new Exception("Config file does not exist: $configFile"); + } + + require_once(__DIR__ . '/../../../yii2/yii/Yii.php'); + $application = new Application(require($configFile)); + $request = $application->getRequest(); + + foreach ((array)$options[self::EXTRA_COMMANDS] as $command) { + $params = str_getcsv($command, ' '); // see http://stackoverflow.com/a/6609509/291573 + $request->setParams($params); + list($route, $params) = $request->resolve(); + echo "Running command: yii {$command}\n"; + $application->runAction($route, $params); + } + } +} diff --git a/extensions/composer/yii/composer/InstallerPlugin.php b/extensions/composer/yii/composer/InstallerPlugin.php new file mode 100644 index 0000000..f6de6b2 --- /dev/null +++ b/extensions/composer/yii/composer/InstallerPlugin.php @@ -0,0 +1,30 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\composer; + +use Composer\Composer; +use Composer\IO\IOInterface; +use Composer\Plugin\PluginInterface; + +/** + * InstallerPlugin is the composer plugin that registers the Yii composer installer. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class InstallerPlugin implements PluginInterface +{ + /** + * @inheritdoc + */ + public function activate(Composer $composer, IOInterface $io) + { + $installer = new Installer($io, $composer); + $composer->getInstallationManager()->addInstaller($installer); + } +} diff --git a/extensions/jui/LICENSE.md b/extensions/jui/LICENSE.md new file mode 100644 index 0000000..0bb1a8d --- /dev/null +++ b/extensions/jui/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/extensions/jui/README.md b/extensions/jui/README.md new file mode 100644 index 0000000..0403acb --- /dev/null +++ b/extensions/jui/README.md @@ -0,0 +1,55 @@ +Yii 2.0 Public Preview - JUI Extension +====================================== + +Thank you for choosing Yii - a high-performance component-based PHP framework. + +If you are looking for a production-ready PHP framework, please use +[Yii v1.1](https://github.com/yiisoft/yii). + +Yii 2.0 is still under heavy development. We may make significant changes +without prior notices. **Yii 2.0 is not ready for production use yet.** + +[](http://travis-ci.org/yiisoft/yii2) + +This is the yii2-jui extension. + + +Installation +------------ + +The preferred way to install this extension is [composer](http://getcomposer.org/download/). + +Either run +``` +php composer.phar require yiisoft/yii2-jui "*" +``` + +or add +``` +"yiisoft/yii2-jui": "*" +``` +to the require section of your composer.json. + + +*Note: You might have to run `php composer.phar selfupdate`* + + +Usage & Documentation +--------------------- + +This extension provides multiple widgets to work with jquery.ui, as well as a set of compatible jquery.ui files. + +You can use these widgets in your view files after you have registered the corresponding assets. + +Example: + +```php +echo ProgressBar::widget(array( + 'clientOptions' => array( + 'value' => 75, + ), +)); +``` + +For further instructions refer to the yii guide. + diff --git a/extensions/jui/composer.json b/extensions/jui/composer.json new file mode 100644 index 0000000..e228973 --- /dev/null +++ b/extensions/jui/composer.json @@ -0,0 +1,21 @@ +{ + "name": "yiisoft/yii2-jui", + "description": "The Jquery UI extension for the Yii framework", + "keywords": ["yii", "Jquery UI", "renderer"], + "type": "library", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "minimum-stability": "dev", + "require": { + "yiisoft/yii2": "*" + }, + "autoload": { + "psr-0": { "yii\\jui\\": "" } + } +} diff --git a/extensions/jui/yii/jui/Accordion.php b/extensions/jui/yii/jui/Accordion.php new file mode 100644 index 0000000..7f32bc2 --- /dev/null +++ b/extensions/jui/yii/jui/Accordion.php @@ -0,0 +1,133 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use yii\base\InvalidConfigException; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * Accordion renders an accordion jQuery UI widget. + * + * For example: + * + * ```php + * echo Accordion::widget(array( + * 'items' => array( + * array( + * 'header' => 'Section 1', + * 'content' => 'Mauris mauris ante, blandit et, ultrices a, suscipit eget...', + * ), + * array( + * 'header' => 'Section 2', + * 'headerOptions' => array( + * 'tag' => 'h3', + * ), + * 'content' => 'Sed non urna. Phasellus eu ligula. Vestibulum sit amet purus...', + * 'options' => array( + * 'tag' => 'div', + * ), + * ), + * ), + * 'options' => array( + * 'tag' => 'div', + * ), + * 'itemOptions' => array( + * 'tag' => 'div', + * ), + * 'headerOptions' => array( + * 'tag' => 'h3', + * ), + * 'clientOptions' => array( + * 'collapsible' => false, + * ), + * )); + * ``` + * + * @see http://api.jqueryui.com/accordion/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class Accordion extends Widget +{ + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the container tag of this widget + */ + public $options = array(); + /** + * @var array list of collapsible items. Each item can be an array of the following structure: + * + * ~~~ + * array( + * 'header' => 'Item header', + * 'content' => 'Item content', + * // the HTML attributes of the item header container tag. This will overwrite "headerOptions". + * 'headerOptions' => array(), + * // the HTML attributes of the item container tag. This will overwrite "itemOptions". + * 'options' => array(), + * ) + * ~~~ + */ + public $items = array(); + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the item container tags. + */ + public $itemOptions = array(); + /** + * @var array list of HTML attributes for the item header container tags. This will be overwritten + * by the "headerOptions" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "h3", the tag name of the item container tags. + */ + public $headerOptions = array(); + + + /** + * Renders the widget. + */ + public function run() + { + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'div'); + echo Html::beginTag($tag, $options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag($tag) . "\n"; + $this->registerWidget('accordion', AccordionAsset::className()); + } + + /** + * Renders collapsible items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + protected function renderItems() + { + $items = array(); + foreach ($this->items as $item) { + if (!isset($item['header'])) { + throw new InvalidConfigException("The 'header' option is required."); + } + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' option is required."); + } + $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', array())); + $headerTag = ArrayHelper::remove($headerOptions, 'tag', 'h3'); + $items[] = Html::tag($headerTag, $item['header'], $headerOptions); + $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', array())); + $tag = ArrayHelper::remove($options, 'tag', 'div'); + $items[] = Html::tag($tag, $item['content'], $options); + } + + return implode("\n", $items); + } +} diff --git a/extensions/jui/yii/jui/AccordionAsset.php b/extensions/jui/yii/jui/AccordionAsset.php new file mode 100644 index 0000000..ae6accb --- /dev/null +++ b/extensions/jui/yii/jui/AccordionAsset.php @@ -0,0 +1,25 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class AccordionAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.accordion.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + 'yii\jui\EffectAsset', + ); +} diff --git a/extensions/jui/yii/jui/AutoComplete.php b/extensions/jui/yii/jui/AutoComplete.php new file mode 100644 index 0000000..be31c55 --- /dev/null +++ b/extensions/jui/yii/jui/AutoComplete.php @@ -0,0 +1,66 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use Yii; +use yii\helpers\Html; + +/** + * AutoComplete renders an autocomplete jQuery UI widget. + * + * For example: + * + * ```php + * echo AutoComplete::widget(array( + * 'model' => $model, + * 'attribute' => 'country', + * 'clientOptions' => array( + * 'source' => array('USA', 'RUS'), + * ), + * )); + * ``` + * + * The following example will use the name property instead: + * + * ```php + * echo AutoComplete::widget(array( + * 'name' => 'country', + * 'clientOptions' => array( + * 'source' => array('USA', 'RUS'), + * ), + * )); + *``` + * + * @see http://api.jqueryui.com/autocomplete/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class AutoComplete extends InputWidget +{ + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderWidget(); + $this->registerWidget('autocomplete', AutoCompleteAsset::className()); + } + + /** + * Renders the AutoComplete widget. + * @return string the rendering result. + */ + public function renderWidget() + { + if ($this->hasModel()) { + return Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + return Html::textInput($this->name, $this->value, $this->options); + } + } +} diff --git a/extensions/jui/yii/jui/AutoCompleteAsset.php b/extensions/jui/yii/jui/AutoCompleteAsset.php new file mode 100644 index 0000000..d0af190 --- /dev/null +++ b/extensions/jui/yii/jui/AutoCompleteAsset.php @@ -0,0 +1,25 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class AutoCompleteAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.autocomplete.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + 'yii\jui\MenuAsset', + ); +} diff --git a/extensions/jui/yii/jui/ButtonAsset.php b/extensions/jui/yii/jui/ButtonAsset.php new file mode 100644 index 0000000..1676e8e --- /dev/null +++ b/extensions/jui/yii/jui/ButtonAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ButtonAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.button.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + ); +} diff --git a/extensions/jui/yii/jui/CoreAsset.php b/extensions/jui/yii/jui/CoreAsset.php new file mode 100644 index 0000000..2f0d62a --- /dev/null +++ b/extensions/jui/yii/jui/CoreAsset.php @@ -0,0 +1,27 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class CoreAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.core.js', + 'jquery.ui.widget.js', + 'jquery.ui.position.js', + 'jquery.ui.mouse.js', + ); + public $depends = array( + 'yii\web\JqueryAsset', + ); +} diff --git a/extensions/jui/yii/jui/DatePicker.php b/extensions/jui/yii/jui/DatePicker.php new file mode 100644 index 0000000..1d3012d --- /dev/null +++ b/extensions/jui/yii/jui/DatePicker.php @@ -0,0 +1,110 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use Yii; +use yii\helpers\Html; +use yii\helpers\Json; + +/** + * DatePicker renders an datepicker jQuery UI widget. + * + * For example: + * + * ```php + * echo DatePicker::widget(array( + * 'language' => 'ru', + * 'model' => $model, + * 'attribute' => 'country', + * 'clientOptions' => array( + * 'dateFormat' => 'yy-mm-dd', + * ), + * )); + * ``` + * + * The following example will use the name property instead: + * + * ```php + * echo DatePicker::widget(array( + * 'language' => 'ru', + * 'name' => 'country', + * 'clientOptions' => array( + * 'dateFormat' => 'yy-mm-dd', + * ), + * )); + *``` + * + * @see http://api.jqueryui.com/datepicker/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class DatePicker extends InputWidget +{ + /** + * @var string the locale ID (eg 'fr', 'de') for the language to be used by the date picker. + * If this property set to false, I18N will not be involved. That is, the date picker will show in English. + */ + public $language = false; + /** + * @var boolean If true, shows the widget as an inline calendar and the input as a hidden field. + */ + public $inline = false; + + + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderWidget() . "\n"; + if ($this->language !== false) { + $view = $this->getView(); + DatePickerRegionalAsset::register($view); + + $options = Json::encode($this->clientOptions); + $view->registerJs("$('#{$this->options['id']}').datepicker($.extend({}, $.datepicker.regional['{$this->language}'], $options));"); + + $options = $this->clientOptions; + $this->clientOptions = false; // the datepicker js widget is already registered + $this->registerWidget('datepicker', DatePickerAsset::className()); + $this->clientOptions = $options; + } else { + $this->registerWidget('datepicker', DatePickerAsset::className()); + } + } + + /** + * Renders the DatePicker widget. + * @return string the rendering result. + */ + protected function renderWidget() + { + $contents = array(); + + if ($this->inline === false) { + if ($this->hasModel()) { + $contents[] = Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + $contents[] = Html::textInput($this->name, $this->value, $this->options); + } + } else { + if ($this->hasModel()) { + $contents[] = Html::activeHiddenInput($this->model, $this->attribute, $this->options); + $this->clientOptions['defaultDate'] = $this->model->{$this->attribute}; + } else { + $contents[] = Html::hiddenInput($this->name, $this->value, $this->options); + $this->clientOptions['defaultDate'] = $this->value; + } + $this->clientOptions['altField'] = '#' . $this->options['id']; + $this->options['id'] .= '-container'; + $contents[] = Html::tag('div', null, $this->options); + } + + return implode("\n", $contents); + } +} diff --git a/extensions/jui/yii/jui/DatePickerAsset.php b/extensions/jui/yii/jui/DatePickerAsset.php new file mode 100644 index 0000000..4102675 --- /dev/null +++ b/extensions/jui/yii/jui/DatePickerAsset.php @@ -0,0 +1,25 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DatePickerAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.datepicker.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + 'yii\jui\EffectAsset', + ); +} diff --git a/extensions/jui/yii/jui/DatePickerRegionalAsset.php b/extensions/jui/yii/jui/DatePickerRegionalAsset.php new file mode 100644 index 0000000..44fc60a --- /dev/null +++ b/extensions/jui/yii/jui/DatePickerRegionalAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DatePickerRegionalAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.datepicker-i18n.js', + ); + public $depends = array( + 'yii\jui\DatePickerAsset', + ); +} diff --git a/extensions/jui/yii/jui/Dialog.php b/extensions/jui/yii/jui/Dialog.php new file mode 100644 index 0000000..1285a90 --- /dev/null +++ b/extensions/jui/yii/jui/Dialog.php @@ -0,0 +1,52 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use yii\helpers\Html; + +/** + * Dialog renders an dialog jQuery UI widget. + * + * For example: + * + * ```php + * Dialog::begin(array( + * 'clientOptions' => array( + * 'modal' => true, + * ), + * )); + * + * echo 'Dialog contents here...'; + * + * Dialog::end(); + * ``` + * + * @see http://api.jqueryui.com/dialog/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class Dialog extends Widget +{ + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('dialog', DialogAsset::className()); + } +} diff --git a/extensions/jui/yii/jui/DialogAsset.php b/extensions/jui/yii/jui/DialogAsset.php new file mode 100644 index 0000000..04ba950 --- /dev/null +++ b/extensions/jui/yii/jui/DialogAsset.php @@ -0,0 +1,27 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DialogAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.dialog.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + 'yii\jui\ButtonAsset', + 'yii\jui\DraggableAsset', + 'yii\jui\ResizableAsset', + ); +} diff --git a/extensions/jui/yii/jui/Draggable.php b/extensions/jui/yii/jui/Draggable.php new file mode 100644 index 0000000..5fa6491 --- /dev/null +++ b/extensions/jui/yii/jui/Draggable.php @@ -0,0 +1,52 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use yii\helpers\Html; + +/** + * Draggable renders an draggable jQuery UI widget. + * + * For example: + * + * ```php + * Draggable::begin(array( + * 'clientOptions' => array( + * 'grid' => array(50, 20), + * ), + * )); + * + * echo 'Draggable contents here...'; + * + * Draggable::end(); + * ``` + * + * @see http://api.jqueryui.com/draggable/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class Draggable extends Widget +{ + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('draggable', DraggableAsset::className()); + } +} diff --git a/extensions/jui/yii/jui/DraggableAsset.php b/extensions/jui/yii/jui/DraggableAsset.php new file mode 100644 index 0000000..44f8e66 --- /dev/null +++ b/extensions/jui/yii/jui/DraggableAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DraggableAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.draggable.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + ); +} diff --git a/extensions/jui/yii/jui/Droppable.php b/extensions/jui/yii/jui/Droppable.php new file mode 100644 index 0000000..d9f366c --- /dev/null +++ b/extensions/jui/yii/jui/Droppable.php @@ -0,0 +1,52 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use yii\helpers\Html; + +/** + * Droppable renders an droppable jQuery UI widget. + * + * For example: + * + * ```php + * Droppable::begin(array( + * 'clientOptions' => array( + * 'accept' => '.special', + * ), + * )); + * + * echo 'Droppable body here...'; + * + * Droppable::end(); + * ``` + * + * @see http://api.jqueryui.com/droppable/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class Droppable extends Widget +{ + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('droppable', DroppableAsset::className()); + } +} diff --git a/extensions/jui/yii/jui/DroppableAsset.php b/extensions/jui/yii/jui/DroppableAsset.php new file mode 100644 index 0000000..3274c74 --- /dev/null +++ b/extensions/jui/yii/jui/DroppableAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DroppableAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.droppable.js', + ); + public $depends = array( + 'yii\jui\DraggableAsset', + ); +} diff --git a/extensions/jui/yii/jui/EffectAsset.php b/extensions/jui/yii/jui/EffectAsset.php new file mode 100644 index 0000000..b882f45 --- /dev/null +++ b/extensions/jui/yii/jui/EffectAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class EffectAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.effect-all.js', + ); + public $depends = array( + 'yii\web\JqueryAsset', + ); +} diff --git a/extensions/jui/yii/jui/InputWidget.php b/extensions/jui/yii/jui/InputWidget.php new file mode 100644 index 0000000..e100d6c --- /dev/null +++ b/extensions/jui/yii/jui/InputWidget.php @@ -0,0 +1,59 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use Yii; +use yii\base\Model; +use yii\base\InvalidConfigException; + +/** + * InputWidget is the base class for all jQuery UI input widgets. + * + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class InputWidget extends Widget +{ + /** + * @var Model the data model that this widget is associated with. + */ + public $model; + /** + * @var string the model attribute that this widget is associated with. + */ + public $attribute; + /** + * @var string the input name. This must be set if [[model]] and [[attribute]] are not set. + */ + public $name; + /** + * @var string the input value. + */ + public $value; + + + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + if (!$this->hasModel() && $this->name === null) { + throw new InvalidConfigException("Either 'name' or 'model' and 'attribute' properties must be specified."); + } + parent::init(); + } + + /** + * @return boolean whether this widget is associated with a data model. + */ + protected function hasModel() + { + return $this->model instanceof Model && $this->attribute !== null; + } +} diff --git a/extensions/jui/yii/jui/Menu.php b/extensions/jui/yii/jui/Menu.php new file mode 100644 index 0000000..a2aadd9 --- /dev/null +++ b/extensions/jui/yii/jui/Menu.php @@ -0,0 +1,78 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use Yii; +use yii\helpers\Json; + +/** + * Menu renders a menu jQuery UI widget. + * + * @see http://api.jqueryui.com/menu/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class Menu extends \yii\widgets\Menu +{ + /** + * @var array the options for the underlying jQuery UI widget. + * Please refer to the corresponding jQuery UI widget Web page for possible options. + * For example, [this page](http://api.jqueryui.com/accordion/) shows + * how to use the "Accordion" widget and the supported options (e.g. "header"). + */ + public $clientOptions = array(); + /** + * @var array the event handlers for the underlying jQuery UI widget. + * Please refer to the corresponding jQuery UI widget Web page for possible events. + * For example, [this page](http://api.jqueryui.com/accordion/) shows + * how to use the "Accordion" widget and the supported events (e.g. "create"). + */ + public $clientEvents = array(); + + + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + } + + /** + * Renders the widget. + */ + public function run() + { + parent::run(); + + $view = $this->getView(); + MenuAsset::register($view); + /** @var \yii\web\AssetBundle $themeAsset */ + $themeAsset = Widget::$theme; + $themeAsset::register($view); + + $id = $this->options['id']; + if ($this->clientOptions !== false) { + $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); + $js = "jQuery('#$id').menu($options);"; + $view->registerJs($js); + } + + if (!empty($this->clientEvents)) { + $js = array(); + foreach ($this->clientEvents as $event => $handler) { + $js[] = "jQuery('#$id').on('menu$event', $handler);"; + } + $view->registerJs(implode("\n", $js)); + } + } +} diff --git a/extensions/jui/yii/jui/MenuAsset.php b/extensions/jui/yii/jui/MenuAsset.php new file mode 100644 index 0000000..96a11a5 --- /dev/null +++ b/extensions/jui/yii/jui/MenuAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class MenuAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.menu.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + ); +} diff --git a/extensions/jui/yii/jui/ProgressBar.php b/extensions/jui/yii/jui/ProgressBar.php new file mode 100644 index 0000000..b06c06a --- /dev/null +++ b/extensions/jui/yii/jui/ProgressBar.php @@ -0,0 +1,62 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use yii\helpers\Html; + +/** + * ProgressBar renders an progressbar jQuery UI widget. + * + * For example: + * + * ```php + * echo ProgressBar::widget(array( + * 'clientOptions' => array( + * 'value' => 75, + * ), + * )); + * ``` + * + * The following example will show the content enclosed between the [[begin()]] + * and [[end()]] calls within the widget container: + * + * ~~~php + * ProgressBar::widget(array( + * 'clientOptions' => array( + * 'value' => 75, + * ), + * )); + * + * echo '<div class="progress-label">Loading...</div>'; + * + * ProgressBar::end(); + * ~~~ + * @see http://api.jqueryui.com/progressbar/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class ProgressBar extends Widget +{ + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('progressbar', ProgressBarAsset::className()); + } +} diff --git a/extensions/jui/yii/jui/ProgressBarAsset.php b/extensions/jui/yii/jui/ProgressBarAsset.php new file mode 100644 index 0000000..cc36d03 --- /dev/null +++ b/extensions/jui/yii/jui/ProgressBarAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ProgressBarAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.progressbar.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + ); +} diff --git a/extensions/jui/yii/jui/Resizable.php b/extensions/jui/yii/jui/Resizable.php new file mode 100644 index 0000000..21ec70c --- /dev/null +++ b/extensions/jui/yii/jui/Resizable.php @@ -0,0 +1,52 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use yii\helpers\Html; + +/** + * Resizable renders an resizable jQuery UI widget. + * + * For example: + * + * ```php + * Resizable::begin(array( + * 'clientOptions' => array( + * 'grid' => array(20, 10), + * ), + * )); + * + * echo 'Resizable contents here...'; + * + * Resizable::end(); + * ``` + * + * @see http://api.jqueryui.com/resizable/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class Resizable extends Widget +{ + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('resizable', ResizableAsset::className()); + } +} diff --git a/extensions/jui/yii/jui/ResizableAsset.php b/extensions/jui/yii/jui/ResizableAsset.php new file mode 100644 index 0000000..35b4849 --- /dev/null +++ b/extensions/jui/yii/jui/ResizableAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ResizableAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.resizable.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + ); +} diff --git a/extensions/jui/yii/jui/Selectable.php b/extensions/jui/yii/jui/Selectable.php new file mode 100644 index 0000000..b77cfa9 --- /dev/null +++ b/extensions/jui/yii/jui/Selectable.php @@ -0,0 +1,116 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use yii\base\InvalidConfigException; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * Selectable renders a selectable jQuery UI widget. + * + * For example: + * + * ```php + * echo Selectable::widget(array( + * 'items' => array( + * 'Item 1', + * array( + * 'content' => 'Item2', + * ), + * array( + * 'content' => 'Item3', + * 'options' => array( + * 'tag' => 'li', + * ), + * ), + * ), + * 'options' => array( + * 'tag' => 'ul', + * ), + * 'itemOptions' => array( + * 'tag' => 'li', + * ), + * 'clientOptions' => array( + * 'tolerance' => 'fit', + * ), + * )); + * ``` + * + * @see http://api.jqueryui.com/selectable/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class Selectable extends Widget +{ + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "ul", the tag name of the container tag of this widget + */ + public $options = array(); + /** + * @var array list of selectable items. Each item can be a string representing the item content + * or an array of the following structure: + * + * ~~~ + * array( + * 'content' => 'item content', + * // the HTML attributes of the item container tag. This will overwrite "itemOptions". + * 'options' => array(), + * ) + * ~~~ + */ + public $items = array(); + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "li", the tag name of the item container tags. + */ + public $itemOptions = array(); + + + /** + * Renders the widget. + */ + public function run() + { + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'ul'); + echo Html::beginTag($tag, $options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag($tag) . "\n"; + $this->registerWidget('selectable', SelectableAsset::className()); + } + + /** + * Renders selectable items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + public function renderItems() + { + $items = array(); + foreach ($this->items as $item) { + $options = $this->itemOptions; + $tag = ArrayHelper::remove($options, 'tag', 'li'); + if (is_array($item)) { + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' option is required."); + } + $options = array_merge($options, ArrayHelper::getValue($item, 'options', array())); + $tag = ArrayHelper::remove($options, 'tag', $tag); + $items[] = Html::tag($tag, $item['content'], $options); + } else { + $items[] = Html::tag($tag, $item, $options); + } + } + return implode("\n", $items); + } +} diff --git a/extensions/jui/yii/jui/SelectableAsset.php b/extensions/jui/yii/jui/SelectableAsset.php new file mode 100644 index 0000000..59cd485 --- /dev/null +++ b/extensions/jui/yii/jui/SelectableAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class SelectableAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.selectable.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + ); +} diff --git a/extensions/jui/yii/jui/Slider.php b/extensions/jui/yii/jui/Slider.php new file mode 100644 index 0000000..e1adb2b --- /dev/null +++ b/extensions/jui/yii/jui/Slider.php @@ -0,0 +1,68 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use Yii; +use yii\helpers\Html; + +/** + * Slider renders a slider jQuery UI widget. + * + * For example: + * + * ```php + * echo Slider::widget(array( + * 'model' => $model, + * 'attribute' => 'amount', + * 'clientOptions' => array( + * 'min' => 1, + * 'max' => 10, + * ), + * )); + * ``` + * + * The following example will use the name property instead: + * + * ```php + * echo Slider::widget(array( + * 'name' => 'amount', + * 'clientOptions' => array( + * 'min' => 1, + * 'max' => 10, + * ), + * )); + *``` + * + * @see http://api.jqueryui.com/slider/ + * @author Alexander Makarov <sam@rmcreative.ru> + * @since 2.0 + */ +class Slider extends InputWidget +{ + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderWidget(); + $this->registerWidget('slider', SliderAsset::className()); + } + + /** + * Renders the Slider widget. + * @return string the rendering result. + */ + public function renderWidget() + { + if ($this->hasModel()) { + return Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + return Html::textInput($this->name, $this->value, $this->options); + } + } +} diff --git a/extensions/jui/yii/jui/SliderAsset.php b/extensions/jui/yii/jui/SliderAsset.php new file mode 100644 index 0000000..f0c7dc2 --- /dev/null +++ b/extensions/jui/yii/jui/SliderAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class SliderAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.slider.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + ); +} diff --git a/extensions/jui/yii/jui/Sortable.php b/extensions/jui/yii/jui/Sortable.php new file mode 100644 index 0000000..6c0d48a --- /dev/null +++ b/extensions/jui/yii/jui/Sortable.php @@ -0,0 +1,116 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use yii\base\InvalidConfigException; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * Sortable renders a sortable jQuery UI widget. + * + * For example: + * + * ```php + * echo Sortable::widget(array( + * 'items' => array( + * 'Item 1', + * array( + * 'content' => 'Item2', + * ), + * array( + * 'content' => 'Item3', + * 'options' => array( + * 'tag' => 'li', + * ), + * ), + * ), + * 'options' => array( + * 'tag' => 'ul', + * ), + * 'itemOptions' => array( + * 'tag' => 'li', + * ), + * 'clientOptions' => array( + * 'cursor' => 'move', + * ), + * )); + * ``` + * + * @see http://api.jqueryui.com/sortable/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class Sortable extends Widget +{ + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "ul", the tag name of the container tag of this widget + */ + public $options = array(); + /** + * @var array list of sortable items. Each item can be a string representing the item content + * or an array of the following structure: + * + * ~~~ + * array( + * 'content' => 'item content', + * // the HTML attributes of the item container tag. This will overwrite "itemOptions". + * 'options' => array(), + * ) + * ~~~ + */ + public $items = array(); + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "li", the tag name of the item container tags. + */ + public $itemOptions = array(); + + + /** + * Renders the widget. + */ + public function run() + { + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'ul'); + echo Html::beginTag($tag, $options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag($tag) . "\n"; + $this->registerWidget('sortable', SortableAsset::className()); + } + + /** + * Renders sortable items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + public function renderItems() + { + $items = array(); + foreach ($this->items as $item) { + $options = $this->itemOptions; + $tag = ArrayHelper::remove($options, 'tag', 'li'); + if (is_array($item)) { + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' option is required."); + } + $options = array_merge($options, ArrayHelper::getValue($item, 'options', array())); + $tag = ArrayHelper::remove($options, 'tag', $tag); + $items[] = Html::tag($tag, $item['content'], $options); + } else { + $items[] = Html::tag($tag, $item, $options); + } + } + return implode("\n", $items); + } +} diff --git a/extensions/jui/yii/jui/SortableAsset.php b/extensions/jui/yii/jui/SortableAsset.php new file mode 100644 index 0000000..9a19e64 --- /dev/null +++ b/extensions/jui/yii/jui/SortableAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class SortableAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.sortable.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + ); +} diff --git a/extensions/jui/yii/jui/Spinner.php b/extensions/jui/yii/jui/Spinner.php new file mode 100644 index 0000000..29e947e --- /dev/null +++ b/extensions/jui/yii/jui/Spinner.php @@ -0,0 +1,66 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use Yii; +use yii\helpers\Html; + +/** + * Spinner renders an spinner jQuery UI widget. + * + * For example: + * + * ```php + * echo Spinner::widget(array( + * 'model' => $model, + * 'attribute' => 'country', + * 'clientOptions' => array( + * 'step' => 2, + * ), + * )); + * ``` + * + * The following example will use the name property instead: + * + * ```php + * echo Spinner::widget(array( + * 'name' => 'country', + * 'clientOptions' => array( + * 'step' => 2, + * ), + * )); + *``` + * + * @see http://api.jqueryui.com/spinner/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class Spinner extends InputWidget +{ + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderWidget(); + $this->registerWidget('spinner', SpinnerAsset::className()); + } + + /** + * Renders the Spinner widget. + * @return string the rendering result. + */ + public function renderWidget() + { + if ($this->hasModel()) { + return Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + return Html::textInput($this->name, $this->value, $this->options); + } + } +} diff --git a/extensions/jui/yii/jui/SpinnerAsset.php b/extensions/jui/yii/jui/SpinnerAsset.php new file mode 100644 index 0000000..2eb67f8 --- /dev/null +++ b/extensions/jui/yii/jui/SpinnerAsset.php @@ -0,0 +1,25 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class SpinnerAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.spinner.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + 'yii\jui\ButtonAsset', + ); +} diff --git a/extensions/jui/yii/jui/Tabs.php b/extensions/jui/yii/jui/Tabs.php new file mode 100644 index 0000000..7ddf7bb --- /dev/null +++ b/extensions/jui/yii/jui/Tabs.php @@ -0,0 +1,159 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use yii\base\InvalidConfigException; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * Tabs renders a tabs jQuery UI widget. + * + * For example: + * + * ```php + * echo Tabs::widget(array( + * 'items' => array( + * array( + * 'label' => 'Tab one', + * 'content' => 'Mauris mauris ante, blandit et, ultrices a, suscipit eget...', + * ), + * array( + * 'label' => 'Tab two', + * 'content' => 'Sed non urna. Phasellus eu ligula. Vestibulum sit amet purus...', + * 'options' => array( + * 'tag' => 'div', + * ), + * 'headerOptions' => array( + * 'class' => 'my-class', + * ), + * ), + * array( + * 'label' => 'Tab with custom id', + * 'content' => 'Morbi tincidunt, dui sit amet facilisis feugiat...', + * 'options' => array( + * 'id' => 'my-tab', + * ), + * ), + * array( + * 'label' => 'Ajax tab', + * 'url' => array('ajax/content'), + * ), + * ), + * 'options' => array( + * 'tag' => 'div', + * ), + * 'itemOptions' => array( + * 'tag' => 'div', + * ), + * 'headerOptions' => array( + * 'class' => 'my-class', + * ), + * 'clientOptions' => array( + * 'collapsible' => false, + * ), + * )); + * ``` + * + * @see http://api.jqueryui.com/tabs/ + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class Tabs extends Widget +{ + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the container tag of this widget + */ + public $options = array(); + /** + * @var array list of tab items. Each item can be an array of the following structure: + * + * - label: string, required, specifies the header link label. When [[encodeLabels]] is true, the label + * will be HTML-encoded. + * - content: string, the content to show when corresponding tab is clicked. Can be omitted if url is specified. + * - url: mixed, mixed, optional, the url to load tab contents via AJAX. It is required if no content is specified. + * - template: string, optional, the header link template to render the header link. If none specified + * [[linkTemplate]] will be used instead. + * - options: array, optional, the HTML attributes of the header. + * - headerOptions: array, optional, the HTML attributes for the header container tag. + */ + public $items = array(); + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the item container tags. + */ + public $itemOptions = array(); + /** + * @var array list of HTML attributes for the header container tags. This will be overwritten + * by the "headerOptions" set in individual [[items]]. + */ + public $headerOptions = array(); + /** + * @var string the default header template to render the link. + */ + public $linkTemplate = '<a href="{url}">{label}</a>'; + /** + * @var boolean whether the labels for header items should be HTML-encoded. + */ + public $encodeLabels = true; + + + /** + * Renders the widget. + */ + public function run() + { + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'div'); + echo Html::beginTag($tag, $options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag($tag) . "\n"; + $this->registerWidget('tabs', TabsAsset::className()); + } + + /** + * Renders tab items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + protected function renderItems() + { + $headers = array(); + $items = array(); + foreach ($this->items as $n => $item) { + if (!isset($item['label'])) { + throw new InvalidConfigException("The 'label' option is required."); + } + if (isset($item['url'])) { + $url = Html::url($item['url']); + } else { + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' or 'url' option is required."); + } + $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', array())); + $tag = ArrayHelper::remove($options, 'tag', 'div'); + if (!isset($options['id'])) { + $options['id'] = $this->options['id'] . '-tab' . $n; + } + $url = '#' . $options['id']; + $items[] = Html::tag($tag, $item['content'], $options); + } + $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', array())); + $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate); + $headers[] = Html::tag('li', strtr($template, array( + '{label}' => $this->encodeLabels ? Html::encode($item['label']) : $item['label'], + '{url}' => $url, + )), $headerOptions); + } + return Html::tag('ul', implode("\n", $headers)) . "\n" . implode("\n", $items); + } +} diff --git a/extensions/jui/yii/jui/TabsAsset.php b/extensions/jui/yii/jui/TabsAsset.php new file mode 100644 index 0000000..2cdf477 --- /dev/null +++ b/extensions/jui/yii/jui/TabsAsset.php @@ -0,0 +1,25 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class TabsAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.tabs.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + 'yii\jui\EffectAsset', + ); +} diff --git a/extensions/jui/yii/jui/ThemeAsset.php b/extensions/jui/yii/jui/ThemeAsset.php new file mode 100644 index 0000000..2ca0631 --- /dev/null +++ b/extensions/jui/yii/jui/ThemeAsset.php @@ -0,0 +1,21 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ThemeAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $css = array( + 'theme/jquery.ui.css', + ); +} diff --git a/extensions/jui/yii/jui/TooltipAsset.php b/extensions/jui/yii/jui/TooltipAsset.php new file mode 100644 index 0000000..9d6f781 --- /dev/null +++ b/extensions/jui/yii/jui/TooltipAsset.php @@ -0,0 +1,25 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class TooltipAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = array( + 'jquery.ui.tooltip.js', + ); + public $depends = array( + 'yii\jui\CoreAsset', + 'yii\jui\EffectAsset', + ); +} diff --git a/extensions/jui/yii/jui/Widget.php b/extensions/jui/yii/jui/Widget.php new file mode 100644 index 0000000..815bbe9 --- /dev/null +++ b/extensions/jui/yii/jui/Widget.php @@ -0,0 +1,87 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\jui; + +use Yii; +use yii\helpers\Json; + +/** + * \yii\jui\Widget is the base class for all jQuery UI widgets. + * + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class Widget extends \yii\base\Widget +{ + /** + * @var string the jQuery UI theme. This refers to an asset bundle class + * representing the JUI theme. The default theme is the official "Smoothness" theme. + */ + public static $theme = 'yii\jui\ThemeAsset'; + /** + * @var array the HTML attributes for the widget container tag. + */ + public $options = array(); + /** + * @var array the options for the underlying jQuery UI widget. + * Please refer to the corresponding jQuery UI widget Web page for possible options. + * For example, [this page](http://api.jqueryui.com/accordion/) shows + * how to use the "Accordion" widget and the supported options (e.g. "header"). + */ + public $clientOptions = array(); + /** + * @var array the event handlers for the underlying jQuery UI widget. + * Please refer to the corresponding jQuery UI widget Web page for possible events. + * For example, [this page](http://api.jqueryui.com/accordion/) shows + * how to use the "Accordion" widget and the supported events (e.g. "create"). + */ + public $clientEvents = array(); + + + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + } + + /** + * Registers a specific jQuery UI widget and the related events + * @param string $name the name of the jQuery UI widget + * @param string $assetBundle the asset bundle for the widget + */ + protected function registerWidget($name, $assetBundle) + { + $view = $this->getView(); + /** @var \yii\web\AssetBundle $assetBundle */ + $assetBundle::register($view); + /** @var \yii\web\AssetBundle $themeAsset */ + $themeAsset = self::$theme; + $themeAsset::register($view); + + $id = $this->options['id']; + if ($this->clientOptions !== false) { + $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); + $js = "jQuery('#$id').$name($options);"; + $view->registerJs($js); + } + + if (!empty($this->clientEvents)) { + $js = array(); + foreach ($this->clientEvents as $event => $handler) { + $js[] = "jQuery('#$id').on('$name$event', $handler);"; + } + $view->registerJs(implode("\n", $js)); + } + } +} diff --git a/extensions/jui/yii/jui/assets.php b/extensions/jui/yii/jui/assets.php new file mode 100644 index 0000000..e6c5151 --- /dev/null +++ b/extensions/jui/yii/jui/assets.php @@ -0,0 +1,23 @@ +<?php + +return array( + yii\jui\CoreAsset::className(), + yii\jui\EffectAsset::className(), + yii\jui\AccordionAsset::className(), + yii\jui\AutoCompleteAsset::className(), + yii\jui\ButtonAsset::className(), + yii\jui\DatePickerAsset::className(), + yii\jui\DatePickerRegionalAsset::className(), + yii\jui\ProgressBarAsset::className(), + yii\jui\ResizableAsset::className(), + yii\jui\SelectableAsset::className(), + yii\jui\SliderAsset::className(), + yii\jui\SortableAsset::className(), + yii\jui\SpinnerAsset::className(), + yii\jui\TabsAsset::className(), + yii\jui\TooltipAsset::className(), + yii\jui\DialogAsset::className(), + yii\jui\DraggableAsset::className(), + yii\jui\DroppableAsset::className(), + yii\jui\MenuAsset::className(), +); diff --git a/extensions/jui/yii/jui/assets/UPGRADE.md b/extensions/jui/yii/jui/assets/UPGRADE.md new file mode 100644 index 0000000..5cd32d2 --- /dev/null +++ b/extensions/jui/yii/jui/assets/UPGRADE.md @@ -0,0 +1,14 @@ +How to Upgrade JQuery UI +======================== + +To upgrade JQuery UI, use [JUI Download Builder](http://jqueryui.com/download/) and toggle all options. +Choose the `Smoothness` theme, download and unpack. + +The following files are needed: + +* UI Core: all JS files +* Interactions: all JS files +* Widgets: all JS files +* DatePicker I18N: only the combined file is needed, and it should be renamed as `jquery.ui.datepicker-i18n.js` +* Effects: only the combined file is needed, and it should be renamed as `jquery.ui.effect-all.js` +* Theme: Only the combined CSS file and the image files are needed. Rename the CSS file as `jquery.ui.css`. diff --git a/extensions/jui/yii/jui/assets/jquery.ui.accordion.js b/extensions/jui/yii/jui/assets/jquery.ui.accordion.js new file mode 100644 index 0000000..bdd2d53 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.accordion.js @@ -0,0 +1,572 @@ +/*! + * jQuery UI Accordion 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/accordion/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + */ +(function( $, undefined ) { + +var uid = 0, + hideProps = {}, + showProps = {}; + +hideProps.height = hideProps.paddingTop = hideProps.paddingBottom = + hideProps.borderTopWidth = hideProps.borderBottomWidth = "hide"; +showProps.height = showProps.paddingTop = showProps.paddingBottom = + showProps.borderTopWidth = showProps.borderBottomWidth = "show"; + +$.widget( "ui.accordion", { + version: "1.10.3", + options: { + active: 0, + animate: {}, + collapsible: false, + event: "click", + header: "> li > :first-child,> :not(li):even", + heightStyle: "auto", + icons: { + activeHeader: "ui-icon-triangle-1-s", + header: "ui-icon-triangle-1-e" + }, + + // callbacks + activate: null, + beforeActivate: null + }, + + _create: function() { + var options = this.options; + this.prevShow = this.prevHide = $(); + this.element.addClass( "ui-accordion ui-widget ui-helper-reset" ) + // ARIA + .attr( "role", "tablist" ); + + // don't allow collapsible: false and active: false / null + if ( !options.collapsible && (options.active === false || options.active == null) ) { + options.active = 0; + } + + this._processPanels(); + // handle negative values + if ( options.active < 0 ) { + options.active += this.headers.length; + } + this._refresh(); + }, + + _getCreateEventData: function() { + return { + header: this.active, + panel: !this.active.length ? $() : this.active.next(), + content: !this.active.length ? $() : this.active.next() + }; + }, + + _createIcons: function() { + var icons = this.options.icons; + if ( icons ) { + $( "<span>" ) + .addClass( "ui-accordion-header-icon ui-icon " + icons.header ) + .prependTo( this.headers ); + this.active.children( ".ui-accordion-header-icon" ) + .removeClass( icons.header ) + .addClass( icons.activeHeader ); + this.headers.addClass( "ui-accordion-icons" ); + } + }, + + _destroyIcons: function() { + this.headers + .removeClass( "ui-accordion-icons" ) + .children( ".ui-accordion-header-icon" ) + .remove(); + }, + + _destroy: function() { + var contents; + + // clean up main element + this.element + .removeClass( "ui-accordion ui-widget ui-helper-reset" ) + .removeAttr( "role" ); + + // clean up headers + this.headers + .removeClass( "ui-accordion-header ui-accordion-header-active ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top" ) + .removeAttr( "role" ) + .removeAttr( "aria-selected" ) + .removeAttr( "aria-controls" ) + .removeAttr( "tabIndex" ) + .each(function() { + if ( /^ui-accordion/.test( this.id ) ) { + this.removeAttribute( "id" ); + } + }); + this._destroyIcons(); + + // clean up content panels + contents = this.headers.next() + .css( "display", "" ) + .removeAttr( "role" ) + .removeAttr( "aria-expanded" ) + .removeAttr( "aria-hidden" ) + .removeAttr( "aria-labelledby" ) + .removeClass( "ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled" ) + .each(function() { + if ( /^ui-accordion/.test( this.id ) ) { + this.removeAttribute( "id" ); + } + }); + if ( this.options.heightStyle !== "content" ) { + contents.css( "height", "" ); + } + }, + + _setOption: function( key, value ) { + if ( key === "active" ) { + // _activate() will handle invalid values and update this.options + this._activate( value ); + return; + } + + if ( key === "event" ) { + if ( this.options.event ) { + this._off( this.headers, this.options.event ); + } + this._setupEvents( value ); + } + + this._super( key, value ); + + // setting collapsible: false while collapsed; open first panel + if ( key === "collapsible" && !value && this.options.active === false ) { + this._activate( 0 ); + } + + if ( key === "icons" ) { + this._destroyIcons(); + if ( value ) { + this._createIcons(); + } + } + + // #5332 - opacity doesn't cascade to positioned elements in IE + // so we need to add the disabled class to the headers and panels + if ( key === "disabled" ) { + this.headers.add( this.headers.next() ) + .toggleClass( "ui-state-disabled", !!value ); + } + }, + + _keydown: function( event ) { + /*jshint maxcomplexity:15*/ + if ( event.altKey || event.ctrlKey ) { + return; + } + + var keyCode = $.ui.keyCode, + length = this.headers.length, + currentIndex = this.headers.index( event.target ), + toFocus = false; + + switch ( event.keyCode ) { + case keyCode.RIGHT: + case keyCode.DOWN: + toFocus = this.headers[ ( currentIndex + 1 ) % length ]; + break; + case keyCode.LEFT: + case keyCode.UP: + toFocus = this.headers[ ( currentIndex - 1 + length ) % length ]; + break; + case keyCode.SPACE: + case keyCode.ENTER: + this._eventHandler( event ); + break; + case keyCode.HOME: + toFocus = this.headers[ 0 ]; + break; + case keyCode.END: + toFocus = this.headers[ length - 1 ]; + break; + } + + if ( toFocus ) { + $( event.target ).attr( "tabIndex", -1 ); + $( toFocus ).attr( "tabIndex", 0 ); + toFocus.focus(); + event.preventDefault(); + } + }, + + _panelKeyDown : function( event ) { + if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) { + $( event.currentTarget ).prev().focus(); + } + }, + + refresh: function() { + var options = this.options; + this._processPanels(); + + // was collapsed or no panel + if ( ( options.active === false && options.collapsible === true ) || !this.headers.length ) { + options.active = false; + this.active = $(); + // active false only when collapsible is true + } else if ( options.active === false ) { + this._activate( 0 ); + // was active, but active panel is gone + } else if ( this.active.length && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { + // all remaining panel are disabled + if ( this.headers.length === this.headers.find(".ui-state-disabled").length ) { + options.active = false; + this.active = $(); + // activate previous panel + } else { + this._activate( Math.max( 0, options.active - 1 ) ); + } + // was active, active panel still exists + } else { + // make sure active index is correct + options.active = this.headers.index( this.active ); + } + + this._destroyIcons(); + + this._refresh(); + }, + + _processPanels: function() { + this.headers = this.element.find( this.options.header ) + .addClass( "ui-accordion-header ui-helper-reset ui-state-default ui-corner-all" ); + + this.headers.next() + .addClass( "ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom" ) + .filter(":not(.ui-accordion-content-active)") + .hide(); + }, + + _refresh: function() { + var maxHeight, + options = this.options, + heightStyle = options.heightStyle, + parent = this.element.parent(), + accordionId = this.accordionId = "ui-accordion-" + + (this.element.attr( "id" ) || ++uid); + + this.active = this._findActive( options.active ) + .addClass( "ui-accordion-header-active ui-state-active ui-corner-top" ) + .removeClass( "ui-corner-all" ); + this.active.next() + .addClass( "ui-accordion-content-active" ) + .show(); + + this.headers + .attr( "role", "tab" ) + .each(function( i ) { + var header = $( this ), + headerId = header.attr( "id" ), + panel = header.next(), + panelId = panel.attr( "id" ); + if ( !headerId ) { + headerId = accordionId + "-header-" + i; + header.attr( "id", headerId ); + } + if ( !panelId ) { + panelId = accordionId + "-panel-" + i; + panel.attr( "id", panelId ); + } + header.attr( "aria-controls", panelId ); + panel.attr( "aria-labelledby", headerId ); + }) + .next() + .attr( "role", "tabpanel" ); + + this.headers + .not( this.active ) + .attr({ + "aria-selected": "false", + tabIndex: -1 + }) + .next() + .attr({ + "aria-expanded": "false", + "aria-hidden": "true" + }) + .hide(); + + // make sure at least one header is in the tab order + if ( !this.active.length ) { + this.headers.eq( 0 ).attr( "tabIndex", 0 ); + } else { + this.active.attr({ + "aria-selected": "true", + tabIndex: 0 + }) + .next() + .attr({ + "aria-expanded": "true", + "aria-hidden": "false" + }); + } + + this._createIcons(); + + this._setupEvents( options.event ); + + if ( heightStyle === "fill" ) { + maxHeight = parent.height(); + this.element.siblings( ":visible" ).each(function() { + var elem = $( this ), + position = elem.css( "position" ); + + if ( position === "absolute" || position === "fixed" ) { + return; + } + maxHeight -= elem.outerHeight( true ); + }); + + this.headers.each(function() { + maxHeight -= $( this ).outerHeight( true ); + }); + + this.headers.next() + .each(function() { + $( this ).height( Math.max( 0, maxHeight - + $( this ).innerHeight() + $( this ).height() ) ); + }) + .css( "overflow", "auto" ); + } else if ( heightStyle === "auto" ) { + maxHeight = 0; + this.headers.next() + .each(function() { + maxHeight = Math.max( maxHeight, $( this ).css( "height", "" ).height() ); + }) + .height( maxHeight ); + } + }, + + _activate: function( index ) { + var active = this._findActive( index )[ 0 ]; + + // trying to activate the already active panel + if ( active === this.active[ 0 ] ) { + return; + } + + // trying to collapse, simulate a click on the currently active header + active = active || this.active[ 0 ]; + + this._eventHandler({ + target: active, + currentTarget: active, + preventDefault: $.noop + }); + }, + + _findActive: function( selector ) { + return typeof selector === "number" ? this.headers.eq( selector ) : $(); + }, + + _setupEvents: function( event ) { + var events = { + keydown: "_keydown" + }; + if ( event ) { + $.each( event.split(" "), function( index, eventName ) { + events[ eventName ] = "_eventHandler"; + }); + } + + this._off( this.headers.add( this.headers.next() ) ); + this._on( this.headers, events ); + this._on( this.headers.next(), { keydown: "_panelKeyDown" }); + this._hoverable( this.headers ); + this._focusable( this.headers ); + }, + + _eventHandler: function( event ) { + var options = this.options, + active = this.active, + clicked = $( event.currentTarget ), + clickedIsActive = clicked[ 0 ] === active[ 0 ], + collapsing = clickedIsActive && options.collapsible, + toShow = collapsing ? $() : clicked.next(), + toHide = active.next(), + eventData = { + oldHeader: active, + oldPanel: toHide, + newHeader: collapsing ? $() : clicked, + newPanel: toShow + }; + + event.preventDefault(); + + if ( + // click on active header, but not collapsible + ( clickedIsActive && !options.collapsible ) || + // allow canceling activation + ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { + return; + } + + options.active = collapsing ? false : this.headers.index( clicked ); + + // when the call to ._toggle() comes after the class changes + // it causes a very odd bug in IE 8 (see #6720) + this.active = clickedIsActive ? $() : clicked; + this._toggle( eventData ); + + // switch classes + // corner classes on the previously active header stay after the animation + active.removeClass( "ui-accordion-header-active ui-state-active" ); + if ( options.icons ) { + active.children( ".ui-accordion-header-icon" ) + .removeClass( options.icons.activeHeader ) + .addClass( options.icons.header ); + } + + if ( !clickedIsActive ) { + clicked + .removeClass( "ui-corner-all" ) + .addClass( "ui-accordion-header-active ui-state-active ui-corner-top" ); + if ( options.icons ) { + clicked.children( ".ui-accordion-header-icon" ) + .removeClass( options.icons.header ) + .addClass( options.icons.activeHeader ); + } + + clicked + .next() + .addClass( "ui-accordion-content-active" ); + } + }, + + _toggle: function( data ) { + var toShow = data.newPanel, + toHide = this.prevShow.length ? this.prevShow : data.oldPanel; + + // handle activating a panel during the animation for another activation + this.prevShow.add( this.prevHide ).stop( true, true ); + this.prevShow = toShow; + this.prevHide = toHide; + + if ( this.options.animate ) { + this._animate( toShow, toHide, data ); + } else { + toHide.hide(); + toShow.show(); + this._toggleComplete( data ); + } + + toHide.attr({ + "aria-expanded": "false", + "aria-hidden": "true" + }); + toHide.prev().attr( "aria-selected", "false" ); + // if we're switching panels, remove the old header from the tab order + // if we're opening from collapsed state, remove the previous header from the tab order + // if we're collapsing, then keep the collapsing header in the tab order + if ( toShow.length && toHide.length ) { + toHide.prev().attr( "tabIndex", -1 ); + } else if ( toShow.length ) { + this.headers.filter(function() { + return $( this ).attr( "tabIndex" ) === 0; + }) + .attr( "tabIndex", -1 ); + } + + toShow + .attr({ + "aria-expanded": "true", + "aria-hidden": "false" + }) + .prev() + .attr({ + "aria-selected": "true", + tabIndex: 0 + }); + }, + + _animate: function( toShow, toHide, data ) { + var total, easing, duration, + that = this, + adjust = 0, + down = toShow.length && + ( !toHide.length || ( toShow.index() < toHide.index() ) ), + animate = this.options.animate || {}, + options = down && animate.down || animate, + complete = function() { + that._toggleComplete( data ); + }; + + if ( typeof options === "number" ) { + duration = options; + } + if ( typeof options === "string" ) { + easing = options; + } + // fall back from options to animation in case of partial down settings + easing = easing || options.easing || animate.easing; + duration = duration || options.duration || animate.duration; + + if ( !toHide.length ) { + return toShow.animate( showProps, duration, easing, complete ); + } + if ( !toShow.length ) { + return toHide.animate( hideProps, duration, easing, complete ); + } + + total = toShow.show().outerHeight(); + toHide.animate( hideProps, { + duration: duration, + easing: easing, + step: function( now, fx ) { + fx.now = Math.round( now ); + } + }); + toShow + .hide() + .animate( showProps, { + duration: duration, + easing: easing, + complete: complete, + step: function( now, fx ) { + fx.now = Math.round( now ); + if ( fx.prop !== "height" ) { + adjust += fx.now; + } else if ( that.options.heightStyle !== "content" ) { + fx.now = Math.round( total - toHide.outerHeight() - adjust ); + adjust = 0; + } + } + }); + }, + + _toggleComplete: function( data ) { + var toHide = data.oldPanel; + + toHide + .removeClass( "ui-accordion-content-active" ) + .prev() + .removeClass( "ui-corner-top" ) + .addClass( "ui-corner-all" ); + + // Work around for rendering bug in IE (#5421) + if ( toHide.length ) { + toHide.parent()[0].className = toHide.parent()[0].className; + } + + this._trigger( "activate", null, data ); + } +}); + +})( jQuery ); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.autocomplete.js b/extensions/jui/yii/jui/assets/jquery.ui.autocomplete.js new file mode 100644 index 0000000..ca53d2c --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.autocomplete.js @@ -0,0 +1,610 @@ +/*! + * jQuery UI Autocomplete 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/autocomplete/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + * jquery.ui.position.js + * jquery.ui.menu.js + */ +(function( $, undefined ) { + +// used to prevent race conditions with remote data sources +var requestIndex = 0; + +$.widget( "ui.autocomplete", { + version: "1.10.3", + defaultElement: "<input>", + options: { + appendTo: null, + autoFocus: false, + delay: 300, + minLength: 1, + position: { + my: "left top", + at: "left bottom", + collision: "none" + }, + source: null, + + // callbacks + change: null, + close: null, + focus: null, + open: null, + response: null, + search: null, + select: null + }, + + pending: 0, + + _create: function() { + // Some browsers only repeat keydown events, not keypress events, + // so we use the suppressKeyPress flag to determine if we've already + // handled the keydown event. #7269 + // Unfortunately the code for & in keypress is the same as the up arrow, + // so we use the suppressKeyPressRepeat flag to avoid handling keypress + // events when we know the keydown event was used to modify the + // search term. #7799 + var suppressKeyPress, suppressKeyPressRepeat, suppressInput, + nodeName = this.element[0].nodeName.toLowerCase(), + isTextarea = nodeName === "textarea", + isInput = nodeName === "input"; + + this.isMultiLine = + // Textareas are always multi-line + isTextarea ? true : + // Inputs are always single-line, even if inside a contentEditable element + // IE also treats inputs as contentEditable + isInput ? false : + // All other element types are determined by whether or not they're contentEditable + this.element.prop( "isContentEditable" ); + + this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ]; + this.isNewMenu = true; + + this.element + .addClass( "ui-autocomplete-input" ) + .attr( "autocomplete", "off" ); + + this._on( this.element, { + keydown: function( event ) { + /*jshint maxcomplexity:15*/ + if ( this.element.prop( "readOnly" ) ) { + suppressKeyPress = true; + suppressInput = true; + suppressKeyPressRepeat = true; + return; + } + + suppressKeyPress = false; + suppressInput = false; + suppressKeyPressRepeat = false; + var keyCode = $.ui.keyCode; + switch( event.keyCode ) { + case keyCode.PAGE_UP: + suppressKeyPress = true; + this._move( "previousPage", event ); + break; + case keyCode.PAGE_DOWN: + suppressKeyPress = true; + this._move( "nextPage", event ); + break; + case keyCode.UP: + suppressKeyPress = true; + this._keyEvent( "previous", event ); + break; + case keyCode.DOWN: + suppressKeyPress = true; + this._keyEvent( "next", event ); + break; + case keyCode.ENTER: + case keyCode.NUMPAD_ENTER: + // when menu is open and has focus + if ( this.menu.active ) { + // #6055 - Opera still allows the keypress to occur + // which causes forms to submit + suppressKeyPress = true; + event.preventDefault(); + this.menu.select( event ); + } + break; + case keyCode.TAB: + if ( this.menu.active ) { + this.menu.select( event ); + } + break; + case keyCode.ESCAPE: + if ( this.menu.element.is( ":visible" ) ) { + this._value( this.term ); + this.close( event ); + // Different browsers have different default behavior for escape + // Single press can mean undo or clear + // Double press in IE means clear the whole form + event.preventDefault(); + } + break; + default: + suppressKeyPressRepeat = true; + // search timeout should be triggered before the input value is changed + this._searchTimeout( event ); + break; + } + }, + keypress: function( event ) { + if ( suppressKeyPress ) { + suppressKeyPress = false; + if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) { + event.preventDefault(); + } + return; + } + if ( suppressKeyPressRepeat ) { + return; + } + + // replicate some key handlers to allow them to repeat in Firefox and Opera + var keyCode = $.ui.keyCode; + switch( event.keyCode ) { + case keyCode.PAGE_UP: + this._move( "previousPage", event ); + break; + case keyCode.PAGE_DOWN: + this._move( "nextPage", event ); + break; + case keyCode.UP: + this._keyEvent( "previous", event ); + break; + case keyCode.DOWN: + this._keyEvent( "next", event ); + break; + } + }, + input: function( event ) { + if ( suppressInput ) { + suppressInput = false; + event.preventDefault(); + return; + } + this._searchTimeout( event ); + }, + focus: function() { + this.selectedItem = null; + this.previous = this._value(); + }, + blur: function( event ) { + if ( this.cancelBlur ) { + delete this.cancelBlur; + return; + } + + clearTimeout( this.searching ); + this.close( event ); + this._change( event ); + } + }); + + this._initSource(); + this.menu = $( "<ul>" ) + .addClass( "ui-autocomplete ui-front" ) + .appendTo( this._appendTo() ) + .menu({ + // disable ARIA support, the live region takes care of that + role: null + }) + .hide() + .data( "ui-menu" ); + + this._on( this.menu.element, { + mousedown: function( event ) { + // prevent moving focus out of the text field + event.preventDefault(); + + // IE doesn't prevent moving focus even with event.preventDefault() + // so we set a flag to know when we should ignore the blur event + this.cancelBlur = true; + this._delay(function() { + delete this.cancelBlur; + }); + + // clicking on the scrollbar causes focus to shift to the body + // but we can't detect a mouseup or a click immediately afterward + // so we have to track the next mousedown and close the menu if + // the user clicks somewhere outside of the autocomplete + var menuElement = this.menu.element[ 0 ]; + if ( !$( event.target ).closest( ".ui-menu-item" ).length ) { + this._delay(function() { + var that = this; + this.document.one( "mousedown", function( event ) { + if ( event.target !== that.element[ 0 ] && + event.target !== menuElement && + !$.contains( menuElement, event.target ) ) { + that.close(); + } + }); + }); + } + }, + menufocus: function( event, ui ) { + // support: Firefox + // Prevent accidental activation of menu items in Firefox (#7024 #9118) + if ( this.isNewMenu ) { + this.isNewMenu = false; + if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) { + this.menu.blur(); + + this.document.one( "mousemove", function() { + $( event.target ).trigger( event.originalEvent ); + }); + + return; + } + } + + var item = ui.item.data( "ui-autocomplete-item" ); + if ( false !== this._trigger( "focus", event, { item: item } ) ) { + // use value to match what will end up in the input, if it was a key event + if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) { + this._value( item.value ); + } + } else { + // Normally the input is populated with the item's value as the + // menu is navigated, causing screen readers to notice a change and + // announce the item. Since the focus event was canceled, this doesn't + // happen, so we update the live region so that screen readers can + // still notice the change and announce it. + this.liveRegion.text( item.value ); + } + }, + menuselect: function( event, ui ) { + var item = ui.item.data( "ui-autocomplete-item" ), + previous = this.previous; + + // only trigger when focus was lost (click on menu) + if ( this.element[0] !== this.document[0].activeElement ) { + this.element.focus(); + this.previous = previous; + // #6109 - IE triggers two focus events and the second + // is asynchronous, so we need to reset the previous + // term synchronously and asynchronously :-( + this._delay(function() { + this.previous = previous; + this.selectedItem = item; + }); + } + + if ( false !== this._trigger( "select", event, { item: item } ) ) { + this._value( item.value ); + } + // reset the term after the select event + // this allows custom select handling to work properly + this.term = this._value(); + + this.close( event ); + this.selectedItem = item; + } + }); + + this.liveRegion = $( "<span>", { + role: "status", + "aria-live": "polite" + }) + .addClass( "ui-helper-hidden-accessible" ) + .insertBefore( this.element ); + + // turning off autocomplete prevents the browser from remembering the + // value when navigating through history, so we re-enable autocomplete + // if the page is unloaded before the widget is destroyed. #7790 + this._on( this.window, { + beforeunload: function() { + this.element.removeAttr( "autocomplete" ); + } + }); + }, + + _destroy: function() { + clearTimeout( this.searching ); + this.element + .removeClass( "ui-autocomplete-input" ) + .removeAttr( "autocomplete" ); + this.menu.element.remove(); + this.liveRegion.remove(); + }, + + _setOption: function( key, value ) { + this._super( key, value ); + if ( key === "source" ) { + this._initSource(); + } + if ( key === "appendTo" ) { + this.menu.element.appendTo( this._appendTo() ); + } + if ( key === "disabled" && value && this.xhr ) { + this.xhr.abort(); + } + }, + + _appendTo: function() { + var element = this.options.appendTo; + + if ( element ) { + element = element.jquery || element.nodeType ? + $( element ) : + this.document.find( element ).eq( 0 ); + } + + if ( !element ) { + element = this.element.closest( ".ui-front" ); + } + + if ( !element.length ) { + element = this.document[0].body; + } + + return element; + }, + + _initSource: function() { + var array, url, + that = this; + if ( $.isArray(this.options.source) ) { + array = this.options.source; + this.source = function( request, response ) { + response( $.ui.autocomplete.filter( array, request.term ) ); + }; + } else if ( typeof this.options.source === "string" ) { + url = this.options.source; + this.source = function( request, response ) { + if ( that.xhr ) { + that.xhr.abort(); + } + that.xhr = $.ajax({ + url: url, + data: request, + dataType: "json", + success: function( data ) { + response( data ); + }, + error: function() { + response( [] ); + } + }); + }; + } else { + this.source = this.options.source; + } + }, + + _searchTimeout: function( event ) { + clearTimeout( this.searching ); + this.searching = this._delay(function() { + // only search if the value has changed + if ( this.term !== this._value() ) { + this.selectedItem = null; + this.search( null, event ); + } + }, this.options.delay ); + }, + + search: function( value, event ) { + value = value != null ? value : this._value(); + + // always save the actual value, not the one passed as an argument + this.term = this._value(); + + if ( value.length < this.options.minLength ) { + return this.close( event ); + } + + if ( this._trigger( "search", event ) === false ) { + return; + } + + return this._search( value ); + }, + + _search: function( value ) { + this.pending++; + this.element.addClass( "ui-autocomplete-loading" ); + this.cancelSearch = false; + + this.source( { term: value }, this._response() ); + }, + + _response: function() { + var that = this, + index = ++requestIndex; + + return function( content ) { + if ( index === requestIndex ) { + that.__response( content ); + } + + that.pending--; + if ( !that.pending ) { + that.element.removeClass( "ui-autocomplete-loading" ); + } + }; + }, + + __response: function( content ) { + if ( content ) { + content = this._normalize( content ); + } + this._trigger( "response", null, { content: content } ); + if ( !this.options.disabled && content && content.length && !this.cancelSearch ) { + this._suggest( content ); + this._trigger( "open" ); + } else { + // use ._close() instead of .close() so we don't cancel future searches + this._close(); + } + }, + + close: function( event ) { + this.cancelSearch = true; + this._close( event ); + }, + + _close: function( event ) { + if ( this.menu.element.is( ":visible" ) ) { + this.menu.element.hide(); + this.menu.blur(); + this.isNewMenu = true; + this._trigger( "close", event ); + } + }, + + _change: function( event ) { + if ( this.previous !== this._value() ) { + this._trigger( "change", event, { item: this.selectedItem } ); + } + }, + + _normalize: function( items ) { + // assume all items have the right format when the first item is complete + if ( items.length && items[0].label && items[0].value ) { + return items; + } + return $.map( items, function( item ) { + if ( typeof item === "string" ) { + return { + label: item, + value: item + }; + } + return $.extend({ + label: item.label || item.value, + value: item.value || item.label + }, item ); + }); + }, + + _suggest: function( items ) { + var ul = this.menu.element.empty(); + this._renderMenu( ul, items ); + this.isNewMenu = true; + this.menu.refresh(); + + // size and position menu + ul.show(); + this._resizeMenu(); + ul.position( $.extend({ + of: this.element + }, this.options.position )); + + if ( this.options.autoFocus ) { + this.menu.next(); + } + }, + + _resizeMenu: function() { + var ul = this.menu.element; + ul.outerWidth( Math.max( + // Firefox wraps long text (possibly a rounding bug) + // so we add 1px to avoid the wrapping (#7513) + ul.width( "" ).outerWidth() + 1, + this.element.outerWidth() + ) ); + }, + + _renderMenu: function( ul, items ) { + var that = this; + $.each( items, function( index, item ) { + that._renderItemData( ul, item ); + }); + }, + + _renderItemData: function( ul, item ) { + return this._renderItem( ul, item ).data( "ui-autocomplete-item", item ); + }, + + _renderItem: function( ul, item ) { + return $( "<li>" ) + .append( $( "<a>" ).text( item.label ) ) + .appendTo( ul ); + }, + + _move: function( direction, event ) { + if ( !this.menu.element.is( ":visible" ) ) { + this.search( null, event ); + return; + } + if ( this.menu.isFirstItem() && /^previous/.test( direction ) || + this.menu.isLastItem() && /^next/.test( direction ) ) { + this._value( this.term ); + this.menu.blur(); + return; + } + this.menu[ direction ]( event ); + }, + + widget: function() { + return this.menu.element; + }, + + _value: function() { + return this.valueMethod.apply( this.element, arguments ); + }, + + _keyEvent: function( keyEvent, event ) { + if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) { + this._move( keyEvent, event ); + + // prevents moving cursor to beginning/end of the text field in some browsers + event.preventDefault(); + } + } +}); + +$.extend( $.ui.autocomplete, { + escapeRegex: function( value ) { + return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"); + }, + filter: function(array, term) { + var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); + return $.grep( array, function(value) { + return matcher.test( value.label || value.value || value ); + }); + } +}); + + +// live region extension, adding a `messages` option +// NOTE: This is an experimental API. We are still investigating +// a full solution for string manipulation and internationalization. +$.widget( "ui.autocomplete", $.ui.autocomplete, { + options: { + messages: { + noResults: "No search results.", + results: function( amount ) { + return amount + ( amount > 1 ? " results are" : " result is" ) + + " available, use up and down arrow keys to navigate."; + } + } + }, + + __response: function( content ) { + var message; + this._superApply( arguments ); + if ( this.options.disabled || this.cancelSearch ) { + return; + } + if ( content && content.length ) { + message = this.options.messages.results( content.length ); + } else { + message = this.options.messages.noResults; + } + this.liveRegion.text( message ); + } +}); + +}( jQuery )); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.button.js b/extensions/jui/yii/jui/assets/jquery.ui.button.js new file mode 100644 index 0000000..5926642 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.button.js @@ -0,0 +1,419 @@ +/*! + * jQuery UI Button 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/button/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + */ +(function( $, undefined ) { + +var lastActive, startXPos, startYPos, clickDragged, + baseClasses = "ui-button ui-widget ui-state-default ui-corner-all", + stateClasses = "ui-state-hover ui-state-active ", + typeClasses = "ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only", + formResetHandler = function() { + var form = $( this ); + setTimeout(function() { + form.find( ":ui-button" ).button( "refresh" ); + }, 1 ); + }, + radioGroup = function( radio ) { + var name = radio.name, + form = radio.form, + radios = $( [] ); + if ( name ) { + name = name.replace( /'/g, "\\'" ); + if ( form ) { + radios = $( form ).find( "[name='" + name + "']" ); + } else { + radios = $( "[name='" + name + "']", radio.ownerDocument ) + .filter(function() { + return !this.form; + }); + } + } + return radios; + }; + +$.widget( "ui.button", { + version: "1.10.3", + defaultElement: "<button>", + options: { + disabled: null, + text: true, + label: null, + icons: { + primary: null, + secondary: null + } + }, + _create: function() { + this.element.closest( "form" ) + .unbind( "reset" + this.eventNamespace ) + .bind( "reset" + this.eventNamespace, formResetHandler ); + + if ( typeof this.options.disabled !== "boolean" ) { + this.options.disabled = !!this.element.prop( "disabled" ); + } else { + this.element.prop( "disabled", this.options.disabled ); + } + + this._determineButtonType(); + this.hasTitle = !!this.buttonElement.attr( "title" ); + + var that = this, + options = this.options, + toggleButton = this.type === "checkbox" || this.type === "radio", + activeClass = !toggleButton ? "ui-state-active" : "", + focusClass = "ui-state-focus"; + + if ( options.label === null ) { + options.label = (this.type === "input" ? this.buttonElement.val() : this.buttonElement.html()); + } + + this._hoverable( this.buttonElement ); + + this.buttonElement + .addClass( baseClasses ) + .attr( "role", "button" ) + .bind( "mouseenter" + this.eventNamespace, function() { + if ( options.disabled ) { + return; + } + if ( this === lastActive ) { + $( this ).addClass( "ui-state-active" ); + } + }) + .bind( "mouseleave" + this.eventNamespace, function() { + if ( options.disabled ) { + return; + } + $( this ).removeClass( activeClass ); + }) + .bind( "click" + this.eventNamespace, function( event ) { + if ( options.disabled ) { + event.preventDefault(); + event.stopImmediatePropagation(); + } + }); + + this.element + .bind( "focus" + this.eventNamespace, function() { + // no need to check disabled, focus won't be triggered anyway + that.buttonElement.addClass( focusClass ); + }) + .bind( "blur" + this.eventNamespace, function() { + that.buttonElement.removeClass( focusClass ); + }); + + if ( toggleButton ) { + this.element.bind( "change" + this.eventNamespace, function() { + if ( clickDragged ) { + return; + } + that.refresh(); + }); + // if mouse moves between mousedown and mouseup (drag) set clickDragged flag + // prevents issue where button state changes but checkbox/radio checked state + // does not in Firefox (see ticket #6970) + this.buttonElement + .bind( "mousedown" + this.eventNamespace, function( event ) { + if ( options.disabled ) { + return; + } + clickDragged = false; + startXPos = event.pageX; + startYPos = event.pageY; + }) + .bind( "mouseup" + this.eventNamespace, function( event ) { + if ( options.disabled ) { + return; + } + if ( startXPos !== event.pageX || startYPos !== event.pageY ) { + clickDragged = true; + } + }); + } + + if ( this.type === "checkbox" ) { + this.buttonElement.bind( "click" + this.eventNamespace, function() { + if ( options.disabled || clickDragged ) { + return false; + } + }); + } else if ( this.type === "radio" ) { + this.buttonElement.bind( "click" + this.eventNamespace, function() { + if ( options.disabled || clickDragged ) { + return false; + } + $( this ).addClass( "ui-state-active" ); + that.buttonElement.attr( "aria-pressed", "true" ); + + var radio = that.element[ 0 ]; + radioGroup( radio ) + .not( radio ) + .map(function() { + return $( this ).button( "widget" )[ 0 ]; + }) + .removeClass( "ui-state-active" ) + .attr( "aria-pressed", "false" ); + }); + } else { + this.buttonElement + .bind( "mousedown" + this.eventNamespace, function() { + if ( options.disabled ) { + return false; + } + $( this ).addClass( "ui-state-active" ); + lastActive = this; + that.document.one( "mouseup", function() { + lastActive = null; + }); + }) + .bind( "mouseup" + this.eventNamespace, function() { + if ( options.disabled ) { + return false; + } + $( this ).removeClass( "ui-state-active" ); + }) + .bind( "keydown" + this.eventNamespace, function(event) { + if ( options.disabled ) { + return false; + } + if ( event.keyCode === $.ui.keyCode.SPACE || event.keyCode === $.ui.keyCode.ENTER ) { + $( this ).addClass( "ui-state-active" ); + } + }) + // see #8559, we bind to blur here in case the button element loses + // focus between keydown and keyup, it would be left in an "active" state + .bind( "keyup" + this.eventNamespace + " blur" + this.eventNamespace, function() { + $( this ).removeClass( "ui-state-active" ); + }); + + if ( this.buttonElement.is("a") ) { + this.buttonElement.keyup(function(event) { + if ( event.keyCode === $.ui.keyCode.SPACE ) { + // TODO pass through original event correctly (just as 2nd argument doesn't work) + $( this ).click(); + } + }); + } + } + + // TODO: pull out $.Widget's handling for the disabled option into + // $.Widget.prototype._setOptionDisabled so it's easy to proxy and can + // be overridden by individual plugins + this._setOption( "disabled", options.disabled ); + this._resetButton(); + }, + + _determineButtonType: function() { + var ancestor, labelSelector, checked; + + if ( this.element.is("[type=checkbox]") ) { + this.type = "checkbox"; + } else if ( this.element.is("[type=radio]") ) { + this.type = "radio"; + } else if ( this.element.is("input") ) { + this.type = "input"; + } else { + this.type = "button"; + } + + if ( this.type === "checkbox" || this.type === "radio" ) { + // we don't search against the document in case the element + // is disconnected from the DOM + ancestor = this.element.parents().last(); + labelSelector = "label[for='" + this.element.attr("id") + "']"; + this.buttonElement = ancestor.find( labelSelector ); + if ( !this.buttonElement.length ) { + ancestor = ancestor.length ? ancestor.siblings() : this.element.siblings(); + this.buttonElement = ancestor.filter( labelSelector ); + if ( !this.buttonElement.length ) { + this.buttonElement = ancestor.find( labelSelector ); + } + } + this.element.addClass( "ui-helper-hidden-accessible" ); + + checked = this.element.is( ":checked" ); + if ( checked ) { + this.buttonElement.addClass( "ui-state-active" ); + } + this.buttonElement.prop( "aria-pressed", checked ); + } else { + this.buttonElement = this.element; + } + }, + + widget: function() { + return this.buttonElement; + }, + + _destroy: function() { + this.element + .removeClass( "ui-helper-hidden-accessible" ); + this.buttonElement + .removeClass( baseClasses + " " + stateClasses + " " + typeClasses ) + .removeAttr( "role" ) + .removeAttr( "aria-pressed" ) + .html( this.buttonElement.find(".ui-button-text").html() ); + + if ( !this.hasTitle ) { + this.buttonElement.removeAttr( "title" ); + } + }, + + _setOption: function( key, value ) { + this._super( key, value ); + if ( key === "disabled" ) { + if ( value ) { + this.element.prop( "disabled", true ); + } else { + this.element.prop( "disabled", false ); + } + return; + } + this._resetButton(); + }, + + refresh: function() { + //See #8237 & #8828 + var isDisabled = this.element.is( "input, button" ) ? this.element.is( ":disabled" ) : this.element.hasClass( "ui-button-disabled" ); + + if ( isDisabled !== this.options.disabled ) { + this._setOption( "disabled", isDisabled ); + } + if ( this.type === "radio" ) { + radioGroup( this.element[0] ).each(function() { + if ( $( this ).is( ":checked" ) ) { + $( this ).button( "widget" ) + .addClass( "ui-state-active" ) + .attr( "aria-pressed", "true" ); + } else { + $( this ).button( "widget" ) + .removeClass( "ui-state-active" ) + .attr( "aria-pressed", "false" ); + } + }); + } else if ( this.type === "checkbox" ) { + if ( this.element.is( ":checked" ) ) { + this.buttonElement + .addClass( "ui-state-active" ) + .attr( "aria-pressed", "true" ); + } else { + this.buttonElement + .removeClass( "ui-state-active" ) + .attr( "aria-pressed", "false" ); + } + } + }, + + _resetButton: function() { + if ( this.type === "input" ) { + if ( this.options.label ) { + this.element.val( this.options.label ); + } + return; + } + var buttonElement = this.buttonElement.removeClass( typeClasses ), + buttonText = $( "<span></span>", this.document[0] ) + .addClass( "ui-button-text" ) + .html( this.options.label ) + .appendTo( buttonElement.empty() ) + .text(), + icons = this.options.icons, + multipleIcons = icons.primary && icons.secondary, + buttonClasses = []; + + if ( icons.primary || icons.secondary ) { + if ( this.options.text ) { + buttonClasses.push( "ui-button-text-icon" + ( multipleIcons ? "s" : ( icons.primary ? "-primary" : "-secondary" ) ) ); + } + + if ( icons.primary ) { + buttonElement.prepend( "<span class='ui-button-icon-primary ui-icon " + icons.primary + "'></span>" ); + } + + if ( icons.secondary ) { + buttonElement.append( "<span class='ui-button-icon-secondary ui-icon " + icons.secondary + "'></span>" ); + } + + if ( !this.options.text ) { + buttonClasses.push( multipleIcons ? "ui-button-icons-only" : "ui-button-icon-only" ); + + if ( !this.hasTitle ) { + buttonElement.attr( "title", $.trim( buttonText ) ); + } + } + } else { + buttonClasses.push( "ui-button-text-only" ); + } + buttonElement.addClass( buttonClasses.join( " " ) ); + } +}); + +$.widget( "ui.buttonset", { + version: "1.10.3", + options: { + items: "button, input[type=button], input[type=submit], input[type=reset], input[type=checkbox], input[type=radio], a, :data(ui-button)" + }, + + _create: function() { + this.element.addClass( "ui-buttonset" ); + }, + + _init: function() { + this.refresh(); + }, + + _setOption: function( key, value ) { + if ( key === "disabled" ) { + this.buttons.button( "option", key, value ); + } + + this._super( key, value ); + }, + + refresh: function() { + var rtl = this.element.css( "direction" ) === "rtl"; + + this.buttons = this.element.find( this.options.items ) + .filter( ":ui-button" ) + .button( "refresh" ) + .end() + .not( ":ui-button" ) + .button() + .end() + .map(function() { + return $( this ).button( "widget" )[ 0 ]; + }) + .removeClass( "ui-corner-all ui-corner-left ui-corner-right" ) + .filter( ":first" ) + .addClass( rtl ? "ui-corner-right" : "ui-corner-left" ) + .end() + .filter( ":last" ) + .addClass( rtl ? "ui-corner-left" : "ui-corner-right" ) + .end() + .end(); + }, + + _destroy: function() { + this.element.removeClass( "ui-buttonset" ); + this.buttons + .map(function() { + return $( this ).button( "widget" )[ 0 ]; + }) + .removeClass( "ui-corner-left ui-corner-right" ) + .end() + .button( "destroy" ); + } +}); + +}( jQuery ) ); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.core.js b/extensions/jui/yii/jui/assets/jquery.ui.core.js new file mode 100644 index 0000000..91ca5ff --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.core.js @@ -0,0 +1,320 @@ +/*! + * jQuery UI Core 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/category/ui-core/ + */ +(function( $, undefined ) { + +var uuid = 0, + runiqueId = /^ui-id-\d+$/; + +// $.ui might exist from components with no dependencies, e.g., $.ui.position +$.ui = $.ui || {}; + +$.extend( $.ui, { + version: "1.10.3", + + keyCode: { + BACKSPACE: 8, + COMMA: 188, + DELETE: 46, + DOWN: 40, + END: 35, + ENTER: 13, + ESCAPE: 27, + HOME: 36, + LEFT: 37, + NUMPAD_ADD: 107, + NUMPAD_DECIMAL: 110, + NUMPAD_DIVIDE: 111, + NUMPAD_ENTER: 108, + NUMPAD_MULTIPLY: 106, + NUMPAD_SUBTRACT: 109, + PAGE_DOWN: 34, + PAGE_UP: 33, + PERIOD: 190, + RIGHT: 39, + SPACE: 32, + TAB: 9, + UP: 38 + } +}); + +// plugins +$.fn.extend({ + focus: (function( orig ) { + return function( delay, fn ) { + return typeof delay === "number" ? + this.each(function() { + var elem = this; + setTimeout(function() { + $( elem ).focus(); + if ( fn ) { + fn.call( elem ); + } + }, delay ); + }) : + orig.apply( this, arguments ); + }; + })( $.fn.focus ), + + scrollParent: function() { + var scrollParent; + if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) { + scrollParent = this.parents().filter(function() { + return (/(relative|absolute|fixed)/).test($.css(this,"position")) && (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); + }).eq(0); + } else { + scrollParent = this.parents().filter(function() { + return (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); + }).eq(0); + } + + return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent; + }, + + zIndex: function( zIndex ) { + if ( zIndex !== undefined ) { + return this.css( "zIndex", zIndex ); + } + + if ( this.length ) { + var elem = $( this[ 0 ] ), position, value; + while ( elem.length && elem[ 0 ] !== document ) { + // Ignore z-index if position is set to a value where z-index is ignored by the browser + // This makes behavior of this function consistent across browsers + // WebKit always returns auto if the element is positioned + position = elem.css( "position" ); + if ( position === "absolute" || position === "relative" || position === "fixed" ) { + // IE returns 0 when zIndex is not specified + // other browsers return a string + // we ignore the case of nested elements with an explicit value of 0 + // <div style="z-index: -10;"><div style="z-index: 0;"></div></div> + value = parseInt( elem.css( "zIndex" ), 10 ); + if ( !isNaN( value ) && value !== 0 ) { + return value; + } + } + elem = elem.parent(); + } + } + + return 0; + }, + + uniqueId: function() { + return this.each(function() { + if ( !this.id ) { + this.id = "ui-id-" + (++uuid); + } + }); + }, + + removeUniqueId: function() { + return this.each(function() { + if ( runiqueId.test( this.id ) ) { + $( this ).removeAttr( "id" ); + } + }); + } +}); + +// selectors +function focusable( element, isTabIndexNotNaN ) { + var map, mapName, img, + nodeName = element.nodeName.toLowerCase(); + if ( "area" === nodeName ) { + map = element.parentNode; + mapName = map.name; + if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { + return false; + } + img = $( "img[usemap=#" + mapName + "]" )[0]; + return !!img && visible( img ); + } + return ( /input|select|textarea|button|object/.test( nodeName ) ? + !element.disabled : + "a" === nodeName ? + element.href || isTabIndexNotNaN : + isTabIndexNotNaN) && + // the element and all of its ancestors must be visible + visible( element ); +} + +function visible( element ) { + return $.expr.filters.visible( element ) && + !$( element ).parents().addBack().filter(function() { + return $.css( this, "visibility" ) === "hidden"; + }).length; +} + +$.extend( $.expr[ ":" ], { + data: $.expr.createPseudo ? + $.expr.createPseudo(function( dataName ) { + return function( elem ) { + return !!$.data( elem, dataName ); + }; + }) : + // support: jQuery <1.8 + function( elem, i, match ) { + return !!$.data( elem, match[ 3 ] ); + }, + + focusable: function( element ) { + return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); + }, + + tabbable: function( element ) { + var tabIndex = $.attr( element, "tabindex" ), + isTabIndexNaN = isNaN( tabIndex ); + return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); + } +}); + +// support: jQuery <1.8 +if ( !$( "<a>" ).outerWidth( 1 ).jquery ) { + $.each( [ "Width", "Height" ], function( i, name ) { + var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], + type = name.toLowerCase(), + orig = { + innerWidth: $.fn.innerWidth, + innerHeight: $.fn.innerHeight, + outerWidth: $.fn.outerWidth, + outerHeight: $.fn.outerHeight + }; + + function reduce( elem, size, border, margin ) { + $.each( side, function() { + size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; + if ( border ) { + size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; + } + if ( margin ) { + size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; + } + }); + return size; + } + + $.fn[ "inner" + name ] = function( size ) { + if ( size === undefined ) { + return orig[ "inner" + name ].call( this ); + } + + return this.each(function() { + $( this ).css( type, reduce( this, size ) + "px" ); + }); + }; + + $.fn[ "outer" + name] = function( size, margin ) { + if ( typeof size !== "number" ) { + return orig[ "outer" + name ].call( this, size ); + } + + return this.each(function() { + $( this).css( type, reduce( this, size, true, margin ) + "px" ); + }); + }; + }); +} + +// support: jQuery <1.8 +if ( !$.fn.addBack ) { + $.fn.addBack = function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + }; +} + +// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) +if ( $( "<a>" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { + $.fn.removeData = (function( removeData ) { + return function( key ) { + if ( arguments.length ) { + return removeData.call( this, $.camelCase( key ) ); + } else { + return removeData.call( this ); + } + }; + })( $.fn.removeData ); +} + + + + + +// deprecated +$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); + +$.support.selectstart = "onselectstart" in document.createElement( "div" ); +$.fn.extend({ + disableSelection: function() { + return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) + + ".ui-disableSelection", function( event ) { + event.preventDefault(); + }); + }, + + enableSelection: function() { + return this.unbind( ".ui-disableSelection" ); + } +}); + +$.extend( $.ui, { + // $.ui.plugin is deprecated. Use $.widget() extensions instead. + plugin: { + add: function( module, option, set ) { + var i, + proto = $.ui[ module ].prototype; + for ( i in set ) { + proto.plugins[ i ] = proto.plugins[ i ] || []; + proto.plugins[ i ].push( [ option, set[ i ] ] ); + } + }, + call: function( instance, name, args ) { + var i, + set = instance.plugins[ name ]; + if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) { + return; + } + + for ( i = 0; i < set.length; i++ ) { + if ( instance.options[ set[ i ][ 0 ] ] ) { + set[ i ][ 1 ].apply( instance.element, args ); + } + } + } + }, + + // only used by resizable + hasScroll: function( el, a ) { + + //If overflow is hidden, the element might have extra content, but the user wants to hide it + if ( $( el ).css( "overflow" ) === "hidden") { + return false; + } + + var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop", + has = false; + + if ( el[ scroll ] > 0 ) { + return true; + } + + // TODO: determine which cases actually cause this to happen + // if the element doesn't have the scroll set, see if it's possible to + // set the scroll + el[ scroll ] = 1; + has = ( el[ scroll ] > 0 ); + el[ scroll ] = 0; + return has; + } +}); + +})( jQuery ); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.datepicker-i18n.js b/extensions/jui/yii/jui/assets/jquery.ui.datepicker-i18n.js new file mode 100755 index 0000000..d3dcc41 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.datepicker-i18n.js @@ -0,0 +1,1793 @@ +/*! jQuery UI - v1.10.3 - 2013-06-12 +* http://jqueryui.com +* Includes: jquery.ui.datepicker-af.js, jquery.ui.datepicker-ar-DZ.js, jquery.ui.datepicker-ar.js, jquery.ui.datepicker-az.js, jquery.ui.datepicker-be.js, jquery.ui.datepicker-bg.js, jquery.ui.datepicker-bs.js, jquery.ui.datepicker-ca.js, jquery.ui.datepicker-cs.js, jquery.ui.datepicker-cy-GB.js, jquery.ui.datepicker-da.js, jquery.ui.datepicker-de.js, jquery.ui.datepicker-el.js, jquery.ui.datepicker-en-AU.js, jquery.ui.datepicker-en-GB.js, jquery.ui.datepicker-en-NZ.js, jquery.ui.datepicker-eo.js, jquery.ui.datepicker-es.js, jquery.ui.datepicker-et.js, jquery.ui.datepicker-eu.js, jquery.ui.datepicker-fa.js, jquery.ui.datepicker-fi.js, jquery.ui.datepicker-fo.js, jquery.ui.datepicker-fr-CA.js, jquery.ui.datepicker-fr-CH.js, jquery.ui.datepicker-fr.js, jquery.ui.datepicker-gl.js, jquery.ui.datepicker-he.js, jquery.ui.datepicker-hi.js, jquery.ui.datepicker-hr.js, jquery.ui.datepicker-hu.js, jquery.ui.datepicker-hy.js, jquery.ui.datepicker-id.js, jquery.ui.datepicker-is.js, jquery.ui.datepicker-it.js, jquery.ui.datepicker-ja.js, jquery.ui.datepicker-ka.js, jquery.ui.datepicker-kk.js, jquery.ui.datepicker-km.js, jquery.ui.datepicker-ko.js, jquery.ui.datepicker-ky.js, jquery.ui.datepicker-lb.js, jquery.ui.datepicker-lt.js, jquery.ui.datepicker-lv.js, jquery.ui.datepicker-mk.js, jquery.ui.datepicker-ml.js, jquery.ui.datepicker-ms.js, jquery.ui.datepicker-nb.js, jquery.ui.datepicker-nl-BE.js, jquery.ui.datepicker-nl.js, jquery.ui.datepicker-nn.js, jquery.ui.datepicker-no.js, jquery.ui.datepicker-pl.js, jquery.ui.datepicker-pt-BR.js, jquery.ui.datepicker-pt.js, jquery.ui.datepicker-rm.js, jquery.ui.datepicker-ro.js, jquery.ui.datepicker-ru.js, jquery.ui.datepicker-sk.js, jquery.ui.datepicker-sl.js, jquery.ui.datepicker-sq.js, jquery.ui.datepicker-sr-SR.js, jquery.ui.datepicker-sr.js, jquery.ui.datepicker-sv.js, jquery.ui.datepicker-ta.js, jquery.ui.datepicker-th.js, jquery.ui.datepicker-tj.js, jquery.ui.datepicker-tr.js, jquery.ui.datepicker-uk.js, jquery.ui.datepicker-vi.js, jquery.ui.datepicker-zh-CN.js, jquery.ui.datepicker-zh-HK.js, jquery.ui.datepicker-zh-TW.js +* Copyright 2013 jQuery Foundation and other contributors; Licensed MIT */ +/* Afrikaans initialisation for the jQuery UI date picker plugin. */ +/* Written by Renier Pretorius. */ +jQuery(function($){ + $.datepicker.regional['af'] = { + closeText: 'Selekteer', + prevText: 'Vorige', + nextText: 'Volgende', + currentText: 'Vandag', + monthNames: ['Januarie','Februarie','Maart','April','Mei','Junie', + 'Julie','Augustus','September','Oktober','November','Desember'], + monthNamesShort: ['Jan', 'Feb', 'Mrt', 'Apr', 'Mei', 'Jun', + 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'], + dayNames: ['Sondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrydag', 'Saterdag'], + dayNamesShort: ['Son', 'Maa', 'Din', 'Woe', 'Don', 'Vry', 'Sat'], + dayNamesMin: ['So','Ma','Di','Wo','Do','Vr','Sa'], + weekHeader: 'Wk', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['af']); +}); + +/* Algerian Arabic Translation for jQuery UI date picker plugin. (can be used for Tunisia)*/ +/* Mohamed Cherif BOUCHELAGHEM -- cherifbouchelaghem@yahoo.fr */ + +jQuery(function($){ + $.datepicker.regional['ar-DZ'] = { + closeText: 'إغلاق', + prevText: '<السابق', + nextText: 'التالي>', + currentText: 'اليوم', + monthNames: ['جانفي', 'فيفري', 'مارس', 'أفريل', 'ماي', 'جوان', + 'جويلية', 'أوت', 'سبتمبر','أكتوبر', 'نوفمبر', 'ديسمبر'], + monthNamesShort: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'], + dayNames: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'], + dayNamesShort: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'], + dayNamesMin: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'], + weekHeader: 'أسبوع', + dateFormat: 'dd/mm/yy', + firstDay: 6, + isRTL: true, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['ar-DZ']); +}); + +/* Arabic Translation for jQuery UI date picker plugin. */ +/* Khaled Alhourani -- me@khaledalhourani.com */ +/* NOTE: monthNames are the original months names and they are the Arabic names, not the new months name فبراير - يناير and there isn't any Arabic roots for these months */ +jQuery(function($){ + $.datepicker.regional['ar'] = { + closeText: 'إغلاق', + prevText: '<السابق', + nextText: 'التالي>', + currentText: 'اليوم', + monthNames: ['كانون الثاني', 'شباط', 'آذار', 'نيسان', 'مايو', 'حزيران', + 'تموز', 'آب', 'أيلول', 'تشرين الأول', 'تشرين الثاني', 'كانون الأول'], + monthNamesShort: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'], + dayNames: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'], + dayNamesShort: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'], + dayNamesMin: ['ح', 'ن', 'ث', 'ر', 'خ', 'ج', 'س'], + weekHeader: 'أسبوع', + dateFormat: 'dd/mm/yy', + firstDay: 6, + isRTL: true, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['ar']); +}); + +/* Azerbaijani (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Jamil Najafov (necefov33@gmail.com). */ +jQuery(function($) { + $.datepicker.regional['az'] = { + closeText: 'Bağla', + prevText: '<Geri', + nextText: 'İrəli>', + currentText: 'Bugün', + monthNames: ['Yanvar','Fevral','Mart','Aprel','May','İyun', + 'İyul','Avqust','Sentyabr','Oktyabr','Noyabr','Dekabr'], + monthNamesShort: ['Yan','Fev','Mar','Apr','May','İyun', + 'İyul','Avq','Sen','Okt','Noy','Dek'], + dayNames: ['Bazar','Bazar ertəsi','Çərşənbə axşamı','Çərşənbə','Cümə axşamı','Cümə','Şənbə'], + dayNamesShort: ['B','Be','Ça','Ç','Ca','C','Ş'], + dayNamesMin: ['B','B','Ç','С','Ç','C','Ş'], + weekHeader: 'Hf', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['az']); +}); + +/* Belarusian initialisation for the jQuery UI date picker plugin. */ +/* Written by Pavel Selitskas <p.selitskas@gmail.com> */ +jQuery(function($){ + $.datepicker.regional['be'] = { + closeText: 'Зачыніць', + prevText: '←Папяр.', + nextText: 'Наст.→', + currentText: 'Сёньня', + monthNames: ['Студзень','Люты','Сакавік','Красавік','Травень','Чэрвень', + 'Ліпень','Жнівень','Верасень','Кастрычнік','Лістапад','Сьнежань'], + monthNamesShort: ['Сту','Лют','Сак','Кра','Тра','Чэр', + 'Ліп','Жні','Вер','Кас','Ліс','Сьн'], + dayNames: ['нядзеля','панядзелак','аўторак','серада','чацьвер','пятніца','субота'], + dayNamesShort: ['ндз','пнд','аўт','срд','чцв','птн','сбт'], + dayNamesMin: ['Нд','Пн','Аў','Ср','Чц','Пт','Сб'], + weekHeader: 'Тд', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['be']); +}); + +/* Bulgarian initialisation for the jQuery UI date picker plugin. */ +/* Written by Stoyan Kyosev (http://svest.org). */ +jQuery(function($){ + $.datepicker.regional['bg'] = { + closeText: 'затвори', + prevText: '<назад', + nextText: 'напред>', + nextBigText: '>>', + currentText: 'днес', + monthNames: ['Януари','Февруари','Март','Април','Май','Юни', + 'Юли','Август','Септември','Октомври','Ноември','Декември'], + monthNamesShort: ['Яну','Фев','Мар','Апр','Май','Юни', + 'Юли','Авг','Сеп','Окт','Нов','Дек'], + dayNames: ['Неделя','Понеделник','Вторник','Сряда','Четвъртък','Петък','Събота'], + dayNamesShort: ['Нед','Пон','Вто','Сря','Чет','Пет','Съб'], + dayNamesMin: ['Не','По','Вт','Ср','Че','Пе','Съ'], + weekHeader: 'Wk', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['bg']); +}); + +/* Bosnian i18n for the jQuery UI date picker plugin. */ +/* Written by Kenan Konjo. */ +jQuery(function($){ + $.datepicker.regional['bs'] = { + closeText: 'Zatvori', + prevText: '<', + nextText: '>', + currentText: 'Danas', + monthNames: ['Januar','Februar','Mart','April','Maj','Juni', + 'Juli','August','Septembar','Oktobar','Novembar','Decembar'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun', + 'Jul','Aug','Sep','Okt','Nov','Dec'], + dayNames: ['Nedelja','Ponedeljak','Utorak','Srijeda','Četvrtak','Petak','Subota'], + dayNamesShort: ['Ned','Pon','Uto','Sri','Čet','Pet','Sub'], + dayNamesMin: ['Ne','Po','Ut','Sr','Če','Pe','Su'], + weekHeader: 'Wk', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['bs']); +}); + +/* Inicialització en català per a l'extensió 'UI date picker' per jQuery. */ +/* Writers: (joan.leon@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['ca'] = { + closeText: 'Tanca', + prevText: 'Anterior', + nextText: 'Següent', + currentText: 'Avui', + monthNames: ['gener','febrer','març','abril','maig','juny', + 'juliol','agost','setembre','octubre','novembre','desembre'], + monthNamesShort: ['gen','feb','març','abr','maig','juny', + 'jul','ag','set','oct','nov','des'], + dayNames: ['diumenge','dilluns','dimarts','dimecres','dijous','divendres','dissabte'], + dayNamesShort: ['dg','dl','dt','dc','dj','dv','ds'], + dayNamesMin: ['dg','dl','dt','dc','dj','dv','ds'], + weekHeader: 'Set', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['ca']); +}); + +/* Czech initialisation for the jQuery UI date picker plugin. */ +/* Written by Tomas Muller (tomas@tomas-muller.net). */ +jQuery(function($){ + $.datepicker.regional['cs'] = { + closeText: 'Zavřít', + prevText: '<Dříve', + nextText: 'Později>', + currentText: 'Nyní', + monthNames: ['leden','únor','březen','duben','květen','červen', + 'červenec','srpen','září','říjen','listopad','prosinec'], + monthNamesShort: ['led','úno','bře','dub','kvě','čer', + 'čvc','srp','zář','říj','lis','pro'], + dayNames: ['neděle', 'pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota'], + dayNamesShort: ['ne', 'po', 'út', 'st', 'čt', 'pá', 'so'], + dayNamesMin: ['ne','po','út','st','čt','pá','so'], + weekHeader: 'Týd', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['cs']); +}); + +/* Welsh/UK initialisation for the jQuery UI date picker plugin. */ +/* Written by William Griffiths. */ +jQuery(function($){ + $.datepicker.regional['cy-GB'] = { + closeText: 'Done', + prevText: 'Prev', + nextText: 'Next', + currentText: 'Today', + monthNames: ['Ionawr','Chwefror','Mawrth','Ebrill','Mai','Mehefin', + 'Gorffennaf','Awst','Medi','Hydref','Tachwedd','Rhagfyr'], + monthNamesShort: ['Ion', 'Chw', 'Maw', 'Ebr', 'Mai', 'Meh', + 'Gor', 'Aws', 'Med', 'Hyd', 'Tac', 'Rha'], + dayNames: ['Dydd Sul', 'Dydd Llun', 'Dydd Mawrth', 'Dydd Mercher', 'Dydd Iau', 'Dydd Gwener', 'Dydd Sadwrn'], + dayNamesShort: ['Sul', 'Llu', 'Maw', 'Mer', 'Iau', 'Gwe', 'Sad'], + dayNamesMin: ['Su','Ll','Ma','Me','Ia','Gw','Sa'], + weekHeader: 'Wy', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['cy-GB']); +}); + +/* Danish initialisation for the jQuery UI date picker plugin. */ +/* Written by Jan Christensen ( deletestuff@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['da'] = { + closeText: 'Luk', + prevText: '<Forrige', + nextText: 'Næste>', + currentText: 'Idag', + monthNames: ['Januar','Februar','Marts','April','Maj','Juni', + 'Juli','August','September','Oktober','November','December'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun', + 'Jul','Aug','Sep','Okt','Nov','Dec'], + dayNames: ['Søndag','Mandag','Tirsdag','Onsdag','Torsdag','Fredag','Lørdag'], + dayNamesShort: ['Søn','Man','Tir','Ons','Tor','Fre','Lør'], + dayNamesMin: ['Sø','Ma','Ti','On','To','Fr','Lø'], + weekHeader: 'Uge', + dateFormat: 'dd-mm-yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['da']); +}); + +/* German initialisation for the jQuery UI date picker plugin. */ +/* Written by Milian Wolff (mail@milianw.de). */ +jQuery(function($){ + $.datepicker.regional['de'] = { + closeText: 'Schließen', + prevText: '<Zurück', + nextText: 'Vor>', + currentText: 'Heute', + monthNames: ['Januar','Februar','März','April','Mai','Juni', + 'Juli','August','September','Oktober','November','Dezember'], + monthNamesShort: ['Jan','Feb','Mär','Apr','Mai','Jun', + 'Jul','Aug','Sep','Okt','Nov','Dez'], + dayNames: ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'], + dayNamesShort: ['So','Mo','Di','Mi','Do','Fr','Sa'], + dayNamesMin: ['So','Mo','Di','Mi','Do','Fr','Sa'], + weekHeader: 'KW', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['de']); +}); + +/* Greek (el) initialisation for the jQuery UI date picker plugin. */ +/* Written by Alex Cicovic (http://www.alexcicovic.com) */ +jQuery(function($){ + $.datepicker.regional['el'] = { + closeText: 'Κλείσιμο', + prevText: 'Προηγούμενος', + nextText: 'Επόμενος', + currentText: 'Τρέχων Μήνας', + monthNames: ['Ιανουάριος','Φεβρουάριος','Μάρτιος','Απρίλιος','Μάιος','Ιούνιος', + 'Ιούλιος','Αύγουστος','Σεπτέμβριος','Οκτώβριος','Νοέμβριος','Δεκέμβριος'], + monthNamesShort: ['Ιαν','Φεβ','Μαρ','Απρ','Μαι','Ιουν', + 'Ιουλ','Αυγ','Σεπ','Οκτ','Νοε','Δεκ'], + dayNames: ['Κυριακή','Δευτέρα','Τρίτη','Τετάρτη','Πέμπτη','Παρασκευή','Σάββατο'], + dayNamesShort: ['Κυρ','Δευ','Τρι','Τετ','Πεμ','Παρ','Σαβ'], + dayNamesMin: ['Κυ','Δε','Τρ','Τε','Πε','Πα','Σα'], + weekHeader: 'Εβδ', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['el']); +}); + +/* English/Australia initialisation for the jQuery UI date picker plugin. */ +/* Based on the en-GB initialisation. */ +jQuery(function($){ + $.datepicker.regional['en-AU'] = { + closeText: 'Done', + prevText: 'Prev', + nextText: 'Next', + currentText: 'Today', + monthNames: ['January','February','March','April','May','June', + 'July','August','September','October','November','December'], + monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + dayNamesMin: ['Su','Mo','Tu','We','Th','Fr','Sa'], + weekHeader: 'Wk', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['en-AU']); +}); + +/* English/UK initialisation for the jQuery UI date picker plugin. */ +/* Written by Stuart. */ +jQuery(function($){ + $.datepicker.regional['en-GB'] = { + closeText: 'Done', + prevText: 'Prev', + nextText: 'Next', + currentText: 'Today', + monthNames: ['January','February','March','April','May','June', + 'July','August','September','October','November','December'], + monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + dayNamesMin: ['Su','Mo','Tu','We','Th','Fr','Sa'], + weekHeader: 'Wk', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['en-GB']); +}); + +/* English/New Zealand initialisation for the jQuery UI date picker plugin. */ +/* Based on the en-GB initialisation. */ +jQuery(function($){ + $.datepicker.regional['en-NZ'] = { + closeText: 'Done', + prevText: 'Prev', + nextText: 'Next', + currentText: 'Today', + monthNames: ['January','February','March','April','May','June', + 'July','August','September','October','November','December'], + monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + dayNamesMin: ['Su','Mo','Tu','We','Th','Fr','Sa'], + weekHeader: 'Wk', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['en-NZ']); +}); + +/* Esperanto initialisation for the jQuery UI date picker plugin. */ +/* Written by Olivier M. (olivierweb@ifrance.com). */ +jQuery(function($){ + $.datepicker.regional['eo'] = { + closeText: 'Fermi', + prevText: '<Anta', + nextText: 'Sekv>', + currentText: 'Nuna', + monthNames: ['Januaro','Februaro','Marto','Aprilo','Majo','Junio', + 'Julio','Aŭgusto','Septembro','Oktobro','Novembro','Decembro'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun', + 'Jul','Aŭg','Sep','Okt','Nov','Dec'], + dayNames: ['Dimanĉo','Lundo','Mardo','Merkredo','Ĵaŭdo','Vendredo','Sabato'], + dayNamesShort: ['Dim','Lun','Mar','Mer','Ĵaŭ','Ven','Sab'], + dayNamesMin: ['Di','Lu','Ma','Me','Ĵa','Ve','Sa'], + weekHeader: 'Sb', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['eo']); +}); + +/* Inicialización en español para la extensión 'UI date picker' para jQuery. */ +/* Traducido por Vester (xvester@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['es'] = { + closeText: 'Cerrar', + prevText: '<Ant', + nextText: 'Sig>', + currentText: 'Hoy', + monthNames: ['Enero','Febrero','Marzo','Abril','Mayo','Junio', + 'Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'], + monthNamesShort: ['Ene','Feb','Mar','Abr','May','Jun', + 'Jul','Ago','Sep','Oct','Nov','Dic'], + dayNames: ['Domingo','Lunes','Martes','Miércoles','Jueves','Viernes','Sábado'], + dayNamesShort: ['Dom','Lun','Mar','Mié','Juv','Vie','Sáb'], + dayNamesMin: ['Do','Lu','Ma','Mi','Ju','Vi','Sá'], + weekHeader: 'Sm', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['es']); +}); + +/* Estonian initialisation for the jQuery UI date picker plugin. */ +/* Written by Mart Sõmermaa (mrts.pydev at gmail com). */ +jQuery(function($){ + $.datepicker.regional['et'] = { + closeText: 'Sulge', + prevText: 'Eelnev', + nextText: 'Järgnev', + currentText: 'Täna', + monthNames: ['Jaanuar','Veebruar','Märts','Aprill','Mai','Juuni', + 'Juuli','August','September','Oktoober','November','Detsember'], + monthNamesShort: ['Jaan', 'Veebr', 'Märts', 'Apr', 'Mai', 'Juuni', + 'Juuli', 'Aug', 'Sept', 'Okt', 'Nov', 'Dets'], + dayNames: ['Pühapäev', 'Esmaspäev', 'Teisipäev', 'Kolmapäev', 'Neljapäev', 'Reede', 'Laupäev'], + dayNamesShort: ['Pühap', 'Esmasp', 'Teisip', 'Kolmap', 'Neljap', 'Reede', 'Laup'], + dayNamesMin: ['P','E','T','K','N','R','L'], + weekHeader: 'näd', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['et']); +}); + +/* Euskarako oinarria 'UI date picker' jquery-ko extentsioarentzat */ +/* Karrikas-ek itzulia (karrikas@karrikas.com) */ +jQuery(function($){ + $.datepicker.regional['eu'] = { + closeText: 'Egina', + prevText: '<Aur', + nextText: 'Hur>', + currentText: 'Gaur', + monthNames: ['urtarrila','otsaila','martxoa','apirila','maiatza','ekaina', + 'uztaila','abuztua','iraila','urria','azaroa','abendua'], + monthNamesShort: ['urt.','ots.','mar.','api.','mai.','eka.', + 'uzt.','abu.','ira.','urr.','aza.','abe.'], + dayNames: ['igandea','astelehena','asteartea','asteazkena','osteguna','ostirala','larunbata'], + dayNamesShort: ['ig.','al.','ar.','az.','og.','ol.','lr.'], + dayNamesMin: ['ig','al','ar','az','og','ol','lr'], + weekHeader: 'As', + dateFormat: 'yy-mm-dd', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['eu']); +}); + +/* Persian (Farsi) Translation for the jQuery UI date picker plugin. */ +/* Javad Mowlanezhad -- jmowla@gmail.com */ +/* Jalali calendar should supported soon! (Its implemented but I have to test it) */ +jQuery(function($) { + $.datepicker.regional['fa'] = { + closeText: 'بستن', + prevText: '<قبلی', + nextText: 'بعدی>', + currentText: 'امروز', + monthNames: [ + 'فروردين', + 'ارديبهشت', + 'خرداد', + 'تير', + 'مرداد', + 'شهريور', + 'مهر', + 'آبان', + 'آذر', + 'دی', + 'بهمن', + 'اسفند' + ], + monthNamesShort: ['1','2','3','4','5','6','7','8','9','10','11','12'], + dayNames: [ + 'يکشنبه', + 'دوشنبه', + 'سهشنبه', + 'چهارشنبه', + 'پنجشنبه', + 'جمعه', + 'شنبه' + ], + dayNamesShort: [ + 'ی', + 'د', + 'س', + 'چ', + 'پ', + 'ج', + 'ش' + ], + dayNamesMin: [ + 'ی', + 'د', + 'س', + 'چ', + 'پ', + 'ج', + 'ش' + ], + weekHeader: 'هف', + dateFormat: 'yy/mm/dd', + firstDay: 6, + isRTL: true, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['fa']); +}); + +/* Finnish initialisation for the jQuery UI date picker plugin. */ +/* Written by Harri Kilpiö (harrikilpio@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['fi'] = { + closeText: 'Sulje', + prevText: '«Edellinen', + nextText: 'Seuraava»', + currentText: 'Tänään', + monthNames: ['Tammikuu','Helmikuu','Maaliskuu','Huhtikuu','Toukokuu','Kesäkuu', + 'Heinäkuu','Elokuu','Syyskuu','Lokakuu','Marraskuu','Joulukuu'], + monthNamesShort: ['Tammi','Helmi','Maalis','Huhti','Touko','Kesä', + 'Heinä','Elo','Syys','Loka','Marras','Joulu'], + dayNamesShort: ['Su','Ma','Ti','Ke','To','Pe','La'], + dayNames: ['Sunnuntai','Maanantai','Tiistai','Keskiviikko','Torstai','Perjantai','Lauantai'], + dayNamesMin: ['Su','Ma','Ti','Ke','To','Pe','La'], + weekHeader: 'Vk', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['fi']); +}); + +/* Faroese initialisation for the jQuery UI date picker plugin */ +/* Written by Sverri Mohr Olsen, sverrimo@gmail.com */ +jQuery(function($){ + $.datepicker.regional['fo'] = { + closeText: 'Lat aftur', + prevText: '<Fyrra', + nextText: 'Næsta>', + currentText: 'Í dag', + monthNames: ['Januar','Februar','Mars','Apríl','Mei','Juni', + 'Juli','August','September','Oktober','November','Desember'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Mei','Jun', + 'Jul','Aug','Sep','Okt','Nov','Des'], + dayNames: ['Sunnudagur','Mánadagur','Týsdagur','Mikudagur','Hósdagur','Fríggjadagur','Leyardagur'], + dayNamesShort: ['Sun','Mán','Týs','Mik','Hós','Frí','Ley'], + dayNamesMin: ['Su','Má','Tý','Mi','Hó','Fr','Le'], + weekHeader: 'Vk', + dateFormat: 'dd-mm-yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['fo']); +}); + +/* Canadian-French initialisation for the jQuery UI date picker plugin. */ +jQuery(function ($) { + $.datepicker.regional['fr-CA'] = { + closeText: 'Fermer', + prevText: 'Précédent', + nextText: 'Suivant', + currentText: 'Aujourd\'hui', + monthNames: ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', + 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'], + monthNamesShort: ['janv.', 'févr.', 'mars', 'avril', 'mai', 'juin', + 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.'], + dayNames: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'], + dayNamesShort: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'], + dayNamesMin: ['D', 'L', 'M', 'M', 'J', 'V', 'S'], + weekHeader: 'Sem.', + dateFormat: 'yy-mm-dd', + firstDay: 0, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: '' + }; + $.datepicker.setDefaults($.datepicker.regional['fr-CA']); +}); + +/* Swiss-French initialisation for the jQuery UI date picker plugin. */ +/* Written Martin Voelkle (martin.voelkle@e-tc.ch). */ +jQuery(function($){ + $.datepicker.regional['fr-CH'] = { + closeText: 'Fermer', + prevText: '<Préc', + nextText: 'Suiv>', + currentText: 'Courant', + monthNames: ['Janvier','Février','Mars','Avril','Mai','Juin', + 'Juillet','Août','Septembre','Octobre','Novembre','Décembre'], + monthNamesShort: ['Jan','Fév','Mar','Avr','Mai','Jun', + 'Jul','Aoû','Sep','Oct','Nov','Déc'], + dayNames: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'], + dayNamesShort: ['Dim','Lun','Mar','Mer','Jeu','Ven','Sam'], + dayNamesMin: ['Di','Lu','Ma','Me','Je','Ve','Sa'], + weekHeader: 'Sm', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['fr-CH']); +}); + +/* French initialisation for the jQuery UI date picker plugin. */ +/* Written by Keith Wood (kbwood{at}iinet.com.au), + Stéphane Nahmani (sholby@sholby.net), + Stéphane Raimbault <stephane.raimbault@gmail.com> */ +jQuery(function($){ + $.datepicker.regional['fr'] = { + closeText: 'Fermer', + prevText: 'Précédent', + nextText: 'Suivant', + currentText: 'Aujourd\'hui', + monthNames: ['Janvier','Février','Mars','Avril','Mai','Juin', + 'Juillet','Août','Septembre','Octobre','Novembre','Décembre'], + monthNamesShort: ['Janv.','Févr.','Mars','Avril','Mai','Juin', + 'Juil.','Août','Sept.','Oct.','Nov.','Déc.'], + dayNames: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'], + dayNamesShort: ['Dim.','Lun.','Mar.','Mer.','Jeu.','Ven.','Sam.'], + dayNamesMin: ['D','L','M','M','J','V','S'], + weekHeader: 'Sem.', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['fr']); +}); + +/* Galician localization for 'UI date picker' jQuery extension. */ +/* Translated by Jorge Barreiro <yortx.barry@gmail.com>. */ +jQuery(function($){ + $.datepicker.regional['gl'] = { + closeText: 'Pechar', + prevText: '<Ant', + nextText: 'Seg>', + currentText: 'Hoxe', + monthNames: ['Xaneiro','Febreiro','Marzo','Abril','Maio','Xuño', + 'Xullo','Agosto','Setembro','Outubro','Novembro','Decembro'], + monthNamesShort: ['Xan','Feb','Mar','Abr','Mai','Xuñ', + 'Xul','Ago','Set','Out','Nov','Dec'], + dayNames: ['Domingo','Luns','Martes','Mércores','Xoves','Venres','Sábado'], + dayNamesShort: ['Dom','Lun','Mar','Mér','Xov','Ven','Sáb'], + dayNamesMin: ['Do','Lu','Ma','Mé','Xo','Ve','Sá'], + weekHeader: 'Sm', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['gl']); +}); + +/* Hebrew initialisation for the UI Datepicker extension. */ +/* Written by Amir Hardon (ahardon at gmail dot com). */ +jQuery(function($){ + $.datepicker.regional['he'] = { + closeText: 'סגור', + prevText: '<הקודם', + nextText: 'הבא>', + currentText: 'היום', + monthNames: ['ינואר','פברואר','מרץ','אפריל','מאי','יוני', + 'יולי','אוגוסט','ספטמבר','אוקטובר','נובמבר','דצמבר'], + monthNamesShort: ['ינו','פבר','מרץ','אפר','מאי','יוני', + 'יולי','אוג','ספט','אוק','נוב','דצמ'], + dayNames: ['ראשון','שני','שלישי','רביעי','חמישי','שישי','שבת'], + dayNamesShort: ['א\'','ב\'','ג\'','ד\'','ה\'','ו\'','שבת'], + dayNamesMin: ['א\'','ב\'','ג\'','ד\'','ה\'','ו\'','שבת'], + weekHeader: 'Wk', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: true, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['he']); +}); + +/* Hindi initialisation for the jQuery UI date picker plugin. */ +/* Written by Michael Dawart. */ +jQuery(function($){ + $.datepicker.regional['hi'] = { + closeText: 'बंद', + prevText: 'पिछला', + nextText: 'अगला', + currentText: 'आज', + monthNames: ['जनवरी ','फरवरी','मार्च','अप्रेल','मई','जून', + 'जूलाई','अगस्त ','सितम्बर','अक्टूबर','नवम्बर','दिसम्बर'], + monthNamesShort: ['जन', 'फर', 'मार्च', 'अप्रेल', 'मई', 'जून', + 'जूलाई', 'अग', 'सित', 'अक्ट', 'नव', 'दि'], + dayNames: ['रविवार', 'सोमवार', 'मंगलवार', 'बुधवार', 'गुरुवार', 'शुक्रवार', 'शनिवार'], + dayNamesShort: ['रवि', 'सोम', 'मंगल', 'बुध', 'गुरु', 'शुक्र', 'शनि'], + dayNamesMin: ['रवि', 'सोम', 'मंगल', 'बुध', 'गुरु', 'शुक्र', 'शनि'], + weekHeader: 'हफ्ता', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['hi']); +}); + +/* Croatian i18n for the jQuery UI date picker plugin. */ +/* Written by Vjekoslav Nesek. */ +jQuery(function($){ + $.datepicker.regional['hr'] = { + closeText: 'Zatvori', + prevText: '<', + nextText: '>', + currentText: 'Danas', + monthNames: ['Siječanj','Veljača','Ožujak','Travanj','Svibanj','Lipanj', + 'Srpanj','Kolovoz','Rujan','Listopad','Studeni','Prosinac'], + monthNamesShort: ['Sij','Velj','Ožu','Tra','Svi','Lip', + 'Srp','Kol','Ruj','Lis','Stu','Pro'], + dayNames: ['Nedjelja','Ponedjeljak','Utorak','Srijeda','Četvrtak','Petak','Subota'], + dayNamesShort: ['Ned','Pon','Uto','Sri','Čet','Pet','Sub'], + dayNamesMin: ['Ne','Po','Ut','Sr','Če','Pe','Su'], + weekHeader: 'Tje', + dateFormat: 'dd.mm.yy.', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['hr']); +}); + +/* Hungarian initialisation for the jQuery UI date picker plugin. */ +/* Written by Istvan Karaszi (jquery@spam.raszi.hu). */ +jQuery(function($){ + $.datepicker.regional['hu'] = { + closeText: 'bezár', + prevText: 'vissza', + nextText: 'előre', + currentText: 'ma', + monthNames: ['Január', 'Február', 'Március', 'Április', 'Május', 'Június', + 'Július', 'Augusztus', 'Szeptember', 'Október', 'November', 'December'], + monthNamesShort: ['Jan', 'Feb', 'Már', 'Ápr', 'Máj', 'Jún', + 'Júl', 'Aug', 'Szep', 'Okt', 'Nov', 'Dec'], + dayNames: ['Vasárnap', 'Hétfő', 'Kedd', 'Szerda', 'Csütörtök', 'Péntek', 'Szombat'], + dayNamesShort: ['Vas', 'Hét', 'Ked', 'Sze', 'Csü', 'Pén', 'Szo'], + dayNamesMin: ['V', 'H', 'K', 'Sze', 'Cs', 'P', 'Szo'], + weekHeader: 'Hét', + dateFormat: 'yy.mm.dd.', + firstDay: 1, + isRTL: false, + showMonthAfterYear: true, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['hu']); +}); + +/* Armenian(UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Levon Zakaryan (levon.zakaryan@gmail.com)*/ +jQuery(function($){ + $.datepicker.regional['hy'] = { + closeText: 'Փակել', + prevText: '<Նախ.', + nextText: 'Հաջ.>', + currentText: 'Այսօր', + monthNames: ['Հունվար','Փետրվար','Մարտ','Ապրիլ','Մայիս','Հունիս', + 'Հուլիս','Օգոստոս','Սեպտեմբեր','Հոկտեմբեր','Նոյեմբեր','Դեկտեմբեր'], + monthNamesShort: ['Հունվ','Փետր','Մարտ','Ապր','Մայիս','Հունիս', + 'Հուլ','Օգս','Սեպ','Հոկ','Նոյ','Դեկ'], + dayNames: ['կիրակի','եկուշաբթի','երեքշաբթի','չորեքշաբթի','հինգշաբթի','ուրբաթ','շաբաթ'], + dayNamesShort: ['կիր','երկ','երք','չրք','հնգ','ուրբ','շբթ'], + dayNamesMin: ['կիր','երկ','երք','չրք','հնգ','ուրբ','շբթ'], + weekHeader: 'ՇԲՏ', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['hy']); +}); + +/* Indonesian initialisation for the jQuery UI date picker plugin. */ +/* Written by Deden Fathurahman (dedenf@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['id'] = { + closeText: 'Tutup', + prevText: '<mundur', + nextText: 'maju>', + currentText: 'hari ini', + monthNames: ['Januari','Februari','Maret','April','Mei','Juni', + 'Juli','Agustus','September','Oktober','Nopember','Desember'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Mei','Jun', + 'Jul','Agus','Sep','Okt','Nop','Des'], + dayNames: ['Minggu','Senin','Selasa','Rabu','Kamis','Jumat','Sabtu'], + dayNamesShort: ['Min','Sen','Sel','Rab','kam','Jum','Sab'], + dayNamesMin: ['Mg','Sn','Sl','Rb','Km','jm','Sb'], + weekHeader: 'Mg', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['id']); +}); + +/* Icelandic initialisation for the jQuery UI date picker plugin. */ +/* Written by Haukur H. Thorsson (haukur@eskill.is). */ +jQuery(function($){ + $.datepicker.regional['is'] = { + closeText: 'Loka', + prevText: '< Fyrri', + nextText: 'Næsti >', + currentText: 'Í dag', + monthNames: ['Janúar','Febrúar','Mars','Apríl','Maí','Júní', + 'Júlí','Ágúst','September','Október','Nóvember','Desember'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Maí','Jún', + 'Júl','Ágú','Sep','Okt','Nóv','Des'], + dayNames: ['Sunnudagur','Mánudagur','Þriðjudagur','Miðvikudagur','Fimmtudagur','Föstudagur','Laugardagur'], + dayNamesShort: ['Sun','Mán','Þri','Mið','Fim','Fös','Lau'], + dayNamesMin: ['Su','Má','Þr','Mi','Fi','Fö','La'], + weekHeader: 'Vika', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['is']); +}); + +/* Italian initialisation for the jQuery UI date picker plugin. */ +/* Written by Antonello Pasella (antonello.pasella@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['it'] = { + closeText: 'Chiudi', + prevText: '<Prec', + nextText: 'Succ>', + currentText: 'Oggi', + monthNames: ['Gennaio','Febbraio','Marzo','Aprile','Maggio','Giugno', + 'Luglio','Agosto','Settembre','Ottobre','Novembre','Dicembre'], + monthNamesShort: ['Gen','Feb','Mar','Apr','Mag','Giu', + 'Lug','Ago','Set','Ott','Nov','Dic'], + dayNames: ['Domenica','Lunedì','Martedì','Mercoledì','Giovedì','Venerdì','Sabato'], + dayNamesShort: ['Dom','Lun','Mar','Mer','Gio','Ven','Sab'], + dayNamesMin: ['Do','Lu','Ma','Me','Gi','Ve','Sa'], + weekHeader: 'Sm', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['it']); +}); + +/* Japanese initialisation for the jQuery UI date picker plugin. */ +/* Written by Kentaro SATO (kentaro@ranvis.com). */ +jQuery(function($){ + $.datepicker.regional['ja'] = { + closeText: '閉じる', + prevText: '<前', + nextText: '次>', + currentText: '今日', + monthNames: ['1月','2月','3月','4月','5月','6月', + '7月','8月','9月','10月','11月','12月'], + monthNamesShort: ['1月','2月','3月','4月','5月','6月', + '7月','8月','9月','10月','11月','12月'], + dayNames: ['日曜日','月曜日','火曜日','水曜日','木曜日','金曜日','土曜日'], + dayNamesShort: ['日','月','火','水','木','金','土'], + dayNamesMin: ['日','月','火','水','木','金','土'], + weekHeader: '週', + dateFormat: 'yy/mm/dd', + firstDay: 0, + isRTL: false, + showMonthAfterYear: true, + yearSuffix: '年'}; + $.datepicker.setDefaults($.datepicker.regional['ja']); +}); + +/* Georgian (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Lado Lomidze (lado.lomidze@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['ka'] = { + closeText: 'დახურვა', + prevText: '< წინა', + nextText: 'შემდეგი >', + currentText: 'დღეს', + monthNames: ['იანვარი','თებერვალი','მარტი','აპრილი','მაისი','ივნისი', 'ივლისი','აგვისტო','სექტემბერი','ოქტომბერი','ნოემბერი','დეკემბერი'], + monthNamesShort: ['იან','თებ','მარ','აპრ','მაი','ივნ', 'ივლ','აგვ','სექ','ოქტ','ნოე','დეკ'], + dayNames: ['კვირა','ორშაბათი','სამშაბათი','ოთხშაბათი','ხუთშაბათი','პარასკევი','შაბათი'], + dayNamesShort: ['კვ','ორშ','სამ','ოთხ','ხუთ','პარ','შაბ'], + dayNamesMin: ['კვ','ორშ','სამ','ოთხ','ხუთ','პარ','შაბ'], + weekHeader: 'კვირა', + dateFormat: 'dd-mm-yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['ka']); +}); + +/* Kazakh (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Dmitriy Karasyov (dmitriy.karasyov@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['kk'] = { + closeText: 'Жабу', + prevText: '<Алдыңғы', + nextText: 'Келесі>', + currentText: 'Бүгін', + monthNames: ['Қаңтар','Ақпан','Наурыз','Сәуір','Мамыр','Маусым', + 'Шілде','Тамыз','Қыркүйек','Қазан','Қараша','Желтоқсан'], + monthNamesShort: ['Қаң','Ақп','Нау','Сәу','Мам','Мау', + 'Шіл','Там','Қыр','Қаз','Қар','Жел'], + dayNames: ['Жексенбі','Дүйсенбі','Сейсенбі','Сәрсенбі','Бейсенбі','Жұма','Сенбі'], + dayNamesShort: ['жкс','дсн','ссн','срс','бсн','жма','снб'], + dayNamesMin: ['Жк','Дс','Сс','Ср','Бс','Жм','Сн'], + weekHeader: 'Не', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['kk']); +}); + +/* Khmer initialisation for the jQuery calendar extension. */ +/* Written by Chandara Om (chandara.teacher@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['km'] = { + closeText: 'ធ្វើរួច', + prevText: 'មុន', + nextText: 'បន្ទាប់', + currentText: 'ថ្ងៃនេះ', + monthNames: ['មករា','កុម្ភៈ','មីនា','មេសា','ឧសភា','មិថុនា', + 'កក្កដា','សីហា','កញ្ញា','តុលា','វិច្ឆិកា','ធ្នូ'], + monthNamesShort: ['មករា','កុម្ភៈ','មីនា','មេសា','ឧសភា','មិថុនា', + 'កក្កដា','សីហា','កញ្ញា','តុលា','វិច្ឆិកា','ធ្នូ'], + dayNames: ['អាទិត្យ', 'ចន្ទ', 'អង្គារ', 'ពុធ', 'ព្រហស្បតិ៍', 'សុក្រ', 'សៅរ៍'], + dayNamesShort: ['អា', 'ច', 'អ', 'ពុ', 'ព្រហ', 'សុ', 'សៅ'], + dayNamesMin: ['អា', 'ច', 'អ', 'ពុ', 'ព្រហ', 'សុ', 'សៅ'], + weekHeader: 'សប្ដាហ៍', + dateFormat: 'dd-mm-yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['km']); +}); + +/* Korean initialisation for the jQuery calendar extension. */ +/* Written by DaeKwon Kang (ncrash.dk@gmail.com), Edited by Genie. */ +jQuery(function($){ + $.datepicker.regional['ko'] = { + closeText: '닫기', + prevText: '이전달', + nextText: '다음달', + currentText: '오늘', + monthNames: ['1월','2월','3월','4월','5월','6월', + '7월','8월','9월','10월','11월','12월'], + monthNamesShort: ['1월','2월','3월','4월','5월','6월', + '7월','8월','9월','10월','11월','12월'], + dayNames: ['일요일','월요일','화요일','수요일','목요일','금요일','토요일'], + dayNamesShort: ['일','월','화','수','목','금','토'], + dayNamesMin: ['일','월','화','수','목','금','토'], + weekHeader: 'Wk', + dateFormat: 'yy-mm-dd', + firstDay: 0, + isRTL: false, + showMonthAfterYear: true, + yearSuffix: '년'}; + $.datepicker.setDefaults($.datepicker.regional['ko']); +}); + +/* Kyrgyz (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Sergey Kartashov (ebishkek@yandex.ru). */ +jQuery(function($){ + $.datepicker.regional['ky'] = { + closeText: 'Жабуу', + prevText: '<Мур', + nextText: 'Кий>', + currentText: 'Бүгүн', + monthNames: ['Январь','Февраль','Март','Апрель','Май','Июнь', + 'Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'], + monthNamesShort: ['Янв','Фев','Мар','Апр','Май','Июн', + 'Июл','Авг','Сен','Окт','Ноя','Дек'], + dayNames: ['жекшемби', 'дүйшөмбү', 'шейшемби', 'шаршемби', 'бейшемби', 'жума', 'ишемби'], + dayNamesShort: ['жек', 'дүй', 'шей', 'шар', 'бей', 'жум', 'ише'], + dayNamesMin: ['Жк','Дш','Шш','Шр','Бш','Жм','Иш'], + weekHeader: 'Жум', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: '' + }; + $.datepicker.setDefaults($.datepicker.regional['ky']); +}); + +/* Luxembourgish initialisation for the jQuery UI date picker plugin. */ +/* Written by Michel Weimerskirch <michel@weimerskirch.net> */ +jQuery(function($){ + $.datepicker.regional['lb'] = { + closeText: 'Fäerdeg', + prevText: 'Zréck', + nextText: 'Weider', + currentText: 'Haut', + monthNames: ['Januar','Februar','Mäerz','Abrëll','Mee','Juni', + 'Juli','August','September','Oktober','November','Dezember'], + monthNamesShort: ['Jan', 'Feb', 'Mäe', 'Abr', 'Mee', 'Jun', + 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'], + dayNames: ['Sonndeg', 'Méindeg', 'Dënschdeg', 'Mëttwoch', 'Donneschdeg', 'Freideg', 'Samschdeg'], + dayNamesShort: ['Son', 'Méi', 'Dën', 'Mët', 'Don', 'Fre', 'Sam'], + dayNamesMin: ['So','Mé','Dë','Më','Do','Fr','Sa'], + weekHeader: 'W', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['lb']); +}); + +/* Lithuanian (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* @author Arturas Paleicikas <arturas@avalon.lt> */ +jQuery(function($){ + $.datepicker.regional['lt'] = { + closeText: 'Uždaryti', + prevText: '<Atgal', + nextText: 'Pirmyn>', + currentText: 'Šiandien', + monthNames: ['Sausis','Vasaris','Kovas','Balandis','Gegužė','Birželis', + 'Liepa','Rugpjūtis','Rugsėjis','Spalis','Lapkritis','Gruodis'], + monthNamesShort: ['Sau','Vas','Kov','Bal','Geg','Bir', + 'Lie','Rugp','Rugs','Spa','Lap','Gru'], + dayNames: ['sekmadienis','pirmadienis','antradienis','trečiadienis','ketvirtadienis','penktadienis','šeštadienis'], + dayNamesShort: ['sek','pir','ant','tre','ket','pen','šeš'], + dayNamesMin: ['Se','Pr','An','Tr','Ke','Pe','Še'], + weekHeader: 'Wk', + dateFormat: 'yy-mm-dd', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['lt']); +}); + +/* Latvian (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* @author Arturas Paleicikas <arturas.paleicikas@metasite.net> */ +jQuery(function($){ + $.datepicker.regional['lv'] = { + closeText: 'Aizvērt', + prevText: 'Iepr', + nextText: 'Nāka', + currentText: 'Šodien', + monthNames: ['Janvāris','Februāris','Marts','Aprīlis','Maijs','Jūnijs', + 'Jūlijs','Augusts','Septembris','Oktobris','Novembris','Decembris'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Mai','Jūn', + 'Jūl','Aug','Sep','Okt','Nov','Dec'], + dayNames: ['svētdiena','pirmdiena','otrdiena','trešdiena','ceturtdiena','piektdiena','sestdiena'], + dayNamesShort: ['svt','prm','otr','tre','ctr','pkt','sst'], + dayNamesMin: ['Sv','Pr','Ot','Tr','Ct','Pk','Ss'], + weekHeader: 'Nav', + dateFormat: 'dd-mm-yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['lv']); +}); + +/* Macedonian i18n for the jQuery UI date picker plugin. */ +/* Written by Stojce Slavkovski. */ +jQuery(function($){ + $.datepicker.regional['mk'] = { + closeText: 'Затвори', + prevText: '<', + nextText: '>', + currentText: 'Денес', + monthNames: ['Јануари','Февруари','Март','Април','Мај','Јуни', + 'Јули','Август','Септември','Октомври','Ноември','Декември'], + monthNamesShort: ['Јан','Фев','Мар','Апр','Мај','Јун', + 'Јул','Авг','Сеп','Окт','Ное','Дек'], + dayNames: ['Недела','Понеделник','Вторник','Среда','Четврток','Петок','Сабота'], + dayNamesShort: ['Нед','Пон','Вто','Сре','Чет','Пет','Саб'], + dayNamesMin: ['Не','По','Вт','Ср','Че','Пе','Са'], + weekHeader: 'Сед', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['mk']); +}); + +/* Malayalam (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Saji Nediyanchath (saji89@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['ml'] = { + closeText: 'ശരി', + prevText: 'മുന്നത്തെ', + nextText: 'അടുത്തത് ', + currentText: 'ഇന്ന്', + monthNames: ['ജനുവരി','ഫെബ്രുവരി','മാര്ച്ച്','ഏപ്രില്','മേയ്','ജൂണ്', + 'ജൂലൈ','ആഗസ്റ്റ്','സെപ്റ്റംബര്','ഒക്ടോബര്','നവംബര്','ഡിസംബര്'], + monthNamesShort: ['ജനു', 'ഫെബ്', 'മാര്', 'ഏപ്രി', 'മേയ്', 'ജൂണ്', + 'ജൂലാ', 'ആഗ', 'സെപ്', 'ഒക്ടോ', 'നവം', 'ഡിസ'], + dayNames: ['ഞായര്', 'തിങ്കള്', 'ചൊവ്വ', 'ബുധന്', 'വ്യാഴം', 'വെള്ളി', 'ശനി'], + dayNamesShort: ['ഞായ', 'തിങ്ക', 'ചൊവ്വ', 'ബുധ', 'വ്യാഴം', 'വെള്ളി', 'ശനി'], + dayNamesMin: ['ഞാ','തി','ചൊ','ബു','വ്യാ','വെ','ശ'], + weekHeader: 'ആ', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['ml']); +}); + +/* Malaysian initialisation for the jQuery UI date picker plugin. */ +/* Written by Mohd Nawawi Mohamad Jamili (nawawi@ronggeng.net). */ +jQuery(function($){ + $.datepicker.regional['ms'] = { + closeText: 'Tutup', + prevText: '<Sebelum', + nextText: 'Selepas>', + currentText: 'hari ini', + monthNames: ['Januari','Februari','Mac','April','Mei','Jun', + 'Julai','Ogos','September','Oktober','November','Disember'], + monthNamesShort: ['Jan','Feb','Mac','Apr','Mei','Jun', + 'Jul','Ogo','Sep','Okt','Nov','Dis'], + dayNames: ['Ahad','Isnin','Selasa','Rabu','Khamis','Jumaat','Sabtu'], + dayNamesShort: ['Aha','Isn','Sel','Rab','kha','Jum','Sab'], + dayNamesMin: ['Ah','Is','Se','Ra','Kh','Ju','Sa'], + weekHeader: 'Mg', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['ms']); +}); + +/* Norwegian Bokmål initialisation for the jQuery UI date picker plugin. */ +/* Written by Bjørn Johansen (post@bjornjohansen.no). */ +jQuery(function($){ + $.datepicker.regional['nb'] = { + closeText: 'Lukk', + prevText: '«Forrige', + nextText: 'Neste»', + currentText: 'I dag', + monthNames: ['januar','februar','mars','april','mai','juni','juli','august','september','oktober','november','desember'], + monthNamesShort: ['jan','feb','mar','apr','mai','jun','jul','aug','sep','okt','nov','des'], + dayNamesShort: ['søn','man','tir','ons','tor','fre','lør'], + dayNames: ['søndag','mandag','tirsdag','onsdag','torsdag','fredag','lørdag'], + dayNamesMin: ['sø','ma','ti','on','to','fr','lø'], + weekHeader: 'Uke', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: '' + }; + $.datepicker.setDefaults($.datepicker.regional['nb']); +}); + +/* Dutch (Belgium) initialisation for the jQuery UI date picker plugin. */ +/* David De Sloovere @DavidDeSloovere */ +jQuery(function($){ + $.datepicker.regional['nl-BE'] = { + closeText: 'Sluiten', + prevText: '←', + nextText: '→', + currentText: 'Vandaag', + monthNames: ['januari', 'februari', 'maart', 'april', 'mei', 'juni', + 'juli', 'augustus', 'september', 'oktober', 'november', 'december'], + monthNamesShort: ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', + 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'], + dayNames: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'], + dayNamesShort: ['zon', 'maa', 'din', 'woe', 'don', 'vri', 'zat'], + dayNamesMin: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'], + weekHeader: 'Wk', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['nl-BE']); +}); + +/* Dutch (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Mathias Bynens <http://mathiasbynens.be/> */ +jQuery(function($){ + $.datepicker.regional.nl = { + closeText: 'Sluiten', + prevText: '←', + nextText: '→', + currentText: 'Vandaag', + monthNames: ['januari', 'februari', 'maart', 'april', 'mei', 'juni', + 'juli', 'augustus', 'september', 'oktober', 'november', 'december'], + monthNamesShort: ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', + 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'], + dayNames: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'], + dayNamesShort: ['zon', 'maa', 'din', 'woe', 'don', 'vri', 'zat'], + dayNamesMin: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'], + weekHeader: 'Wk', + dateFormat: 'dd-mm-yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional.nl); +}); + +/* Norwegian Nynorsk initialisation for the jQuery UI date picker plugin. */ +/* Written by Bjørn Johansen (post@bjornjohansen.no). */ +jQuery(function($){ + $.datepicker.regional['nn'] = { + closeText: 'Lukk', + prevText: '«Førre', + nextText: 'Neste»', + currentText: 'I dag', + monthNames: ['januar','februar','mars','april','mai','juni','juli','august','september','oktober','november','desember'], + monthNamesShort: ['jan','feb','mar','apr','mai','jun','jul','aug','sep','okt','nov','des'], + dayNamesShort: ['sun','mån','tys','ons','tor','fre','lau'], + dayNames: ['sundag','måndag','tysdag','onsdag','torsdag','fredag','laurdag'], + dayNamesMin: ['su','må','ty','on','to','fr','la'], + weekHeader: 'Veke', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: '' + }; + $.datepicker.setDefaults($.datepicker.regional['nn']); +}); + +/* Norwegian initialisation for the jQuery UI date picker plugin. */ +/* Written by Naimdjon Takhirov (naimdjon@gmail.com). */ + +jQuery(function($){ + $.datepicker.regional['no'] = { + closeText: 'Lukk', + prevText: '«Forrige', + nextText: 'Neste»', + currentText: 'I dag', + monthNames: ['januar','februar','mars','april','mai','juni','juli','august','september','oktober','november','desember'], + monthNamesShort: ['jan','feb','mar','apr','mai','jun','jul','aug','sep','okt','nov','des'], + dayNamesShort: ['søn','man','tir','ons','tor','fre','lør'], + dayNames: ['søndag','mandag','tirsdag','onsdag','torsdag','fredag','lørdag'], + dayNamesMin: ['sø','ma','ti','on','to','fr','lø'], + weekHeader: 'Uke', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: '' + }; + $.datepicker.setDefaults($.datepicker.regional['no']); +}); + +/* Polish initialisation for the jQuery UI date picker plugin. */ +/* Written by Jacek Wysocki (jacek.wysocki@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['pl'] = { + closeText: 'Zamknij', + prevText: '<Poprzedni', + nextText: 'Następny>', + currentText: 'Dziś', + monthNames: ['Styczeń','Luty','Marzec','Kwiecień','Maj','Czerwiec', + 'Lipiec','Sierpień','Wrzesień','Październik','Listopad','Grudzień'], + monthNamesShort: ['Sty','Lu','Mar','Kw','Maj','Cze', + 'Lip','Sie','Wrz','Pa','Lis','Gru'], + dayNames: ['Niedziela','Poniedziałek','Wtorek','Środa','Czwartek','Piątek','Sobota'], + dayNamesShort: ['Nie','Pn','Wt','Śr','Czw','Pt','So'], + dayNamesMin: ['N','Pn','Wt','Śr','Cz','Pt','So'], + weekHeader: 'Tydz', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['pl']); +}); + +/* Brazilian initialisation for the jQuery UI date picker plugin. */ +/* Written by Leonildo Costa Silva (leocsilva@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['pt-BR'] = { + closeText: 'Fechar', + prevText: '<Anterior', + nextText: 'Próximo>', + currentText: 'Hoje', + monthNames: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho', + 'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'], + monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun', + 'Jul','Ago','Set','Out','Nov','Dez'], + dayNames: ['Domingo','Segunda-feira','Terça-feira','Quarta-feira','Quinta-feira','Sexta-feira','Sábado'], + dayNamesShort: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'], + dayNamesMin: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'], + weekHeader: 'Sm', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['pt-BR']); +}); + +/* Portuguese initialisation for the jQuery UI date picker plugin. */ +jQuery(function($){ + $.datepicker.regional['pt'] = { + closeText: 'Fechar', + prevText: '<Anterior', + nextText: 'Seguinte', + currentText: 'Hoje', + monthNames: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho', + 'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'], + monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun', + 'Jul','Ago','Set','Out','Nov','Dez'], + dayNames: ['Domingo','Segunda-feira','Terça-feira','Quarta-feira','Quinta-feira','Sexta-feira','Sábado'], + dayNamesShort: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'], + dayNamesMin: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'], + weekHeader: 'Sem', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['pt']); +}); + +/* Romansh initialisation for the jQuery UI date picker plugin. */ +/* Written by Yvonne Gienal (yvonne.gienal@educa.ch). */ +jQuery(function($){ + $.datepicker.regional['rm'] = { + closeText: 'Serrar', + prevText: '<Suandant', + nextText: 'Precedent>', + currentText: 'Actual', + monthNames: ['Schaner','Favrer','Mars','Avrigl','Matg','Zercladur', 'Fanadur','Avust','Settember','October','November','December'], + monthNamesShort: ['Scha','Fev','Mar','Avr','Matg','Zer', 'Fan','Avu','Sett','Oct','Nov','Dec'], + dayNames: ['Dumengia','Glindesdi','Mardi','Mesemna','Gievgia','Venderdi','Sonda'], + dayNamesShort: ['Dum','Gli','Mar','Mes','Gie','Ven','Som'], + dayNamesMin: ['Du','Gl','Ma','Me','Gi','Ve','So'], + weekHeader: 'emna', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['rm']); +}); + +/* Romanian initialisation for the jQuery UI date picker plugin. + * + * Written by Edmond L. (ll_edmond@walla.com) + * and Ionut G. Stan (ionut.g.stan@gmail.com) + */ +jQuery(function($){ + $.datepicker.regional['ro'] = { + closeText: 'Închide', + prevText: '« Luna precedentă', + nextText: 'Luna următoare »', + currentText: 'Azi', + monthNames: ['Ianuarie','Februarie','Martie','Aprilie','Mai','Iunie', + 'Iulie','August','Septembrie','Octombrie','Noiembrie','Decembrie'], + monthNamesShort: ['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', + 'Iul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + dayNames: ['Duminică', 'Luni', 'Marţi', 'Miercuri', 'Joi', 'Vineri', 'Sâmbătă'], + dayNamesShort: ['Dum', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm'], + dayNamesMin: ['Du','Lu','Ma','Mi','Jo','Vi','Sâ'], + weekHeader: 'Săpt', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['ro']); +}); + +/* Russian (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Andrew Stromnov (stromnov@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['ru'] = { + closeText: 'Закрыть', + prevText: '<Пред', + nextText: 'След>', + currentText: 'Сегодня', + monthNames: ['Январь','Февраль','Март','Апрель','Май','Июнь', + 'Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'], + monthNamesShort: ['Янв','Фев','Мар','Апр','Май','Июн', + 'Июл','Авг','Сен','Окт','Ноя','Дек'], + dayNames: ['воскресенье','понедельник','вторник','среда','четверг','пятница','суббота'], + dayNamesShort: ['вск','пнд','втр','срд','чтв','птн','сбт'], + dayNamesMin: ['Вс','Пн','Вт','Ср','Чт','Пт','Сб'], + weekHeader: 'Нед', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['ru']); +}); + +/* Slovak initialisation for the jQuery UI date picker plugin. */ +/* Written by Vojtech Rinik (vojto@hmm.sk). */ +jQuery(function($){ + $.datepicker.regional['sk'] = { + closeText: 'Zavrieť', + prevText: '<Predchádzajúci', + nextText: 'Nasledujúci>', + currentText: 'Dnes', + monthNames: ['január','február','marec','apríl','máj','jún', + 'júl','august','september','október','november','december'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Máj','Jún', + 'Júl','Aug','Sep','Okt','Nov','Dec'], + dayNames: ['nedeľa','pondelok','utorok','streda','štvrtok','piatok','sobota'], + dayNamesShort: ['Ned','Pon','Uto','Str','Štv','Pia','Sob'], + dayNamesMin: ['Ne','Po','Ut','St','Št','Pia','So'], + weekHeader: 'Ty', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['sk']); +}); + +/* Slovenian initialisation for the jQuery UI date picker plugin. */ +/* Written by Jaka Jancar (jaka@kubje.org). */ +/* c = č, s = š z = ž C = Č S = Š Z = Ž */ +jQuery(function($){ + $.datepicker.regional['sl'] = { + closeText: 'Zapri', + prevText: '<Prejšnji', + nextText: 'Naslednji>', + currentText: 'Trenutni', + monthNames: ['Januar','Februar','Marec','April','Maj','Junij', + 'Julij','Avgust','September','Oktober','November','December'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun', + 'Jul','Avg','Sep','Okt','Nov','Dec'], + dayNames: ['Nedelja','Ponedeljek','Torek','Sreda','Četrtek','Petek','Sobota'], + dayNamesShort: ['Ned','Pon','Tor','Sre','Čet','Pet','Sob'], + dayNamesMin: ['Ne','Po','To','Sr','Če','Pe','So'], + weekHeader: 'Teden', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['sl']); +}); + +/* Albanian initialisation for the jQuery UI date picker plugin. */ +/* Written by Flakron Bytyqi (flakron@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['sq'] = { + closeText: 'mbylle', + prevText: '<mbrapa', + nextText: 'Përpara>', + currentText: 'sot', + monthNames: ['Janar','Shkurt','Mars','Prill','Maj','Qershor', + 'Korrik','Gusht','Shtator','Tetor','Nëntor','Dhjetor'], + monthNamesShort: ['Jan','Shk','Mar','Pri','Maj','Qer', + 'Kor','Gus','Sht','Tet','Nën','Dhj'], + dayNames: ['E Diel','E Hënë','E Martë','E Mërkurë','E Enjte','E Premte','E Shtune'], + dayNamesShort: ['Di','Hë','Ma','Më','En','Pr','Sh'], + dayNamesMin: ['Di','Hë','Ma','Më','En','Pr','Sh'], + weekHeader: 'Ja', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['sq']); +}); + +/* Serbian i18n for the jQuery UI date picker plugin. */ +/* Written by Dejan Dimić. */ +jQuery(function($){ + $.datepicker.regional['sr-SR'] = { + closeText: 'Zatvori', + prevText: '<', + nextText: '>', + currentText: 'Danas', + monthNames: ['Januar','Februar','Mart','April','Maj','Jun', + 'Jul','Avgust','Septembar','Oktobar','Novembar','Decembar'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun', + 'Jul','Avg','Sep','Okt','Nov','Dec'], + dayNames: ['Nedelja','Ponedeljak','Utorak','Sreda','Četvrtak','Petak','Subota'], + dayNamesShort: ['Ned','Pon','Uto','Sre','Čet','Pet','Sub'], + dayNamesMin: ['Ne','Po','Ut','Sr','Če','Pe','Su'], + weekHeader: 'Sed', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['sr-SR']); +}); + +/* Serbian i18n for the jQuery UI date picker plugin. */ +/* Written by Dejan Dimić. */ +jQuery(function($){ + $.datepicker.regional['sr'] = { + closeText: 'Затвори', + prevText: '<', + nextText: '>', + currentText: 'Данас', + monthNames: ['Јануар','Фебруар','Март','Април','Мај','Јун', + 'Јул','Август','Септембар','Октобар','Новембар','Децембар'], + monthNamesShort: ['Јан','Феб','Мар','Апр','Мај','Јун', + 'Јул','Авг','Сеп','Окт','Нов','Дец'], + dayNames: ['Недеља','Понедељак','Уторак','Среда','Четвртак','Петак','Субота'], + dayNamesShort: ['Нед','Пон','Уто','Сре','Чет','Пет','Суб'], + dayNamesMin: ['Не','По','Ут','Ср','Че','Пе','Су'], + weekHeader: 'Сед', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['sr']); +}); + +/* Swedish initialisation for the jQuery UI date picker plugin. */ +/* Written by Anders Ekdahl ( anders@nomadiz.se). */ +jQuery(function($){ + $.datepicker.regional['sv'] = { + closeText: 'Stäng', + prevText: '«Förra', + nextText: 'Nästa»', + currentText: 'Idag', + monthNames: ['Januari','Februari','Mars','April','Maj','Juni', + 'Juli','Augusti','September','Oktober','November','December'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun', + 'Jul','Aug','Sep','Okt','Nov','Dec'], + dayNamesShort: ['Sön','Mån','Tis','Ons','Tor','Fre','Lör'], + dayNames: ['Söndag','Måndag','Tisdag','Onsdag','Torsdag','Fredag','Lördag'], + dayNamesMin: ['Sö','Må','Ti','On','To','Fr','Lö'], + weekHeader: 'Ve', + dateFormat: 'yy-mm-dd', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['sv']); +}); + +/* Tamil (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by S A Sureshkumar (saskumar@live.com). */ +jQuery(function($){ + $.datepicker.regional['ta'] = { + closeText: 'மூடு', + prevText: 'முன்னையது', + nextText: 'அடுத்தது', + currentText: 'இன்று', + monthNames: ['தை','மாசி','பங்குனி','சித்திரை','வைகாசி','ஆனி', + 'ஆடி','ஆவணி','புரட்டாசி','ஐப்பசி','கார்த்திகை','மார்கழி'], + monthNamesShort: ['தை','மாசி','பங்','சித்','வைகா','ஆனி', + 'ஆடி','ஆவ','புர','ஐப்','கார்','மார்'], + dayNames: ['ஞாயிற்றுக்கிழமை','திங்கட்கிழமை','செவ்வாய்க்கிழமை','புதன்கிழமை','வியாழக்கிழமை','வெள்ளிக்கிழமை','சனிக்கிழமை'], + dayNamesShort: ['ஞாயிறு','திங்கள்','செவ்வாய்','புதன்','வியாழன்','வெள்ளி','சனி'], + dayNamesMin: ['ஞா','தி','செ','பு','வி','வெ','ச'], + weekHeader: 'Не', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['ta']); +}); + +/* Thai initialisation for the jQuery UI date picker plugin. */ +/* Written by pipo (pipo@sixhead.com). */ +jQuery(function($){ + $.datepicker.regional['th'] = { + closeText: 'ปิด', + prevText: '« ย้อน', + nextText: 'ถัดไป »', + currentText: 'วันนี้', + monthNames: ['มกราคม','กุมภาพันธ์','มีนาคม','เมษายน','พฤษภาคม','มิถุนายน', + 'กรกฎาคม','สิงหาคม','กันยายน','ตุลาคม','พฤศจิกายน','ธันวาคม'], + monthNamesShort: ['ม.ค.','ก.พ.','มี.ค.','เม.ย.','พ.ค.','มิ.ย.', + 'ก.ค.','ส.ค.','ก.ย.','ต.ค.','พ.ย.','ธ.ค.'], + dayNames: ['อาทิตย์','จันทร์','อังคาร','พุธ','พฤหัสบดี','ศุกร์','เสาร์'], + dayNamesShort: ['อา.','จ.','อ.','พ.','พฤ.','ศ.','ส.'], + dayNamesMin: ['อา.','จ.','อ.','พ.','พฤ.','ศ.','ส.'], + weekHeader: 'Wk', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['th']); +}); + +/* Tajiki (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Abdurahmon Saidov (saidovab@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['tj'] = { + closeText: 'Идома', + prevText: '<Қафо', + nextText: 'Пеш>', + currentText: 'Имрӯз', + monthNames: ['Январ','Феврал','Март','Апрел','Май','Июн', + 'Июл','Август','Сентябр','Октябр','Ноябр','Декабр'], + monthNamesShort: ['Янв','Фев','Мар','Апр','Май','Июн', + 'Июл','Авг','Сен','Окт','Ноя','Дек'], + dayNames: ['якшанбе','душанбе','сешанбе','чоршанбе','панҷшанбе','ҷумъа','шанбе'], + dayNamesShort: ['якш','душ','сеш','чор','пан','ҷум','шан'], + dayNamesMin: ['Як','Дш','Сш','Чш','Пш','Ҷм','Шн'], + weekHeader: 'Хф', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['tj']); +}); + +/* Turkish initialisation for the jQuery UI date picker plugin. */ +/* Written by Izzet Emre Erkan (kara@karalamalar.net). */ +jQuery(function($){ + $.datepicker.regional['tr'] = { + closeText: 'kapat', + prevText: '<geri', + nextText: 'ileri>', + currentText: 'bugün', + monthNames: ['Ocak','Şubat','Mart','Nisan','Mayıs','Haziran', + 'Temmuz','Ağustos','Eylül','Ekim','Kasım','Aralık'], + monthNamesShort: ['Oca','Şub','Mar','Nis','May','Haz', + 'Tem','Ağu','Eyl','Eki','Kas','Ara'], + dayNames: ['Pazar','Pazartesi','Salı','Çarşamba','Perşembe','Cuma','Cumartesi'], + dayNamesShort: ['Pz','Pt','Sa','Ça','Pe','Cu','Ct'], + dayNamesMin: ['Pz','Pt','Sa','Ça','Pe','Cu','Ct'], + weekHeader: 'Hf', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['tr']); +}); + +/* Ukrainian (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Maxim Drogobitskiy (maxdao@gmail.com). */ +/* Corrected by Igor Milla (igor.fsp.milla@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['uk'] = { + closeText: 'Закрити', + prevText: '<', + nextText: '>', + currentText: 'Сьогодні', + monthNames: ['Січень','Лютий','Березень','Квітень','Травень','Червень', + 'Липень','Серпень','Вересень','Жовтень','Листопад','Грудень'], + monthNamesShort: ['Січ','Лют','Бер','Кві','Тра','Чер', + 'Лип','Сер','Вер','Жов','Лис','Гру'], + dayNames: ['неділя','понеділок','вівторок','середа','четвер','п’ятниця','субота'], + dayNamesShort: ['нед','пнд','вів','срд','чтв','птн','сбт'], + dayNamesMin: ['Нд','Пн','Вт','Ср','Чт','Пт','Сб'], + weekHeader: 'Тиж', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['uk']); +}); + +/* Vietnamese initialisation for the jQuery UI date picker plugin. */ +/* Translated by Le Thanh Huy (lthanhhuy@cit.ctu.edu.vn). */ +jQuery(function($){ + $.datepicker.regional['vi'] = { + closeText: 'Đóng', + prevText: '<Trước', + nextText: 'Tiếp>', + currentText: 'Hôm nay', + monthNames: ['Tháng Một', 'Tháng Hai', 'Tháng Ba', 'Tháng Tư', 'Tháng Năm', 'Tháng Sáu', + 'Tháng Bảy', 'Tháng Tám', 'Tháng Chín', 'Tháng Mười', 'Tháng Mười Một', 'Tháng Mười Hai'], + monthNamesShort: ['Tháng 1', 'Tháng 2', 'Tháng 3', 'Tháng 4', 'Tháng 5', 'Tháng 6', + 'Tháng 7', 'Tháng 8', 'Tháng 9', 'Tháng 10', 'Tháng 11', 'Tháng 12'], + dayNames: ['Chủ Nhật', 'Thứ Hai', 'Thứ Ba', 'Thứ Tư', 'Thứ Năm', 'Thứ Sáu', 'Thứ Bảy'], + dayNamesShort: ['CN', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7'], + dayNamesMin: ['CN', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7'], + weekHeader: 'Tu', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['vi']); +}); + +/* Chinese initialisation for the jQuery UI date picker plugin. */ +/* Written by Cloudream (cloudream@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['zh-CN'] = { + closeText: '关闭', + prevText: '<上月', + nextText: '下月>', + currentText: '今天', + monthNames: ['一月','二月','三月','四月','五月','六月', + '七月','八月','九月','十月','十一月','十二月'], + monthNamesShort: ['一月','二月','三月','四月','五月','六月', + '七月','八月','九月','十月','十一月','十二月'], + dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'], + dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'], + dayNamesMin: ['日','一','二','三','四','五','六'], + weekHeader: '周', + dateFormat: 'yy-mm-dd', + firstDay: 1, + isRTL: false, + showMonthAfterYear: true, + yearSuffix: '年'}; + $.datepicker.setDefaults($.datepicker.regional['zh-CN']); +}); + +/* Chinese initialisation for the jQuery UI date picker plugin. */ +/* Written by SCCY (samuelcychan@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['zh-HK'] = { + closeText: '關閉', + prevText: '<上月', + nextText: '下月>', + currentText: '今天', + monthNames: ['一月','二月','三月','四月','五月','六月', + '七月','八月','九月','十月','十一月','十二月'], + monthNamesShort: ['一月','二月','三月','四月','五月','六月', + '七月','八月','九月','十月','十一月','十二月'], + dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'], + dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'], + dayNamesMin: ['日','一','二','三','四','五','六'], + weekHeader: '周', + dateFormat: 'dd-mm-yy', + firstDay: 0, + isRTL: false, + showMonthAfterYear: true, + yearSuffix: '年'}; + $.datepicker.setDefaults($.datepicker.regional['zh-HK']); +}); + +/* Chinese initialisation for the jQuery UI date picker plugin. */ +/* Written by Ressol (ressol@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['zh-TW'] = { + closeText: '關閉', + prevText: '<上月', + nextText: '下月>', + currentText: '今天', + monthNames: ['一月','二月','三月','四月','五月','六月', + '七月','八月','九月','十月','十一月','十二月'], + monthNamesShort: ['一月','二月','三月','四月','五月','六月', + '七月','八月','九月','十月','十一月','十二月'], + dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'], + dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'], + dayNamesMin: ['日','一','二','三','四','五','六'], + weekHeader: '周', + dateFormat: 'yy/mm/dd', + firstDay: 1, + isRTL: false, + showMonthAfterYear: true, + yearSuffix: '年'}; + $.datepicker.setDefaults($.datepicker.regional['zh-TW']); +}); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.datepicker.js b/extensions/jui/yii/jui/assets/jquery.ui.datepicker.js new file mode 100644 index 0000000..8afddbd --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.datepicker.js @@ -0,0 +1,2038 @@ +/*! + * jQuery UI Datepicker 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/datepicker/ + * + * Depends: + * jquery.ui.core.js + */ +(function( $, undefined ) { + +$.extend($.ui, { datepicker: { version: "1.10.3" } }); + +var PROP_NAME = "datepicker", + instActive; + +/* Date picker manager. + Use the singleton instance of this class, $.datepicker, to interact with the date picker. + Settings for (groups of) date pickers are maintained in an instance object, + allowing multiple different settings on the same page. */ + +function Datepicker() { + this._curInst = null; // The current instance in use + this._keyEvent = false; // If the last event was a key event + this._disabledInputs = []; // List of date picker inputs that have been disabled + this._datepickerShowing = false; // True if the popup picker is showing , false if not + this._inDialog = false; // True if showing within a "dialog", false if not + this._mainDivId = "ui-datepicker-div"; // The ID of the main datepicker division + this._inlineClass = "ui-datepicker-inline"; // The name of the inline marker class + this._appendClass = "ui-datepicker-append"; // The name of the append marker class + this._triggerClass = "ui-datepicker-trigger"; // The name of the trigger marker class + this._dialogClass = "ui-datepicker-dialog"; // The name of the dialog marker class + this._disableClass = "ui-datepicker-disabled"; // The name of the disabled covering marker class + this._unselectableClass = "ui-datepicker-unselectable"; // The name of the unselectable cell marker class + this._currentClass = "ui-datepicker-current-day"; // The name of the current day marker class + this._dayOverClass = "ui-datepicker-days-cell-over"; // The name of the day hover marker class + this.regional = []; // Available regional settings, indexed by language code + this.regional[""] = { // Default regional settings + closeText: "Done", // Display text for close link + prevText: "Prev", // Display text for previous month link + nextText: "Next", // Display text for next month link + currentText: "Today", // Display text for current month link + monthNames: ["January","February","March","April","May","June", + "July","August","September","October","November","December"], // Names of months for drop-down and formatting + monthNamesShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], // For formatting + dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], // For formatting + dayNamesShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], // For formatting + dayNamesMin: ["Su","Mo","Tu","We","Th","Fr","Sa"], // Column headings for days starting at Sunday + weekHeader: "Wk", // Column header for week of the year + dateFormat: "mm/dd/yy", // See format options on parseDate + firstDay: 0, // The first day of the week, Sun = 0, Mon = 1, ... + isRTL: false, // True if right-to-left language, false if left-to-right + showMonthAfterYear: false, // True if the year select precedes month, false for month then year + yearSuffix: "" // Additional text to append to the year in the month headers + }; + this._defaults = { // Global defaults for all the date picker instances + showOn: "focus", // "focus" for popup on focus, + // "button" for trigger button, or "both" for either + showAnim: "fadeIn", // Name of jQuery animation for popup + showOptions: {}, // Options for enhanced animations + defaultDate: null, // Used when field is blank: actual date, + // +/-number for offset from today, null for today + appendText: "", // Display text following the input box, e.g. showing the format + buttonText: "...", // Text for trigger button + buttonImage: "", // URL for trigger button image + buttonImageOnly: false, // True if the image appears alone, false if it appears on a button + hideIfNoPrevNext: false, // True to hide next/previous month links + // if not applicable, false to just disable them + navigationAsDateFormat: false, // True if date formatting applied to prev/today/next links + gotoCurrent: false, // True if today link goes back to current selection instead + changeMonth: false, // True if month can be selected directly, false if only prev/next + changeYear: false, // True if year can be selected directly, false if only prev/next + yearRange: "c-10:c+10", // Range of years to display in drop-down, + // either relative to today's year (-nn:+nn), relative to currently displayed year + // (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n) + showOtherMonths: false, // True to show dates in other months, false to leave blank + selectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable + showWeek: false, // True to show week of the year, false to not show it + calculateWeek: this.iso8601Week, // How to calculate the week of the year, + // takes a Date and returns the number of the week for it + shortYearCutoff: "+10", // Short year values < this are in the current century, + // > this are in the previous century, + // string value starting with "+" for current year + value + minDate: null, // The earliest selectable date, or null for no limit + maxDate: null, // The latest selectable date, or null for no limit + duration: "fast", // Duration of display/closure + beforeShowDay: null, // Function that takes a date and returns an array with + // [0] = true if selectable, false if not, [1] = custom CSS class name(s) or "", + // [2] = cell title (optional), e.g. $.datepicker.noWeekends + beforeShow: null, // Function that takes an input field and + // returns a set of custom settings for the date picker + onSelect: null, // Define a callback function when a date is selected + onChangeMonthYear: null, // Define a callback function when the month or year is changed + onClose: null, // Define a callback function when the datepicker is closed + numberOfMonths: 1, // Number of months to show at a time + showCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0) + stepMonths: 1, // Number of months to step back/forward + stepBigMonths: 12, // Number of months to step back/forward for the big links + altField: "", // Selector for an alternate field to store selected dates into + altFormat: "", // The date format to use for the alternate field + constrainInput: true, // The input is constrained by the current date format + showButtonPanel: false, // True to show button panel, false to not show it + autoSize: false, // True to size the input for the date format, false to leave as is + disabled: false // The initial disabled state + }; + $.extend(this._defaults, this.regional[""]); + this.dpDiv = bindHover($("<div id='" + this._mainDivId + "' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")); +} + +$.extend(Datepicker.prototype, { + /* Class name added to elements to indicate already configured with a date picker. */ + markerClassName: "hasDatepicker", + + //Keep track of the maximum number of rows displayed (see #7043) + maxRows: 4, + + // TODO rename to "widget" when switching to widget factory + _widgetDatepicker: function() { + return this.dpDiv; + }, + + /* Override the default settings for all instances of the date picker. + * @param settings object - the new settings to use as defaults (anonymous object) + * @return the manager object + */ + setDefaults: function(settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + /* Attach the date picker to a jQuery selection. + * @param target element - the target input field or division or span + * @param settings object - the new settings to use for this date picker instance (anonymous) + */ + _attachDatepicker: function(target, settings) { + var nodeName, inline, inst; + nodeName = target.nodeName.toLowerCase(); + inline = (nodeName === "div" || nodeName === "span"); + if (!target.id) { + this.uuid += 1; + target.id = "dp" + this.uuid; + } + inst = this._newInst($(target), inline); + inst.settings = $.extend({}, settings || {}); + if (nodeName === "input") { + this._connectDatepicker(target, inst); + } else if (inline) { + this._inlineDatepicker(target, inst); + } + }, + + /* Create a new instance object. */ + _newInst: function(target, inline) { + var id = target[0].id.replace(/([^A-Za-z0-9_\-])/g, "\\\\$1"); // escape jQuery meta chars + return {id: id, input: target, // associated target + selectedDay: 0, selectedMonth: 0, selectedYear: 0, // current selection + drawMonth: 0, drawYear: 0, // month being drawn + inline: inline, // is datepicker inline or not + dpDiv: (!inline ? this.dpDiv : // presentation div + bindHover($("<div class='" + this._inlineClass + " ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")))}; + }, + + /* Attach the date picker to an input field. */ + _connectDatepicker: function(target, inst) { + var input = $(target); + inst.append = $([]); + inst.trigger = $([]); + if (input.hasClass(this.markerClassName)) { + return; + } + this._attachments(input, inst); + input.addClass(this.markerClassName).keydown(this._doKeyDown). + keypress(this._doKeyPress).keyup(this._doKeyUp); + this._autoSize(inst); + $.data(target, PROP_NAME, inst); + //If disabled option is true, disable the datepicker once it has been attached to the input (see ticket #5665) + if( inst.settings.disabled ) { + this._disableDatepicker( target ); + } + }, + + /* Make attachments based on settings. */ + _attachments: function(input, inst) { + var showOn, buttonText, buttonImage, + appendText = this._get(inst, "appendText"), + isRTL = this._get(inst, "isRTL"); + + if (inst.append) { + inst.append.remove(); + } + if (appendText) { + inst.append = $("<span class='" + this._appendClass + "'>" + appendText + "</span>"); + input[isRTL ? "before" : "after"](inst.append); + } + + input.unbind("focus", this._showDatepicker); + + if (inst.trigger) { + inst.trigger.remove(); + } + + showOn = this._get(inst, "showOn"); + if (showOn === "focus" || showOn === "both") { // pop-up date picker when in the marked field + input.focus(this._showDatepicker); + } + if (showOn === "button" || showOn === "both") { // pop-up date picker when button clicked + buttonText = this._get(inst, "buttonText"); + buttonImage = this._get(inst, "buttonImage"); + inst.trigger = $(this._get(inst, "buttonImageOnly") ? + $("<img/>").addClass(this._triggerClass). + attr({ src: buttonImage, alt: buttonText, title: buttonText }) : + $("<button type='button'></button>").addClass(this._triggerClass). + html(!buttonImage ? buttonText : $("<img/>").attr( + { src:buttonImage, alt:buttonText, title:buttonText }))); + input[isRTL ? "before" : "after"](inst.trigger); + inst.trigger.click(function() { + if ($.datepicker._datepickerShowing && $.datepicker._lastInput === input[0]) { + $.datepicker._hideDatepicker(); + } else if ($.datepicker._datepickerShowing && $.datepicker._lastInput !== input[0]) { + $.datepicker._hideDatepicker(); + $.datepicker._showDatepicker(input[0]); + } else { + $.datepicker._showDatepicker(input[0]); + } + return false; + }); + } + }, + + /* Apply the maximum length for the date format. */ + _autoSize: function(inst) { + if (this._get(inst, "autoSize") && !inst.inline) { + var findMax, max, maxI, i, + date = new Date(2009, 12 - 1, 20), // Ensure double digits + dateFormat = this._get(inst, "dateFormat"); + + if (dateFormat.match(/[DM]/)) { + findMax = function(names) { + max = 0; + maxI = 0; + for (i = 0; i < names.length; i++) { + if (names[i].length > max) { + max = names[i].length; + maxI = i; + } + } + return maxI; + }; + date.setMonth(findMax(this._get(inst, (dateFormat.match(/MM/) ? + "monthNames" : "monthNamesShort")))); + date.setDate(findMax(this._get(inst, (dateFormat.match(/DD/) ? + "dayNames" : "dayNamesShort"))) + 20 - date.getDay()); + } + inst.input.attr("size", this._formatDate(inst, date).length); + } + }, + + /* Attach an inline date picker to a div. */ + _inlineDatepicker: function(target, inst) { + var divSpan = $(target); + if (divSpan.hasClass(this.markerClassName)) { + return; + } + divSpan.addClass(this.markerClassName).append(inst.dpDiv); + $.data(target, PROP_NAME, inst); + this._setDate(inst, this._getDefaultDate(inst), true); + this._updateDatepicker(inst); + this._updateAlternate(inst); + //If disabled option is true, disable the datepicker before showing it (see ticket #5665) + if( inst.settings.disabled ) { + this._disableDatepicker( target ); + } + // Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements + // http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height + inst.dpDiv.css( "display", "block" ); + }, + + /* Pop-up the date picker in a "dialog" box. + * @param input element - ignored + * @param date string or Date - the initial date to display + * @param onSelect function - the function to call when a date is selected + * @param settings object - update the dialog date picker instance's settings (anonymous object) + * @param pos int[2] - coordinates for the dialog's position within the screen or + * event - with x/y coordinates or + * leave empty for default (screen centre) + * @return the manager object + */ + _dialogDatepicker: function(input, date, onSelect, settings, pos) { + var id, browserWidth, browserHeight, scrollX, scrollY, + inst = this._dialogInst; // internal instance + + if (!inst) { + this.uuid += 1; + id = "dp" + this.uuid; + this._dialogInput = $("<input type='text' id='" + id + + "' style='position: absolute; top: -100px; width: 0px;'/>"); + this._dialogInput.keydown(this._doKeyDown); + $("body").append(this._dialogInput); + inst = this._dialogInst = this._newInst(this._dialogInput, false); + inst.settings = {}; + $.data(this._dialogInput[0], PROP_NAME, inst); + } + extendRemove(inst.settings, settings || {}); + date = (date && date.constructor === Date ? this._formatDate(inst, date) : date); + this._dialogInput.val(date); + + this._pos = (pos ? (pos.length ? pos : [pos.pageX, pos.pageY]) : null); + if (!this._pos) { + browserWidth = document.documentElement.clientWidth; + browserHeight = document.documentElement.clientHeight; + scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; + scrollY = document.documentElement.scrollTop || document.body.scrollTop; + this._pos = // should use actual width/height below + [(browserWidth / 2) - 100 + scrollX, (browserHeight / 2) - 150 + scrollY]; + } + + // move input on screen for focus, but hidden behind dialog + this._dialogInput.css("left", (this._pos[0] + 20) + "px").css("top", this._pos[1] + "px"); + inst.settings.onSelect = onSelect; + this._inDialog = true; + this.dpDiv.addClass(this._dialogClass); + this._showDatepicker(this._dialogInput[0]); + if ($.blockUI) { + $.blockUI(this.dpDiv); + } + $.data(this._dialogInput[0], PROP_NAME, inst); + return this; + }, + + /* Detach a datepicker from its control. + * @param target element - the target input field or division or span + */ + _destroyDatepicker: function(target) { + var nodeName, + $target = $(target), + inst = $.data(target, PROP_NAME); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + $.removeData(target, PROP_NAME); + if (nodeName === "input") { + inst.append.remove(); + inst.trigger.remove(); + $target.removeClass(this.markerClassName). + unbind("focus", this._showDatepicker). + unbind("keydown", this._doKeyDown). + unbind("keypress", this._doKeyPress). + unbind("keyup", this._doKeyUp); + } else if (nodeName === "div" || nodeName === "span") { + $target.removeClass(this.markerClassName).empty(); + } + }, + + /* Enable the date picker to a jQuery selection. + * @param target element - the target input field or division or span + */ + _enableDatepicker: function(target) { + var nodeName, inline, + $target = $(target), + inst = $.data(target, PROP_NAME); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + if (nodeName === "input") { + target.disabled = false; + inst.trigger.filter("button"). + each(function() { this.disabled = false; }).end(). + filter("img").css({opacity: "1.0", cursor: ""}); + } else if (nodeName === "div" || nodeName === "span") { + inline = $target.children("." + this._inlineClass); + inline.children().removeClass("ui-state-disabled"); + inline.find("select.ui-datepicker-month, select.ui-datepicker-year"). + prop("disabled", false); + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value === target ? null : value); }); // delete entry + }, + + /* Disable the date picker to a jQuery selection. + * @param target element - the target input field or division or span + */ + _disableDatepicker: function(target) { + var nodeName, inline, + $target = $(target), + inst = $.data(target, PROP_NAME); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + if (nodeName === "input") { + target.disabled = true; + inst.trigger.filter("button"). + each(function() { this.disabled = true; }).end(). + filter("img").css({opacity: "0.5", cursor: "default"}); + } else if (nodeName === "div" || nodeName === "span") { + inline = $target.children("." + this._inlineClass); + inline.children().addClass("ui-state-disabled"); + inline.find("select.ui-datepicker-month, select.ui-datepicker-year"). + prop("disabled", true); + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value === target ? null : value); }); // delete entry + this._disabledInputs[this._disabledInputs.length] = target; + }, + + /* Is the first field in a jQuery collection disabled as a datepicker? + * @param target element - the target input field or division or span + * @return boolean - true if disabled, false if enabled + */ + _isDisabledDatepicker: function(target) { + if (!target) { + return false; + } + for (var i = 0; i < this._disabledInputs.length; i++) { + if (this._disabledInputs[i] === target) { + return true; + } + } + return false; + }, + + /* Retrieve the instance data for the target control. + * @param target element - the target input field or division or span + * @return object - the associated instance data + * @throws error if a jQuery problem getting data + */ + _getInst: function(target) { + try { + return $.data(target, PROP_NAME); + } + catch (err) { + throw "Missing instance data for this datepicker"; + } + }, + + /* Update or retrieve the settings for a date picker attached to an input field or division. + * @param target element - the target input field or division or span + * @param name object - the new settings to update or + * string - the name of the setting to change or retrieve, + * when retrieving also "all" for all instance settings or + * "defaults" for all global defaults + * @param value any - the new value for the setting + * (omit if above is an object or to retrieve a value) + */ + _optionDatepicker: function(target, name, value) { + var settings, date, minDate, maxDate, + inst = this._getInst(target); + + if (arguments.length === 2 && typeof name === "string") { + return (name === "defaults" ? $.extend({}, $.datepicker._defaults) : + (inst ? (name === "all" ? $.extend({}, inst.settings) : + this._get(inst, name)) : null)); + } + + settings = name || {}; + if (typeof name === "string") { + settings = {}; + settings[name] = value; + } + + if (inst) { + if (this._curInst === inst) { + this._hideDatepicker(); + } + + date = this._getDateDatepicker(target, true); + minDate = this._getMinMaxDate(inst, "min"); + maxDate = this._getMinMaxDate(inst, "max"); + extendRemove(inst.settings, settings); + // reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided + if (minDate !== null && settings.dateFormat !== undefined && settings.minDate === undefined) { + inst.settings.minDate = this._formatDate(inst, minDate); + } + if (maxDate !== null && settings.dateFormat !== undefined && settings.maxDate === undefined) { + inst.settings.maxDate = this._formatDate(inst, maxDate); + } + if ( "disabled" in settings ) { + if ( settings.disabled ) { + this._disableDatepicker(target); + } else { + this._enableDatepicker(target); + } + } + this._attachments($(target), inst); + this._autoSize(inst); + this._setDate(inst, date); + this._updateAlternate(inst); + this._updateDatepicker(inst); + } + }, + + // change method deprecated + _changeDatepicker: function(target, name, value) { + this._optionDatepicker(target, name, value); + }, + + /* Redraw the date picker attached to an input field or division. + * @param target element - the target input field or division or span + */ + _refreshDatepicker: function(target) { + var inst = this._getInst(target); + if (inst) { + this._updateDatepicker(inst); + } + }, + + /* Set the dates for a jQuery selection. + * @param target element - the target input field or division or span + * @param date Date - the new date + */ + _setDateDatepicker: function(target, date) { + var inst = this._getInst(target); + if (inst) { + this._setDate(inst, date); + this._updateDatepicker(inst); + this._updateAlternate(inst); + } + }, + + /* Get the date(s) for the first entry in a jQuery selection. + * @param target element - the target input field or division or span + * @param noDefault boolean - true if no default date is to be used + * @return Date - the current date + */ + _getDateDatepicker: function(target, noDefault) { + var inst = this._getInst(target); + if (inst && !inst.inline) { + this._setDateFromField(inst, noDefault); + } + return (inst ? this._getDate(inst) : null); + }, + + /* Handle keystrokes. */ + _doKeyDown: function(event) { + var onSelect, dateStr, sel, + inst = $.datepicker._getInst(event.target), + handled = true, + isRTL = inst.dpDiv.is(".ui-datepicker-rtl"); + + inst._keyEvent = true; + if ($.datepicker._datepickerShowing) { + switch (event.keyCode) { + case 9: $.datepicker._hideDatepicker(); + handled = false; + break; // hide on tab out + case 13: sel = $("td." + $.datepicker._dayOverClass + ":not(." + + $.datepicker._currentClass + ")", inst.dpDiv); + if (sel[0]) { + $.datepicker._selectDay(event.target, inst.selectedMonth, inst.selectedYear, sel[0]); + } + + onSelect = $.datepicker._get(inst, "onSelect"); + if (onSelect) { + dateStr = $.datepicker._formatDate(inst); + + // trigger custom callback + onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); + } else { + $.datepicker._hideDatepicker(); + } + + return false; // don't submit the form + case 27: $.datepicker._hideDatepicker(); + break; // hide on escape + case 33: $.datepicker._adjustDate(event.target, (event.ctrlKey ? + -$.datepicker._get(inst, "stepBigMonths") : + -$.datepicker._get(inst, "stepMonths")), "M"); + break; // previous month/year on page up/+ ctrl + case 34: $.datepicker._adjustDate(event.target, (event.ctrlKey ? + +$.datepicker._get(inst, "stepBigMonths") : + +$.datepicker._get(inst, "stepMonths")), "M"); + break; // next month/year on page down/+ ctrl + case 35: if (event.ctrlKey || event.metaKey) { + $.datepicker._clearDate(event.target); + } + handled = event.ctrlKey || event.metaKey; + break; // clear on ctrl or command +end + case 36: if (event.ctrlKey || event.metaKey) { + $.datepicker._gotoToday(event.target); + } + handled = event.ctrlKey || event.metaKey; + break; // current on ctrl or command +home + case 37: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, (isRTL ? +1 : -1), "D"); + } + handled = event.ctrlKey || event.metaKey; + // -1 day on ctrl or command +left + if (event.originalEvent.altKey) { + $.datepicker._adjustDate(event.target, (event.ctrlKey ? + -$.datepicker._get(inst, "stepBigMonths") : + -$.datepicker._get(inst, "stepMonths")), "M"); + } + // next month/year on alt +left on Mac + break; + case 38: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, -7, "D"); + } + handled = event.ctrlKey || event.metaKey; + break; // -1 week on ctrl or command +up + case 39: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, (isRTL ? -1 : +1), "D"); + } + handled = event.ctrlKey || event.metaKey; + // +1 day on ctrl or command +right + if (event.originalEvent.altKey) { + $.datepicker._adjustDate(event.target, (event.ctrlKey ? + +$.datepicker._get(inst, "stepBigMonths") : + +$.datepicker._get(inst, "stepMonths")), "M"); + } + // next month/year on alt +right + break; + case 40: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, +7, "D"); + } + handled = event.ctrlKey || event.metaKey; + break; // +1 week on ctrl or command +down + default: handled = false; + } + } else if (event.keyCode === 36 && event.ctrlKey) { // display the date picker on ctrl+home + $.datepicker._showDatepicker(this); + } else { + handled = false; + } + + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + }, + + /* Filter entered characters - based on date format. */ + _doKeyPress: function(event) { + var chars, chr, + inst = $.datepicker._getInst(event.target); + + if ($.datepicker._get(inst, "constrainInput")) { + chars = $.datepicker._possibleChars($.datepicker._get(inst, "dateFormat")); + chr = String.fromCharCode(event.charCode == null ? event.keyCode : event.charCode); + return event.ctrlKey || event.metaKey || (chr < " " || !chars || chars.indexOf(chr) > -1); + } + }, + + /* Synchronise manual entry and field/alternate field. */ + _doKeyUp: function(event) { + var date, + inst = $.datepicker._getInst(event.target); + + if (inst.input.val() !== inst.lastVal) { + try { + date = $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"), + (inst.input ? inst.input.val() : null), + $.datepicker._getFormatConfig(inst)); + + if (date) { // only if valid + $.datepicker._setDateFromField(inst); + $.datepicker._updateAlternate(inst); + $.datepicker._updateDatepicker(inst); + } + } + catch (err) { + } + } + return true; + }, + + /* Pop-up the date picker for a given input field. + * If false returned from beforeShow event handler do not show. + * @param input element - the input field attached to the date picker or + * event - if triggered by focus + */ + _showDatepicker: function(input) { + input = input.target || input; + if (input.nodeName.toLowerCase() !== "input") { // find from button/image trigger + input = $("input", input.parentNode)[0]; + } + + if ($.datepicker._isDisabledDatepicker(input) || $.datepicker._lastInput === input) { // already here + return; + } + + var inst, beforeShow, beforeShowSettings, isFixed, + offset, showAnim, duration; + + inst = $.datepicker._getInst(input); + if ($.datepicker._curInst && $.datepicker._curInst !== inst) { + $.datepicker._curInst.dpDiv.stop(true, true); + if ( inst && $.datepicker._datepickerShowing ) { + $.datepicker._hideDatepicker( $.datepicker._curInst.input[0] ); + } + } + + beforeShow = $.datepicker._get(inst, "beforeShow"); + beforeShowSettings = beforeShow ? beforeShow.apply(input, [input, inst]) : {}; + if(beforeShowSettings === false){ + return; + } + extendRemove(inst.settings, beforeShowSettings); + + inst.lastVal = null; + $.datepicker._lastInput = input; + $.datepicker._setDateFromField(inst); + + if ($.datepicker._inDialog) { // hide cursor + input.value = ""; + } + if (!$.datepicker._pos) { // position below input + $.datepicker._pos = $.datepicker._findPos(input); + $.datepicker._pos[1] += input.offsetHeight; // add the height + } + + isFixed = false; + $(input).parents().each(function() { + isFixed |= $(this).css("position") === "fixed"; + return !isFixed; + }); + + offset = {left: $.datepicker._pos[0], top: $.datepicker._pos[1]}; + $.datepicker._pos = null; + //to avoid flashes on Firefox + inst.dpDiv.empty(); + // determine sizing offscreen + inst.dpDiv.css({position: "absolute", display: "block", top: "-1000px"}); + $.datepicker._updateDatepicker(inst); + // fix width for dynamic number of date pickers + // and adjust position before showing + offset = $.datepicker._checkOffset(inst, offset, isFixed); + inst.dpDiv.css({position: ($.datepicker._inDialog && $.blockUI ? + "static" : (isFixed ? "fixed" : "absolute")), display: "none", + left: offset.left + "px", top: offset.top + "px"}); + + if (!inst.inline) { + showAnim = $.datepicker._get(inst, "showAnim"); + duration = $.datepicker._get(inst, "duration"); + inst.dpDiv.zIndex($(input).zIndex()+1); + $.datepicker._datepickerShowing = true; + + if ( $.effects && $.effects.effect[ showAnim ] ) { + inst.dpDiv.show(showAnim, $.datepicker._get(inst, "showOptions"), duration); + } else { + inst.dpDiv[showAnim || "show"](showAnim ? duration : null); + } + + if ( $.datepicker._shouldFocusInput( inst ) ) { + inst.input.focus(); + } + + $.datepicker._curInst = inst; + } + }, + + /* Generate the date picker content. */ + _updateDatepicker: function(inst) { + this.maxRows = 4; //Reset the max number of rows being displayed (see #7043) + instActive = inst; // for delegate hover events + inst.dpDiv.empty().append(this._generateHTML(inst)); + this._attachHandlers(inst); + inst.dpDiv.find("." + this._dayOverClass + " a").mouseover(); + + var origyearshtml, + numMonths = this._getNumberOfMonths(inst), + cols = numMonths[1], + width = 17; + + inst.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""); + if (cols > 1) { + inst.dpDiv.addClass("ui-datepicker-multi-" + cols).css("width", (width * cols) + "em"); + } + inst.dpDiv[(numMonths[0] !== 1 || numMonths[1] !== 1 ? "add" : "remove") + + "Class"]("ui-datepicker-multi"); + inst.dpDiv[(this._get(inst, "isRTL") ? "add" : "remove") + + "Class"]("ui-datepicker-rtl"); + + if (inst === $.datepicker._curInst && $.datepicker._datepickerShowing && $.datepicker._shouldFocusInput( inst ) ) { + inst.input.focus(); + } + + // deffered render of the years select (to avoid flashes on Firefox) + if( inst.yearshtml ){ + origyearshtml = inst.yearshtml; + setTimeout(function(){ + //assure that inst.yearshtml didn't change. + if( origyearshtml === inst.yearshtml && inst.yearshtml ){ + inst.dpDiv.find("select.ui-datepicker-year:first").replaceWith(inst.yearshtml); + } + origyearshtml = inst.yearshtml = null; + }, 0); + } + }, + + // #6694 - don't focus the input if it's already focused + // this breaks the change event in IE + // Support: IE and jQuery <1.9 + _shouldFocusInput: function( inst ) { + return inst.input && inst.input.is( ":visible" ) && !inst.input.is( ":disabled" ) && !inst.input.is( ":focus" ); + }, + + /* Check positioning to remain on screen. */ + _checkOffset: function(inst, offset, isFixed) { + var dpWidth = inst.dpDiv.outerWidth(), + dpHeight = inst.dpDiv.outerHeight(), + inputWidth = inst.input ? inst.input.outerWidth() : 0, + inputHeight = inst.input ? inst.input.outerHeight() : 0, + viewWidth = document.documentElement.clientWidth + (isFixed ? 0 : $(document).scrollLeft()), + viewHeight = document.documentElement.clientHeight + (isFixed ? 0 : $(document).scrollTop()); + + offset.left -= (this._get(inst, "isRTL") ? (dpWidth - inputWidth) : 0); + offset.left -= (isFixed && offset.left === inst.input.offset().left) ? $(document).scrollLeft() : 0; + offset.top -= (isFixed && offset.top === (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0; + + // now check if datepicker is showing outside window viewport - move to a better place if so. + offset.left -= Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ? + Math.abs(offset.left + dpWidth - viewWidth) : 0); + offset.top -= Math.min(offset.top, (offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ? + Math.abs(dpHeight + inputHeight) : 0); + + return offset; + }, + + /* Find an object's position on the screen. */ + _findPos: function(obj) { + var position, + inst = this._getInst(obj), + isRTL = this._get(inst, "isRTL"); + + while (obj && (obj.type === "hidden" || obj.nodeType !== 1 || $.expr.filters.hidden(obj))) { + obj = obj[isRTL ? "previousSibling" : "nextSibling"]; + } + + position = $(obj).offset(); + return [position.left, position.top]; + }, + + /* Hide the date picker from view. + * @param input element - the input field attached to the date picker + */ + _hideDatepicker: function(input) { + var showAnim, duration, postProcess, onClose, + inst = this._curInst; + + if (!inst || (input && inst !== $.data(input, PROP_NAME))) { + return; + } + + if (this._datepickerShowing) { + showAnim = this._get(inst, "showAnim"); + duration = this._get(inst, "duration"); + postProcess = function() { + $.datepicker._tidyDialog(inst); + }; + + // DEPRECATED: after BC for 1.8.x $.effects[ showAnim ] is not needed + if ( $.effects && ( $.effects.effect[ showAnim ] || $.effects[ showAnim ] ) ) { + inst.dpDiv.hide(showAnim, $.datepicker._get(inst, "showOptions"), duration, postProcess); + } else { + inst.dpDiv[(showAnim === "slideDown" ? "slideUp" : + (showAnim === "fadeIn" ? "fadeOut" : "hide"))]((showAnim ? duration : null), postProcess); + } + + if (!showAnim) { + postProcess(); + } + this._datepickerShowing = false; + + onClose = this._get(inst, "onClose"); + if (onClose) { + onClose.apply((inst.input ? inst.input[0] : null), [(inst.input ? inst.input.val() : ""), inst]); + } + + this._lastInput = null; + if (this._inDialog) { + this._dialogInput.css({ position: "absolute", left: "0", top: "-100px" }); + if ($.blockUI) { + $.unblockUI(); + $("body").append(this.dpDiv); + } + } + this._inDialog = false; + } + }, + + /* Tidy up after a dialog display. */ + _tidyDialog: function(inst) { + inst.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar"); + }, + + /* Close date picker if clicked elsewhere. */ + _checkExternalClick: function(event) { + if (!$.datepicker._curInst) { + return; + } + + var $target = $(event.target), + inst = $.datepicker._getInst($target[0]); + + if ( ( ( $target[0].id !== $.datepicker._mainDivId && + $target.parents("#" + $.datepicker._mainDivId).length === 0 && + !$target.hasClass($.datepicker.markerClassName) && + !$target.closest("." + $.datepicker._triggerClass).length && + $.datepicker._datepickerShowing && !($.datepicker._inDialog && $.blockUI) ) ) || + ( $target.hasClass($.datepicker.markerClassName) && $.datepicker._curInst !== inst ) ) { + $.datepicker._hideDatepicker(); + } + }, + + /* Adjust one of the date sub-fields. */ + _adjustDate: function(id, offset, period) { + var target = $(id), + inst = this._getInst(target[0]); + + if (this._isDisabledDatepicker(target[0])) { + return; + } + this._adjustInstDate(inst, offset + + (period === "M" ? this._get(inst, "showCurrentAtPos") : 0), // undo positioning + period); + this._updateDatepicker(inst); + }, + + /* Action for current link. */ + _gotoToday: function(id) { + var date, + target = $(id), + inst = this._getInst(target[0]); + + if (this._get(inst, "gotoCurrent") && inst.currentDay) { + inst.selectedDay = inst.currentDay; + inst.drawMonth = inst.selectedMonth = inst.currentMonth; + inst.drawYear = inst.selectedYear = inst.currentYear; + } else { + date = new Date(); + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + } + this._notifyChange(inst); + this._adjustDate(target); + }, + + /* Action for selecting a new month/year. */ + _selectMonthYear: function(id, select, period) { + var target = $(id), + inst = this._getInst(target[0]); + + inst["selected" + (period === "M" ? "Month" : "Year")] = + inst["draw" + (period === "M" ? "Month" : "Year")] = + parseInt(select.options[select.selectedIndex].value,10); + + this._notifyChange(inst); + this._adjustDate(target); + }, + + /* Action for selecting a day. */ + _selectDay: function(id, month, year, td) { + var inst, + target = $(id); + + if ($(td).hasClass(this._unselectableClass) || this._isDisabledDatepicker(target[0])) { + return; + } + + inst = this._getInst(target[0]); + inst.selectedDay = inst.currentDay = $("a", td).html(); + inst.selectedMonth = inst.currentMonth = month; + inst.selectedYear = inst.currentYear = year; + this._selectDate(id, this._formatDate(inst, + inst.currentDay, inst.currentMonth, inst.currentYear)); + }, + + /* Erase the input field and hide the date picker. */ + _clearDate: function(id) { + var target = $(id); + this._selectDate(target, ""); + }, + + /* Update the input field with the selected date. */ + _selectDate: function(id, dateStr) { + var onSelect, + target = $(id), + inst = this._getInst(target[0]); + + dateStr = (dateStr != null ? dateStr : this._formatDate(inst)); + if (inst.input) { + inst.input.val(dateStr); + } + this._updateAlternate(inst); + + onSelect = this._get(inst, "onSelect"); + if (onSelect) { + onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); // trigger custom callback + } else if (inst.input) { + inst.input.trigger("change"); // fire the change event + } + + if (inst.inline){ + this._updateDatepicker(inst); + } else { + this._hideDatepicker(); + this._lastInput = inst.input[0]; + if (typeof(inst.input[0]) !== "object") { + inst.input.focus(); // restore focus + } + this._lastInput = null; + } + }, + + /* Update any alternate field to synchronise with the main field. */ + _updateAlternate: function(inst) { + var altFormat, date, dateStr, + altField = this._get(inst, "altField"); + + if (altField) { // update alternate field too + altFormat = this._get(inst, "altFormat") || this._get(inst, "dateFormat"); + date = this._getDate(inst); + dateStr = this.formatDate(altFormat, date, this._getFormatConfig(inst)); + $(altField).each(function() { $(this).val(dateStr); }); + } + }, + + /* Set as beforeShowDay function to prevent selection of weekends. + * @param date Date - the date to customise + * @return [boolean, string] - is this date selectable?, what is its CSS class? + */ + noWeekends: function(date) { + var day = date.getDay(); + return [(day > 0 && day < 6), ""]; + }, + + /* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. + * @param date Date - the date to get the week for + * @return number - the number of the week within the year that contains this date + */ + iso8601Week: function(date) { + var time, + checkDate = new Date(date.getTime()); + + // Find Thursday of this week starting on Monday + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); + + time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + }, + + /* Parse a string value into a date object. + * See formatDate below for the possible formats. + * + * @param format string - the expected format of the date + * @param value string - the date in the above format + * @param settings Object - attributes include: + * shortYearCutoff number - the cutoff year for determining the century (optional) + * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) + * dayNames string[7] - names of the days from Sunday (optional) + * monthNamesShort string[12] - abbreviated names of the months (optional) + * monthNames string[12] - names of the months (optional) + * @return Date - the extracted date value or null if value is blank + */ + parseDate: function (format, value, settings) { + if (format == null || value == null) { + throw "Invalid arguments"; + } + + value = (typeof value === "object" ? value.toString() : value + ""); + if (value === "") { + return null; + } + + var iFormat, dim, extra, + iValue = 0, + shortYearCutoffTemp = (settings ? settings.shortYearCutoff : null) || this._defaults.shortYearCutoff, + shortYearCutoff = (typeof shortYearCutoffTemp !== "string" ? shortYearCutoffTemp : + new Date().getFullYear() % 100 + parseInt(shortYearCutoffTemp, 10)), + dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort, + dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames, + monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort, + monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames, + year = -1, + month = -1, + day = -1, + doy = -1, + literal = false, + date, + // Check whether a format character is doubled + lookAhead = function(match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }, + // Extract a number from the string value + getNumber = function(match) { + var isDoubled = lookAhead(match), + size = (match === "@" ? 14 : (match === "!" ? 20 : + (match === "y" && isDoubled ? 4 : (match === "o" ? 3 : 2)))), + digits = new RegExp("^\\d{1," + size + "}"), + num = value.substring(iValue).match(digits); + if (!num) { + throw "Missing number at position " + iValue; + } + iValue += num[0].length; + return parseInt(num[0], 10); + }, + // Extract a name from the string value and convert to an index + getName = function(match, shortNames, longNames) { + var index = -1, + names = $.map(lookAhead(match) ? longNames : shortNames, function (v, k) { + return [ [k, v] ]; + }).sort(function (a, b) { + return -(a[1].length - b[1].length); + }); + + $.each(names, function (i, pair) { + var name = pair[1]; + if (value.substr(iValue, name.length).toLowerCase() === name.toLowerCase()) { + index = pair[0]; + iValue += name.length; + return false; + } + }); + if (index !== -1) { + return index + 1; + } else { + throw "Unknown name at position " + iValue; + } + }, + // Confirm that a literal character matches the string value + checkLiteral = function() { + if (value.charAt(iValue) !== format.charAt(iFormat)) { + throw "Unexpected literal at position " + iValue; + } + iValue++; + }; + + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + checkLiteral(); + } + } else { + switch (format.charAt(iFormat)) { + case "d": + day = getNumber("d"); + break; + case "D": + getName("D", dayNamesShort, dayNames); + break; + case "o": + doy = getNumber("o"); + break; + case "m": + month = getNumber("m"); + break; + case "M": + month = getName("M", monthNamesShort, monthNames); + break; + case "y": + year = getNumber("y"); + break; + case "@": + date = new Date(getNumber("@")); + year = date.getFullYear(); + month = date.getMonth() + 1; + day = date.getDate(); + break; + case "!": + date = new Date((getNumber("!") - this._ticksTo1970) / 10000); + year = date.getFullYear(); + month = date.getMonth() + 1; + day = date.getDate(); + break; + case "'": + if (lookAhead("'")){ + checkLiteral(); + } else { + literal = true; + } + break; + default: + checkLiteral(); + } + } + } + + if (iValue < value.length){ + extra = value.substr(iValue); + if (!/^\s+/.test(extra)) { + throw "Extra/unparsed characters found in date: " + extra; + } + } + + if (year === -1) { + year = new Date().getFullYear(); + } else if (year < 100) { + year += new Date().getFullYear() - new Date().getFullYear() % 100 + + (year <= shortYearCutoff ? 0 : -100); + } + + if (doy > -1) { + month = 1; + day = doy; + do { + dim = this._getDaysInMonth(year, month - 1); + if (day <= dim) { + break; + } + month++; + day -= dim; + } while (true); + } + + date = this._daylightSavingAdjust(new Date(year, month - 1, day)); + if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) { + throw "Invalid date"; // E.g. 31/02/00 + } + return date; + }, + + /* Standard date formats. */ + ATOM: "yy-mm-dd", // RFC 3339 (ISO 8601) + COOKIE: "D, dd M yy", + ISO_8601: "yy-mm-dd", + RFC_822: "D, d M y", + RFC_850: "DD, dd-M-y", + RFC_1036: "D, d M y", + RFC_1123: "D, d M yy", + RFC_2822: "D, d M yy", + RSS: "D, d M y", // RFC 822 + TICKS: "!", + TIMESTAMP: "@", + W3C: "yy-mm-dd", // ISO 8601 + + _ticksTo1970: (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) + + Math.floor(1970 / 400)) * 24 * 60 * 60 * 10000000), + + /* Format a date object into a string value. + * The format can be combinations of the following: + * d - day of month (no leading zero) + * dd - day of month (two digit) + * o - day of year (no leading zeros) + * oo - day of year (three digit) + * D - day name short + * DD - day name long + * m - month of year (no leading zero) + * mm - month of year (two digit) + * M - month name short + * MM - month name long + * y - year (two digit) + * yy - year (four digit) + * @ - Unix timestamp (ms since 01/01/1970) + * ! - Windows ticks (100ns since 01/01/0001) + * "..." - literal text + * '' - single quote + * + * @param format string - the desired format of the date + * @param date Date - the date value to format + * @param settings Object - attributes include: + * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) + * dayNames string[7] - names of the days from Sunday (optional) + * monthNamesShort string[12] - abbreviated names of the months (optional) + * monthNames string[12] - names of the months (optional) + * @return string - the date in the above format + */ + formatDate: function (format, date, settings) { + if (!date) { + return ""; + } + + var iFormat, + dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort, + dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames, + monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort, + monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames, + // Check whether a format character is doubled + lookAhead = function(match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }, + // Format a number, with leading zero if necessary + formatNumber = function(match, value, len) { + var num = "" + value; + if (lookAhead(match)) { + while (num.length < len) { + num = "0" + num; + } + } + return num; + }, + // Format a name, short or long as requested + formatName = function(match, value, shortNames, longNames) { + return (lookAhead(match) ? longNames[value] : shortNames[value]); + }, + output = "", + literal = false; + + if (date) { + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + output += format.charAt(iFormat); + } + } else { + switch (format.charAt(iFormat)) { + case "d": + output += formatNumber("d", date.getDate(), 2); + break; + case "D": + output += formatName("D", date.getDay(), dayNamesShort, dayNames); + break; + case "o": + output += formatNumber("o", + Math.round((new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000), 3); + break; + case "m": + output += formatNumber("m", date.getMonth() + 1, 2); + break; + case "M": + output += formatName("M", date.getMonth(), monthNamesShort, monthNames); + break; + case "y": + output += (lookAhead("y") ? date.getFullYear() : + (date.getYear() % 100 < 10 ? "0" : "") + date.getYear() % 100); + break; + case "@": + output += date.getTime(); + break; + case "!": + output += date.getTime() * 10000 + this._ticksTo1970; + break; + case "'": + if (lookAhead("'")) { + output += "'"; + } else { + literal = true; + } + break; + default: + output += format.charAt(iFormat); + } + } + } + } + return output; + }, + + /* Extract all possible characters from the date format. */ + _possibleChars: function (format) { + var iFormat, + chars = "", + literal = false, + // Check whether a format character is doubled + lookAhead = function(match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }; + + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + chars += format.charAt(iFormat); + } + } else { + switch (format.charAt(iFormat)) { + case "d": case "m": case "y": case "@": + chars += "0123456789"; + break; + case "D": case "M": + return null; // Accept anything + case "'": + if (lookAhead("'")) { + chars += "'"; + } else { + literal = true; + } + break; + default: + chars += format.charAt(iFormat); + } + } + } + return chars; + }, + + /* Get a setting value, defaulting if necessary. */ + _get: function(inst, name) { + return inst.settings[name] !== undefined ? + inst.settings[name] : this._defaults[name]; + }, + + /* Parse existing date and initialise date picker. */ + _setDateFromField: function(inst, noDefault) { + if (inst.input.val() === inst.lastVal) { + return; + } + + var dateFormat = this._get(inst, "dateFormat"), + dates = inst.lastVal = inst.input ? inst.input.val() : null, + defaultDate = this._getDefaultDate(inst), + date = defaultDate, + settings = this._getFormatConfig(inst); + + try { + date = this.parseDate(dateFormat, dates, settings) || defaultDate; + } catch (event) { + dates = (noDefault ? "" : dates); + } + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + inst.currentDay = (dates ? date.getDate() : 0); + inst.currentMonth = (dates ? date.getMonth() : 0); + inst.currentYear = (dates ? date.getFullYear() : 0); + this._adjustInstDate(inst); + }, + + /* Retrieve the default date shown on opening. */ + _getDefaultDate: function(inst) { + return this._restrictMinMax(inst, + this._determineDate(inst, this._get(inst, "defaultDate"), new Date())); + }, + + /* A date may be specified as an exact value or a relative one. */ + _determineDate: function(inst, date, defaultDate) { + var offsetNumeric = function(offset) { + var date = new Date(); + date.setDate(date.getDate() + offset); + return date; + }, + offsetString = function(offset) { + try { + return $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"), + offset, $.datepicker._getFormatConfig(inst)); + } + catch (e) { + // Ignore + } + + var date = (offset.toLowerCase().match(/^c/) ? + $.datepicker._getDate(inst) : null) || new Date(), + year = date.getFullYear(), + month = date.getMonth(), + day = date.getDate(), + pattern = /([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g, + matches = pattern.exec(offset); + + while (matches) { + switch (matches[2] || "d") { + case "d" : case "D" : + day += parseInt(matches[1],10); break; + case "w" : case "W" : + day += parseInt(matches[1],10) * 7; break; + case "m" : case "M" : + month += parseInt(matches[1],10); + day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); + break; + case "y": case "Y" : + year += parseInt(matches[1],10); + day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); + break; + } + matches = pattern.exec(offset); + } + return new Date(year, month, day); + }, + newDate = (date == null || date === "" ? defaultDate : (typeof date === "string" ? offsetString(date) : + (typeof date === "number" ? (isNaN(date) ? defaultDate : offsetNumeric(date)) : new Date(date.getTime())))); + + newDate = (newDate && newDate.toString() === "Invalid Date" ? defaultDate : newDate); + if (newDate) { + newDate.setHours(0); + newDate.setMinutes(0); + newDate.setSeconds(0); + newDate.setMilliseconds(0); + } + return this._daylightSavingAdjust(newDate); + }, + + /* Handle switch to/from daylight saving. + * Hours may be non-zero on daylight saving cut-over: + * > 12 when midnight changeover, but then cannot generate + * midnight datetime, so jump to 1AM, otherwise reset. + * @param date (Date) the date to check + * @return (Date) the corrected date + */ + _daylightSavingAdjust: function(date) { + if (!date) { + return null; + } + date.setHours(date.getHours() > 12 ? date.getHours() + 2 : 0); + return date; + }, + + /* Set the date(s) directly. */ + _setDate: function(inst, date, noChange) { + var clear = !date, + origMonth = inst.selectedMonth, + origYear = inst.selectedYear, + newDate = this._restrictMinMax(inst, this._determineDate(inst, date, new Date())); + + inst.selectedDay = inst.currentDay = newDate.getDate(); + inst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth(); + inst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear(); + if ((origMonth !== inst.selectedMonth || origYear !== inst.selectedYear) && !noChange) { + this._notifyChange(inst); + } + this._adjustInstDate(inst); + if (inst.input) { + inst.input.val(clear ? "" : this._formatDate(inst)); + } + }, + + /* Retrieve the date(s) directly. */ + _getDate: function(inst) { + var startDate = (!inst.currentYear || (inst.input && inst.input.val() === "") ? null : + this._daylightSavingAdjust(new Date( + inst.currentYear, inst.currentMonth, inst.currentDay))); + return startDate; + }, + + /* Attach the onxxx handlers. These are declared statically so + * they work with static code transformers like Caja. + */ + _attachHandlers: function(inst) { + var stepMonths = this._get(inst, "stepMonths"), + id = "#" + inst.id.replace( /\\\\/g, "\\" ); + inst.dpDiv.find("[data-handler]").map(function () { + var handler = { + prev: function () { + $.datepicker._adjustDate(id, -stepMonths, "M"); + }, + next: function () { + $.datepicker._adjustDate(id, +stepMonths, "M"); + }, + hide: function () { + $.datepicker._hideDatepicker(); + }, + today: function () { + $.datepicker._gotoToday(id); + }, + selectDay: function () { + $.datepicker._selectDay(id, +this.getAttribute("data-month"), +this.getAttribute("data-year"), this); + return false; + }, + selectMonth: function () { + $.datepicker._selectMonthYear(id, this, "M"); + return false; + }, + selectYear: function () { + $.datepicker._selectMonthYear(id, this, "Y"); + return false; + } + }; + $(this).bind(this.getAttribute("data-event"), handler[this.getAttribute("data-handler")]); + }); + }, + + /* Generate the HTML for the current state of the date picker. */ + _generateHTML: function(inst) { + var maxDraw, prevText, prev, nextText, next, currentText, gotoDate, + controls, buttonPanel, firstDay, showWeek, dayNames, dayNamesMin, + monthNames, monthNamesShort, beforeShowDay, showOtherMonths, + selectOtherMonths, defaultDate, html, dow, row, group, col, selectedDate, + cornerClass, calender, thead, day, daysInMonth, leadDays, curRows, numRows, + printDate, dRow, tbody, daySettings, otherMonth, unselectable, + tempDate = new Date(), + today = this._daylightSavingAdjust( + new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate())), // clear time + isRTL = this._get(inst, "isRTL"), + showButtonPanel = this._get(inst, "showButtonPanel"), + hideIfNoPrevNext = this._get(inst, "hideIfNoPrevNext"), + navigationAsDateFormat = this._get(inst, "navigationAsDateFormat"), + numMonths = this._getNumberOfMonths(inst), + showCurrentAtPos = this._get(inst, "showCurrentAtPos"), + stepMonths = this._get(inst, "stepMonths"), + isMultiMonth = (numMonths[0] !== 1 || numMonths[1] !== 1), + currentDate = this._daylightSavingAdjust((!inst.currentDay ? new Date(9999, 9, 9) : + new Date(inst.currentYear, inst.currentMonth, inst.currentDay))), + minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + drawMonth = inst.drawMonth - showCurrentAtPos, + drawYear = inst.drawYear; + + if (drawMonth < 0) { + drawMonth += 12; + drawYear--; + } + if (maxDate) { + maxDraw = this._daylightSavingAdjust(new Date(maxDate.getFullYear(), + maxDate.getMonth() - (numMonths[0] * numMonths[1]) + 1, maxDate.getDate())); + maxDraw = (minDate && maxDraw < minDate ? minDate : maxDraw); + while (this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1)) > maxDraw) { + drawMonth--; + if (drawMonth < 0) { + drawMonth = 11; + drawYear--; + } + } + } + inst.drawMonth = drawMonth; + inst.drawYear = drawYear; + + prevText = this._get(inst, "prevText"); + prevText = (!navigationAsDateFormat ? prevText : this.formatDate(prevText, + this._daylightSavingAdjust(new Date(drawYear, drawMonth - stepMonths, 1)), + this._getFormatConfig(inst))); + + prev = (this._canAdjustMonth(inst, -1, drawYear, drawMonth) ? + "<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click'" + + " title='" + prevText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "e" : "w") + "'>" + prevText + "</span></a>" : + (hideIfNoPrevNext ? "" : "<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='"+ prevText +"'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "e" : "w") + "'>" + prevText + "</span></a>")); + + nextText = this._get(inst, "nextText"); + nextText = (!navigationAsDateFormat ? nextText : this.formatDate(nextText, + this._daylightSavingAdjust(new Date(drawYear, drawMonth + stepMonths, 1)), + this._getFormatConfig(inst))); + + next = (this._canAdjustMonth(inst, +1, drawYear, drawMonth) ? + "<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click'" + + " title='" + nextText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "w" : "e") + "'>" + nextText + "</span></a>" : + (hideIfNoPrevNext ? "" : "<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='"+ nextText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "w" : "e") + "'>" + nextText + "</span></a>")); + + currentText = this._get(inst, "currentText"); + gotoDate = (this._get(inst, "gotoCurrent") && inst.currentDay ? currentDate : today); + currentText = (!navigationAsDateFormat ? currentText : + this.formatDate(currentText, gotoDate, this._getFormatConfig(inst))); + + controls = (!inst.inline ? "<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>" + + this._get(inst, "closeText") + "</button>" : ""); + + buttonPanel = (showButtonPanel) ? "<div class='ui-datepicker-buttonpane ui-widget-content'>" + (isRTL ? controls : "") + + (this._isInRange(inst, gotoDate) ? "<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'" + + ">" + currentText + "</button>" : "") + (isRTL ? "" : controls) + "</div>" : ""; + + firstDay = parseInt(this._get(inst, "firstDay"),10); + firstDay = (isNaN(firstDay) ? 0 : firstDay); + + showWeek = this._get(inst, "showWeek"); + dayNames = this._get(inst, "dayNames"); + dayNamesMin = this._get(inst, "dayNamesMin"); + monthNames = this._get(inst, "monthNames"); + monthNamesShort = this._get(inst, "monthNamesShort"); + beforeShowDay = this._get(inst, "beforeShowDay"); + showOtherMonths = this._get(inst, "showOtherMonths"); + selectOtherMonths = this._get(inst, "selectOtherMonths"); + defaultDate = this._getDefaultDate(inst); + html = ""; + dow; + for (row = 0; row < numMonths[0]; row++) { + group = ""; + this.maxRows = 4; + for (col = 0; col < numMonths[1]; col++) { + selectedDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, inst.selectedDay)); + cornerClass = " ui-corner-all"; + calender = ""; + if (isMultiMonth) { + calender += "<div class='ui-datepicker-group"; + if (numMonths[1] > 1) { + switch (col) { + case 0: calender += " ui-datepicker-group-first"; + cornerClass = " ui-corner-" + (isRTL ? "right" : "left"); break; + case numMonths[1]-1: calender += " ui-datepicker-group-last"; + cornerClass = " ui-corner-" + (isRTL ? "left" : "right"); break; + default: calender += " ui-datepicker-group-middle"; cornerClass = ""; break; + } + } + calender += "'>"; + } + calender += "<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix" + cornerClass + "'>" + + (/all|left/.test(cornerClass) && row === 0 ? (isRTL ? next : prev) : "") + + (/all|right/.test(cornerClass) && row === 0 ? (isRTL ? prev : next) : "") + + this._generateMonthYearHeader(inst, drawMonth, drawYear, minDate, maxDate, + row > 0 || col > 0, monthNames, monthNamesShort) + // draw month headers + "</div><table class='ui-datepicker-calendar'><thead>" + + "<tr>"; + thead = (showWeek ? "<th class='ui-datepicker-week-col'>" + this._get(inst, "weekHeader") + "</th>" : ""); + for (dow = 0; dow < 7; dow++) { // days of the week + day = (dow + firstDay) % 7; + thead += "<th" + ((dow + firstDay + 6) % 7 >= 5 ? " class='ui-datepicker-week-end'" : "") + ">" + + "<span title='" + dayNames[day] + "'>" + dayNamesMin[day] + "</span></th>"; + } + calender += thead + "</tr></thead><tbody>"; + daysInMonth = this._getDaysInMonth(drawYear, drawMonth); + if (drawYear === inst.selectedYear && drawMonth === inst.selectedMonth) { + inst.selectedDay = Math.min(inst.selectedDay, daysInMonth); + } + leadDays = (this._getFirstDayOfMonth(drawYear, drawMonth) - firstDay + 7) % 7; + curRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate + numRows = (isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows); //If multiple months, use the higher number of rows (see #7043) + this.maxRows = numRows; + printDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1 - leadDays)); + for (dRow = 0; dRow < numRows; dRow++) { // create date picker rows + calender += "<tr>"; + tbody = (!showWeek ? "" : "<td class='ui-datepicker-week-col'>" + + this._get(inst, "calculateWeek")(printDate) + "</td>"); + for (dow = 0; dow < 7; dow++) { // create date picker days + daySettings = (beforeShowDay ? + beforeShowDay.apply((inst.input ? inst.input[0] : null), [printDate]) : [true, ""]); + otherMonth = (printDate.getMonth() !== drawMonth); + unselectable = (otherMonth && !selectOtherMonths) || !daySettings[0] || + (minDate && printDate < minDate) || (maxDate && printDate > maxDate); + tbody += "<td class='" + + ((dow + firstDay + 6) % 7 >= 5 ? " ui-datepicker-week-end" : "") + // highlight weekends + (otherMonth ? " ui-datepicker-other-month" : "") + // highlight days from other months + ((printDate.getTime() === selectedDate.getTime() && drawMonth === inst.selectedMonth && inst._keyEvent) || // user pressed key + (defaultDate.getTime() === printDate.getTime() && defaultDate.getTime() === selectedDate.getTime()) ? + // or defaultDate is current printedDate and defaultDate is selectedDate + " " + this._dayOverClass : "") + // highlight selected day + (unselectable ? " " + this._unselectableClass + " ui-state-disabled": "") + // highlight unselectable days + (otherMonth && !showOtherMonths ? "" : " " + daySettings[1] + // highlight custom dates + (printDate.getTime() === currentDate.getTime() ? " " + this._currentClass : "") + // highlight selected day + (printDate.getTime() === today.getTime() ? " ui-datepicker-today" : "")) + "'" + // highlight today (if different) + ((!otherMonth || showOtherMonths) && daySettings[2] ? " title='" + daySettings[2].replace(/'/g, "'") + "'" : "") + // cell title + (unselectable ? "" : " data-handler='selectDay' data-event='click' data-month='" + printDate.getMonth() + "' data-year='" + printDate.getFullYear() + "'") + ">" + // actions + (otherMonth && !showOtherMonths ? " " : // display for other months + (unselectable ? "<span class='ui-state-default'>" + printDate.getDate() + "</span>" : "<a class='ui-state-default" + + (printDate.getTime() === today.getTime() ? " ui-state-highlight" : "") + + (printDate.getTime() === currentDate.getTime() ? " ui-state-active" : "") + // highlight selected day + (otherMonth ? " ui-priority-secondary" : "") + // distinguish dates from other months + "' href='#'>" + printDate.getDate() + "</a>")) + "</td>"; // display selectable date + printDate.setDate(printDate.getDate() + 1); + printDate = this._daylightSavingAdjust(printDate); + } + calender += tbody + "</tr>"; + } + drawMonth++; + if (drawMonth > 11) { + drawMonth = 0; + drawYear++; + } + calender += "</tbody></table>" + (isMultiMonth ? "</div>" + + ((numMonths[0] > 0 && col === numMonths[1]-1) ? "<div class='ui-datepicker-row-break'></div>" : "") : ""); + group += calender; + } + html += group; + } + html += buttonPanel; + inst._keyEvent = false; + return html; + }, + + /* Generate the month and year header. */ + _generateMonthYearHeader: function(inst, drawMonth, drawYear, minDate, maxDate, + secondary, monthNames, monthNamesShort) { + + var inMinYear, inMaxYear, month, years, thisYear, determineYear, year, endYear, + changeMonth = this._get(inst, "changeMonth"), + changeYear = this._get(inst, "changeYear"), + showMonthAfterYear = this._get(inst, "showMonthAfterYear"), + html = "<div class='ui-datepicker-title'>", + monthHtml = ""; + + // month selection + if (secondary || !changeMonth) { + monthHtml += "<span class='ui-datepicker-month'>" + monthNames[drawMonth] + "</span>"; + } else { + inMinYear = (minDate && minDate.getFullYear() === drawYear); + inMaxYear = (maxDate && maxDate.getFullYear() === drawYear); + monthHtml += "<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>"; + for ( month = 0; month < 12; month++) { + if ((!inMinYear || month >= minDate.getMonth()) && (!inMaxYear || month <= maxDate.getMonth())) { + monthHtml += "<option value='" + month + "'" + + (month === drawMonth ? " selected='selected'" : "") + + ">" + monthNamesShort[month] + "</option>"; + } + } + monthHtml += "</select>"; + } + + if (!showMonthAfterYear) { + html += monthHtml + (secondary || !(changeMonth && changeYear) ? " " : ""); + } + + // year selection + if ( !inst.yearshtml ) { + inst.yearshtml = ""; + if (secondary || !changeYear) { + html += "<span class='ui-datepicker-year'>" + drawYear + "</span>"; + } else { + // determine range of years to display + years = this._get(inst, "yearRange").split(":"); + thisYear = new Date().getFullYear(); + determineYear = function(value) { + var year = (value.match(/c[+\-].*/) ? drawYear + parseInt(value.substring(1), 10) : + (value.match(/[+\-].*/) ? thisYear + parseInt(value, 10) : + parseInt(value, 10))); + return (isNaN(year) ? thisYear : year); + }; + year = determineYear(years[0]); + endYear = Math.max(year, determineYear(years[1] || "")); + year = (minDate ? Math.max(year, minDate.getFullYear()) : year); + endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear); + inst.yearshtml += "<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>"; + for (; year <= endYear; year++) { + inst.yearshtml += "<option value='" + year + "'" + + (year === drawYear ? " selected='selected'" : "") + + ">" + year + "</option>"; + } + inst.yearshtml += "</select>"; + + html += inst.yearshtml; + inst.yearshtml = null; + } + } + + html += this._get(inst, "yearSuffix"); + if (showMonthAfterYear) { + html += (secondary || !(changeMonth && changeYear) ? " " : "") + monthHtml; + } + html += "</div>"; // Close datepicker_header + return html; + }, + + /* Adjust one of the date sub-fields. */ + _adjustInstDate: function(inst, offset, period) { + var year = inst.drawYear + (period === "Y" ? offset : 0), + month = inst.drawMonth + (period === "M" ? offset : 0), + day = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) + (period === "D" ? offset : 0), + date = this._restrictMinMax(inst, this._daylightSavingAdjust(new Date(year, month, day))); + + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + if (period === "M" || period === "Y") { + this._notifyChange(inst); + } + }, + + /* Ensure a date is within any min/max bounds. */ + _restrictMinMax: function(inst, date) { + var minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + newDate = (minDate && date < minDate ? minDate : date); + return (maxDate && newDate > maxDate ? maxDate : newDate); + }, + + /* Notify change of month/year. */ + _notifyChange: function(inst) { + var onChange = this._get(inst, "onChangeMonthYear"); + if (onChange) { + onChange.apply((inst.input ? inst.input[0] : null), + [inst.selectedYear, inst.selectedMonth + 1, inst]); + } + }, + + /* Determine the number of months to show. */ + _getNumberOfMonths: function(inst) { + var numMonths = this._get(inst, "numberOfMonths"); + return (numMonths == null ? [1, 1] : (typeof numMonths === "number" ? [1, numMonths] : numMonths)); + }, + + /* Determine the current maximum date - ensure no time components are set. */ + _getMinMaxDate: function(inst, minMax) { + return this._determineDate(inst, this._get(inst, minMax + "Date"), null); + }, + + /* Find the number of days in a given month. */ + _getDaysInMonth: function(year, month) { + return 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate(); + }, + + /* Find the day of the week of the first of a month. */ + _getFirstDayOfMonth: function(year, month) { + return new Date(year, month, 1).getDay(); + }, + + /* Determines if we should allow a "next/prev" month display change. */ + _canAdjustMonth: function(inst, offset, curYear, curMonth) { + var numMonths = this._getNumberOfMonths(inst), + date = this._daylightSavingAdjust(new Date(curYear, + curMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1)); + + if (offset < 0) { + date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth())); + } + return this._isInRange(inst, date); + }, + + /* Is the given date in the accepted range? */ + _isInRange: function(inst, date) { + var yearSplit, currentYear, + minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + minYear = null, + maxYear = null, + years = this._get(inst, "yearRange"); + if (years){ + yearSplit = years.split(":"); + currentYear = new Date().getFullYear(); + minYear = parseInt(yearSplit[0], 10); + maxYear = parseInt(yearSplit[1], 10); + if ( yearSplit[0].match(/[+\-].*/) ) { + minYear += currentYear; + } + if ( yearSplit[1].match(/[+\-].*/) ) { + maxYear += currentYear; + } + } + + return ((!minDate || date.getTime() >= minDate.getTime()) && + (!maxDate || date.getTime() <= maxDate.getTime()) && + (!minYear || date.getFullYear() >= minYear) && + (!maxYear || date.getFullYear() <= maxYear)); + }, + + /* Provide the configuration settings for formatting/parsing. */ + _getFormatConfig: function(inst) { + var shortYearCutoff = this._get(inst, "shortYearCutoff"); + shortYearCutoff = (typeof shortYearCutoff !== "string" ? shortYearCutoff : + new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10)); + return {shortYearCutoff: shortYearCutoff, + dayNamesShort: this._get(inst, "dayNamesShort"), dayNames: this._get(inst, "dayNames"), + monthNamesShort: this._get(inst, "monthNamesShort"), monthNames: this._get(inst, "monthNames")}; + }, + + /* Format the given date for display. */ + _formatDate: function(inst, day, month, year) { + if (!day) { + inst.currentDay = inst.selectedDay; + inst.currentMonth = inst.selectedMonth; + inst.currentYear = inst.selectedYear; + } + var date = (day ? (typeof day === "object" ? day : + this._daylightSavingAdjust(new Date(year, month, day))) : + this._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay))); + return this.formatDate(this._get(inst, "dateFormat"), date, this._getFormatConfig(inst)); + } +}); + +/* + * Bind hover events for datepicker elements. + * Done via delegate so the binding only occurs once in the lifetime of the parent div. + * Global instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker. + */ +function bindHover(dpDiv) { + var selector = "button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a"; + return dpDiv.delegate(selector, "mouseout", function() { + $(this).removeClass("ui-state-hover"); + if (this.className.indexOf("ui-datepicker-prev") !== -1) { + $(this).removeClass("ui-datepicker-prev-hover"); + } + if (this.className.indexOf("ui-datepicker-next") !== -1) { + $(this).removeClass("ui-datepicker-next-hover"); + } + }) + .delegate(selector, "mouseover", function(){ + if (!$.datepicker._isDisabledDatepicker( instActive.inline ? dpDiv.parent()[0] : instActive.input[0])) { + $(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"); + $(this).addClass("ui-state-hover"); + if (this.className.indexOf("ui-datepicker-prev") !== -1) { + $(this).addClass("ui-datepicker-prev-hover"); + } + if (this.className.indexOf("ui-datepicker-next") !== -1) { + $(this).addClass("ui-datepicker-next-hover"); + } + } + }); +} + +/* jQuery extend now ignores nulls! */ +function extendRemove(target, props) { + $.extend(target, props); + for (var name in props) { + if (props[name] == null) { + target[name] = props[name]; + } + } + return target; +} + +/* Invoke the datepicker functionality. + @param options string - a command, optionally followed by additional parameters or + Object - settings for attaching new datepicker functionality + @return jQuery object */ +$.fn.datepicker = function(options){ + + /* Verify an empty collection wasn't passed - Fixes #6976 */ + if ( !this.length ) { + return this; + } + + /* Initialise the date picker. */ + if (!$.datepicker.initialized) { + $(document).mousedown($.datepicker._checkExternalClick); + $.datepicker.initialized = true; + } + + /* Append datepicker main container to body if not exist. */ + if ($("#"+$.datepicker._mainDivId).length === 0) { + $("body").append($.datepicker.dpDiv); + } + + var otherArgs = Array.prototype.slice.call(arguments, 1); + if (typeof options === "string" && (options === "isDisabled" || options === "getDate" || options === "widget")) { + return $.datepicker["_" + options + "Datepicker"]. + apply($.datepicker, [this[0]].concat(otherArgs)); + } + if (options === "option" && arguments.length === 2 && typeof arguments[1] === "string") { + return $.datepicker["_" + options + "Datepicker"]. + apply($.datepicker, [this[0]].concat(otherArgs)); + } + return this.each(function() { + typeof options === "string" ? + $.datepicker["_" + options + "Datepicker"]. + apply($.datepicker, [this].concat(otherArgs)) : + $.datepicker._attachDatepicker(this, options); + }); +}; + +$.datepicker = new Datepicker(); // singleton instance +$.datepicker.initialized = false; +$.datepicker.uuid = new Date().getTime(); +$.datepicker.version = "1.10.3"; + +})(jQuery); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.dialog.js b/extensions/jui/yii/jui/assets/jquery.ui.dialog.js new file mode 100644 index 0000000..3a01e0c --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.dialog.js @@ -0,0 +1,808 @@ +/*! + * jQuery UI Dialog 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/dialog/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + * jquery.ui.button.js + * jquery.ui.draggable.js + * jquery.ui.mouse.js + * jquery.ui.position.js + * jquery.ui.resizable.js + */ +(function( $, undefined ) { + +var sizeRelatedOptions = { + buttons: true, + height: true, + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true, + width: true + }, + resizableRelatedOptions = { + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true + }; + +$.widget( "ui.dialog", { + version: "1.10.3", + options: { + appendTo: "body", + autoOpen: true, + buttons: [], + closeOnEscape: true, + closeText: "close", + dialogClass: "", + draggable: true, + hide: null, + height: "auto", + maxHeight: null, + maxWidth: null, + minHeight: 150, + minWidth: 150, + modal: false, + position: { + my: "center", + at: "center", + of: window, + collision: "fit", + // Ensure the titlebar is always visible + using: function( pos ) { + var topOffset = $( this ).css( pos ).offset().top; + if ( topOffset < 0 ) { + $( this ).css( "top", pos.top - topOffset ); + } + } + }, + resizable: true, + show: null, + title: null, + width: 300, + + // callbacks + beforeClose: null, + close: null, + drag: null, + dragStart: null, + dragStop: null, + focus: null, + open: null, + resize: null, + resizeStart: null, + resizeStop: null + }, + + _create: function() { + this.originalCss = { + display: this.element[0].style.display, + width: this.element[0].style.width, + minHeight: this.element[0].style.minHeight, + maxHeight: this.element[0].style.maxHeight, + height: this.element[0].style.height + }; + this.originalPosition = { + parent: this.element.parent(), + index: this.element.parent().children().index( this.element ) + }; + this.originalTitle = this.element.attr("title"); + this.options.title = this.options.title || this.originalTitle; + + this._createWrapper(); + + this.element + .show() + .removeAttr("title") + .addClass("ui-dialog-content ui-widget-content") + .appendTo( this.uiDialog ); + + this._createTitlebar(); + this._createButtonPane(); + + if ( this.options.draggable && $.fn.draggable ) { + this._makeDraggable(); + } + if ( this.options.resizable && $.fn.resizable ) { + this._makeResizable(); + } + + this._isOpen = false; + }, + + _init: function() { + if ( this.options.autoOpen ) { + this.open(); + } + }, + + _appendTo: function() { + var element = this.options.appendTo; + if ( element && (element.jquery || element.nodeType) ) { + return $( element ); + } + return this.document.find( element || "body" ).eq( 0 ); + }, + + _destroy: function() { + var next, + originalPosition = this.originalPosition; + + this._destroyOverlay(); + + this.element + .removeUniqueId() + .removeClass("ui-dialog-content ui-widget-content") + .css( this.originalCss ) + // Without detaching first, the following becomes really slow + .detach(); + + this.uiDialog.stop( true, true ).remove(); + + if ( this.originalTitle ) { + this.element.attr( "title", this.originalTitle ); + } + + next = originalPosition.parent.children().eq( originalPosition.index ); + // Don't try to place the dialog next to itself (#8613) + if ( next.length && next[0] !== this.element[0] ) { + next.before( this.element ); + } else { + originalPosition.parent.append( this.element ); + } + }, + + widget: function() { + return this.uiDialog; + }, + + disable: $.noop, + enable: $.noop, + + close: function( event ) { + var that = this; + + if ( !this._isOpen || this._trigger( "beforeClose", event ) === false ) { + return; + } + + this._isOpen = false; + this._destroyOverlay(); + + if ( !this.opener.filter(":focusable").focus().length ) { + // Hiding a focused element doesn't trigger blur in WebKit + // so in case we have nothing to focus on, explicitly blur the active element + // https://bugs.webkit.org/show_bug.cgi?id=47182 + $( this.document[0].activeElement ).blur(); + } + + this._hide( this.uiDialog, this.options.hide, function() { + that._trigger( "close", event ); + }); + }, + + isOpen: function() { + return this._isOpen; + }, + + moveToTop: function() { + this._moveToTop(); + }, + + _moveToTop: function( event, silent ) { + var moved = !!this.uiDialog.nextAll(":visible").insertBefore( this.uiDialog ).length; + if ( moved && !silent ) { + this._trigger( "focus", event ); + } + return moved; + }, + + open: function() { + var that = this; + if ( this._isOpen ) { + if ( this._moveToTop() ) { + this._focusTabbable(); + } + return; + } + + this._isOpen = true; + this.opener = $( this.document[0].activeElement ); + + this._size(); + this._position(); + this._createOverlay(); + this._moveToTop( null, true ); + this._show( this.uiDialog, this.options.show, function() { + that._focusTabbable(); + that._trigger("focus"); + }); + + this._trigger("open"); + }, + + _focusTabbable: function() { + // Set focus to the first match: + // 1. First element inside the dialog matching [autofocus] + // 2. Tabbable element inside the content element + // 3. Tabbable element inside the buttonpane + // 4. The close button + // 5. The dialog itself + var hasFocus = this.element.find("[autofocus]"); + if ( !hasFocus.length ) { + hasFocus = this.element.find(":tabbable"); + } + if ( !hasFocus.length ) { + hasFocus = this.uiDialogButtonPane.find(":tabbable"); + } + if ( !hasFocus.length ) { + hasFocus = this.uiDialogTitlebarClose.filter(":tabbable"); + } + if ( !hasFocus.length ) { + hasFocus = this.uiDialog; + } + hasFocus.eq( 0 ).focus(); + }, + + _keepFocus: function( event ) { + function checkFocus() { + var activeElement = this.document[0].activeElement, + isActive = this.uiDialog[0] === activeElement || + $.contains( this.uiDialog[0], activeElement ); + if ( !isActive ) { + this._focusTabbable(); + } + } + event.preventDefault(); + checkFocus.call( this ); + // support: IE + // IE <= 8 doesn't prevent moving focus even with event.preventDefault() + // so we check again later + this._delay( checkFocus ); + }, + + _createWrapper: function() { + this.uiDialog = $("<div>") + .addClass( "ui-dialog ui-widget ui-widget-content ui-corner-all ui-front " + + this.options.dialogClass ) + .hide() + .attr({ + // Setting tabIndex makes the div focusable + tabIndex: -1, + role: "dialog" + }) + .appendTo( this._appendTo() ); + + this._on( this.uiDialog, { + keydown: function( event ) { + if ( this.options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode && + event.keyCode === $.ui.keyCode.ESCAPE ) { + event.preventDefault(); + this.close( event ); + return; + } + + // prevent tabbing out of dialogs + if ( event.keyCode !== $.ui.keyCode.TAB ) { + return; + } + var tabbables = this.uiDialog.find(":tabbable"), + first = tabbables.filter(":first"), + last = tabbables.filter(":last"); + + if ( ( event.target === last[0] || event.target === this.uiDialog[0] ) && !event.shiftKey ) { + first.focus( 1 ); + event.preventDefault(); + } else if ( ( event.target === first[0] || event.target === this.uiDialog[0] ) && event.shiftKey ) { + last.focus( 1 ); + event.preventDefault(); + } + }, + mousedown: function( event ) { + if ( this._moveToTop( event ) ) { + this._focusTabbable(); + } + } + }); + + // We assume that any existing aria-describedby attribute means + // that the dialog content is marked up properly + // otherwise we brute force the content as the description + if ( !this.element.find("[aria-describedby]").length ) { + this.uiDialog.attr({ + "aria-describedby": this.element.uniqueId().attr("id") + }); + } + }, + + _createTitlebar: function() { + var uiDialogTitle; + + this.uiDialogTitlebar = $("<div>") + .addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix") + .prependTo( this.uiDialog ); + this._on( this.uiDialogTitlebar, { + mousedown: function( event ) { + // Don't prevent click on close button (#8838) + // Focusing a dialog that is partially scrolled out of view + // causes the browser to scroll it into view, preventing the click event + if ( !$( event.target ).closest(".ui-dialog-titlebar-close") ) { + // Dialog isn't getting focus when dragging (#8063) + this.uiDialog.focus(); + } + } + }); + + this.uiDialogTitlebarClose = $("<button></button>") + .button({ + label: this.options.closeText, + icons: { + primary: "ui-icon-closethick" + }, + text: false + }) + .addClass("ui-dialog-titlebar-close") + .appendTo( this.uiDialogTitlebar ); + this._on( this.uiDialogTitlebarClose, { + click: function( event ) { + event.preventDefault(); + this.close( event ); + } + }); + + uiDialogTitle = $("<span>") + .uniqueId() + .addClass("ui-dialog-title") + .prependTo( this.uiDialogTitlebar ); + this._title( uiDialogTitle ); + + this.uiDialog.attr({ + "aria-labelledby": uiDialogTitle.attr("id") + }); + }, + + _title: function( title ) { + if ( !this.options.title ) { + title.html(" "); + } + title.text( this.options.title ); + }, + + _createButtonPane: function() { + this.uiDialogButtonPane = $("<div>") + .addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"); + + this.uiButtonSet = $("<div>") + .addClass("ui-dialog-buttonset") + .appendTo( this.uiDialogButtonPane ); + + this._createButtons(); + }, + + _createButtons: function() { + var that = this, + buttons = this.options.buttons; + + // if we already have a button pane, remove it + this.uiDialogButtonPane.remove(); + this.uiButtonSet.empty(); + + if ( $.isEmptyObject( buttons ) || ($.isArray( buttons ) && !buttons.length) ) { + this.uiDialog.removeClass("ui-dialog-buttons"); + return; + } + + $.each( buttons, function( name, props ) { + var click, buttonOptions; + props = $.isFunction( props ) ? + { click: props, text: name } : + props; + // Default to a non-submitting button + props = $.extend( { type: "button" }, props ); + // Change the context for the click callback to be the main element + click = props.click; + props.click = function() { + click.apply( that.element[0], arguments ); + }; + buttonOptions = { + icons: props.icons, + text: props.showText + }; + delete props.icons; + delete props.showText; + $( "<button></button>", props ) + .button( buttonOptions ) + .appendTo( that.uiButtonSet ); + }); + this.uiDialog.addClass("ui-dialog-buttons"); + this.uiDialogButtonPane.appendTo( this.uiDialog ); + }, + + _makeDraggable: function() { + var that = this, + options = this.options; + + function filteredUi( ui ) { + return { + position: ui.position, + offset: ui.offset + }; + } + + this.uiDialog.draggable({ + cancel: ".ui-dialog-content, .ui-dialog-titlebar-close", + handle: ".ui-dialog-titlebar", + containment: "document", + start: function( event, ui ) { + $( this ).addClass("ui-dialog-dragging"); + that._blockFrames(); + that._trigger( "dragStart", event, filteredUi( ui ) ); + }, + drag: function( event, ui ) { + that._trigger( "drag", event, filteredUi( ui ) ); + }, + stop: function( event, ui ) { + options.position = [ + ui.position.left - that.document.scrollLeft(), + ui.position.top - that.document.scrollTop() + ]; + $( this ).removeClass("ui-dialog-dragging"); + that._unblockFrames(); + that._trigger( "dragStop", event, filteredUi( ui ) ); + } + }); + }, + + _makeResizable: function() { + var that = this, + options = this.options, + handles = options.resizable, + // .ui-resizable has position: relative defined in the stylesheet + // but dialogs have to use absolute or fixed positioning + position = this.uiDialog.css("position"), + resizeHandles = typeof handles === "string" ? + handles : + "n,e,s,w,se,sw,ne,nw"; + + function filteredUi( ui ) { + return { + originalPosition: ui.originalPosition, + originalSize: ui.originalSize, + position: ui.position, + size: ui.size + }; + } + + this.uiDialog.resizable({ + cancel: ".ui-dialog-content", + containment: "document", + alsoResize: this.element, + maxWidth: options.maxWidth, + maxHeight: options.maxHeight, + minWidth: options.minWidth, + minHeight: this._minHeight(), + handles: resizeHandles, + start: function( event, ui ) { + $( this ).addClass("ui-dialog-resizing"); + that._blockFrames(); + that._trigger( "resizeStart", event, filteredUi( ui ) ); + }, + resize: function( event, ui ) { + that._trigger( "resize", event, filteredUi( ui ) ); + }, + stop: function( event, ui ) { + options.height = $( this ).height(); + options.width = $( this ).width(); + $( this ).removeClass("ui-dialog-resizing"); + that._unblockFrames(); + that._trigger( "resizeStop", event, filteredUi( ui ) ); + } + }) + .css( "position", position ); + }, + + _minHeight: function() { + var options = this.options; + + return options.height === "auto" ? + options.minHeight : + Math.min( options.minHeight, options.height ); + }, + + _position: function() { + // Need to show the dialog to get the actual offset in the position plugin + var isVisible = this.uiDialog.is(":visible"); + if ( !isVisible ) { + this.uiDialog.show(); + } + this.uiDialog.position( this.options.position ); + if ( !isVisible ) { + this.uiDialog.hide(); + } + }, + + _setOptions: function( options ) { + var that = this, + resize = false, + resizableOptions = {}; + + $.each( options, function( key, value ) { + that._setOption( key, value ); + + if ( key in sizeRelatedOptions ) { + resize = true; + } + if ( key in resizableRelatedOptions ) { + resizableOptions[ key ] = value; + } + }); + + if ( resize ) { + this._size(); + this._position(); + } + if ( this.uiDialog.is(":data(ui-resizable)") ) { + this.uiDialog.resizable( "option", resizableOptions ); + } + }, + + _setOption: function( key, value ) { + /*jshint maxcomplexity:15*/ + var isDraggable, isResizable, + uiDialog = this.uiDialog; + + if ( key === "dialogClass" ) { + uiDialog + .removeClass( this.options.dialogClass ) + .addClass( value ); + } + + if ( key === "disabled" ) { + return; + } + + this._super( key, value ); + + if ( key === "appendTo" ) { + this.uiDialog.appendTo( this._appendTo() ); + } + + if ( key === "buttons" ) { + this._createButtons(); + } + + if ( key === "closeText" ) { + this.uiDialogTitlebarClose.button({ + // Ensure that we always pass a string + label: "" + value + }); + } + + if ( key === "draggable" ) { + isDraggable = uiDialog.is(":data(ui-draggable)"); + if ( isDraggable && !value ) { + uiDialog.draggable("destroy"); + } + + if ( !isDraggable && value ) { + this._makeDraggable(); + } + } + + if ( key === "position" ) { + this._position(); + } + + if ( key === "resizable" ) { + // currently resizable, becoming non-resizable + isResizable = uiDialog.is(":data(ui-resizable)"); + if ( isResizable && !value ) { + uiDialog.resizable("destroy"); + } + + // currently resizable, changing handles + if ( isResizable && typeof value === "string" ) { + uiDialog.resizable( "option", "handles", value ); + } + + // currently non-resizable, becoming resizable + if ( !isResizable && value !== false ) { + this._makeResizable(); + } + } + + if ( key === "title" ) { + this._title( this.uiDialogTitlebar.find(".ui-dialog-title") ); + } + }, + + _size: function() { + // If the user has resized the dialog, the .ui-dialog and .ui-dialog-content + // divs will both have width and height set, so we need to reset them + var nonContentHeight, minContentHeight, maxContentHeight, + options = this.options; + + // Reset content sizing + this.element.show().css({ + width: "auto", + minHeight: 0, + maxHeight: "none", + height: 0 + }); + + if ( options.minWidth > options.width ) { + options.width = options.minWidth; + } + + // reset wrapper sizing + // determine the height of all the non-content elements + nonContentHeight = this.uiDialog.css({ + height: "auto", + width: options.width + }) + .outerHeight(); + minContentHeight = Math.max( 0, options.minHeight - nonContentHeight ); + maxContentHeight = typeof options.maxHeight === "number" ? + Math.max( 0, options.maxHeight - nonContentHeight ) : + "none"; + + if ( options.height === "auto" ) { + this.element.css({ + minHeight: minContentHeight, + maxHeight: maxContentHeight, + height: "auto" + }); + } else { + this.element.height( Math.max( 0, options.height - nonContentHeight ) ); + } + + if (this.uiDialog.is(":data(ui-resizable)") ) { + this.uiDialog.resizable( "option", "minHeight", this._minHeight() ); + } + }, + + _blockFrames: function() { + this.iframeBlocks = this.document.find( "iframe" ).map(function() { + var iframe = $( this ); + + return $( "<div>" ) + .css({ + position: "absolute", + width: iframe.outerWidth(), + height: iframe.outerHeight() + }) + .appendTo( iframe.parent() ) + .offset( iframe.offset() )[0]; + }); + }, + + _unblockFrames: function() { + if ( this.iframeBlocks ) { + this.iframeBlocks.remove(); + delete this.iframeBlocks; + } + }, + + _allowInteraction: function( event ) { + if ( $( event.target ).closest(".ui-dialog").length ) { + return true; + } + + // TODO: Remove hack when datepicker implements + // the .ui-front logic (#8989) + return !!$( event.target ).closest(".ui-datepicker").length; + }, + + _createOverlay: function() { + if ( !this.options.modal ) { + return; + } + + var that = this, + widgetFullName = this.widgetFullName; + if ( !$.ui.dialog.overlayInstances ) { + // Prevent use of anchors and inputs. + // We use a delay in case the overlay is created from an + // event that we're going to be cancelling. (#2804) + this._delay(function() { + // Handle .dialog().dialog("close") (#4065) + if ( $.ui.dialog.overlayInstances ) { + this.document.bind( "focusin.dialog", function( event ) { + if ( !that._allowInteraction( event ) ) { + event.preventDefault(); + $(".ui-dialog:visible:last .ui-dialog-content") + .data( widgetFullName )._focusTabbable(); + } + }); + } + }); + } + + this.overlay = $("<div>") + .addClass("ui-widget-overlay ui-front") + .appendTo( this._appendTo() ); + this._on( this.overlay, { + mousedown: "_keepFocus" + }); + $.ui.dialog.overlayInstances++; + }, + + _destroyOverlay: function() { + if ( !this.options.modal ) { + return; + } + + if ( this.overlay ) { + $.ui.dialog.overlayInstances--; + + if ( !$.ui.dialog.overlayInstances ) { + this.document.unbind( "focusin.dialog" ); + } + this.overlay.remove(); + this.overlay = null; + } + } +}); + +$.ui.dialog.overlayInstances = 0; + +// DEPRECATED +if ( $.uiBackCompat !== false ) { + // position option with array notation + // just override with old implementation + $.widget( "ui.dialog", $.ui.dialog, { + _position: function() { + var position = this.options.position, + myAt = [], + offset = [ 0, 0 ], + isVisible; + + if ( position ) { + if ( typeof position === "string" || (typeof position === "object" && "0" in position ) ) { + myAt = position.split ? position.split(" ") : [ position[0], position[1] ]; + if ( myAt.length === 1 ) { + myAt[1] = myAt[0]; + } + + $.each( [ "left", "top" ], function( i, offsetPosition ) { + if ( +myAt[ i ] === myAt[ i ] ) { + offset[ i ] = myAt[ i ]; + myAt[ i ] = offsetPosition; + } + }); + + position = { + my: myAt[0] + (offset[0] < 0 ? offset[0] : "+" + offset[0]) + " " + + myAt[1] + (offset[1] < 0 ? offset[1] : "+" + offset[1]), + at: myAt.join(" ") + }; + } + + position = $.extend( {}, $.ui.dialog.prototype.options.position, position ); + } else { + position = $.ui.dialog.prototype.options.position; + } + + // need to show the dialog to get the actual offset in the position plugin + isVisible = this.uiDialog.is(":visible"); + if ( !isVisible ) { + this.uiDialog.show(); + } + this.uiDialog.position( position ); + if ( !isVisible ) { + this.uiDialog.hide(); + } + } + }); +} + +}( jQuery ) ); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.draggable.js b/extensions/jui/yii/jui/assets/jquery.ui.draggable.js new file mode 100644 index 0000000..a52519b --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.draggable.js @@ -0,0 +1,958 @@ +/*! + * jQuery UI Draggable 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/draggable/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.mouse.js + * jquery.ui.widget.js + */ +(function( $, undefined ) { + +$.widget("ui.draggable", $.ui.mouse, { + version: "1.10.3", + widgetEventPrefix: "drag", + options: { + addClasses: true, + appendTo: "parent", + axis: false, + connectToSortable: false, + containment: false, + cursor: "auto", + cursorAt: false, + grid: false, + handle: false, + helper: "original", + iframeFix: false, + opacity: false, + refreshPositions: false, + revert: false, + revertDuration: 500, + scope: "default", + scroll: true, + scrollSensitivity: 20, + scrollSpeed: 20, + snap: false, + snapMode: "both", + snapTolerance: 20, + stack: false, + zIndex: false, + + // callbacks + drag: null, + start: null, + stop: null + }, + _create: function() { + + if (this.options.helper === "original" && !(/^(?:r|a|f)/).test(this.element.css("position"))) { + this.element[0].style.position = "relative"; + } + if (this.options.addClasses){ + this.element.addClass("ui-draggable"); + } + if (this.options.disabled){ + this.element.addClass("ui-draggable-disabled"); + } + + this._mouseInit(); + + }, + + _destroy: function() { + this.element.removeClass( "ui-draggable ui-draggable-dragging ui-draggable-disabled" ); + this._mouseDestroy(); + }, + + _mouseCapture: function(event) { + + var o = this.options; + + // among others, prevent a drag on a resizable-handle + if (this.helper || o.disabled || $(event.target).closest(".ui-resizable-handle").length > 0) { + return false; + } + + //Quit if we're not on a valid handle + this.handle = this._getHandle(event); + if (!this.handle) { + return false; + } + + $(o.iframeFix === true ? "iframe" : o.iframeFix).each(function() { + $("<div class='ui-draggable-iframeFix' style='background: #fff;'></div>") + .css({ + width: this.offsetWidth+"px", height: this.offsetHeight+"px", + position: "absolute", opacity: "0.001", zIndex: 1000 + }) + .css($(this).offset()) + .appendTo("body"); + }); + + return true; + + }, + + _mouseStart: function(event) { + + var o = this.options; + + //Create and append the visible helper + this.helper = this._createHelper(event); + + this.helper.addClass("ui-draggable-dragging"); + + //Cache the helper size + this._cacheHelperProportions(); + + //If ddmanager is used for droppables, set the global draggable + if($.ui.ddmanager) { + $.ui.ddmanager.current = this; + } + + /* + * - Position generation - + * This block generates everything position related - it's the core of draggables. + */ + + //Cache the margins of the original element + this._cacheMargins(); + + //Store the helper's css position + this.cssPosition = this.helper.css( "position" ); + this.scrollParent = this.helper.scrollParent(); + this.offsetParent = this.helper.offsetParent(); + this.offsetParentCssPosition = this.offsetParent.css( "position" ); + + //The element's absolute position on the page minus margins + this.offset = this.positionAbs = this.element.offset(); + this.offset = { + top: this.offset.top - this.margins.top, + left: this.offset.left - this.margins.left + }; + + //Reset scroll cache + this.offset.scroll = false; + + $.extend(this.offset, { + click: { //Where the click happened, relative to the element + left: event.pageX - this.offset.left, + top: event.pageY - this.offset.top + }, + parent: this._getParentOffset(), + relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper + }); + + //Generate the original position + this.originalPosition = this.position = this._generatePosition(event); + this.originalPageX = event.pageX; + this.originalPageY = event.pageY; + + //Adjust the mouse offset relative to the helper if "cursorAt" is supplied + (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); + + //Set a containment if given in the options + this._setContainment(); + + //Trigger event + callbacks + if(this._trigger("start", event) === false) { + this._clear(); + return false; + } + + //Recache the helper size + this._cacheHelperProportions(); + + //Prepare the droppable offsets + if ($.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(this, event); + } + + + this._mouseDrag(event, true); //Execute the drag once - this causes the helper not to be visible before getting its correct position + + //If the ddmanager is used for droppables, inform the manager that dragging has started (see #5003) + if ( $.ui.ddmanager ) { + $.ui.ddmanager.dragStart(this, event); + } + + return true; + }, + + _mouseDrag: function(event, noPropagation) { + // reset any necessary cached properties (see #5009) + if ( this.offsetParentCssPosition === "fixed" ) { + this.offset.parent = this._getParentOffset(); + } + + //Compute the helpers position + this.position = this._generatePosition(event); + this.positionAbs = this._convertPositionTo("absolute"); + + //Call plugins and callbacks and use the resulting position if something is returned + if (!noPropagation) { + var ui = this._uiHash(); + if(this._trigger("drag", event, ui) === false) { + this._mouseUp({}); + return false; + } + this.position = ui.position; + } + + if(!this.options.axis || this.options.axis !== "y") { + this.helper[0].style.left = this.position.left+"px"; + } + if(!this.options.axis || this.options.axis !== "x") { + this.helper[0].style.top = this.position.top+"px"; + } + if($.ui.ddmanager) { + $.ui.ddmanager.drag(this, event); + } + + return false; + }, + + _mouseStop: function(event) { + + //If we are using droppables, inform the manager about the drop + var that = this, + dropped = false; + if ($.ui.ddmanager && !this.options.dropBehaviour) { + dropped = $.ui.ddmanager.drop(this, event); + } + + //if a drop comes from outside (a sortable) + if(this.dropped) { + dropped = this.dropped; + this.dropped = false; + } + + //if the original element is no longer in the DOM don't bother to continue (see #8269) + if ( this.options.helper === "original" && !$.contains( this.element[ 0 ].ownerDocument, this.element[ 0 ] ) ) { + return false; + } + + if((this.options.revert === "invalid" && !dropped) || (this.options.revert === "valid" && dropped) || this.options.revert === true || ($.isFunction(this.options.revert) && this.options.revert.call(this.element, dropped))) { + $(this.helper).animate(this.originalPosition, parseInt(this.options.revertDuration, 10), function() { + if(that._trigger("stop", event) !== false) { + that._clear(); + } + }); + } else { + if(this._trigger("stop", event) !== false) { + this._clear(); + } + } + + return false; + }, + + _mouseUp: function(event) { + //Remove frame helpers + $("div.ui-draggable-iframeFix").each(function() { + this.parentNode.removeChild(this); + }); + + //If the ddmanager is used for droppables, inform the manager that dragging has stopped (see #5003) + if( $.ui.ddmanager ) { + $.ui.ddmanager.dragStop(this, event); + } + + return $.ui.mouse.prototype._mouseUp.call(this, event); + }, + + cancel: function() { + + if(this.helper.is(".ui-draggable-dragging")) { + this._mouseUp({}); + } else { + this._clear(); + } + + return this; + + }, + + _getHandle: function(event) { + return this.options.handle ? + !!$( event.target ).closest( this.element.find( this.options.handle ) ).length : + true; + }, + + _createHelper: function(event) { + + var o = this.options, + helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event])) : (o.helper === "clone" ? this.element.clone().removeAttr("id") : this.element); + + if(!helper.parents("body").length) { + helper.appendTo((o.appendTo === "parent" ? this.element[0].parentNode : o.appendTo)); + } + + if(helper[0] !== this.element[0] && !(/(fixed|absolute)/).test(helper.css("position"))) { + helper.css("position", "absolute"); + } + + return helper; + + }, + + _adjustOffsetFromHelper: function(obj) { + if (typeof obj === "string") { + obj = obj.split(" "); + } + if ($.isArray(obj)) { + obj = {left: +obj[0], top: +obj[1] || 0}; + } + if ("left" in obj) { + this.offset.click.left = obj.left + this.margins.left; + } + if ("right" in obj) { + this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; + } + if ("top" in obj) { + this.offset.click.top = obj.top + this.margins.top; + } + if ("bottom" in obj) { + this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; + } + }, + + _getParentOffset: function() { + + //Get the offsetParent and cache its position + var po = this.offsetParent.offset(); + + // This is a special case where we need to modify a offset calculated on start, since the following happened: + // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent + // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that + // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag + if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { + po.left += this.scrollParent.scrollLeft(); + po.top += this.scrollParent.scrollTop(); + } + + //This needs to be actually done for all browsers, since pageX/pageY includes this information + //Ugly IE fix + if((this.offsetParent[0] === document.body) || + (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { + po = { top: 0, left: 0 }; + } + + return { + top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), + left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) + }; + + }, + + _getRelativeOffset: function() { + + if(this.cssPosition === "relative") { + var p = this.element.position(); + return { + top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), + left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() + }; + } else { + return { top: 0, left: 0 }; + } + + }, + + _cacheMargins: function() { + this.margins = { + left: (parseInt(this.element.css("marginLeft"),10) || 0), + top: (parseInt(this.element.css("marginTop"),10) || 0), + right: (parseInt(this.element.css("marginRight"),10) || 0), + bottom: (parseInt(this.element.css("marginBottom"),10) || 0) + }; + }, + + _cacheHelperProportions: function() { + this.helperProportions = { + width: this.helper.outerWidth(), + height: this.helper.outerHeight() + }; + }, + + _setContainment: function() { + + var over, c, ce, + o = this.options; + + if ( !o.containment ) { + this.containment = null; + return; + } + + if ( o.containment === "window" ) { + this.containment = [ + $( window ).scrollLeft() - this.offset.relative.left - this.offset.parent.left, + $( window ).scrollTop() - this.offset.relative.top - this.offset.parent.top, + $( window ).scrollLeft() + $( window ).width() - this.helperProportions.width - this.margins.left, + $( window ).scrollTop() + ( $( window ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top + ]; + return; + } + + if ( o.containment === "document") { + this.containment = [ + 0, + 0, + $( document ).width() - this.helperProportions.width - this.margins.left, + ( $( document ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top + ]; + return; + } + + if ( o.containment.constructor === Array ) { + this.containment = o.containment; + return; + } + + if ( o.containment === "parent" ) { + o.containment = this.helper[ 0 ].parentNode; + } + + c = $( o.containment ); + ce = c[ 0 ]; + + if( !ce ) { + return; + } + + over = c.css( "overflow" ) !== "hidden"; + + this.containment = [ + ( parseInt( c.css( "borderLeftWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingLeft" ), 10 ) || 0 ), + ( parseInt( c.css( "borderTopWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingTop" ), 10 ) || 0 ) , + ( over ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - ( parseInt( c.css( "borderRightWidth" ), 10 ) || 0 ) - ( parseInt( c.css( "paddingRight" ), 10 ) || 0 ) - this.helperProportions.width - this.margins.left - this.margins.right, + ( over ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - ( parseInt( c.css( "borderBottomWidth" ), 10 ) || 0 ) - ( parseInt( c.css( "paddingBottom" ), 10 ) || 0 ) - this.helperProportions.height - this.margins.top - this.margins.bottom + ]; + this.relative_container = c; + }, + + _convertPositionTo: function(d, pos) { + + if(!pos) { + pos = this.position; + } + + var mod = d === "absolute" ? 1 : -1, + scroll = this.cssPosition === "absolute" && !( this.scrollParent[ 0 ] !== document && $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? this.offsetParent : this.scrollParent; + + //Cache the scroll + if (!this.offset.scroll) { + this.offset.scroll = {top : scroll.scrollTop(), left : scroll.scrollLeft()}; + } + + return { + top: ( + pos.top + // The absolute mouse position + this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top ) * mod ) + ), + left: ( + pos.left + // The absolute mouse position + this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left ) * mod ) + ) + }; + + }, + + _generatePosition: function(event) { + + var containment, co, top, left, + o = this.options, + scroll = this.cssPosition === "absolute" && !( this.scrollParent[ 0 ] !== document && $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? this.offsetParent : this.scrollParent, + pageX = event.pageX, + pageY = event.pageY; + + //Cache the scroll + if (!this.offset.scroll) { + this.offset.scroll = {top : scroll.scrollTop(), left : scroll.scrollLeft()}; + } + + /* + * - Position constraining - + * Constrain the position to a mix of grid, containment. + */ + + // If we are not dragging yet, we won't check for options + if ( this.originalPosition ) { + if ( this.containment ) { + if ( this.relative_container ){ + co = this.relative_container.offset(); + containment = [ + this.containment[ 0 ] + co.left, + this.containment[ 1 ] + co.top, + this.containment[ 2 ] + co.left, + this.containment[ 3 ] + co.top + ]; + } + else { + containment = this.containment; + } + + if(event.pageX - this.offset.click.left < containment[0]) { + pageX = containment[0] + this.offset.click.left; + } + if(event.pageY - this.offset.click.top < containment[1]) { + pageY = containment[1] + this.offset.click.top; + } + if(event.pageX - this.offset.click.left > containment[2]) { + pageX = containment[2] + this.offset.click.left; + } + if(event.pageY - this.offset.click.top > containment[3]) { + pageY = containment[3] + this.offset.click.top; + } + } + + if(o.grid) { + //Check for grid elements set to 0 to prevent divide by 0 error causing invalid argument errors in IE (see ticket #6950) + top = o.grid[1] ? this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1] : this.originalPageY; + pageY = containment ? ((top - this.offset.click.top >= containment[1] || top - this.offset.click.top > containment[3]) ? top : ((top - this.offset.click.top >= containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; + + left = o.grid[0] ? this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0] : this.originalPageX; + pageX = containment ? ((left - this.offset.click.left >= containment[0] || left - this.offset.click.left > containment[2]) ? left : ((left - this.offset.click.left >= containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; + } + + } + + return { + top: ( + pageY - // The absolute mouse position + this.offset.click.top - // Click offset (relative to the element) + this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top + // The offsetParent's offset without borders (offset + border) + ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top ) + ), + left: ( + pageX - // The absolute mouse position + this.offset.click.left - // Click offset (relative to the element) + this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left + // The offsetParent's offset without borders (offset + border) + ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left ) + ) + }; + + }, + + _clear: function() { + this.helper.removeClass("ui-draggable-dragging"); + if(this.helper[0] !== this.element[0] && !this.cancelHelperRemoval) { + this.helper.remove(); + } + this.helper = null; + this.cancelHelperRemoval = false; + }, + + // From now on bulk stuff - mainly helpers + + _trigger: function(type, event, ui) { + ui = ui || this._uiHash(); + $.ui.plugin.call(this, type, [event, ui]); + //The absolute position has to be recalculated after plugins + if(type === "drag") { + this.positionAbs = this._convertPositionTo("absolute"); + } + return $.Widget.prototype._trigger.call(this, type, event, ui); + }, + + plugins: {}, + + _uiHash: function() { + return { + helper: this.helper, + position: this.position, + originalPosition: this.originalPosition, + offset: this.positionAbs + }; + } + +}); + +$.ui.plugin.add("draggable", "connectToSortable", { + start: function(event, ui) { + + var inst = $(this).data("ui-draggable"), o = inst.options, + uiSortable = $.extend({}, ui, { item: inst.element }); + inst.sortables = []; + $(o.connectToSortable).each(function() { + var sortable = $.data(this, "ui-sortable"); + if (sortable && !sortable.options.disabled) { + inst.sortables.push({ + instance: sortable, + shouldRevert: sortable.options.revert + }); + sortable.refreshPositions(); // Call the sortable's refreshPositions at drag start to refresh the containerCache since the sortable container cache is used in drag and needs to be up to date (this will ensure it's initialised as well as being kept in step with any changes that might have happened on the page). + sortable._trigger("activate", event, uiSortable); + } + }); + + }, + stop: function(event, ui) { + + //If we are still over the sortable, we fake the stop event of the sortable, but also remove helper + var inst = $(this).data("ui-draggable"), + uiSortable = $.extend({}, ui, { item: inst.element }); + + $.each(inst.sortables, function() { + if(this.instance.isOver) { + + this.instance.isOver = 0; + + inst.cancelHelperRemoval = true; //Don't remove the helper in the draggable instance + this.instance.cancelHelperRemoval = false; //Remove it in the sortable instance (so sortable plugins like revert still work) + + //The sortable revert is supported, and we have to set a temporary dropped variable on the draggable to support revert: "valid/invalid" + if(this.shouldRevert) { + this.instance.options.revert = this.shouldRevert; + } + + //Trigger the stop of the sortable + this.instance._mouseStop(event); + + this.instance.options.helper = this.instance.options._helper; + + //If the helper has been the original item, restore properties in the sortable + if(inst.options.helper === "original") { + this.instance.currentItem.css({ top: "auto", left: "auto" }); + } + + } else { + this.instance.cancelHelperRemoval = false; //Remove the helper in the sortable instance + this.instance._trigger("deactivate", event, uiSortable); + } + + }); + + }, + drag: function(event, ui) { + + var inst = $(this).data("ui-draggable"), that = this; + + $.each(inst.sortables, function() { + + var innermostIntersecting = false, + thisSortable = this; + + //Copy over some variables to allow calling the sortable's native _intersectsWith + this.instance.positionAbs = inst.positionAbs; + this.instance.helperProportions = inst.helperProportions; + this.instance.offset.click = inst.offset.click; + + if(this.instance._intersectsWith(this.instance.containerCache)) { + innermostIntersecting = true; + $.each(inst.sortables, function () { + this.instance.positionAbs = inst.positionAbs; + this.instance.helperProportions = inst.helperProportions; + this.instance.offset.click = inst.offset.click; + if (this !== thisSortable && + this.instance._intersectsWith(this.instance.containerCache) && + $.contains(thisSortable.instance.element[0], this.instance.element[0]) + ) { + innermostIntersecting = false; + } + return innermostIntersecting; + }); + } + + + if(innermostIntersecting) { + //If it intersects, we use a little isOver variable and set it once, so our move-in stuff gets fired only once + if(!this.instance.isOver) { + + this.instance.isOver = 1; + //Now we fake the start of dragging for the sortable instance, + //by cloning the list group item, appending it to the sortable and using it as inst.currentItem + //We can then fire the start event of the sortable with our passed browser event, and our own helper (so it doesn't create a new one) + this.instance.currentItem = $(that).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item", true); + this.instance.options._helper = this.instance.options.helper; //Store helper option to later restore it + this.instance.options.helper = function() { return ui.helper[0]; }; + + event.target = this.instance.currentItem[0]; + this.instance._mouseCapture(event, true); + this.instance._mouseStart(event, true, true); + + //Because the browser event is way off the new appended portlet, we modify a couple of variables to reflect the changes + this.instance.offset.click.top = inst.offset.click.top; + this.instance.offset.click.left = inst.offset.click.left; + this.instance.offset.parent.left -= inst.offset.parent.left - this.instance.offset.parent.left; + this.instance.offset.parent.top -= inst.offset.parent.top - this.instance.offset.parent.top; + + inst._trigger("toSortable", event); + inst.dropped = this.instance.element; //draggable revert needs that + //hack so receive/update callbacks work (mostly) + inst.currentItem = inst.element; + this.instance.fromOutside = inst; + + } + + //Provided we did all the previous steps, we can fire the drag event of the sortable on every draggable drag, when it intersects with the sortable + if(this.instance.currentItem) { + this.instance._mouseDrag(event); + } + + } else { + + //If it doesn't intersect with the sortable, and it intersected before, + //we fake the drag stop of the sortable, but make sure it doesn't remove the helper by using cancelHelperRemoval + if(this.instance.isOver) { + + this.instance.isOver = 0; + this.instance.cancelHelperRemoval = true; + + //Prevent reverting on this forced stop + this.instance.options.revert = false; + + // The out event needs to be triggered independently + this.instance._trigger("out", event, this.instance._uiHash(this.instance)); + + this.instance._mouseStop(event, true); + this.instance.options.helper = this.instance.options._helper; + + //Now we remove our currentItem, the list group clone again, and the placeholder, and animate the helper back to it's original size + this.instance.currentItem.remove(); + if(this.instance.placeholder) { + this.instance.placeholder.remove(); + } + + inst._trigger("fromSortable", event); + inst.dropped = false; //draggable revert needs that + } + + } + + }); + + } +}); + +$.ui.plugin.add("draggable", "cursor", { + start: function() { + var t = $("body"), o = $(this).data("ui-draggable").options; + if (t.css("cursor")) { + o._cursor = t.css("cursor"); + } + t.css("cursor", o.cursor); + }, + stop: function() { + var o = $(this).data("ui-draggable").options; + if (o._cursor) { + $("body").css("cursor", o._cursor); + } + } +}); + +$.ui.plugin.add("draggable", "opacity", { + start: function(event, ui) { + var t = $(ui.helper), o = $(this).data("ui-draggable").options; + if(t.css("opacity")) { + o._opacity = t.css("opacity"); + } + t.css("opacity", o.opacity); + }, + stop: function(event, ui) { + var o = $(this).data("ui-draggable").options; + if(o._opacity) { + $(ui.helper).css("opacity", o._opacity); + } + } +}); + +$.ui.plugin.add("draggable", "scroll", { + start: function() { + var i = $(this).data("ui-draggable"); + if(i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { + i.overflowOffset = i.scrollParent.offset(); + } + }, + drag: function( event ) { + + var i = $(this).data("ui-draggable"), o = i.options, scrolled = false; + + if(i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { + + if(!o.axis || o.axis !== "x") { + if((i.overflowOffset.top + i.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { + i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop + o.scrollSpeed; + } else if(event.pageY - i.overflowOffset.top < o.scrollSensitivity) { + i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop - o.scrollSpeed; + } + } + + if(!o.axis || o.axis !== "y") { + if((i.overflowOffset.left + i.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { + i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft + o.scrollSpeed; + } else if(event.pageX - i.overflowOffset.left < o.scrollSensitivity) { + i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft - o.scrollSpeed; + } + } + + } else { + + if(!o.axis || o.axis !== "x") { + if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); + } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); + } + } + + if(!o.axis || o.axis !== "y") { + if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); + } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); + } + } + + } + + if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(i, event); + } + + } +}); + +$.ui.plugin.add("draggable", "snap", { + start: function() { + + var i = $(this).data("ui-draggable"), + o = i.options; + + i.snapElements = []; + + $(o.snap.constructor !== String ? ( o.snap.items || ":data(ui-draggable)" ) : o.snap).each(function() { + var $t = $(this), + $o = $t.offset(); + if(this !== i.element[0]) { + i.snapElements.push({ + item: this, + width: $t.outerWidth(), height: $t.outerHeight(), + top: $o.top, left: $o.left + }); + } + }); + + }, + drag: function(event, ui) { + + var ts, bs, ls, rs, l, r, t, b, i, first, + inst = $(this).data("ui-draggable"), + o = inst.options, + d = o.snapTolerance, + x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width, + y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height; + + for (i = inst.snapElements.length - 1; i >= 0; i--){ + + l = inst.snapElements[i].left; + r = l + inst.snapElements[i].width; + t = inst.snapElements[i].top; + b = t + inst.snapElements[i].height; + + if ( x2 < l - d || x1 > r + d || y2 < t - d || y1 > b + d || !$.contains( inst.snapElements[ i ].item.ownerDocument, inst.snapElements[ i ].item ) ) { + if(inst.snapElements[i].snapping) { + (inst.options.snap.release && inst.options.snap.release.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item }))); + } + inst.snapElements[i].snapping = false; + continue; + } + + if(o.snapMode !== "inner") { + ts = Math.abs(t - y2) <= d; + bs = Math.abs(b - y1) <= d; + ls = Math.abs(l - x2) <= d; + rs = Math.abs(r - x1) <= d; + if(ts) { + ui.position.top = inst._convertPositionTo("relative", { top: t - inst.helperProportions.height, left: 0 }).top - inst.margins.top; + } + if(bs) { + ui.position.top = inst._convertPositionTo("relative", { top: b, left: 0 }).top - inst.margins.top; + } + if(ls) { + ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l - inst.helperProportions.width }).left - inst.margins.left; + } + if(rs) { + ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r }).left - inst.margins.left; + } + } + + first = (ts || bs || ls || rs); + + if(o.snapMode !== "outer") { + ts = Math.abs(t - y1) <= d; + bs = Math.abs(b - y2) <= d; + ls = Math.abs(l - x1) <= d; + rs = Math.abs(r - x2) <= d; + if(ts) { + ui.position.top = inst._convertPositionTo("relative", { top: t, left: 0 }).top - inst.margins.top; + } + if(bs) { + ui.position.top = inst._convertPositionTo("relative", { top: b - inst.helperProportions.height, left: 0 }).top - inst.margins.top; + } + if(ls) { + ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l }).left - inst.margins.left; + } + if(rs) { + ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r - inst.helperProportions.width }).left - inst.margins.left; + } + } + + if(!inst.snapElements[i].snapping && (ts || bs || ls || rs || first)) { + (inst.options.snap.snap && inst.options.snap.snap.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item }))); + } + inst.snapElements[i].snapping = (ts || bs || ls || rs || first); + + } + + } +}); + +$.ui.plugin.add("draggable", "stack", { + start: function() { + var min, + o = this.data("ui-draggable").options, + group = $.makeArray($(o.stack)).sort(function(a,b) { + return (parseInt($(a).css("zIndex"),10) || 0) - (parseInt($(b).css("zIndex"),10) || 0); + }); + + if (!group.length) { return; } + + min = parseInt($(group[0]).css("zIndex"), 10) || 0; + $(group).each(function(i) { + $(this).css("zIndex", min + i); + }); + this.css("zIndex", (min + group.length)); + } +}); + +$.ui.plugin.add("draggable", "zIndex", { + start: function(event, ui) { + var t = $(ui.helper), o = $(this).data("ui-draggable").options; + if(t.css("zIndex")) { + o._zIndex = t.css("zIndex"); + } + t.css("zIndex", o.zIndex); + }, + stop: function(event, ui) { + var o = $(this).data("ui-draggable").options; + if(o._zIndex) { + $(ui.helper).css("zIndex", o._zIndex); + } + } +}); + +})(jQuery); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.droppable.js b/extensions/jui/yii/jui/assets/jquery.ui.droppable.js new file mode 100644 index 0000000..24337d0 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.droppable.js @@ -0,0 +1,372 @@ +/*! + * jQuery UI Droppable 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/droppable/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + * jquery.ui.mouse.js + * jquery.ui.draggable.js + */ +(function( $, undefined ) { + +function isOverAxis( x, reference, size ) { + return ( x > reference ) && ( x < ( reference + size ) ); +} + +$.widget("ui.droppable", { + version: "1.10.3", + widgetEventPrefix: "drop", + options: { + accept: "*", + activeClass: false, + addClasses: true, + greedy: false, + hoverClass: false, + scope: "default", + tolerance: "intersect", + + // callbacks + activate: null, + deactivate: null, + drop: null, + out: null, + over: null + }, + _create: function() { + + var o = this.options, + accept = o.accept; + + this.isover = false; + this.isout = true; + + this.accept = $.isFunction(accept) ? accept : function(d) { + return d.is(accept); + }; + + //Store the droppable's proportions + this.proportions = { width: this.element[0].offsetWidth, height: this.element[0].offsetHeight }; + + // Add the reference and positions to the manager + $.ui.ddmanager.droppables[o.scope] = $.ui.ddmanager.droppables[o.scope] || []; + $.ui.ddmanager.droppables[o.scope].push(this); + + (o.addClasses && this.element.addClass("ui-droppable")); + + }, + + _destroy: function() { + var i = 0, + drop = $.ui.ddmanager.droppables[this.options.scope]; + + for ( ; i < drop.length; i++ ) { + if ( drop[i] === this ) { + drop.splice(i, 1); + } + } + + this.element.removeClass("ui-droppable ui-droppable-disabled"); + }, + + _setOption: function(key, value) { + + if(key === "accept") { + this.accept = $.isFunction(value) ? value : function(d) { + return d.is(value); + }; + } + $.Widget.prototype._setOption.apply(this, arguments); + }, + + _activate: function(event) { + var draggable = $.ui.ddmanager.current; + if(this.options.activeClass) { + this.element.addClass(this.options.activeClass); + } + if(draggable){ + this._trigger("activate", event, this.ui(draggable)); + } + }, + + _deactivate: function(event) { + var draggable = $.ui.ddmanager.current; + if(this.options.activeClass) { + this.element.removeClass(this.options.activeClass); + } + if(draggable){ + this._trigger("deactivate", event, this.ui(draggable)); + } + }, + + _over: function(event) { + + var draggable = $.ui.ddmanager.current; + + // Bail if draggable and droppable are same element + if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { + return; + } + + if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { + if(this.options.hoverClass) { + this.element.addClass(this.options.hoverClass); + } + this._trigger("over", event, this.ui(draggable)); + } + + }, + + _out: function(event) { + + var draggable = $.ui.ddmanager.current; + + // Bail if draggable and droppable are same element + if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { + return; + } + + if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { + if(this.options.hoverClass) { + this.element.removeClass(this.options.hoverClass); + } + this._trigger("out", event, this.ui(draggable)); + } + + }, + + _drop: function(event,custom) { + + var draggable = custom || $.ui.ddmanager.current, + childrenIntersection = false; + + // Bail if draggable and droppable are same element + if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { + return false; + } + + this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function() { + var inst = $.data(this, "ui-droppable"); + if( + inst.options.greedy && + !inst.options.disabled && + inst.options.scope === draggable.options.scope && + inst.accept.call(inst.element[0], (draggable.currentItem || draggable.element)) && + $.ui.intersect(draggable, $.extend(inst, { offset: inst.element.offset() }), inst.options.tolerance) + ) { childrenIntersection = true; return false; } + }); + if(childrenIntersection) { + return false; + } + + if(this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { + if(this.options.activeClass) { + this.element.removeClass(this.options.activeClass); + } + if(this.options.hoverClass) { + this.element.removeClass(this.options.hoverClass); + } + this._trigger("drop", event, this.ui(draggable)); + return this.element; + } + + return false; + + }, + + ui: function(c) { + return { + draggable: (c.currentItem || c.element), + helper: c.helper, + position: c.position, + offset: c.positionAbs + }; + } + +}); + +$.ui.intersect = function(draggable, droppable, toleranceMode) { + + if (!droppable.offset) { + return false; + } + + var draggableLeft, draggableTop, + x1 = (draggable.positionAbs || draggable.position.absolute).left, x2 = x1 + draggable.helperProportions.width, + y1 = (draggable.positionAbs || draggable.position.absolute).top, y2 = y1 + draggable.helperProportions.height, + l = droppable.offset.left, r = l + droppable.proportions.width, + t = droppable.offset.top, b = t + droppable.proportions.height; + + switch (toleranceMode) { + case "fit": + return (l <= x1 && x2 <= r && t <= y1 && y2 <= b); + case "intersect": + return (l < x1 + (draggable.helperProportions.width / 2) && // Right Half + x2 - (draggable.helperProportions.width / 2) < r && // Left Half + t < y1 + (draggable.helperProportions.height / 2) && // Bottom Half + y2 - (draggable.helperProportions.height / 2) < b ); // Top Half + case "pointer": + draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left); + draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top); + return isOverAxis( draggableTop, t, droppable.proportions.height ) && isOverAxis( draggableLeft, l, droppable.proportions.width ); + case "touch": + return ( + (y1 >= t && y1 <= b) || // Top edge touching + (y2 >= t && y2 <= b) || // Bottom edge touching + (y1 < t && y2 > b) // Surrounded vertically + ) && ( + (x1 >= l && x1 <= r) || // Left edge touching + (x2 >= l && x2 <= r) || // Right edge touching + (x1 < l && x2 > r) // Surrounded horizontally + ); + default: + return false; + } + +}; + +/* + This manager tracks offsets of draggables and droppables +*/ +$.ui.ddmanager = { + current: null, + droppables: { "default": [] }, + prepareOffsets: function(t, event) { + + var i, j, + m = $.ui.ddmanager.droppables[t.options.scope] || [], + type = event ? event.type : null, // workaround for #2317 + list = (t.currentItem || t.element).find(":data(ui-droppable)").addBack(); + + droppablesLoop: for (i = 0; i < m.length; i++) { + + //No disabled and non-accepted + if(m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0],(t.currentItem || t.element)))) { + continue; + } + + // Filter out elements in the current dragged item + for (j=0; j < list.length; j++) { + if(list[j] === m[i].element[0]) { + m[i].proportions.height = 0; + continue droppablesLoop; + } + } + + m[i].visible = m[i].element.css("display") !== "none"; + if(!m[i].visible) { + continue; + } + + //Activate the droppable if used directly from draggables + if(type === "mousedown") { + m[i]._activate.call(m[i], event); + } + + m[i].offset = m[i].element.offset(); + m[i].proportions = { width: m[i].element[0].offsetWidth, height: m[i].element[0].offsetHeight }; + + } + + }, + drop: function(draggable, event) { + + var dropped = false; + // Create a copy of the droppables in case the list changes during the drop (#9116) + $.each(($.ui.ddmanager.droppables[draggable.options.scope] || []).slice(), function() { + + if(!this.options) { + return; + } + if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance)) { + dropped = this._drop.call(this, event) || dropped; + } + + if (!this.options.disabled && this.visible && this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { + this.isout = true; + this.isover = false; + this._deactivate.call(this, event); + } + + }); + return dropped; + + }, + dragStart: function( draggable, event ) { + //Listen for scrolling so that if the dragging causes scrolling the position of the droppables can be recalculated (see #5003) + draggable.element.parentsUntil( "body" ).bind( "scroll.droppable", function() { + if( !draggable.options.refreshPositions ) { + $.ui.ddmanager.prepareOffsets( draggable, event ); + } + }); + }, + drag: function(draggable, event) { + + //If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse. + if(draggable.options.refreshPositions) { + $.ui.ddmanager.prepareOffsets(draggable, event); + } + + //Run through all droppables and check their positions based on specific tolerance options + $.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() { + + if(this.options.disabled || this.greedyChild || !this.visible) { + return; + } + + var parentInstance, scope, parent, + intersects = $.ui.intersect(draggable, this, this.options.tolerance), + c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null); + if(!c) { + return; + } + + if (this.options.greedy) { + // find droppable parents with same scope + scope = this.options.scope; + parent = this.element.parents(":data(ui-droppable)").filter(function () { + return $.data(this, "ui-droppable").options.scope === scope; + }); + + if (parent.length) { + parentInstance = $.data(parent[0], "ui-droppable"); + parentInstance.greedyChild = (c === "isover"); + } + } + + // we just moved into a greedy child + if (parentInstance && c === "isover") { + parentInstance.isover = false; + parentInstance.isout = true; + parentInstance._out.call(parentInstance, event); + } + + this[c] = true; + this[c === "isout" ? "isover" : "isout"] = false; + this[c === "isover" ? "_over" : "_out"].call(this, event); + + // we just moved out of a greedy child + if (parentInstance && c === "isout") { + parentInstance.isout = false; + parentInstance.isover = true; + parentInstance._over.call(parentInstance, event); + } + }); + + }, + dragStop: function( draggable, event ) { + draggable.element.parentsUntil( "body" ).unbind( "scroll.droppable" ); + //Call prepareOffsets one final time since IE does not fire return scroll events when overflow was caused by drag (see #5003) + if( !draggable.options.refreshPositions ) { + $.ui.ddmanager.prepareOffsets( draggable, event ); + } + } +}; + +})(jQuery); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.effect-all.js b/extensions/jui/yii/jui/assets/jquery.ui.effect-all.js new file mode 100755 index 0000000..f5c6bc7 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.effect-all.js @@ -0,0 +1,2261 @@ +/*! jQuery UI - v1.10.3 - 2013-07-18 +* http://jqueryui.com +* Includes: jquery.ui.effect.js, jquery.ui.effect-blind.js, jquery.ui.effect-bounce.js, jquery.ui.effect-clip.js, jquery.ui.effect-drop.js, jquery.ui.effect-explode.js, jquery.ui.effect-fade.js, jquery.ui.effect-fold.js, jquery.ui.effect-highlight.js, jquery.ui.effect-pulsate.js, jquery.ui.effect-scale.js, jquery.ui.effect-shake.js, jquery.ui.effect-slide.js, jquery.ui.effect-transfer.js +* Copyright 2013 jQuery Foundation and other contributors Licensed MIT */ + +(function($, undefined) { + +var dataSpace = "ui-effects-"; + +$.effects = { + effect: {} +}; + +/*! + * jQuery Color Animations v2.1.2 + * https://github.com/jquery/jquery-color + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * Date: Wed Jan 16 08:47:09 2013 -0600 + */ +(function( jQuery, undefined ) { + + var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor", + + // plusequals test for += 100 -= 100 + rplusequals = /^([\-+])=\s*(\d+\.?\d*)/, + // a set of RE's that can match strings and generate color tuples. + stringParsers = [{ + re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, + parse: function( execResult ) { + return [ + execResult[ 1 ], + execResult[ 2 ], + execResult[ 3 ], + execResult[ 4 ] + ]; + } + }, { + re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, + parse: function( execResult ) { + return [ + execResult[ 1 ] * 2.55, + execResult[ 2 ] * 2.55, + execResult[ 3 ] * 2.55, + execResult[ 4 ] + ]; + } + }, { + // this regex ignores A-F because it's compared against an already lowercased string + re: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/, + parse: function( execResult ) { + return [ + parseInt( execResult[ 1 ], 16 ), + parseInt( execResult[ 2 ], 16 ), + parseInt( execResult[ 3 ], 16 ) + ]; + } + }, { + // this regex ignores A-F because it's compared against an already lowercased string + re: /#([a-f0-9])([a-f0-9])([a-f0-9])/, + parse: function( execResult ) { + return [ + parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ), + parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ), + parseInt( execResult[ 3 ] + execResult[ 3 ], 16 ) + ]; + } + }, { + re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, + space: "hsla", + parse: function( execResult ) { + return [ + execResult[ 1 ], + execResult[ 2 ] / 100, + execResult[ 3 ] / 100, + execResult[ 4 ] + ]; + } + }], + + // jQuery.Color( ) + color = jQuery.Color = function( color, green, blue, alpha ) { + return new jQuery.Color.fn.parse( color, green, blue, alpha ); + }, + spaces = { + rgba: { + props: { + red: { + idx: 0, + type: "byte" + }, + green: { + idx: 1, + type: "byte" + }, + blue: { + idx: 2, + type: "byte" + } + } + }, + + hsla: { + props: { + hue: { + idx: 0, + type: "degrees" + }, + saturation: { + idx: 1, + type: "percent" + }, + lightness: { + idx: 2, + type: "percent" + } + } + } + }, + propTypes = { + "byte": { + floor: true, + max: 255 + }, + "percent": { + max: 1 + }, + "degrees": { + mod: 360, + floor: true + } + }, + support = color.support = {}, + + // element for support tests + supportElem = jQuery( "<p>" )[ 0 ], + + // colors = jQuery.Color.names + colors, + + // local aliases of functions called often + each = jQuery.each; + +// determine rgba support immediately +supportElem.style.cssText = "background-color:rgba(1,1,1,.5)"; +support.rgba = supportElem.style.backgroundColor.indexOf( "rgba" ) > -1; + +// define cache name and alpha properties +// for rgba and hsla spaces +each( spaces, function( spaceName, space ) { + space.cache = "_" + spaceName; + space.props.alpha = { + idx: 3, + type: "percent", + def: 1 + }; +}); + +function clamp( value, prop, allowEmpty ) { + var type = propTypes[ prop.type ] || {}; + + if ( value == null ) { + return (allowEmpty || !prop.def) ? null : prop.def; + } + + // ~~ is an short way of doing floor for positive numbers + value = type.floor ? ~~value : parseFloat( value ); + + // IE will pass in empty strings as value for alpha, + // which will hit this case + if ( isNaN( value ) ) { + return prop.def; + } + + if ( type.mod ) { + // we add mod before modding to make sure that negatives values + // get converted properly: -10 -> 350 + return (value + type.mod) % type.mod; + } + + // for now all property types without mod have min and max + return 0 > value ? 0 : type.max < value ? type.max : value; +} + +function stringParse( string ) { + var inst = color(), + rgba = inst._rgba = []; + + string = string.toLowerCase(); + + each( stringParsers, function( i, parser ) { + var parsed, + match = parser.re.exec( string ), + values = match && parser.parse( match ), + spaceName = parser.space || "rgba"; + + if ( values ) { + parsed = inst[ spaceName ]( values ); + + // if this was an rgba parse the assignment might happen twice + // oh well.... + inst[ spaces[ spaceName ].cache ] = parsed[ spaces[ spaceName ].cache ]; + rgba = inst._rgba = parsed._rgba; + + // exit each( stringParsers ) here because we matched + return false; + } + }); + + // Found a stringParser that handled it + if ( rgba.length ) { + + // if this came from a parsed string, force "transparent" when alpha is 0 + // chrome, (and maybe others) return "transparent" as rgba(0,0,0,0) + if ( rgba.join() === "0,0,0,0" ) { + jQuery.extend( rgba, colors.transparent ); + } + return inst; + } + + // named colors + return colors[ string ]; +} + +color.fn = jQuery.extend( color.prototype, { + parse: function( red, green, blue, alpha ) { + if ( red === undefined ) { + this._rgba = [ null, null, null, null ]; + return this; + } + if ( red.jquery || red.nodeType ) { + red = jQuery( red ).css( green ); + green = undefined; + } + + var inst = this, + type = jQuery.type( red ), + rgba = this._rgba = []; + + // more than 1 argument specified - assume ( red, green, blue, alpha ) + if ( green !== undefined ) { + red = [ red, green, blue, alpha ]; + type = "array"; + } + + if ( type === "string" ) { + return this.parse( stringParse( red ) || colors._default ); + } + + if ( type === "array" ) { + each( spaces.rgba.props, function( key, prop ) { + rgba[ prop.idx ] = clamp( red[ prop.idx ], prop ); + }); + return this; + } + + if ( type === "object" ) { + if ( red instanceof color ) { + each( spaces, function( spaceName, space ) { + if ( red[ space.cache ] ) { + inst[ space.cache ] = red[ space.cache ].slice(); + } + }); + } else { + each( spaces, function( spaceName, space ) { + var cache = space.cache; + each( space.props, function( key, prop ) { + + // if the cache doesn't exist, and we know how to convert + if ( !inst[ cache ] && space.to ) { + + // if the value was null, we don't need to copy it + // if the key was alpha, we don't need to copy it either + if ( key === "alpha" || red[ key ] == null ) { + return; + } + inst[ cache ] = space.to( inst._rgba ); + } + + // this is the only case where we allow nulls for ALL properties. + // call clamp with alwaysAllowEmpty + inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true ); + }); + + // everything defined but alpha? + if ( inst[ cache ] && jQuery.inArray( null, inst[ cache ].slice( 0, 3 ) ) < 0 ) { + // use the default of 1 + inst[ cache ][ 3 ] = 1; + if ( space.from ) { + inst._rgba = space.from( inst[ cache ] ); + } + } + }); + } + return this; + } + }, + is: function( compare ) { + var is = color( compare ), + same = true, + inst = this; + + each( spaces, function( _, space ) { + var localCache, + isCache = is[ space.cache ]; + if (isCache) { + localCache = inst[ space.cache ] || space.to && space.to( inst._rgba ) || []; + each( space.props, function( _, prop ) { + if ( isCache[ prop.idx ] != null ) { + same = ( isCache[ prop.idx ] === localCache[ prop.idx ] ); + return same; + } + }); + } + return same; + }); + return same; + }, + _space: function() { + var used = [], + inst = this; + each( spaces, function( spaceName, space ) { + if ( inst[ space.cache ] ) { + used.push( spaceName ); + } + }); + return used.pop(); + }, + transition: function( other, distance ) { + var end = color( other ), + spaceName = end._space(), + space = spaces[ spaceName ], + startColor = this.alpha() === 0 ? color( "transparent" ) : this, + start = startColor[ space.cache ] || space.to( startColor._rgba ), + result = start.slice(); + + end = end[ space.cache ]; + each( space.props, function( key, prop ) { + var index = prop.idx, + startValue = start[ index ], + endValue = end[ index ], + type = propTypes[ prop.type ] || {}; + + // if null, don't override start value + if ( endValue === null ) { + return; + } + // if null - use end + if ( startValue === null ) { + result[ index ] = endValue; + } else { + if ( type.mod ) { + if ( endValue - startValue > type.mod / 2 ) { + startValue += type.mod; + } else if ( startValue - endValue > type.mod / 2 ) { + startValue -= type.mod; + } + } + result[ index ] = clamp( ( endValue - startValue ) * distance + startValue, prop ); + } + }); + return this[ spaceName ]( result ); + }, + blend: function( opaque ) { + // if we are already opaque - return ourself + if ( this._rgba[ 3 ] === 1 ) { + return this; + } + + var rgb = this._rgba.slice(), + a = rgb.pop(), + blend = color( opaque )._rgba; + + return color( jQuery.map( rgb, function( v, i ) { + return ( 1 - a ) * blend[ i ] + a * v; + })); + }, + toRgbaString: function() { + var prefix = "rgba(", + rgba = jQuery.map( this._rgba, function( v, i ) { + return v == null ? ( i > 2 ? 1 : 0 ) : v; + }); + + if ( rgba[ 3 ] === 1 ) { + rgba.pop(); + prefix = "rgb("; + } + + return prefix + rgba.join() + ")"; + }, + toHslaString: function() { + var prefix = "hsla(", + hsla = jQuery.map( this.hsla(), function( v, i ) { + if ( v == null ) { + v = i > 2 ? 1 : 0; + } + + // catch 1 and 2 + if ( i && i < 3 ) { + v = Math.round( v * 100 ) + "%"; + } + return v; + }); + + if ( hsla[ 3 ] === 1 ) { + hsla.pop(); + prefix = "hsl("; + } + return prefix + hsla.join() + ")"; + }, + toHexString: function( includeAlpha ) { + var rgba = this._rgba.slice(), + alpha = rgba.pop(); + + if ( includeAlpha ) { + rgba.push( ~~( alpha * 255 ) ); + } + + return "#" + jQuery.map( rgba, function( v ) { + + // default to 0 when nulls exist + v = ( v || 0 ).toString( 16 ); + return v.length === 1 ? "0" + v : v; + }).join(""); + }, + toString: function() { + return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString(); + } +}); +color.fn.parse.prototype = color.fn; + +// hsla conversions adapted from: +// https://code.google.com/p/maashaack/source/browse/packages/graphics/trunk/src/graphics/colors/HUE2RGB.as?r=5021 + +function hue2rgb( p, q, h ) { + h = ( h + 1 ) % 1; + if ( h * 6 < 1 ) { + return p + (q - p) * h * 6; + } + if ( h * 2 < 1) { + return q; + } + if ( h * 3 < 2 ) { + return p + (q - p) * ((2/3) - h) * 6; + } + return p; +} + +spaces.hsla.to = function ( rgba ) { + if ( rgba[ 0 ] == null || rgba[ 1 ] == null || rgba[ 2 ] == null ) { + return [ null, null, null, rgba[ 3 ] ]; + } + var r = rgba[ 0 ] / 255, + g = rgba[ 1 ] / 255, + b = rgba[ 2 ] / 255, + a = rgba[ 3 ], + max = Math.max( r, g, b ), + min = Math.min( r, g, b ), + diff = max - min, + add = max + min, + l = add * 0.5, + h, s; + + if ( min === max ) { + h = 0; + } else if ( r === max ) { + h = ( 60 * ( g - b ) / diff ) + 360; + } else if ( g === max ) { + h = ( 60 * ( b - r ) / diff ) + 120; + } else { + h = ( 60 * ( r - g ) / diff ) + 240; + } + + // chroma (diff) == 0 means greyscale which, by definition, saturation = 0% + // otherwise, saturation is based on the ratio of chroma (diff) to lightness (add) + if ( diff === 0 ) { + s = 0; + } else if ( l <= 0.5 ) { + s = diff / add; + } else { + s = diff / ( 2 - add ); + } + return [ Math.round(h) % 360, s, l, a == null ? 1 : a ]; +}; + +spaces.hsla.from = function ( hsla ) { + if ( hsla[ 0 ] == null || hsla[ 1 ] == null || hsla[ 2 ] == null ) { + return [ null, null, null, hsla[ 3 ] ]; + } + var h = hsla[ 0 ] / 360, + s = hsla[ 1 ], + l = hsla[ 2 ], + a = hsla[ 3 ], + q = l <= 0.5 ? l * ( 1 + s ) : l + s - l * s, + p = 2 * l - q; + + return [ + Math.round( hue2rgb( p, q, h + ( 1 / 3 ) ) * 255 ), + Math.round( hue2rgb( p, q, h ) * 255 ), + Math.round( hue2rgb( p, q, h - ( 1 / 3 ) ) * 255 ), + a + ]; +}; + + +each( spaces, function( spaceName, space ) { + var props = space.props, + cache = space.cache, + to = space.to, + from = space.from; + + // makes rgba() and hsla() + color.fn[ spaceName ] = function( value ) { + + // generate a cache for this space if it doesn't exist + if ( to && !this[ cache ] ) { + this[ cache ] = to( this._rgba ); + } + if ( value === undefined ) { + return this[ cache ].slice(); + } + + var ret, + type = jQuery.type( value ), + arr = ( type === "array" || type === "object" ) ? value : arguments, + local = this[ cache ].slice(); + + each( props, function( key, prop ) { + var val = arr[ type === "object" ? key : prop.idx ]; + if ( val == null ) { + val = local[ prop.idx ]; + } + local[ prop.idx ] = clamp( val, prop ); + }); + + if ( from ) { + ret = color( from( local ) ); + ret[ cache ] = local; + return ret; + } else { + return color( local ); + } + }; + + // makes red() green() blue() alpha() hue() saturation() lightness() + each( props, function( key, prop ) { + // alpha is included in more than one space + if ( color.fn[ key ] ) { + return; + } + color.fn[ key ] = function( value ) { + var vtype = jQuery.type( value ), + fn = ( key === "alpha" ? ( this._hsla ? "hsla" : "rgba" ) : spaceName ), + local = this[ fn ](), + cur = local[ prop.idx ], + match; + + if ( vtype === "undefined" ) { + return cur; + } + + if ( vtype === "function" ) { + value = value.call( this, cur ); + vtype = jQuery.type( value ); + } + if ( value == null && prop.empty ) { + return this; + } + if ( vtype === "string" ) { + match = rplusequals.exec( value ); + if ( match ) { + value = cur + parseFloat( match[ 2 ] ) * ( match[ 1 ] === "+" ? 1 : -1 ); + } + } + local[ prop.idx ] = value; + return this[ fn ]( local ); + }; + }); +}); + +// add cssHook and .fx.step function for each named hook. +// accept a space separated string of properties +color.hook = function( hook ) { + var hooks = hook.split( " " ); + each( hooks, function( i, hook ) { + jQuery.cssHooks[ hook ] = { + set: function( elem, value ) { + var parsed, curElem, + backgroundColor = ""; + + if ( value !== "transparent" && ( jQuery.type( value ) !== "string" || ( parsed = stringParse( value ) ) ) ) { + value = color( parsed || value ); + if ( !support.rgba && value._rgba[ 3 ] !== 1 ) { + curElem = hook === "backgroundColor" ? elem.parentNode : elem; + while ( + (backgroundColor === "" || backgroundColor === "transparent") && + curElem && curElem.style + ) { + try { + backgroundColor = jQuery.css( curElem, "backgroundColor" ); + curElem = curElem.parentNode; + } catch ( e ) { + } + } + + value = value.blend( backgroundColor && backgroundColor !== "transparent" ? + backgroundColor : + "_default" ); + } + + value = value.toRgbaString(); + } + try { + elem.style[ hook ] = value; + } catch( e ) { + // wrapped to prevent IE from throwing errors on "invalid" values like 'auto' or 'inherit' + } + } + }; + jQuery.fx.step[ hook ] = function( fx ) { + if ( !fx.colorInit ) { + fx.start = color( fx.elem, hook ); + fx.end = color( fx.end ); + fx.colorInit = true; + } + jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) ); + }; + }); + +}; + +color.hook( stepHooks ); + +jQuery.cssHooks.borderColor = { + expand: function( value ) { + var expanded = {}; + + each( [ "Top", "Right", "Bottom", "Left" ], function( i, part ) { + expanded[ "border" + part + "Color" ] = value; + }); + return expanded; + } +}; + +// Basic color names only. +// Usage of any of the other color names requires adding yourself or including +// jquery.color.svg-names.js. +colors = jQuery.Color.names = { + // 4.1. Basic color keywords + aqua: "#00ffff", + black: "#000000", + blue: "#0000ff", + fuchsia: "#ff00ff", + gray: "#808080", + green: "#008000", + lime: "#00ff00", + maroon: "#800000", + navy: "#000080", + olive: "#808000", + purple: "#800080", + red: "#ff0000", + silver: "#c0c0c0", + teal: "#008080", + white: "#ffffff", + yellow: "#ffff00", + + // 4.2.3. "transparent" color keyword + transparent: [ null, null, null, 0 ], + + _default: "#ffffff" +}; + +})( jQuery ); + + +/******************************************************************************/ +/****************************** CLASS ANIMATIONS ******************************/ +/******************************************************************************/ +(function() { + +var classAnimationActions = [ "add", "remove", "toggle" ], + shorthandStyles = { + border: 1, + borderBottom: 1, + borderColor: 1, + borderLeft: 1, + borderRight: 1, + borderTop: 1, + borderWidth: 1, + margin: 1, + padding: 1 + }; + +$.each([ "borderLeftStyle", "borderRightStyle", "borderBottomStyle", "borderTopStyle" ], function( _, prop ) { + $.fx.step[ prop ] = function( fx ) { + if ( fx.end !== "none" && !fx.setAttr || fx.pos === 1 && !fx.setAttr ) { + jQuery.style( fx.elem, prop, fx.end ); + fx.setAttr = true; + } + }; +}); + +function getElementStyles( elem ) { + var key, len, + style = elem.ownerDocument.defaultView ? + elem.ownerDocument.defaultView.getComputedStyle( elem, null ) : + elem.currentStyle, + styles = {}; + + if ( style && style.length && style[ 0 ] && style[ style[ 0 ] ] ) { + len = style.length; + while ( len-- ) { + key = style[ len ]; + if ( typeof style[ key ] === "string" ) { + styles[ $.camelCase( key ) ] = style[ key ]; + } + } + // support: Opera, IE <9 + } else { + for ( key in style ) { + if ( typeof style[ key ] === "string" ) { + styles[ key ] = style[ key ]; + } + } + } + + return styles; +} + + +function styleDifference( oldStyle, newStyle ) { + var diff = {}, + name, value; + + for ( name in newStyle ) { + value = newStyle[ name ]; + if ( oldStyle[ name ] !== value ) { + if ( !shorthandStyles[ name ] ) { + if ( $.fx.step[ name ] || !isNaN( parseFloat( value ) ) ) { + diff[ name ] = value; + } + } + } + } + + return diff; +} + +// support: jQuery <1.8 +if ( !$.fn.addBack ) { + $.fn.addBack = function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + }; +} + +$.effects.animateClass = function( value, duration, easing, callback ) { + var o = $.speed( duration, easing, callback ); + + return this.queue( function() { + var animated = $( this ), + baseClass = animated.attr( "class" ) || "", + applyClassChange, + allAnimations = o.children ? animated.find( "*" ).addBack() : animated; + + // map the animated objects to store the original styles. + allAnimations = allAnimations.map(function() { + var el = $( this ); + return { + el: el, + start: getElementStyles( this ) + }; + }); + + // apply class change + applyClassChange = function() { + $.each( classAnimationActions, function(i, action) { + if ( value[ action ] ) { + animated[ action + "Class" ]( value[ action ] ); + } + }); + }; + applyClassChange(); + + // map all animated objects again - calculate new styles and diff + allAnimations = allAnimations.map(function() { + this.end = getElementStyles( this.el[ 0 ] ); + this.diff = styleDifference( this.start, this.end ); + return this; + }); + + // apply original class + animated.attr( "class", baseClass ); + + // map all animated objects again - this time collecting a promise + allAnimations = allAnimations.map(function() { + var styleInfo = this, + dfd = $.Deferred(), + opts = $.extend({}, o, { + queue: false, + complete: function() { + dfd.resolve( styleInfo ); + } + }); + + this.el.animate( this.diff, opts ); + return dfd.promise(); + }); + + // once all animations have completed: + $.when.apply( $, allAnimations.get() ).done(function() { + + // set the final class + applyClassChange(); + + // for each animated element, + // clear all css properties that were animated + $.each( arguments, function() { + var el = this.el; + $.each( this.diff, function(key) { + el.css( key, "" ); + }); + }); + + // this is guarnteed to be there if you use jQuery.speed() + // it also handles dequeuing the next anim... + o.complete.call( animated[ 0 ] ); + }); + }); +}; + +$.fn.extend({ + addClass: (function( orig ) { + return function( classNames, speed, easing, callback ) { + return speed ? + $.effects.animateClass.call( this, + { add: classNames }, speed, easing, callback ) : + orig.apply( this, arguments ); + }; + })( $.fn.addClass ), + + removeClass: (function( orig ) { + return function( classNames, speed, easing, callback ) { + return arguments.length > 1 ? + $.effects.animateClass.call( this, + { remove: classNames }, speed, easing, callback ) : + orig.apply( this, arguments ); + }; + })( $.fn.removeClass ), + + toggleClass: (function( orig ) { + return function( classNames, force, speed, easing, callback ) { + if ( typeof force === "boolean" || force === undefined ) { + if ( !speed ) { + // without speed parameter + return orig.apply( this, arguments ); + } else { + return $.effects.animateClass.call( this, + (force ? { add: classNames } : { remove: classNames }), + speed, easing, callback ); + } + } else { + // without force parameter + return $.effects.animateClass.call( this, + { toggle: classNames }, force, speed, easing ); + } + }; + })( $.fn.toggleClass ), + + switchClass: function( remove, add, speed, easing, callback) { + return $.effects.animateClass.call( this, { + add: add, + remove: remove + }, speed, easing, callback ); + } +}); + +})(); + +/******************************************************************************/ +/*********************************** EFFECTS **********************************/ +/******************************************************************************/ + +(function() { + +$.extend( $.effects, { + version: "1.10.3", + + // Saves a set of properties in a data storage + save: function( element, set ) { + for( var i=0; i < set.length; i++ ) { + if ( set[ i ] !== null ) { + element.data( dataSpace + set[ i ], element[ 0 ].style[ set[ i ] ] ); + } + } + }, + + // Restores a set of previously saved properties from a data storage + restore: function( element, set ) { + var val, i; + for( i=0; i < set.length; i++ ) { + if ( set[ i ] !== null ) { + val = element.data( dataSpace + set[ i ] ); + // support: jQuery 1.6.2 + // http://bugs.jquery.com/ticket/9917 + // jQuery 1.6.2 incorrectly returns undefined for any falsy value. + // We can't differentiate between "" and 0 here, so we just assume + // empty string since it's likely to be a more common value... + if ( val === undefined ) { + val = ""; + } + element.css( set[ i ], val ); + } + } + }, + + setMode: function( el, mode ) { + if (mode === "toggle") { + mode = el.is( ":hidden" ) ? "show" : "hide"; + } + return mode; + }, + + // Translates a [top,left] array into a baseline value + // this should be a little more flexible in the future to handle a string & hash + getBaseline: function( origin, original ) { + var y, x; + switch ( origin[ 0 ] ) { + case "top": y = 0; break; + case "middle": y = 0.5; break; + case "bottom": y = 1; break; + default: y = origin[ 0 ] / original.height; + } + switch ( origin[ 1 ] ) { + case "left": x = 0; break; + case "center": x = 0.5; break; + case "right": x = 1; break; + default: x = origin[ 1 ] / original.width; + } + return { + x: x, + y: y + }; + }, + + // Wraps the element around a wrapper that copies position properties + createWrapper: function( element ) { + + // if the element is already wrapped, return it + if ( element.parent().is( ".ui-effects-wrapper" )) { + return element.parent(); + } + + // wrap the element + var props = { + width: element.outerWidth(true), + height: element.outerHeight(true), + "float": element.css( "float" ) + }, + wrapper = $( "<div></div>" ) + .addClass( "ui-effects-wrapper" ) + .css({ + fontSize: "100%", + background: "transparent", + border: "none", + margin: 0, + padding: 0 + }), + // Store the size in case width/height are defined in % - Fixes #5245 + size = { + width: element.width(), + height: element.height() + }, + active = document.activeElement; + + // support: Firefox + // Firefox incorrectly exposes anonymous content + // https://bugzilla.mozilla.org/show_bug.cgi?id=561664 + try { + active.id; + } catch( e ) { + active = document.body; + } + + element.wrap( wrapper ); + + // Fixes #7595 - Elements lose focus when wrapped. + if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) { + $( active ).focus(); + } + + wrapper = element.parent(); //Hotfix for jQuery 1.4 since some change in wrap() seems to actually lose the reference to the wrapped element + + // transfer positioning properties to the wrapper + if ( element.css( "position" ) === "static" ) { + wrapper.css({ position: "relative" }); + element.css({ position: "relative" }); + } else { + $.extend( props, { + position: element.css( "position" ), + zIndex: element.css( "z-index" ) + }); + $.each([ "top", "left", "bottom", "right" ], function(i, pos) { + props[ pos ] = element.css( pos ); + if ( isNaN( parseInt( props[ pos ], 10 ) ) ) { + props[ pos ] = "auto"; + } + }); + element.css({ + position: "relative", + top: 0, + left: 0, + right: "auto", + bottom: "auto" + }); + } + element.css(size); + + return wrapper.css( props ).show(); + }, + + removeWrapper: function( element ) { + var active = document.activeElement; + + if ( element.parent().is( ".ui-effects-wrapper" ) ) { + element.parent().replaceWith( element ); + + // Fixes #7595 - Elements lose focus when wrapped. + if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) { + $( active ).focus(); + } + } + + + return element; + }, + + setTransition: function( element, list, factor, value ) { + value = value || {}; + $.each( list, function( i, x ) { + var unit = element.cssUnit( x ); + if ( unit[ 0 ] > 0 ) { + value[ x ] = unit[ 0 ] * factor + unit[ 1 ]; + } + }); + return value; + } +}); + +// return an effect options object for the given parameters: +function _normalizeArguments( effect, options, speed, callback ) { + + // allow passing all options as the first parameter + if ( $.isPlainObject( effect ) ) { + options = effect; + effect = effect.effect; + } + + // convert to an object + effect = { effect: effect }; + + // catch (effect, null, ...) + if ( options == null ) { + options = {}; + } + + // catch (effect, callback) + if ( $.isFunction( options ) ) { + callback = options; + speed = null; + options = {}; + } + + // catch (effect, speed, ?) + if ( typeof options === "number" || $.fx.speeds[ options ] ) { + callback = speed; + speed = options; + options = {}; + } + + // catch (effect, options, callback) + if ( $.isFunction( speed ) ) { + callback = speed; + speed = null; + } + + // add options to effect + if ( options ) { + $.extend( effect, options ); + } + + speed = speed || options.duration; + effect.duration = $.fx.off ? 0 : + typeof speed === "number" ? speed : + speed in $.fx.speeds ? $.fx.speeds[ speed ] : + $.fx.speeds._default; + + effect.complete = callback || options.complete; + + return effect; +} + +function standardAnimationOption( option ) { + // Valid standard speeds (nothing, number, named speed) + if ( !option || typeof option === "number" || $.fx.speeds[ option ] ) { + return true; + } + + // Invalid strings - treat as "normal" speed + if ( typeof option === "string" && !$.effects.effect[ option ] ) { + return true; + } + + // Complete callback + if ( $.isFunction( option ) ) { + return true; + } + + // Options hash (but not naming an effect) + if ( typeof option === "object" && !option.effect ) { + return true; + } + + // Didn't match any standard API + return false; +} + +$.fn.extend({ + effect: function( /* effect, options, speed, callback */ ) { + var args = _normalizeArguments.apply( this, arguments ), + mode = args.mode, + queue = args.queue, + effectMethod = $.effects.effect[ args.effect ]; + + if ( $.fx.off || !effectMethod ) { + // delegate to the original method (e.g., .show()) if possible + if ( mode ) { + return this[ mode ]( args.duration, args.complete ); + } else { + return this.each( function() { + if ( args.complete ) { + args.complete.call( this ); + } + }); + } + } + + function run( next ) { + var elem = $( this ), + complete = args.complete, + mode = args.mode; + + function done() { + if ( $.isFunction( complete ) ) { + complete.call( elem[0] ); + } + if ( $.isFunction( next ) ) { + next(); + } + } + + // If the element already has the correct final state, delegate to + // the core methods so the internal tracking of "olddisplay" works. + if ( elem.is( ":hidden" ) ? mode === "hide" : mode === "show" ) { + elem[ mode ](); + done(); + } else { + effectMethod.call( elem[0], args, done ); + } + } + + return queue === false ? this.each( run ) : this.queue( queue || "fx", run ); + }, + + show: (function( orig ) { + return function( option ) { + if ( standardAnimationOption( option ) ) { + return orig.apply( this, arguments ); + } else { + var args = _normalizeArguments.apply( this, arguments ); + args.mode = "show"; + return this.effect.call( this, args ); + } + }; + })( $.fn.show ), + + hide: (function( orig ) { + return function( option ) { + if ( standardAnimationOption( option ) ) { + return orig.apply( this, arguments ); + } else { + var args = _normalizeArguments.apply( this, arguments ); + args.mode = "hide"; + return this.effect.call( this, args ); + } + }; + })( $.fn.hide ), + + toggle: (function( orig ) { + return function( option ) { + if ( standardAnimationOption( option ) || typeof option === "boolean" ) { + return orig.apply( this, arguments ); + } else { + var args = _normalizeArguments.apply( this, arguments ); + args.mode = "toggle"; + return this.effect.call( this, args ); + } + }; + })( $.fn.toggle ), + + // helper functions + cssUnit: function(key) { + var style = this.css( key ), + val = []; + + $.each( [ "em", "px", "%", "pt" ], function( i, unit ) { + if ( style.indexOf( unit ) > 0 ) { + val = [ parseFloat( style ), unit ]; + } + }); + return val; + } +}); + +})(); + +/******************************************************************************/ +/*********************************** EASING ***********************************/ +/******************************************************************************/ + +(function() { + +// based on easing equations from Robert Penner (http://www.robertpenner.com/easing) + +var baseEasings = {}; + +$.each( [ "Quad", "Cubic", "Quart", "Quint", "Expo" ], function( i, name ) { + baseEasings[ name ] = function( p ) { + return Math.pow( p, i + 2 ); + }; +}); + +$.extend( baseEasings, { + Sine: function ( p ) { + return 1 - Math.cos( p * Math.PI / 2 ); + }, + Circ: function ( p ) { + return 1 - Math.sqrt( 1 - p * p ); + }, + Elastic: function( p ) { + return p === 0 || p === 1 ? p : + -Math.pow( 2, 8 * (p - 1) ) * Math.sin( ( (p - 1) * 80 - 7.5 ) * Math.PI / 15 ); + }, + Back: function( p ) { + return p * p * ( 3 * p - 2 ); + }, + Bounce: function ( p ) { + var pow2, + bounce = 4; + + while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {} + return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 ); + } +}); + +$.each( baseEasings, function( name, easeIn ) { + $.easing[ "easeIn" + name ] = easeIn; + $.easing[ "easeOut" + name ] = function( p ) { + return 1 - easeIn( 1 - p ); + }; + $.easing[ "easeInOut" + name ] = function( p ) { + return p < 0.5 ? + easeIn( p * 2 ) / 2 : + 1 - easeIn( p * -2 + 2 ) / 2; + }; +}); + +})(); + +})(jQuery); +(function( $, undefined ) { + +var rvertical = /up|down|vertical/, + rpositivemotion = /up|left|vertical|horizontal/; + +$.effects.effect.blind = function( o, done ) { + // Create element + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "height", "width" ], + mode = $.effects.setMode( el, o.mode || "hide" ), + direction = o.direction || "up", + vertical = rvertical.test( direction ), + ref = vertical ? "height" : "width", + ref2 = vertical ? "top" : "left", + motion = rpositivemotion.test( direction ), + animation = {}, + show = mode === "show", + wrapper, distance, margin; + + // if already wrapped, the wrapper's properties are my property. #6245 + if ( el.parent().is( ".ui-effects-wrapper" ) ) { + $.effects.save( el.parent(), props ); + } else { + $.effects.save( el, props ); + } + el.show(); + wrapper = $.effects.createWrapper( el ).css({ + overflow: "hidden" + }); + + distance = wrapper[ ref ](); + margin = parseFloat( wrapper.css( ref2 ) ) || 0; + + animation[ ref ] = show ? distance : 0; + if ( !motion ) { + el + .css( vertical ? "bottom" : "right", 0 ) + .css( vertical ? "top" : "left", "auto" ) + .css({ position: "absolute" }); + + animation[ ref2 ] = show ? margin : distance + margin; + } + + // start at 0 if we are showing + if ( show ) { + wrapper.css( ref, 0 ); + if ( ! motion ) { + wrapper.css( ref2, margin + distance ); + } + } + + // Animate + wrapper.animate( animation, { + duration: o.duration, + easing: o.easing, + queue: false, + complete: function() { + if ( mode === "hide" ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + } + }); + +}; + +})(jQuery); +(function( $, undefined ) { + +$.effects.effect.bounce = function( o, done ) { + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "height", "width" ], + + // defaults: + mode = $.effects.setMode( el, o.mode || "effect" ), + hide = mode === "hide", + show = mode === "show", + direction = o.direction || "up", + distance = o.distance, + times = o.times || 5, + + // number of internal animations + anims = times * 2 + ( show || hide ? 1 : 0 ), + speed = o.duration / anims, + easing = o.easing, + + // utility: + ref = ( direction === "up" || direction === "down" ) ? "top" : "left", + motion = ( direction === "up" || direction === "left" ), + i, + upAnim, + downAnim, + + // we will need to re-assemble the queue to stack our animations in place + queue = el.queue(), + queuelen = queue.length; + + // Avoid touching opacity to prevent clearType and PNG issues in IE + if ( show || hide ) { + props.push( "opacity" ); + } + + $.effects.save( el, props ); + el.show(); + $.effects.createWrapper( el ); // Create Wrapper + + // default distance for the BIGGEST bounce is the outer Distance / 3 + if ( !distance ) { + distance = el[ ref === "top" ? "outerHeight" : "outerWidth" ]() / 3; + } + + if ( show ) { + downAnim = { opacity: 1 }; + downAnim[ ref ] = 0; + + // if we are showing, force opacity 0 and set the initial position + // then do the "first" animation + el.css( "opacity", 0 ) + .css( ref, motion ? -distance * 2 : distance * 2 ) + .animate( downAnim, speed, easing ); + } + + // start at the smallest distance if we are hiding + if ( hide ) { + distance = distance / Math.pow( 2, times - 1 ); + } + + downAnim = {}; + downAnim[ ref ] = 0; + // Bounces up/down/left/right then back to 0 -- times * 2 animations happen here + for ( i = 0; i < times; i++ ) { + upAnim = {}; + upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance; + + el.animate( upAnim, speed, easing ) + .animate( downAnim, speed, easing ); + + distance = hide ? distance * 2 : distance / 2; + } + + // Last Bounce when Hiding + if ( hide ) { + upAnim = { opacity: 0 }; + upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance; + + el.animate( upAnim, speed, easing ); + } + + el.queue(function() { + if ( hide ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + }); + + // inject all the animations we just queued to be first in line (after "inprogress") + if ( queuelen > 1) { + queue.splice.apply( queue, + [ 1, 0 ].concat( queue.splice( queuelen, anims + 1 ) ) ); + } + el.dequeue(); + +}; + +})(jQuery); +(function( $, undefined ) { + +$.effects.effect.clip = function( o, done ) { + // Create element + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "height", "width" ], + mode = $.effects.setMode( el, o.mode || "hide" ), + show = mode === "show", + direction = o.direction || "vertical", + vert = direction === "vertical", + size = vert ? "height" : "width", + position = vert ? "top" : "left", + animation = {}, + wrapper, animate, distance; + + // Save & Show + $.effects.save( el, props ); + el.show(); + + // Create Wrapper + wrapper = $.effects.createWrapper( el ).css({ + overflow: "hidden" + }); + animate = ( el[0].tagName === "IMG" ) ? wrapper : el; + distance = animate[ size ](); + + // Shift + if ( show ) { + animate.css( size, 0 ); + animate.css( position, distance / 2 ); + } + + // Create Animation Object: + animation[ size ] = show ? distance : 0; + animation[ position ] = show ? 0 : distance / 2; + + // Animate + animate.animate( animation, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function() { + if ( !show ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + } + }); + +}; + +})(jQuery); +(function( $, undefined ) { + +$.effects.effect.drop = function( o, done ) { + + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "opacity", "height", "width" ], + mode = $.effects.setMode( el, o.mode || "hide" ), + show = mode === "show", + direction = o.direction || "left", + ref = ( direction === "up" || direction === "down" ) ? "top" : "left", + motion = ( direction === "up" || direction === "left" ) ? "pos" : "neg", + animation = { + opacity: show ? 1 : 0 + }, + distance; + + // Adjust + $.effects.save( el, props ); + el.show(); + $.effects.createWrapper( el ); + + distance = o.distance || el[ ref === "top" ? "outerHeight": "outerWidth" ]( true ) / 2; + + if ( show ) { + el + .css( "opacity", 0 ) + .css( ref, motion === "pos" ? -distance : distance ); + } + + // Animation + animation[ ref ] = ( show ? + ( motion === "pos" ? "+=" : "-=" ) : + ( motion === "pos" ? "-=" : "+=" ) ) + + distance; + + // Animate + el.animate( animation, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function() { + if ( mode === "hide" ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + } + }); +}; + +})(jQuery); +(function( $, undefined ) { + +$.effects.effect.explode = function( o, done ) { + + var rows = o.pieces ? Math.round( Math.sqrt( o.pieces ) ) : 3, + cells = rows, + el = $( this ), + mode = $.effects.setMode( el, o.mode || "hide" ), + show = mode === "show", + + // show and then visibility:hidden the element before calculating offset + offset = el.show().css( "visibility", "hidden" ).offset(), + + // width and height of a piece + width = Math.ceil( el.outerWidth() / cells ), + height = Math.ceil( el.outerHeight() / rows ), + pieces = [], + + // loop + i, j, left, top, mx, my; + + // children animate complete: + function childComplete() { + pieces.push( this ); + if ( pieces.length === rows * cells ) { + animComplete(); + } + } + + // clone the element for each row and cell. + for( i = 0; i < rows ; i++ ) { // ===> + top = offset.top + i * height; + my = i - ( rows - 1 ) / 2 ; + + for( j = 0; j < cells ; j++ ) { // ||| + left = offset.left + j * width; + mx = j - ( cells - 1 ) / 2 ; + + // Create a clone of the now hidden main element that will be absolute positioned + // within a wrapper div off the -left and -top equal to size of our pieces + el + .clone() + .appendTo( "body" ) + .wrap( "<div></div>" ) + .css({ + position: "absolute", + visibility: "visible", + left: -j * width, + top: -i * height + }) + + // select the wrapper - make it overflow: hidden and absolute positioned based on + // where the original was located +left and +top equal to the size of pieces + .parent() + .addClass( "ui-effects-explode" ) + .css({ + position: "absolute", + overflow: "hidden", + width: width, + height: height, + left: left + ( show ? mx * width : 0 ), + top: top + ( show ? my * height : 0 ), + opacity: show ? 0 : 1 + }).animate({ + left: left + ( show ? 0 : mx * width ), + top: top + ( show ? 0 : my * height ), + opacity: show ? 1 : 0 + }, o.duration || 500, o.easing, childComplete ); + } + } + + function animComplete() { + el.css({ + visibility: "visible" + }); + $( pieces ).remove(); + if ( !show ) { + el.hide(); + } + done(); + } +}; + +})(jQuery); +(function( $, undefined ) { + +$.effects.effect.fade = function( o, done ) { + var el = $( this ), + mode = $.effects.setMode( el, o.mode || "toggle" ); + + el.animate({ + opacity: mode + }, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: done + }); +}; + +})( jQuery ); +(function( $, undefined ) { + +$.effects.effect.fold = function( o, done ) { + + // Create element + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "height", "width" ], + mode = $.effects.setMode( el, o.mode || "hide" ), + show = mode === "show", + hide = mode === "hide", + size = o.size || 15, + percent = /([0-9]+)%/.exec( size ), + horizFirst = !!o.horizFirst, + widthFirst = show !== horizFirst, + ref = widthFirst ? [ "width", "height" ] : [ "height", "width" ], + duration = o.duration / 2, + wrapper, distance, + animation1 = {}, + animation2 = {}; + + $.effects.save( el, props ); + el.show(); + + // Create Wrapper + wrapper = $.effects.createWrapper( el ).css({ + overflow: "hidden" + }); + distance = widthFirst ? + [ wrapper.width(), wrapper.height() ] : + [ wrapper.height(), wrapper.width() ]; + + if ( percent ) { + size = parseInt( percent[ 1 ], 10 ) / 100 * distance[ hide ? 0 : 1 ]; + } + if ( show ) { + wrapper.css( horizFirst ? { + height: 0, + width: size + } : { + height: size, + width: 0 + }); + } + + // Animation + animation1[ ref[ 0 ] ] = show ? distance[ 0 ] : size; + animation2[ ref[ 1 ] ] = show ? distance[ 1 ] : 0; + + // Animate + wrapper + .animate( animation1, duration, o.easing ) + .animate( animation2, duration, o.easing, function() { + if ( hide ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + }); + +}; + +})(jQuery); +(function( $, undefined ) { + +$.effects.effect.highlight = function( o, done ) { + var elem = $( this ), + props = [ "backgroundImage", "backgroundColor", "opacity" ], + mode = $.effects.setMode( elem, o.mode || "show" ), + animation = { + backgroundColor: elem.css( "backgroundColor" ) + }; + + if (mode === "hide") { + animation.opacity = 0; + } + + $.effects.save( elem, props ); + + elem + .show() + .css({ + backgroundImage: "none", + backgroundColor: o.color || "#ffff99" + }) + .animate( animation, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function() { + if ( mode === "hide" ) { + elem.hide(); + } + $.effects.restore( elem, props ); + done(); + } + }); +}; + +})(jQuery); +(function( $, undefined ) { + +$.effects.effect.pulsate = function( o, done ) { + var elem = $( this ), + mode = $.effects.setMode( elem, o.mode || "show" ), + show = mode === "show", + hide = mode === "hide", + showhide = ( show || mode === "hide" ), + + // showing or hiding leaves of the "last" animation + anims = ( ( o.times || 5 ) * 2 ) + ( showhide ? 1 : 0 ), + duration = o.duration / anims, + animateTo = 0, + queue = elem.queue(), + queuelen = queue.length, + i; + + if ( show || !elem.is(":visible")) { + elem.css( "opacity", 0 ).show(); + animateTo = 1; + } + + // anims - 1 opacity "toggles" + for ( i = 1; i < anims; i++ ) { + elem.animate({ + opacity: animateTo + }, duration, o.easing ); + animateTo = 1 - animateTo; + } + + elem.animate({ + opacity: animateTo + }, duration, o.easing); + + elem.queue(function() { + if ( hide ) { + elem.hide(); + } + done(); + }); + + // We just queued up "anims" animations, we need to put them next in the queue + if ( queuelen > 1 ) { + queue.splice.apply( queue, + [ 1, 0 ].concat( queue.splice( queuelen, anims + 1 ) ) ); + } + elem.dequeue(); +}; + +})(jQuery); +(function( $, undefined ) { + +$.effects.effect.puff = function( o, done ) { + var elem = $( this ), + mode = $.effects.setMode( elem, o.mode || "hide" ), + hide = mode === "hide", + percent = parseInt( o.percent, 10 ) || 150, + factor = percent / 100, + original = { + height: elem.height(), + width: elem.width(), + outerHeight: elem.outerHeight(), + outerWidth: elem.outerWidth() + }; + + $.extend( o, { + effect: "scale", + queue: false, + fade: true, + mode: mode, + complete: done, + percent: hide ? percent : 100, + from: hide ? + original : + { + height: original.height * factor, + width: original.width * factor, + outerHeight: original.outerHeight * factor, + outerWidth: original.outerWidth * factor + } + }); + + elem.effect( o ); +}; + +$.effects.effect.scale = function( o, done ) { + + // Create element + var el = $( this ), + options = $.extend( true, {}, o ), + mode = $.effects.setMode( el, o.mode || "effect" ), + percent = parseInt( o.percent, 10 ) || + ( parseInt( o.percent, 10 ) === 0 ? 0 : ( mode === "hide" ? 0 : 100 ) ), + direction = o.direction || "both", + origin = o.origin, + original = { + height: el.height(), + width: el.width(), + outerHeight: el.outerHeight(), + outerWidth: el.outerWidth() + }, + factor = { + y: direction !== "horizontal" ? (percent / 100) : 1, + x: direction !== "vertical" ? (percent / 100) : 1 + }; + + // We are going to pass this effect to the size effect: + options.effect = "size"; + options.queue = false; + options.complete = done; + + // Set default origin and restore for show/hide + if ( mode !== "effect" ) { + options.origin = origin || ["middle","center"]; + options.restore = true; + } + + options.from = o.from || ( mode === "show" ? { + height: 0, + width: 0, + outerHeight: 0, + outerWidth: 0 + } : original ); + options.to = { + height: original.height * factor.y, + width: original.width * factor.x, + outerHeight: original.outerHeight * factor.y, + outerWidth: original.outerWidth * factor.x + }; + + // Fade option to support puff + if ( options.fade ) { + if ( mode === "show" ) { + options.from.opacity = 0; + options.to.opacity = 1; + } + if ( mode === "hide" ) { + options.from.opacity = 1; + options.to.opacity = 0; + } + } + + // Animate + el.effect( options ); + +}; + +$.effects.effect.size = function( o, done ) { + + // Create element + var original, baseline, factor, + el = $( this ), + props0 = [ "position", "top", "bottom", "left", "right", "width", "height", "overflow", "opacity" ], + + // Always restore + props1 = [ "position", "top", "bottom", "left", "right", "overflow", "opacity" ], + + // Copy for children + props2 = [ "width", "height", "overflow" ], + cProps = [ "fontSize" ], + vProps = [ "borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom" ], + hProps = [ "borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight" ], + + // Set options + mode = $.effects.setMode( el, o.mode || "effect" ), + restore = o.restore || mode !== "effect", + scale = o.scale || "both", + origin = o.origin || [ "middle", "center" ], + position = el.css( "position" ), + props = restore ? props0 : props1, + zero = { + height: 0, + width: 0, + outerHeight: 0, + outerWidth: 0 + }; + + if ( mode === "show" ) { + el.show(); + } + original = { + height: el.height(), + width: el.width(), + outerHeight: el.outerHeight(), + outerWidth: el.outerWidth() + }; + + if ( o.mode === "toggle" && mode === "show" ) { + el.from = o.to || zero; + el.to = o.from || original; + } else { + el.from = o.from || ( mode === "show" ? zero : original ); + el.to = o.to || ( mode === "hide" ? zero : original ); + } + + // Set scaling factor + factor = { + from: { + y: el.from.height / original.height, + x: el.from.width / original.width + }, + to: { + y: el.to.height / original.height, + x: el.to.width / original.width + } + }; + + // Scale the css box + if ( scale === "box" || scale === "both" ) { + + // Vertical props scaling + if ( factor.from.y !== factor.to.y ) { + props = props.concat( vProps ); + el.from = $.effects.setTransition( el, vProps, factor.from.y, el.from ); + el.to = $.effects.setTransition( el, vProps, factor.to.y, el.to ); + } + + // Horizontal props scaling + if ( factor.from.x !== factor.to.x ) { + props = props.concat( hProps ); + el.from = $.effects.setTransition( el, hProps, factor.from.x, el.from ); + el.to = $.effects.setTransition( el, hProps, factor.to.x, el.to ); + } + } + + // Scale the content + if ( scale === "content" || scale === "both" ) { + + // Vertical props scaling + if ( factor.from.y !== factor.to.y ) { + props = props.concat( cProps ).concat( props2 ); + el.from = $.effects.setTransition( el, cProps, factor.from.y, el.from ); + el.to = $.effects.setTransition( el, cProps, factor.to.y, el.to ); + } + } + + $.effects.save( el, props ); + el.show(); + $.effects.createWrapper( el ); + el.css( "overflow", "hidden" ).css( el.from ); + + // Adjust + if (origin) { // Calculate baseline shifts + baseline = $.effects.getBaseline( origin, original ); + el.from.top = ( original.outerHeight - el.outerHeight() ) * baseline.y; + el.from.left = ( original.outerWidth - el.outerWidth() ) * baseline.x; + el.to.top = ( original.outerHeight - el.to.outerHeight ) * baseline.y; + el.to.left = ( original.outerWidth - el.to.outerWidth ) * baseline.x; + } + el.css( el.from ); // set top & left + + // Animate + if ( scale === "content" || scale === "both" ) { // Scale the children + + // Add margins/font-size + vProps = vProps.concat([ "marginTop", "marginBottom" ]).concat(cProps); + hProps = hProps.concat([ "marginLeft", "marginRight" ]); + props2 = props0.concat(vProps).concat(hProps); + + el.find( "*[width]" ).each( function(){ + var child = $( this ), + c_original = { + height: child.height(), + width: child.width(), + outerHeight: child.outerHeight(), + outerWidth: child.outerWidth() + }; + if (restore) { + $.effects.save(child, props2); + } + + child.from = { + height: c_original.height * factor.from.y, + width: c_original.width * factor.from.x, + outerHeight: c_original.outerHeight * factor.from.y, + outerWidth: c_original.outerWidth * factor.from.x + }; + child.to = { + height: c_original.height * factor.to.y, + width: c_original.width * factor.to.x, + outerHeight: c_original.height * factor.to.y, + outerWidth: c_original.width * factor.to.x + }; + + // Vertical props scaling + if ( factor.from.y !== factor.to.y ) { + child.from = $.effects.setTransition( child, vProps, factor.from.y, child.from ); + child.to = $.effects.setTransition( child, vProps, factor.to.y, child.to ); + } + + // Horizontal props scaling + if ( factor.from.x !== factor.to.x ) { + child.from = $.effects.setTransition( child, hProps, factor.from.x, child.from ); + child.to = $.effects.setTransition( child, hProps, factor.to.x, child.to ); + } + + // Animate children + child.css( child.from ); + child.animate( child.to, o.duration, o.easing, function() { + + // Restore children + if ( restore ) { + $.effects.restore( child, props2 ); + } + }); + }); + } + + // Animate + el.animate( el.to, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function() { + if ( el.to.opacity === 0 ) { + el.css( "opacity", el.from.opacity ); + } + if( mode === "hide" ) { + el.hide(); + } + $.effects.restore( el, props ); + if ( !restore ) { + + // we need to calculate our new positioning based on the scaling + if ( position === "static" ) { + el.css({ + position: "relative", + top: el.to.top, + left: el.to.left + }); + } else { + $.each([ "top", "left" ], function( idx, pos ) { + el.css( pos, function( _, str ) { + var val = parseInt( str, 10 ), + toRef = idx ? el.to.left : el.to.top; + + // if original was "auto", recalculate the new value from wrapper + if ( str === "auto" ) { + return toRef + "px"; + } + + return val + toRef + "px"; + }); + }); + } + } + + $.effects.removeWrapper( el ); + done(); + } + }); + +}; + +})(jQuery); +(function( $, undefined ) { + +$.effects.effect.shake = function( o, done ) { + + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "height", "width" ], + mode = $.effects.setMode( el, o.mode || "effect" ), + direction = o.direction || "left", + distance = o.distance || 20, + times = o.times || 3, + anims = times * 2 + 1, + speed = Math.round(o.duration/anims), + ref = (direction === "up" || direction === "down") ? "top" : "left", + positiveMotion = (direction === "up" || direction === "left"), + animation = {}, + animation1 = {}, + animation2 = {}, + i, + + // we will need to re-assemble the queue to stack our animations in place + queue = el.queue(), + queuelen = queue.length; + + $.effects.save( el, props ); + el.show(); + $.effects.createWrapper( el ); + + // Animation + animation[ ref ] = ( positiveMotion ? "-=" : "+=" ) + distance; + animation1[ ref ] = ( positiveMotion ? "+=" : "-=" ) + distance * 2; + animation2[ ref ] = ( positiveMotion ? "-=" : "+=" ) + distance * 2; + + // Animate + el.animate( animation, speed, o.easing ); + + // Shakes + for ( i = 1; i < times; i++ ) { + el.animate( animation1, speed, o.easing ).animate( animation2, speed, o.easing ); + } + el + .animate( animation1, speed, o.easing ) + .animate( animation, speed / 2, o.easing ) + .queue(function() { + if ( mode === "hide" ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + }); + + // inject all the animations we just queued to be first in line (after "inprogress") + if ( queuelen > 1) { + queue.splice.apply( queue, + [ 1, 0 ].concat( queue.splice( queuelen, anims + 1 ) ) ); + } + el.dequeue(); + +}; + +})(jQuery); +(function( $, undefined ) { + +$.effects.effect.slide = function( o, done ) { + + // Create element + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "width", "height" ], + mode = $.effects.setMode( el, o.mode || "show" ), + show = mode === "show", + direction = o.direction || "left", + ref = (direction === "up" || direction === "down") ? "top" : "left", + positiveMotion = (direction === "up" || direction === "left"), + distance, + animation = {}; + + // Adjust + $.effects.save( el, props ); + el.show(); + distance = o.distance || el[ ref === "top" ? "outerHeight" : "outerWidth" ]( true ); + + $.effects.createWrapper( el ).css({ + overflow: "hidden" + }); + + if ( show ) { + el.css( ref, positiveMotion ? (isNaN(distance) ? "-" + distance : -distance) : distance ); + } + + // Animation + animation[ ref ] = ( show ? + ( positiveMotion ? "+=" : "-=") : + ( positiveMotion ? "-=" : "+=")) + + distance; + + // Animate + el.animate( animation, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function() { + if ( mode === "hide" ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + } + }); +}; + +})(jQuery); +(function( $, undefined ) { + +$.effects.effect.transfer = function( o, done ) { + var elem = $( this ), + target = $( o.to ), + targetFixed = target.css( "position" ) === "fixed", + body = $("body"), + fixTop = targetFixed ? body.scrollTop() : 0, + fixLeft = targetFixed ? body.scrollLeft() : 0, + endPosition = target.offset(), + animation = { + top: endPosition.top - fixTop , + left: endPosition.left - fixLeft , + height: target.innerHeight(), + width: target.innerWidth() + }, + startPosition = elem.offset(), + transfer = $( "<div class='ui-effects-transfer'></div>" ) + .appendTo( document.body ) + .addClass( o.className ) + .css({ + top: startPosition.top - fixTop , + left: startPosition.left - fixLeft , + height: elem.innerHeight(), + width: elem.innerWidth(), + position: targetFixed ? "fixed" : "absolute" + }) + .animate( animation, o.duration, o.easing, function() { + transfer.remove(); + done(); + }); +}; + +})(jQuery); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.menu.js b/extensions/jui/yii/jui/assets/jquery.ui.menu.js new file mode 100644 index 0000000..ace12e2 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.menu.js @@ -0,0 +1,621 @@ +/*! + * jQuery UI Menu 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/menu/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + * jquery.ui.position.js + */ +(function( $, undefined ) { + +$.widget( "ui.menu", { + version: "1.10.3", + defaultElement: "<ul>", + delay: 300, + options: { + icons: { + submenu: "ui-icon-carat-1-e" + }, + menus: "ul", + position: { + my: "left top", + at: "right top" + }, + role: "menu", + + // callbacks + blur: null, + focus: null, + select: null + }, + + _create: function() { + this.activeMenu = this.element; + // flag used to prevent firing of the click handler + // as the event bubbles up through nested menus + this.mouseHandled = false; + this.element + .uniqueId() + .addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" ) + .toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length ) + .attr({ + role: this.options.role, + tabIndex: 0 + }) + // need to catch all clicks on disabled menu + // not possible through _on + .bind( "click" + this.eventNamespace, $.proxy(function( event ) { + if ( this.options.disabled ) { + event.preventDefault(); + } + }, this )); + + if ( this.options.disabled ) { + this.element + .addClass( "ui-state-disabled" ) + .attr( "aria-disabled", "true" ); + } + + this._on({ + // Prevent focus from sticking to links inside menu after clicking + // them (focus should always stay on UL during navigation). + "mousedown .ui-menu-item > a": function( event ) { + event.preventDefault(); + }, + "click .ui-state-disabled > a": function( event ) { + event.preventDefault(); + }, + "click .ui-menu-item:has(a)": function( event ) { + var target = $( event.target ).closest( ".ui-menu-item" ); + if ( !this.mouseHandled && target.not( ".ui-state-disabled" ).length ) { + this.mouseHandled = true; + + this.select( event ); + // Open submenu on click + if ( target.has( ".ui-menu" ).length ) { + this.expand( event ); + } else if ( !this.element.is( ":focus" ) ) { + // Redirect focus to the menu + this.element.trigger( "focus", [ true ] ); + + // If the active item is on the top level, let it stay active. + // Otherwise, blur the active item since it is no longer visible. + if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) { + clearTimeout( this.timer ); + } + } + } + }, + "mouseenter .ui-menu-item": function( event ) { + var target = $( event.currentTarget ); + // Remove ui-state-active class from siblings of the newly focused menu item + // to avoid a jump caused by adjacent elements both having a class with a border + target.siblings().children( ".ui-state-active" ).removeClass( "ui-state-active" ); + this.focus( event, target ); + }, + mouseleave: "collapseAll", + "mouseleave .ui-menu": "collapseAll", + focus: function( event, keepActiveItem ) { + // If there's already an active item, keep it active + // If not, activate the first item + var item = this.active || this.element.children( ".ui-menu-item" ).eq( 0 ); + + if ( !keepActiveItem ) { + this.focus( event, item ); + } + }, + blur: function( event ) { + this._delay(function() { + if ( !$.contains( this.element[0], this.document[0].activeElement ) ) { + this.collapseAll( event ); + } + }); + }, + keydown: "_keydown" + }); + + this.refresh(); + + // Clicks outside of a menu collapse any open menus + this._on( this.document, { + click: function( event ) { + if ( !$( event.target ).closest( ".ui-menu" ).length ) { + this.collapseAll( event ); + } + + // Reset the mouseHandled flag + this.mouseHandled = false; + } + }); + }, + + _destroy: function() { + // Destroy (sub)menus + this.element + .removeAttr( "aria-activedescendant" ) + .find( ".ui-menu" ).addBack() + .removeClass( "ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons" ) + .removeAttr( "role" ) + .removeAttr( "tabIndex" ) + .removeAttr( "aria-labelledby" ) + .removeAttr( "aria-expanded" ) + .removeAttr( "aria-hidden" ) + .removeAttr( "aria-disabled" ) + .removeUniqueId() + .show(); + + // Destroy menu items + this.element.find( ".ui-menu-item" ) + .removeClass( "ui-menu-item" ) + .removeAttr( "role" ) + .removeAttr( "aria-disabled" ) + .children( "a" ) + .removeUniqueId() + .removeClass( "ui-corner-all ui-state-hover" ) + .removeAttr( "tabIndex" ) + .removeAttr( "role" ) + .removeAttr( "aria-haspopup" ) + .children().each( function() { + var elem = $( this ); + if ( elem.data( "ui-menu-submenu-carat" ) ) { + elem.remove(); + } + }); + + // Destroy menu dividers + this.element.find( ".ui-menu-divider" ).removeClass( "ui-menu-divider ui-widget-content" ); + }, + + _keydown: function( event ) { + /*jshint maxcomplexity:20*/ + var match, prev, character, skip, regex, + preventDefault = true; + + function escape( value ) { + return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ); + } + + switch ( event.keyCode ) { + case $.ui.keyCode.PAGE_UP: + this.previousPage( event ); + break; + case $.ui.keyCode.PAGE_DOWN: + this.nextPage( event ); + break; + case $.ui.keyCode.HOME: + this._move( "first", "first", event ); + break; + case $.ui.keyCode.END: + this._move( "last", "last", event ); + break; + case $.ui.keyCode.UP: + this.previous( event ); + break; + case $.ui.keyCode.DOWN: + this.next( event ); + break; + case $.ui.keyCode.LEFT: + this.collapse( event ); + break; + case $.ui.keyCode.RIGHT: + if ( this.active && !this.active.is( ".ui-state-disabled" ) ) { + this.expand( event ); + } + break; + case $.ui.keyCode.ENTER: + case $.ui.keyCode.SPACE: + this._activate( event ); + break; + case $.ui.keyCode.ESCAPE: + this.collapse( event ); + break; + default: + preventDefault = false; + prev = this.previousFilter || ""; + character = String.fromCharCode( event.keyCode ); + skip = false; + + clearTimeout( this.filterTimer ); + + if ( character === prev ) { + skip = true; + } else { + character = prev + character; + } + + regex = new RegExp( "^" + escape( character ), "i" ); + match = this.activeMenu.children( ".ui-menu-item" ).filter(function() { + return regex.test( $( this ).children( "a" ).text() ); + }); + match = skip && match.index( this.active.next() ) !== -1 ? + this.active.nextAll( ".ui-menu-item" ) : + match; + + // If no matches on the current filter, reset to the last character pressed + // to move down the menu to the first item that starts with that character + if ( !match.length ) { + character = String.fromCharCode( event.keyCode ); + regex = new RegExp( "^" + escape( character ), "i" ); + match = this.activeMenu.children( ".ui-menu-item" ).filter(function() { + return regex.test( $( this ).children( "a" ).text() ); + }); + } + + if ( match.length ) { + this.focus( event, match ); + if ( match.length > 1 ) { + this.previousFilter = character; + this.filterTimer = this._delay(function() { + delete this.previousFilter; + }, 1000 ); + } else { + delete this.previousFilter; + } + } else { + delete this.previousFilter; + } + } + + if ( preventDefault ) { + event.preventDefault(); + } + }, + + _activate: function( event ) { + if ( !this.active.is( ".ui-state-disabled" ) ) { + if ( this.active.children( "a[aria-haspopup='true']" ).length ) { + this.expand( event ); + } else { + this.select( event ); + } + } + }, + + refresh: function() { + var menus, + icon = this.options.icons.submenu, + submenus = this.element.find( this.options.menus ); + + // Initialize nested menus + submenus.filter( ":not(.ui-menu)" ) + .addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" ) + .hide() + .attr({ + role: this.options.role, + "aria-hidden": "true", + "aria-expanded": "false" + }) + .each(function() { + var menu = $( this ), + item = menu.prev( "a" ), + submenuCarat = $( "<span>" ) + .addClass( "ui-menu-icon ui-icon " + icon ) + .data( "ui-menu-submenu-carat", true ); + + item + .attr( "aria-haspopup", "true" ) + .prepend( submenuCarat ); + menu.attr( "aria-labelledby", item.attr( "id" ) ); + }); + + menus = submenus.add( this.element ); + + // Don't refresh list items that are already adapted + menus.children( ":not(.ui-menu-item):has(a)" ) + .addClass( "ui-menu-item" ) + .attr( "role", "presentation" ) + .children( "a" ) + .uniqueId() + .addClass( "ui-corner-all" ) + .attr({ + tabIndex: -1, + role: this._itemRole() + }); + + // Initialize unlinked menu-items containing spaces and/or dashes only as dividers + menus.children( ":not(.ui-menu-item)" ).each(function() { + var item = $( this ); + // hyphen, em dash, en dash + if ( !/[^\-\u2014\u2013\s]/.test( item.text() ) ) { + item.addClass( "ui-widget-content ui-menu-divider" ); + } + }); + + // Add aria-disabled attribute to any disabled menu item + menus.children( ".ui-state-disabled" ).attr( "aria-disabled", "true" ); + + // If the active item has been removed, blur the menu + if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { + this.blur(); + } + }, + + _itemRole: function() { + return { + menu: "menuitem", + listbox: "option" + }[ this.options.role ]; + }, + + _setOption: function( key, value ) { + if ( key === "icons" ) { + this.element.find( ".ui-menu-icon" ) + .removeClass( this.options.icons.submenu ) + .addClass( value.submenu ); + } + this._super( key, value ); + }, + + focus: function( event, item ) { + var nested, focused; + this.blur( event, event && event.type === "focus" ); + + this._scrollIntoView( item ); + + this.active = item.first(); + focused = this.active.children( "a" ).addClass( "ui-state-focus" ); + // Only update aria-activedescendant if there's a role + // otherwise we assume focus is managed elsewhere + if ( this.options.role ) { + this.element.attr( "aria-activedescendant", focused.attr( "id" ) ); + } + + // Highlight active parent menu item, if any + this.active + .parent() + .closest( ".ui-menu-item" ) + .children( "a:first" ) + .addClass( "ui-state-active" ); + + if ( event && event.type === "keydown" ) { + this._close(); + } else { + this.timer = this._delay(function() { + this._close(); + }, this.delay ); + } + + nested = item.children( ".ui-menu" ); + if ( nested.length && ( /^mouse/.test( event.type ) ) ) { + this._startOpening(nested); + } + this.activeMenu = item.parent(); + + this._trigger( "focus", event, { item: item } ); + }, + + _scrollIntoView: function( item ) { + var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight; + if ( this._hasScroll() ) { + borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0; + paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0; + offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop; + scroll = this.activeMenu.scrollTop(); + elementHeight = this.activeMenu.height(); + itemHeight = item.height(); + + if ( offset < 0 ) { + this.activeMenu.scrollTop( scroll + offset ); + } else if ( offset + itemHeight > elementHeight ) { + this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight ); + } + } + }, + + blur: function( event, fromFocus ) { + if ( !fromFocus ) { + clearTimeout( this.timer ); + } + + if ( !this.active ) { + return; + } + + this.active.children( "a" ).removeClass( "ui-state-focus" ); + this.active = null; + + this._trigger( "blur", event, { item: this.active } ); + }, + + _startOpening: function( submenu ) { + clearTimeout( this.timer ); + + // Don't open if already open fixes a Firefox bug that caused a .5 pixel + // shift in the submenu position when mousing over the carat icon + if ( submenu.attr( "aria-hidden" ) !== "true" ) { + return; + } + + this.timer = this._delay(function() { + this._close(); + this._open( submenu ); + }, this.delay ); + }, + + _open: function( submenu ) { + var position = $.extend({ + of: this.active + }, this.options.position ); + + clearTimeout( this.timer ); + this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) ) + .hide() + .attr( "aria-hidden", "true" ); + + submenu + .show() + .removeAttr( "aria-hidden" ) + .attr( "aria-expanded", "true" ) + .position( position ); + }, + + collapseAll: function( event, all ) { + clearTimeout( this.timer ); + this.timer = this._delay(function() { + // If we were passed an event, look for the submenu that contains the event + var currentMenu = all ? this.element : + $( event && event.target ).closest( this.element.find( ".ui-menu" ) ); + + // If we found no valid submenu ancestor, use the main menu to close all sub menus anyway + if ( !currentMenu.length ) { + currentMenu = this.element; + } + + this._close( currentMenu ); + + this.blur( event ); + this.activeMenu = currentMenu; + }, this.delay ); + }, + + // With no arguments, closes the currently active menu - if nothing is active + // it closes all menus. If passed an argument, it will search for menus BELOW + _close: function( startMenu ) { + if ( !startMenu ) { + startMenu = this.active ? this.active.parent() : this.element; + } + + startMenu + .find( ".ui-menu" ) + .hide() + .attr( "aria-hidden", "true" ) + .attr( "aria-expanded", "false" ) + .end() + .find( "a.ui-state-active" ) + .removeClass( "ui-state-active" ); + }, + + collapse: function( event ) { + var newItem = this.active && + this.active.parent().closest( ".ui-menu-item", this.element ); + if ( newItem && newItem.length ) { + this._close(); + this.focus( event, newItem ); + } + }, + + expand: function( event ) { + var newItem = this.active && + this.active + .children( ".ui-menu " ) + .children( ".ui-menu-item" ) + .first(); + + if ( newItem && newItem.length ) { + this._open( newItem.parent() ); + + // Delay so Firefox will not hide activedescendant change in expanding submenu from AT + this._delay(function() { + this.focus( event, newItem ); + }); + } + }, + + next: function( event ) { + this._move( "next", "first", event ); + }, + + previous: function( event ) { + this._move( "prev", "last", event ); + }, + + isFirstItem: function() { + return this.active && !this.active.prevAll( ".ui-menu-item" ).length; + }, + + isLastItem: function() { + return this.active && !this.active.nextAll( ".ui-menu-item" ).length; + }, + + _move: function( direction, filter, event ) { + var next; + if ( this.active ) { + if ( direction === "first" || direction === "last" ) { + next = this.active + [ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" ) + .eq( -1 ); + } else { + next = this.active + [ direction + "All" ]( ".ui-menu-item" ) + .eq( 0 ); + } + } + if ( !next || !next.length || !this.active ) { + next = this.activeMenu.children( ".ui-menu-item" )[ filter ](); + } + + this.focus( event, next ); + }, + + nextPage: function( event ) { + var item, base, height; + + if ( !this.active ) { + this.next( event ); + return; + } + if ( this.isLastItem() ) { + return; + } + if ( this._hasScroll() ) { + base = this.active.offset().top; + height = this.element.height(); + this.active.nextAll( ".ui-menu-item" ).each(function() { + item = $( this ); + return item.offset().top - base - height < 0; + }); + + this.focus( event, item ); + } else { + this.focus( event, this.activeMenu.children( ".ui-menu-item" ) + [ !this.active ? "first" : "last" ]() ); + } + }, + + previousPage: function( event ) { + var item, base, height; + if ( !this.active ) { + this.next( event ); + return; + } + if ( this.isFirstItem() ) { + return; + } + if ( this._hasScroll() ) { + base = this.active.offset().top; + height = this.element.height(); + this.active.prevAll( ".ui-menu-item" ).each(function() { + item = $( this ); + return item.offset().top - base + height > 0; + }); + + this.focus( event, item ); + } else { + this.focus( event, this.activeMenu.children( ".ui-menu-item" ).first() ); + } + }, + + _hasScroll: function() { + return this.element.outerHeight() < this.element.prop( "scrollHeight" ); + }, + + select: function( event ) { + // TODO: It should never be possible to not have an active item at this + // point, but the tests don't trigger mouseenter before click. + this.active = this.active || $( event.target ).closest( ".ui-menu-item" ); + var ui = { item: this.active }; + if ( !this.active.has( ".ui-menu" ).length ) { + this.collapseAll( event, true ); + } + this._trigger( "select", event, ui ); + } +}); + +}( jQuery )); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.mouse.js b/extensions/jui/yii/jui/assets/jquery.ui.mouse.js new file mode 100644 index 0000000..62022ce --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.mouse.js @@ -0,0 +1,169 @@ +/*! + * jQuery UI Mouse 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/mouse/ + * + * Depends: + * jquery.ui.widget.js + */ +(function( $, undefined ) { + +var mouseHandled = false; +$( document ).mouseup( function() { + mouseHandled = false; +}); + +$.widget("ui.mouse", { + version: "1.10.3", + options: { + cancel: "input,textarea,button,select,option", + distance: 1, + delay: 0 + }, + _mouseInit: function() { + var that = this; + + this.element + .bind("mousedown."+this.widgetName, function(event) { + return that._mouseDown(event); + }) + .bind("click."+this.widgetName, function(event) { + if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) { + $.removeData(event.target, that.widgetName + ".preventClickEvent"); + event.stopImmediatePropagation(); + return false; + } + }); + + this.started = false; + }, + + // TODO: make sure destroying one instance of mouse doesn't mess with + // other instances of mouse + _mouseDestroy: function() { + this.element.unbind("."+this.widgetName); + if ( this._mouseMoveDelegate ) { + $(document) + .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) + .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); + } + }, + + _mouseDown: function(event) { + // don't let more than one widget handle mouseStart + if( mouseHandled ) { return; } + + // we may have missed mouseup (out of window) + (this._mouseStarted && this._mouseUp(event)); + + this._mouseDownEvent = event; + + var that = this, + btnIsLeft = (event.which === 1), + // event.target.nodeName works around a bug in IE 8 with + // disabled inputs (#7620) + elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false); + if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) { + return true; + } + + this.mouseDelayMet = !this.options.delay; + if (!this.mouseDelayMet) { + this._mouseDelayTimer = setTimeout(function() { + that.mouseDelayMet = true; + }, this.options.delay); + } + + if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { + this._mouseStarted = (this._mouseStart(event) !== false); + if (!this._mouseStarted) { + event.preventDefault(); + return true; + } + } + + // Click event may never have fired (Gecko & Opera) + if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) { + $.removeData(event.target, this.widgetName + ".preventClickEvent"); + } + + // these delegates are required to keep context + this._mouseMoveDelegate = function(event) { + return that._mouseMove(event); + }; + this._mouseUpDelegate = function(event) { + return that._mouseUp(event); + }; + $(document) + .bind("mousemove."+this.widgetName, this._mouseMoveDelegate) + .bind("mouseup."+this.widgetName, this._mouseUpDelegate); + + event.preventDefault(); + + mouseHandled = true; + return true; + }, + + _mouseMove: function(event) { + // IE mouseup check - mouseup happened when mouse was out of window + if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) { + return this._mouseUp(event); + } + + if (this._mouseStarted) { + this._mouseDrag(event); + return event.preventDefault(); + } + + if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { + this._mouseStarted = + (this._mouseStart(this._mouseDownEvent, event) !== false); + (this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event)); + } + + return !this._mouseStarted; + }, + + _mouseUp: function(event) { + $(document) + .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) + .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); + + if (this._mouseStarted) { + this._mouseStarted = false; + + if (event.target === this._mouseDownEvent.target) { + $.data(event.target, this.widgetName + ".preventClickEvent", true); + } + + this._mouseStop(event); + } + + return false; + }, + + _mouseDistanceMet: function(event) { + return (Math.max( + Math.abs(this._mouseDownEvent.pageX - event.pageX), + Math.abs(this._mouseDownEvent.pageY - event.pageY) + ) >= this.options.distance + ); + }, + + _mouseDelayMet: function(/* event */) { + return this.mouseDelayMet; + }, + + // These are placeholder methods, to be overriden by extending plugin + _mouseStart: function(/* event */) {}, + _mouseDrag: function(/* event */) {}, + _mouseStop: function(/* event */) {}, + _mouseCapture: function(/* event */) { return true; } +}); + +})(jQuery); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.position.js b/extensions/jui/yii/jui/assets/jquery.ui.position.js new file mode 100644 index 0000000..3fcdf25 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.position.js @@ -0,0 +1,497 @@ +/*! + * jQuery UI Position 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/position/ + */ +(function( $, undefined ) { + +$.ui = $.ui || {}; + +var cachedScrollbarWidth, + max = Math.max, + abs = Math.abs, + round = Math.round, + rhorizontal = /left|center|right/, + rvertical = /top|center|bottom/, + roffset = /[\+\-]\d+(\.[\d]+)?%?/, + rposition = /^\w+/, + rpercent = /%$/, + _position = $.fn.position; + +function getOffsets( offsets, width, height ) { + return [ + parseFloat( offsets[ 0 ] ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ), + parseFloat( offsets[ 1 ] ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 ) + ]; +} + +function parseCss( element, property ) { + return parseInt( $.css( element, property ), 10 ) || 0; +} + +function getDimensions( elem ) { + var raw = elem[0]; + if ( raw.nodeType === 9 ) { + return { + width: elem.width(), + height: elem.height(), + offset: { top: 0, left: 0 } + }; + } + if ( $.isWindow( raw ) ) { + return { + width: elem.width(), + height: elem.height(), + offset: { top: elem.scrollTop(), left: elem.scrollLeft() } + }; + } + if ( raw.preventDefault ) { + return { + width: 0, + height: 0, + offset: { top: raw.pageY, left: raw.pageX } + }; + } + return { + width: elem.outerWidth(), + height: elem.outerHeight(), + offset: elem.offset() + }; +} + +$.position = { + scrollbarWidth: function() { + if ( cachedScrollbarWidth !== undefined ) { + return cachedScrollbarWidth; + } + var w1, w2, + div = $( "<div style='display:block;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>" ), + innerDiv = div.children()[0]; + + $( "body" ).append( div ); + w1 = innerDiv.offsetWidth; + div.css( "overflow", "scroll" ); + + w2 = innerDiv.offsetWidth; + + if ( w1 === w2 ) { + w2 = div[0].clientWidth; + } + + div.remove(); + + return (cachedScrollbarWidth = w1 - w2); + }, + getScrollInfo: function( within ) { + var overflowX = within.isWindow ? "" : within.element.css( "overflow-x" ), + overflowY = within.isWindow ? "" : within.element.css( "overflow-y" ), + hasOverflowX = overflowX === "scroll" || + ( overflowX === "auto" && within.width < within.element[0].scrollWidth ), + hasOverflowY = overflowY === "scroll" || + ( overflowY === "auto" && within.height < within.element[0].scrollHeight ); + return { + width: hasOverflowY ? $.position.scrollbarWidth() : 0, + height: hasOverflowX ? $.position.scrollbarWidth() : 0 + }; + }, + getWithinInfo: function( element ) { + var withinElement = $( element || window ), + isWindow = $.isWindow( withinElement[0] ); + return { + element: withinElement, + isWindow: isWindow, + offset: withinElement.offset() || { left: 0, top: 0 }, + scrollLeft: withinElement.scrollLeft(), + scrollTop: withinElement.scrollTop(), + width: isWindow ? withinElement.width() : withinElement.outerWidth(), + height: isWindow ? withinElement.height() : withinElement.outerHeight() + }; + } +}; + +$.fn.position = function( options ) { + if ( !options || !options.of ) { + return _position.apply( this, arguments ); + } + + // make a copy, we don't want to modify arguments + options = $.extend( {}, options ); + + var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions, + target = $( options.of ), + within = $.position.getWithinInfo( options.within ), + scrollInfo = $.position.getScrollInfo( within ), + collision = ( options.collision || "flip" ).split( " " ), + offsets = {}; + + dimensions = getDimensions( target ); + if ( target[0].preventDefault ) { + // force left top to allow flipping + options.at = "left top"; + } + targetWidth = dimensions.width; + targetHeight = dimensions.height; + targetOffset = dimensions.offset; + // clone to reuse original targetOffset later + basePosition = $.extend( {}, targetOffset ); + + // force my and at to have valid horizontal and vertical positions + // if a value is missing or invalid, it will be converted to center + $.each( [ "my", "at" ], function() { + var pos = ( options[ this ] || "" ).split( " " ), + horizontalOffset, + verticalOffset; + + if ( pos.length === 1) { + pos = rhorizontal.test( pos[ 0 ] ) ? + pos.concat( [ "center" ] ) : + rvertical.test( pos[ 0 ] ) ? + [ "center" ].concat( pos ) : + [ "center", "center" ]; + } + pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center"; + pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center"; + + // calculate offsets + horizontalOffset = roffset.exec( pos[ 0 ] ); + verticalOffset = roffset.exec( pos[ 1 ] ); + offsets[ this ] = [ + horizontalOffset ? horizontalOffset[ 0 ] : 0, + verticalOffset ? verticalOffset[ 0 ] : 0 + ]; + + // reduce to just the positions without the offsets + options[ this ] = [ + rposition.exec( pos[ 0 ] )[ 0 ], + rposition.exec( pos[ 1 ] )[ 0 ] + ]; + }); + + // normalize collision option + if ( collision.length === 1 ) { + collision[ 1 ] = collision[ 0 ]; + } + + if ( options.at[ 0 ] === "right" ) { + basePosition.left += targetWidth; + } else if ( options.at[ 0 ] === "center" ) { + basePosition.left += targetWidth / 2; + } + + if ( options.at[ 1 ] === "bottom" ) { + basePosition.top += targetHeight; + } else if ( options.at[ 1 ] === "center" ) { + basePosition.top += targetHeight / 2; + } + + atOffset = getOffsets( offsets.at, targetWidth, targetHeight ); + basePosition.left += atOffset[ 0 ]; + basePosition.top += atOffset[ 1 ]; + + return this.each(function() { + var collisionPosition, using, + elem = $( this ), + elemWidth = elem.outerWidth(), + elemHeight = elem.outerHeight(), + marginLeft = parseCss( this, "marginLeft" ), + marginTop = parseCss( this, "marginTop" ), + collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + scrollInfo.width, + collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + scrollInfo.height, + position = $.extend( {}, basePosition ), + myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() ); + + if ( options.my[ 0 ] === "right" ) { + position.left -= elemWidth; + } else if ( options.my[ 0 ] === "center" ) { + position.left -= elemWidth / 2; + } + + if ( options.my[ 1 ] === "bottom" ) { + position.top -= elemHeight; + } else if ( options.my[ 1 ] === "center" ) { + position.top -= elemHeight / 2; + } + + position.left += myOffset[ 0 ]; + position.top += myOffset[ 1 ]; + + // if the browser doesn't support fractions, then round for consistent results + if ( !$.support.offsetFractions ) { + position.left = round( position.left ); + position.top = round( position.top ); + } + + collisionPosition = { + marginLeft: marginLeft, + marginTop: marginTop + }; + + $.each( [ "left", "top" ], function( i, dir ) { + if ( $.ui.position[ collision[ i ] ] ) { + $.ui.position[ collision[ i ] ][ dir ]( position, { + targetWidth: targetWidth, + targetHeight: targetHeight, + elemWidth: elemWidth, + elemHeight: elemHeight, + collisionPosition: collisionPosition, + collisionWidth: collisionWidth, + collisionHeight: collisionHeight, + offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ], + my: options.my, + at: options.at, + within: within, + elem : elem + }); + } + }); + + if ( options.using ) { + // adds feedback as second argument to using callback, if present + using = function( props ) { + var left = targetOffset.left - position.left, + right = left + targetWidth - elemWidth, + top = targetOffset.top - position.top, + bottom = top + targetHeight - elemHeight, + feedback = { + target: { + element: target, + left: targetOffset.left, + top: targetOffset.top, + width: targetWidth, + height: targetHeight + }, + element: { + element: elem, + left: position.left, + top: position.top, + width: elemWidth, + height: elemHeight + }, + horizontal: right < 0 ? "left" : left > 0 ? "right" : "center", + vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle" + }; + if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) { + feedback.horizontal = "center"; + } + if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) { + feedback.vertical = "middle"; + } + if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) { + feedback.important = "horizontal"; + } else { + feedback.important = "vertical"; + } + options.using.call( this, props, feedback ); + }; + } + + elem.offset( $.extend( position, { using: using } ) ); + }); +}; + +$.ui.position = { + fit: { + left: function( position, data ) { + var within = data.within, + withinOffset = within.isWindow ? within.scrollLeft : within.offset.left, + outerWidth = within.width, + collisionPosLeft = position.left - data.collisionPosition.marginLeft, + overLeft = withinOffset - collisionPosLeft, + overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset, + newOverRight; + + // element is wider than within + if ( data.collisionWidth > outerWidth ) { + // element is initially over the left side of within + if ( overLeft > 0 && overRight <= 0 ) { + newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset; + position.left += overLeft - newOverRight; + // element is initially over right side of within + } else if ( overRight > 0 && overLeft <= 0 ) { + position.left = withinOffset; + // element is initially over both left and right sides of within + } else { + if ( overLeft > overRight ) { + position.left = withinOffset + outerWidth - data.collisionWidth; + } else { + position.left = withinOffset; + } + } + // too far left -> align with left edge + } else if ( overLeft > 0 ) { + position.left += overLeft; + // too far right -> align with right edge + } else if ( overRight > 0 ) { + position.left -= overRight; + // adjust based on position and margin + } else { + position.left = max( position.left - collisionPosLeft, position.left ); + } + }, + top: function( position, data ) { + var within = data.within, + withinOffset = within.isWindow ? within.scrollTop : within.offset.top, + outerHeight = data.within.height, + collisionPosTop = position.top - data.collisionPosition.marginTop, + overTop = withinOffset - collisionPosTop, + overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset, + newOverBottom; + + // element is taller than within + if ( data.collisionHeight > outerHeight ) { + // element is initially over the top of within + if ( overTop > 0 && overBottom <= 0 ) { + newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset; + position.top += overTop - newOverBottom; + // element is initially over bottom of within + } else if ( overBottom > 0 && overTop <= 0 ) { + position.top = withinOffset; + // element is initially over both top and bottom of within + } else { + if ( overTop > overBottom ) { + position.top = withinOffset + outerHeight - data.collisionHeight; + } else { + position.top = withinOffset; + } + } + // too far up -> align with top + } else if ( overTop > 0 ) { + position.top += overTop; + // too far down -> align with bottom edge + } else if ( overBottom > 0 ) { + position.top -= overBottom; + // adjust based on position and margin + } else { + position.top = max( position.top - collisionPosTop, position.top ); + } + } + }, + flip: { + left: function( position, data ) { + var within = data.within, + withinOffset = within.offset.left + within.scrollLeft, + outerWidth = within.width, + offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left, + collisionPosLeft = position.left - data.collisionPosition.marginLeft, + overLeft = collisionPosLeft - offsetLeft, + overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft, + myOffset = data.my[ 0 ] === "left" ? + -data.elemWidth : + data.my[ 0 ] === "right" ? + data.elemWidth : + 0, + atOffset = data.at[ 0 ] === "left" ? + data.targetWidth : + data.at[ 0 ] === "right" ? + -data.targetWidth : + 0, + offset = -2 * data.offset[ 0 ], + newOverRight, + newOverLeft; + + if ( overLeft < 0 ) { + newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset; + if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) { + position.left += myOffset + atOffset + offset; + } + } + else if ( overRight > 0 ) { + newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft; + if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) { + position.left += myOffset + atOffset + offset; + } + } + }, + top: function( position, data ) { + var within = data.within, + withinOffset = within.offset.top + within.scrollTop, + outerHeight = within.height, + offsetTop = within.isWindow ? within.scrollTop : within.offset.top, + collisionPosTop = position.top - data.collisionPosition.marginTop, + overTop = collisionPosTop - offsetTop, + overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop, + top = data.my[ 1 ] === "top", + myOffset = top ? + -data.elemHeight : + data.my[ 1 ] === "bottom" ? + data.elemHeight : + 0, + atOffset = data.at[ 1 ] === "top" ? + data.targetHeight : + data.at[ 1 ] === "bottom" ? + -data.targetHeight : + 0, + offset = -2 * data.offset[ 1 ], + newOverTop, + newOverBottom; + if ( overTop < 0 ) { + newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset; + if ( ( position.top + myOffset + atOffset + offset) > overTop && ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) ) { + position.top += myOffset + atOffset + offset; + } + } + else if ( overBottom > 0 ) { + newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop; + if ( ( position.top + myOffset + atOffset + offset) > overBottom && ( newOverTop > 0 || abs( newOverTop ) < overBottom ) ) { + position.top += myOffset + atOffset + offset; + } + } + } + }, + flipfit: { + left: function() { + $.ui.position.flip.left.apply( this, arguments ); + $.ui.position.fit.left.apply( this, arguments ); + }, + top: function() { + $.ui.position.flip.top.apply( this, arguments ); + $.ui.position.fit.top.apply( this, arguments ); + } + } +}; + +// fraction support test +(function () { + var testElement, testElementParent, testElementStyle, offsetLeft, i, + body = document.getElementsByTagName( "body" )[ 0 ], + div = document.createElement( "div" ); + + //Create a "fake body" for testing based on method used in jQuery.support + testElement = document.createElement( body ? "div" : "body" ); + testElementStyle = { + visibility: "hidden", + width: 0, + height: 0, + border: 0, + margin: 0, + background: "none" + }; + if ( body ) { + $.extend( testElementStyle, { + position: "absolute", + left: "-1000px", + top: "-1000px" + }); + } + for ( i in testElementStyle ) { + testElement.style[ i ] = testElementStyle[ i ]; + } + testElement.appendChild( div ); + testElementParent = body || document.documentElement; + testElementParent.insertBefore( testElement, testElementParent.firstChild ); + + div.style.cssText = "position: absolute; left: 10.7432222px;"; + + offsetLeft = $( div ).offset().left; + $.support.offsetFractions = offsetLeft > 10 && offsetLeft < 11; + + testElement.innerHTML = ""; + testElementParent.removeChild( testElement ); +})(); + +}( jQuery ) ); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.progressbar.js b/extensions/jui/yii/jui/assets/jquery.ui.progressbar.js new file mode 100644 index 0000000..70d716e --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.progressbar.js @@ -0,0 +1,145 @@ +/*! + * jQuery UI Progressbar 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/progressbar/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + */ +(function( $, undefined ) { + +$.widget( "ui.progressbar", { + version: "1.10.3", + options: { + max: 100, + value: 0, + + change: null, + complete: null + }, + + min: 0, + + _create: function() { + // Constrain initial value + this.oldValue = this.options.value = this._constrainedValue(); + + this.element + .addClass( "ui-progressbar ui-widget ui-widget-content ui-corner-all" ) + .attr({ + // Only set static values, aria-valuenow and aria-valuemax are + // set inside _refreshValue() + role: "progressbar", + "aria-valuemin": this.min + }); + + this.valueDiv = $( "<div class='ui-progressbar-value ui-widget-header ui-corner-left'></div>" ) + .appendTo( this.element ); + + this._refreshValue(); + }, + + _destroy: function() { + this.element + .removeClass( "ui-progressbar ui-widget ui-widget-content ui-corner-all" ) + .removeAttr( "role" ) + .removeAttr( "aria-valuemin" ) + .removeAttr( "aria-valuemax" ) + .removeAttr( "aria-valuenow" ); + + this.valueDiv.remove(); + }, + + value: function( newValue ) { + if ( newValue === undefined ) { + return this.options.value; + } + + this.options.value = this._constrainedValue( newValue ); + this._refreshValue(); + }, + + _constrainedValue: function( newValue ) { + if ( newValue === undefined ) { + newValue = this.options.value; + } + + this.indeterminate = newValue === false; + + // sanitize value + if ( typeof newValue !== "number" ) { + newValue = 0; + } + + return this.indeterminate ? false : + Math.min( this.options.max, Math.max( this.min, newValue ) ); + }, + + _setOptions: function( options ) { + // Ensure "value" option is set after other values (like max) + var value = options.value; + delete options.value; + + this._super( options ); + + this.options.value = this._constrainedValue( value ); + this._refreshValue(); + }, + + _setOption: function( key, value ) { + if ( key === "max" ) { + // Don't allow a max less than min + value = Math.max( this.min, value ); + } + + this._super( key, value ); + }, + + _percentage: function() { + return this.indeterminate ? 100 : 100 * ( this.options.value - this.min ) / ( this.options.max - this.min ); + }, + + _refreshValue: function() { + var value = this.options.value, + percentage = this._percentage(); + + this.valueDiv + .toggle( this.indeterminate || value > this.min ) + .toggleClass( "ui-corner-right", value === this.options.max ) + .width( percentage.toFixed(0) + "%" ); + + this.element.toggleClass( "ui-progressbar-indeterminate", this.indeterminate ); + + if ( this.indeterminate ) { + this.element.removeAttr( "aria-valuenow" ); + if ( !this.overlayDiv ) { + this.overlayDiv = $( "<div class='ui-progressbar-overlay'></div>" ).appendTo( this.valueDiv ); + } + } else { + this.element.attr({ + "aria-valuemax": this.options.max, + "aria-valuenow": value + }); + if ( this.overlayDiv ) { + this.overlayDiv.remove(); + this.overlayDiv = null; + } + } + + if ( this.oldValue !== value ) { + this.oldValue = value; + this._trigger( "change" ); + } + if ( value === this.options.max ) { + this._trigger( "complete" ); + } + } +}); + +})( jQuery ); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.resizable.js b/extensions/jui/yii/jui/assets/jquery.ui.resizable.js new file mode 100644 index 0000000..f007732 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.resizable.js @@ -0,0 +1,968 @@ +/*! + * jQuery UI Resizable 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/resizable/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.mouse.js + * jquery.ui.widget.js + */ +(function( $, undefined ) { + +function num(v) { + return parseInt(v, 10) || 0; +} + +function isNumber(value) { + return !isNaN(parseInt(value, 10)); +} + +$.widget("ui.resizable", $.ui.mouse, { + version: "1.10.3", + widgetEventPrefix: "resize", + options: { + alsoResize: false, + animate: false, + animateDuration: "slow", + animateEasing: "swing", + aspectRatio: false, + autoHide: false, + containment: false, + ghost: false, + grid: false, + handles: "e,s,se", + helper: false, + maxHeight: null, + maxWidth: null, + minHeight: 10, + minWidth: 10, + // See #7960 + zIndex: 90, + + // callbacks + resize: null, + start: null, + stop: null + }, + _create: function() { + + var n, i, handle, axis, hname, + that = this, + o = this.options; + this.element.addClass("ui-resizable"); + + $.extend(this, { + _aspectRatio: !!(o.aspectRatio), + aspectRatio: o.aspectRatio, + originalElement: this.element, + _proportionallyResizeElements: [], + _helper: o.helper || o.ghost || o.animate ? o.helper || "ui-resizable-helper" : null + }); + + //Wrap the element if it cannot hold child nodes + if(this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)) { + + //Create a wrapper element and set the wrapper to the new current internal element + this.element.wrap( + $("<div class='ui-wrapper' style='overflow: hidden;'></div>").css({ + position: this.element.css("position"), + width: this.element.outerWidth(), + height: this.element.outerHeight(), + top: this.element.css("top"), + left: this.element.css("left") + }) + ); + + //Overwrite the original this.element + this.element = this.element.parent().data( + "ui-resizable", this.element.data("ui-resizable") + ); + + this.elementIsWrapper = true; + + //Move margins to the wrapper + this.element.css({ marginLeft: this.originalElement.css("marginLeft"), marginTop: this.originalElement.css("marginTop"), marginRight: this.originalElement.css("marginRight"), marginBottom: this.originalElement.css("marginBottom") }); + this.originalElement.css({ marginLeft: 0, marginTop: 0, marginRight: 0, marginBottom: 0}); + + //Prevent Safari textarea resize + this.originalResizeStyle = this.originalElement.css("resize"); + this.originalElement.css("resize", "none"); + + //Push the actual element to our proportionallyResize internal array + this._proportionallyResizeElements.push(this.originalElement.css({ position: "static", zoom: 1, display: "block" })); + + // avoid IE jump (hard set the margin) + this.originalElement.css({ margin: this.originalElement.css("margin") }); + + // fix handlers offset + this._proportionallyResize(); + + } + + this.handles = o.handles || (!$(".ui-resizable-handle", this.element).length ? "e,s,se" : { n: ".ui-resizable-n", e: ".ui-resizable-e", s: ".ui-resizable-s", w: ".ui-resizable-w", se: ".ui-resizable-se", sw: ".ui-resizable-sw", ne: ".ui-resizable-ne", nw: ".ui-resizable-nw" }); + if(this.handles.constructor === String) { + + if ( this.handles === "all") { + this.handles = "n,e,s,w,se,sw,ne,nw"; + } + + n = this.handles.split(","); + this.handles = {}; + + for(i = 0; i < n.length; i++) { + + handle = $.trim(n[i]); + hname = "ui-resizable-"+handle; + axis = $("<div class='ui-resizable-handle " + hname + "'></div>"); + + // Apply zIndex to all handles - see #7960 + axis.css({ zIndex: o.zIndex }); + + //TODO : What's going on here? + if ("se" === handle) { + axis.addClass("ui-icon ui-icon-gripsmall-diagonal-se"); + } + + //Insert into internal handles object and append to element + this.handles[handle] = ".ui-resizable-"+handle; + this.element.append(axis); + } + + } + + this._renderAxis = function(target) { + + var i, axis, padPos, padWrapper; + + target = target || this.element; + + for(i in this.handles) { + + if(this.handles[i].constructor === String) { + this.handles[i] = $(this.handles[i], this.element).show(); + } + + //Apply pad to wrapper element, needed to fix axis position (textarea, inputs, scrolls) + if (this.elementIsWrapper && this.originalElement[0].nodeName.match(/textarea|input|select|button/i)) { + + axis = $(this.handles[i], this.element); + + //Checking the correct pad and border + padWrapper = /sw|ne|nw|se|n|s/.test(i) ? axis.outerHeight() : axis.outerWidth(); + + //The padding type i have to apply... + padPos = [ "padding", + /ne|nw|n/.test(i) ? "Top" : + /se|sw|s/.test(i) ? "Bottom" : + /^e$/.test(i) ? "Right" : "Left" ].join(""); + + target.css(padPos, padWrapper); + + this._proportionallyResize(); + + } + + //TODO: What's that good for? There's not anything to be executed left + if(!$(this.handles[i]).length) { + continue; + } + } + }; + + //TODO: make renderAxis a prototype function + this._renderAxis(this.element); + + this._handles = $(".ui-resizable-handle", this.element) + .disableSelection(); + + //Matching axis name + this._handles.mouseover(function() { + if (!that.resizing) { + if (this.className) { + axis = this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i); + } + //Axis, default = se + that.axis = axis && axis[1] ? axis[1] : "se"; + } + }); + + //If we want to auto hide the elements + if (o.autoHide) { + this._handles.hide(); + $(this.element) + .addClass("ui-resizable-autohide") + .mouseenter(function() { + if (o.disabled) { + return; + } + $(this).removeClass("ui-resizable-autohide"); + that._handles.show(); + }) + .mouseleave(function(){ + if (o.disabled) { + return; + } + if (!that.resizing) { + $(this).addClass("ui-resizable-autohide"); + that._handles.hide(); + } + }); + } + + //Initialize the mouse interaction + this._mouseInit(); + + }, + + _destroy: function() { + + this._mouseDestroy(); + + var wrapper, + _destroy = function(exp) { + $(exp).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing") + .removeData("resizable").removeData("ui-resizable").unbind(".resizable").find(".ui-resizable-handle").remove(); + }; + + //TODO: Unwrap at same DOM position + if (this.elementIsWrapper) { + _destroy(this.element); + wrapper = this.element; + this.originalElement.css({ + position: wrapper.css("position"), + width: wrapper.outerWidth(), + height: wrapper.outerHeight(), + top: wrapper.css("top"), + left: wrapper.css("left") + }).insertAfter( wrapper ); + wrapper.remove(); + } + + this.originalElement.css("resize", this.originalResizeStyle); + _destroy(this.originalElement); + + return this; + }, + + _mouseCapture: function(event) { + var i, handle, + capture = false; + + for (i in this.handles) { + handle = $(this.handles[i])[0]; + if (handle === event.target || $.contains(handle, event.target)) { + capture = true; + } + } + + return !this.options.disabled && capture; + }, + + _mouseStart: function(event) { + + var curleft, curtop, cursor, + o = this.options, + iniPos = this.element.position(), + el = this.element; + + this.resizing = true; + + // bugfix for http://dev.jquery.com/ticket/1749 + if ( (/absolute/).test( el.css("position") ) ) { + el.css({ position: "absolute", top: el.css("top"), left: el.css("left") }); + } else if (el.is(".ui-draggable")) { + el.css({ position: "absolute", top: iniPos.top, left: iniPos.left }); + } + + this._renderProxy(); + + curleft = num(this.helper.css("left")); + curtop = num(this.helper.css("top")); + + if (o.containment) { + curleft += $(o.containment).scrollLeft() || 0; + curtop += $(o.containment).scrollTop() || 0; + } + + //Store needed variables + this.offset = this.helper.offset(); + this.position = { left: curleft, top: curtop }; + this.size = this._helper ? { width: el.outerWidth(), height: el.outerHeight() } : { width: el.width(), height: el.height() }; + this.originalSize = this._helper ? { width: el.outerWidth(), height: el.outerHeight() } : { width: el.width(), height: el.height() }; + this.originalPosition = { left: curleft, top: curtop }; + this.sizeDiff = { width: el.outerWidth() - el.width(), height: el.outerHeight() - el.height() }; + this.originalMousePosition = { left: event.pageX, top: event.pageY }; + + //Aspect Ratio + this.aspectRatio = (typeof o.aspectRatio === "number") ? o.aspectRatio : ((this.originalSize.width / this.originalSize.height) || 1); + + cursor = $(".ui-resizable-" + this.axis).css("cursor"); + $("body").css("cursor", cursor === "auto" ? this.axis + "-resize" : cursor); + + el.addClass("ui-resizable-resizing"); + this._propagate("start", event); + return true; + }, + + _mouseDrag: function(event) { + + //Increase performance, avoid regex + var data, + el = this.helper, props = {}, + smp = this.originalMousePosition, + a = this.axis, + prevTop = this.position.top, + prevLeft = this.position.left, + prevWidth = this.size.width, + prevHeight = this.size.height, + dx = (event.pageX-smp.left)||0, + dy = (event.pageY-smp.top)||0, + trigger = this._change[a]; + + if (!trigger) { + return false; + } + + // Calculate the attrs that will be change + data = trigger.apply(this, [event, dx, dy]); + + // Put this in the mouseDrag handler since the user can start pressing shift while resizing + this._updateVirtualBoundaries(event.shiftKey); + if (this._aspectRatio || event.shiftKey) { + data = this._updateRatio(data, event); + } + + data = this._respectSize(data, event); + + this._updateCache(data); + + // plugins callbacks need to be called first + this._propagate("resize", event); + + if (this.position.top !== prevTop) { + props.top = this.position.top + "px"; + } + if (this.position.left !== prevLeft) { + props.left = this.position.left + "px"; + } + if (this.size.width !== prevWidth) { + props.width = this.size.width + "px"; + } + if (this.size.height !== prevHeight) { + props.height = this.size.height + "px"; + } + el.css(props); + + if (!this._helper && this._proportionallyResizeElements.length) { + this._proportionallyResize(); + } + + // Call the user callback if the element was resized + if ( ! $.isEmptyObject(props) ) { + this._trigger("resize", event, this.ui()); + } + + return false; + }, + + _mouseStop: function(event) { + + this.resizing = false; + var pr, ista, soffseth, soffsetw, s, left, top, + o = this.options, that = this; + + if(this._helper) { + + pr = this._proportionallyResizeElements; + ista = pr.length && (/textarea/i).test(pr[0].nodeName); + soffseth = ista && $.ui.hasScroll(pr[0], "left") /* TODO - jump height */ ? 0 : that.sizeDiff.height; + soffsetw = ista ? 0 : that.sizeDiff.width; + + s = { width: (that.helper.width() - soffsetw), height: (that.helper.height() - soffseth) }; + left = (parseInt(that.element.css("left"), 10) + (that.position.left - that.originalPosition.left)) || null; + top = (parseInt(that.element.css("top"), 10) + (that.position.top - that.originalPosition.top)) || null; + + if (!o.animate) { + this.element.css($.extend(s, { top: top, left: left })); + } + + that.helper.height(that.size.height); + that.helper.width(that.size.width); + + if (this._helper && !o.animate) { + this._proportionallyResize(); + } + } + + $("body").css("cursor", "auto"); + + this.element.removeClass("ui-resizable-resizing"); + + this._propagate("stop", event); + + if (this._helper) { + this.helper.remove(); + } + + return false; + + }, + + _updateVirtualBoundaries: function(forceAspectRatio) { + var pMinWidth, pMaxWidth, pMinHeight, pMaxHeight, b, + o = this.options; + + b = { + minWidth: isNumber(o.minWidth) ? o.minWidth : 0, + maxWidth: isNumber(o.maxWidth) ? o.maxWidth : Infinity, + minHeight: isNumber(o.minHeight) ? o.minHeight : 0, + maxHeight: isNumber(o.maxHeight) ? o.maxHeight : Infinity + }; + + if(this._aspectRatio || forceAspectRatio) { + // We want to create an enclosing box whose aspect ration is the requested one + // First, compute the "projected" size for each dimension based on the aspect ratio and other dimension + pMinWidth = b.minHeight * this.aspectRatio; + pMinHeight = b.minWidth / this.aspectRatio; + pMaxWidth = b.maxHeight * this.aspectRatio; + pMaxHeight = b.maxWidth / this.aspectRatio; + + if(pMinWidth > b.minWidth) { + b.minWidth = pMinWidth; + } + if(pMinHeight > b.minHeight) { + b.minHeight = pMinHeight; + } + if(pMaxWidth < b.maxWidth) { + b.maxWidth = pMaxWidth; + } + if(pMaxHeight < b.maxHeight) { + b.maxHeight = pMaxHeight; + } + } + this._vBoundaries = b; + }, + + _updateCache: function(data) { + this.offset = this.helper.offset(); + if (isNumber(data.left)) { + this.position.left = data.left; + } + if (isNumber(data.top)) { + this.position.top = data.top; + } + if (isNumber(data.height)) { + this.size.height = data.height; + } + if (isNumber(data.width)) { + this.size.width = data.width; + } + }, + + _updateRatio: function( data ) { + + var cpos = this.position, + csize = this.size, + a = this.axis; + + if (isNumber(data.height)) { + data.width = (data.height * this.aspectRatio); + } else if (isNumber(data.width)) { + data.height = (data.width / this.aspectRatio); + } + + if (a === "sw") { + data.left = cpos.left + (csize.width - data.width); + data.top = null; + } + if (a === "nw") { + data.top = cpos.top + (csize.height - data.height); + data.left = cpos.left + (csize.width - data.width); + } + + return data; + }, + + _respectSize: function( data ) { + + var o = this._vBoundaries, + a = this.axis, + ismaxw = isNumber(data.width) && o.maxWidth && (o.maxWidth < data.width), ismaxh = isNumber(data.height) && o.maxHeight && (o.maxHeight < data.height), + isminw = isNumber(data.width) && o.minWidth && (o.minWidth > data.width), isminh = isNumber(data.height) && o.minHeight && (o.minHeight > data.height), + dw = this.originalPosition.left + this.originalSize.width, + dh = this.position.top + this.size.height, + cw = /sw|nw|w/.test(a), ch = /nw|ne|n/.test(a); + if (isminw) { + data.width = o.minWidth; + } + if (isminh) { + data.height = o.minHeight; + } + if (ismaxw) { + data.width = o.maxWidth; + } + if (ismaxh) { + data.height = o.maxHeight; + } + + if (isminw && cw) { + data.left = dw - o.minWidth; + } + if (ismaxw && cw) { + data.left = dw - o.maxWidth; + } + if (isminh && ch) { + data.top = dh - o.minHeight; + } + if (ismaxh && ch) { + data.top = dh - o.maxHeight; + } + + // fixing jump error on top/left - bug #2330 + if (!data.width && !data.height && !data.left && data.top) { + data.top = null; + } else if (!data.width && !data.height && !data.top && data.left) { + data.left = null; + } + + return data; + }, + + _proportionallyResize: function() { + + if (!this._proportionallyResizeElements.length) { + return; + } + + var i, j, borders, paddings, prel, + element = this.helper || this.element; + + for ( i=0; i < this._proportionallyResizeElements.length; i++) { + + prel = this._proportionallyResizeElements[i]; + + if (!this.borderDif) { + this.borderDif = []; + borders = [prel.css("borderTopWidth"), prel.css("borderRightWidth"), prel.css("borderBottomWidth"), prel.css("borderLeftWidth")]; + paddings = [prel.css("paddingTop"), prel.css("paddingRight"), prel.css("paddingBottom"), prel.css("paddingLeft")]; + + for ( j = 0; j < borders.length; j++ ) { + this.borderDif[ j ] = ( parseInt( borders[ j ], 10 ) || 0 ) + ( parseInt( paddings[ j ], 10 ) || 0 ); + } + } + + prel.css({ + height: (element.height() - this.borderDif[0] - this.borderDif[2]) || 0, + width: (element.width() - this.borderDif[1] - this.borderDif[3]) || 0 + }); + + } + + }, + + _renderProxy: function() { + + var el = this.element, o = this.options; + this.elementOffset = el.offset(); + + if(this._helper) { + + this.helper = this.helper || $("<div style='overflow:hidden;'></div>"); + + this.helper.addClass(this._helper).css({ + width: this.element.outerWidth() - 1, + height: this.element.outerHeight() - 1, + position: "absolute", + left: this.elementOffset.left +"px", + top: this.elementOffset.top +"px", + zIndex: ++o.zIndex //TODO: Don't modify option + }); + + this.helper + .appendTo("body") + .disableSelection(); + + } else { + this.helper = this.element; + } + + }, + + _change: { + e: function(event, dx) { + return { width: this.originalSize.width + dx }; + }, + w: function(event, dx) { + var cs = this.originalSize, sp = this.originalPosition; + return { left: sp.left + dx, width: cs.width - dx }; + }, + n: function(event, dx, dy) { + var cs = this.originalSize, sp = this.originalPosition; + return { top: sp.top + dy, height: cs.height - dy }; + }, + s: function(event, dx, dy) { + return { height: this.originalSize.height + dy }; + }, + se: function(event, dx, dy) { + return $.extend(this._change.s.apply(this, arguments), this._change.e.apply(this, [event, dx, dy])); + }, + sw: function(event, dx, dy) { + return $.extend(this._change.s.apply(this, arguments), this._change.w.apply(this, [event, dx, dy])); + }, + ne: function(event, dx, dy) { + return $.extend(this._change.n.apply(this, arguments), this._change.e.apply(this, [event, dx, dy])); + }, + nw: function(event, dx, dy) { + return $.extend(this._change.n.apply(this, arguments), this._change.w.apply(this, [event, dx, dy])); + } + }, + + _propagate: function(n, event) { + $.ui.plugin.call(this, n, [event, this.ui()]); + (n !== "resize" && this._trigger(n, event, this.ui())); + }, + + plugins: {}, + + ui: function() { + return { + originalElement: this.originalElement, + element: this.element, + helper: this.helper, + position: this.position, + size: this.size, + originalSize: this.originalSize, + originalPosition: this.originalPosition + }; + } + +}); + +/* + * Resizable Extensions + */ + +$.ui.plugin.add("resizable", "animate", { + + stop: function( event ) { + var that = $(this).data("ui-resizable"), + o = that.options, + pr = that._proportionallyResizeElements, + ista = pr.length && (/textarea/i).test(pr[0].nodeName), + soffseth = ista && $.ui.hasScroll(pr[0], "left") /* TODO - jump height */ ? 0 : that.sizeDiff.height, + soffsetw = ista ? 0 : that.sizeDiff.width, + style = { width: (that.size.width - soffsetw), height: (that.size.height - soffseth) }, + left = (parseInt(that.element.css("left"), 10) + (that.position.left - that.originalPosition.left)) || null, + top = (parseInt(that.element.css("top"), 10) + (that.position.top - that.originalPosition.top)) || null; + + that.element.animate( + $.extend(style, top && left ? { top: top, left: left } : {}), { + duration: o.animateDuration, + easing: o.animateEasing, + step: function() { + + var data = { + width: parseInt(that.element.css("width"), 10), + height: parseInt(that.element.css("height"), 10), + top: parseInt(that.element.css("top"), 10), + left: parseInt(that.element.css("left"), 10) + }; + + if (pr && pr.length) { + $(pr[0]).css({ width: data.width, height: data.height }); + } + + // propagating resize, and updating values for each animation step + that._updateCache(data); + that._propagate("resize", event); + + } + } + ); + } + +}); + +$.ui.plugin.add("resizable", "containment", { + + start: function() { + var element, p, co, ch, cw, width, height, + that = $(this).data("ui-resizable"), + o = that.options, + el = that.element, + oc = o.containment, + ce = (oc instanceof $) ? oc.get(0) : (/parent/.test(oc)) ? el.parent().get(0) : oc; + + if (!ce) { + return; + } + + that.containerElement = $(ce); + + if (/document/.test(oc) || oc === document) { + that.containerOffset = { left: 0, top: 0 }; + that.containerPosition = { left: 0, top: 0 }; + + that.parentData = { + element: $(document), left: 0, top: 0, + width: $(document).width(), height: $(document).height() || document.body.parentNode.scrollHeight + }; + } + + // i'm a node, so compute top, left, right, bottom + else { + element = $(ce); + p = []; + $([ "Top", "Right", "Left", "Bottom" ]).each(function(i, name) { p[i] = num(element.css("padding" + name)); }); + + that.containerOffset = element.offset(); + that.containerPosition = element.position(); + that.containerSize = { height: (element.innerHeight() - p[3]), width: (element.innerWidth() - p[1]) }; + + co = that.containerOffset; + ch = that.containerSize.height; + cw = that.containerSize.width; + width = ($.ui.hasScroll(ce, "left") ? ce.scrollWidth : cw ); + height = ($.ui.hasScroll(ce) ? ce.scrollHeight : ch); + + that.parentData = { + element: ce, left: co.left, top: co.top, width: width, height: height + }; + } + }, + + resize: function( event ) { + var woset, hoset, isParent, isOffsetRelative, + that = $(this).data("ui-resizable"), + o = that.options, + co = that.containerOffset, cp = that.position, + pRatio = that._aspectRatio || event.shiftKey, + cop = { top:0, left:0 }, ce = that.containerElement; + + if (ce[0] !== document && (/static/).test(ce.css("position"))) { + cop = co; + } + + if (cp.left < (that._helper ? co.left : 0)) { + that.size.width = that.size.width + (that._helper ? (that.position.left - co.left) : (that.position.left - cop.left)); + if (pRatio) { + that.size.height = that.size.width / that.aspectRatio; + } + that.position.left = o.helper ? co.left : 0; + } + + if (cp.top < (that._helper ? co.top : 0)) { + that.size.height = that.size.height + (that._helper ? (that.position.top - co.top) : that.position.top); + if (pRatio) { + that.size.width = that.size.height * that.aspectRatio; + } + that.position.top = that._helper ? co.top : 0; + } + + that.offset.left = that.parentData.left+that.position.left; + that.offset.top = that.parentData.top+that.position.top; + + woset = Math.abs( (that._helper ? that.offset.left - cop.left : (that.offset.left - cop.left)) + that.sizeDiff.width ); + hoset = Math.abs( (that._helper ? that.offset.top - cop.top : (that.offset.top - co.top)) + that.sizeDiff.height ); + + isParent = that.containerElement.get(0) === that.element.parent().get(0); + isOffsetRelative = /relative|absolute/.test(that.containerElement.css("position")); + + if(isParent && isOffsetRelative) { + woset -= that.parentData.left; + } + + if (woset + that.size.width >= that.parentData.width) { + that.size.width = that.parentData.width - woset; + if (pRatio) { + that.size.height = that.size.width / that.aspectRatio; + } + } + + if (hoset + that.size.height >= that.parentData.height) { + that.size.height = that.parentData.height - hoset; + if (pRatio) { + that.size.width = that.size.height * that.aspectRatio; + } + } + }, + + stop: function(){ + var that = $(this).data("ui-resizable"), + o = that.options, + co = that.containerOffset, + cop = that.containerPosition, + ce = that.containerElement, + helper = $(that.helper), + ho = helper.offset(), + w = helper.outerWidth() - that.sizeDiff.width, + h = helper.outerHeight() - that.sizeDiff.height; + + if (that._helper && !o.animate && (/relative/).test(ce.css("position"))) { + $(this).css({ left: ho.left - cop.left - co.left, width: w, height: h }); + } + + if (that._helper && !o.animate && (/static/).test(ce.css("position"))) { + $(this).css({ left: ho.left - cop.left - co.left, width: w, height: h }); + } + + } +}); + +$.ui.plugin.add("resizable", "alsoResize", { + + start: function () { + var that = $(this).data("ui-resizable"), + o = that.options, + _store = function (exp) { + $(exp).each(function() { + var el = $(this); + el.data("ui-resizable-alsoresize", { + width: parseInt(el.width(), 10), height: parseInt(el.height(), 10), + left: parseInt(el.css("left"), 10), top: parseInt(el.css("top"), 10) + }); + }); + }; + + if (typeof(o.alsoResize) === "object" && !o.alsoResize.parentNode) { + if (o.alsoResize.length) { o.alsoResize = o.alsoResize[0]; _store(o.alsoResize); } + else { $.each(o.alsoResize, function (exp) { _store(exp); }); } + }else{ + _store(o.alsoResize); + } + }, + + resize: function (event, ui) { + var that = $(this).data("ui-resizable"), + o = that.options, + os = that.originalSize, + op = that.originalPosition, + delta = { + height: (that.size.height - os.height) || 0, width: (that.size.width - os.width) || 0, + top: (that.position.top - op.top) || 0, left: (that.position.left - op.left) || 0 + }, + + _alsoResize = function (exp, c) { + $(exp).each(function() { + var el = $(this), start = $(this).data("ui-resizable-alsoresize"), style = {}, + css = c && c.length ? c : el.parents(ui.originalElement[0]).length ? ["width", "height"] : ["width", "height", "top", "left"]; + + $.each(css, function (i, prop) { + var sum = (start[prop]||0) + (delta[prop]||0); + if (sum && sum >= 0) { + style[prop] = sum || null; + } + }); + + el.css(style); + }); + }; + + if (typeof(o.alsoResize) === "object" && !o.alsoResize.nodeType) { + $.each(o.alsoResize, function (exp, c) { _alsoResize(exp, c); }); + }else{ + _alsoResize(o.alsoResize); + } + }, + + stop: function () { + $(this).removeData("resizable-alsoresize"); + } +}); + +$.ui.plugin.add("resizable", "ghost", { + + start: function() { + + var that = $(this).data("ui-resizable"), o = that.options, cs = that.size; + + that.ghost = that.originalElement.clone(); + that.ghost + .css({ opacity: 0.25, display: "block", position: "relative", height: cs.height, width: cs.width, margin: 0, left: 0, top: 0 }) + .addClass("ui-resizable-ghost") + .addClass(typeof o.ghost === "string" ? o.ghost : ""); + + that.ghost.appendTo(that.helper); + + }, + + resize: function(){ + var that = $(this).data("ui-resizable"); + if (that.ghost) { + that.ghost.css({ position: "relative", height: that.size.height, width: that.size.width }); + } + }, + + stop: function() { + var that = $(this).data("ui-resizable"); + if (that.ghost && that.helper) { + that.helper.get(0).removeChild(that.ghost.get(0)); + } + } + +}); + +$.ui.plugin.add("resizable", "grid", { + + resize: function() { + var that = $(this).data("ui-resizable"), + o = that.options, + cs = that.size, + os = that.originalSize, + op = that.originalPosition, + a = that.axis, + grid = typeof o.grid === "number" ? [o.grid, o.grid] : o.grid, + gridX = (grid[0]||1), + gridY = (grid[1]||1), + ox = Math.round((cs.width - os.width) / gridX) * gridX, + oy = Math.round((cs.height - os.height) / gridY) * gridY, + newWidth = os.width + ox, + newHeight = os.height + oy, + isMaxWidth = o.maxWidth && (o.maxWidth < newWidth), + isMaxHeight = o.maxHeight && (o.maxHeight < newHeight), + isMinWidth = o.minWidth && (o.minWidth > newWidth), + isMinHeight = o.minHeight && (o.minHeight > newHeight); + + o.grid = grid; + + if (isMinWidth) { + newWidth = newWidth + gridX; + } + if (isMinHeight) { + newHeight = newHeight + gridY; + } + if (isMaxWidth) { + newWidth = newWidth - gridX; + } + if (isMaxHeight) { + newHeight = newHeight - gridY; + } + + if (/^(se|s|e)$/.test(a)) { + that.size.width = newWidth; + that.size.height = newHeight; + } else if (/^(ne)$/.test(a)) { + that.size.width = newWidth; + that.size.height = newHeight; + that.position.top = op.top - oy; + } else if (/^(sw)$/.test(a)) { + that.size.width = newWidth; + that.size.height = newHeight; + that.position.left = op.left - ox; + } else { + that.size.width = newWidth; + that.size.height = newHeight; + that.position.top = op.top - oy; + that.position.left = op.left - ox; + } + } + +}); + +})(jQuery); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.selectable.js b/extensions/jui/yii/jui/assets/jquery.ui.selectable.js new file mode 100644 index 0000000..e99ecb6 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.selectable.js @@ -0,0 +1,277 @@ +/*! + * jQuery UI Selectable 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/selectable/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.mouse.js + * jquery.ui.widget.js + */ +(function( $, undefined ) { + +$.widget("ui.selectable", $.ui.mouse, { + version: "1.10.3", + options: { + appendTo: "body", + autoRefresh: true, + distance: 0, + filter: "*", + tolerance: "touch", + + // callbacks + selected: null, + selecting: null, + start: null, + stop: null, + unselected: null, + unselecting: null + }, + _create: function() { + var selectees, + that = this; + + this.element.addClass("ui-selectable"); + + this.dragged = false; + + // cache selectee children based on filter + this.refresh = function() { + selectees = $(that.options.filter, that.element[0]); + selectees.addClass("ui-selectee"); + selectees.each(function() { + var $this = $(this), + pos = $this.offset(); + $.data(this, "selectable-item", { + element: this, + $element: $this, + left: pos.left, + top: pos.top, + right: pos.left + $this.outerWidth(), + bottom: pos.top + $this.outerHeight(), + startselected: false, + selected: $this.hasClass("ui-selected"), + selecting: $this.hasClass("ui-selecting"), + unselecting: $this.hasClass("ui-unselecting") + }); + }); + }; + this.refresh(); + + this.selectees = selectees.addClass("ui-selectee"); + + this._mouseInit(); + + this.helper = $("<div class='ui-selectable-helper'></div>"); + }, + + _destroy: function() { + this.selectees + .removeClass("ui-selectee") + .removeData("selectable-item"); + this.element + .removeClass("ui-selectable ui-selectable-disabled"); + this._mouseDestroy(); + }, + + _mouseStart: function(event) { + var that = this, + options = this.options; + + this.opos = [event.pageX, event.pageY]; + + if (this.options.disabled) { + return; + } + + this.selectees = $(options.filter, this.element[0]); + + this._trigger("start", event); + + $(options.appendTo).append(this.helper); + // position helper (lasso) + this.helper.css({ + "left": event.pageX, + "top": event.pageY, + "width": 0, + "height": 0 + }); + + if (options.autoRefresh) { + this.refresh(); + } + + this.selectees.filter(".ui-selected").each(function() { + var selectee = $.data(this, "selectable-item"); + selectee.startselected = true; + if (!event.metaKey && !event.ctrlKey) { + selectee.$element.removeClass("ui-selected"); + selectee.selected = false; + selectee.$element.addClass("ui-unselecting"); + selectee.unselecting = true; + // selectable UNSELECTING callback + that._trigger("unselecting", event, { + unselecting: selectee.element + }); + } + }); + + $(event.target).parents().addBack().each(function() { + var doSelect, + selectee = $.data(this, "selectable-item"); + if (selectee) { + doSelect = (!event.metaKey && !event.ctrlKey) || !selectee.$element.hasClass("ui-selected"); + selectee.$element + .removeClass(doSelect ? "ui-unselecting" : "ui-selected") + .addClass(doSelect ? "ui-selecting" : "ui-unselecting"); + selectee.unselecting = !doSelect; + selectee.selecting = doSelect; + selectee.selected = doSelect; + // selectable (UN)SELECTING callback + if (doSelect) { + that._trigger("selecting", event, { + selecting: selectee.element + }); + } else { + that._trigger("unselecting", event, { + unselecting: selectee.element + }); + } + return false; + } + }); + + }, + + _mouseDrag: function(event) { + + this.dragged = true; + + if (this.options.disabled) { + return; + } + + var tmp, + that = this, + options = this.options, + x1 = this.opos[0], + y1 = this.opos[1], + x2 = event.pageX, + y2 = event.pageY; + + if (x1 > x2) { tmp = x2; x2 = x1; x1 = tmp; } + if (y1 > y2) { tmp = y2; y2 = y1; y1 = tmp; } + this.helper.css({left: x1, top: y1, width: x2-x1, height: y2-y1}); + + this.selectees.each(function() { + var selectee = $.data(this, "selectable-item"), + hit = false; + + //prevent helper from being selected if appendTo: selectable + if (!selectee || selectee.element === that.element[0]) { + return; + } + + if (options.tolerance === "touch") { + hit = ( !(selectee.left > x2 || selectee.right < x1 || selectee.top > y2 || selectee.bottom < y1) ); + } else if (options.tolerance === "fit") { + hit = (selectee.left > x1 && selectee.right < x2 && selectee.top > y1 && selectee.bottom < y2); + } + + if (hit) { + // SELECT + if (selectee.selected) { + selectee.$element.removeClass("ui-selected"); + selectee.selected = false; + } + if (selectee.unselecting) { + selectee.$element.removeClass("ui-unselecting"); + selectee.unselecting = false; + } + if (!selectee.selecting) { + selectee.$element.addClass("ui-selecting"); + selectee.selecting = true; + // selectable SELECTING callback + that._trigger("selecting", event, { + selecting: selectee.element + }); + } + } else { + // UNSELECT + if (selectee.selecting) { + if ((event.metaKey || event.ctrlKey) && selectee.startselected) { + selectee.$element.removeClass("ui-selecting"); + selectee.selecting = false; + selectee.$element.addClass("ui-selected"); + selectee.selected = true; + } else { + selectee.$element.removeClass("ui-selecting"); + selectee.selecting = false; + if (selectee.startselected) { + selectee.$element.addClass("ui-unselecting"); + selectee.unselecting = true; + } + // selectable UNSELECTING callback + that._trigger("unselecting", event, { + unselecting: selectee.element + }); + } + } + if (selectee.selected) { + if (!event.metaKey && !event.ctrlKey && !selectee.startselected) { + selectee.$element.removeClass("ui-selected"); + selectee.selected = false; + + selectee.$element.addClass("ui-unselecting"); + selectee.unselecting = true; + // selectable UNSELECTING callback + that._trigger("unselecting", event, { + unselecting: selectee.element + }); + } + } + } + }); + + return false; + }, + + _mouseStop: function(event) { + var that = this; + + this.dragged = false; + + $(".ui-unselecting", this.element[0]).each(function() { + var selectee = $.data(this, "selectable-item"); + selectee.$element.removeClass("ui-unselecting"); + selectee.unselecting = false; + selectee.startselected = false; + that._trigger("unselected", event, { + unselected: selectee.element + }); + }); + $(".ui-selecting", this.element[0]).each(function() { + var selectee = $.data(this, "selectable-item"); + selectee.$element.removeClass("ui-selecting").addClass("ui-selected"); + selectee.selecting = false; + selectee.selected = true; + selectee.startselected = true; + that._trigger("selected", event, { + selected: selectee.element + }); + }); + this._trigger("stop", event); + + this.helper.remove(); + + return false; + } + +}); + +})(jQuery); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.slider.js b/extensions/jui/yii/jui/assets/jquery.ui.slider.js new file mode 100644 index 0000000..b2c4aa3 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.slider.js @@ -0,0 +1,672 @@ +/*! + * jQuery UI Slider 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/slider/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.mouse.js + * jquery.ui.widget.js + */ +(function( $, undefined ) { + +// number of pages in a slider +// (how many times can you page up/down to go through the whole range) +var numPages = 5; + +$.widget( "ui.slider", $.ui.mouse, { + version: "1.10.3", + widgetEventPrefix: "slide", + + options: { + animate: false, + distance: 0, + max: 100, + min: 0, + orientation: "horizontal", + range: false, + step: 1, + value: 0, + values: null, + + // callbacks + change: null, + slide: null, + start: null, + stop: null + }, + + _create: function() { + this._keySliding = false; + this._mouseSliding = false; + this._animateOff = true; + this._handleIndex = null; + this._detectOrientation(); + this._mouseInit(); + + this.element + .addClass( "ui-slider" + + " ui-slider-" + this.orientation + + " ui-widget" + + " ui-widget-content" + + " ui-corner-all"); + + this._refresh(); + this._setOption( "disabled", this.options.disabled ); + + this._animateOff = false; + }, + + _refresh: function() { + this._createRange(); + this._createHandles(); + this._setupEvents(); + this._refreshValue(); + }, + + _createHandles: function() { + var i, handleCount, + options = this.options, + existingHandles = this.element.find( ".ui-slider-handle" ).addClass( "ui-state-default ui-corner-all" ), + handle = "<a class='ui-slider-handle ui-state-default ui-corner-all' href='#'></a>", + handles = []; + + handleCount = ( options.values && options.values.length ) || 1; + + if ( existingHandles.length > handleCount ) { + existingHandles.slice( handleCount ).remove(); + existingHandles = existingHandles.slice( 0, handleCount ); + } + + for ( i = existingHandles.length; i < handleCount; i++ ) { + handles.push( handle ); + } + + this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( this.element ) ); + + this.handle = this.handles.eq( 0 ); + + this.handles.each(function( i ) { + $( this ).data( "ui-slider-handle-index", i ); + }); + }, + + _createRange: function() { + var options = this.options, + classes = ""; + + if ( options.range ) { + if ( options.range === true ) { + if ( !options.values ) { + options.values = [ this._valueMin(), this._valueMin() ]; + } else if ( options.values.length && options.values.length !== 2 ) { + options.values = [ options.values[0], options.values[0] ]; + } else if ( $.isArray( options.values ) ) { + options.values = options.values.slice(0); + } + } + + if ( !this.range || !this.range.length ) { + this.range = $( "<div></div>" ) + .appendTo( this.element ); + + classes = "ui-slider-range" + + // note: this isn't the most fittingly semantic framework class for this element, + // but worked best visually with a variety of themes + " ui-widget-header ui-corner-all"; + } else { + this.range.removeClass( "ui-slider-range-min ui-slider-range-max" ) + // Handle range switching from true to min/max + .css({ + "left": "", + "bottom": "" + }); + } + + this.range.addClass( classes + + ( ( options.range === "min" || options.range === "max" ) ? " ui-slider-range-" + options.range : "" ) ); + } else { + this.range = $([]); + } + }, + + _setupEvents: function() { + var elements = this.handles.add( this.range ).filter( "a" ); + this._off( elements ); + this._on( elements, this._handleEvents ); + this._hoverable( elements ); + this._focusable( elements ); + }, + + _destroy: function() { + this.handles.remove(); + this.range.remove(); + + this.element + .removeClass( "ui-slider" + + " ui-slider-horizontal" + + " ui-slider-vertical" + + " ui-widget" + + " ui-widget-content" + + " ui-corner-all" ); + + this._mouseDestroy(); + }, + + _mouseCapture: function( event ) { + var position, normValue, distance, closestHandle, index, allowed, offset, mouseOverHandle, + that = this, + o = this.options; + + if ( o.disabled ) { + return false; + } + + this.elementSize = { + width: this.element.outerWidth(), + height: this.element.outerHeight() + }; + this.elementOffset = this.element.offset(); + + position = { x: event.pageX, y: event.pageY }; + normValue = this._normValueFromMouse( position ); + distance = this._valueMax() - this._valueMin() + 1; + this.handles.each(function( i ) { + var thisDistance = Math.abs( normValue - that.values(i) ); + if (( distance > thisDistance ) || + ( distance === thisDistance && + (i === that._lastChangedValue || that.values(i) === o.min ))) { + distance = thisDistance; + closestHandle = $( this ); + index = i; + } + }); + + allowed = this._start( event, index ); + if ( allowed === false ) { + return false; + } + this._mouseSliding = true; + + this._handleIndex = index; + + closestHandle + .addClass( "ui-state-active" ) + .focus(); + + offset = closestHandle.offset(); + mouseOverHandle = !$( event.target ).parents().addBack().is( ".ui-slider-handle" ); + this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : { + left: event.pageX - offset.left - ( closestHandle.width() / 2 ), + top: event.pageY - offset.top - + ( closestHandle.height() / 2 ) - + ( parseInt( closestHandle.css("borderTopWidth"), 10 ) || 0 ) - + ( parseInt( closestHandle.css("borderBottomWidth"), 10 ) || 0) + + ( parseInt( closestHandle.css("marginTop"), 10 ) || 0) + }; + + if ( !this.handles.hasClass( "ui-state-hover" ) ) { + this._slide( event, index, normValue ); + } + this._animateOff = true; + return true; + }, + + _mouseStart: function() { + return true; + }, + + _mouseDrag: function( event ) { + var position = { x: event.pageX, y: event.pageY }, + normValue = this._normValueFromMouse( position ); + + this._slide( event, this._handleIndex, normValue ); + + return false; + }, + + _mouseStop: function( event ) { + this.handles.removeClass( "ui-state-active" ); + this._mouseSliding = false; + + this._stop( event, this._handleIndex ); + this._change( event, this._handleIndex ); + + this._handleIndex = null; + this._clickOffset = null; + this._animateOff = false; + + return false; + }, + + _detectOrientation: function() { + this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal"; + }, + + _normValueFromMouse: function( position ) { + var pixelTotal, + pixelMouse, + percentMouse, + valueTotal, + valueMouse; + + if ( this.orientation === "horizontal" ) { + pixelTotal = this.elementSize.width; + pixelMouse = position.x - this.elementOffset.left - ( this._clickOffset ? this._clickOffset.left : 0 ); + } else { + pixelTotal = this.elementSize.height; + pixelMouse = position.y - this.elementOffset.top - ( this._clickOffset ? this._clickOffset.top : 0 ); + } + + percentMouse = ( pixelMouse / pixelTotal ); + if ( percentMouse > 1 ) { + percentMouse = 1; + } + if ( percentMouse < 0 ) { + percentMouse = 0; + } + if ( this.orientation === "vertical" ) { + percentMouse = 1 - percentMouse; + } + + valueTotal = this._valueMax() - this._valueMin(); + valueMouse = this._valueMin() + percentMouse * valueTotal; + + return this._trimAlignValue( valueMouse ); + }, + + _start: function( event, index ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + return this._trigger( "start", event, uiHash ); + }, + + _slide: function( event, index, newVal ) { + var otherVal, + newValues, + allowed; + + if ( this.options.values && this.options.values.length ) { + otherVal = this.values( index ? 0 : 1 ); + + if ( ( this.options.values.length === 2 && this.options.range === true ) && + ( ( index === 0 && newVal > otherVal) || ( index === 1 && newVal < otherVal ) ) + ) { + newVal = otherVal; + } + + if ( newVal !== this.values( index ) ) { + newValues = this.values(); + newValues[ index ] = newVal; + // A slide can be canceled by returning false from the slide callback + allowed = this._trigger( "slide", event, { + handle: this.handles[ index ], + value: newVal, + values: newValues + } ); + otherVal = this.values( index ? 0 : 1 ); + if ( allowed !== false ) { + this.values( index, newVal, true ); + } + } + } else { + if ( newVal !== this.value() ) { + // A slide can be canceled by returning false from the slide callback + allowed = this._trigger( "slide", event, { + handle: this.handles[ index ], + value: newVal + } ); + if ( allowed !== false ) { + this.value( newVal ); + } + } + } + }, + + _stop: function( event, index ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + + this._trigger( "stop", event, uiHash ); + }, + + _change: function( event, index ) { + if ( !this._keySliding && !this._mouseSliding ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + + //store the last changed value index for reference when handles overlap + this._lastChangedValue = index; + + this._trigger( "change", event, uiHash ); + } + }, + + value: function( newValue ) { + if ( arguments.length ) { + this.options.value = this._trimAlignValue( newValue ); + this._refreshValue(); + this._change( null, 0 ); + return; + } + + return this._value(); + }, + + values: function( index, newValue ) { + var vals, + newValues, + i; + + if ( arguments.length > 1 ) { + this.options.values[ index ] = this._trimAlignValue( newValue ); + this._refreshValue(); + this._change( null, index ); + return; + } + + if ( arguments.length ) { + if ( $.isArray( arguments[ 0 ] ) ) { + vals = this.options.values; + newValues = arguments[ 0 ]; + for ( i = 0; i < vals.length; i += 1 ) { + vals[ i ] = this._trimAlignValue( newValues[ i ] ); + this._change( null, i ); + } + this._refreshValue(); + } else { + if ( this.options.values && this.options.values.length ) { + return this._values( index ); + } else { + return this.value(); + } + } + } else { + return this._values(); + } + }, + + _setOption: function( key, value ) { + var i, + valsLength = 0; + + if ( key === "range" && this.options.range === true ) { + if ( value === "min" ) { + this.options.value = this._values( 0 ); + this.options.values = null; + } else if ( value === "max" ) { + this.options.value = this._values( this.options.values.length-1 ); + this.options.values = null; + } + } + + if ( $.isArray( this.options.values ) ) { + valsLength = this.options.values.length; + } + + $.Widget.prototype._setOption.apply( this, arguments ); + + switch ( key ) { + case "orientation": + this._detectOrientation(); + this.element + .removeClass( "ui-slider-horizontal ui-slider-vertical" ) + .addClass( "ui-slider-" + this.orientation ); + this._refreshValue(); + break; + case "value": + this._animateOff = true; + this._refreshValue(); + this._change( null, 0 ); + this._animateOff = false; + break; + case "values": + this._animateOff = true; + this._refreshValue(); + for ( i = 0; i < valsLength; i += 1 ) { + this._change( null, i ); + } + this._animateOff = false; + break; + case "min": + case "max": + this._animateOff = true; + this._refreshValue(); + this._animateOff = false; + break; + case "range": + this._animateOff = true; + this._refresh(); + this._animateOff = false; + break; + } + }, + + //internal value getter + // _value() returns value trimmed by min and max, aligned by step + _value: function() { + var val = this.options.value; + val = this._trimAlignValue( val ); + + return val; + }, + + //internal values getter + // _values() returns array of values trimmed by min and max, aligned by step + // _values( index ) returns single value trimmed by min and max, aligned by step + _values: function( index ) { + var val, + vals, + i; + + if ( arguments.length ) { + val = this.options.values[ index ]; + val = this._trimAlignValue( val ); + + return val; + } else if ( this.options.values && this.options.values.length ) { + // .slice() creates a copy of the array + // this copy gets trimmed by min and max and then returned + vals = this.options.values.slice(); + for ( i = 0; i < vals.length; i+= 1) { + vals[ i ] = this._trimAlignValue( vals[ i ] ); + } + + return vals; + } else { + return []; + } + }, + + // returns the step-aligned value that val is closest to, between (inclusive) min and max + _trimAlignValue: function( val ) { + if ( val <= this._valueMin() ) { + return this._valueMin(); + } + if ( val >= this._valueMax() ) { + return this._valueMax(); + } + var step = ( this.options.step > 0 ) ? this.options.step : 1, + valModStep = (val - this._valueMin()) % step, + alignValue = val - valModStep; + + if ( Math.abs(valModStep) * 2 >= step ) { + alignValue += ( valModStep > 0 ) ? step : ( -step ); + } + + // Since JavaScript has problems with large floats, round + // the final value to 5 digits after the decimal point (see #4124) + return parseFloat( alignValue.toFixed(5) ); + }, + + _valueMin: function() { + return this.options.min; + }, + + _valueMax: function() { + return this.options.max; + }, + + _refreshValue: function() { + var lastValPercent, valPercent, value, valueMin, valueMax, + oRange = this.options.range, + o = this.options, + that = this, + animate = ( !this._animateOff ) ? o.animate : false, + _set = {}; + + if ( this.options.values && this.options.values.length ) { + this.handles.each(function( i ) { + valPercent = ( that.values(i) - that._valueMin() ) / ( that._valueMax() - that._valueMin() ) * 100; + _set[ that.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; + $( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); + if ( that.options.range === true ) { + if ( that.orientation === "horizontal" ) { + if ( i === 0 ) { + that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { left: valPercent + "%" }, o.animate ); + } + if ( i === 1 ) { + that.range[ animate ? "animate" : "css" ]( { width: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } else { + if ( i === 0 ) { + that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { bottom: ( valPercent ) + "%" }, o.animate ); + } + if ( i === 1 ) { + that.range[ animate ? "animate" : "css" ]( { height: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } + } + lastValPercent = valPercent; + }); + } else { + value = this.value(); + valueMin = this._valueMin(); + valueMax = this._valueMax(); + valPercent = ( valueMax !== valueMin ) ? + ( value - valueMin ) / ( valueMax - valueMin ) * 100 : + 0; + _set[ this.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; + this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); + + if ( oRange === "min" && this.orientation === "horizontal" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { width: valPercent + "%" }, o.animate ); + } + if ( oRange === "max" && this.orientation === "horizontal" ) { + this.range[ animate ? "animate" : "css" ]( { width: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + if ( oRange === "min" && this.orientation === "vertical" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { height: valPercent + "%" }, o.animate ); + } + if ( oRange === "max" && this.orientation === "vertical" ) { + this.range[ animate ? "animate" : "css" ]( { height: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } + }, + + _handleEvents: { + keydown: function( event ) { + /*jshint maxcomplexity:25*/ + var allowed, curVal, newVal, step, + index = $( event.target ).data( "ui-slider-handle-index" ); + + switch ( event.keyCode ) { + case $.ui.keyCode.HOME: + case $.ui.keyCode.END: + case $.ui.keyCode.PAGE_UP: + case $.ui.keyCode.PAGE_DOWN: + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + event.preventDefault(); + if ( !this._keySliding ) { + this._keySliding = true; + $( event.target ).addClass( "ui-state-active" ); + allowed = this._start( event, index ); + if ( allowed === false ) { + return; + } + } + break; + } + + step = this.options.step; + if ( this.options.values && this.options.values.length ) { + curVal = newVal = this.values( index ); + } else { + curVal = newVal = this.value(); + } + + switch ( event.keyCode ) { + case $.ui.keyCode.HOME: + newVal = this._valueMin(); + break; + case $.ui.keyCode.END: + newVal = this._valueMax(); + break; + case $.ui.keyCode.PAGE_UP: + newVal = this._trimAlignValue( curVal + ( (this._valueMax() - this._valueMin()) / numPages ) ); + break; + case $.ui.keyCode.PAGE_DOWN: + newVal = this._trimAlignValue( curVal - ( (this._valueMax() - this._valueMin()) / numPages ) ); + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + if ( curVal === this._valueMax() ) { + return; + } + newVal = this._trimAlignValue( curVal + step ); + break; + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + if ( curVal === this._valueMin() ) { + return; + } + newVal = this._trimAlignValue( curVal - step ); + break; + } + + this._slide( event, index, newVal ); + }, + click: function( event ) { + event.preventDefault(); + }, + keyup: function( event ) { + var index = $( event.target ).data( "ui-slider-handle-index" ); + + if ( this._keySliding ) { + this._keySliding = false; + this._stop( event, index ); + this._change( event, index ); + $( event.target ).removeClass( "ui-state-active" ); + } + } + } + +}); + +}(jQuery)); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.sortable.js b/extensions/jui/yii/jui/assets/jquery.ui.sortable.js new file mode 100644 index 0000000..c9f2c90 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.sortable.js @@ -0,0 +1,1285 @@ +/*! + * jQuery UI Sortable 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/sortable/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.mouse.js + * jquery.ui.widget.js + */ +(function( $, undefined ) { + +/*jshint loopfunc: true */ + +function isOverAxis( x, reference, size ) { + return ( x > reference ) && ( x < ( reference + size ) ); +} + +function isFloating(item) { + return (/left|right/).test(item.css("float")) || (/inline|table-cell/).test(item.css("display")); +} + +$.widget("ui.sortable", $.ui.mouse, { + version: "1.10.3", + widgetEventPrefix: "sort", + ready: false, + options: { + appendTo: "parent", + axis: false, + connectWith: false, + containment: false, + cursor: "auto", + cursorAt: false, + dropOnEmpty: true, + forcePlaceholderSize: false, + forceHelperSize: false, + grid: false, + handle: false, + helper: "original", + items: "> *", + opacity: false, + placeholder: false, + revert: false, + scroll: true, + scrollSensitivity: 20, + scrollSpeed: 20, + scope: "default", + tolerance: "intersect", + zIndex: 1000, + + // callbacks + activate: null, + beforeStop: null, + change: null, + deactivate: null, + out: null, + over: null, + receive: null, + remove: null, + sort: null, + start: null, + stop: null, + update: null + }, + _create: function() { + + var o = this.options; + this.containerCache = {}; + this.element.addClass("ui-sortable"); + + //Get the items + this.refresh(); + + //Let's determine if the items are being displayed horizontally + this.floating = this.items.length ? o.axis === "x" || isFloating(this.items[0].item) : false; + + //Let's determine the parent's offset + this.offset = this.element.offset(); + + //Initialize mouse events for interaction + this._mouseInit(); + + //We're ready to go + this.ready = true; + + }, + + _destroy: function() { + this.element + .removeClass("ui-sortable ui-sortable-disabled"); + this._mouseDestroy(); + + for ( var i = this.items.length - 1; i >= 0; i-- ) { + this.items[i].item.removeData(this.widgetName + "-item"); + } + + return this; + }, + + _setOption: function(key, value){ + if ( key === "disabled" ) { + this.options[ key ] = value; + + this.widget().toggleClass( "ui-sortable-disabled", !!value ); + } else { + // Don't call widget base _setOption for disable as it adds ui-state-disabled class + $.Widget.prototype._setOption.apply(this, arguments); + } + }, + + _mouseCapture: function(event, overrideHandle) { + var currentItem = null, + validHandle = false, + that = this; + + if (this.reverting) { + return false; + } + + if(this.options.disabled || this.options.type === "static") { + return false; + } + + //We have to refresh the items data once first + this._refreshItems(event); + + //Find out if the clicked node (or one of its parents) is a actual item in this.items + $(event.target).parents().each(function() { + if($.data(this, that.widgetName + "-item") === that) { + currentItem = $(this); + return false; + } + }); + if($.data(event.target, that.widgetName + "-item") === that) { + currentItem = $(event.target); + } + + if(!currentItem) { + return false; + } + if(this.options.handle && !overrideHandle) { + $(this.options.handle, currentItem).find("*").addBack().each(function() { + if(this === event.target) { + validHandle = true; + } + }); + if(!validHandle) { + return false; + } + } + + this.currentItem = currentItem; + this._removeCurrentsFromItems(); + return true; + + }, + + _mouseStart: function(event, overrideHandle, noActivation) { + + var i, body, + o = this.options; + + this.currentContainer = this; + + //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture + this.refreshPositions(); + + //Create and append the visible helper + this.helper = this._createHelper(event); + + //Cache the helper size + this._cacheHelperProportions(); + + /* + * - Position generation - + * This block generates everything position related - it's the core of draggables. + */ + + //Cache the margins of the original element + this._cacheMargins(); + + //Get the next scrolling parent + this.scrollParent = this.helper.scrollParent(); + + //The element's absolute position on the page minus margins + this.offset = this.currentItem.offset(); + this.offset = { + top: this.offset.top - this.margins.top, + left: this.offset.left - this.margins.left + }; + + $.extend(this.offset, { + click: { //Where the click happened, relative to the element + left: event.pageX - this.offset.left, + top: event.pageY - this.offset.top + }, + parent: this._getParentOffset(), + relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper + }); + + // Only after we got the offset, we can change the helper's position to absolute + // TODO: Still need to figure out a way to make relative sorting possible + this.helper.css("position", "absolute"); + this.cssPosition = this.helper.css("position"); + + //Generate the original position + this.originalPosition = this._generatePosition(event); + this.originalPageX = event.pageX; + this.originalPageY = event.pageY; + + //Adjust the mouse offset relative to the helper if "cursorAt" is supplied + (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); + + //Cache the former DOM position + this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] }; + + //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way + if(this.helper[0] !== this.currentItem[0]) { + this.currentItem.hide(); + } + + //Create the placeholder + this._createPlaceholder(); + + //Set a containment if given in the options + if(o.containment) { + this._setContainment(); + } + + if( o.cursor && o.cursor !== "auto" ) { // cursor option + body = this.document.find( "body" ); + + // support: IE + this.storedCursor = body.css( "cursor" ); + body.css( "cursor", o.cursor ); + + this.storedStylesheet = $( "<style>*{ cursor: "+o.cursor+" !important; }</style>" ).appendTo( body ); + } + + if(o.opacity) { // opacity option + if (this.helper.css("opacity")) { + this._storedOpacity = this.helper.css("opacity"); + } + this.helper.css("opacity", o.opacity); + } + + if(o.zIndex) { // zIndex option + if (this.helper.css("zIndex")) { + this._storedZIndex = this.helper.css("zIndex"); + } + this.helper.css("zIndex", o.zIndex); + } + + //Prepare scrolling + if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { + this.overflowOffset = this.scrollParent.offset(); + } + + //Call callbacks + this._trigger("start", event, this._uiHash()); + + //Recache the helper size + if(!this._preserveHelperProportions) { + this._cacheHelperProportions(); + } + + + //Post "activate" events to possible containers + if( !noActivation ) { + for ( i = this.containers.length - 1; i >= 0; i-- ) { + this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) ); + } + } + + //Prepare possible droppables + if($.ui.ddmanager) { + $.ui.ddmanager.current = this; + } + + if ($.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(this, event); + } + + this.dragging = true; + + this.helper.addClass("ui-sortable-helper"); + this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position + return true; + + }, + + _mouseDrag: function(event) { + var i, item, itemElement, intersection, + o = this.options, + scrolled = false; + + //Compute the helpers position + this.position = this._generatePosition(event); + this.positionAbs = this._convertPositionTo("absolute"); + + if (!this.lastPositionAbs) { + this.lastPositionAbs = this.positionAbs; + } + + //Do scrolling + if(this.options.scroll) { + if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { + + if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { + this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed; + } else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) { + this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed; + } + + if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { + this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed; + } else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) { + this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed; + } + + } else { + + if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); + } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); + } + + if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); + } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); + } + + } + + if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(this, event); + } + } + + //Regenerate the absolute position used for position checks + this.positionAbs = this._convertPositionTo("absolute"); + + //Set the helper position + if(!this.options.axis || this.options.axis !== "y") { + this.helper[0].style.left = this.position.left+"px"; + } + if(!this.options.axis || this.options.axis !== "x") { + this.helper[0].style.top = this.position.top+"px"; + } + + //Rearrange + for (i = this.items.length - 1; i >= 0; i--) { + + //Cache variables and intersection, continue if no intersection + item = this.items[i]; + itemElement = item.item[0]; + intersection = this._intersectsWithPointer(item); + if (!intersection) { + continue; + } + + // Only put the placeholder inside the current Container, skip all + // items form other containers. This works because when moving + // an item from one container to another the + // currentContainer is switched before the placeholder is moved. + // + // Without this moving items in "sub-sortables" can cause the placeholder to jitter + // beetween the outer and inner container. + if (item.instance !== this.currentContainer) { + continue; + } + + // cannot intersect with itself + // no useless actions that have been done before + // no action if the item moved is the parent of the item checked + if (itemElement !== this.currentItem[0] && + this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement && + !$.contains(this.placeholder[0], itemElement) && + (this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true) + ) { + + this.direction = intersection === 1 ? "down" : "up"; + + if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) { + this._rearrange(event, item); + } else { + break; + } + + this._trigger("change", event, this._uiHash()); + break; + } + } + + //Post events to containers + this._contactContainers(event); + + //Interconnect with droppables + if($.ui.ddmanager) { + $.ui.ddmanager.drag(this, event); + } + + //Call callbacks + this._trigger("sort", event, this._uiHash()); + + this.lastPositionAbs = this.positionAbs; + return false; + + }, + + _mouseStop: function(event, noPropagation) { + + if(!event) { + return; + } + + //If we are using droppables, inform the manager about the drop + if ($.ui.ddmanager && !this.options.dropBehaviour) { + $.ui.ddmanager.drop(this, event); + } + + if(this.options.revert) { + var that = this, + cur = this.placeholder.offset(), + axis = this.options.axis, + animation = {}; + + if ( !axis || axis === "x" ) { + animation.left = cur.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollLeft); + } + if ( !axis || axis === "y" ) { + animation.top = cur.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollTop); + } + this.reverting = true; + $(this.helper).animate( animation, parseInt(this.options.revert, 10) || 500, function() { + that._clear(event); + }); + } else { + this._clear(event, noPropagation); + } + + return false; + + }, + + cancel: function() { + + if(this.dragging) { + + this._mouseUp({ target: null }); + + if(this.options.helper === "original") { + this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); + } else { + this.currentItem.show(); + } + + //Post deactivating events to containers + for (var i = this.containers.length - 1; i >= 0; i--){ + this.containers[i]._trigger("deactivate", null, this._uiHash(this)); + if(this.containers[i].containerCache.over) { + this.containers[i]._trigger("out", null, this._uiHash(this)); + this.containers[i].containerCache.over = 0; + } + } + + } + + if (this.placeholder) { + //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! + if(this.placeholder[0].parentNode) { + this.placeholder[0].parentNode.removeChild(this.placeholder[0]); + } + if(this.options.helper !== "original" && this.helper && this.helper[0].parentNode) { + this.helper.remove(); + } + + $.extend(this, { + helper: null, + dragging: false, + reverting: false, + _noFinalSort: null + }); + + if(this.domPosition.prev) { + $(this.domPosition.prev).after(this.currentItem); + } else { + $(this.domPosition.parent).prepend(this.currentItem); + } + } + + return this; + + }, + + serialize: function(o) { + + var items = this._getItemsAsjQuery(o && o.connected), + str = []; + o = o || {}; + + $(items).each(function() { + var res = ($(o.item || this).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[\-=_](.+)/)); + if (res) { + str.push((o.key || res[1]+"[]")+"="+(o.key && o.expression ? res[1] : res[2])); + } + }); + + if(!str.length && o.key) { + str.push(o.key + "="); + } + + return str.join("&"); + + }, + + toArray: function(o) { + + var items = this._getItemsAsjQuery(o && o.connected), + ret = []; + + o = o || {}; + + items.each(function() { ret.push($(o.item || this).attr(o.attribute || "id") || ""); }); + return ret; + + }, + + /* Be careful with the following core functions */ + _intersectsWith: function(item) { + + var x1 = this.positionAbs.left, + x2 = x1 + this.helperProportions.width, + y1 = this.positionAbs.top, + y2 = y1 + this.helperProportions.height, + l = item.left, + r = l + item.width, + t = item.top, + b = t + item.height, + dyClick = this.offset.click.top, + dxClick = this.offset.click.left, + isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && ( y1 + dyClick ) < b ), + isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && ( x1 + dxClick ) < r ), + isOverElement = isOverElementHeight && isOverElementWidth; + + if ( this.options.tolerance === "pointer" || + this.options.forcePointerForContainers || + (this.options.tolerance !== "pointer" && this.helperProportions[this.floating ? "width" : "height"] > item[this.floating ? "width" : "height"]) + ) { + return isOverElement; + } else { + + return (l < x1 + (this.helperProportions.width / 2) && // Right Half + x2 - (this.helperProportions.width / 2) < r && // Left Half + t < y1 + (this.helperProportions.height / 2) && // Bottom Half + y2 - (this.helperProportions.height / 2) < b ); // Top Half + + } + }, + + _intersectsWithPointer: function(item) { + + var isOverElementHeight = (this.options.axis === "x") || isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height), + isOverElementWidth = (this.options.axis === "y") || isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width), + isOverElement = isOverElementHeight && isOverElementWidth, + verticalDirection = this._getDragVerticalDirection(), + horizontalDirection = this._getDragHorizontalDirection(); + + if (!isOverElement) { + return false; + } + + return this.floating ? + ( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 ) + : ( verticalDirection && (verticalDirection === "down" ? 2 : 1) ); + + }, + + _intersectsWithSides: function(item) { + + var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height), + isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width), + verticalDirection = this._getDragVerticalDirection(), + horizontalDirection = this._getDragHorizontalDirection(); + + if (this.floating && horizontalDirection) { + return ((horizontalDirection === "right" && isOverRightHalf) || (horizontalDirection === "left" && !isOverRightHalf)); + } else { + return verticalDirection && ((verticalDirection === "down" && isOverBottomHalf) || (verticalDirection === "up" && !isOverBottomHalf)); + } + + }, + + _getDragVerticalDirection: function() { + var delta = this.positionAbs.top - this.lastPositionAbs.top; + return delta !== 0 && (delta > 0 ? "down" : "up"); + }, + + _getDragHorizontalDirection: function() { + var delta = this.positionAbs.left - this.lastPositionAbs.left; + return delta !== 0 && (delta > 0 ? "right" : "left"); + }, + + refresh: function(event) { + this._refreshItems(event); + this.refreshPositions(); + return this; + }, + + _connectWith: function() { + var options = this.options; + return options.connectWith.constructor === String ? [options.connectWith] : options.connectWith; + }, + + _getItemsAsjQuery: function(connected) { + + var i, j, cur, inst, + items = [], + queries = [], + connectWith = this._connectWith(); + + if(connectWith && connected) { + for (i = connectWith.length - 1; i >= 0; i--){ + cur = $(connectWith[i]); + for ( j = cur.length - 1; j >= 0; j--){ + inst = $.data(cur[j], this.widgetFullName); + if(inst && inst !== this && !inst.options.disabled) { + queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), inst]); + } + } + } + } + + queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { options: this.options, item: this.currentItem }) : $(this.options.items, this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), this]); + + for (i = queries.length - 1; i >= 0; i--){ + queries[i][0].each(function() { + items.push(this); + }); + } + + return $(items); + + }, + + _removeCurrentsFromItems: function() { + + var list = this.currentItem.find(":data(" + this.widgetName + "-item)"); + + this.items = $.grep(this.items, function (item) { + for (var j=0; j < list.length; j++) { + if(list[j] === item.item[0]) { + return false; + } + } + return true; + }); + + }, + + _refreshItems: function(event) { + + this.items = []; + this.containers = [this]; + + var i, j, cur, inst, targetData, _queries, item, queriesLength, + items = this.items, + queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]], + connectWith = this._connectWith(); + + if(connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down + for (i = connectWith.length - 1; i >= 0; i--){ + cur = $(connectWith[i]); + for (j = cur.length - 1; j >= 0; j--){ + inst = $.data(cur[j], this.widgetFullName); + if(inst && inst !== this && !inst.options.disabled) { + queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]); + this.containers.push(inst); + } + } + } + } + + for (i = queries.length - 1; i >= 0; i--) { + targetData = queries[i][1]; + _queries = queries[i][0]; + + for (j=0, queriesLength = _queries.length; j < queriesLength; j++) { + item = $(_queries[j]); + + item.data(this.widgetName + "-item", targetData); // Data for target checking (mouse manager) + + items.push({ + item: item, + instance: targetData, + width: 0, height: 0, + left: 0, top: 0 + }); + } + } + + }, + + refreshPositions: function(fast) { + + //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change + if(this.offsetParent && this.helper) { + this.offset.parent = this._getParentOffset(); + } + + var i, item, t, p; + + for (i = this.items.length - 1; i >= 0; i--){ + item = this.items[i]; + + //We ignore calculating positions of all connected containers when we're not over them + if(item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) { + continue; + } + + t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item; + + if (!fast) { + item.width = t.outerWidth(); + item.height = t.outerHeight(); + } + + p = t.offset(); + item.left = p.left; + item.top = p.top; + } + + if(this.options.custom && this.options.custom.refreshContainers) { + this.options.custom.refreshContainers.call(this); + } else { + for (i = this.containers.length - 1; i >= 0; i--){ + p = this.containers[i].element.offset(); + this.containers[i].containerCache.left = p.left; + this.containers[i].containerCache.top = p.top; + this.containers[i].containerCache.width = this.containers[i].element.outerWidth(); + this.containers[i].containerCache.height = this.containers[i].element.outerHeight(); + } + } + + return this; + }, + + _createPlaceholder: function(that) { + that = that || this; + var className, + o = that.options; + + if(!o.placeholder || o.placeholder.constructor === String) { + className = o.placeholder; + o.placeholder = { + element: function() { + + var nodeName = that.currentItem[0].nodeName.toLowerCase(), + element = $( "<" + nodeName + ">", that.document[0] ) + .addClass(className || that.currentItem[0].className+" ui-sortable-placeholder") + .removeClass("ui-sortable-helper"); + + if ( nodeName === "tr" ) { + that.currentItem.children().each(function() { + $( "<td> </td>", that.document[0] ) + .attr( "colspan", $( this ).attr( "colspan" ) || 1 ) + .appendTo( element ); + }); + } else if ( nodeName === "img" ) { + element.attr( "src", that.currentItem.attr( "src" ) ); + } + + if ( !className ) { + element.css( "visibility", "hidden" ); + } + + return element; + }, + update: function(container, p) { + + // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that + // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified + if(className && !o.forcePlaceholderSize) { + return; + } + + //If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item + if(!p.height()) { p.height(that.currentItem.innerHeight() - parseInt(that.currentItem.css("paddingTop")||0, 10) - parseInt(that.currentItem.css("paddingBottom")||0, 10)); } + if(!p.width()) { p.width(that.currentItem.innerWidth() - parseInt(that.currentItem.css("paddingLeft")||0, 10) - parseInt(that.currentItem.css("paddingRight")||0, 10)); } + } + }; + } + + //Create the placeholder + that.placeholder = $(o.placeholder.element.call(that.element, that.currentItem)); + + //Append it after the actual current item + that.currentItem.after(that.placeholder); + + //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) + o.placeholder.update(that, that.placeholder); + + }, + + _contactContainers: function(event) { + var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, base, cur, nearBottom, floating, + innermostContainer = null, + innermostIndex = null; + + // get innermost container that intersects with item + for (i = this.containers.length - 1; i >= 0; i--) { + + // never consider a container that's located within the item itself + if($.contains(this.currentItem[0], this.containers[i].element[0])) { + continue; + } + + if(this._intersectsWith(this.containers[i].containerCache)) { + + // if we've already found a container and it's more "inner" than this, then continue + if(innermostContainer && $.contains(this.containers[i].element[0], innermostContainer.element[0])) { + continue; + } + + innermostContainer = this.containers[i]; + innermostIndex = i; + + } else { + // container doesn't intersect. trigger "out" event if necessary + if(this.containers[i].containerCache.over) { + this.containers[i]._trigger("out", event, this._uiHash(this)); + this.containers[i].containerCache.over = 0; + } + } + + } + + // if no intersecting containers found, return + if(!innermostContainer) { + return; + } + + // move the item into the container if it's not there already + if(this.containers.length === 1) { + if (!this.containers[innermostIndex].containerCache.over) { + this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); + this.containers[innermostIndex].containerCache.over = 1; + } + } else { + + //When entering a new container, we will find the item with the least distance and append our item near it + dist = 10000; + itemWithLeastDistance = null; + floating = innermostContainer.floating || isFloating(this.currentItem); + posProperty = floating ? "left" : "top"; + sizeProperty = floating ? "width" : "height"; + base = this.positionAbs[posProperty] + this.offset.click[posProperty]; + for (j = this.items.length - 1; j >= 0; j--) { + if(!$.contains(this.containers[innermostIndex].element[0], this.items[j].item[0])) { + continue; + } + if(this.items[j].item[0] === this.currentItem[0]) { + continue; + } + if (floating && !isOverAxis(this.positionAbs.top + this.offset.click.top, this.items[j].top, this.items[j].height)) { + continue; + } + cur = this.items[j].item.offset()[posProperty]; + nearBottom = false; + if(Math.abs(cur - base) > Math.abs(cur + this.items[j][sizeProperty] - base)){ + nearBottom = true; + cur += this.items[j][sizeProperty]; + } + + if(Math.abs(cur - base) < dist) { + dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j]; + this.direction = nearBottom ? "up": "down"; + } + } + + //Check if dropOnEmpty is enabled + if(!itemWithLeastDistance && !this.options.dropOnEmpty) { + return; + } + + if(this.currentContainer === this.containers[innermostIndex]) { + return; + } + + itemWithLeastDistance ? this._rearrange(event, itemWithLeastDistance, null, true) : this._rearrange(event, null, this.containers[innermostIndex].element, true); + this._trigger("change", event, this._uiHash()); + this.containers[innermostIndex]._trigger("change", event, this._uiHash(this)); + this.currentContainer = this.containers[innermostIndex]; + + //Update the placeholder + this.options.placeholder.update(this.currentContainer, this.placeholder); + + this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); + this.containers[innermostIndex].containerCache.over = 1; + } + + + }, + + _createHelper: function(event) { + + var o = this.options, + helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper === "clone" ? this.currentItem.clone() : this.currentItem); + + //Add the helper to the DOM if that didn't happen already + if(!helper.parents("body").length) { + $(o.appendTo !== "parent" ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]); + } + + if(helper[0] === this.currentItem[0]) { + this._storedCSS = { width: this.currentItem[0].style.width, height: this.currentItem[0].style.height, position: this.currentItem.css("position"), top: this.currentItem.css("top"), left: this.currentItem.css("left") }; + } + + if(!helper[0].style.width || o.forceHelperSize) { + helper.width(this.currentItem.width()); + } + if(!helper[0].style.height || o.forceHelperSize) { + helper.height(this.currentItem.height()); + } + + return helper; + + }, + + _adjustOffsetFromHelper: function(obj) { + if (typeof obj === "string") { + obj = obj.split(" "); + } + if ($.isArray(obj)) { + obj = {left: +obj[0], top: +obj[1] || 0}; + } + if ("left" in obj) { + this.offset.click.left = obj.left + this.margins.left; + } + if ("right" in obj) { + this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; + } + if ("top" in obj) { + this.offset.click.top = obj.top + this.margins.top; + } + if ("bottom" in obj) { + this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; + } + }, + + _getParentOffset: function() { + + + //Get the offsetParent and cache its position + this.offsetParent = this.helper.offsetParent(); + var po = this.offsetParent.offset(); + + // This is a special case where we need to modify a offset calculated on start, since the following happened: + // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent + // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that + // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag + if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { + po.left += this.scrollParent.scrollLeft(); + po.top += this.scrollParent.scrollTop(); + } + + // This needs to be actually done for all browsers, since pageX/pageY includes this information + // with an ugly IE fix + if( this.offsetParent[0] === document.body || (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { + po = { top: 0, left: 0 }; + } + + return { + top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), + left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) + }; + + }, + + _getRelativeOffset: function() { + + if(this.cssPosition === "relative") { + var p = this.currentItem.position(); + return { + top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), + left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() + }; + } else { + return { top: 0, left: 0 }; + } + + }, + + _cacheMargins: function() { + this.margins = { + left: (parseInt(this.currentItem.css("marginLeft"),10) || 0), + top: (parseInt(this.currentItem.css("marginTop"),10) || 0) + }; + }, + + _cacheHelperProportions: function() { + this.helperProportions = { + width: this.helper.outerWidth(), + height: this.helper.outerHeight() + }; + }, + + _setContainment: function() { + + var ce, co, over, + o = this.options; + if(o.containment === "parent") { + o.containment = this.helper[0].parentNode; + } + if(o.containment === "document" || o.containment === "window") { + this.containment = [ + 0 - this.offset.relative.left - this.offset.parent.left, + 0 - this.offset.relative.top - this.offset.parent.top, + $(o.containment === "document" ? document : window).width() - this.helperProportions.width - this.margins.left, + ($(o.containment === "document" ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top + ]; + } + + if(!(/^(document|window|parent)$/).test(o.containment)) { + ce = $(o.containment)[0]; + co = $(o.containment).offset(); + over = ($(ce).css("overflow") !== "hidden"); + + this.containment = [ + co.left + (parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0) - this.margins.left, + co.top + (parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0) - this.margins.top, + co.left+(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left, + co.top+(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top + ]; + } + + }, + + _convertPositionTo: function(d, pos) { + + if(!pos) { + pos = this.position; + } + var mod = d === "absolute" ? 1 : -1, + scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, + scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); + + return { + top: ( + pos.top + // The absolute mouse position + this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod) + ), + left: ( + pos.left + // The absolute mouse position + this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod) + ) + }; + + }, + + _generatePosition: function(event) { + + var top, left, + o = this.options, + pageX = event.pageX, + pageY = event.pageY, + scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); + + // This is another very weird special case that only happens for relative elements: + // 1. If the css position is relative + // 2. and the scroll parent is the document or similar to the offset parent + // we have to refresh the relative offset during the scroll so there are no jumps + if(this.cssPosition === "relative" && !(this.scrollParent[0] !== document && this.scrollParent[0] !== this.offsetParent[0])) { + this.offset.relative = this._getRelativeOffset(); + } + + /* + * - Position constraining - + * Constrain the position to a mix of grid, containment. + */ + + if(this.originalPosition) { //If we are not dragging yet, we won't check for options + + if(this.containment) { + if(event.pageX - this.offset.click.left < this.containment[0]) { + pageX = this.containment[0] + this.offset.click.left; + } + if(event.pageY - this.offset.click.top < this.containment[1]) { + pageY = this.containment[1] + this.offset.click.top; + } + if(event.pageX - this.offset.click.left > this.containment[2]) { + pageX = this.containment[2] + this.offset.click.left; + } + if(event.pageY - this.offset.click.top > this.containment[3]) { + pageY = this.containment[3] + this.offset.click.top; + } + } + + if(o.grid) { + top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1]; + pageY = this.containment ? ( (top - this.offset.click.top >= this.containment[1] && top - this.offset.click.top <= this.containment[3]) ? top : ((top - this.offset.click.top >= this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; + + left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0]; + pageX = this.containment ? ( (left - this.offset.click.left >= this.containment[0] && left - this.offset.click.left <= this.containment[2]) ? left : ((left - this.offset.click.left >= this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; + } + + } + + return { + top: ( + pageY - // The absolute mouse position + this.offset.click.top - // Click offset (relative to the element) + this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top + // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) )) + ), + left: ( + pageX - // The absolute mouse position + this.offset.click.left - // Click offset (relative to the element) + this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left + // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() )) + ) + }; + + }, + + _rearrange: function(event, i, a, hardRefresh) { + + a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling)); + + //Various things done here to improve the performance: + // 1. we create a setTimeout, that calls refreshPositions + // 2. on the instance, we have a counter variable, that get's higher after every append + // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same + // 4. this lets only the last addition to the timeout stack through + this.counter = this.counter ? ++this.counter : 1; + var counter = this.counter; + + this._delay(function() { + if(counter === this.counter) { + this.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove + } + }); + + }, + + _clear: function(event, noPropagation) { + + this.reverting = false; + // We delay all events that have to be triggered to after the point where the placeholder has been removed and + // everything else normalized again + var i, + delayedTriggers = []; + + // We first have to update the dom position of the actual currentItem + // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088) + if(!this._noFinalSort && this.currentItem.parent().length) { + this.placeholder.before(this.currentItem); + } + this._noFinalSort = null; + + if(this.helper[0] === this.currentItem[0]) { + for(i in this._storedCSS) { + if(this._storedCSS[i] === "auto" || this._storedCSS[i] === "static") { + this._storedCSS[i] = ""; + } + } + this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); + } else { + this.currentItem.show(); + } + + if(this.fromOutside && !noPropagation) { + delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); }); + } + if((this.fromOutside || this.domPosition.prev !== this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent !== this.currentItem.parent()[0]) && !noPropagation) { + delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed + } + + // Check if the items Container has Changed and trigger appropriate + // events. + if (this !== this.currentContainer) { + if(!noPropagation) { + delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); }); + delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); + delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); + } + } + + + //Post events to containers + for (i = this.containers.length - 1; i >= 0; i--){ + if(!noPropagation) { + delayedTriggers.push((function(c) { return function(event) { c._trigger("deactivate", event, this._uiHash(this)); }; }).call(this, this.containers[i])); + } + if(this.containers[i].containerCache.over) { + delayedTriggers.push((function(c) { return function(event) { c._trigger("out", event, this._uiHash(this)); }; }).call(this, this.containers[i])); + this.containers[i].containerCache.over = 0; + } + } + + //Do what was originally in plugins + if ( this.storedCursor ) { + this.document.find( "body" ).css( "cursor", this.storedCursor ); + this.storedStylesheet.remove(); + } + if(this._storedOpacity) { + this.helper.css("opacity", this._storedOpacity); + } + if(this._storedZIndex) { + this.helper.css("zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex); + } + + this.dragging = false; + if(this.cancelHelperRemoval) { + if(!noPropagation) { + this._trigger("beforeStop", event, this._uiHash()); + for (i=0; i < delayedTriggers.length; i++) { + delayedTriggers[i].call(this, event); + } //Trigger all delayed events + this._trigger("stop", event, this._uiHash()); + } + + this.fromOutside = false; + return false; + } + + if(!noPropagation) { + this._trigger("beforeStop", event, this._uiHash()); + } + + //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! + this.placeholder[0].parentNode.removeChild(this.placeholder[0]); + + if(this.helper[0] !== this.currentItem[0]) { + this.helper.remove(); + } + this.helper = null; + + if(!noPropagation) { + for (i=0; i < delayedTriggers.length; i++) { + delayedTriggers[i].call(this, event); + } //Trigger all delayed events + this._trigger("stop", event, this._uiHash()); + } + + this.fromOutside = false; + return true; + + }, + + _trigger: function() { + if ($.Widget.prototype._trigger.apply(this, arguments) === false) { + this.cancel(); + } + }, + + _uiHash: function(_inst) { + var inst = _inst || this; + return { + helper: inst.helper, + placeholder: inst.placeholder || $([]), + position: inst.position, + originalPosition: inst.originalPosition, + offset: inst.positionAbs, + item: inst.currentItem, + sender: _inst ? _inst.element : null + }; + } + +}); + +})(jQuery); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.spinner.js b/extensions/jui/yii/jui/assets/jquery.ui.spinner.js new file mode 100644 index 0000000..5bfbb27 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.spinner.js @@ -0,0 +1,493 @@ +/*! + * jQuery UI Spinner 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/spinner/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + * jquery.ui.button.js + */ +(function( $ ) { + +function modifier( fn ) { + return function() { + var previous = this.element.val(); + fn.apply( this, arguments ); + this._refresh(); + if ( previous !== this.element.val() ) { + this._trigger( "change" ); + } + }; +} + +$.widget( "ui.spinner", { + version: "1.10.3", + defaultElement: "<input>", + widgetEventPrefix: "spin", + options: { + culture: null, + icons: { + down: "ui-icon-triangle-1-s", + up: "ui-icon-triangle-1-n" + }, + incremental: true, + max: null, + min: null, + numberFormat: null, + page: 10, + step: 1, + + change: null, + spin: null, + start: null, + stop: null + }, + + _create: function() { + // handle string values that need to be parsed + this._setOption( "max", this.options.max ); + this._setOption( "min", this.options.min ); + this._setOption( "step", this.options.step ); + + // format the value, but don't constrain + this._value( this.element.val(), true ); + + this._draw(); + this._on( this._events ); + this._refresh(); + + // turning off autocomplete prevents the browser from remembering the + // value when navigating through history, so we re-enable autocomplete + // if the page is unloaded before the widget is destroyed. #7790 + this._on( this.window, { + beforeunload: function() { + this.element.removeAttr( "autocomplete" ); + } + }); + }, + + _getCreateOptions: function() { + var options = {}, + element = this.element; + + $.each( [ "min", "max", "step" ], function( i, option ) { + var value = element.attr( option ); + if ( value !== undefined && value.length ) { + options[ option ] = value; + } + }); + + return options; + }, + + _events: { + keydown: function( event ) { + if ( this._start( event ) && this._keydown( event ) ) { + event.preventDefault(); + } + }, + keyup: "_stop", + focus: function() { + this.previous = this.element.val(); + }, + blur: function( event ) { + if ( this.cancelBlur ) { + delete this.cancelBlur; + return; + } + + this._stop(); + this._refresh(); + if ( this.previous !== this.element.val() ) { + this._trigger( "change", event ); + } + }, + mousewheel: function( event, delta ) { + if ( !delta ) { + return; + } + if ( !this.spinning && !this._start( event ) ) { + return false; + } + + this._spin( (delta > 0 ? 1 : -1) * this.options.step, event ); + clearTimeout( this.mousewheelTimer ); + this.mousewheelTimer = this._delay(function() { + if ( this.spinning ) { + this._stop( event ); + } + }, 100 ); + event.preventDefault(); + }, + "mousedown .ui-spinner-button": function( event ) { + var previous; + + // We never want the buttons to have focus; whenever the user is + // interacting with the spinner, the focus should be on the input. + // If the input is focused then this.previous is properly set from + // when the input first received focus. If the input is not focused + // then we need to set this.previous based on the value before spinning. + previous = this.element[0] === this.document[0].activeElement ? + this.previous : this.element.val(); + function checkFocus() { + var isActive = this.element[0] === this.document[0].activeElement; + if ( !isActive ) { + this.element.focus(); + this.previous = previous; + // support: IE + // IE sets focus asynchronously, so we need to check if focus + // moved off of the input because the user clicked on the button. + this._delay(function() { + this.previous = previous; + }); + } + } + + // ensure focus is on (or stays on) the text field + event.preventDefault(); + checkFocus.call( this ); + + // support: IE + // IE doesn't prevent moving focus even with event.preventDefault() + // so we set a flag to know when we should ignore the blur event + // and check (again) if focus moved off of the input. + this.cancelBlur = true; + this._delay(function() { + delete this.cancelBlur; + checkFocus.call( this ); + }); + + if ( this._start( event ) === false ) { + return; + } + + this._repeat( null, $( event.currentTarget ).hasClass( "ui-spinner-up" ) ? 1 : -1, event ); + }, + "mouseup .ui-spinner-button": "_stop", + "mouseenter .ui-spinner-button": function( event ) { + // button will add ui-state-active if mouse was down while mouseleave and kept down + if ( !$( event.currentTarget ).hasClass( "ui-state-active" ) ) { + return; + } + + if ( this._start( event ) === false ) { + return false; + } + this._repeat( null, $( event.currentTarget ).hasClass( "ui-spinner-up" ) ? 1 : -1, event ); + }, + // TODO: do we really want to consider this a stop? + // shouldn't we just stop the repeater and wait until mouseup before + // we trigger the stop event? + "mouseleave .ui-spinner-button": "_stop" + }, + + _draw: function() { + var uiSpinner = this.uiSpinner = this.element + .addClass( "ui-spinner-input" ) + .attr( "autocomplete", "off" ) + .wrap( this._uiSpinnerHtml() ) + .parent() + // add buttons + .append( this._buttonHtml() ); + + this.element.attr( "role", "spinbutton" ); + + // button bindings + this.buttons = uiSpinner.find( ".ui-spinner-button" ) + .attr( "tabIndex", -1 ) + .button() + .removeClass( "ui-corner-all" ); + + // IE 6 doesn't understand height: 50% for the buttons + // unless the wrapper has an explicit height + if ( this.buttons.height() > Math.ceil( uiSpinner.height() * 0.5 ) && + uiSpinner.height() > 0 ) { + uiSpinner.height( uiSpinner.height() ); + } + + // disable spinner if element was already disabled + if ( this.options.disabled ) { + this.disable(); + } + }, + + _keydown: function( event ) { + var options = this.options, + keyCode = $.ui.keyCode; + + switch ( event.keyCode ) { + case keyCode.UP: + this._repeat( null, 1, event ); + return true; + case keyCode.DOWN: + this._repeat( null, -1, event ); + return true; + case keyCode.PAGE_UP: + this._repeat( null, options.page, event ); + return true; + case keyCode.PAGE_DOWN: + this._repeat( null, -options.page, event ); + return true; + } + + return false; + }, + + _uiSpinnerHtml: function() { + return "<span class='ui-spinner ui-widget ui-widget-content ui-corner-all'></span>"; + }, + + _buttonHtml: function() { + return "" + + "<a class='ui-spinner-button ui-spinner-up ui-corner-tr'>" + + "<span class='ui-icon " + this.options.icons.up + "'>▲</span>" + + "</a>" + + "<a class='ui-spinner-button ui-spinner-down ui-corner-br'>" + + "<span class='ui-icon " + this.options.icons.down + "'>▼</span>" + + "</a>"; + }, + + _start: function( event ) { + if ( !this.spinning && this._trigger( "start", event ) === false ) { + return false; + } + + if ( !this.counter ) { + this.counter = 1; + } + this.spinning = true; + return true; + }, + + _repeat: function( i, steps, event ) { + i = i || 500; + + clearTimeout( this.timer ); + this.timer = this._delay(function() { + this._repeat( 40, steps, event ); + }, i ); + + this._spin( steps * this.options.step, event ); + }, + + _spin: function( step, event ) { + var value = this.value() || 0; + + if ( !this.counter ) { + this.counter = 1; + } + + value = this._adjustValue( value + step * this._increment( this.counter ) ); + + if ( !this.spinning || this._trigger( "spin", event, { value: value } ) !== false) { + this._value( value ); + this.counter++; + } + }, + + _increment: function( i ) { + var incremental = this.options.incremental; + + if ( incremental ) { + return $.isFunction( incremental ) ? + incremental( i ) : + Math.floor( i*i*i/50000 - i*i/500 + 17*i/200 + 1 ); + } + + return 1; + }, + + _precision: function() { + var precision = this._precisionOf( this.options.step ); + if ( this.options.min !== null ) { + precision = Math.max( precision, this._precisionOf( this.options.min ) ); + } + return precision; + }, + + _precisionOf: function( num ) { + var str = num.toString(), + decimal = str.indexOf( "." ); + return decimal === -1 ? 0 : str.length - decimal - 1; + }, + + _adjustValue: function( value ) { + var base, aboveMin, + options = this.options; + + // make sure we're at a valid step + // - find out where we are relative to the base (min or 0) + base = options.min !== null ? options.min : 0; + aboveMin = value - base; + // - round to the nearest step + aboveMin = Math.round(aboveMin / options.step) * options.step; + // - rounding is based on 0, so adjust back to our base + value = base + aboveMin; + + // fix precision from bad JS floating point math + value = parseFloat( value.toFixed( this._precision() ) ); + + // clamp the value + if ( options.max !== null && value > options.max) { + return options.max; + } + if ( options.min !== null && value < options.min ) { + return options.min; + } + + return value; + }, + + _stop: function( event ) { + if ( !this.spinning ) { + return; + } + + clearTimeout( this.timer ); + clearTimeout( this.mousewheelTimer ); + this.counter = 0; + this.spinning = false; + this._trigger( "stop", event ); + }, + + _setOption: function( key, value ) { + if ( key === "culture" || key === "numberFormat" ) { + var prevValue = this._parse( this.element.val() ); + this.options[ key ] = value; + this.element.val( this._format( prevValue ) ); + return; + } + + if ( key === "max" || key === "min" || key === "step" ) { + if ( typeof value === "string" ) { + value = this._parse( value ); + } + } + if ( key === "icons" ) { + this.buttons.first().find( ".ui-icon" ) + .removeClass( this.options.icons.up ) + .addClass( value.up ); + this.buttons.last().find( ".ui-icon" ) + .removeClass( this.options.icons.down ) + .addClass( value.down ); + } + + this._super( key, value ); + + if ( key === "disabled" ) { + if ( value ) { + this.element.prop( "disabled", true ); + this.buttons.button( "disable" ); + } else { + this.element.prop( "disabled", false ); + this.buttons.button( "enable" ); + } + } + }, + + _setOptions: modifier(function( options ) { + this._super( options ); + this._value( this.element.val() ); + }), + + _parse: function( val ) { + if ( typeof val === "string" && val !== "" ) { + val = window.Globalize && this.options.numberFormat ? + Globalize.parseFloat( val, 10, this.options.culture ) : +val; + } + return val === "" || isNaN( val ) ? null : val; + }, + + _format: function( value ) { + if ( value === "" ) { + return ""; + } + return window.Globalize && this.options.numberFormat ? + Globalize.format( value, this.options.numberFormat, this.options.culture ) : + value; + }, + + _refresh: function() { + this.element.attr({ + "aria-valuemin": this.options.min, + "aria-valuemax": this.options.max, + // TODO: what should we do with values that can't be parsed? + "aria-valuenow": this._parse( this.element.val() ) + }); + }, + + // update the value without triggering change + _value: function( value, allowAny ) { + var parsed; + if ( value !== "" ) { + parsed = this._parse( value ); + if ( parsed !== null ) { + if ( !allowAny ) { + parsed = this._adjustValue( parsed ); + } + value = this._format( parsed ); + } + } + this.element.val( value ); + this._refresh(); + }, + + _destroy: function() { + this.element + .removeClass( "ui-spinner-input" ) + .prop( "disabled", false ) + .removeAttr( "autocomplete" ) + .removeAttr( "role" ) + .removeAttr( "aria-valuemin" ) + .removeAttr( "aria-valuemax" ) + .removeAttr( "aria-valuenow" ); + this.uiSpinner.replaceWith( this.element ); + }, + + stepUp: modifier(function( steps ) { + this._stepUp( steps ); + }), + _stepUp: function( steps ) { + if ( this._start() ) { + this._spin( (steps || 1) * this.options.step ); + this._stop(); + } + }, + + stepDown: modifier(function( steps ) { + this._stepDown( steps ); + }), + _stepDown: function( steps ) { + if ( this._start() ) { + this._spin( (steps || 1) * -this.options.step ); + this._stop(); + } + }, + + pageUp: modifier(function( pages ) { + this._stepUp( (pages || 1) * this.options.page ); + }), + + pageDown: modifier(function( pages ) { + this._stepDown( (pages || 1) * this.options.page ); + }), + + value: function( newVal ) { + if ( !arguments.length ) { + return this._parse( this.element.val() ); + } + modifier( this._value ).call( this, newVal ); + }, + + widget: function() { + return this.uiSpinner; + } +}); + +}( jQuery ) ); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.tabs.js b/extensions/jui/yii/jui/assets/jquery.ui.tabs.js new file mode 100644 index 0000000..b37858e --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.tabs.js @@ -0,0 +1,846 @@ +/*! + * jQuery UI Tabs 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/tabs/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + */ +(function( $, undefined ) { + +var tabId = 0, + rhash = /#.*$/; + +function getNextTabId() { + return ++tabId; +} + +function isLocal( anchor ) { + return anchor.hash.length > 1 && + decodeURIComponent( anchor.href.replace( rhash, "" ) ) === + decodeURIComponent( location.href.replace( rhash, "" ) ); +} + +$.widget( "ui.tabs", { + version: "1.10.3", + delay: 300, + options: { + active: null, + collapsible: false, + event: "click", + heightStyle: "content", + hide: null, + show: null, + + // callbacks + activate: null, + beforeActivate: null, + beforeLoad: null, + load: null + }, + + _create: function() { + var that = this, + options = this.options; + + this.running = false; + + this.element + .addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" ) + .toggleClass( "ui-tabs-collapsible", options.collapsible ) + // Prevent users from focusing disabled tabs via click + .delegate( ".ui-tabs-nav > li", "mousedown" + this.eventNamespace, function( event ) { + if ( $( this ).is( ".ui-state-disabled" ) ) { + event.preventDefault(); + } + }) + // support: IE <9 + // Preventing the default action in mousedown doesn't prevent IE + // from focusing the element, so if the anchor gets focused, blur. + // We don't have to worry about focusing the previously focused + // element since clicking on a non-focusable element should focus + // the body anyway. + .delegate( ".ui-tabs-anchor", "focus" + this.eventNamespace, function() { + if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) { + this.blur(); + } + }); + + this._processTabs(); + options.active = this._initialActive(); + + // Take disabling tabs via class attribute from HTML + // into account and update option properly. + if ( $.isArray( options.disabled ) ) { + options.disabled = $.unique( options.disabled.concat( + $.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) { + return that.tabs.index( li ); + }) + ) ).sort(); + } + + // check for length avoids error when initializing empty list + if ( this.options.active !== false && this.anchors.length ) { + this.active = this._findActive( options.active ); + } else { + this.active = $(); + } + + this._refresh(); + + if ( this.active.length ) { + this.load( options.active ); + } + }, + + _initialActive: function() { + var active = this.options.active, + collapsible = this.options.collapsible, + locationHash = location.hash.substring( 1 ); + + if ( active === null ) { + // check the fragment identifier in the URL + if ( locationHash ) { + this.tabs.each(function( i, tab ) { + if ( $( tab ).attr( "aria-controls" ) === locationHash ) { + active = i; + return false; + } + }); + } + + // check for a tab marked active via a class + if ( active === null ) { + active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) ); + } + + // no active tab, set to false + if ( active === null || active === -1 ) { + active = this.tabs.length ? 0 : false; + } + } + + // handle numbers: negative, out of range + if ( active !== false ) { + active = this.tabs.index( this.tabs.eq( active ) ); + if ( active === -1 ) { + active = collapsible ? false : 0; + } + } + + // don't allow collapsible: false and active: false + if ( !collapsible && active === false && this.anchors.length ) { + active = 0; + } + + return active; + }, + + _getCreateEventData: function() { + return { + tab: this.active, + panel: !this.active.length ? $() : this._getPanelForTab( this.active ) + }; + }, + + _tabKeydown: function( event ) { + /*jshint maxcomplexity:15*/ + var focusedTab = $( this.document[0].activeElement ).closest( "li" ), + selectedIndex = this.tabs.index( focusedTab ), + goingForward = true; + + if ( this._handlePageNav( event ) ) { + return; + } + + switch ( event.keyCode ) { + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + selectedIndex++; + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.LEFT: + goingForward = false; + selectedIndex--; + break; + case $.ui.keyCode.END: + selectedIndex = this.anchors.length - 1; + break; + case $.ui.keyCode.HOME: + selectedIndex = 0; + break; + case $.ui.keyCode.SPACE: + // Activate only, no collapsing + event.preventDefault(); + clearTimeout( this.activating ); + this._activate( selectedIndex ); + return; + case $.ui.keyCode.ENTER: + // Toggle (cancel delayed activation, allow collapsing) + event.preventDefault(); + clearTimeout( this.activating ); + // Determine if we should collapse or activate + this._activate( selectedIndex === this.options.active ? false : selectedIndex ); + return; + default: + return; + } + + // Focus the appropriate tab, based on which key was pressed + event.preventDefault(); + clearTimeout( this.activating ); + selectedIndex = this._focusNextTab( selectedIndex, goingForward ); + + // Navigating with control key will prevent automatic activation + if ( !event.ctrlKey ) { + // Update aria-selected immediately so that AT think the tab is already selected. + // Otherwise AT may confuse the user by stating that they need to activate the tab, + // but the tab will already be activated by the time the announcement finishes. + focusedTab.attr( "aria-selected", "false" ); + this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" ); + + this.activating = this._delay(function() { + this.option( "active", selectedIndex ); + }, this.delay ); + } + }, + + _panelKeydown: function( event ) { + if ( this._handlePageNav( event ) ) { + return; + } + + // Ctrl+up moves focus to the current tab + if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) { + event.preventDefault(); + this.active.focus(); + } + }, + + // Alt+page up/down moves focus to the previous/next tab (and activates) + _handlePageNav: function( event ) { + if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) { + this._activate( this._focusNextTab( this.options.active - 1, false ) ); + return true; + } + if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) { + this._activate( this._focusNextTab( this.options.active + 1, true ) ); + return true; + } + }, + + _findNextTab: function( index, goingForward ) { + var lastTabIndex = this.tabs.length - 1; + + function constrain() { + if ( index > lastTabIndex ) { + index = 0; + } + if ( index < 0 ) { + index = lastTabIndex; + } + return index; + } + + while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) { + index = goingForward ? index + 1 : index - 1; + } + + return index; + }, + + _focusNextTab: function( index, goingForward ) { + index = this._findNextTab( index, goingForward ); + this.tabs.eq( index ).focus(); + return index; + }, + + _setOption: function( key, value ) { + if ( key === "active" ) { + // _activate() will handle invalid values and update this.options + this._activate( value ); + return; + } + + if ( key === "disabled" ) { + // don't use the widget factory's disabled handling + this._setupDisabled( value ); + return; + } + + this._super( key, value); + + if ( key === "collapsible" ) { + this.element.toggleClass( "ui-tabs-collapsible", value ); + // Setting collapsible: false while collapsed; open first panel + if ( !value && this.options.active === false ) { + this._activate( 0 ); + } + } + + if ( key === "event" ) { + this._setupEvents( value ); + } + + if ( key === "heightStyle" ) { + this._setupHeightStyle( value ); + } + }, + + _tabId: function( tab ) { + return tab.attr( "aria-controls" ) || "ui-tabs-" + getNextTabId(); + }, + + _sanitizeSelector: function( hash ) { + return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : ""; + }, + + refresh: function() { + var options = this.options, + lis = this.tablist.children( ":has(a[href])" ); + + // get disabled tabs from class attribute from HTML + // this will get converted to a boolean if needed in _refresh() + options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) { + return lis.index( tab ); + }); + + this._processTabs(); + + // was collapsed or no tabs + if ( options.active === false || !this.anchors.length ) { + options.active = false; + this.active = $(); + // was active, but active tab is gone + } else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) { + // all remaining tabs are disabled + if ( this.tabs.length === options.disabled.length ) { + options.active = false; + this.active = $(); + // activate previous tab + } else { + this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) ); + } + // was active, active tab still exists + } else { + // make sure active index is correct + options.active = this.tabs.index( this.active ); + } + + this._refresh(); + }, + + _refresh: function() { + this._setupDisabled( this.options.disabled ); + this._setupEvents( this.options.event ); + this._setupHeightStyle( this.options.heightStyle ); + + this.tabs.not( this.active ).attr({ + "aria-selected": "false", + tabIndex: -1 + }); + this.panels.not( this._getPanelForTab( this.active ) ) + .hide() + .attr({ + "aria-expanded": "false", + "aria-hidden": "true" + }); + + // Make sure one tab is in the tab order + if ( !this.active.length ) { + this.tabs.eq( 0 ).attr( "tabIndex", 0 ); + } else { + this.active + .addClass( "ui-tabs-active ui-state-active" ) + .attr({ + "aria-selected": "true", + tabIndex: 0 + }); + this._getPanelForTab( this.active ) + .show() + .attr({ + "aria-expanded": "true", + "aria-hidden": "false" + }); + } + }, + + _processTabs: function() { + var that = this; + + this.tablist = this._getList() + .addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ) + .attr( "role", "tablist" ); + + this.tabs = this.tablist.find( "> li:has(a[href])" ) + .addClass( "ui-state-default ui-corner-top" ) + .attr({ + role: "tab", + tabIndex: -1 + }); + + this.anchors = this.tabs.map(function() { + return $( "a", this )[ 0 ]; + }) + .addClass( "ui-tabs-anchor" ) + .attr({ + role: "presentation", + tabIndex: -1 + }); + + this.panels = $(); + + this.anchors.each(function( i, anchor ) { + var selector, panel, panelId, + anchorId = $( anchor ).uniqueId().attr( "id" ), + tab = $( anchor ).closest( "li" ), + originalAriaControls = tab.attr( "aria-controls" ); + + // inline tab + if ( isLocal( anchor ) ) { + selector = anchor.hash; + panel = that.element.find( that._sanitizeSelector( selector ) ); + // remote tab + } else { + panelId = that._tabId( tab ); + selector = "#" + panelId; + panel = that.element.find( selector ); + if ( !panel.length ) { + panel = that._createPanel( panelId ); + panel.insertAfter( that.panels[ i - 1 ] || that.tablist ); + } + panel.attr( "aria-live", "polite" ); + } + + if ( panel.length) { + that.panels = that.panels.add( panel ); + } + if ( originalAriaControls ) { + tab.data( "ui-tabs-aria-controls", originalAriaControls ); + } + tab.attr({ + "aria-controls": selector.substring( 1 ), + "aria-labelledby": anchorId + }); + panel.attr( "aria-labelledby", anchorId ); + }); + + this.panels + .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) + .attr( "role", "tabpanel" ); + }, + + // allow overriding how to find the list for rare usage scenarios (#7715) + _getList: function() { + return this.element.find( "ol,ul" ).eq( 0 ); + }, + + _createPanel: function( id ) { + return $( "<div>" ) + .attr( "id", id ) + .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) + .data( "ui-tabs-destroy", true ); + }, + + _setupDisabled: function( disabled ) { + if ( $.isArray( disabled ) ) { + if ( !disabled.length ) { + disabled = false; + } else if ( disabled.length === this.anchors.length ) { + disabled = true; + } + } + + // disable tabs + for ( var i = 0, li; ( li = this.tabs[ i ] ); i++ ) { + if ( disabled === true || $.inArray( i, disabled ) !== -1 ) { + $( li ) + .addClass( "ui-state-disabled" ) + .attr( "aria-disabled", "true" ); + } else { + $( li ) + .removeClass( "ui-state-disabled" ) + .removeAttr( "aria-disabled" ); + } + } + + this.options.disabled = disabled; + }, + + _setupEvents: function( event ) { + var events = { + click: function( event ) { + event.preventDefault(); + } + }; + if ( event ) { + $.each( event.split(" "), function( index, eventName ) { + events[ eventName ] = "_eventHandler"; + }); + } + + this._off( this.anchors.add( this.tabs ).add( this.panels ) ); + this._on( this.anchors, events ); + this._on( this.tabs, { keydown: "_tabKeydown" } ); + this._on( this.panels, { keydown: "_panelKeydown" } ); + + this._focusable( this.tabs ); + this._hoverable( this.tabs ); + }, + + _setupHeightStyle: function( heightStyle ) { + var maxHeight, + parent = this.element.parent(); + + if ( heightStyle === "fill" ) { + maxHeight = parent.height(); + maxHeight -= this.element.outerHeight() - this.element.height(); + + this.element.siblings( ":visible" ).each(function() { + var elem = $( this ), + position = elem.css( "position" ); + + if ( position === "absolute" || position === "fixed" ) { + return; + } + maxHeight -= elem.outerHeight( true ); + }); + + this.element.children().not( this.panels ).each(function() { + maxHeight -= $( this ).outerHeight( true ); + }); + + this.panels.each(function() { + $( this ).height( Math.max( 0, maxHeight - + $( this ).innerHeight() + $( this ).height() ) ); + }) + .css( "overflow", "auto" ); + } else if ( heightStyle === "auto" ) { + maxHeight = 0; + this.panels.each(function() { + maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() ); + }).height( maxHeight ); + } + }, + + _eventHandler: function( event ) { + var options = this.options, + active = this.active, + anchor = $( event.currentTarget ), + tab = anchor.closest( "li" ), + clickedIsActive = tab[ 0 ] === active[ 0 ], + collapsing = clickedIsActive && options.collapsible, + toShow = collapsing ? $() : this._getPanelForTab( tab ), + toHide = !active.length ? $() : this._getPanelForTab( active ), + eventData = { + oldTab: active, + oldPanel: toHide, + newTab: collapsing ? $() : tab, + newPanel: toShow + }; + + event.preventDefault(); + + if ( tab.hasClass( "ui-state-disabled" ) || + // tab is already loading + tab.hasClass( "ui-tabs-loading" ) || + // can't switch durning an animation + this.running || + // click on active header, but not collapsible + ( clickedIsActive && !options.collapsible ) || + // allow canceling activation + ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { + return; + } + + options.active = collapsing ? false : this.tabs.index( tab ); + + this.active = clickedIsActive ? $() : tab; + if ( this.xhr ) { + this.xhr.abort(); + } + + if ( !toHide.length && !toShow.length ) { + $.error( "jQuery UI Tabs: Mismatching fragment identifier." ); + } + + if ( toShow.length ) { + this.load( this.tabs.index( tab ), event ); + } + this._toggle( event, eventData ); + }, + + // handles show/hide for selecting tabs + _toggle: function( event, eventData ) { + var that = this, + toShow = eventData.newPanel, + toHide = eventData.oldPanel; + + this.running = true; + + function complete() { + that.running = false; + that._trigger( "activate", event, eventData ); + } + + function show() { + eventData.newTab.closest( "li" ).addClass( "ui-tabs-active ui-state-active" ); + + if ( toShow.length && that.options.show ) { + that._show( toShow, that.options.show, complete ); + } else { + toShow.show(); + complete(); + } + } + + // start out by hiding, then showing, then completing + if ( toHide.length && this.options.hide ) { + this._hide( toHide, this.options.hide, function() { + eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" ); + show(); + }); + } else { + eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" ); + toHide.hide(); + show(); + } + + toHide.attr({ + "aria-expanded": "false", + "aria-hidden": "true" + }); + eventData.oldTab.attr( "aria-selected", "false" ); + // If we're switching tabs, remove the old tab from the tab order. + // If we're opening from collapsed state, remove the previous tab from the tab order. + // If we're collapsing, then keep the collapsing tab in the tab order. + if ( toShow.length && toHide.length ) { + eventData.oldTab.attr( "tabIndex", -1 ); + } else if ( toShow.length ) { + this.tabs.filter(function() { + return $( this ).attr( "tabIndex" ) === 0; + }) + .attr( "tabIndex", -1 ); + } + + toShow.attr({ + "aria-expanded": "true", + "aria-hidden": "false" + }); + eventData.newTab.attr({ + "aria-selected": "true", + tabIndex: 0 + }); + }, + + _activate: function( index ) { + var anchor, + active = this._findActive( index ); + + // trying to activate the already active panel + if ( active[ 0 ] === this.active[ 0 ] ) { + return; + } + + // trying to collapse, simulate a click on the current active header + if ( !active.length ) { + active = this.active; + } + + anchor = active.find( ".ui-tabs-anchor" )[ 0 ]; + this._eventHandler({ + target: anchor, + currentTarget: anchor, + preventDefault: $.noop + }); + }, + + _findActive: function( index ) { + return index === false ? $() : this.tabs.eq( index ); + }, + + _getIndex: function( index ) { + // meta-function to give users option to provide a href string instead of a numerical index. + if ( typeof index === "string" ) { + index = this.anchors.index( this.anchors.filter( "[href$='" + index + "']" ) ); + } + + return index; + }, + + _destroy: function() { + if ( this.xhr ) { + this.xhr.abort(); + } + + this.element.removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" ); + + this.tablist + .removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ) + .removeAttr( "role" ); + + this.anchors + .removeClass( "ui-tabs-anchor" ) + .removeAttr( "role" ) + .removeAttr( "tabIndex" ) + .removeUniqueId(); + + this.tabs.add( this.panels ).each(function() { + if ( $.data( this, "ui-tabs-destroy" ) ) { + $( this ).remove(); + } else { + $( this ) + .removeClass( "ui-state-default ui-state-active ui-state-disabled " + + "ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel" ) + .removeAttr( "tabIndex" ) + .removeAttr( "aria-live" ) + .removeAttr( "aria-busy" ) + .removeAttr( "aria-selected" ) + .removeAttr( "aria-labelledby" ) + .removeAttr( "aria-hidden" ) + .removeAttr( "aria-expanded" ) + .removeAttr( "role" ); + } + }); + + this.tabs.each(function() { + var li = $( this ), + prev = li.data( "ui-tabs-aria-controls" ); + if ( prev ) { + li + .attr( "aria-controls", prev ) + .removeData( "ui-tabs-aria-controls" ); + } else { + li.removeAttr( "aria-controls" ); + } + }); + + this.panels.show(); + + if ( this.options.heightStyle !== "content" ) { + this.panels.css( "height", "" ); + } + }, + + enable: function( index ) { + var disabled = this.options.disabled; + if ( disabled === false ) { + return; + } + + if ( index === undefined ) { + disabled = false; + } else { + index = this._getIndex( index ); + if ( $.isArray( disabled ) ) { + disabled = $.map( disabled, function( num ) { + return num !== index ? num : null; + }); + } else { + disabled = $.map( this.tabs, function( li, num ) { + return num !== index ? num : null; + }); + } + } + this._setupDisabled( disabled ); + }, + + disable: function( index ) { + var disabled = this.options.disabled; + if ( disabled === true ) { + return; + } + + if ( index === undefined ) { + disabled = true; + } else { + index = this._getIndex( index ); + if ( $.inArray( index, disabled ) !== -1 ) { + return; + } + if ( $.isArray( disabled ) ) { + disabled = $.merge( [ index ], disabled ).sort(); + } else { + disabled = [ index ]; + } + } + this._setupDisabled( disabled ); + }, + + load: function( index, event ) { + index = this._getIndex( index ); + var that = this, + tab = this.tabs.eq( index ), + anchor = tab.find( ".ui-tabs-anchor" ), + panel = this._getPanelForTab( tab ), + eventData = { + tab: tab, + panel: panel + }; + + // not remote + if ( isLocal( anchor[ 0 ] ) ) { + return; + } + + this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) ); + + // support: jQuery <1.8 + // jQuery <1.8 returns false if the request is canceled in beforeSend, + // but as of 1.8, $.ajax() always returns a jqXHR object. + if ( this.xhr && this.xhr.statusText !== "canceled" ) { + tab.addClass( "ui-tabs-loading" ); + panel.attr( "aria-busy", "true" ); + + this.xhr + .success(function( response ) { + // support: jQuery <1.8 + // http://bugs.jquery.com/ticket/11778 + setTimeout(function() { + panel.html( response ); + that._trigger( "load", event, eventData ); + }, 1 ); + }) + .complete(function( jqXHR, status ) { + // support: jQuery <1.8 + // http://bugs.jquery.com/ticket/11778 + setTimeout(function() { + if ( status === "abort" ) { + that.panels.stop( false, true ); + } + + tab.removeClass( "ui-tabs-loading" ); + panel.removeAttr( "aria-busy" ); + + if ( jqXHR === that.xhr ) { + delete that.xhr; + } + }, 1 ); + }); + } + }, + + _ajaxSettings: function( anchor, event, eventData ) { + var that = this; + return { + url: anchor.attr( "href" ), + beforeSend: function( jqXHR, settings ) { + return that._trigger( "beforeLoad", event, + $.extend( { jqXHR : jqXHR, ajaxSettings: settings }, eventData ) ); + } + }; + }, + + _getPanelForTab: function( tab ) { + var id = $( tab ).attr( "aria-controls" ); + return this.element.find( this._sanitizeSelector( "#" + id ) ); + } +}); + +})( jQuery ); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.tooltip.js b/extensions/jui/yii/jui/assets/jquery.ui.tooltip.js new file mode 100644 index 0000000..8ebf7b3 --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.tooltip.js @@ -0,0 +1,402 @@ +/*! + * jQuery UI Tooltip 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/tooltip/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + * jquery.ui.position.js + */ +(function( $ ) { + +var increments = 0; + +function addDescribedBy( elem, id ) { + var describedby = (elem.attr( "aria-describedby" ) || "").split( /\s+/ ); + describedby.push( id ); + elem + .data( "ui-tooltip-id", id ) + .attr( "aria-describedby", $.trim( describedby.join( " " ) ) ); +} + +function removeDescribedBy( elem ) { + var id = elem.data( "ui-tooltip-id" ), + describedby = (elem.attr( "aria-describedby" ) || "").split( /\s+/ ), + index = $.inArray( id, describedby ); + if ( index !== -1 ) { + describedby.splice( index, 1 ); + } + + elem.removeData( "ui-tooltip-id" ); + describedby = $.trim( describedby.join( " " ) ); + if ( describedby ) { + elem.attr( "aria-describedby", describedby ); + } else { + elem.removeAttr( "aria-describedby" ); + } +} + +$.widget( "ui.tooltip", { + version: "1.10.3", + options: { + content: function() { + // support: IE<9, Opera in jQuery <1.7 + // .text() can't accept undefined, so coerce to a string + var title = $( this ).attr( "title" ) || ""; + // Escape title, since we're going from an attribute to raw HTML + return $( "<a>" ).text( title ).html(); + }, + hide: true, + // Disabled elements have inconsistent behavior across browsers (#8661) + items: "[title]:not([disabled])", + position: { + my: "left top+15", + at: "left bottom", + collision: "flipfit flip" + }, + show: true, + tooltipClass: null, + track: false, + + // callbacks + close: null, + open: null + }, + + _create: function() { + this._on({ + mouseover: "open", + focusin: "open" + }); + + // IDs of generated tooltips, needed for destroy + this.tooltips = {}; + // IDs of parent tooltips where we removed the title attribute + this.parents = {}; + + if ( this.options.disabled ) { + this._disable(); + } + }, + + _setOption: function( key, value ) { + var that = this; + + if ( key === "disabled" ) { + this[ value ? "_disable" : "_enable" ](); + this.options[ key ] = value; + // disable element style changes + return; + } + + this._super( key, value ); + + if ( key === "content" ) { + $.each( this.tooltips, function( id, element ) { + that._updateContent( element ); + }); + } + }, + + _disable: function() { + var that = this; + + // close open tooltips + $.each( this.tooltips, function( id, element ) { + var event = $.Event( "blur" ); + event.target = event.currentTarget = element[0]; + that.close( event, true ); + }); + + // remove title attributes to prevent native tooltips + this.element.find( this.options.items ).addBack().each(function() { + var element = $( this ); + if ( element.is( "[title]" ) ) { + element + .data( "ui-tooltip-title", element.attr( "title" ) ) + .attr( "title", "" ); + } + }); + }, + + _enable: function() { + // restore title attributes + this.element.find( this.options.items ).addBack().each(function() { + var element = $( this ); + if ( element.data( "ui-tooltip-title" ) ) { + element.attr( "title", element.data( "ui-tooltip-title" ) ); + } + }); + }, + + open: function( event ) { + var that = this, + target = $( event ? event.target : this.element ) + // we need closest here due to mouseover bubbling, + // but always pointing at the same event target + .closest( this.options.items ); + + // No element to show a tooltip for or the tooltip is already open + if ( !target.length || target.data( "ui-tooltip-id" ) ) { + return; + } + + if ( target.attr( "title" ) ) { + target.data( "ui-tooltip-title", target.attr( "title" ) ); + } + + target.data( "ui-tooltip-open", true ); + + // kill parent tooltips, custom or native, for hover + if ( event && event.type === "mouseover" ) { + target.parents().each(function() { + var parent = $( this ), + blurEvent; + if ( parent.data( "ui-tooltip-open" ) ) { + blurEvent = $.Event( "blur" ); + blurEvent.target = blurEvent.currentTarget = this; + that.close( blurEvent, true ); + } + if ( parent.attr( "title" ) ) { + parent.uniqueId(); + that.parents[ this.id ] = { + element: this, + title: parent.attr( "title" ) + }; + parent.attr( "title", "" ); + } + }); + } + + this._updateContent( target, event ); + }, + + _updateContent: function( target, event ) { + var content, + contentOption = this.options.content, + that = this, + eventType = event ? event.type : null; + + if ( typeof contentOption === "string" ) { + return this._open( event, target, contentOption ); + } + + content = contentOption.call( target[0], function( response ) { + // ignore async response if tooltip was closed already + if ( !target.data( "ui-tooltip-open" ) ) { + return; + } + // IE may instantly serve a cached response for ajax requests + // delay this call to _open so the other call to _open runs first + that._delay(function() { + // jQuery creates a special event for focusin when it doesn't + // exist natively. To improve performance, the native event + // object is reused and the type is changed. Therefore, we can't + // rely on the type being correct after the event finished + // bubbling, so we set it back to the previous value. (#8740) + if ( event ) { + event.type = eventType; + } + this._open( event, target, response ); + }); + }); + if ( content ) { + this._open( event, target, content ); + } + }, + + _open: function( event, target, content ) { + var tooltip, events, delayedShow, + positionOption = $.extend( {}, this.options.position ); + + if ( !content ) { + return; + } + + // Content can be updated multiple times. If the tooltip already + // exists, then just update the content and bail. + tooltip = this._find( target ); + if ( tooltip.length ) { + tooltip.find( ".ui-tooltip-content" ).html( content ); + return; + } + + // if we have a title, clear it to prevent the native tooltip + // we have to check first to avoid defining a title if none exists + // (we don't want to cause an element to start matching [title]) + // + // We use removeAttr only for key events, to allow IE to export the correct + // accessible attributes. For mouse events, set to empty string to avoid + // native tooltip showing up (happens only when removing inside mouseover). + if ( target.is( "[title]" ) ) { + if ( event && event.type === "mouseover" ) { + target.attr( "title", "" ); + } else { + target.removeAttr( "title" ); + } + } + + tooltip = this._tooltip( target ); + addDescribedBy( target, tooltip.attr( "id" ) ); + tooltip.find( ".ui-tooltip-content" ).html( content ); + + function position( event ) { + positionOption.of = event; + if ( tooltip.is( ":hidden" ) ) { + return; + } + tooltip.position( positionOption ); + } + if ( this.options.track && event && /^mouse/.test( event.type ) ) { + this._on( this.document, { + mousemove: position + }); + // trigger once to override element-relative positioning + position( event ); + } else { + tooltip.position( $.extend({ + of: target + }, this.options.position ) ); + } + + tooltip.hide(); + + this._show( tooltip, this.options.show ); + // Handle tracking tooltips that are shown with a delay (#8644). As soon + // as the tooltip is visible, position the tooltip using the most recent + // event. + if ( this.options.show && this.options.show.delay ) { + delayedShow = this.delayedShow = setInterval(function() { + if ( tooltip.is( ":visible" ) ) { + position( positionOption.of ); + clearInterval( delayedShow ); + } + }, $.fx.interval ); + } + + this._trigger( "open", event, { tooltip: tooltip } ); + + events = { + keyup: function( event ) { + if ( event.keyCode === $.ui.keyCode.ESCAPE ) { + var fakeEvent = $.Event(event); + fakeEvent.currentTarget = target[0]; + this.close( fakeEvent, true ); + } + }, + remove: function() { + this._removeTooltip( tooltip ); + } + }; + if ( !event || event.type === "mouseover" ) { + events.mouseleave = "close"; + } + if ( !event || event.type === "focusin" ) { + events.focusout = "close"; + } + this._on( true, target, events ); + }, + + close: function( event ) { + var that = this, + target = $( event ? event.currentTarget : this.element ), + tooltip = this._find( target ); + + // disabling closes the tooltip, so we need to track when we're closing + // to avoid an infinite loop in case the tooltip becomes disabled on close + if ( this.closing ) { + return; + } + + // Clear the interval for delayed tracking tooltips + clearInterval( this.delayedShow ); + + // only set title if we had one before (see comment in _open()) + if ( target.data( "ui-tooltip-title" ) ) { + target.attr( "title", target.data( "ui-tooltip-title" ) ); + } + + removeDescribedBy( target ); + + tooltip.stop( true ); + this._hide( tooltip, this.options.hide, function() { + that._removeTooltip( $( this ) ); + }); + + target.removeData( "ui-tooltip-open" ); + this._off( target, "mouseleave focusout keyup" ); + // Remove 'remove' binding only on delegated targets + if ( target[0] !== this.element[0] ) { + this._off( target, "remove" ); + } + this._off( this.document, "mousemove" ); + + if ( event && event.type === "mouseleave" ) { + $.each( this.parents, function( id, parent ) { + $( parent.element ).attr( "title", parent.title ); + delete that.parents[ id ]; + }); + } + + this.closing = true; + this._trigger( "close", event, { tooltip: tooltip } ); + this.closing = false; + }, + + _tooltip: function( element ) { + var id = "ui-tooltip-" + increments++, + tooltip = $( "<div>" ) + .attr({ + id: id, + role: "tooltip" + }) + .addClass( "ui-tooltip ui-widget ui-corner-all ui-widget-content " + + ( this.options.tooltipClass || "" ) ); + $( "<div>" ) + .addClass( "ui-tooltip-content" ) + .appendTo( tooltip ); + tooltip.appendTo( this.document[0].body ); + this.tooltips[ id ] = element; + return tooltip; + }, + + _find: function( target ) { + var id = target.data( "ui-tooltip-id" ); + return id ? $( "#" + id ) : $(); + }, + + _removeTooltip: function( tooltip ) { + tooltip.remove(); + delete this.tooltips[ tooltip.attr( "id" ) ]; + }, + + _destroy: function() { + var that = this; + + // close open tooltips + $.each( this.tooltips, function( id, element ) { + // Delegate to close method to handle common cleanup + var event = $.Event( "blur" ); + event.target = event.currentTarget = element[0]; + that.close( event, true ); + + // Remove immediately; destroying an open tooltip doesn't use the + // hide animation + $( "#" + id ).remove(); + + // Restore the title + if ( element.data( "ui-tooltip-title" ) ) { + element.attr( "title", element.data( "ui-tooltip-title" ) ); + element.removeData( "ui-tooltip-title" ); + } + }); + } +}); + +}( jQuery ) ); diff --git a/extensions/jui/yii/jui/assets/jquery.ui.widget.js b/extensions/jui/yii/jui/assets/jquery.ui.widget.js new file mode 100644 index 0000000..916a6ad --- /dev/null +++ b/extensions/jui/yii/jui/assets/jquery.ui.widget.js @@ -0,0 +1,521 @@ +/*! + * jQuery UI Widget 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/jQuery.widget/ + */ +(function( $, undefined ) { + +var uuid = 0, + slice = Array.prototype.slice, + _cleanData = $.cleanData; +$.cleanData = function( elems ) { + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + try { + $( elem ).triggerHandler( "remove" ); + // http://bugs.jquery.com/ticket/8235 + } catch( e ) {} + } + _cleanData( elems ); +}; + +$.widget = function( name, base, prototype ) { + var fullName, existingConstructor, constructor, basePrototype, + // proxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + proxiedPrototype = {}, + namespace = name.split( "." )[ 0 ]; + + name = name.split( "." )[ 1 ]; + fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + // create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + // allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + // extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + // copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + // track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); + + basePrototype = new base(); + // we need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = (function() { + var _super = function() { + return base.prototype[ prop ].apply( this, arguments ); + }, + _superApply = function( args ) { + return base.prototype[ prop ].apply( this, args ); + }; + return function() { + var __super = this._super, + __superApply = this._superApply, + returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + }); + constructor.prototype = $.widget.extend( basePrototype, { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? basePrototype.widgetEventPrefix : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + }); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); + }); + // remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); +}; + +$.widget.extend = function( target ) { + var input = slice.call( arguments, 1 ), + inputIndex = 0, + inputLength = input.length, + key, + value; + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; +}; + +$.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string", + args = slice.call( arguments, 1 ), + returnValue = this; + + // allow multiple hashes to be passed on init + options = !isMethodCall && args.length ? + $.widget.extend.apply( null, [ options ].concat(args) ) : + options; + + if ( isMethodCall ) { + this.each(function() { + var methodValue, + instance = $.data( this, fullName ); + if ( !instance ) { + return $.error( "cannot call methods on " + name + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + " widget instance" ); + } + methodValue = instance[ options ].apply( instance, args ); + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + }); + } else { + this.each(function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} )._init(); + } else { + $.data( this, fullName, new object( options, this ) ); + } + }); + } + + return returnValue; + }; +}; + +$.Widget = function( /* options, element */ ) {}; +$.Widget._childConstructors = []; + +$.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "<div>", + options: { + disabled: false, + + // callbacks + create: null + }, + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = uuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + }); + this.document = $( element.style ? + // element within the document + element.ownerDocument : + // element is window or document + element.document || element ); + this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); + } + + this._create(); + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + _getCreateOptions: $.noop, + _getCreateEventData: $.noop, + _create: $.noop, + _init: $.noop, + + destroy: function() { + this._destroy(); + // we can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .unbind( this.eventNamespace ) + // 1.9 BC for #7810 + // TODO remove dual storage + .removeData( this.widgetName ) + .removeData( this.widgetFullName ) + // support: jquery <1.6.3 + // http://bugs.jquery.com/ticket/9413 + .removeData( $.camelCase( this.widgetFullName ) ); + this.widget() + .unbind( this.eventNamespace ) + .removeAttr( "aria-disabled" ) + .removeClass( + this.widgetFullName + "-disabled " + + "ui-state-disabled" ); + + // clean up events and states + this.bindings.unbind( this.eventNamespace ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + }, + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key, + parts, + curOption, + i; + + if ( arguments.length === 0 ) { + // don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( value === undefined ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( value === undefined ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + _setOption: function( key, value ) { + this.options[ key ] = value; + + if ( key === "disabled" ) { + this.widget() + .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) + .attr( "aria-disabled", value ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + } + + return this; + }, + + enable: function() { + return this._setOption( "disabled", false ); + }, + disable: function() { + return this._setOption( "disabled", true ); + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement, + instance = this; + + // no suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // no element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + // accept selectors, DOM elements + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + // allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^(\w+)\s*(.*)$/ ), + eventName = match[1] + instance.eventNamespace, + selector = match[2]; + if ( selector ) { + delegateElement.delegate( selector, eventName, handlerProxy ); + } else { + element.bind( eventName, handlerProxy ); + } + }); + }, + + _off: function( element, eventName ) { + eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; + element.unbind( eventName ).undelegate( eventName ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + $( event.currentTarget ).addClass( "ui-state-hover" ); + }, + mouseleave: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-hover" ); + } + }); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + $( event.currentTarget ).addClass( "ui-state-focus" ); + }, + focusout: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-focus" ); + } + }); + }, + + _trigger: function( type, event, data ) { + var prop, orig, + callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + // the original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[0], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } +}; + +$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + var hasOptions, + effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + if ( options.delay ) { + element.delay( options.delay ); + } + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue(function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + }); + } + }; +}); + +})( jQuery ); diff --git a/extensions/jui/yii/jui/assets/theme/images/animated-overlay.gif b/extensions/jui/yii/jui/assets/theme/images/animated-overlay.gif new file mode 100755 index 0000000..d441f75 Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/animated-overlay.gif differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-bg_flat_0_aaaaaa_40x100.png b/extensions/jui/yii/jui/assets/theme/images/ui-bg_flat_0_aaaaaa_40x100.png new file mode 100755 index 0000000..414803b Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-bg_flat_0_aaaaaa_40x100.png differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-bg_flat_75_ffffff_40x100.png b/extensions/jui/yii/jui/assets/theme/images/ui-bg_flat_75_ffffff_40x100.png new file mode 100755 index 0000000..462409f Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-bg_flat_75_ffffff_40x100.png differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_55_fbf9ee_1x400.png b/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_55_fbf9ee_1x400.png new file mode 100755 index 0000000..6b78278 Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_55_fbf9ee_1x400.png differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_65_ffffff_1x400.png b/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_65_ffffff_1x400.png new file mode 100755 index 0000000..b706fba Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_65_ffffff_1x400.png differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_75_dadada_1x400.png b/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_75_dadada_1x400.png new file mode 100755 index 0000000..b650a7c Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_75_dadada_1x400.png differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_75_e6e6e6_1x400.png b/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_75_e6e6e6_1x400.png new file mode 100755 index 0000000..e70ab8f Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_75_e6e6e6_1x400.png differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_95_fef1ec_1x400.png b/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_95_fef1ec_1x400.png new file mode 100755 index 0000000..6e86fb1 Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_95_fef1ec_1x400.png differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/extensions/jui/yii/jui/assets/theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png new file mode 100755 index 0000000..93ad9a6 Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-icons_222222_256x240.png b/extensions/jui/yii/jui/assets/theme/images/ui-icons_222222_256x240.png new file mode 100755 index 0000000..c1cb117 Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-icons_222222_256x240.png differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-icons_2e83ff_256x240.png b/extensions/jui/yii/jui/assets/theme/images/ui-icons_2e83ff_256x240.png new file mode 100755 index 0000000..84b601b Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-icons_2e83ff_256x240.png differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-icons_454545_256x240.png b/extensions/jui/yii/jui/assets/theme/images/ui-icons_454545_256x240.png new file mode 100755 index 0000000..b6db1ac Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-icons_454545_256x240.png differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-icons_888888_256x240.png b/extensions/jui/yii/jui/assets/theme/images/ui-icons_888888_256x240.png new file mode 100755 index 0000000..feea0e2 Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-icons_888888_256x240.png differ diff --git a/extensions/jui/yii/jui/assets/theme/images/ui-icons_cd0a0a_256x240.png b/extensions/jui/yii/jui/assets/theme/images/ui-icons_cd0a0a_256x240.png new file mode 100755 index 0000000..ed5b6b0 Binary files /dev/null and b/extensions/jui/yii/jui/assets/theme/images/ui-icons_cd0a0a_256x240.png differ diff --git a/extensions/jui/yii/jui/assets/theme/jquery.ui.css b/extensions/jui/yii/jui/assets/theme/jquery.ui.css new file mode 100755 index 0000000..105197e --- /dev/null +++ b/extensions/jui/yii/jui/assets/theme/jquery.ui.css @@ -0,0 +1,1177 @@ +/*! jQuery UI - v1.10.3 - 2013-07-18 +* http://jqueryui.com +* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.accordion.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.slider.css, jquery.ui.spinner.css, jquery.ui.tabs.css, jquery.ui.tooltip.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px +* Copyright 2013 jQuery Foundation and other contributors Licensed MIT */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} +.ui-helper-clearfix:before, +.ui-helper-clearfix:after { + content: ""; + display: table; + border-collapse: collapse; +} +.ui-helper-clearfix:after { + clear: both; +} +.ui-helper-clearfix { + min-height: 0; /* support: IE7 */ +} +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; + filter:Alpha(Opacity=0); +} + +.ui-front { + z-index: 100; +} + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { + cursor: default !important; +} + + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} + + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.ui-resizable { + position: relative; +} +.ui-resizable-handle { + position: absolute; + font-size: 0.1px; + display: block; +} +.ui-resizable-disabled .ui-resizable-handle, +.ui-resizable-autohide .ui-resizable-handle { + display: none; +} +.ui-resizable-n { + cursor: n-resize; + height: 7px; + width: 100%; + top: -5px; + left: 0; +} +.ui-resizable-s { + cursor: s-resize; + height: 7px; + width: 100%; + bottom: -5px; + left: 0; +} +.ui-resizable-e { + cursor: e-resize; + width: 7px; + right: -5px; + top: 0; + height: 100%; +} +.ui-resizable-w { + cursor: w-resize; + width: 7px; + left: -5px; + top: 0; + height: 100%; +} +.ui-resizable-se { + cursor: se-resize; + width: 12px; + height: 12px; + right: 1px; + bottom: 1px; +} +.ui-resizable-sw { + cursor: sw-resize; + width: 9px; + height: 9px; + left: -5px; + bottom: -5px; +} +.ui-resizable-nw { + cursor: nw-resize; + width: 9px; + height: 9px; + left: -5px; + top: -5px; +} +.ui-resizable-ne { + cursor: ne-resize; + width: 9px; + height: 9px; + right: -5px; + top: -5px; +} +.ui-selectable-helper { + position: absolute; + z-index: 100; + border: 1px dotted black; +} +.ui-accordion .ui-accordion-header { + display: block; + cursor: pointer; + position: relative; + margin-top: 2px; + padding: .5em .5em .5em .7em; + min-height: 0; /* support: IE7 */ +} +.ui-accordion .ui-accordion-icons { + padding-left: 2.2em; +} +.ui-accordion .ui-accordion-noicons { + padding-left: .7em; +} +.ui-accordion .ui-accordion-icons .ui-accordion-icons { + padding-left: 2.2em; +} +.ui-accordion .ui-accordion-header .ui-accordion-header-icon { + position: absolute; + left: .5em; + top: 50%; + margin-top: -8px; +} +.ui-accordion .ui-accordion-content { + padding: 1em 2.2em; + border-top: 0; + overflow: auto; +} +.ui-autocomplete { + position: absolute; + top: 0; + left: 0; + cursor: default; +} +.ui-button { + display: inline-block; + position: relative; + padding: 0; + line-height: normal; + margin-right: .1em; + cursor: pointer; + vertical-align: middle; + text-align: center; + overflow: visible; /* removes extra width in IE */ +} +.ui-button, +.ui-button:link, +.ui-button:visited, +.ui-button:hover, +.ui-button:active { + text-decoration: none; +} +/* to make room for the icon, a width needs to be set here */ +.ui-button-icon-only { + width: 2.2em; +} +/* button elements seem to need a little more width */ +button.ui-button-icon-only { + width: 2.4em; +} +.ui-button-icons-only { + width: 3.4em; +} +button.ui-button-icons-only { + width: 3.7em; +} + +/* button text element */ +.ui-button .ui-button-text { + display: block; + line-height: normal; +} +.ui-button-text-only .ui-button-text { + padding: .4em 1em; +} +.ui-button-icon-only .ui-button-text, +.ui-button-icons-only .ui-button-text { + padding: .4em; + text-indent: -9999999px; +} +.ui-button-text-icon-primary .ui-button-text, +.ui-button-text-icons .ui-button-text { + padding: .4em 1em .4em 2.1em; +} +.ui-button-text-icon-secondary .ui-button-text, +.ui-button-text-icons .ui-button-text { + padding: .4em 2.1em .4em 1em; +} +.ui-button-text-icons .ui-button-text { + padding-left: 2.1em; + padding-right: 2.1em; +} +/* no icon support for input elements, provide padding by default */ +input.ui-button { + padding: .4em 1em; +} + +/* button icon element(s) */ +.ui-button-icon-only .ui-icon, +.ui-button-text-icon-primary .ui-icon, +.ui-button-text-icon-secondary .ui-icon, +.ui-button-text-icons .ui-icon, +.ui-button-icons-only .ui-icon { + position: absolute; + top: 50%; + margin-top: -8px; +} +.ui-button-icon-only .ui-icon { + left: 50%; + margin-left: -8px; +} +.ui-button-text-icon-primary .ui-button-icon-primary, +.ui-button-text-icons .ui-button-icon-primary, +.ui-button-icons-only .ui-button-icon-primary { + left: .5em; +} +.ui-button-text-icon-secondary .ui-button-icon-secondary, +.ui-button-text-icons .ui-button-icon-secondary, +.ui-button-icons-only .ui-button-icon-secondary { + right: .5em; +} + +/* button sets */ +.ui-buttonset { + margin-right: 7px; +} +.ui-buttonset .ui-button { + margin-left: 0; + margin-right: -.3em; +} + +/* workarounds */ +/* reset extra padding in Firefox, see h5bp.com/l */ +input.ui-button::-moz-focus-inner, +button.ui-button::-moz-focus-inner { + border: 0; + padding: 0; +} +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; + display: none; +} +.ui-datepicker .ui-datepicker-header { + position: relative; + padding: .2em 0; +} +.ui-datepicker .ui-datepicker-prev, +.ui-datepicker .ui-datepicker-next { + position: absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} +.ui-datepicker .ui-datepicker-prev-hover, +.ui-datepicker .ui-datepicker-next-hover { + top: 1px; +} +.ui-datepicker .ui-datepicker-prev { + left: 2px; +} +.ui-datepicker .ui-datepicker-next { + right: 2px; +} +.ui-datepicker .ui-datepicker-prev-hover { + left: 1px; +} +.ui-datepicker .ui-datepicker-next-hover { + right: 1px; +} +.ui-datepicker .ui-datepicker-prev span, +.ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} +.ui-datepicker .ui-datepicker-title select { + font-size: 1em; + margin: 1px 0; +} +.ui-datepicker select.ui-datepicker-month-year { + width: 100%; +} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { + width: 49%; +} +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin: 0 0 .4em; +} +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} +.ui-datepicker td { + border: 0; + padding: 1px; +} +.ui-datepicker td span, +.ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding: 0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width: auto; + overflow: visible; +} +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float: left; +} + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { + width: auto; +} +.ui-datepicker-multi .ui-datepicker-group { + float: left; +} +.ui-datepicker-multi .ui-datepicker-group table { + width: 95%; + margin: 0 auto .4em; +} +.ui-datepicker-multi-2 .ui-datepicker-group { + width: 50%; +} +.ui-datepicker-multi-3 .ui-datepicker-group { + width: 33.3%; +} +.ui-datepicker-multi-4 .ui-datepicker-group { + width: 25%; +} +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width: 0; +} +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear: left; +} +.ui-datepicker-row-break { + clear: both; + width: 100%; + font-size: 0; +} + +/* RTL support */ +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear: right; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, +.ui-datepicker-rtl .ui-datepicker-group { + float: right; +} +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} +.ui-dialog { + position: absolute; + top: 0; + left: 0; + padding: .2em; + outline: 0; +} +.ui-dialog .ui-dialog-titlebar { + padding: .4em 1em; + position: relative; +} +.ui-dialog .ui-dialog-title { + float: left; + margin: .1em 0; + white-space: nowrap; + width: 90%; + overflow: hidden; + text-overflow: ellipsis; +} +.ui-dialog .ui-dialog-titlebar-close { + position: absolute; + right: .3em; + top: 50%; + width: 21px; + margin: -10px 0 0 0; + padding: 1px; + height: 20px; +} +.ui-dialog .ui-dialog-content { + position: relative; + border: 0; + padding: .5em 1em; + background: none; + overflow: auto; +} +.ui-dialog .ui-dialog-buttonpane { + text-align: left; + border-width: 1px 0 0 0; + background-image: none; + margin-top: .5em; + padding: .3em 1em .5em .4em; +} +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { + float: right; +} +.ui-dialog .ui-dialog-buttonpane button { + margin: .5em .4em .5em 0; + cursor: pointer; +} +.ui-dialog .ui-resizable-se { + width: 12px; + height: 12px; + right: -5px; + bottom: -5px; + background-position: 16px 16px; +} +.ui-draggable .ui-dialog-titlebar { + cursor: move; +} +.ui-menu { + list-style: none; + padding: 2px; + margin: 0; + display: block; + outline: none; +} +.ui-menu .ui-menu { + margin-top: -3px; + position: absolute; +} +.ui-menu .ui-menu-item { + margin: 0; + padding: 0; + width: 100%; + /* support: IE10, see #8844 */ + list-style-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7); +} +.ui-menu .ui-menu-divider { + margin: 5px -2px 5px -2px; + height: 0; + font-size: 0; + line-height: 0; + border-width: 1px 0 0 0; +} +.ui-menu .ui-menu-item a { + text-decoration: none; + display: block; + padding: 2px .4em; + line-height: 1.5; + min-height: 0; /* support: IE7 */ + font-weight: normal; +} +.ui-menu .ui-menu-item a.ui-state-focus, +.ui-menu .ui-menu-item a.ui-state-active { + font-weight: normal; + margin: -1px; +} + +.ui-menu .ui-state-disabled { + font-weight: normal; + margin: .4em 0 .2em; + line-height: 1.5; +} +.ui-menu .ui-state-disabled a { + cursor: default; +} + +/* icon support */ +.ui-menu-icons { + position: relative; +} +.ui-menu-icons .ui-menu-item a { + position: relative; + padding-left: 2em; +} + +/* left-aligned */ +.ui-menu .ui-icon { + position: absolute; + top: .2em; + left: .2em; +} + +/* right-aligned */ +.ui-menu .ui-menu-icon { + position: static; + float: right; +} +.ui-progressbar { + height: 2em; + text-align: left; + overflow: hidden; +} +.ui-progressbar .ui-progressbar-value { + margin: -1px; + height: 100%; +} +.ui-progressbar .ui-progressbar-overlay { + background: url("images/animated-overlay.gif"); + height: 100%; + filter: alpha(opacity=25); + opacity: 0.25; +} +.ui-progressbar-indeterminate .ui-progressbar-value { + background-image: none; +} +.ui-slider { + position: relative; + text-align: left; +} +.ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 1.2em; + height: 1.2em; + cursor: default; +} +.ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + font-size: .7em; + display: block; + border: 0; + background-position: 0 0; +} + +/* For IE8 - See #6727 */ +.ui-slider.ui-state-disabled .ui-slider-handle, +.ui-slider.ui-state-disabled .ui-slider-range { + filter: inherit; +} + +.ui-slider-horizontal { + height: .8em; +} +.ui-slider-horizontal .ui-slider-handle { + top: -.3em; + margin-left: -.6em; +} +.ui-slider-horizontal .ui-slider-range { + top: 0; + height: 100%; +} +.ui-slider-horizontal .ui-slider-range-min { + left: 0; +} +.ui-slider-horizontal .ui-slider-range-max { + right: 0; +} + +.ui-slider-vertical { + width: .8em; + height: 100px; +} +.ui-slider-vertical .ui-slider-handle { + left: -.3em; + margin-left: 0; + margin-bottom: -.6em; +} +.ui-slider-vertical .ui-slider-range { + left: 0; + width: 100%; +} +.ui-slider-vertical .ui-slider-range-min { + bottom: 0; +} +.ui-slider-vertical .ui-slider-range-max { + top: 0; +} +.ui-spinner { + position: relative; + display: inline-block; + overflow: hidden; + padding: 0; + vertical-align: middle; +} +.ui-spinner-input { + border: none; + background: none; + color: inherit; + padding: 0; + margin: .2em 0; + vertical-align: middle; + margin-left: .4em; + margin-right: 22px; +} +.ui-spinner-button { + width: 16px; + height: 50%; + font-size: .5em; + padding: 0; + margin: 0; + text-align: center; + position: absolute; + cursor: default; + display: block; + overflow: hidden; + right: 0; +} +/* more specificity required here to overide default borders */ +.ui-spinner a.ui-spinner-button { + border-top: none; + border-bottom: none; + border-right: none; +} +/* vertical centre icon */ +.ui-spinner .ui-icon { + position: absolute; + margin-top: -8px; + top: 50%; + left: 0; +} +.ui-spinner-up { + top: 0; +} +.ui-spinner-down { + bottom: 0; +} + +/* TR overrides */ +.ui-spinner .ui-icon-triangle-1-s { + /* need to fix icons sprite */ + background-position: -65px -16px; +} +.ui-tabs { + position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ + padding: .2em; +} +.ui-tabs .ui-tabs-nav { + margin: 0; + padding: .2em .2em 0; +} +.ui-tabs .ui-tabs-nav li { + list-style: none; + float: left; + position: relative; + top: 0; + margin: 1px .2em 0 0; + border-bottom-width: 0; + padding: 0; + white-space: nowrap; +} +.ui-tabs .ui-tabs-nav li a { + float: left; + padding: .5em 1em; + text-decoration: none; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active { + margin-bottom: -1px; + padding-bottom: 1px; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active a, +.ui-tabs .ui-tabs-nav li.ui-state-disabled a, +.ui-tabs .ui-tabs-nav li.ui-tabs-loading a { + cursor: text; +} +.ui-tabs .ui-tabs-nav li a, /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ +.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active a { + cursor: pointer; +} +.ui-tabs .ui-tabs-panel { + display: block; + border-width: 0; + padding: 1em 1.4em; + background: none; +} +.ui-tooltip { + padding: 8px; + position: absolute; + z-index: 9999; + max-width: 300px; + -webkit-box-shadow: 0 0 5px #aaa; + box-shadow: 0 0 5px #aaa; +} +body .ui-tooltip { + border-width: 2px; +} + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Verdana,Arial,sans-serif; + font-size: 1.1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Verdana,Arial,sans-serif; + font-size: 1em; +} +.ui-widget-content { + border: 1px solid #aaaaaa; + background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; + color: #222222; +} +.ui-widget-content a { + color: #222222; +} +.ui-widget-header { + border: 1px solid #aaaaaa; + background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; + color: #222222; + font-weight: bold; +} +.ui-widget-header a { + color: #222222; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default { + border: 1px solid #d3d3d3; + background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x; + font-weight: normal; + color: #555555; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited { + color: #555555; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus { + border: 1px solid #999999; + background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x; + font-weight: normal; + color: #212121; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited { + color: #212121; + text-decoration: none; +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active { + border: 1px solid #aaaaaa; + background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; + font-weight: normal; + color: #212121; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #212121; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #fcefa1; + background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; + color: #363636; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #363636; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #cd0a0a; + background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; + color: #cd0a0a; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #cd0a0a; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #cd0a0a; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* For IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url(images/ui-icons_222222_256x240.png); +} +.ui-widget-header .ui-icon { + background-image: url(images/ui-icons_222222_256x240.png); +} +.ui-state-default .ui-icon { + background-image: url(images/ui-icons_888888_256x240.png); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon { + background-image: url(images/ui-icons_454545_256x240.png); +} +.ui-state-active .ui-icon { + background-image: url(images/ui-icons_454545_256x240.png); +} +.ui-state-highlight .ui-icon { + background-image: url(images/ui-icons_2e83ff_256x240.png); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url(images/ui-icons_cd0a0a_256x240.png); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 4px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; + opacity: .3; + filter: Alpha(Opacity=30); +} +.ui-widget-shadow { + margin: -8px 0 0 -8px; + padding: 8px; + background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; + opacity: .3; + filter: Alpha(Opacity=30); + border-radius: 8px; +} diff --git a/extensions/mutex/LICENSE.md b/extensions/mutex/LICENSE.md new file mode 100644 index 0000000..0bb1a8d --- /dev/null +++ b/extensions/mutex/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/extensions/mutex/README.md b/extensions/mutex/README.md new file mode 100644 index 0000000..161ee8a --- /dev/null +++ b/extensions/mutex/README.md @@ -0,0 +1,42 @@ +Yii 2.0 Public Preview - Mutex Extension +======================================== + +Thank you for choosing Yii - a high-performance component-based PHP framework. + +If you are looking for a production-ready PHP framework, please use +[Yii v1.1](https://github.com/yiisoft/yii). + +Yii 2.0 is still under heavy development. We may make significant changes +without prior notices. **Yii 2.0 is not ready for production use yet.** + +[](http://travis-ci.org/yiisoft/yii2) + +This is the yii2-mutex extension. + + +Installation +------------ + +The prefered way to install this extension is through [composer](http://getcomposer.org/download/). + +Either run +``` +php composer.phar require yiisoft/yii2-mutex "*" +``` + +or add +``` +"yiisoft/yii2-mutex": "*" +``` +to the require section of your composer.json. + + +*Note: You might have to run `php composer.phar selfupdate`* + + +Usage & Documentation +--------------------- + +This component can be used to perform actions similar to the concept of [mutual exclusion](http://en.wikipedia.org/wiki/Mutual_exclusion). + +For concrete examples and advanced usage refer to the yii guide. diff --git a/extensions/mutex/composer.json b/extensions/mutex/composer.json new file mode 100644 index 0000000..2fb95c4 --- /dev/null +++ b/extensions/mutex/composer.json @@ -0,0 +1,27 @@ +{ + "name": "yiisoft/yii2-mutex", + "description": "Mutual exclusion extension for the Yii framework", + "keywords": ["yii", "mutex"], + "type": "library", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "authors": [ + { + "name": "resurtm", + "email": "resurtm@gmail.com" + } + ], + "minimum-stability": "dev", + "require": { + "yiisoft/yii2": "*" + }, + "autoload": { + "psr-0": { "yii\\mutex\\": "" } + } +} diff --git a/extensions/mutex/yii/mutex/DbMutex.php b/extensions/mutex/yii/mutex/DbMutex.php new file mode 100644 index 0000000..3699c36 --- /dev/null +++ b/extensions/mutex/yii/mutex/DbMutex.php @@ -0,0 +1,41 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\mutex; + +use Yii; +use yii\db\Connection; +use yii\base\InvalidConfigException; + +/** + * @author resurtm <resurtm@gmail.com> + * @since 2.0 + */ +abstract class DbMutex extends Mutex +{ + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the Mutex object is created, if you want to change this property, you should only assign + * it with a DB connection object. + */ + public $db = 'db'; + + /** + * Initializes generic database table based mutex implementation. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException('Mutex::db must be either a DB connection instance or the application component ID of a DB connection.'); + } + } +} diff --git a/extensions/mutex/yii/mutex/FileMutex.php b/extensions/mutex/yii/mutex/FileMutex.php new file mode 100644 index 0000000..5ac9262 --- /dev/null +++ b/extensions/mutex/yii/mutex/FileMutex.php @@ -0,0 +1,104 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\mutex; + +use Yii; +use yii\base\InvalidConfigException; +use yii\helpers\FileHelper; + +/** + * @author resurtm <resurtm@gmail.com> + * @since 2.0 + */ +class FileMutex extends Mutex +{ + /** + * @var string the directory to store mutex files. You may use path alias here. + * Defaults to the "mutex" subdirectory under the application runtime path. + */ + public $mutexPath = '@runtime/mutex'; + /** + * @var integer the permission to be set for newly created mutex files. + * This value will be used by PHP chmod() function. No umask will be applied. + * If not set, the permission will be determined by the current environment. + */ + public $fileMode; + /** + * @var integer the permission to be set for newly created directories. + * This value will be used by PHP chmod() function. No umask will be applied. + * Defaults to 0775, meaning the directory is read-writable by owner and group, + * but read-only for other users. + */ + public $dirMode = 0775; + /** + * @var resource[] stores all opened lock files. Keys are lock names and values are file handles. + */ + private $_files = array(); + + + /** + * Initializes mutex component implementation dedicated for UNIX, GNU/Linux, Mac OS X, and other UNIX-like + * operating systems. + * @throws InvalidConfigException + */ + public function init() + { + if (stripos(php_uname('s'), 'win') === 0) { + throw new InvalidConfigException('FileMutex does not have MS Windows operating system support.'); + } + $this->mutexPath = Yii::getAlias($this->mutexPath); + if (!is_dir($this->mutexPath)) { + FileHelper::createDirectory($this->mutexPath, $this->dirMode, true); + } + } + + /** + * Acquires lock by given name. + * @param string $name of the lock to be acquired. + * @param integer $timeout to wait for lock to become released. + * @return boolean acquiring result. + */ + protected function acquireLock($name, $timeout = 0) + { + $fileName = $this->mutexPath . '/' . md5($name) . '.lock'; + $file = fopen($fileName, 'w+'); + if ($file === false) { + return false; + } + if ($this->fileMode !== null) { + @chmod($fileName, $this->fileMode); + } + $waitTime = 0; + while (!flock($file, LOCK_EX | LOCK_NB)) { + $waitTime++; + if ($waitTime > $timeout) { + fclose($file); + return false; + } + sleep(1); + } + $this->_files[$name] = $file; + return true; + } + + /** + * Releases lock by given name. + * @param string $name of the lock to be released. + * @return boolean release result. + */ + protected function releaseLock($name) + { + if (!isset($this->_files[$name]) || !flock($this->_files[$name], LOCK_UN)) { + return false; + } else { + fclose($this->_files[$name]); + unset($this->_files[$name]); + return true; + } + } +} diff --git a/extensions/mutex/yii/mutex/Mutex.php b/extensions/mutex/yii/mutex/Mutex.php new file mode 100644 index 0000000..db14ce8 --- /dev/null +++ b/extensions/mutex/yii/mutex/Mutex.php @@ -0,0 +1,95 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\mutex; + +use Yii; +use yii\base\Component; + +/** + * @author resurtm <resurtm@gmail.com> + * @since 2.0 + */ +abstract class Mutex extends Component +{ + /** + * @var boolean whether all locks acquired in this process (i.e. local locks) must be released automagically + * before finishing script execution. Defaults to true. Setting this property to true means that all locks + * acquire in this process must be released in any case (regardless any kind of errors or exceptions). + */ + public $autoRelease = true; + /** + * @var string[] names of the locks acquired in the current PHP process. + */ + private $_locks = array(); + + + /** + * Initializes the mutex component. + */ + public function init() + { + if ($this->autoRelease) { + $locks = &$this->_locks; + register_shutdown_function(function () use ($this, &$locks) { + foreach ($locks as $lock) { + $this->release($lock); + } + }); + } + } + + /** + * Acquires lock by given name. + * @param string $name of the lock to be acquired. Must be unique. + * @param integer $timeout to wait for lock to be released. Defaults to zero meaning that method will return + * false immediately in case lock was already acquired. + * @return boolean lock acquiring result. + */ + public function acquire($name, $timeout = 0) + { + if ($this->acquireLock($name, $timeout)) { + $this->_locks[] = $name; + return true; + } else { + return false; + } + } + + /** + * Release acquired lock. This method will return false in case named lock was not found. + * @param string $name of the lock to be released. This lock must be already created. + * @return boolean lock release result: false in case named lock was not found.. + */ + public function release($name) + { + if ($this->releaseLock($name)) { + $index = array_search($name, $this->_locks); + if ($index !== false) { + unset($this->_locks[$index]); + } + return true; + } else { + return false; + } + } + + /** + * This method should be extended by concrete mutex implementations. Acquires lock by given name. + * @param string $name of the lock to be acquired. + * @param integer $timeout to wait for lock to become released. + * @return boolean acquiring result. + */ + abstract protected function acquireLock($name, $timeout = 0); + + /** + * This method should be extended by concrete mutex implementations. Releases lock by given name. + * @param string $name of the lock to be released. + * @return boolean release result. + */ + abstract protected function releaseLock($name); +} diff --git a/extensions/mutex/yii/mutex/MysqlMutex.php b/extensions/mutex/yii/mutex/MysqlMutex.php new file mode 100644 index 0000000..f1c7cd1 --- /dev/null +++ b/extensions/mutex/yii/mutex/MysqlMutex.php @@ -0,0 +1,57 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\mutex; + +use Yii; +use yii\base\InvalidConfigException; + +/** + * @author resurtm <resurtm@gmail.com> + * @since 2.0 + */ +class MysqlMutex extends Mutex +{ + /** + * Initializes MySQL specific mutex component implementation. + * @throws InvalidConfigException if [[db]] is not MySQL connection. + */ + public function init() + { + parent::init(); + if ($this->db->driverName !== 'mysql') { + throw new InvalidConfigException('In order to use MysqlMutex connection must be configured to use MySQL database.'); + } + } + + /** + * Acquires lock by given name. + * @param string $name of the lock to be acquired. + * @param integer $timeout to wait for lock to become released. + * @return boolean acquiring result. + * @see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock + */ + protected function acquireLock($name, $timeout = 0) + { + return (boolean)$this->db + ->createCommand('SELECT GET_LOCK(:name, :timeout)', array(':name' => $name, ':timeout' => $timeout)) + ->queryScalar(); + } + + /** + * Releases lock by given name. + * @param string $name of the lock to be released. + * @return boolean release result. + * @see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock + */ + protected function releaseLock($name) + { + return (boolean)$this->db + ->createCommand('SELECT RELEASE_LOCK(:name)', array(':name' => $name)) + ->queryScalar(); + } +} diff --git a/extensions/smarty/LICENSE.md b/extensions/smarty/LICENSE.md new file mode 100644 index 0000000..0bb1a8d --- /dev/null +++ b/extensions/smarty/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/extensions/smarty/README.md b/extensions/smarty/README.md new file mode 100644 index 0000000..adf764f --- /dev/null +++ b/extensions/smarty/README.md @@ -0,0 +1,64 @@ +Yii 2.0 Public Preview - Smarty View Renderer +============================================= + +Thank you for choosing Yii - a high-performance component-based PHP framework. + +If you are looking for a production-ready PHP framework, please use +[Yii v1.1](https://github.com/yiisoft/yii). + +Yii 2.0 is still under heavy development. We may make significant changes +without prior notices. **Yii 2.0 is not ready for production use yet.** + +[](http://travis-ci.org/yiisoft/yii2) + +This is the yii2-smarty extension. + + +Installation +------------ + +The prefered way to install this extension is through [composer](http://getcomposer.org/download/). + +Either run +``` +php composer.phar require yiisoft/yii2-smarty "*" +``` + +or add +```json +"yiisoft/yii2-smarty": "*" +``` +to the require section of your composer.json. + + +*Note: You might have to run `php composer.phar selfupdate`* + + +Usage & Documentation +--------------------- + +This extension has to be registered prior to usage. +To enable this view renderer add it to the $rendereres property of your view object. + +Example: + +```php +<?php +// config.php +return array( + //.... + 'components' => array( + 'view' => array( + 'class' => 'yii\base\View', + 'renderers' => array( + 'tpl' => array( + 'class' => 'yii\smarty\ViewRenderer', + //'cachePath' => '@runtime/Smarty/cache', + ), + ), + ), + ), +); +``` + +For further instructions refer to the related section in the yii guide. diff --git a/extensions/smarty/composer.json b/extensions/smarty/composer.json new file mode 100644 index 0000000..333e76e --- /dev/null +++ b/extensions/smarty/composer.json @@ -0,0 +1,28 @@ +{ + "name": "yiisoft/yii2-smarty", + "description": "The Smarty integration for the Yii framework", + "keywords": ["yii", "smarty", "renderer"], + "type": "library", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "authors": [ + { + "name": "Alenxader Makarov", + "email": "sam@rmcreative.ru" + } + ], + "minimum-stability": "dev", + "require": { + "yiisoft/yii2": "*", + "smarty/smarty": ">=v3.1.13" + }, + "autoload": { + "psr-0": { "yii\\smarty\\": "" } + } +} diff --git a/yii/renderers/SmartyViewRenderer.php b/extensions/smarty/yii/smarty/ViewRenderer.php similarity index 92% rename from yii/renderers/SmartyViewRenderer.php rename to extensions/smarty/yii/smarty/ViewRenderer.php index c6147c0..7a9554e 100644 --- a/yii/renderers/SmartyViewRenderer.php +++ b/extensions/smarty/yii/smarty/ViewRenderer.php @@ -7,13 +7,13 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\renderers; +namespace yii\smarty; use Yii; use Smarty; use yii\base\View; -use yii\base\ViewRenderer; use yii\helpers\Html; +use yii\base\ViewRenderer as BaseViewRenderer; /** * SmartyViewRenderer allows you to use Smarty templates in views. @@ -21,17 +21,17 @@ use yii\helpers\Html; * @author Alexander Makarov <sam@rmcreative.ru> * @since 2.0 */ -class SmartyViewRenderer extends ViewRenderer +class ViewRenderer extends BaseViewRenderer { /** * @var string the directory or path alias pointing to where Smarty cache will be stored. */ - public $cachePath = '@app/runtime/Smarty/cache'; + public $cachePath = '@runtime/Smarty/cache'; /** * @var string the directory or path alias pointing to where Smarty compiled templates will be stored. */ - public $compilePath = '@app/runtime/Smarty/compile'; + public $compilePath = '@runtime/Smarty/compile'; /** * @var Smarty @@ -63,7 +63,7 @@ class SmartyViewRenderer extends ViewRenderer */ public function smarty_function_path($params, \Smarty_Internal_Template $template) { - if(!isset($params['route'])) { + if (!isset($params['route'])) { trigger_error("path: missing 'route' parameter"); } @@ -87,7 +87,6 @@ class SmartyViewRenderer extends ViewRenderer */ public function render($view, $file, $params) { - $ext = pathinfo($file, PATHINFO_EXTENSION); /** @var \Smarty_Internal_Template $template */ $template = $this->smarty->createTemplate($file, null, null, $params, true); diff --git a/extensions/twig/LICENSE.md b/extensions/twig/LICENSE.md new file mode 100644 index 0000000..0bb1a8d --- /dev/null +++ b/extensions/twig/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/extensions/twig/README.md b/extensions/twig/README.md new file mode 100644 index 0000000..cc6bab0 --- /dev/null +++ b/extensions/twig/README.md @@ -0,0 +1,64 @@ +Yii 2.0 Public Preview - Twig View Renderer +=========================================== + +Thank you for choosing Yii - a high-performance component-based PHP framework. + +If you are looking for a production-ready PHP framework, please use +[Yii v1.1](https://github.com/yiisoft/yii). + +Yii 2.0 is still under heavy development. We may make significant changes +without prior notices. **Yii 2.0 is not ready for production use yet.** + +[](http://travis-ci.org/yiisoft/yii2) + +This is the yii2-twig extension. + + +Installation +------------ + +The prefered way to install this extension is through [composer](http://getcomposer.org/download/). + +Either run +``` +php composer.phar require yiisoft/yii2-twig "*" +``` + +or add +``` +"yiisoft/yii2-twig": "*" +``` +to the require section of your composer.json. + + +*Note: You might have to run `php composer.phar selfupdate`* + + +Usage & Documentation +--------------------- + +This extension has to be registered prior to usage. +To enable this view renderer add it to the $rendereres property of your view object. + +Example: +```php +<?php +// config.php +return array( + //.... + 'components' => array( + 'view' => array( + 'class' => 'yii\base\View', + 'renderers' => array( + 'twig' => array( + 'class' => 'yii\twig\ViewRenderer', + //'cachePath' => '@runtime/Twig/cache', + //'options' => array(), /* Array of twig options */ + ), + ), + ), + ), +); +``` + +For further instructions refer to the related section in the yii guide. diff --git a/extensions/twig/composer.json b/extensions/twig/composer.json new file mode 100644 index 0000000..ba21065 --- /dev/null +++ b/extensions/twig/composer.json @@ -0,0 +1,28 @@ +{ + "name": "yiisoft/yii2-twig", + "description": "The Twig integration for the Yii framework", + "keywords": ["yii", "twig", "renderer"], + "type": "library", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "authors": [ + { + "name": "Alenxader Makarov", + "email": "sam@rmcreative.ru" + } + ], + "minimum-stability": "dev", + "require": { + "yiisoft/yii2": "*", + "twig/twig": "1.13.*" + }, + "autoload": { + "psr-0": { "yii\\twig\\": "" } + } +} diff --git a/yii/renderers/TwigViewRenderer.php b/extensions/twig/yii/twig/ViewRenderer.php similarity index 90% rename from yii/renderers/TwigViewRenderer.php rename to extensions/twig/yii/twig/ViewRenderer.php index de561d3..860a2f2 100644 --- a/yii/renderers/TwigViewRenderer.php +++ b/extensions/twig/yii/twig/ViewRenderer.php @@ -7,11 +7,11 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\renderers; +namespace yii\twig; use Yii; use yii\base\View; -use yii\base\ViewRenderer; +use yii\base\ViewRenderer as BaseViewRenderer; use yii\helpers\Html; /** @@ -20,12 +20,12 @@ use yii\helpers\Html; * @author Alexander Makarov <sam@rmcreative.ru> * @since 2.0 */ -class TwigViewRenderer extends ViewRenderer +class ViewRenderer extends BaseViewRenderer { /** * @var string the directory or path alias pointing to where Twig cache will be stored. */ - public $cachePath = '@app/runtime/Twig/cache'; + public $cachePath = '@runtime/Twig/cache'; /** * @var array Twig options @@ -46,7 +46,7 @@ class TwigViewRenderer extends ViewRenderer 'cache' => Yii::getAlias($this->cachePath), ), $this->options)); - $this->twig->addFunction('path', new \Twig_Function_Function(function($path, $args = array()){ + $this->twig->addFunction('path', new \Twig_Function_Function(function ($path, $args = array()) { return Html::url(array_merge(array($path), $args)); })); diff --git a/changelog.md b/framework/CHANGELOG.md similarity index 100% rename from changelog.md rename to framework/CHANGELOG.md diff --git a/framework/LICENSE.md b/framework/LICENSE.md new file mode 100644 index 0000000..6edcc4f --- /dev/null +++ b/framework/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/framework/README.md b/framework/README.md new file mode 100644 index 0000000..1cbfdf8 --- /dev/null +++ b/framework/README.md @@ -0,0 +1,21 @@ +Yii 2.0 Public Preview +====================== + +Thank you for choosing Yii - a high-performance component-based PHP framework. + +If you are looking for a production-ready PHP framework, please use +[Yii v1.1](https://github.com/yiisoft/yii). + +Yii 2.0 is still under heavy development. We may make significant changes +without prior notices. **Yii 2.0 is not ready for production use yet.** + +[](http://travis-ci.org/yiisoft/yii2) + + + +REQUIREMENTS +------------ + +The minimum requirement by Yii is that your Web server supports PHP 5.3.?. + + diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md new file mode 100644 index 0000000..99aced0 --- /dev/null +++ b/framework/UPGRADE.md @@ -0,0 +1,9 @@ +Upgrading Instructions for Yii Framework v2 +=========================================== + +!!!IMPORTANT!!! + +The following upgrading instructions are cumulative. That is, +if you want to upgrade from version A to version C and there is +version B between A and C, you need to following the instructions +for both A and B. diff --git a/composer.json b/framework/composer.json similarity index 87% rename from composer.json rename to framework/composer.json index e9c3927..c5ff072 100644 --- a/composer.json +++ b/framework/composer.json @@ -63,17 +63,17 @@ "irc": "irc://irc.freenode.net/yii", "source": "https://github.com/yiisoft/yii2" }, - "config": { - "vendor-dir": "yii/vendor" - }, - "bin": [ - "yii/yiic" - ], "require": { - "php": ">=5.3.0", - "michelf/php-markdown": "1.3", - "twig/twig": "1.12.*", - "smarty/smarty": "3.1.*", - "ezyang/htmlpurifier": "v4.5.0" + "php": ">=5.3.7", + "ext-mbstring": "*", + "lib-pcre": "*", + "phpspec/php-diff": "1.0.*", + "ezyang/htmlpurifier": "4.5.*" + }, + "autoload": { + "psr-0": { "yii\\": "/" } + }, + "suggest": { + "michelf/php-markdown": "Required by Markdown." } } diff --git a/framework/yii/.gitignore b/framework/yii/.gitignore new file mode 100644 index 0000000..6f84e8f --- /dev/null +++ b/framework/yii/.gitignore @@ -0,0 +1,3 @@ +phpunit.xml +composer.lock + diff --git a/yii/.htaccess b/framework/yii/.htaccess similarity index 100% rename from yii/.htaccess rename to framework/yii/.htaccess index e019832..8d2f256 100644 --- a/yii/.htaccess +++ b/framework/yii/.htaccess @@ -1 +1 @@ -deny from all +deny from all diff --git a/yii/YiiBase.php b/framework/yii/BaseYii.php similarity index 79% rename from yii/YiiBase.php rename to framework/yii/BaseYii.php index 1a3f50c..cf94166 100644 --- a/yii/YiiBase.php +++ b/framework/yii/BaseYii.php @@ -10,26 +10,38 @@ use yii\base\Exception; use yii\base\InvalidConfigException; use yii\base\InvalidParamException; use yii\base\UnknownClassException; -use yii\logging\Logger; +use yii\log\Logger; /** * Gets the application start timestamp. */ defined('YII_BEGIN_TIME') or define('YII_BEGIN_TIME', microtime(true)); /** + * This constant defines the framework installation directory. + */ +defined('YII_PATH') or define('YII_PATH', __DIR__); +/** * This constant defines whether the application should be in debug mode or not. Defaults to false. */ defined('YII_DEBUG') or define('YII_DEBUG', false); /** - * This constant defines how much call stack information (file name and line number) should be logged by Yii::trace(). - * Defaults to 0, meaning no backtrace information. If it is greater than 0, - * at most that number of call stacks will be logged. Note, only user application call stacks are considered. + * This constant defines in which environment the application is running. Defaults to 'prod', meaning production environment. + * You may define this constant in the bootstrap script. The value could be 'prod' (production), 'dev' (development), 'test', 'staging', etc. */ -defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL', 0); +defined('YII_ENV') or define('YII_ENV', 'prod'); /** - * This constant defines the framework installation directory. + * Whether the the application is running in production environment */ -defined('YII_PATH') or define('YII_PATH', __DIR__); +defined('YII_ENV_PROD') or define('YII_ENV_PROD', YII_ENV === 'prod'); +/** + * Whether the the application is running in development environment + */ +defined('YII_ENV_DEV') or define('YII_ENV_DEV', YII_ENV === 'dev'); +/** + * Whether the the application is running in testing environment + */ +defined('YII_ENV_TEST') or define('YII_ENV_TEST', YII_ENV === 'test'); + /** * This constant defines whether error handling should be enabled. Defaults to true. */ @@ -37,15 +49,15 @@ defined('YII_ENABLE_ERROR_HANDLER') or define('YII_ENABLE_ERROR_HANDLER', true); /** - * YiiBase is the core helper class for the Yii framework. + * BaseYii is the core helper class for the Yii framework. * - * Do not use YiiBase directly. Instead, use its child class [[Yii]] where - * you can customize methods of YiiBase. + * Do not use BaseYii directly. Instead, use its child class [[Yii]] where + * you can customize methods of BaseYii. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class YiiBase +class BaseYii { /** * @var array class map used by the Yii autoloading mechanism. @@ -57,11 +69,6 @@ class YiiBase */ public static $classMap = array(); /** - * @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 $enableIncludePath = true; - /** * @var \yii\console\Application|\yii\web\Application the application instance */ public static $app; @@ -70,9 +77,7 @@ class YiiBase * @see getAlias * @see setAlias */ - public static $aliases = array( - '@yii' => __DIR__, - ); + public static $aliases = array('@yii' => __DIR__); /** * @var array initial property values that will be applied to objects newly created via [[createObject]]. * The array keys are class names without leading backslashes "\", and the array values are the corresponding @@ -95,8 +100,6 @@ class YiiBase */ public static $objectConfig = array(); - private static $_imported = array(); // alias => class name or directory - private static $_logger; /** * @return string the version of Yii framework @@ -107,40 +110,6 @@ class YiiBase } /** - * Imports a class by its alias. - * - * This method is provided to support autoloading of non-namespaced classes. - * Such a class can be specified in terms of an alias. For example, the alias `@old/code/Sample` - * may represent the `Sample` class under the directory `@old/code` (a path alias). - * - * By importing a class, the class is put in an internal storage such that when - * the class is used for the first time, the class autoloader will be able to - * find the corresponding class file and include it. For this reason, this method - * is much lighter than `include()`. - * - * You may import the same class multiple times. Only the first importing will count. - * - * @param string $alias the class to be imported. This may be either a class alias or a fully-qualified class name. - * If the latter, it will be returned back without change. - * @return string the actual class name that `$alias` refers to - * @throws Exception if the alias is invalid - */ - public static function import($alias) - { - if (strncmp($alias, '@', 1)) { - return $alias; - } else { - $alias = static::getAlias($alias); - if (!isset(self::$_imported[$alias])) { - $className = basename($alias); - self::$_imported[$alias] = $className; - self::$classMap[$className] = $alias . '.php'; - } - return self::$_imported[$alias]; - } - } - - /** * Imports a set of namespaces. * * By importing a namespace, the method will create an alias for the directory corresponding @@ -159,6 +128,9 @@ class YiiBase foreach ($namespaces as $name => $path) { if ($name !== '') { $name = trim(strtr($name, array('\\' => '/', '_' => '/')), '/'); + if (is_array($path)) { + $path = reset($path); + } static::setAlias('@' . $name, rtrim($path, '/\\') . '/' . $name); } } @@ -287,7 +259,11 @@ class YiiBase if ($path !== null) { $path = strncmp($path, '@', 1) ? rtrim($path, '\\/') : static::getAlias($path); if (!isset(self::$aliases[$root])) { - self::$aliases[$root] = $path; + if ($pos === false) { + self::$aliases[$root] = $path; + } else { + self::$aliases[$root] = array($alias => $path); + } } elseif (is_string(self::$aliases[$root])) { if ($pos === false) { self::$aliases[$root] = $path; @@ -322,22 +298,19 @@ 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 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 InvalidConfigException if the class file does not exist + * This autoloader allows loading classes that follow the [PSR-0 standard](http://www.php-fig.org/psr/0/). + * Therefor a path alias has to be defined for each top-level namespace. + * + * @param string $className the fully qualified class name without a leading backslash "\" * @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 = static::getAlias(self::$classMap[$className]); - if (!is_file($classFile)) { - throw new InvalidConfigException("Class file does not exist: $classFile"); + $classFile = self::$classMap[$className]; + if ($classFile[0] === '@') { + $classFile = static::getAlias($classFile); } } else { // follow PSR-0 to determine the class file @@ -349,31 +322,21 @@ class YiiBase $path = str_replace('_', '/', $className) . '.php'; } - // try via path alias first - if (strpos($path, '/') !== false) { - $fullPath = static::getAlias('@' . $path, false); - if ($fullPath !== false && is_file($fullPath)) { - $classFile = $fullPath; + // try loading via path alias + if (strpos($path, '/') === false) { + return; + } else { + $classFile = static::getAlias('@' . $path, false); + if ($classFile === false || !is_file($classFile)) { + return; } } - - // search include_path - if (!isset($classFile) && self::$enableIncludePath && ($fullPath = stream_resolve_include_path($path)) !== false) { - $classFile = $fullPath; - } - - if (!isset($classFile)) { - // return false to let other autoloaders to try loading the class - return false; - } } include($classFile); - if (class_exists($className, false) || interface_exists($className, false) || - function_exists('trait_exists') && trait_exists($className, false)) { - return true; - } else { + if (YII_DEBUG && !class_exists($className, false) && !interface_exists($className, false) && + (!function_exists('trait_exists') || !trait_exists($className, false))) { throw new UnknownClassException("Unable to find '$className' in file: $classFile"); } } @@ -433,10 +396,6 @@ class YiiBase throw new InvalidConfigException('Object configuration must be an array containing a "class" element.'); } - if (!class_exists($class, false)) { - $class = static::import($class); - } - $class = ltrim($class, '\\'); if (isset(self::$objectConfig[$class])) { @@ -471,7 +430,7 @@ class YiiBase public static function trace($message, $category = 'application') { if (YII_DEBUG) { - self::getLogger()->log($message, Logger::LEVEL_TRACE, $category); + self::$app->getLog()->log($message, Logger::LEVEL_TRACE, $category); } } @@ -484,7 +443,7 @@ class YiiBase */ public static function error($message, $category = 'application') { - self::getLogger()->log($message, Logger::LEVEL_ERROR, $category); + self::$app->getLog()->log($message, Logger::LEVEL_ERROR, $category); } /** @@ -496,7 +455,7 @@ class YiiBase */ public static function warning($message, $category = 'application') { - self::getLogger()->log($message, Logger::LEVEL_WARNING, $category); + self::$app->getLog()->log($message, Logger::LEVEL_WARNING, $category); } /** @@ -508,7 +467,7 @@ class YiiBase */ public static function info($message, $category = 'application') { - self::getLogger()->log($message, Logger::LEVEL_INFO, $category); + self::$app->getLog()->log($message, Logger::LEVEL_INFO, $category); } /** @@ -530,7 +489,7 @@ class YiiBase */ public static function beginProfile($token, $category = 'application') { - self::getLogger()->log($token, Logger::LEVEL_PROFILE_BEGIN, $category); + self::$app->getLog()->log($token, Logger::LEVEL_PROFILE_BEGIN, $category); } /** @@ -542,29 +501,7 @@ class YiiBase */ public static function endProfile($token, $category = 'application') { - self::getLogger()->log($token, Logger::LEVEL_PROFILE_END, $category); - } - - /** - * Returns the message logger object. - * @return \yii\logging\Logger message logger - */ - public static function getLogger() - { - if (self::$_logger !== null) { - return self::$_logger; - } else { - return self::$_logger = new Logger; - } - } - - /** - * Sets the logger object. - * @param Logger $logger the logger object. - */ - public static function setLogger($logger) - { - self::$_logger = $logger; + self::$app->getLog()->log($token, Logger::LEVEL_PROFILE_END, $category); } /** @@ -573,16 +510,15 @@ class YiiBase */ public static function powered() { - return 'Powered by <a href="http://www.yiiframework.com/" rel="external">Yii Framework</a>.'; + return 'Powered by <a href="http://www.yiiframework.com/" rel="external">Yii Framework</a>'; } /** * Translates a message to the specified language. * - * 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]]. + * This is a shortcut method of [[\yii\i18n\I18N::translate()]]. + * + * The translation will be conducted according to the message category and the target language will be used. * * 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 @@ -595,21 +531,44 @@ class YiiBase * 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 $category the message category. * @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) + public static function t($category, $message, $params = array(), $language = null) { if (self::$app !== null) { - return self::$app->getI18N()->translate($message, $params, $language); + return self::$app->getI18n()->translate($category, $message, $params, $language ?: self::$app->language); } else { - if (strpos($message, '|') !== false && preg_match('/^([\w\-\\/\.\\\\]+)\|(.*)/', $message, $matches)) { - $message = $matches[2]; - } return is_array($params) ? strtr($message, $params) : $message; } } + + /** + * Configures an object with the initial property values. + * @param object $object the object to be configured + * @param array $properties the property initial values given in terms of name-value pairs. + */ + public static function configure($object, $properties) + { + foreach ($properties as $name => $value) { + $object->$name = $value; + } + } + + /** + * Returns the public member variables of an object. + * This method is provided such that we can get the public member variables of an object. + * It is different from "get_object_vars()" because the latter will return private + * and protected variables if it is called within the object itself. + * @param object $object the object to be handled + * @return array the public member variables of the object + */ + public static function getObjectVars($object) + { + return get_object_vars($object); + } } diff --git a/yii/Yii.php b/framework/yii/Yii.php similarity index 72% rename from yii/Yii.php rename to framework/yii/Yii.php index 109e2fd..232117f 100644 --- a/yii/Yii.php +++ b/framework/yii/Yii.php @@ -7,19 +7,20 @@ * @license http://www.yiiframework.com/license/ */ -require(__DIR__ . '/YiiBase.php'); +require(__DIR__ . '/BaseYii.php'); /** * Yii is a helper class serving common framework functionalities. * - * It extends from [[YiiBase]] which provides the actual implementation. - * By writing your own Yii class, you can customize some functionalities of [[YiiBase]]. + * It extends from [[BaseYii]] which provides the actual implementation. + * By writing your own Yii class, you can customize some functionalities of [[BaseYii]]. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Yii extends \yii\YiiBase +class Yii extends \yii\BaseYii { } -spl_autoload_register(array('Yii', 'autoload')); +spl_autoload_register(array('Yii', 'autoload'), true, true); +Yii::$classMap = include(__DIR__ . '/classes.php'); diff --git a/framework/yii/assets.php b/framework/yii/assets.php new file mode 100644 index 0000000..c52bca6 --- /dev/null +++ b/framework/yii/assets.php @@ -0,0 +1,11 @@ +<?php + +return array( + yii\web\YiiAsset::className(), + yii\web\JqueryAsset::className(), + yii\validators\PunycodeAsset::className(), + yii\validators\ValidationAsset::className(), + yii\widgets\ActiveFormAsset::className(), + yii\captcha\CaptchaAsset::className(), + yii\widgets\MaskedInputAsset::className(), +); diff --git a/framework/yii/assets/jquery.js b/framework/yii/assets/jquery.js new file mode 100644 index 0000000..e2c203f --- /dev/null +++ b/framework/yii/assets/jquery.js @@ -0,0 +1,9597 @@ +/*! + * jQuery JavaScript Library v1.9.1 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright 2005, 2012 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2013-2-4 + */ +(function( window, undefined ) { + +// Can't do this because several apps including ASP.NET trace +// the stack via arguments.caller.callee and Firefox dies if +// you try to trace through "use strict" call chains. (#13335) +// Support: Firefox 18+ +//"use strict"; +var + // The deferred used on DOM ready + readyList, + + // A central reference to the root jQuery(document) + rootjQuery, + + // Support: IE<9 + // For `typeof node.method` instead of `node.method !== undefined` + core_strundefined = typeof undefined, + + // Use the correct document accordingly with window argument (sandbox) + document = window.document, + location = window.location, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // [[Class]] -> type pairs + class2type = {}, + + // List of deleted data cache ids, so we can reuse them + core_deletedIds = [], + + core_version = "1.9.1", + + // Save a reference to some core methods + core_concat = core_deletedIds.concat, + core_push = core_deletedIds.push, + core_slice = core_deletedIds.slice, + core_indexOf = core_deletedIds.indexOf, + core_toString = class2type.toString, + core_hasOwn = class2type.hasOwnProperty, + core_trim = core_version.trim, + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Used for matching numbers + core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, + + // Used for splitting on whitespace + core_rnotwhite = /\S+/g, + + // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // A simple way to check for HTML strings + // Prioritize #id over <tag> to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, + rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }, + + // The ready event handler + completed = function( event ) { + + // readyState === "complete" is good enough for us to call the dom ready in oldIE + if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { + detach(); + jQuery.ready(); + } + }, + // Clean-up method for dom ready events + detach = function() { + if ( document.addEventListener ) { + document.removeEventListener( "DOMContentLoaded", completed, false ); + window.removeEventListener( "load", completed, false ); + + } else { + document.detachEvent( "onreadystatechange", completed ); + window.detachEvent( "onload", completed ); + } + }; + +jQuery.fn = jQuery.prototype = { + // The current version of jQuery being used + jquery: core_version, + + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + + // scripts is true for back-compat + jQuery.merge( this, jQuery.parseHTML( + match[1], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return core_slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; + }, + + slice: function() { + return this.pushStack( core_slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: core_push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var src, copyIsArray, copy, name, options, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger("ready").off("ready"); + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + if ( obj == null ) { + return String( obj ); + } + return typeof obj === "object" || typeof obj === "function" ? + class2type[ core_toString.call(obj) ] || "object" : + typeof obj; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !core_hasOwn.call(obj, "constructor") && + !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || core_hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + // data: string of html + // context (optional): If specified, the fragment will be created in this context, defaults to document + // keepScripts (optional): If true, will include scripts passed in the html string + parseHTML: function( data, context, keepScripts ) { + if ( !data || typeof data !== "string" ) { + return null; + } + if ( typeof context === "boolean" ) { + keepScripts = context; + context = false; + } + context = context || document; + + var parsed = rsingleTag.exec( data ), + scripts = !keepScripts && []; + + // Single tag + if ( parsed ) { + return [ context.createElement( parsed[1] ) ]; + } + + parsed = jQuery.buildFragment( [ data ], context, scripts ); + if ( scripts ) { + jQuery( scripts ).remove(); + } + return jQuery.merge( [], parsed.childNodes ); + }, + + parseJSON: function( data ) { + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + if ( data === null ) { + return data; + } + + if ( typeof data === "string" ) { + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + if ( data ) { + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + } + } + } + + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + var xml, tmp; + if ( !data || typeof data !== "string" ) { + return null; + } + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && jQuery.trim( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + // args is for internal usage only + each: function( obj, callback, args ) { + var value, + i = 0, + length = obj.length, + isArray = isArraylike( obj ); + + if ( args ) { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } + } + + return obj; + }, + + // Use native String.trim function wherever possible + trim: core_trim && !core_trim.call("\uFEFF\xA0") ? + function( text ) { + return text == null ? + "" : + core_trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArraylike( Object(arr) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + core_push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + var len; + + if ( arr ) { + if ( core_indexOf ) { + return core_indexOf.call( arr, elem, i ); + } + + len = arr.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in arr && arr[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var l = second.length, + i = first.length, + j = 0; + + if ( typeof l === "number" ) { + for ( ; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var retVal, + ret = [], + i = 0, + length = elems.length; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, + i = 0, + length = elems.length, + isArray = isArraylike( elems ), + ret = []; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return core_concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var args, proxy, tmp; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = core_slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + // Multifunctional method to get and set values of a collection + // The value/s can optionally be executed if it's a function + access: function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + length = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < length; i++ ) { + fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; + }, + + now: function() { + return ( new Date() ).getTime(); + } +}); + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // we once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout( jQuery.ready ); + + // Standards-based browsers support DOMContentLoaded + } else if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed, false ); + + // If IE event model is used + } else { + // Ensure firing before onload, maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", completed ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", completed ); + + // If IE and not a frame + // continually check to see if the document is ready + var top = false; + + try { + top = window.frameElement == null && document.documentElement; + } catch(e) {} + + if ( top && top.doScroll ) { + (function doScrollCheck() { + if ( !jQuery.isReady ) { + + try { + // Use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + top.doScroll("left"); + } catch(e) { + return setTimeout( doScrollCheck, 50 ); + } + + // detach all dom ready events + detach(); + + // and execute any waiting functions + jQuery.ready(); + } + })(); + } + } + } + return readyList.promise( obj ); +}; + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +function isArraylike( obj ) { + var length = obj.length, + type = jQuery.type( obj ); + + if ( jQuery.isWindow( obj ) ) { + return false; + } + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === "array" || type !== "function" && + ( length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj ); +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // First callback to fire (used internally by add and fireWith) + firingStart, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( list && ( !fired || stack ) ) { + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ](function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[0] ] = function() { + deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = core_slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; + if( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +}); +jQuery.support = (function() { + + var support, all, a, + input, select, fragment, + opt, eventName, isSupported, i, + div = document.createElement("div"); + + // Setup + div.setAttribute( "className", "t" ); + div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>"; + + // Support tests won't run in some limited or non-browser environments + all = div.getElementsByTagName("*"); + a = div.getElementsByTagName("a")[ 0 ]; + if ( !all || !a || !all.length ) { + return {}; + } + + // First batch of tests + select = document.createElement("select"); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName("input")[ 0 ]; + + a.style.cssText = "top:1px;float:left;opacity:.5"; + support = { + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: div.firstChild.nodeType === 3, + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: a.getAttribute("href") === "/a", + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.5/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) + checkOn: !!input.value, + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Tests for enctype support on a form (#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>", + + // jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode + boxModel: document.compatMode === "CSS1Compat", + + // Will be defined later + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true, + boxSizingReliable: true, + pixelPosition: false + }; + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Support: IE<9 + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + // Check if we can trust getAttribute("value") + input = document.createElement("input"); + input.setAttribute( "value", "" ); + support.input = input.getAttribute( "value" ) === ""; + + // Check if an input maintains its value after becoming a radio + input.value = "t"; + input.setAttribute( "type", "radio" ); + support.radioValue = input.value === "t"; + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "checked", "t" ); + input.setAttribute( "name", "t" ); + + fragment = document.createDocumentFragment(); + fragment.appendChild( input ); + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<9 + // Opera does not clone events (and typeof div.attachEvent === undefined). + // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() + if ( div.attachEvent ) { + div.attachEvent( "onclick", function() { + support.noCloneEvent = false; + }); + + div.cloneNode( true ).click(); + } + + // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) + // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP), test/csp.php + for ( i in { submit: true, change: true, focusin: true }) { + div.setAttribute( eventName = "on" + i, "t" ); + + support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; + } + + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, marginDiv, tds, + divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + container = document.createElement("div"); + container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; + + body.appendChild( container ).appendChild( div ); + + // Support: IE8 + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>"; + tds = div.getElementsByTagName("td"); + tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Support: IE8 + // Check if empty table cells still have offsetWidth/Height + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check box-sizing and margin behavior + div.innerHTML = ""; + div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; + support.boxSizing = ( div.offsetWidth === 4 ); + support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 ); + + // Use window.getComputedStyle because jsdom on node.js will break without it. + if ( window.getComputedStyle ) { + support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; + support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. (#3333) + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + marginDiv = div.appendChild( document.createElement("div") ); + marginDiv.style.cssText = div.style.cssText = divReset; + marginDiv.style.marginRight = marginDiv.style.width = "0"; + div.style.width = "1px"; + + support.reliableMarginRight = + !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); + } + + if ( typeof div.style.zoom !== core_strundefined ) { + // Support: IE<8 + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + div.innerHTML = ""; + div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Support: IE6 + // Check if elements with layout shrink-wrap their children + div.style.display = "block"; + div.innerHTML = "<div></div>"; + div.firstChild.style.width = "5px"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + + if ( support.inlineBlockNeedsLayout ) { + // Prevent IE 6 from affecting layout for positioned elements #11048 + // Prevent IE from shrinking the body in IE 7 mode #12869 + // Support: IE<8 + body.style.zoom = 1; + } + } + + body.removeChild( container ); + + // Null elements to avoid leaks in IE + container = div = tds = marginDiv = null; + }); + + // Null elements to avoid leaks in IE + all = select = fragment = opt = a = input = null; + + return support; +})(); + +var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, + rmultiDash = /([A-Z])/g; + +function internalData( elem, name, data, pvt /* Internal Use Only */ ){ + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = core_deletedIds.pop() || jQuery.guid++; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; +} + +function internalRemoveData( elem, name, pvt ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var i, l, thisCache, + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split(" "); + } + } + } else { + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = name.concat( jQuery.map( name, jQuery.camelCase ) ); + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject( cache[ id ] ) ) { + return; + } + } + + // Destroy the cache + if ( isNode ) { + jQuery.cleanData( [ elem ], true ); + + // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) + } else if ( jQuery.support.deleteExpando || cache != cache.window ) { + delete cache[ id ]; + + // When all else fails, null + } else { + cache[ id ] = null; + } +} + +jQuery.extend({ + cache: {}, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data ) { + return internalData( elem, name, data ); + }, + + removeData: function( elem, name ) { + return internalRemoveData( elem, name ); + }, + + // For internal use only. + _data: function( elem, name, data ) { + return internalData( elem, name, data, true ); + }, + + _removeData: function( elem, name ) { + return internalRemoveData( elem, name, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + // Do not set data on non-element because it will not be cleared (#8335). + if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { + return false; + } + + var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; + + // nodes accept data unless otherwise specified; rejection can be conditional + return !noData || noData !== true && elem.getAttribute("classid") === noData; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var attrs, name, + elem = this[0], + i = 0, + data = null; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attrs = elem.attributes; + for ( ; i < attrs.length; i++ ) { + name = attrs[i].name; + + if ( !name.indexOf( "data-" ) ) { + name = jQuery.camelCase( name.slice(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + return jQuery.access( this, function( value ) { + + if ( value === undefined ) { + // Try to fetch any internally stored data first + return elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; + } + + this.each(function() { + jQuery.data( this, key, value ); + }); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + var name; + for ( name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray(data) ) { + queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + hooks.cur = fn; + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return jQuery._data( elem, key ) || jQuery._data( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + jQuery._removeData( elem, type + "queue" ); + jQuery._removeData( elem, key ); + }) + }); + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while( i-- ) { + tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var nodeHook, boolHook, + rclass = /[\t\r\n]/g, + rreturn = /\r/g, + rfocusable = /^(?:input|select|textarea|button|object)$/i, + rclickable = /^(?:a|area)$/i, + rboolean = /^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i, + ruseDefault = /^(?:checked|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + getSetInput = jQuery.support.input; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call( this, j, this.className ) ); + }); + } + + if ( proceed ) { + // The disjunction here is for better compressibility (see removeClass) + classes = ( value || "" ).match( core_rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + " " + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + elem.className = jQuery.trim( cur ); + + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = arguments.length === 0 || typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call( this, j, this.className ) ); + }); + } + if ( proceed ) { + classes = ( value || "" ).match( core_rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + "" + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + elem.className = value ? jQuery.trim( cur ) : ""; + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.match( core_rnotwhite ) || []; + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space separated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + // Toggle whole class name + } else if ( type === core_strundefined || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // If the element has a class name or if we're passed "false", + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var ret, hooks, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var val, + self = jQuery(this); + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, option, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one" || index < 0, + values = one ? null : [], + max = one ? index + 1 : options.length, + i = index < 0 ? + max : + one ? index : 0; + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // oldIE doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + // Don't return options that are disabled or in a disabled optgroup + ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && + ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attr: function( elem, name, value ) { + var hooks, notxml, ret, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === core_strundefined ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + + } else if ( hooks && notxml && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, value + "" ); + return value; + } + + } else if ( hooks && notxml && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + // In IE9+, Flash objects don't have .getAttribute (#12945) + // Support: IE9+ + if ( typeof elem.getAttribute !== core_strundefined ) { + ret = elem.getAttribute( name ); + } + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var name, propName, + i = 0, + attrNames = value && value.match( core_rnotwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( (name = attrNames[i++]) ) { + propName = jQuery.propFix[ name ] || name; + + // Boolean attributes get special treatment (#10870) + if ( rboolean.test( name ) ) { + // Set corresponding property to false for boolean attributes + // Also clear defaultChecked/defaultSelected (if appropriate) for IE<8 + if ( !getSetAttribute && ruseDefault.test( name ) ) { + elem[ jQuery.camelCase( "default-" + name ) ] = + elem[ propName ] = false; + } else { + elem[ propName ] = false; + } + + // See #9699 for explanation of this approach (setting first, then removal) + } else { + jQuery.attr( elem, name, "" ); + } + + elem.removeAttribute( getSetAttribute ? name : propName ); + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to default in case type is set after value during creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + var + // Use .prop to determine if this attribute is understood as boolean + prop = jQuery.prop( elem, name ), + + // Fetch it accordingly + attr = typeof prop === "boolean" && elem.getAttribute( name ), + detail = typeof prop === "boolean" ? + + getSetInput && getSetAttribute ? + attr != null : + // oldIE fabricates an empty string for missing boolean attributes + // and conflates checked/selected into attroperties + ruseDefault.test( name ) ? + elem[ jQuery.camelCase( "default-" + name ) ] : + !!attr : + + // fetch an attribute node for properties not recognized as boolean + elem.getAttributeNode( name ); + + return detail && detail.value !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + // IE<8 needs the *property* name + elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); + + // Use defaultChecked and defaultSelected for oldIE + } else { + elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; + } + + return name; + } +}; + +// fix oldIE value attroperty +if ( !getSetInput || !getSetAttribute ) { + jQuery.attrHooks.value = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return jQuery.nodeName( elem, "input" ) ? + + // Ignore the value *property* by using defaultValue + elem.defaultValue : + + ret && ret.specified ? ret.value : undefined; + }, + set: function( elem, value, name ) { + if ( jQuery.nodeName( elem, "input" ) ) { + // Does not return so that setAttribute is also used + elem.defaultValue = value; + } else { + // Use nodeHook if defined (#1954); otherwise setAttribute is fine + return nodeHook && nodeHook.set( elem, value, name ); + } + } + }; +} + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return ret && ( name === "id" || name === "name" || name === "coords" ? ret.value !== "" : ret.specified ) ? + ret.value : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + elem.setAttributeNode( + (ret = elem.ownerDocument.createAttribute( name )) + ); + } + + ret.value = value += ""; + + // Break association with cloned elements by also using setAttribute (#9646) + return name === "value" || value === elem.getAttribute( name ) ? + value : + undefined; + } + }; + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + nodeHook.set( elem, value === "" ? false : value, name ); + } + }; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); +} + + +// Some attributes require a special call on IE +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret == null ? undefined : ret; + } + }); + }); + + // href/src property should get the full normalized URL (#10299/#12915) + jQuery.each([ "href", "src" ], function( i, name ) { + jQuery.propHooks[ name ] = { + get: function( elem ) { + return elem.getAttribute( name, 4 ); + } + }; + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Note: IE uppercases css property names, but if we were to .toLowerCase() + // .cssText, that would destroy case senstitivity in URL's, like in "background" + return elem.style.cssText || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = value + "" ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); +var rformElems = /^(?:input|select|textarea)$/i, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + var tmp, events, t, handleObjIn, + special, eventHandle, handleObj, + handlers, type, namespaces, origType, + elemData = jQuery._data( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !(events = elemData.events) ) { + events = elemData.events = {}; + } + if ( !(eventHandle = elemData.handle) ) { + eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !(handlers = events[ type ]) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + var j, handleObj, tmp, + origCount, t, events, + special, handlers, type, + namespaces, origType, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ); + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery._removeData( elem, "events" ); + } + }, + + trigger: function( event, data, elem, onlyHandlers ) { + var handle, ontype, cur, + bubbleType, special, tmp, i, + eventPath = [ elem || document ], + type = core_hasOwn.call( event, "type" ) ? event.type : event, + namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; + + cur = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + event.isTrigger = true; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === (elem.ownerDocument || document) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { + + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + try { + elem[ type ](); + } catch ( e ) { + // IE<9 dies on focus/blur to hidden element (#1486,#12518) + // only reproducible on winXP IE8 native, not IE9 in IE8 mode + } + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, ret, handleObj, matched, j, + handlerQueue = [], + args = core_slice.call( arguments ), + handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( (event.result = ret) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var sel, handleObj, matches, i, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + // Black-hole SVG <use> instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + + for ( ; cur != this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, handlers: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + } + + return handlerQueue; + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: IE<9 + // Fix target property (#1925) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Support: Chrome 23+, Safari? + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Support: IE<9 + // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) + event.metaKey = !!event.metaKey; + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var body, eventDoc, doc, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { + this.click(); + return false; + } + } + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== document.activeElement && this.focus ) { + try { + this.focus(); + return false; + } catch ( e ) { + // Support: IE<9 + // If we error on focus to hidden element (#1486, #12518), + // let .trigger() run the handlers + } + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === document.activeElement && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + + beforeunload: { + postDispatch: function( event ) { + + // Even when returnValue equals to undefined Firefox will still show alert + if ( event.result !== undefined ) { + event.originalEvent.returnValue = event.result; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + var name = "on" + type; + + if ( elem.detachEvent ) { + + // #8545, #7054, preventing memory leaks for custom events in IE6-8 + // detachEvent needed property on element, by name of that event, to properly expose it to GC + if ( typeof elem[ name ] === core_strundefined ) { + elem[ name ] = null; + } + + elem.detachEvent( name, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + if ( !e ) { + return; + } + + // If preventDefault exists, run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // Support: IE + // Otherwise set the returnValue property of the original event to false + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + if ( !e ) { + return; + } + // If stopPropagation exists, run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + + // Support: IE + // Set the cancelBubble property of the original event to true + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !jQuery._data( form, "submitBubbles" ) ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + jQuery._data( form, "submitBubbles", true ); + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + } + // Allow triggered, simulated change events (#11500) + jQuery.event.simulate( "change", this, event, true ); + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + jQuery._data( elem, "changeBubbles", true ); + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return !rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var type, origFn; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + var elem = this[0]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +}); +/*! + * Sizzle CSS Selector Engine + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license + * http://sizzlejs.com/ + */ +(function( window, undefined ) { + +var i, + cachedruns, + Expr, + getText, + isXML, + compile, + hasDuplicate, + outermostContext, + + // Local document vars + setDocument, + document, + docElem, + documentIsXML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + sortOrder, + + // Instance-specific data + expando = "sizzle" + -(new Date()), + preferredDoc = window.document, + support = {}, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + + // General-purpose constants + strundefined = typeof undefined, + MAX_NEGATIVE = 1 << 31, + + // Array methods + arr = [], + pop = arr.pop, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf if we can't use a native one + indexOf = arr.indexOf || function( elem ) { + var i = 0, + len = this.length; + for ( ; i < len; i++ ) { + if ( this[i] === elem ) { + return i; + } + } + return -1; + }, + + + // Regular expressions + + // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + // http://www.w3.org/TR/css3-syntax/#characters + characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Loosely modeled on CSS identifier characters + // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors + // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = characterEncoding.replace( "w", "w#" ), + + // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors + operators = "([*^$|!~]?=)", + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + + "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", + + // Prefer arguments quoted, + // then not containing pseudos/brackets, + // then attribute selectors/non-parenthetical expressions, + // then anything else + // These preferences are here to reduce the number of selectors + // needing tokenize in the PSEUDO preFilter + pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ), + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + characterEncoding + ")" ), + "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), + "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ), + "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rsibling = /[\x20\t\r\n\f]*[+~]/, + + rnative = /^[^{]+\{\s*\[native code/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rescape = /'|\\/g, + rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = /\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g, + funescape = function( _, escaped ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + return high !== high ? + escaped : + // BMP codepoint + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }; + +// Use a stripped-down slice if we can't use a native one +try { + slice.call( preferredDoc.documentElement.childNodes, 0 )[0].nodeType; +} catch ( e ) { + slice = function( i ) { + var elem, + results = []; + while ( (elem = this[i++]) ) { + results.push( elem ); + } + return results; + }; +} + +/** + * For feature detection + * @param {Function} fn The function to test for native support + */ +function isNative( fn ) { + return rnative.test( fn + "" ); +} + +/** + * Create key-value caches of limited size + * @returns {Function(string, Object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var cache, + keys = []; + + return (cache = function( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key += " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key ] = value); + }); +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); + + try { + return fn( div ); + } catch (e) { + return false; + } finally { + // release memory in IE + div = null; + } +} + +function Sizzle( selector, context, results, seed ) { + var match, elem, m, nodeType, + // QSA vars + i, groups, old, nid, newContext, newSelector; + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + + context = context || document; + results = results || []; + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { + return []; + } + + if ( !documentIsXML && !seed ) { + + // Shortcuts + if ( (match = rquickExpr.exec( selector )) ) { + // Speed-up: Sizzle("#ID") + if ( (m = match[1]) ) { + if ( nodeType === 9 ) { + elem = context.getElementById( m ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE, Opera, and Webkit return items + // by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + } else { + // Context is not a document + if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && + contains( context, elem ) && elem.id === m ) { + results.push( elem ); + return results; + } + } + + // Speed-up: Sizzle("TAG") + } else if ( match[2] ) { + push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) ); + return results; + + // Speed-up: Sizzle(".CLASS") + } else if ( (m = match[3]) && support.getByClassName && context.getElementsByClassName ) { + push.apply( results, slice.call(context.getElementsByClassName( m ), 0) ); + return results; + } + } + + // QSA path + if ( support.qsa && !rbuggyQSA.test(selector) ) { + old = true; + nid = expando; + newContext = context; + newSelector = nodeType === 9 && selector; + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + groups = tokenize( selector ); + + if ( (old = context.getAttribute("id")) ) { + nid = old.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", nid ); + } + nid = "[id='" + nid + "'] "; + + i = groups.length; + while ( i-- ) { + groups[i] = nid + toSelector( groups[i] ); + } + newContext = rsibling.test( selector ) && context.parentNode || context; + newSelector = groups.join(","); + } + + if ( newSelector ) { + try { + push.apply( results, slice.call( newContext.querySelectorAll( + newSelector + ), 0 ) ); + return results; + } catch(qsaError) { + } finally { + if ( !old ) { + context.removeAttribute("id"); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Detect xml + * @param {Element|Object} elem An element or a document + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var doc = node ? node.ownerDocument || node : preferredDoc; + + // If no document and documentElement is available, return + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Set our document + document = doc; + docElem = doc.documentElement; + + // Support tests + documentIsXML = isXML( doc ); + + // Check if getElementsByTagName("*") returns only elements + support.tagNameNoComments = assert(function( div ) { + div.appendChild( doc.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Check if attributes should be retrieved by attribute nodes + support.attributes = assert(function( div ) { + div.innerHTML = "<select></select>"; + var type = typeof div.lastChild.getAttribute("multiple"); + // IE8 returns a string for some attributes even when not present + return type !== "boolean" && type !== "string"; + }); + + // Check if getElementsByClassName can be trusted + support.getByClassName = assert(function( div ) { + // Opera can't find a second classname (in 9.6) + div.innerHTML = "<div class='hidden e'></div><div class='hidden'></div>"; + if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) { + return false; + } + + // Safari 3.2 caches class attributes and doesn't catch changes + div.lastChild.className = "e"; + return div.getElementsByClassName("e").length === 2; + }); + + // Check if getElementById returns elements by name + // Check if getElementsByName privileges form controls or returns elements by ID + support.getByName = assert(function( div ) { + // Inject content + div.id = expando + 0; + div.innerHTML = "<a name='" + expando + "'></a><div name='" + expando + "'></div>"; + docElem.insertBefore( div, docElem.firstChild ); + + // Test + var pass = doc.getElementsByName && + // buggy browsers will return fewer than the correct 2 + doc.getElementsByName( expando ).length === 2 + + // buggy browsers will return more than the correct 0 + doc.getElementsByName( expando + 0 ).length; + support.getIdNotName = !doc.getElementById( expando ); + + // Cleanup + docElem.removeChild( div ); + + return pass; + }); + + // IE6/7 return modified attributes + Expr.attrHandle = assert(function( div ) { + div.innerHTML = "<a href='#'></a>"; + return div.firstChild && typeof div.firstChild.getAttribute !== strundefined && + div.firstChild.getAttribute("href") === "#"; + }) ? + {} : + { + "href": function( elem ) { + return elem.getAttribute( "href", 2 ); + }, + "type": function( elem ) { + return elem.getAttribute("type"); + } + }; + + // ID find and filter + if ( support.getIdNotName ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && !documentIsXML ) { + var m = context.getElementById( id ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && !documentIsXML ) { + var m = context.getElementById( id ); + + return m ? + m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ? + [m] : + undefined : + []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.tagNameNoComments ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== strundefined ) { + return context.getElementsByTagName( tag ); + } + } : + function( tag, context ) { + var elem, + tmp = [], + i = 0, + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Name + Expr.find["NAME"] = support.getByName && function( tag, context ) { + if ( typeof context.getElementsByName !== strundefined ) { + return context.getElementsByName( name ); + } + }; + + // Class + Expr.find["CLASS"] = support.getByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== strundefined && !documentIsXML ) { + return context.getElementsByClassName( className ); + } + }; + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21), + // no need to also add to buggyMatches since matches checks buggyQSA + // A support test would require too much code (would include document ready) + rbuggyQSA = [ ":focus" ]; + + if ( (support.qsa = isNative(doc.querySelectorAll)) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explictly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + div.innerHTML = "<select><option selected=''></option></select>"; + + // IE8 - Some boolean attributes are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + }); + + assert(function( div ) { + + // Opera 10-12/IE8 - ^= $= *= and empty values + // Should not select anything + div.innerHTML = "<input type='hidden' i=''/>"; + if ( div.querySelectorAll("[i^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = isNative( (matches = docElem.matchesSelector || + docElem.mozMatchesSelector || + docElem.webkitMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = new RegExp( rbuggyMatches.join("|") ); + + // Element contains another + // Purposefully does not implement inclusive descendent + // As in, an element does not contain itself + contains = isNative(docElem.contains) || docElem.compareDocumentPosition ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + // Document order sorting + sortOrder = docElem.compareDocumentPosition ? + function( a, b ) { + var compare; + + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( (compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b )) ) { + if ( compare & 1 || a.parentNode && a.parentNode.nodeType === 11 ) { + if ( a === doc || contains( preferredDoc, a ) ) { + return -1; + } + if ( b === doc || contains( preferredDoc, b ) ) { + return 1; + } + return 0; + } + return compare & 4 ? -1 : 1; + } + + return a.compareDocumentPosition ? -1 : 1; + } : + function( a, b ) { + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Parentless nodes are either documents or disconnected + } else if ( !aup || !bup ) { + return a === doc ? -1 : + b === doc ? 1 : + aup ? -1 : + bup ? 1 : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + // Always assume the presence of duplicates if sort doesn't + // pass them to our comparison function (as in Google Chrome). + hasDuplicate = false; + [0, 0].sort( sortOrder ); + support.detectDuplicates = hasDuplicate; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + // rbuggyQSA always contains :focus, so no need for an existence check + if ( support.matchesSelector && !documentIsXML && (!rbuggyMatches || !rbuggyMatches.test(expr)) && !rbuggyQSA.test(expr) ) { + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch(e) {} + } + + return Sizzle( expr, document, null, [elem] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + var val; + + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + if ( !documentIsXML ) { + name = name.toLowerCase(); + } + if ( (val = Expr.attrHandle[ name ]) ) { + return val( elem ); + } + if ( documentIsXML || support.attributes ) { + return elem.getAttribute( name ); + } + return ( (val = elem.getAttributeNode( name )) || elem.getAttribute( name ) ) && elem[ name ] === true ? + name : + val && val.specified ? val.value : null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +// Document sorting and removing duplicates +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + i = 1, + j = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( ; (elem = results[i]); i++ ) { + if ( elem === results[ i - 1 ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + return results; +}; + +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && ( ~b.sourceIndex || MAX_NEGATIVE ) - ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +// Returns a function to use in pseudos for input types +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +// Returns a function to use in pseudos for buttons +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +// Returns a function to use in pseudos for positionals +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + for ( ; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (see #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[5] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[4] ) { + match[2] = match[4]; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeName ) { + if ( nodeName === "*" ) { + return function() { return true; }; + } + + nodeName = nodeName.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, outerCache, node, diff, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + // Seek `elem` from a previously-cached index + outerCache = parent[ expando ] || (parent[ expando ] = {}); + cache = outerCache[ type ] || []; + nodeIndex = cache[0] === dirruns && cache[1]; + diff = cache[0] === dirruns && cache[2]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + // Use previously-cached element index if available + } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { + diff = cache[1]; + + // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) + } else { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { + // Cache the index of each encountered element + if ( useCache ) { + (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifider + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsXML ? + elem.getAttribute("xml:lang") || elem.getAttribute("lang") : + elem.lang) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), + // not comment, processing instructions, or others + // Thanks to Diego Perini for the nodeName shortcut + // Greater than "@" means alpha characters (specifically not starting with "#" or "?") + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( tokens = [] ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push( { + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +} + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var data, cache, outerCache, + dirkey = dirruns + " " + doneName; + + // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { + if ( (data = cache[1]) === true || data === cachedruns ) { + return data === true; + } + } else { + cache = outerCache[ dir ] = [ dirkey ]; + cache[1] = matcher( elem, context, xml ) || cachedruns; + if ( cache[1] === true ) { + return true; + } + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( tokens.slice( 0, i - 1 ) ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + // A counter to specify which element is currently being matched + var matcherCachedRuns = 0, + bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, expandContext ) { + var elem, j, matcher, + setMatched = [], + matchedCount = 0, + i = "0", + unmatched = seed && [], + outermost = expandContext != null, + contextBackup = outermostContext, + // We must always have either seed elements or context + elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); + + if ( outermost ) { + outermostContext = context !== document && context; + cachedruns = matcherCachedRuns; + } + + // Add elements passing elementMatchers directly to results + // Keep `i` a string if there are no elements so `matchedCount` will be "00" below + for ( ; (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + cachedruns = ++matcherCachedRuns; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // Apply set filters to unmatched elements + matchedCount += i; + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !group ) { + group = tokenize( selector ); + } + i = group.length; + while ( i-- ) { + cached = matcherFromTokens( group[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + } + return cached; +}; + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function select( selector, context, results, seed ) { + var i, tokens, token, type, find, + match = tokenize( selector ); + + if ( !seed ) { + // Try to minimize operations if there is only one group + if ( match.length === 1 ) { + + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + context.nodeType === 9 && !documentIsXML && + Expr.relative[ tokens[1].type ] ) { + + context = Expr.find["ID"]( token.matches[0].replace( runescape, funescape ), context )[0]; + if ( !context ) { + return results; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && context.parentNode || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, slice.call( seed, 0 ) ); + return results; + } + + break; + } + } + } + } + } + + // Compile and execute a filtering function + // Provide `match` to avoid retokenization if we modified the selector above + compile( selector, match )( + seed, + context, + documentIsXML, + results, + rsibling.test( selector ) + ); + return results; +} + +// Deprecated +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Easy API for creating new setFilters +function setFilters() {} +Expr.filters = setFilters.prototype = Expr.pseudos; +Expr.setFilters = new setFilters(); + +// Initialize with the default document +setDocument(); + +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})( window ); +var runtil = /Until$/, + rparentsprev = /^(?:parents|prev(?:Until|All))/, + isSimple = /^.[^:#\[\.,]*$/, + rneedsContext = jQuery.expr.match.needsContext, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var i, ret, self, + len = this.length; + + if ( typeof selector !== "string" ) { + self = this; + return this.pushStack( jQuery( selector ).filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }) ); + } + + ret = []; + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, this[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = ( this.selector ? this.selector + " " : "" ) + selector; + return ret; + }, + + has: function( target ) { + var i, + targets = jQuery( target, this ), + len = targets.length; + + return this.filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false) ); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true) ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + rneedsContext.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + ret = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + cur = this[i]; + + while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + } + cur = cur.parentNode; + } + } + + return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( jQuery.unique(all) ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +jQuery.fn.andSelf = jQuery.fn.addBack; + +function sibling( cur, dir ) { + do { + cur = cur[ dir ]; + } while ( cur && cur.nodeType !== 1 ); + + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( this.length > 1 && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, + rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rtbody = /<tbody/i, + rhtml = /<|&#?\w+;/, + rnoInnerhtml = /<(?:script|style|link)/i, + manipulation_rcheckableType = /^(?:checkbox|radio)$/i, + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /^$|\/(?:java|ecma)script/i, + rscriptTypeMasked = /^true\/(.*)/, + rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g, + + // We have to close these tags to support XHTML (#13200) + wrapMap = { + option: [ 1, "<select multiple='multiple'>", "</select>" ], + legend: [ 1, "<fieldset>", "</fieldset>" ], + area: [ 1, "<map>", "</map>" ], + param: [ 1, "<object>", "</object>" ], + thead: [ 1, "<table>", "</table>" ], + tr: [ 2, "<table><tbody>", "</tbody></table>" ], + col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ], + td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], + + // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, + // unless wrapped in a div with non-breaking characters in front of it. + _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X<div>", "</div>" ] + }, + safeFragment = createSafeFragment( document ), + fragmentDiv = safeFragment.appendChild( document.createElement("div") ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +jQuery.fn.extend({ + text: function( value ) { + return jQuery.access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); + }, null, value, arguments.length ); + }, + + wrapAll: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapAll( html.call(this, i) ); + }); + } + + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); + + if ( this[0].parentNode ) { + wrap.insertBefore( this[0] ); + } + + wrap.map(function() { + var elem = this; + + while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { + elem = elem.firstChild; + } + + return elem; + }).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapInner( html.call(this, i) ); + }); + } + + return this.each(function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + }); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); + } + }).end(); + }, + + append: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.insertBefore( elem, this.firstChild ); + } + }); + }, + + before: function() { + return this.domManip( arguments, false, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + }); + }, + + after: function() { + return this.domManip( arguments, false, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + }); + }, + + // keepData is for internal use only--do not document + remove: function( selector, keepData ) { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + if ( !selector || jQuery.filter( selector, [ elem ] ).length > 0 ) { + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem ) ); + } + + if ( elem.parentNode ) { + if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { + setGlobalEval( getAll( elem, "script" ) ); + } + elem.parentNode.removeChild( elem ); + } + } + } + + return this; + }, + + empty: function() { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + } + + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + + // If this is a select, ensure that it displays empty (#12336) + // Support: IE<9 + if ( elem.options && jQuery.nodeName( elem, "select" ) ) { + elem.options.length = 0; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function () { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + return jQuery.access( this, function( value ) { + var elem = this[0] || {}, + i = 0, + l = this.length; + + if ( value === undefined ) { + return elem.nodeType === 1 ? + elem.innerHTML.replace( rinlinejQuery, "" ) : + undefined; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && + ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && + !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { + + value = value.replace( rxhtmlTag, "<$1></$2>" ); + + try { + for (; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + elem = this[i] || {}; + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch(e) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function( value ) { + var isFunc = jQuery.isFunction( value ); + + // Make sure that the elements are removed from the DOM before they are inserted + // this can help fix replacing a parent with child elements + if ( !isFunc && typeof value !== "string" ) { + value = jQuery( value ).not( this ).detach(); + } + + return this.domManip( [ value ], true, function( elem ) { + var next = this.nextSibling, + parent = this.parentNode; + + if ( parent ) { + jQuery( this ).remove(); + parent.insertBefore( elem, next ); + } + }); + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, table, callback ) { + + // Flatten any nested arrays + args = core_concat.apply( [], args ); + + var first, node, hasScripts, + scripts, doc, fragment, + i = 0, + l = this.length, + set = this, + iNoClone = l - 1, + value = args[0], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) { + return this.each(function( index ) { + var self = set.eq( index ); + if ( isFunction ) { + args[0] = value.call( this, index, table ? self.html() : undefined ); + } + self.domManip( args, table, callback ); + }); + } + + if ( l ) { + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first ) { + table = table && jQuery.nodeName( first, "tr" ); + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( + table && jQuery.nodeName( this[i], "table" ) ? + findOrAppend( this[i], "tbody" ) : + this[i], + node, + i + ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + + if ( node.src ) { + // Hope ajax is available... + jQuery.ajax({ + url: node.src, + type: "GET", + dataType: "script", + async: false, + global: false, + "throws": true + }); + } else { + jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); + } + } + } + } + + // Fix #11809: Avoid leaking memory + fragment = first = null; + } + } + + return this; + } +}); + +function findOrAppend( elem, tag ) { + return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) ); +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + var attr = elem.getAttributeNode("type"); + elem.type = ( attr && attr.specified ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + if ( match ) { + elem.type = match[1]; + } else { + elem.removeAttribute("type"); + } + return elem; +} + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var elem, + i = 0; + for ( ; (elem = elems[i]) != null; i++ ) { + jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); + } +} + +function cloneCopyEvent( src, dest ) { + + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } + + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function fixCloneNodeIssues( src, dest ) { + var nodeName, e, data; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + nodeName = dest.nodeName.toLowerCase(); + + // IE6-8 copies events bound via attachEvent when using cloneNode. + if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) { + data = jQuery._data( dest ); + + for ( e in data.events ) { + jQuery.removeEvent( dest, e, data.handle ); + } + + // Event data gets referenced instead of copied if the expando gets copied too + dest.removeAttribute( jQuery.expando ); + } + + // IE blanks contents when cloning scripts, and tries to evaluate newly-set text + if ( nodeName === "script" && dest.text !== src.text ) { + disableScript( dest ).text = src.text; + restoreScript( dest ); + + // IE6-10 improperly clones children of object elements using classid. + // IE10 throws NoModificationAllowedError if parent is null, #12132. + } else if ( nodeName === "object" ) { + if ( dest.parentNode ) { + dest.outerHTML = src.outerHTML; + } + + // This path appears unavoidable for IE9. When cloning an object + // element in IE9, the outerHTML strategy above is not sufficient. + // If the src has innerHTML and the destination does not, + // copy the src.innerHTML into the dest.innerHTML. #10324 + if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { + dest.innerHTML = src.innerHTML; + } + + } else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + + dest.defaultChecked = dest.checked = src.checked; + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.defaultSelected = dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + i = 0, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone(true); + jQuery( insert[i] )[ original ]( elems ); + + // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() + core_push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +}); + +function getAll( context, tag ) { + var elems, elem, + i = 0, + found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) : + undefined; + + if ( !found ) { + for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { + if ( !tag || jQuery.nodeName( elem, tag ) ) { + found.push( elem ); + } else { + jQuery.merge( found, getAll( elem, tag ) ); + } + } + } + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], found ) : + found; +} + +// Used in buildFragment, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( manipulation_rcheckableType.test( elem.type ) ) { + elem.defaultChecked = elem.checked; + } +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var destElements, node, clone, i, srcElements, + inPage = jQuery.contains( elem.ownerDocument, elem ); + + if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { + clone = elem.cloneNode( true ); + + // IE<=8 does not properly clone detached, unknown element nodes + } else { + fragmentDiv.innerHTML = elem.outerHTML; + fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); + } + + if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + // Fix all IE cloning issues + for ( i = 0; (node = srcElements[i]) != null; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + fixCloneNodeIssues( node, destElements[i] ); + } + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0; (node = srcElements[i]) != null; i++ ) { + cloneCopyEvent( node, destElements[i] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + destElements = srcElements = node = null; + + // Return the cloned set + return clone; + }, + + buildFragment: function( elems, context, scripts, selection ) { + var j, elem, contains, + tmp, tag, tbody, wrap, + l = elems.length, + + // Ensure a safe fragment + safe = createSafeFragment( context ), + + nodes = [], + i = 0; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || safe.appendChild( context.createElement("div") ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + + tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[2]; + + // Descend through wrappers to the right content + j = wrap[0]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Manually add leading whitespace removed by IE + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); + } + + // Remove IE's autoinserted <tbody> from table fragments + if ( !jQuery.support.tbody ) { + + // String was a <table>, *may* have spurious <tbody> + elem = tag === "table" && !rtbody.test( elem ) ? + tmp.firstChild : + + // String was a bare <thead> or <tfoot> + wrap[1] === "<table>" && !rtbody.test( elem ) ? + tmp : + 0; + + j = elem && elem.childNodes.length; + while ( j-- ) { + if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { + elem.removeChild( tbody ); + } + } + } + + jQuery.merge( nodes, tmp.childNodes ); + + // Fix #12392 for WebKit and IE > 9 + tmp.textContent = ""; + + // Fix #12392 for oldIE + while ( tmp.firstChild ) { + tmp.removeChild( tmp.firstChild ); + } + + // Remember the top-level container for proper cleanup + tmp = safe.lastChild; + } + } + } + + // Fix #11356: Clear elements from fragment + if ( tmp ) { + safe.removeChild( tmp ); + } + + // Reset defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + if ( !jQuery.support.appendChecked ) { + jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); + } + + i = 0; + while ( (elem = nodes[ i++ ]) ) { + + // #4087 - If origin and destination elements are the same, and this is + // that element, do not do anything + if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( safe.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( (elem = tmp[ j++ ]) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + tmp = null; + + return safe; + }, + + cleanData: function( elems, /* internal */ acceptData ) { + var elem, type, id, data, + i = 0, + internalKey = jQuery.expando, + cache = jQuery.cache, + deleteExpando = jQuery.support.deleteExpando, + special = jQuery.event.special; + + for ( ; (elem = elems[i]) != null; i++ ) { + + if ( acceptData || jQuery.acceptData( elem ) ) { + + id = elem[ internalKey ]; + data = id && cache[ id ]; + + if ( data ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Remove cache only if it was not already removed by jQuery.event.remove + if ( cache[ id ] ) { + + delete cache[ id ]; + + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( deleteExpando ) { + delete elem[ internalKey ]; + + } else if ( typeof elem.removeAttribute !== core_strundefined ) { + elem.removeAttribute( internalKey ); + + } else { + elem[ internalKey ] = null; + } + + core_deletedIds.push( id ); + } + } + } + } + } +}); +var iframe, getStyles, curCSS, + ralpha = /alpha\([^)]*\)/i, + ropacity = /opacity\s*=\s*([^)]*)/, + rposition = /^(top|right|bottom|left)$/, + // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" + // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rmargin = /^margin/, + rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), + rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), + rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ), + elemdisplay = { BODY: "block" }, + + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: 0, + fontWeight: 400 + }, + + cssExpand = [ "Top", "Right", "Bottom", "Left" ], + cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; + +// return a css property mapped to a potentially vendor prefixed property +function vendorPropName( style, name ) { + + // shortcut for names that are not vendor prefixed + if ( name in style ) { + return name; + } + + // check for vendor prefixed names + var capName = name.charAt(0).toUpperCase() + name.slice(1), + origName = name, + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in style ) { + return name; + } + } + + return origName; +} + +function isHidden( elem, el ) { + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); +} + +function showHide( elements, show ) { + var display, elem, hidden, + values = [], + index = 0, + length = elements.length; + + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + values[ index ] = jQuery._data( elem, "olddisplay" ); + display = elem.style.display; + if ( show ) { + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !values[ index ] && display === "none" ) { + elem.style.display = ""; + } + + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( elem.style.display === "" && isHidden( elem ) ) { + values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); + } + } else { + + if ( !values[ index ] ) { + hidden = isHidden( elem ); + + if ( display && display !== "none" || !hidden ) { + jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); + } + } + } + } + + // Set the display of most of the elements in a second loop + // to avoid the constant reflow + for ( index = 0; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + if ( !show || elem.style.display === "none" || elem.style.display === "" ) { + elem.style.display = show ? values[ index ] || "" : "none"; + } + } + + return elements; +} + +jQuery.fn.extend({ + css: function( name, value ) { + return jQuery.access( this, function( elem, name, value ) { + var len, styles, + map = {}, + i = 0; + + if ( jQuery.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + }, + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + var bool = typeof state === "boolean"; + + return this.each(function() { + if ( bool ? state : isHidden( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + }); + } +}); + +jQuery.extend({ + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Exclude the following css properties to add px + cssNumber: { + "columnCount": true, + "fillOpacity": true, + "fontWeight": true, + "lineHeight": true, + "opacity": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: { + // normalize float css property + "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" + }, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = jQuery.camelCase( name ), + style = elem.style; + + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // convert relative number strings (+= or -=) to relative numbers. #7345 + if ( type === "string" && (ret = rrelNum.exec( value )) ) { + value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); + // Fixes bug #9237 + type = "number"; + } + + // Make sure that NaN and null values aren't set. See: #7116 + if ( value == null || type === "number" && isNaN( value ) ) { + return; + } + + // If a number was passed in, add 'px' to the (except for certain CSS properties) + if ( type === "number" && !jQuery.cssNumber[ origName ] ) { + value += "px"; + } + + // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, + // but it would mean to define eight (for every problematic property) identical functions + if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { + + // Wrapped to prevent IE from throwing errors when 'invalid' values are provided + // Fixes bug #5509 + try { + style[ name ] = value; + } catch(e) {} + } + + } else { + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var num, val, hooks, + origName = jQuery.camelCase( name ); + + // Make sure that we're working with the right name + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + //convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Return, converting to number if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; + } + return val; + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback, args ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.apply( elem, args || [] ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; + } +}); + +// NOTE: we've included the "window" in window.getComputedStyle +// because jsdom on node.js will break without it. +if ( window.getComputedStyle ) { + getStyles = function( elem ) { + return window.getComputedStyle( elem, null ); + }; + + curCSS = function( elem, name, _computed ) { + var width, minWidth, maxWidth, + computed = _computed || getStyles( elem ), + + // getPropertyValue is only needed for .css('filter') in IE9, see #12537 + ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined, + style = elem.style; + + if ( computed ) { + + if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right + // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels + // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values + if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret; + }; +} else if ( document.documentElement.currentStyle ) { + getStyles = function( elem ) { + return elem.currentStyle; + }; + + curCSS = function( elem, name, _computed ) { + var left, rs, rsLeft, + computed = _computed || getStyles( elem ), + ret = computed ? computed[ name ] : undefined, + style = elem.style; + + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret == null && style && style[ name ] ) { + ret = style[ name ]; + } + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + // but not position css attributes, as those are proportional to the parent element instead + // and we can't measure the parent instead because it might trigger a "stacking dolls" problem + if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { + + // Remember the original values + left = style.left; + rs = elem.runtimeStyle; + rsLeft = rs && rs.left; + + // Put in the new values to get a computed value out + if ( rsLeft ) { + rs.left = elem.currentStyle.left; + } + style.left = name === "fontSize" ? "1em" : ret; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + if ( rsLeft ) { + rs.left = rsLeft; + } + } + + return ret === "" ? "auto" : ret; + }; +} + +function setPositiveNumber( elem, value, subtract ) { + var matches = rnumsplit.exec( value ); + return matches ? + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : + value; +} + +function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { + var i = extra === ( isBorderBox ? "border" : "content" ) ? + // If we already have the right measurement, avoid augmentation + 4 : + // Otherwise initialize for horizontal or vertical properties + name === "width" ? 1 : 0, + + val = 0; + + for ( ; i < 4; i += 2 ) { + // both box models exclude margin, so add it if we want it + if ( extra === "margin" ) { + val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); + } + + if ( isBorderBox ) { + // border-box includes padding, so remove it if we want content + if ( extra === "content" ) { + val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // at this point, extra isn't border nor margin, so remove border + if ( extra !== "margin" ) { + val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } else { + // at this point, extra isn't content, so add padding + val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // at this point, extra isn't content nor padding, so add border + if ( extra !== "padding" ) { + val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + return val; +} + +function getWidthOrHeight( elem, name, extra ) { + + // Start with offset property, which is equivalent to the border-box value + var valueIsBorderBox = true, + val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + styles = getStyles( elem ), + isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // some non-html elements return undefined for offsetWidth, so check for null/undefined + // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 + // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 + if ( val <= 0 || val == null ) { + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name, styles ); + if ( val < 0 || val == null ) { + val = elem.style[ name ]; + } + + // Computed unit is not pixels. Stop here and return. + if ( rnumnonpx.test(val) ) { + return val; + } + + // we need the check for style in case a browser which returns unreliable values + // for getComputedStyle silently falls back to the reliable elem.style + valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); + + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + } + + // use the active box-sizing model to add/subtract irrelevant styles + return ( val + + augmentWidthOrHeight( + elem, + name, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles + ) + ) + "px"; +} + +// Try to determine the default display value of an element +function css_defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + // Use the already-created iframe if possible + iframe = ( iframe || + jQuery("<iframe frameborder='0' width='0' height='0'/>") + .css( "cssText", "display:block !important" ) + ).appendTo( doc.documentElement ); + + // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse + doc = ( iframe[0].contentWindow || iframe[0].contentDocument ).document; + doc.write("<!doctype html><html><body>"); + doc.close(); + + display = actualDisplay( nodeName, doc ); + iframe.detach(); + } + + // Store the correct default display + elemdisplay[ nodeName ] = display; + } + + return display; +} + +// Called ONLY from within css_defaultDisplay +function actualDisplay( name, doc ) { + var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), + display = jQuery.css( elem[0], "display" ); + elem.remove(); + return display; +} + +jQuery.each([ "height", "width" ], function( i, name ) { + jQuery.cssHooks[ name ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + // certain elements can have dimension info if we invisibly show them + // however, it must have a current display style that would benefit from this + return elem.offsetWidth === 0 && rdisplayswap.test( jQuery.css( elem, "display" ) ) ? + jQuery.swap( elem, cssShow, function() { + return getWidthOrHeight( elem, name, extra ); + }) : + getWidthOrHeight( elem, name, extra ); + } + }, + + set: function( elem, value, extra ) { + var styles = extra && getStyles( elem ); + return setPositiveNumber( elem, value, extra ? + augmentWidthOrHeight( + elem, + name, + extra, + jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + styles + ) : 0 + ); + } + }; +}); + +if ( !jQuery.support.opacity ) { + jQuery.cssHooks.opacity = { + get: function( elem, computed ) { + // IE uses filters for opacity + return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? + ( 0.01 * parseFloat( RegExp.$1 ) ) + "" : + computed ? "1" : ""; + }, + + set: function( elem, value ) { + var style = elem.style, + currentStyle = elem.currentStyle, + opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", + filter = currentStyle && currentStyle.filter || style.filter || ""; + + // IE has trouble with opacity if it does not have layout + // Force it by setting the zoom level + style.zoom = 1; + + // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 + // if value === "", then remove inline opacity #12685 + if ( ( value >= 1 || value === "" ) && + jQuery.trim( filter.replace( ralpha, "" ) ) === "" && + style.removeAttribute ) { + + // Setting style.filter to null, "" & " " still leave "filter:" in the cssText + // if "filter:" is present at all, clearType is disabled, we want to avoid this + // style.removeAttribute is IE Only, but so apparently is this code path... + style.removeAttribute( "filter" ); + + // if there is no filter style applied in a css rule or unset inline opacity, we are done + if ( value === "" || currentStyle && !currentStyle.filter ) { + return; + } + } + + // otherwise, set new filter values + style.filter = ralpha.test( filter ) ? + filter.replace( ralpha, opacity ) : + filter + " " + opacity; + } + }; +} + +// These hooks cannot be added until DOM ready because the support test +// for it is not run until after DOM ready +jQuery(function() { + if ( !jQuery.support.reliableMarginRight ) { + jQuery.cssHooks.marginRight = { + get: function( elem, computed ) { + if ( computed ) { + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + // Work around by temporarily setting element display to inline-block + return jQuery.swap( elem, { "display": "inline-block" }, + curCSS, [ elem, "marginRight" ] ); + } + } + }; + } + + // Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 + // getComputedStyle returns percent when specified for top/left/bottom/right + // rather than make the css module depend on the offset module, we just check for it here + if ( !jQuery.support.pixelPosition && jQuery.fn.position ) { + jQuery.each( [ "top", "left" ], function( i, prop ) { + jQuery.cssHooks[ prop ] = { + get: function( elem, computed ) { + if ( computed ) { + computed = curCSS( elem, prop ); + // if curCSS returns percentage, fallback to offset + return rnumnonpx.test( computed ) ? + jQuery( elem ).position()[ prop ] + "px" : + computed; + } + } + }; + }); + } + +}); + +if ( jQuery.expr && jQuery.expr.filters ) { + jQuery.expr.filters.hidden = function( elem ) { + // Support: Opera <= 12.12 + // Opera reports offsetWidths and offsetHeights less than zero on some elements + return elem.offsetWidth <= 0 && elem.offsetHeight <= 0 || + (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none"); + }; + + jQuery.expr.filters.visible = function( elem ) { + return !jQuery.expr.filters.hidden( elem ); + }; +} + +// These hooks are used by animate to expand properties +jQuery.each({ + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + // assumes a single number if not a string + parts = typeof value === "string" ? value.split(" ") : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( !rmargin.test( prefix ) ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +}); +var r20 = /%20/g, + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +jQuery.fn.extend({ + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map(function(){ + // Can add propHook for "elements" to filter or add form elements + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + }) + .filter(function(){ + var type = this.type; + // Use .is(":disabled") so that fieldset[disabled] works + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !manipulation_rcheckableType.test( type ) ); + }) + .map(function( i, elem ){ + var val = jQuery( this ).val(); + + return val == null ? + null : + jQuery.isArray( val ) ? + jQuery.map( val, function( val ){ + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + }) : + { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + }).get(); + } +}); + +//Serialize an array of form elements or a set of +//key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, value ) { + // If value is a function, invoke it and return its value + value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value ); + s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); + }; + + // Set traditional to true for jQuery <= 1.3.2 behavior. + if ( traditional === undefined ) { + traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + }); + + } else { + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ).replace( r20, "+" ); +}; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( jQuery.isArray( obj ) ) { + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + // Item is non-scalar (array or object), encode its numeric index. + buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add ); + } + }); + + } else if ( !traditional && jQuery.type( obj ) === "object" ) { + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + // Serialize scalar item. + add( prefix, obj ); + } +} +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; +}); + +jQuery.fn.hover = function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); +}; +var + // Document location + ajaxLocParts, + ajaxLocation, + ajax_nonce = jQuery.now(), + + ajax_rquery = /\?/, + rhash = /#.*$/, + rts = /([?&])_=[^&]*/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + rurl = /^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/, + + // Keep a copy of the old load method + _load = jQuery.fn.load, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = "*/".concat("*"); + +// #8138, IE may throw an exception when accessing +// a field from window.location if document.domain has been set +try { + ajaxLocation = location.href; +} catch( e ) { + // Use the href attribute of an A element + // since IE will modify it given document.location + ajaxLocation = document.createElement( "a" ); + ajaxLocation.href = ""; + ajaxLocation = ajaxLocation.href; +} + +// Segment location into parts +ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, + i = 0, + dataTypes = dataTypeExpression.toLowerCase().match( core_rnotwhite ) || []; + + if ( jQuery.isFunction( func ) ) { + // For each dataType in the dataTypeExpression + while ( (dataType = dataTypes[i++]) ) { + // Prepend if requested + if ( dataType[0] === "+" ) { + dataType = dataType.slice( 1 ) || "*"; + (structure[ dataType ] = structure[ dataType ] || []).unshift( func ); + + // Otherwise append + } else { + (structure[ dataType ] = structure[ dataType ] || []).push( func ); + } + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { + + var inspected = {}, + seekingTransport = ( structure === transports ); + + function inspect( dataType ) { + var selected; + inspected[ dataType ] = true; + jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { + var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); + if( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + options.dataTypes.unshift( dataTypeOrTransport ); + inspect( dataTypeOrTransport ); + return false; + } else if ( seekingTransport ) { + return !( selected = dataTypeOrTransport ); + } + }); + return selected; + } + + return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var deep, key, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } + + return target; +} + +jQuery.fn.load = function( url, params, callback ) { + if ( typeof url !== "string" && _load ) { + return _load.apply( this, arguments ); + } + + var selector, response, type, + self = this, + off = url.indexOf(" "); + + if ( off >= 0 ) { + selector = url.slice( off, url.length ); + url = url.slice( 0, off ); + } + + // If it's a function + if ( jQuery.isFunction( params ) ) { + + // We assume that it's the callback + callback = params; + params = undefined; + + // Otherwise, build a param string + } else if ( params && typeof params === "object" ) { + type = "POST"; + } + + // If we have elements to modify, make the request + if ( self.length > 0 ) { + jQuery.ajax({ + url: url, + + // if "type" variable is undefined, then "GET" method will be used + type: type, + dataType: "html", + data: params + }).done(function( responseText ) { + + // Save response for use in complete callback + response = arguments; + + self.html( selector ? + + // If a selector was specified, locate the right elements in a dummy div + // Exclude scripts to avoid IE 'Permission Denied' errors + jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) : + + // Otherwise use the full result + responseText ); + + }).complete( callback && function( jqXHR, status ) { + self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] ); + }); + } + + return this; +}; + +// Attach a bunch of functions for handling common AJAX events +jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ){ + jQuery.fn[ type ] = function( fn ){ + return this.on( type, fn ); + }; +}); + +jQuery.each( [ "get", "post" ], function( i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + // shift arguments if data argument was omitted + if ( jQuery.isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + return jQuery.ajax({ + url: url, + type: method, + dataType: type, + data: data, + success: callback + }); + }; +}); + +jQuery.extend({ + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {}, + + ajaxSettings: { + url: ajaxLocation, + type: "GET", + isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), + global: true, + processData: true, + async: true, + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + + accepts: { + "*": allTypes, + text: "text/plain", + html: "text/html", + xml: "application/xml, text/xml", + json: "application/json, text/javascript" + }, + + contents: { + xml: /xml/, + html: /html/, + json: /json/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText" + }, + + // Data converters + // Keys separate source (or catchall "*") and destination types with a single space + converters: { + + // Convert anything to text + "* text": window.String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": jQuery.parseJSON, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + url: true, + context: true + } + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + return settings ? + + // Building a settings object + ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : + + // Extending ajaxSettings + ajaxExtend( jQuery.ajaxSettings, target ); + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var // Cross-domain detection vars + parts, + // Loop variable + i, + // URL without anti-cache param + cacheURL, + // Response headers as string + responseHeadersString, + // timeout handle + timeoutTimer, + + // To know if global events are to be dispatched + fireGlobals, + + transport, + // Response headers + responseHeaders, + // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + // Callbacks context + callbackContext = s.context || s, + // Context for global events is callbackContext if it is a DOM node or jQuery collection + globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ? + jQuery( callbackContext ) : + jQuery.event, + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks("once memory"), + // Status-dependent callbacks + statusCode = s.statusCode || {}, + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + // The jqXHR state + state = 0, + // Default abort message + strAbort = "canceled", + // Fake xhr + jqXHR = { + readyState: 0, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( state === 2 ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while ( (match = rheaders.exec( responseHeadersString )) ) { + responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; + } + } + match = responseHeaders[ key.toLowerCase() ]; + } + return match == null ? null : match; + }, + + // Raw string + getAllResponseHeaders: function() { + return state === 2 ? responseHeadersString : null; + }, + + // Caches the header + setRequestHeader: function( name, value ) { + var lname = name.toLowerCase(); + if ( !state ) { + name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( !state ) { + s.mimeType = type; + } + return this; + }, + + // Status-dependent callbacks + statusCode: function( map ) { + var code; + if ( map ) { + if ( state < 2 ) { + for ( code in map ) { + // Lazy-add the new callback in a way that preserves old ones + statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; + } + } else { + // Execute the appropriate callbacks + jqXHR.always( map[ jqXHR.status ] ); + } + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + var finalText = statusText || strAbort; + if ( transport ) { + transport.abort( finalText ); + } + done( 0, finalText ); + return this; + } + }; + + // Attach deferreds + deferred.promise( jqXHR ).complete = completeDeferred.add; + jqXHR.success = jqXHR.done; + jqXHR.error = jqXHR.fail; + + // Remove hash character (#7531: and string promotion) + // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) + // Handle falsy url in the settings object (#10093: consistency with old signature) + // We also use the url parameter if available + s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); + + // Alias method option to type as per ticket #12004 + s.type = options.method || options.type || s.method || s.type; + + // Extract dataTypes list + s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( core_rnotwhite ) || [""]; + + // A cross-domain request is in order when we have a protocol:host:port mismatch + if ( s.crossDomain == null ) { + parts = rurl.exec( s.url.toLowerCase() ); + s.crossDomain = !!( parts && + ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] || + ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) != + ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) ) + ); + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefilter, stop there + if ( state === 2 ) { + return jqXHR; + } + + // We can fire global events as of now if asked to + fireGlobals = s.global; + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger("ajaxStart"); + } + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Save the URL in case we're toying with the If-Modified-Since + // and/or If-None-Match header later on + cacheURL = s.url; + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // If data is available, append data to url + if ( s.data ) { + cacheURL = ( s.url += ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + s.data ); + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Add anti-cache in url if needed + if ( s.cache === false ) { + s.url = rts.test( cacheURL ) ? + + // If there is already a '_' parameter, set its value + cacheURL.replace( rts, "$1_=" + ajax_nonce++ ) : + + // Otherwise add one to the end + cacheURL + ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ajax_nonce++; + } + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery.lastModified[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); + } + if ( jQuery.etag[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? + s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { + // Abort if not done already and return + return jqXHR.abort(); + } + + // aborting is no longer a cancellation + strAbort = "abort"; + + // Install callbacks on deferreds + for ( i in { success: 1, error: 1, complete: 1 } ) { + jqXHR[ i ]( s[ i ] ); + } + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = setTimeout(function() { + jqXHR.abort("timeout"); + }, s.timeout ); + } + + try { + state = 1; + transport.send( requestHeaders, done ); + } catch ( e ) { + // Propagate exception as error if not done + if ( state < 2 ) { + done( -1, e ); + // Simply rethrow otherwise + } else { + throw e; + } + } + } + + // Callback for when everything is done + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + // Called once + if ( state === 2 ) { + return; + } + + // State is "done" now + state = 2; + + // Clear timeout if it exists + if ( timeoutTimer ) { + clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + // Get response data + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + // If successful, handle type chaining + if ( status >= 200 && status < 300 || status === 304 ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + modified = jqXHR.getResponseHeader("Last-Modified"); + if ( modified ) { + jQuery.lastModified[ cacheURL ] = modified; + } + modified = jqXHR.getResponseHeader("etag"); + if ( modified ) { + jQuery.etag[ cacheURL ] = modified; + } + } + + // if no content + if ( status === 204 ) { + isSuccess = true; + statusText = "nocontent"; + + // if not modified + } else if ( status === 304 ) { + isSuccess = true; + statusText = "notmodified"; + + // If we have data, let's convert it + } else { + isSuccess = ajaxConvert( s, response ); + statusText = isSuccess.state; + success = isSuccess.data; + error = isSuccess.error; + isSuccess = !error; + } + } else { + // We extract error from statusText + // then normalize statusText and status for non-aborts + error = statusText; + if ( status || !statusText ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger("ajaxStop"); + } + } + } + + return jqXHR; + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + } +}); + +/* Handles responses to an ajax request: + * - sets all responseXXX fields accordingly + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + var firstDataType, ct, finalDataType, type, + contents = s.contents, + dataTypes = s.dataTypes, + responseFields = s.responseFields; + + // Fill responseXXX fields + for ( type in responseFields ) { + if ( type in responses ) { + jqXHR[ responseFields[type] ] = responses[ type ]; + } + } + + // Remove auto dataType and get content-type in the process + while( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader("Content-Type"); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +// Chain conversions given the request and the original response +function ajaxConvert( s, response ) { + var conv2, current, conv, tmp, + converters = {}, + i = 0, + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(), + prev = dataTypes[ 0 ]; + + // Apply the dataFilter if provided + if ( s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + // Convert to each sequential dataType, tolerating list modification + for ( ; (current = dataTypes[++i]); ) { + + // There's only work to do if current dataType is non-auto + if ( current !== "*" ) { + + // Convert response if prev dataType is non-auto and differs from current + if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split(" "); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.splice( i--, 0, current ); + } + + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s["throws"] ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current }; + } + } + } + } + + // Update prev for next iteration + prev = current; + } + } + + return { state: "success", data: response }; +} +// Install script dataType +jQuery.ajaxSetup({ + accepts: { + script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /(?:java|ecma)script/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +}); + +// Handle cache's special case and global +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + s.global = false; + } +}); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function(s) { + + // This transport only deals with cross domain requests + if ( s.crossDomain ) { + + var script, + head = document.head || jQuery("head")[0] || document.documentElement; + + return { + + send: function( _, callback ) { + + script = document.createElement("script"); + + script.async = true; + + if ( s.scriptCharset ) { + script.charset = s.scriptCharset; + } + + script.src = s.url; + + // Attach handlers for all browsers + script.onload = script.onreadystatechange = function( _, isAbort ) { + + if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { + + // Handle memory leak in IE + script.onload = script.onreadystatechange = null; + + // Remove the script + if ( script.parentNode ) { + script.parentNode.removeChild( script ); + } + + // Dereference the script + script = null; + + // Callback if not abort + if ( !isAbort ) { + callback( 200, "success" ); + } + } + }; + + // Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending + // Use native DOM manipulation to avoid our domManip AJAX trickery + head.insertBefore( script, head.firstChild ); + }, + + abort: function() { + if ( script ) { + script.onload( undefined, true ); + } + } + }; + } +}); +var oldCallbacks = [], + rjsonp = /(=)\?(?=&|$)|\?\?/; + +// Default jsonp settings +jQuery.ajaxSetup({ + jsonp: "callback", + jsonpCallback: function() { + var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( ajax_nonce++ ) ); + this[ callback ] = true; + return callback; + } +}); + +// Detect, normalize options and install callbacks for jsonp requests +jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { + + var callbackName, overwritten, responseContainer, + jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ? + "url" : + typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data" + ); + + // Handle iff the expected data type is "jsonp" or we have a parameter to set + if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) { + + // Get callback name, remembering preexisting value associated with it + callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ? + s.jsonpCallback() : + s.jsonpCallback; + + // Insert callback into url or form data + if ( jsonProp ) { + s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName ); + } else if ( s.jsonp !== false ) { + s.url += ( ajax_rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName; + } + + // Use data converter to retrieve json after script execution + s.converters["script json"] = function() { + if ( !responseContainer ) { + jQuery.error( callbackName + " was not called" ); + } + return responseContainer[ 0 ]; + }; + + // force json dataType + s.dataTypes[ 0 ] = "json"; + + // Install callback + overwritten = window[ callbackName ]; + window[ callbackName ] = function() { + responseContainer = arguments; + }; + + // Clean-up function (fires after converters) + jqXHR.always(function() { + // Restore preexisting value + window[ callbackName ] = overwritten; + + // Save back as free + if ( s[ callbackName ] ) { + // make sure that re-using the options doesn't screw things around + s.jsonpCallback = originalSettings.jsonpCallback; + + // save the callback name for future use + oldCallbacks.push( callbackName ); + } + + // Call if it was a function and we have a response + if ( responseContainer && jQuery.isFunction( overwritten ) ) { + overwritten( responseContainer[ 0 ] ); + } + + responseContainer = overwritten = undefined; + }); + + // Delegate to script + return "script"; + } +}); +var xhrCallbacks, xhrSupported, + xhrId = 0, + // #5280: Internet Explorer will keep connections alive if we don't abort on unload + xhrOnUnloadAbort = window.ActiveXObject && function() { + // Abort all pending requests + var key; + for ( key in xhrCallbacks ) { + xhrCallbacks[ key ]( undefined, true ); + } + }; + +// Functions to create xhrs +function createStandardXHR() { + try { + return new window.XMLHttpRequest(); + } catch( e ) {} +} + +function createActiveXHR() { + try { + return new window.ActiveXObject("Microsoft.XMLHTTP"); + } catch( e ) {} +} + +// Create the request object +// (This is still attached to ajaxSettings for backward compatibility) +jQuery.ajaxSettings.xhr = window.ActiveXObject ? + /* Microsoft failed to properly + * implement the XMLHttpRequest in IE7 (can't request local files), + * so we use the ActiveXObject when it is available + * Additionally XMLHttpRequest can be disabled in IE7/IE8 so + * we need a fallback. + */ + function() { + return !this.isLocal && createStandardXHR() || createActiveXHR(); + } : + // For all other browsers, use the standard XMLHttpRequest object + createStandardXHR; + +// Determine support properties +xhrSupported = jQuery.ajaxSettings.xhr(); +jQuery.support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +xhrSupported = jQuery.support.ajax = !!xhrSupported; + +// Create transport if the browser can provide an xhr +if ( xhrSupported ) { + + jQuery.ajaxTransport(function( s ) { + // Cross domain only allowed if supported through XMLHttpRequest + if ( !s.crossDomain || jQuery.support.cors ) { + + var callback; + + return { + send: function( headers, complete ) { + + // Get a new xhr + var handle, i, + xhr = s.xhr(); + + // Open the socket + // Passing null username, generates a login popup on Opera (#2865) + if ( s.username ) { + xhr.open( s.type, s.url, s.async, s.username, s.password ); + } else { + xhr.open( s.type, s.url, s.async ); + } + + // Apply custom fields if provided + if ( s.xhrFields ) { + for ( i in s.xhrFields ) { + xhr[ i ] = s.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( s.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( s.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !s.crossDomain && !headers["X-Requested-With"] ) { + headers["X-Requested-With"] = "XMLHttpRequest"; + } + + // Need an extra try/catch for cross domain requests in Firefox 3 + try { + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + } catch( err ) {} + + // Do send the request + // This may raise an exception which is actually + // handled in jQuery.ajax (so no try/catch here) + xhr.send( ( s.hasContent && s.data ) || null ); + + // Listener + callback = function( _, isAbort ) { + var status, responseHeaders, statusText, responses; + + // Firefox throws exceptions when accessing properties + // of an xhr when a network error occurred + // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE) + try { + + // Was never called and is aborted or complete + if ( callback && ( isAbort || xhr.readyState === 4 ) ) { + + // Only called once + callback = undefined; + + // Do not keep as active anymore + if ( handle ) { + xhr.onreadystatechange = jQuery.noop; + if ( xhrOnUnloadAbort ) { + delete xhrCallbacks[ handle ]; + } + } + + // If it's an abort + if ( isAbort ) { + // Abort it manually if needed + if ( xhr.readyState !== 4 ) { + xhr.abort(); + } + } else { + responses = {}; + status = xhr.status; + responseHeaders = xhr.getAllResponseHeaders(); + + // When requesting binary data, IE6-9 will throw an exception + // on any attempt to access responseText (#11426) + if ( typeof xhr.responseText === "string" ) { + responses.text = xhr.responseText; + } + + // Firefox throws an exception when accessing + // statusText for faulty cross-domain requests + try { + statusText = xhr.statusText; + } catch( e ) { + // We normalize with Webkit giving an empty statusText + statusText = ""; + } + + // Filter status for non standard behaviors + + // If the request is local and we have data: assume a success + // (success with no data won't get notified, that's the best we + // can do given current implementations) + if ( !status && s.isLocal && !s.crossDomain ) { + status = responses.text ? 200 : 404; + // IE - #1450: sometimes returns 1223 when it should be 204 + } else if ( status === 1223 ) { + status = 204; + } + } + } + } catch( firefoxAccessException ) { + if ( !isAbort ) { + complete( -1, firefoxAccessException ); + } + } + + // Call complete if needed + if ( responses ) { + complete( status, statusText, responses, responseHeaders ); + } + }; + + if ( !s.async ) { + // if we're in sync mode we fire the callback + callback(); + } else if ( xhr.readyState === 4 ) { + // (IE6 & IE7) if it's in cache and has been + // retrieved directly we need to fire the callback + setTimeout( callback ); + } else { + handle = ++xhrId; + if ( xhrOnUnloadAbort ) { + // Create the active xhrs callbacks list if needed + // and attach the unload handler + if ( !xhrCallbacks ) { + xhrCallbacks = {}; + jQuery( window ).unload( xhrOnUnloadAbort ); + } + // Add to list of active xhrs callbacks + xhrCallbacks[ handle ] = callback; + } + xhr.onreadystatechange = callback; + } + }, + + abort: function() { + if ( callback ) { + callback( undefined, true ); + } + } + }; + } + }); +} +var fxNow, timerId, + rfxtypes = /^(?:toggle|show|hide)$/, + rfxnum = new RegExp( "^(?:([+-])=|)(" + core_pnum + ")([a-z%]*)$", "i" ), + rrun = /queueHooks$/, + animationPrefilters = [ defaultPrefilter ], + tweeners = { + "*": [function( prop, value ) { + var end, unit, + tween = this.createTween( prop, value ), + parts = rfxnum.exec( value ), + target = tween.cur(), + start = +target || 0, + scale = 1, + maxIterations = 20; + + if ( parts ) { + end = +parts[2]; + unit = parts[3] || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + + // We need to compute starting value + if ( unit !== "px" && start ) { + // Iteratively approximate from a nonzero starting point + // Prefer the current property, because this process will be trivial if it uses the same units + // Fallback to end or a simple constant + start = jQuery.css( tween.elem, prop, true ) || end || 1; + + do { + // If previous iteration zeroed out, double until we get *something* + // Use a string for doubling factor so we don't accidentally see scale as unchanged below + scale = scale || ".5"; + + // Adjust and apply + start = start / scale; + jQuery.style( tween.elem, prop, start + unit ); + + // Update scale, tolerating zero or NaN from tween.cur() + // And breaking the loop if scale is unchanged or perfect, or if we've just had enough + } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations ); + } + + tween.unit = unit; + tween.start = start; + // If a +=/-= token was provided, we're doing a relative animation + tween.end = parts[1] ? start + ( parts[1] + 1 ) * end : end; + } + return tween; + }] + }; + +// Animations created synchronously will run synchronously +function createFxNow() { + setTimeout(function() { + fxNow = undefined; + }); + return ( fxNow = jQuery.now() ); +} + +function createTweens( animation, props ) { + jQuery.each( props, function( prop, value ) { + var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( collection[ index ].call( animation, prop, value ) ) { + + // we're done with this property + return; + } + } + }); +} + +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = animationPrefilters.length, + deferred = jQuery.Deferred().always( function() { + // don't match elem in the :animated selector + delete tick.elem; + }), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length ; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ]); + + if ( percent < 1 && length ) { + return remaining; + } else { + deferred.resolveWith( elem, [ animation ] ); + return false; + } + }, + animation = deferred.promise({ + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { specialEasing: {} }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + // if we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length ; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + // resolve when we played the last frame + // otherwise, reject + if ( gotoEnd ) { + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + }), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length ; index++ ) { + result = animationPrefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + return result; + } + } + + createTweens( animation, props ); + + if ( jQuery.isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + }) + ); + + // attach callbacks from options + return animation.progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); +} + +function propFilter( props, specialEasing ) { + var value, name, index, easing, hooks; + + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = jQuery.camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( jQuery.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + // not quite $.extend, this wont overwrite keys already present. + // also - reusing 'index' from above because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweener: function( props, callback ) { + if ( jQuery.isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.split(" "); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length ; index++ ) { + prop = props[ index ]; + tweeners[ prop ] = tweeners[ prop ] || []; + tweeners[ prop ].unshift( callback ); + } + }, + + prefilter: function( callback, prepend ) { + if ( prepend ) { + animationPrefilters.unshift( callback ); + } else { + animationPrefilters.push( callback ); + } + } +}); + +function defaultPrefilter( elem, props, opts ) { + /*jshint validthis:true */ + var prop, index, length, + value, dataShow, toggle, + tween, hooks, oldfire, + anim = this, + style = elem.style, + orig = {}, + handled = [], + hidden = elem.nodeType && isHidden( elem ); + + // handle queue: false promises + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always(function() { + // doing this makes sure that the complete handler will be called + // before this completes + anim.always(function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + }); + }); + } + + // height/width overflow pass + if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) { + // Make sure that nothing sneaks out + // Record all 3 overflow attributes because IE does not + // change the overflow attribute when overflowX and + // overflowY are set to the same value + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Set display property to inline-block for height/width + // animations on inline elements that are having width/height animated + if ( jQuery.css( elem, "display" ) === "inline" && + jQuery.css( elem, "float" ) === "none" ) { + + // inline-level elements accept inline-block; + // block-level elements need to be inline with layout + if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) { + style.display = "inline-block"; + + } else { + style.zoom = 1; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + if ( !jQuery.support.shrinkWrapBlocks ) { + anim.always(function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + }); + } + } + + + // show/hide pass + for ( index in props ) { + value = props[ index ]; + if ( rfxtypes.exec( value ) ) { + delete props[ index ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + continue; + } + handled.push( index ); + } + } + + length = handled.length; + if ( length ) { + dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} ); + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + + // store state if its toggle - enables .stop().toggle() to "reverse" + if ( toggle ) { + dataShow.hidden = !hidden; + } + if ( hidden ) { + jQuery( elem ).show(); + } else { + anim.done(function() { + jQuery( elem ).hide(); + }); + } + anim.done(function() { + var prop; + jQuery._removeData( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + }); + for ( index = 0 ; index < length ; index++ ) { + prop = handled[ index ]; + tween = anim.createTween( prop, hidden ? dataShow[ prop ] : 0 ); + orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop ); + + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = tween.start; + if ( hidden ) { + tween.end = tween.start; + tween.start = prop === "width" || prop === "height" ? 1 : 0; + } + } + } + } +} + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || "swing"; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + if ( tween.elem[ tween.prop ] != null && + (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) { + return tween.elem[ tween.prop ]; + } + + // passing an empty string as a 3rd parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails + // so, simple values such as "10px" are parsed to Float. + // complex values such as "rotate(1rad)" are returned as is. + result = jQuery.css( tween.elem, tween.prop, "" ); + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + // use step hook for back compat - use cssHook if its there - use .style if its + // available and use plain properties where available + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +// Remove in 2.0 - this supports IE8's panic based approach +// to setting things on disconnected nodes + +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.each([ "toggle", "show", "hide" ], function( i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +}); + +jQuery.fn.extend({ + fadeTo: function( speed, to, easing, callback ) { + + // show any hidden elements after setting opacity to 0 + return this.filter( isHidden ).css( "opacity", 0 ).show() + + // animate to the value specified + .end().animate({ opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + doAnimation.finish = function() { + anim.stop( true ); + }; + // Empty animations, or finishing resolves immediately + if ( empty || jQuery._data( this, "finish" ) ) { + anim.stop( true ); + } + }; + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue && type !== false ) { + this.queue( type || "fx", [] ); + } + + return this.each(function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = jQuery._data( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // start the next in the queue if the last step wasn't forced + // timers currently will call their complete callbacks, which will dequeue + // but only if they were gotoEnd + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + }); + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each(function() { + var index, + data = jQuery._data( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + // enable finishing flag on private data + data.finish = true; + + // empty the queue first + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.cur && hooks.cur.finish ) { + hooks.cur.finish.call( this ); + } + + // look for any active animations, and finish them + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + // look for any animations in the old queue and finish them + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + // turn off finishing flag + delete data.finish; + }); + } +}); + +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + attrs = { height: type }, + i = 0; + + // if we include width, step value is 1 to do all cssExpand values, + // if we don't include width, step value is 2 to skip over Left and Right + includeWidth = includeWidth? 1 : 0; + for( ; i < 4 ; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +// Generate shortcuts for custom animations +jQuery.each({ + slideDown: genFx("show"), + slideUp: genFx("hide"), + slideToggle: genFx("toggle"), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +}); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + jQuery.isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing + }; + + opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : + opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; + + // normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( jQuery.isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p*Math.PI ) / 2; + } +}; + +jQuery.timers = []; +jQuery.fx = Tween.prototype.init; +jQuery.fx.tick = function() { + var timer, + timers = jQuery.timers, + i = 0; + + fxNow = jQuery.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + // Checks the timer has not already been removed + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + if ( timer() && jQuery.timers.push( timer ) ) { + jQuery.fx.start(); + } +}; + +jQuery.fx.interval = 13; + +jQuery.fx.start = function() { + if ( !timerId ) { + timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); + } +}; + +jQuery.fx.stop = function() { + clearInterval( timerId ); + timerId = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + // Default speed + _default: 400 +}; + +// Back Compat <1.8 extension point +jQuery.fx.step = {}; + +if ( jQuery.expr && jQuery.expr.filters ) { + jQuery.expr.filters.animated = function( elem ) { + return jQuery.grep(jQuery.timers, function( fn ) { + return elem === fn.elem; + }).length; + }; +} +jQuery.fn.offset = function( options ) { + if ( arguments.length ) { + return options === undefined ? + this : + this.each(function( i ) { + jQuery.offset.setOffset( this, options, i ); + }); + } + + var docElem, win, + box = { top: 0, left: 0 }, + elem = this[ 0 ], + doc = elem && elem.ownerDocument; + + if ( !doc ) { + return; + } + + docElem = doc.documentElement; + + // Make sure it's not a disconnected DOM node + if ( !jQuery.contains( docElem, elem ) ) { + return box; + } + + // If we don't have gBCR, just use 0,0 rather than error + // BlackBerry 5, iOS 3 (original iPhone) + if ( typeof elem.getBoundingClientRect !== core_strundefined ) { + box = elem.getBoundingClientRect(); + } + win = getWindow( doc ); + return { + top: box.top + ( win.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ), + left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 ) + }; +}; + +jQuery.offset = { + + setOffset: function( elem, options, i ) { + var position = jQuery.css( elem, "position" ); + + // set position first, in-case top/left are set even on static elem + if ( position === "static" ) { + elem.style.position = "relative"; + } + + var curElem = jQuery( elem ), + curOffset = curElem.offset(), + curCSSTop = jQuery.css( elem, "top" ), + curCSSLeft = jQuery.css( elem, "left" ), + calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1, + props = {}, curPosition = {}, curTop, curLeft; + + // need to be able to calculate position if either top or left is auto and position is either absolute or fixed + if ( calculatePosition ) { + curPosition = curElem.position(); + curTop = curPosition.top; + curLeft = curPosition.left; + } else { + curTop = parseFloat( curCSSTop ) || 0; + curLeft = parseFloat( curCSSLeft ) || 0; + } + + if ( jQuery.isFunction( options ) ) { + options = options.call( elem, i, curOffset ); + } + + if ( options.top != null ) { + props.top = ( options.top - curOffset.top ) + curTop; + } + if ( options.left != null ) { + props.left = ( options.left - curOffset.left ) + curLeft; + } + + if ( "using" in options ) { + options.using.call( elem, props ); + } else { + curElem.css( props ); + } + } +}; + + +jQuery.fn.extend({ + + position: function() { + if ( !this[ 0 ] ) { + return; + } + + var offsetParent, offset, + parentOffset = { top: 0, left: 0 }, + elem = this[ 0 ]; + + // fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is it's only offset parent + if ( jQuery.css( elem, "position" ) === "fixed" ) { + // we assume that getBoundingClientRect is available when computed position is fixed + offset = elem.getBoundingClientRect(); + } else { + // Get *real* offsetParent + offsetParent = this.offsetParent(); + + // Get correct offsets + offset = this.offset(); + if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) { + parentOffset = offsetParent.offset(); + } + + // Add offsetParent borders + parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ); + parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true ); + } + + // Subtract parent offsets and element margins + // note: when an element has margin: auto the offsetLeft and marginLeft + // are the same in Safari causing offset.left to incorrectly be 0 + return { + top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ), + left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true) + }; + }, + + offsetParent: function() { + return this.map(function() { + var offsetParent = this.offsetParent || document.documentElement; + while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position") === "static" ) ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || document.documentElement; + }); + } +}); + + +// Create scrollLeft and scrollTop methods +jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) { + var top = /Y/.test( prop ); + + jQuery.fn[ method ] = function( val ) { + return jQuery.access( this, function( elem, method, val ) { + var win = getWindow( elem ); + + if ( val === undefined ) { + return win ? (prop in win) ? win[ prop ] : + win.document.documentElement[ method ] : + elem[ method ]; + } + + if ( win ) { + win.scrollTo( + !top ? val : jQuery( win ).scrollLeft(), + top ? val : jQuery( win ).scrollTop() + ); + + } else { + elem[ method ] = val; + } + }, method, val, arguments.length, null ); + }; +}); + +function getWindow( elem ) { + return jQuery.isWindow( elem ) ? + elem : + elem.nodeType === 9 ? + elem.defaultView || elem.parentWindow : + false; +} +// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods +jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { + jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) { + // margin is only for outerHeight, outerWidth + jQuery.fn[ funcName ] = function( margin, value ) { + var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ), + extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" ); + + return jQuery.access( this, function( elem, type, value ) { + var doc; + + if ( jQuery.isWindow( elem ) ) { + // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there + // isn't a whole lot we can do. See pull request at this URL for discussion: + // https://github.com/jquery/jquery/pull/764 + return elem.document.documentElement[ "client" + name ]; + } + + // Get document width or height + if ( elem.nodeType === 9 ) { + doc = elem.documentElement; + + // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest + // unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it. + return Math.max( + elem.body[ "scroll" + name ], doc[ "scroll" + name ], + elem.body[ "offset" + name ], doc[ "offset" + name ], + doc[ "client" + name ] + ); + } + + return value === undefined ? + // Get width or height on the element, requesting but not forcing parseFloat + jQuery.css( elem, type, extra ) : + + // Set width or height on the element + jQuery.style( elem, type, value, extra ); + }, type, chainable ? margin : undefined, chainable, null ); + }; + }); +}); +// Limit scope pollution from any deprecated API +// (function() { + +// })(); +// Expose jQuery to the global object +window.jQuery = window.$ = jQuery; + +// Expose jQuery as an AMD module, but only for AMD loaders that +// understand the issues with loading multiple versions of jQuery +// in a page that all might call define(). The loader will indicate +// they have special allowances for multiple jQuery versions by +// specifying define.amd.jQuery = true. Register as a named module, +// since jQuery can be concatenated with other files that may use define, +// but not use a proper concatenation script that understands anonymous +// AMD modules. A named AMD is safest and most robust way to register. +// Lowercase jquery is used because AMD module names are derived from +// file names, and jQuery is normally delivered in a lowercase file name. +// Do this after creating the global so that if an AMD module wants to call +// noConflict to hide this version of jQuery, it will work. +if ( typeof define === "function" && define.amd && define.amd.jQuery ) { + define( "jquery", [], function () { return jQuery; } ); +} + +})( window ); diff --git a/framework/yii/assets/jquery.maskedinput.js b/framework/yii/assets/jquery.maskedinput.js new file mode 100644 index 0000000..49a5a72 --- /dev/null +++ b/framework/yii/assets/jquery.maskedinput.js @@ -0,0 +1,338 @@ +/* + Masked Input plugin for jQuery + Copyright (c) 2007-2013 Josh Bush (digitalbush.com) + Licensed under the MIT license (http://digitalbush.com/projects/masked-input-plugin/#license) + Version: 1.3.1 +*/ +(function($) { + function getPasteEvent() { + var el = document.createElement('input'), + name = 'onpaste'; + el.setAttribute(name, ''); + return (typeof el[name] === 'function')?'paste':'input'; +} + +var pasteEventName = getPasteEvent() + ".mask", + ua = navigator.userAgent, + iPhone = /iphone/i.test(ua), + android=/android/i.test(ua), + caretTimeoutId; + +$.mask = { + //Predefined character definitions + definitions: { + '9': "[0-9]", + 'a': "[A-Za-z]", + '*': "[A-Za-z0-9]" + }, + dataName: "rawMaskFn", + placeholder: '_', +}; + +$.fn.extend({ + //Helper Function for Caret positioning + caret: function(begin, end) { + var range; + + if (this.length === 0 || this.is(":hidden")) { + return; + } + + if (typeof begin == 'number') { + end = (typeof end === 'number') ? end : begin; + return this.each(function() { + if (this.setSelectionRange) { + this.setSelectionRange(begin, end); + } else if (this.createTextRange) { + range = this.createTextRange(); + range.collapse(true); + range.moveEnd('character', end); + range.moveStart('character', begin); + range.select(); + } + }); + } else { + if (this[0].setSelectionRange) { + begin = this[0].selectionStart; + end = this[0].selectionEnd; + } else if (document.selection && document.selection.createRange) { + range = document.selection.createRange(); + begin = 0 - range.duplicate().moveStart('character', -100000); + end = begin + range.text.length; + } + return { begin: begin, end: end }; + } + }, + unmask: function() { + return this.trigger("unmask"); + }, + mask: function(mask, settings) { + var input, + defs, + tests, + partialPosition, + firstNonMaskPos, + len; + + if (!mask && this.length > 0) { + input = $(this[0]); + return input.data($.mask.dataName)(); + } + settings = $.extend({ + placeholder: $.mask.placeholder, // Load default placeholder + completed: null + }, settings); + + + defs = $.mask.definitions; + tests = []; + partialPosition = len = mask.length; + firstNonMaskPos = null; + + $.each(mask.split(""), function(i, c) { + if (c == '?') { + len--; + partialPosition = i; + } else if (defs[c]) { + tests.push(new RegExp(defs[c])); + if (firstNonMaskPos === null) { + firstNonMaskPos = tests.length - 1; + } + } else { + tests.push(null); + } + }); + + return this.trigger("unmask").each(function() { + var input = $(this), + buffer = $.map( + mask.split(""), + function(c, i) { + if (c != '?') { + return defs[c] ? settings.placeholder : c; + } + }), + focusText = input.val(); + + function seekNext(pos) { + while (++pos < len && !tests[pos]); + return pos; + } + + function seekPrev(pos) { + while (--pos >= 0 && !tests[pos]); + return pos; + } + + function shiftL(begin,end) { + var i, + j; + + if (begin<0) { + return; + } + + for (i = begin, j = seekNext(end); i < len; i++) { + if (tests[i]) { + if (j < len && tests[i].test(buffer[j])) { + buffer[i] = buffer[j]; + buffer[j] = settings.placeholder; + } else { + break; + } + + j = seekNext(j); + } + } + writeBuffer(); + input.caret(Math.max(firstNonMaskPos, begin)); + } + + function shiftR(pos) { + var i, + c, + j, + t; + + for (i = pos, c = settings.placeholder; i < len; i++) { + if (tests[i]) { + j = seekNext(i); + t = buffer[i]; + buffer[i] = c; + if (j < len && tests[j].test(t)) { + c = t; + } else { + break; + } + } + } + } + + function keydownEvent(e) { + var k = e.which, + pos, + begin, + end; + + //backspace, delete, and escape get special treatment + if (k === 8 || k === 46 || (iPhone && k === 127)) { + pos = input.caret(); + begin = pos.begin; + end = pos.end; + + if (end - begin === 0) { + begin=k!==46?seekPrev(begin):(end=seekNext(begin-1)); + end=k===46?seekNext(end):end; + } + clearBuffer(begin, end); + shiftL(begin, end - 1); + + e.preventDefault(); + } else if (k == 27) {//escape + input.val(focusText); + input.caret(0, checkVal()); + e.preventDefault(); + } + } + + function keypressEvent(e) { + var k = e.which, + pos = input.caret(), + p, + c, + next; + + if (e.ctrlKey || e.altKey || e.metaKey || k < 32) {//Ignore + return; + } else if (k) { + if (pos.end - pos.begin !== 0){ + clearBuffer(pos.begin, pos.end); + shiftL(pos.begin, pos.end-1); + } + + p = seekNext(pos.begin - 1); + if (p < len) { + c = String.fromCharCode(k); + if (tests[p].test(c)) { + shiftR(p); + + buffer[p] = c; + writeBuffer(); + next = seekNext(p); + + if(android){ + setTimeout($.proxy($.fn.caret,input,next),0); + }else{ + input.caret(next); + } + + if (settings.completed && next >= len) { + settings.completed.call(input); + } + } + } + e.preventDefault(); + } + } + + function clearBuffer(start, end) { + var i; + for (i = start; i < end && i < len; i++) { + if (tests[i]) { + buffer[i] = settings.placeholder; + } + } + } + + function writeBuffer() { input.val(buffer.join('')); } + + function checkVal(allow) { + //try to place characters where they belong + var test = input.val(), + lastMatch = -1, + i, + c; + + for (i = 0, pos = 0; i < len; i++) { + if (tests[i]) { + buffer[i] = settings.placeholder; + while (pos++ < test.length) { + c = test.charAt(pos - 1); + if (tests[i].test(c)) { + buffer[i] = c; + lastMatch = i; + break; + } + } + if (pos > test.length) { + break; + } + } else if (buffer[i] === test.charAt(pos) && i !== partialPosition) { + pos++; + lastMatch = i; + } + } + if (allow) { + writeBuffer(); + } else if (lastMatch + 1 < partialPosition) { + input.val(""); + clearBuffer(0, len); + } else { + writeBuffer(); + input.val(input.val().substring(0, lastMatch + 1)); + } + return (partialPosition ? i : firstNonMaskPos); + } + + input.data($.mask.dataName,function(){ + return $.map(buffer, function(c, i) { + return tests[i]&&c!=settings.placeholder ? c : null; + }).join(''); + }); + + if (!input.attr("readonly")) + input + .one("unmask", function() { + input + .unbind(".mask") + .removeData($.mask.dataName); + }) + .bind("focus.mask", function() { + clearTimeout(caretTimeoutId); + var pos, + moveCaret; + + focusText = input.val(); + pos = checkVal(); + + caretTimeoutId = setTimeout(function(){ + writeBuffer(); + if (pos == mask.length) { + input.caret(0, pos); + } else { + input.caret(pos); + } + }, 10); + }) + .bind("blur.mask", function() { + checkVal(); + if (input.val() != focusText) + input.change(); + }) + .bind("keydown.mask", keydownEvent) + .bind("keypress.mask", keypressEvent) + .bind(pasteEventName, function() { + setTimeout(function() { + var pos=checkVal(true); + input.caret(pos); + if (settings.completed && pos == input.val().length) + settings.completed.call(input); + }, 0); + }); + checkVal(); //Perform initial check for existing values + }); + } +}); + + +})(jQuery); \ No newline at end of file diff --git a/yii/assets/jquery.min.js b/framework/yii/assets/jquery.min.js similarity index 100% rename from yii/assets/jquery.min.js rename to framework/yii/assets/jquery.min.js diff --git a/framework/yii/assets/punycode/LICENSE-GPL.txt b/framework/yii/assets/punycode/LICENSE-GPL.txt new file mode 100644 index 0000000..11dddd0 --- /dev/null +++ b/framework/yii/assets/punycode/LICENSE-GPL.txt @@ -0,0 +1,278 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. diff --git a/framework/yii/assets/punycode/LICENSE-MIT.txt b/framework/yii/assets/punycode/LICENSE-MIT.txt new file mode 100644 index 0000000..97067e5 --- /dev/null +++ b/framework/yii/assets/punycode/LICENSE-MIT.txt @@ -0,0 +1,20 @@ +Copyright Mathias Bynens <http://mathiasbynens.be/> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/framework/yii/assets/punycode/punycode.js b/framework/yii/assets/punycode/punycode.js new file mode 100644 index 0000000..6242382 --- /dev/null +++ b/framework/yii/assets/punycode/punycode.js @@ -0,0 +1,502 @@ +/*! http://mths.be/punycode v1.2.1 by @mathias */ +;(function(root) { + + /** Detect free variables */ + var freeExports = typeof exports == 'object' && exports; + var freeModule = typeof module == 'object' && module && + module.exports == freeExports && module; + var freeGlobal = typeof global == 'object' && global; + if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) { + root = freeGlobal; + } + + /** + * The `punycode` object. + * @name punycode + * @type Object + */ + var punycode, + + /** Highest positive signed 32-bit float value */ + maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1 + + /** Bootstring parameters */ + base = 36, + tMin = 1, + tMax = 26, + skew = 38, + damp = 700, + initialBias = 72, + initialN = 128, // 0x80 + delimiter = '-', // '\x2D' + + /** Regular expressions */ + regexPunycode = /^xn--/, + regexNonASCII = /[^ -~]/, // unprintable ASCII chars + non-ASCII chars + regexSeparators = /\x2E|\u3002|\uFF0E|\uFF61/g, // RFC 3490 separators + + /** Error messages */ + errors = { + 'overflow': 'Overflow: input needs wider integers to process', + 'not-basic': 'Illegal input >= 0x80 (not a basic code point)', + 'invalid-input': 'Invalid input' + }, + + /** Convenience shortcuts */ + baseMinusTMin = base - tMin, + floor = Math.floor, + stringFromCharCode = String.fromCharCode, + + /** Temporary variable */ + key; + + /*--------------------------------------------------------------------------*/ + + /** + * A generic error utility function. + * @private + * @param {String} type The error type. + * @returns {Error} Throws a `RangeError` with the applicable error message. + */ + function error(type) { + throw RangeError(errors[type]); + } + + /** + * A generic `Array#map` utility function. + * @private + * @param {Array} array The array to iterate over. + * @param {Function} callback The function that gets called for every array + * item. + * @returns {Array} A new array of values returned by the callback function. + */ + function map(array, fn) { + var length = array.length; + while (length--) { + array[length] = fn(array[length]); + } + return array; + } + + /** + * A simple `Array#map`-like wrapper to work with domain name strings. + * @private + * @param {String} domain The domain name. + * @param {Function} callback The function that gets called for every + * character. + * @returns {Array} A new string of characters returned by the callback + * function. + */ + function mapDomain(string, fn) { + return map(string.split(regexSeparators), fn).join('.'); + } + + /** + * Creates an array containing the decimal code points of each Unicode + * character in the string. While JavaScript uses UCS-2 internally, + * this function will convert a pair of surrogate halves (each of which + * UCS-2 exposes as separate characters) into a single code point, + * matching UTF-16. + * @see `punycode.ucs2.encode` + * @see <http://mathiasbynens.be/notes/javascript-encoding> + * @memberOf punycode.ucs2 + * @name decode + * @param {String} string The Unicode input string (UCS-2). + * @returns {Array} The new array of code points. + */ + function ucs2decode(string) { + var output = [], + counter = 0, + length = string.length, + value, + extra; + while (counter < length) { + value = string.charCodeAt(counter++); + if ((value & 0xF800) == 0xD800 && counter < length) { + // high surrogate, and there is a next character + extra = string.charCodeAt(counter++); + if ((extra & 0xFC00) == 0xDC00) { // low surrogate + output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); + } else { + output.push(value, extra); + } + } else { + output.push(value); + } + } + return output; + } + + /** + * Creates a string based on an array of decimal code points. + * @see `punycode.ucs2.decode` + * @memberOf punycode.ucs2 + * @name encode + * @param {Array} codePoints The array of decimal code points. + * @returns {String} The new Unicode string (UCS-2). + */ + function ucs2encode(array) { + return map(array, function(value) { + var output = ''; + if (value > 0xFFFF) { + value -= 0x10000; + output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800); + value = 0xDC00 | value & 0x3FF; + } + output += stringFromCharCode(value); + return output; + }).join(''); + } + + /** + * Converts a basic code point into a digit/integer. + * @see `digitToBasic()` + * @private + * @param {Number} codePoint The basic (decimal) code point. + * @returns {Number} The numeric value of a basic code point (for use in + * representing integers) in the range `0` to `base - 1`, or `base` if + * the code point does not represent a value. + */ + function basicToDigit(codePoint) { + return codePoint - 48 < 10 + ? codePoint - 22 + : codePoint - 65 < 26 + ? codePoint - 65 + : codePoint - 97 < 26 + ? codePoint - 97 + : base; + } + + /** + * Converts a digit/integer into a basic code point. + * @see `basicToDigit()` + * @private + * @param {Number} digit The numeric value of a basic code point. + * @returns {Number} The basic code point whose value (when used for + * representing integers) is `digit`, which needs to be in the range + * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is + * used; else, the lowercase form is used. The behavior is undefined + * if flag is non-zero and `digit` has no uppercase form. + */ + function digitToBasic(digit, flag) { + // 0..25 map to ASCII a..z or A..Z + // 26..35 map to ASCII 0..9 + return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5); + } + + /** + * Bias adaptation function as per section 3.4 of RFC 3492. + * http://tools.ietf.org/html/rfc3492#section-3.4 + * @private + */ + function adapt(delta, numPoints, firstTime) { + var k = 0; + delta = firstTime ? floor(delta / damp) : delta >> 1; + delta += floor(delta / numPoints); + for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) { + delta = floor(delta / baseMinusTMin); + } + return floor(k + (baseMinusTMin + 1) * delta / (delta + skew)); + } + + /** + * Converts a Punycode string of ASCII code points to a string of Unicode + * code points. + * @memberOf punycode + * @param {String} input The Punycode string of ASCII code points. + * @returns {String} The resulting string of Unicode code points. + */ + function decode(input) { + // Don't use UCS-2 + var output = [], + inputLength = input.length, + out, + i = 0, + n = initialN, + bias = initialBias, + basic, + j, + index, + oldi, + w, + k, + digit, + t, + length, + /** Cached calculation results */ + baseMinusT; + + // Handle the basic code points: let `basic` be the number of input code + // points before the last delimiter, or `0` if there is none, then copy + // the first basic code points to the output. + + basic = input.lastIndexOf(delimiter); + if (basic < 0) { + basic = 0; + } + + for (j = 0; j < basic; ++j) { + // if it's not a basic code point + if (input.charCodeAt(j) >= 0x80) { + error('not-basic'); + } + output.push(input.charCodeAt(j)); + } + + // Main decoding loop: start just after the last delimiter if any basic code + // points were copied; start at the beginning otherwise. + + for (index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) { + + // `index` is the index of the next character to be consumed. + // Decode a generalized variable-length integer into `delta`, + // which gets added to `i`. The overflow checking is easier + // if we increase `i` as we go, then subtract off its starting + // value at the end to obtain `delta`. + for (oldi = i, w = 1, k = base; /* no condition */; k += base) { + + if (index >= inputLength) { + error('invalid-input'); + } + + digit = basicToDigit(input.charCodeAt(index++)); + + if (digit >= base || digit > floor((maxInt - i) / w)) { + error('overflow'); + } + + i += digit * w; + t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); + + if (digit < t) { + break; + } + + baseMinusT = base - t; + if (w > floor(maxInt / baseMinusT)) { + error('overflow'); + } + + w *= baseMinusT; + + } + + out = output.length + 1; + bias = adapt(i - oldi, out, oldi == 0); + + // `i` was supposed to wrap around from `out` to `0`, + // incrementing `n` each time, so we'll fix that now: + if (floor(i / out) > maxInt - n) { + error('overflow'); + } + + n += floor(i / out); + i %= out; + + // Insert `n` at position `i` of the output + output.splice(i++, 0, n); + + } + + return ucs2encode(output); + } + + /** + * Converts a string of Unicode code points to a Punycode string of ASCII + * code points. + * @memberOf punycode + * @param {String} input The string of Unicode code points. + * @returns {String} The resulting Punycode string of ASCII code points. + */ + function encode(input) { + var n, + delta, + handledCPCount, + basicLength, + bias, + j, + m, + q, + k, + t, + currentValue, + output = [], + /** `inputLength` will hold the number of code points in `input`. */ + inputLength, + /** Cached calculation results */ + handledCPCountPlusOne, + baseMinusT, + qMinusT; + + // Convert the input in UCS-2 to Unicode + input = ucs2decode(input); + + // Cache the length + inputLength = input.length; + + // Initialize the state + n = initialN; + delta = 0; + bias = initialBias; + + // Handle the basic code points + for (j = 0; j < inputLength; ++j) { + currentValue = input[j]; + if (currentValue < 0x80) { + output.push(stringFromCharCode(currentValue)); + } + } + + handledCPCount = basicLength = output.length; + + // `handledCPCount` is the number of code points that have been handled; + // `basicLength` is the number of basic code points. + + // Finish the basic string - if it is not empty - with a delimiter + if (basicLength) { + output.push(delimiter); + } + + // Main encoding loop: + while (handledCPCount < inputLength) { + + // All non-basic code points < n have been handled already. Find the next + // larger one: + for (m = maxInt, j = 0; j < inputLength; ++j) { + currentValue = input[j]; + if (currentValue >= n && currentValue < m) { + m = currentValue; + } + } + + // Increase `delta` enough to advance the decoder's <n,i> state to <m,0>, + // but guard against overflow + handledCPCountPlusOne = handledCPCount + 1; + if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) { + error('overflow'); + } + + delta += (m - n) * handledCPCountPlusOne; + n = m; + + for (j = 0; j < inputLength; ++j) { + currentValue = input[j]; + + if (currentValue < n && ++delta > maxInt) { + error('overflow'); + } + + if (currentValue == n) { + // Represent delta as a generalized variable-length integer + for (q = delta, k = base; /* no condition */; k += base) { + t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); + if (q < t) { + break; + } + qMinusT = q - t; + baseMinusT = base - t; + output.push( + stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0)) + ); + q = floor(qMinusT / baseMinusT); + } + + output.push(stringFromCharCode(digitToBasic(q, 0))); + bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength); + delta = 0; + ++handledCPCount; + } + } + + ++delta; + ++n; + + } + return output.join(''); + } + + /** + * Converts a Punycode string representing a domain name to Unicode. Only the + * Punycoded parts of the domain name will be converted, i.e. it doesn't + * matter if you call it on a string that has already been converted to + * Unicode. + * @memberOf punycode + * @param {String} domain The Punycode domain name to convert to Unicode. + * @returns {String} The Unicode representation of the given Punycode + * string. + */ + function toUnicode(domain) { + return mapDomain(domain, function(string) { + return regexPunycode.test(string) + ? decode(string.slice(4).toLowerCase()) + : string; + }); + } + + /** + * Converts a Unicode string representing a domain name to Punycode. Only the + * non-ASCII parts of the domain name will be converted, i.e. it doesn't + * matter if you call it with a domain that's already in ASCII. + * @memberOf punycode + * @param {String} domain The domain name to convert, as a Unicode string. + * @returns {String} The Punycode representation of the given domain name. + */ + function toASCII(domain) { + return mapDomain(domain, function(string) { + return regexNonASCII.test(string) + ? 'xn--' + encode(string) + : string; + }); + } + + /*--------------------------------------------------------------------------*/ + + /** Define the public API */ + punycode = { + /** + * A string representing the current Punycode.js version number. + * @memberOf punycode + * @type String + */ + 'version': '1.2.1', + /** + * An object of methods to convert from JavaScript's internal character + * representation (UCS-2) to decimal Unicode code points, and back. + * @see <http://mathiasbynens.be/notes/javascript-encoding> + * @memberOf punycode + * @type Object + */ + 'ucs2': { + 'decode': ucs2decode, + 'encode': ucs2encode + }, + 'decode': decode, + 'encode': encode, + 'toASCII': toASCII, + 'toUnicode': toUnicode + }; + + /** Expose `punycode` */ + // Some AMD build optimizers, like r.js, check for specific condition patterns + // like the following: + if ( + typeof define == 'function' && + typeof define.amd == 'object' && + define.amd + ) { + define(function() { + return punycode; + }); + } else if (freeExports && !freeExports.nodeType) { + if (freeModule) { // in Node.js or RingoJS v0.8.0+ + freeModule.exports = punycode; + } else { // in Narwhal or RingoJS v0.7.0- + for (key in punycode) { + punycode.hasOwnProperty(key) && (freeExports[key] = punycode[key]); + } + } + } else { // in Rhino or a web browser + root.punycode = punycode; + } + +}(this)); diff --git a/framework/yii/assets/punycode/punycode.min.js b/framework/yii/assets/punycode/punycode.min.js new file mode 100644 index 0000000..a61badf --- /dev/null +++ b/framework/yii/assets/punycode/punycode.min.js @@ -0,0 +1,2 @@ +/*! http://mths.be/punycode v1.2.1 by @mathias */ +(function(o){function e(o){throw RangeError(L[o])}function n(o,e){for(var n=o.length;n--;)o[n]=e(o[n]);return o}function t(o,e){return n(o.split(S),e).join(".")}function r(o){for(var e,n,t=[],r=0,u=o.length;u>r;)e=o.charCodeAt(r++),55296==(63488&e)&&u>r?(n=o.charCodeAt(r++),56320==(64512&n)?t.push(((1023&e)<<10)+(1023&n)+65536):t.push(e,n)):t.push(e);return t}function u(o){return n(o,function(o){var e="";return o>65535&&(o-=65536,e+=R(55296|1023&o>>>10),o=56320|1023&o),e+=R(o)}).join("")}function i(o){return 10>o-48?o-22:26>o-65?o-65:26>o-97?o-97:x}function f(o,e){return o+22+75*(26>o)-((0!=e)<<5)}function c(o,e,n){var t=0;for(o=n?P(o/m):o>>1,o+=P(o/e);o>M*y>>1;t+=x)o=P(o/M);return P(t+(M+1)*o/(o+j))}function l(o){var n,t,r,f,l,d,s,a,p,h,v=[],g=o.length,w=0,j=I,m=A;for(t=o.lastIndexOf(F),0>t&&(t=0),r=0;t>r;++r)o.charCodeAt(r)>=128&&e("not-basic"),v.push(o.charCodeAt(r));for(f=t>0?t+1:0;g>f;){for(l=w,d=1,s=x;f>=g&&e("invalid-input"),a=i(o.charCodeAt(f++)),(a>=x||a>P((b-w)/d))&&e("overflow"),w+=a*d,p=m>=s?C:s>=m+y?y:s-m,!(p>a);s+=x)h=x-p,d>P(b/h)&&e("overflow"),d*=h;n=v.length+1,m=c(w-l,n,0==l),P(w/n)>b-j&&e("overflow"),j+=P(w/n),w%=n,v.splice(w++,0,j)}return u(v)}function d(o){var n,t,u,i,l,d,s,a,p,h,v,g,w,j,m,E=[];for(o=r(o),g=o.length,n=I,t=0,l=A,d=0;g>d;++d)v=o[d],128>v&&E.push(R(v));for(u=i=E.length,i&&E.push(F);g>u;){for(s=b,d=0;g>d;++d)v=o[d],v>=n&&s>v&&(s=v);for(w=u+1,s-n>P((b-t)/w)&&e("overflow"),t+=(s-n)*w,n=s,d=0;g>d;++d)if(v=o[d],n>v&&++t>b&&e("overflow"),v==n){for(a=t,p=x;h=l>=p?C:p>=l+y?y:p-l,!(h>a);p+=x)m=a-h,j=x-h,E.push(R(f(h+m%j,0))),a=P(m/j);E.push(R(f(a,0))),l=c(t,w,u==i),t=0,++u}++t,++n}return E.join("")}function s(o){return t(o,function(o){return E.test(o)?l(o.slice(4).toLowerCase()):o})}function a(o){return t(o,function(o){return O.test(o)?"xn--"+d(o):o})}var p="object"==typeof exports&&exports,h="object"==typeof module&&module&&module.exports==p&&module,v="object"==typeof global&&global;(v.global===v||v.window===v)&&(o=v);var g,w,b=2147483647,x=36,C=1,y=26,j=38,m=700,A=72,I=128,F="-",E=/^xn--/,O=/[^ -~]/,S=/\x2E|\u3002|\uFF0E|\uFF61/g,L={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},M=x-C,P=Math.floor,R=String.fromCharCode;if(g={version:"1.2.1",ucs2:{decode:r,encode:u},decode:l,encode:d,toASCII:a,toUnicode:s},"function"==typeof define&&"object"==typeof define.amd&&define.amd)define(function(){return g});else if(p&&!p.nodeType)if(h)h.exports=g;else for(w in g)g.hasOwnProperty(w)&&(p[w]=g[w]);else o.punycode=g})(this); \ No newline at end of file diff --git a/yii/assets/yii.activeForm.js b/framework/yii/assets/yii.activeForm.js similarity index 91% rename from yii/assets/yii.activeForm.js rename to framework/yii/assets/yii.activeForm.js index d987879..2cb3b90 100644 --- a/yii/assets/yii.activeForm.js +++ b/framework/yii/assets/yii.activeForm.js @@ -41,6 +41,9 @@ // a callback that is called before validating each attribute. The signature of the callback should be: // function ($form, attribute, messages) { ...return false to cancel the validation...} beforeValidate: undefined, + // a callback that is called after an attribute is validated. The signature of the callback should be: + // function ($form, attribute, messages) + afterValidate: undefined, // the GET parameter name indicating an AJAX-based validation ajaxVar: 'ajax' }; @@ -57,7 +60,7 @@ // whether to perform validation when a change is detected on the input validateOnChange: false, // whether to perform validation when the user is typing. - validateOnType: false, + validateOnType: false, // number of milliseconds that the validation should be delayed when a user is typing in the input field. validationDelay: 200, // whether to enable AJAX-based validation. @@ -80,7 +83,7 @@ var settings = $.extend({}, defaults, options || {}); if (settings.validationUrl === undefined) { - settings.validationUrl = $form.attr('action'); + settings.validationUrl = $form.prop('action'); } $.each(attributes, function (i) { attributes[i] = $.extend({value: getValue($form, this)}, attributeDefaults, this); @@ -135,12 +138,20 @@ data.submitting = true; if (!data.settings.beforeSubmit || data.settings.beforeSubmit($form)) { validate($form, function (messages) { - var hasError = false; + var errors = []; $.each(data.attributes, function () { - hasError = updateInput($form, this, messages) || hasError; + if (updateInput($form, this, messages)) { + errors.push(this.input); + } }); updateSummary($form, messages); - if (!hasError) { + if (errors.length) { + var top = $form.find(errors.join(',')).first().offset().top; + var wtop = $(window).scrollTop(); + if (top < wtop || top > wtop + $(window).height) { + $(window).scrollTop(top); + } + } else { data.validated = true; var $button = data.submitObject || $form.find(':submit:first'); // TODO: if the submission is caused by "change" event, it will not work @@ -280,13 +291,13 @@ // If the validation is triggered by form submission, ajax validation // should be done only when all inputs pass client validation var $button = data.submitObject, - extData = '&' + data.settings.ajaxVar + '=' + $form.attr('id'); - if ($button && $button.length && $button.attr('name')) { - extData += '&' + $button.attr('name') + '=' + $button.attr('value'); + extData = '&' + data.settings.ajaxVar + '=' + $form.prop('id'); + if ($button && $button.length && $button.prop('name')) { + extData += '&' + $button.prop('name') + '=' + $button.prop('value'); } $.ajax({ url: data.settings.validationUrl, - type: $form.attr('method'), + type: $form.prop('method'), data: $form.serialize() + extData, dataType: 'json', success: function (msgs) { @@ -325,13 +336,16 @@ $input = findInput($form, attribute), hasError = false; + if (data.settings.afterValidate) { + data.settings.afterValidate($form, attribute, messages); + } attribute.status = 1; if ($input.length) { hasError = messages && $.isArray(messages[attribute.name]) && messages[attribute.name].length; var $container = $form.find(attribute.container); var $error = $container.find(attribute.error); if (hasError) { - $error.html(messages[attribute.name][0]); + $error.text(messages[attribute.name][0]); $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass) .addClass(data.settings.errorCssClass); } else { @@ -366,7 +380,7 @@ var getValue = function ($form, attribute) { var $input = findInput($form, attribute); - var type = $input.attr('type'); + var type = $input.prop('type'); if (type === 'checkbox' || type === 'radio') { return $input.filter(':checked').val(); } else { diff --git a/yii/assets/yii.captcha.js b/framework/yii/assets/yii.captcha.js similarity index 95% rename from yii/assets/yii.captcha.js rename to framework/yii/assets/yii.captcha.js index 9211edb..af14faa 100644 --- a/yii/assets/yii.captcha.js +++ b/framework/yii/assets/yii.captcha.js @@ -1,7 +1,7 @@ /** * Yii Captcha widget. * - * This is the JavaScript widget used by the yii\widgets\Captcha widget. + * This is the JavaScript widget used by the yii\captcha\Captcha widget. * * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC @@ -51,8 +51,8 @@ dataType: 'json', cache: false, success: function(data) { - $e.attr('src', data['url']); - $('body').data(settings.hashKey, [data['hash1'], data['hash2']]); + $e.attr('src', data.url); + $('body').data(settings.hashKey, [data.hash1, data.hash2]); } }); }, diff --git a/framework/yii/assets/yii.gridView.js b/framework/yii/assets/yii.gridView.js new file mode 100644 index 0000000..a452c17 --- /dev/null +++ b/framework/yii/assets/yii.gridView.js @@ -0,0 +1,139 @@ +/** + * Yii GridView widget. + * + * This is the JavaScript widget used by the yii\grid\GridView widget. + * + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +(function ($) { + $.fn.yiiGridView = function (method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist on jQuery.yiiGridView'); + return false; + } + }; + + var defaults = { + filterUrl: undefined, + filterSelector: undefined + }; + + var methods = { + init: function (options) { + return this.each(function () { + var $e = $(this); + var settings = $.extend({}, defaults, options || {}); + $e.data('yiiGridView', { + settings: settings + }); + + var enterPressed = false; + $(document).on('change.yiiGridView keydown.yiiGridView', settings.filterSelector, function (event) { + if (event.type === 'keydown') { + if (event.keyCode !== 13) { + return; // only react to enter key + } else { + enterPressed = true; + } + } else { + // prevent processing for both keydown and change events + if (enterPressed) { + enterPressed = false; + return; + } + } + var data = $(settings.filterSelector).serialize(); + var url = settings.filterUrl; + if (url.indexOf('?') >= 0) { + url += '&' + data; + } else { + url += '?' + data; + } + window.location.href = url; + return false; + }); + }); + }, + + setSelectionColumn: function (options) { + var $grid = $(this); + var data = $grid.data('yiiGridView'); + data.selectionColumn = options.name; + if (!options.multiple) { + return; + } + $grid.on('click.yiiGridView', "input[name='" + options.checkAll + "']", function () { + $grid.find("input[name='" + options.name + "']:enabled").prop('checked', this.checked); + }); + $grid.on('click.yiiGridView', "input[name='" + options.name + "']:enabled", function () { + var all = $grid.find("input[name='" + options.name + "']").length == $grid.find("input[name='" + options.name + "']:checked").length; + $grid.find("input[name='" + options.checkAll + "']").prop('checked', all); + }); + }, + + getSelectedRows: function () { + var $grid = $(this); + var data = $grid.data('yiiGridView'); + var keys = []; + if (data.selectionColumn) { + $grid.find("input[name='" + data.selectionColumn + "']:checked").each(function () { + keys.push($(this).parent().closest('tr').data('key')); + }); + } + return keys; + }, + + destroy: function () { + return this.each(function () { + $(window).unbind('.yiiGridView'); + $(this).removeData('yiiGridView'); + }); + }, + + data: function() { + return this.data('yiiGridView'); + } + }; + + var enterPressed = false; + + var filterChanged = function (event) { + if (event.type === 'keydown') { + if (event.keyCode !== 13) { + return; // only react to enter key + } else { + enterPressed = true; + } + } else { + // prevent processing for both keydown and change events + if (enterPressed) { + enterPressed = false; + return; + } + } + var data = $(settings.filterSelector).serialize(); + if (settings.pageVar !== undefined) { + data += '&' + settings.pageVar + '=1'; + } + if (settings.enableHistory && settings.ajaxUpdate !== false && window.History.enabled) { + // Ajaxify this link + var url = $('#' + id).yiiGridView('getUrl'), + params = $.deparam.querystring($.param.querystring(url, data)); + + delete params[settings.ajaxVar]; + window.History.pushState(null, document.title, decodeURIComponent($.param.querystring(url.substr(0, url.indexOf('?')), params))); + } else { + $('#' + id).yiiGridView('update', {data: data}); + } + return false; + }; +})(window.jQuery); + diff --git a/yii/assets/yii.js b/framework/yii/assets/yii.js similarity index 50% rename from yii/assets/yii.js rename to framework/yii/assets/yii.js index 1e847c4..add3a02 100644 --- a/yii/assets/yii.js +++ b/framework/yii/assets/yii.js @@ -32,18 +32,121 @@ * // ... private functions and properties go here ... * * return pub; - * }); + * })(jQuery); * ~~~ * * Using this structure, you can define public and private functions/properties for a module. * Private functions/properties are only visible within the module, while public functions/properties - * may be accessed outside of the module. For example, you can access "yii.sample.init()". + * may be accessed outside of the module. For example, you can access "yii.sample.isActive". * * You must call "yii.initModule()" once for the root module of all your modules. */ yii = (function ($) { var pub = { - version: '2.0', + /** + * The selector for clickable elements that need to support confirmation and form submission. + */ + clickableSelector: 'a, button, input[type="submit"], input[type="button"], input[type="reset"], input[type="image"]', + /** + * The selector for changeable elements that need to support confirmation and form submission. + */ + changeableSelector: 'select, input, textarea', + + /** + * @return string|undefined the CSRF variable name. Undefined is returned is CSRF validation is not enabled. + */ + getCsrfVar: function () { + return $('meta[name=csrf-var]').prop('content'); + }, + + /** + * @return string|undefined the CSRF token. Undefined is returned is CSRF validation is not enabled. + */ + getCsrfToken: function () { + return $('meta[name=csrf-token]').prop('content'); + }, + + /** + * Displays a confirmation dialog. + * The default implementation simply displays a js confirmation dialog. + * You may override this by setting `yii.confirm`. + * @param message the confirmation message. + * @return boolean whether the user confirms with the message in the dialog + */ + confirm: function (message) { + return confirm(message); + }, + + /** + * Returns a value indicating whether to allow executing the action defined for the specified element. + * This method recognizes the `data-confirm` attribute of the element and uses it + * as the message in a confirmation dialog. The method will return true if this special attribute + * is not defined or if the user confirms the message. + * @param $e the jQuery representation of the element + * @return boolean whether to allow executing the action defined for the specified element. + */ + allowAction: function ($e) { + var message = $e.data('confirm'); + return message === undefined || pub.confirm(message); + }, + + /** + * Handles the action triggered by user. + * This method recognizes the `data-method` attribute of the element. If the attribute exists, + * the method will submit the form containing this element. If there is no containing form, a form + * will be created and submitted using the method given by this attribute value (e.g. "post", "put"). + * For hyperlinks, the form action will take the value of the "href" attribute of the link. + * For other elements, either the containing form action or the current page URL will be used + * as the form action URL. + * + * If the `data-method` attribute is not defined, the default element action will be performed. + * + * @param $e the jQuery representation of the element + * @return boolean whether to execute the default action for the element. + */ + handleAction: function ($e) { + var method = $e.data('method'); + if (method === undefined) { + return true; + } + + var $form = $e.closest('form'); + var newForm = !$form.length; + if (newForm) { + var action = $e.prop('href'); + if (!action || !action.match(/(^\/|:\/\/)/)) { + action = window.location.href; + } + $form = $('<form method="' + method + '" action="' + action + '"></form>'); + var target = $e.prop('target'); + if (target) { + $form.attr('target', target); + } + if (!method.match(/(get|post)/i)) { + $form.append('<input name="_method" value="' + method + '" type="hidden">'); + } + var csrfVar = pub.getCsrfVar(); + if (csrfVar) { + $form.append('<input name="' + csrfVar + '" value="' + pub.getCsrfToken() + '" type="hidden">'); + } + $form.hide().appendTo('body'); + } + + var activeFormData = $form.data('yiiActiveForm'); + if (activeFormData) { + // remember who triggers the form submission. This is used by yii.activeForm.js + activeFormData.submitObject = $e; + } + + $form.trigger('submit'); + + if (newForm) { + $form.remove(); + } + + return false; + }, + initModule: function (module) { if (module.isActive === undefined || module.isActive) { if ($.isFunction(module.init)) { @@ -55,6 +158,47 @@ yii = (function ($) { } }); } + }, + + init: function () { + var $document = $(document); + + // automatically send CSRF token for all AJAX requests + $.ajaxPrefilter(function (options, originalOptions, xhr) { + if (!options.crossDomain && pub.getCsrfVar()) { + xhr.setRequestHeader('X-CSRF-Token', pub.getCsrfToken()); + } + }); + + // handle AJAX redirection + $document.ajaxComplete(function (event, xhr, settings) { + var url = xhr.getResponseHeader('X-Redirect'); + if (url) { + window.location = url; + } + }); + + // handle data-confirm and data-method for clickable elements + $document.on('click.yii', pub.clickableSelector, function (event) { + var $this = $(this); + if (pub.allowAction($this)) { + return pub.handleAction($this); + } else { + event.stopImmediatePropagation(); + return false; + } + }); + + // handle data-confirm and data-method for changeable elements + $document.on('change.yii', pub.changeableSelector, function (event) { + var $this = $(this); + if (pub.allowAction($this)) { + return pub.handleAction($this); + } else { + event.stopImmediatePropagation(); + return false; + } + }); } }; return pub; diff --git a/yii/assets/yii.validation.js b/framework/yii/assets/yii.validation.js similarity index 74% rename from yii/assets/yii.validation.js rename to framework/yii/assets/yii.validation.js index 5fa8492..3ce9edb 100644 --- a/yii/assets/yii.validation.js +++ b/framework/yii/assets/yii.validation.js @@ -16,6 +16,10 @@ yii.validation = (function ($) { || value === '' || trim && $.trim(value) === ''; }; + var addMessage = function (messages, message, value) { + messages.push(message.replace(/\{value\}/g, value)); + }; + return { required: function (value, messages, options) { var valid = false; @@ -28,7 +32,7 @@ yii.validation = (function ($) { } if (!valid) { - messages.push(options.message); + addMessage(messages, options.message, value); } }, @@ -40,7 +44,7 @@ yii.validation = (function ($) { || options.strict && (value === options.trueValue || value === options.falseValue); if (!valid) { - messages.push(options.message); + addMessage(messages, options.message, value); } }, @@ -50,18 +54,18 @@ yii.validation = (function ($) { } if (typeof value !== 'string') { - messages.push(options.message); + addMessage(messages, options.message, value); return; } if (options.min !== undefined && value.length < options.min) { - messages.push(options.tooShort); + addMessage(messages, options.tooShort, value); } if (options.max !== undefined && value.length > options.max) { - messages.push(options.tooLong); + addMessage(messages, options.tooLong, value); } if (options.is !== undefined && value.length != options.is) { - messages.push(options.is); + addMessage(messages, options.is, value); } }, @@ -71,15 +75,15 @@ yii.validation = (function ($) { } if (typeof value === 'string' && !value.match(options.pattern)) { - messages.push(options.message); + addMessage(messages, options.message, value); return; } if (options.min !== undefined && value < options.min) { - messages.push(options.tooSmall); + addMessage(messages, options.tooSmall, value); } if (options.max !== undefined && value > options.max) { - messages.push(options.tooBig); + addMessage(messages, options.tooBig, value); } }, @@ -87,11 +91,11 @@ yii.validation = (function ($) { if (options.skipOnEmpty && isEmpty(value)) { return; } - var valid = !options.not && $.inArray(value, options.range) - || options.not && !$.inArray(value, options.range); + var valid = !options.not && $.inArray(value, options.range) > -1 + || options.not && $.inArray(value, options.range) == -1; if (!valid) { - messages.push(options.message); + addMessage(messages, options.message, value); } }, @@ -101,7 +105,7 @@ yii.validation = (function ($) { } if (!options.not && !value.match(options.pattern) || options.not && value.match(options.pattern)) { - messages.push(options.message) + addMessage(messages, options.message, value); } }, @@ -110,10 +114,20 @@ yii.validation = (function ($) { return; } - var valid = value.match(options.pattern) && (!options.allowName || value.match(options.fullPattern)); + var valid = true; - if (!valid) { - messages.push(options.message); + if (options.enableIDN) { + var regexp = /^(.*)@(.*)$/, + matches = regexp.exec(value); + if (matches === null) { + valid = false; + } else { + value = punycode.toASCII(matches[1]) + '@' + punycode.toASCII(matches[2]); + } + } + + if (!valid || !(value.match(options.pattern) && (!options.allowName || value.match(options.fullPattern)))) { + addMessage(messages, options.message, value); } }, @@ -126,8 +140,20 @@ yii.validation = (function ($) { value = options.defaultScheme + '://' + value; } - if (!value.match(options.pattern)) { - messages.push(options.message); + var valid = true; + + if (options.enableIDN) { + var regexp = /^([^:]+):\/\/([^\/]+)(.*)$/, + matches = regexp.exec(value); + if (matches === null) { + valid = false; + } else { + value = matches[1] + '://' + punycode.toASCII(matches[2]) + matches[3]; + } + } + + if (!valid || !value.match(options.pattern)) { + addMessage(messages, options.message, value); } }, @@ -148,7 +174,7 @@ yii.validation = (function ($) { h += v.charCodeAt(i); } if (h != hash) { - messages.push(options.message); + addMessage(messages, options.message, value); } }, @@ -188,10 +214,13 @@ yii.validation = (function ($) { case '<=': valid = value <= compareValue; break; + default: + valid = false; + break; } if (!valid) { - messages.push(options.message); + addMessage(messages, options.message, value); } } }; diff --git a/yii/base/Action.php b/framework/yii/base/Action.php similarity index 92% rename from yii/base/Action.php rename to framework/yii/base/Action.php index 7142539..2693003 100644 --- a/yii/base/Action.php +++ b/framework/yii/base/Action.php @@ -7,6 +7,8 @@ namespace yii\base; +use Yii; + /** * Action is the base class for all controller action classes. * @@ -26,6 +28,9 @@ namespace yii\base; * And the parameters provided for the action are: `array('id' => 1)`. * Then the `run()` method will be invoked as `run(1)` automatically. * + * @property string $uniqueId The unique ID of this action among the whole application. This property is + * read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -66,7 +71,7 @@ class Action extends Component * Runs this action with the specified parameters. * This method is mainly invoked by the controller. * @param array $params the parameters to be bound to the action's run() method. - * @return integer the exit status (0 means normal, non-zero means abnormal). + * @return mixed the result of the action * @throws InvalidConfigException if the action class does not have a run() method */ public function runWithParams($params) @@ -75,6 +80,10 @@ class Action extends Component throw new InvalidConfigException(get_class($this) . ' must define a "run()" method.'); } $args = $this->controller->bindActionParams($this, $params); - return (int)call_user_func_array(array($this, 'run'), $args); + Yii::trace('Running action: ' . get_class($this) . '::run()', __METHOD__); + if (Yii::$app->requestedParams === null) { + Yii::$app->requestedParams = $args; + } + return call_user_func_array(array($this, 'run'), $args); } } diff --git a/yii/base/ActionEvent.php b/framework/yii/base/ActionEvent.php similarity index 94% rename from yii/base/ActionEvent.php rename to framework/yii/base/ActionEvent.php index 9507b12..9b6c2f0 100644 --- a/yii/base/ActionEvent.php +++ b/framework/yii/base/ActionEvent.php @@ -22,6 +22,10 @@ class ActionEvent extends Event */ public $action; /** + * @var mixed the action result. Event handlers may modify this property to change the action result. + */ + public $result; + /** * @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. diff --git a/yii/base/ActionFilter.php b/framework/yii/base/ActionFilter.php similarity index 92% rename from yii/base/ActionFilter.php rename to framework/yii/base/ActionFilter.php index d69c0fe..bc3b60c 100644 --- a/yii/base/ActionFilter.php +++ b/framework/yii/base/ActionFilter.php @@ -16,10 +16,13 @@ class ActionFilter extends Behavior /** * @var array list of action IDs that this filter should apply to. If this property is not set, * then the filter applies to all actions, unless they are listed in [[except]]. + * If an action ID appears in both [[only]] and [[except]], this filter will NOT apply to it. + * @see except */ public $only; /** * @var array list of action IDs that this filter should not apply to. + * @see only */ public $except = array(); @@ -30,8 +33,8 @@ class ActionFilter extends Behavior public function events() { return array( - 'beforeAction' => 'beforeFilter', - 'afterAction' => 'afterFilter', + Controller::EVENT_BEFORE_ACTION => 'beforeFilter', + Controller::EVENT_AFTER_ACTION => 'afterFilter', ); } @@ -54,7 +57,7 @@ class ActionFilter extends Behavior public function afterFilter($event) { if ($this->isActive($event->action)) { - $this->afterAction($event->action); + $this->afterAction($event->action, $event->result); } } @@ -73,8 +76,9 @@ class ActionFilter extends Behavior * This method is invoked right after an action is executed. * You may override this method to do some postprocessing for the action. * @param Action $action the action just executed. + * @param mixed $result the action execution result */ - public function afterAction($action) + public function afterAction($action, &$result) { } diff --git a/yii/base/Application.php b/framework/yii/base/Application.php similarity index 58% rename from yii/base/Application.php rename to framework/yii/base/Application.php index fb9a6c1..683b9ce 100644 --- a/yii/base/Application.php +++ b/framework/yii/base/Application.php @@ -8,18 +8,53 @@ namespace yii\base; use Yii; +use yii\helpers\Console; +use yii\web\HttpException; /** * Application is the base class for all application classes. * + * @property \yii\rbac\Manager $authManager The auth manager for this application. This property is read-only. + * @property \yii\caching\Cache $cache The cache application component. Null if the component is not enabled. + * This property is read-only. + * @property \yii\db\Connection $db The database connection. This property is read-only. + * @property ErrorHandler $errorHandler The error handler application component. This property is read-only. + * @property \yii\base\Formatter $formatter The formatter application component. This property is read-only. + * @property \yii\i18n\I18N $i18N The internationalization component. This property is read-only. + * @property \yii\log\Logger $log The log component. This property is read-only. + * @property \yii\web\Request|\yii\console\Request $request The request component. This property is read-only. + * @property string $runtimePath The directory that stores runtime files. Defaults to the "runtime" + * subdirectory under [[basePath]]. + * @property string $timeZone The time zone used by this application. + * @property string $uniqueId The unique ID of the module. This property is read-only. + * @property \yii\web\UrlManager $urlManager The URL manager for this application. This property is read-only. + * @property string $vendorPath The directory that stores vendor files. Defaults to "vendor" directory under + * [[basePath]]. + * @property View $view The view object that is used to render various view files. This property is read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Application extends Module +abstract class Application extends Module { + /** + * @event Event an event raised before the application starts to handle a request. + */ const EVENT_BEFORE_REQUEST = 'beforeRequest'; + /** + * @event Event an event raised after the application successfully handles a request (before the response is sent out). + */ const EVENT_AFTER_REQUEST = 'afterRequest'; /** + * @event ActionEvent an event raised before executing a controller action. + * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. + */ + const EVENT_BEFORE_ACTION = 'beforeAction'; + /** + * @event ActionEvent an event raised after executing a controller action. + */ + const EVENT_AFTER_ACTION = 'afterAction'; + /** * @var string the application name. */ public $name = 'My Application'; @@ -47,16 +82,33 @@ class Application extends Module */ public $preload = array(); /** - * @var \yii\web\Controller|\yii\console\Controller the currently active controller instance + * @var Controller the currently active controller instance */ public $controller; /** - * @var mixed the layout that should be applied for views in this application. Defaults to 'main'. + * @var string|boolean the layout that should be applied for views in this application. Defaults to 'main'. * If this is false, layout will be disabled. */ public $layout = 'main'; - - private $_ended = false; + /** + * @var integer the size of the reserved memory. A portion of memory is pre-allocated so that + * when an out-of-memory issue occurs, the error handler is able to handle the error with + * the help of this reserved memory. If you set this value to be 0, no memory will be reserved. + * Defaults to 256KB. + */ + public $memoryReserveSize = 262144; + /** + * @var string the requested route + */ + public $requestedRoute; + /** + * @var Action the requested Action. If null, it means the request cannot be resolved into an action. + */ + public $requestedAction; + /** + * @var array the parameters supplied to the requested action. + */ + public $requestedParams; /** * @var string Used to reserve memory for fatal error handler. @@ -72,30 +124,61 @@ class Application extends Module public function __construct($config = array()) { Yii::$app = $this; - if (!isset($config['id'])) { throw new InvalidConfigException('The "id" configuration is required.'); } - if (isset($config['basePath'])) { $this->setBasePath($config['basePath']); - Yii::setAlias('@app', $this->getBasePath()); unset($config['basePath']); } else { throw new InvalidConfigException('The "basePath" configuration is required.'); } - + + $this->preInit($config); + + $this->registerErrorHandlers(); + $this->registerCoreComponents(); + + Component::__construct($config); + } + + /** + * Pre-initializes the application. + * This method is called at the beginning of the application constructor. + * @param array $config the application configuration + */ + public function preInit(&$config) + { + if (isset($config['vendorPath'])) { + $this->setVendorPath($config['vendorPath']); + unset($config['vendorPath']); + } else { + // set "@vendor" + $this->getVendorPath(); + } + if (isset($config['runtimePath'])) { + $this->setRuntimePath($config['runtimePath']); + unset($config['runtimePath']); + } else { + // set "@runtime" + $this->getRuntimePath(); + } if (isset($config['timeZone'])) { $this->setTimeZone($config['timeZone']); unset($config['timeZone']); } elseif (!ini_get('date.timezone')) { $this->setTimeZone('UTC'); - } - - $this->registerErrorHandlers(); - $this->registerCoreComponents(); + } + } - Component::__construct($config); + /** + * Loads components that are declared in [[preload]]. + * @throws InvalidConfigException if a component or module to be preloaded is unknown + */ + public function preloadComponents() + { + $this->getComponent('log'); + parent::preloadComponents(); } /** @@ -107,28 +190,21 @@ class Application extends Module ini_set('display_errors', 0); set_exception_handler(array($this, 'handleException')); set_error_handler(array($this, 'handleError'), error_reporting()); + if ($this->memoryReserveSize > 0) { + $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize); + } + register_shutdown_function(array($this, 'handleFatalError')); } } /** - * Terminates the application. - * This method replaces PHP's exit() function by calling [[afterRequest()]] before exiting. - * @param integer $status exit status (value 0 means normal exit while other values mean abnormal exit). - * @param boolean $exit whether to exit the current request. - * It defaults to true, meaning the PHP's exit() function will be called at the end of this method. + * Returns an ID that uniquely identifies this module among all modules within the current application. + * Since this is an application instance, it will always return an empty string. + * @return string the unique ID of the module. */ - public function end($status = 0, $exit = true) + public function getUniqueId() { - if (!$this->_ended) { - $this->_ended = true; - $this->afterRequest(); - } - - $this->handleFatalError(); - - if ($exit) { - exit($status); - } + return ''; } /** @@ -138,47 +214,31 @@ class Application extends Module */ public function run() { - $this->beforeRequest(); - // Allocating twice more than required to display memory exhausted error - // in case of trying to allocate last 1 byte while all memory is taken. - $this->_memoryReserve = str_repeat('x', 1024 * 256); - register_shutdown_function(array($this, 'end'), 0, false); - $status = $this->processRequest(); - $this->afterRequest(); - return $status; - } - - /** - * Raises the [[EVENT_BEFORE_REQUEST]] event right BEFORE the application processes the request. - */ - public function beforeRequest() - { $this->trigger(self::EVENT_BEFORE_REQUEST); - } - - /** - * Raises the [[EVENT_AFTER_REQUEST]] event right AFTER the application processes the request. - */ - public function afterRequest() - { + $response = $this->handleRequest($this->getRequest()); $this->trigger(self::EVENT_AFTER_REQUEST); + $response->send(); + return $response->exitStatus; } /** - * Processes the request. - * Child classes should override this method with actual request processing logic. - * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) + * Handles the specified request. + * + * This method should return an instance of [[Response]] or its child class + * which represents the handling result of the request. + * + * @param Request $request the request to be handled + * @return Response the resulting response */ - public function processRequest() - { - return 0; - } + abstract public function handleRequest($request); + private $_runtimePath; /** * Returns the directory that stores runtime files. - * @return string the directory that stores runtime files. Defaults to 'protected/runtime'. + * @return string the directory that stores runtime files. + * Defaults to the "runtime" subdirectory under [[basePath]]. */ public function getRuntimePath() { @@ -191,23 +251,19 @@ class Application extends Module /** * Sets the directory that stores runtime files. * @param string $path the directory that stores runtime files. - * @throws InvalidConfigException if the directory does not exist or is not writable */ public function setRuntimePath($path) { - $path = Yii::getAlias($path); - if (is_dir($path) && is_writable($path)) { - $this->_runtimePath = $path; - } else { - throw new InvalidConfigException("Runtime path must be a directory writable by the Web server process: $path"); - } + $this->_runtimePath = Yii::getAlias($path); + Yii::setAlias('@runtime', $this->_runtimePath); } private $_vendorPath; /** * Returns the directory that stores vendor files. - * @return string the directory that stores vendor files. Defaults to 'protected/vendor'. + * @return string the directory that stores vendor files. + * Defaults to "vendor" directory under [[basePath]]. */ public function getVendorPath() { @@ -224,6 +280,7 @@ class Application extends Module public function setVendorPath($path) { $this->_vendorPath = Yii::getAlias($path); + Yii::setAlias('@vendor', $this->_vendorPath); } /** @@ -260,6 +317,15 @@ class Application extends Module } /** + * Returns the log component. + * @return \yii\log\Logger the log component + */ + public function getLog() + { + return $this->getComponent('log'); + } + + /** * Returns the error handler component. * @return ErrorHandler the error handler application component. */ @@ -278,6 +344,15 @@ class Application extends Module } /** + * Returns the formatter component. + * @return \yii\base\Formatter the formatter application component. + */ + public function getFormatter() + { + return $this->getComponent('formatter'); + } + + /** * Returns the request component. * @return \yii\web\Request|\yii\console\Request the request component */ @@ -308,7 +383,7 @@ class Application extends Module * Returns the internationalization (i18n) component * @return \yii\i18n\I18N the internationalization component */ - public function getI18N() + public function getI18n() { return $this->getComponent('i18n'); } @@ -329,9 +404,15 @@ class Application extends Module public function registerCoreComponents() { $this->setComponents(array( + 'log' => array( + 'class' => 'yii\log\Logger', + ), 'errorHandler' => array( 'class' => 'yii\base\ErrorHandler', ), + 'formatter' => array( + 'class' => 'yii\base\Formatter', + ), 'i18n' => array( 'class' => 'yii\i18n\I18N', ), @@ -347,35 +428,33 @@ 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. + * This method is implemented as a PHP exception handler. * - * @param \Exception $exception exception that is not caught + * @param \Exception $exception the 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); + echo $this->renderException($exception); } - - $this->end(1); - } catch (\Exception $e) { - // exception could be thrown in end() or ErrorHandler::handle() + // exception could be thrown in ErrorHandler::handle() $msg = (string)$e; $msg .= "\nPrevious exception:\n"; $msg .= (string)$exception; if (YII_DEBUG) { - echo $msg; + if (PHP_SAPI === 'cli') { + echo $msg . "\n"; + } else { + echo '<pre>' . htmlspecialchars($msg, ENT_QUOTES, $this->charset) . '</pre>'; + } } $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); error_log($msg); @@ -398,6 +477,14 @@ class Application extends Module public function handleError($code, $message, $file, $line) { if (error_reporting() !== 0) { + // load ErrorException manually here because autoloading them will not work + // when error occurs while autoloading a class + if (!class_exists('\\yii\\base\\Exception', false)) { + require_once(__DIR__ . '/Exception.php'); + } + if (!class_exists('\\yii\\base\\ErrorException', false)) { + require_once(__DIR__ . '/ErrorException.php'); + } $exception = new ErrorException($message, $code, $code, $file, $line); // in case error appeared in __toString method we can't throw any exception @@ -406,6 +493,7 @@ class Application extends Module foreach ($trace as $frame) { if ($frame['function'] == '__toString') { $this->handleException($exception); + exit(1); } } @@ -418,45 +506,60 @@ class Application extends Module */ public function handleFatalError() { - if (YII_ENABLE_ERROR_HANDLER) { - $error = error_get_last(); + unset($this->_memoryReserve); - 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); + // load ErrorException manually here because autoloading them will not work + // when error occurs while autoloading a class + if (!class_exists('\\yii\\base\\Exception', false)) { + require_once(__DIR__ . '/Exception.php'); + } + if (!class_exists('\\yii\\base\\ErrorException', false)) { + require_once(__DIR__ . '/ErrorException.php'); + } - if (($handler = $this->getErrorHandler()) !== null) { - $handler->handle($exception); - } else { - $this->renderException($exception); - } + $error = error_get_last(); - exit(1); + if (ErrorException::isFatalError($error)) { + $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); + + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + echo $this->renderException($exception); } + + exit(1); } } /** * Renders an exception without using rich format. * @param \Exception $exception the exception to be rendered. + * @return string the rendering result */ public function renderException($exception) { if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) { $message = $exception->getName() . ': ' . $exception->getMessage(); + if (Yii::$app->controller instanceof \yii\console\Controller) { + $message = Yii::$app->controller->ansiFormat($message, Console::FG_RED); + } } else { $message = YII_DEBUG ? (string)$exception : 'Error: ' . $exception->getMessage(); } - if (PHP_SAPI) { - echo $message . "\n"; + if (PHP_SAPI === 'cli') { + return $message . "\n"; } else { - echo '<pre>' . htmlspecialchars($message, ENT_QUOTES, $this->charset) . '</pre>'; + return '<pre>' . htmlspecialchars($message, ENT_QUOTES, $this->charset) . '</pre>'; } } - // todo: to be polished + /** + * Logs the given exception + * @param \Exception $exception the exception to be logged + */ protected function logException($exception) { $category = get_class($exception); diff --git a/yii/base/InvalidRequestException.php b/framework/yii/base/Arrayable.php similarity index 69% rename from yii/base/InvalidRequestException.php rename to framework/yii/base/Arrayable.php index 6663e29..1822e51 100644 --- a/yii/base/InvalidRequestException.php +++ b/framework/yii/base/Arrayable.php @@ -8,19 +8,16 @@ namespace yii\base; /** - * InvalidRequestException represents an exception caused by incorrect end user request. + * Arrayable should be implemented by classes that need to be represented in array format. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class InvalidRequestException extends UserException +interface Arrayable { /** - * @return string the user-friendly name of this exception + * Converts the object into an array. + * @return array the array representation of this object */ - public function getName() - { - return \Yii::t('yii|Invalid Request'); - } + public function toArray(); } - diff --git a/yii/base/Behavior.php b/framework/yii/base/Behavior.php similarity index 100% rename from yii/base/Behavior.php rename to framework/yii/base/Behavior.php diff --git a/yii/base/Component.php b/framework/yii/base/Component.php similarity index 86% rename from yii/base/Component.php rename to framework/yii/base/Component.php index 8e75835..2ad2c94 100644 --- a/yii/base/Component.php +++ b/framework/yii/base/Component.php @@ -11,6 +11,9 @@ use Yii; /** * @include @yii/base/Component.md + * + * @property Behavior[] $behaviors List of behaviors attached to this component. This property is read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -35,9 +38,9 @@ class Component extends Object * Do not call this method directly as it is a PHP magic method that * will be implicitly called when executing `$value = $component->property;`. * @param string $name the property name - * @return mixed the property value, event handlers attached to the event, - * the behavior, or the value of a behavior's property + * @return mixed the property value or the value of a behavior's property * @throws UnknownPropertyException if the property is not defined + * @throws InvalidCallException if the property is write-only. * @see __set */ public function __get($name) @@ -55,7 +58,11 @@ class Component extends Object } } } - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); + if (method_exists($this, 'set' . $name)) { + throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name); + } else { + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); + } } /** @@ -172,9 +179,8 @@ class Component extends Object /** * Calls the named method which is not a class method. - * If the name refers to a component property whose value is - * an anonymous function, the method will execute the function. - * Otherwise, it will check if any attached behavior has + * + * This method will check if any attached behavior has * the named method and will execute it if available. * * Do not call this method directly as it is a PHP magic method that @@ -186,17 +192,9 @@ class Component extends Object */ public function __call($name, $params) { - $getter = 'get' . $name; - if (method_exists($this, $getter)) { - $func = $this->$getter(); - if ($func instanceof \Closure) { - return call_user_func_array($func, $params); - } - } - $this->ensureBehaviors(); foreach ($this->_behaviors as $object) { - if (method_exists($object, $name)) { + if ($object->hasMethod($name)) { return call_user_func_array(array($object, $name), $params); } } @@ -220,19 +218,19 @@ class Component extends Object * * - the class has a getter or setter method associated with the specified name * (in this case, property name is case-insensitive); - * - the class has a member variable with the specified name (when `$checkVar` is true); - * - an attached behavior has a property of the given name (when `$checkBehavior` is true). + * - the class has a member variable with the specified name (when `$checkVars` is true); + * - an attached behavior has a property of the given name (when `$checkBehaviors` is true). * * @param string $name the property name - * @param boolean $checkVar whether to treat member variables as properties - * @param boolean $checkBehavior whether to treat behaviors' properties as properties of this component + * @param boolean $checkVars whether to treat member variables as properties + * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component * @return boolean whether the property is defined * @see canGetProperty * @see canSetProperty */ - public function hasProperty($name, $checkVar = true, $checkBehavior = true) + public function hasProperty($name, $checkVars = true, $checkBehaviors = true) { - return $this->canGetProperty($name, $checkVar, $checkBehavior) || $this->canSetProperty($name, $checkVar, $checkBehavior); + return $this->canGetProperty($name, $checkVars, $checkBehaviors) || $this->canSetProperty($name, false, $checkBehaviors); } /** @@ -241,28 +239,28 @@ class Component extends Object * * - the class has a getter method associated with the specified name * (in this case, property name is case-insensitive); - * - the class has a member variable with the specified name (when `$checkVar` is true); - * - an attached behavior has a readable property of the given name (when `$checkBehavior` is true). + * - the class has a member variable with the specified name (when `$checkVars` is true); + * - an attached behavior has a readable property of the given name (when `$checkBehaviors` is true). * * @param string $name the property name - * @param boolean $checkVar whether to treat member variables as properties - * @param boolean $checkBehavior whether to treat behaviors' properties as properties of this component + * @param boolean $checkVars whether to treat member variables as properties + * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component * @return boolean whether the property can be read * @see canSetProperty */ - public function canGetProperty($name, $checkVar = true, $checkBehavior = true) + public function canGetProperty($name, $checkVars = true, $checkBehaviors = true) { - if (method_exists($this, 'get' . $name) || $checkVar && property_exists($this, $name)) { + if (method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name)) { return true; - } else { + } elseif ($checkBehaviors) { $this->ensureBehaviors(); foreach ($this->_behaviors as $behavior) { - if ($behavior->canGetProperty($name, $checkVar)) { + if ($behavior->canGetProperty($name, $checkVars)) { return true; } } - return false; } + return false; } /** @@ -271,28 +269,54 @@ class Component extends Object * * - the class has a setter method associated with the specified name * (in this case, property name is case-insensitive); - * - the class has a member variable with the specified name (when `$checkVar` is true); - * - an attached behavior has a writable property of the given name (when `$checkBehavior` is true). + * - the class has a member variable with the specified name (when `$checkVars` is true); + * - an attached behavior has a writable property of the given name (when `$checkBehaviors` is true). * * @param string $name the property name - * @param boolean $checkVar whether to treat member variables as properties - * @param boolean $checkBehavior whether to treat behaviors' properties as properties of this component + * @param boolean $checkVars whether to treat member variables as properties + * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component * @return boolean whether the property can be written * @see canGetProperty */ - public function canSetProperty($name, $checkVar = true, $checkBehavior = true) + public function canSetProperty($name, $checkVars = true, $checkBehaviors = true) { - if (method_exists($this, 'set' . $name) || $checkVar && property_exists($this, $name)) { + if (method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name)) { return true; - } else { + } elseif ($checkBehaviors) { $this->ensureBehaviors(); foreach ($this->_behaviors as $behavior) { - if ($behavior->canSetProperty($name, $checkVar)) { + if ($behavior->canSetProperty($name, $checkVars)) { return true; } } - return false; } + return false; + } + + /** + * Returns a value indicating whether a method is defined. + * A method is defined if: + * + * - the class has a method with the specified name + * - an attached behavior has a method with the given name (when `$checkBehaviors` is true). + * + * @param string $name the property name + * @param boolean $checkBehaviors whether to treat behaviors' methods as methods of this component + * @return boolean whether the property is defined + */ + public function hasMethod($name, $checkBehaviors = true) + { + if (method_exists($this, $name)) { + return true; + } elseif ($checkBehaviors) { + $this->ensureBehaviors(); + foreach ($this->_behaviors as $behavior) { + if ($behavior->hasMethod($name)) { + return true; + } + } + } + return false; } /** diff --git a/yii/base/Controller.php b/framework/yii/base/Controller.php similarity index 79% rename from yii/base/Controller.php rename to framework/yii/base/Controller.php index 3b16a93..3eebaa0 100644 --- a/yii/base/Controller.php +++ b/framework/yii/base/Controller.php @@ -8,11 +8,20 @@ namespace yii\base; use Yii; -use yii\helpers\StringHelper; /** * Controller is the base class for classes containing controller logic. * + * @property array $actionParams The request parameters (name-value pairs) to be used for action parameter + * binding. This property is read-only. + * @property string $route The route (module ID, controller ID and action ID) of the current request. This + * property is read-only. + * @property string $uniqueId The controller ID that is prefixed with the module ID (if any). This property is + * read-only. + * @property View $view The view object that can be used to render views or view files. + * @property string $viewPath The directory containing the view files for this controller. This property is + * read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -27,7 +36,6 @@ class Controller extends Component * @event ActionEvent an event raised right after executing a controller action. */ const EVENT_AFTER_ACTION = 'afterAction'; - /** * @var string the ID of this controller */ @@ -97,11 +105,11 @@ class Controller extends Component } /** - * Runs an action with the specified action ID and parameters. + * Runs an action within this controller with the specified action ID and parameters. * If the action ID is empty, the method will use [[defaultAction]]. * @param string $id the ID of the action to be executed. * @param array $params the parameters (name-value pairs) to be passed to the action. - * @return integer the status of the action execution. 0 means normal, other values mean abnormal. + * @return mixed the result of the action * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully. * @see createAction */ @@ -109,18 +117,25 @@ class Controller extends Component { $action = $this->createAction($id); if ($action !== null) { + Yii::trace("Route to run: " . $action->getUniqueId(), __METHOD__); + if (Yii::$app->requestedAction === null) { + Yii::$app->requestedAction = $action; + } $oldAction = $this->action; $this->action = $action; - $status = 1; - if ($this->module->beforeAction($action)) { - if ($this->beforeAction($action)) { - $status = $action->runWithParams($params); - $this->afterAction($action); - } - $this->module->afterAction($action); + $result = null; + $event = new ActionEvent($action); + Yii::$app->trigger(Application::EVENT_BEFORE_ACTION, $event); + if ($event->isValid && $this->module->beforeAction($action) && $this->beforeAction($action)) { + $result = $action->runWithParams($params); + $this->afterAction($action, $result); + $this->module->afterAction($action, $result); + $event = new ActionEvent($action); + $event->result = &$result; + Yii::$app->trigger(Application::EVENT_AFTER_ACTION, $event); } $this->action = $oldAction; - return $status; + return $result; } else { throw new InvalidRouteException('Unable to resolve the request: ' . $this->getUniqueId() . '/' . $id); } @@ -133,7 +148,7 @@ class Controller extends Component * the route will start from the application; otherwise, it will start from the parent module of this controller. * @param string $route the route to be handled, e.g., 'view', 'comment/view', '/admin/comment/view'. * @param array $params the parameters to be passed to the action. - * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. + * @return mixed the result of the action * @see runAction * @see forward */ @@ -152,58 +167,13 @@ class Controller extends Component /** * Binds the parameters to the action. * This method is invoked by [[Action]] when it begins to run with the given parameters. - * This method will check the parameter names that the action requires and return - * the provided parameters according to the requirement. If there is any missing parameter, - * an exception will be thrown. * @param Action $action the action to be bound with parameters * @param array $params the parameters to be bound to the action * @return array the valid parameters that the action can run with. - * @throws InvalidRequestException if there are missing parameters. */ public function bindActionParams($action, $params) { - if ($action instanceof InlineAction) { - $method = new \ReflectionMethod($this, $action->actionMethod); - } else { - $method = new \ReflectionMethod($action, 'run'); - } - - $args = array(); - $missing = array(); - foreach ($method->getParameters() as $param) { - $name = $param->getName(); - if (array_key_exists($name, $params)) { - $args[] = $params[$name]; - unset($params[$name]); - } elseif ($param->isDefaultValueAvailable()) { - $args[] = $param->getDefaultValue(); - } else { - $missing[] = $name; - } - } - - if (!empty($missing)) { - throw new InvalidRequestException(Yii::t('yii|Missing required parameters: {params}', array( - '{params}' => implode(', ', $missing), - ))); - } - - return $args; - } - - /** - * Forwards the current execution flow to handle a new request specified by a route. - * The only difference between this method and [[run()]] is that after calling this method, - * the application will exit. - * @param string $route the route to be handled, e.g., 'view', 'comment/view', '/admin/comment/view'. - * @param array $params the parameters to be passed to the action. - * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. - * @see run - */ - public function forward($route, $params = array()) - { - $status = $this->run($route, $params); - Yii::$app->end($status); + return array(); } /** @@ -226,7 +196,7 @@ class Controller extends Component if (isset($actionMap[$id])) { return Yii::createObject($actionMap[$id], $id, $this); } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { - $methodName = 'action' . StringHelper::id2camel($id); + $methodName = 'action' . str_replace(' ', '', ucwords(implode(' ', explode('-', $id)))); if (method_exists($this, $methodName)) { $method = new \ReflectionMethod($this, $methodName); if ($method->getName() === $methodName) { @@ -240,6 +210,7 @@ class Controller extends Component /** * This method is invoked right before an action is to be executed (after all possible filters.) * You may override this method to do last-minute preparation for the action. + * If you override this method, please make sure you call the parent implementation first. * @param Action $action the action to be executed. * @return boolean whether the action should continue to be executed. */ @@ -253,11 +224,15 @@ class Controller extends Component /** * This method is invoked right after an action is executed. * You may override this method to do some postprocessing for the action. + * If you override this method, please make sure you call the parent implementation first. * @param Action $action the action just executed. + * @param mixed $result the action return result. */ - public function afterAction($action) + public function afterAction($action, &$result) { - $this->trigger(self::EVENT_AFTER_ACTION, new ActionEvent($action)); + $event = new ActionEvent($action); + $event->result = & $result; + $this->trigger(self::EVENT_AFTER_ACTION, $event); } /** @@ -273,18 +248,6 @@ class Controller extends Component } /** - * Validates the parameter being bound to actions. - * This method is invoked when parameters are being bound to the currently requested action. - * Child classes may override this method to throw exceptions when there are missing and/or unknown parameters. - * @param Action $action the currently requested action - * @param array $missingParams the names of the missing parameters - * @param array $unknownParams the unknown parameters (name=>value) - */ - public function validateActionParams($action, $missingParams, $unknownParams) - { - } - - /** * @return string the controller ID that is prefixed with the module ID (if any). */ public function getUniqueId() @@ -302,34 +265,6 @@ class Controller extends Component } /** - * Populates one or multiple models from the given data array. - * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array. - * @param Model $model the model to be populated. If there are more than one model to be populated, - * you may supply them as additional parameters. - * @return boolean whether at least one model is successfully populated with the data. - */ - public function populate($data, $model) - { - $success = false; - if (!empty($data) && is_array($data)) { - $models = func_get_args(); - array_shift($models); - foreach ($models as $model) { - /** @var Model $model */ - $scope = $model->formName(); - if ($scope == '') { - $model->attributes = $data; - $success = true; - } elseif (isset($data[$scope])) { - $model->attributes = $data[$scope]; - $success = true; - } - } - } - return $success; - } - - /** * Renders a view and applies layout if available. * * The view to be rendered can be specified in one of the following formats: @@ -358,7 +293,7 @@ class Controller extends Component * - 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. + * [[Module::layoutPath|layout path]] of the context module. * * If the layout name does not contain a file extension, it will use the default one `.php`. * @@ -410,6 +345,7 @@ class Controller extends Component * 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. + * If not set, it will default to the "view" application component. * @return View the view object that can be used to render views or view files. */ public function getView() diff --git a/yii/base/ErrorException.php b/framework/yii/base/ErrorException.php similarity index 85% rename from yii/base/ErrorException.php rename to framework/yii/base/ErrorException.php index b41e9ed..8e1977a 100644 --- a/yii/base/ErrorException.php +++ b/framework/yii/base/ErrorException.php @@ -90,20 +90,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'); } } diff --git a/framework/yii/base/ErrorHandler.php b/framework/yii/base/ErrorHandler.php new file mode 100644 index 0000000..dec5779 --- /dev/null +++ b/framework/yii/base/ErrorHandler.php @@ -0,0 +1,332 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\base; + +use Yii; +use yii\web\HttpException; + +/** + * ErrorHandler handles uncaught PHP errors and exceptions. + * + * ErrorHandler displays these errors using appropriate views based on the + * nature of the errors and the mode the application runs at. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Timur Ruziev <resurtm@gmail.com> + * @since 2.0 + */ +class ErrorHandler extends Component +{ + /** + * @var integer maximum number of source code lines to be displayed. Defaults to 25. + */ + public $maxSourceLines = 25; + /** + * @var integer maximum number of trace source code lines to be displayed. Defaults to 10. + */ + public $maxTraceSourceLines = 10; + /** + * @var boolean whether to discard any existing page output before error display. Defaults to true. + */ + public $discardExistingOutput = true; + /** + * @var string the route (e.g. 'site/error') to the controller action that will be used + * to display external errors. Inside the action, it can retrieve the error information + * by Yii::$app->errorHandler->exception. This property defaults to null, meaning ErrorHandler + * will handle the error display. + */ + public $errorAction; + /** + * @var string the path of the view file for rendering exceptions without call stack information. + */ + public $errorView = '@yii/views/errorHandler/error.php'; + /** + * @var string the path of the view file for rendering exceptions. + */ + public $exceptionView = '@yii/views/errorHandler/exception.php'; + /** + * @var string the path of the view file for rendering exceptions and errors call stack element. + */ + public $callStackItemView = '@yii/views/errorHandler/callStackItem.php'; + /** + * @var string the path of the view file for rendering previous exceptions. + */ + public $previousExceptionView = '@yii/views/errorHandler/previousException.php'; + /** + * @var \Exception the exception that is being handled currently. + */ + public $exception; + + + /** + * Handles exception. + * @param \Exception $exception to be handled. + */ + public function handle($exception) + { + $this->exception = $exception; + if ($this->discardExistingOutput) { + $this->clearOutput(); + } + $this->renderException($exception); + } + + /** + * Renders the exception. + * @param \Exception $exception the exception to be handled. + */ + protected function renderException($exception) + { + if (Yii::$app instanceof \yii\console\Application || YII_ENV_TEST) { + echo Yii::$app->renderException($exception); + return; + } + + $useErrorView = !YII_DEBUG || $exception instanceof UserException; + + $response = Yii::$app->getResponse(); + $response->getHeaders()->removeAll(); + + if ($useErrorView && $this->errorAction !== null) { + // disable CSRF validation so that errorAction can run in case the error is caused by CSRF validation failure + Yii::$app->getRequest()->enableCsrfValidation = false; + $result = Yii::$app->runAction($this->errorAction); + if ($result instanceof Response) { + $response = $result; + } else { + $response->data = $result; + } + } elseif ($response->format === \yii\web\Response::FORMAT_HTML) { + if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { + // AJAX request + $response->data = 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); + } + $file = $useErrorView ? $this->errorView : $this->exceptionView; + $response->data = $this->renderFile($file, array( + 'exception' => $exception, + )); + } + } elseif ($exception instanceof Arrayable) { + $response->data = $exception; + } else { + $response->data = array( + 'type' => get_class($exception), + 'name' => 'Exception', + 'message' => $exception->getMessage(), + 'code' => $exception->getCode(), + ); + } + + if ($exception instanceof HttpException) { + $response->setStatusCode($exception->statusCode); + } else { + $response->setStatusCode(500); + } + + $response->send(); + } + + /** + * Converts special characters to HTML entities. + * @param string $text to encode. + * @return string encoded original text. + */ + public function htmlEncode($text) + { + return htmlspecialchars($text, ENT_QUOTES, Yii::$app->charset); + } + + /** + * Removes all output echoed before calling this method. + */ + public function clearOutput() + { + // the following manual level counting is to deal with zlib.output_compression set to On + for ($level = ob_get_level(); $level > 0; --$level) { + if (!@ob_end_clean()) { + ob_clean(); + } + } + } + + /** + * Adds informational links to the given PHP type/class. + * @param string $code type/class name to be linkified. + * @return string linkified with HTML type/class name. + */ + public function addTypeLinks($code) + { + $html = ''; + if (strpos($code, '\\') !== false) { + // namespaced class + foreach (explode('\\', $code) as $part) { + $html .= '<a href="http://yiiframework.com/doc/api/2.0/' . $this->htmlEncode($part) . '" target="_blank">' . $this->htmlEncode($part) . '</a>\\'; + } + $html = rtrim($html, '\\'); + } elseif (strpos($code, '()') !== false) { + // method/function call + $html = preg_replace_callback('/^(.*)\(\)$/', function ($matches) { + return '<a href="http://yiiframework.com/doc/api/2.0/' . $this->htmlEncode($matches[1]) . '" target="_blank">' . + $this->htmlEncode($matches[1]) . '</a>()'; + }, $code); + } + return $html; + } + + /** + * Renders a view file as a PHP script. + * @param string $_file_ the view file. + * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. + * @return string the rendering result + */ + public function renderFile($_file_, $_params_) + { + $_params_['handler'] = $this; + if ($this->exception instanceof ErrorException) { + ob_start(); + ob_implicit_flush(false); + extract($_params_, EXTR_OVERWRITE); + require(Yii::getAlias($_file_)); + return ob_get_clean(); + } else { + return Yii::$app->getView()->renderFile($_file_, $_params_, $this); + } + } + + /** + * Renders the previous exception stack for a given Exception. + * @param \Exception $exception the exception whose precursors should be rendered. + * @return string HTML content of the rendered previous exceptions. + * Empty string if there are none. + */ + public function renderPreviousExceptions($exception) + { + if (($previous = $exception->getPrevious()) !== null) { + return $this->renderFile($this->previousExceptionView, array( + 'exception' => $previous, + )); + } else { + return ''; + } + } + + /** + * Renders a single call stack element. + * @param string|null $file name where call has happened. + * @param integer|null $line number on which call has happened. + * @param string|null $class called class name. + * @param string|null $method called function/method name. + * @param integer $index number of the call stack element. + * @return string HTML content of the rendered call stack element. + */ + public function renderCallStackItem($file, $line, $class, $method, $index) + { + $lines = array(); + $begin = $end = 0; + if ($file !== null && $line !== null) { + $line--; // adjust line number from one-based to zero-based + $lines = @file($file); + if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line + 1) { + return ''; + } + + $half = (int)(($index == 0 ? $this->maxSourceLines : $this->maxTraceSourceLines) / 2); + $begin = $line - $half > 0 ? $line - $half : 0; + $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1; + } + + return $this->renderFile($this->callStackItemView, array( + 'file' => $file, + 'line' => $line, + 'class' => $class, + 'method' => $method, + 'index' => $index, + 'lines' => $lines, + 'begin' => $begin, + 'end' => $end, + )); + } + + /** + * Renders the request information. + * @return string the rendering result + */ + public function renderRequest() + { + $request = ''; + foreach (array('_GET', '_POST', '_SERVER', '_FILES', '_COOKIE', '_SESSION', '_ENV') as $name) { + if (!empty($GLOBALS[$name])) { + $request .= '$' . $name . ' = ' . var_export($GLOBALS[$name], true) . ";\n\n"; + } + } + return '<pre>' . rtrim($request, "\n") . '</pre>'; + } + + /** + * Determines whether given name of the file belongs to the framework. + * @param string $file name to be checked. + * @return boolean whether given name of the file belongs to the framework. + */ + public function isCoreFile($file) + { + return $file === null || strpos(realpath($file), YII_PATH . DIRECTORY_SEPARATOR) === 0; + } + + /** + * Creates HTML containing link to the page with the information on given HTTP status code. + * @param integer $statusCode to be used to generate information link. + * @param string $statusDescription Description to display after the the status code. + * @return string generated HTML with HTTP status code information. + */ + public function createHttpStatusLink($statusCode, $statusDescription) + { + return '<a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#' . (int)$statusCode .'" target="_blank">HTTP ' . (int)$statusCode . ' – ' . $statusDescription . '</a>'; + } + + /** + * Creates string containing HTML link which refers to the home page of determined web-server software + * and its full name. + * @return string server software information hyperlink. + */ + public function createServerInformationLink() + { + static $serverUrls = array( + 'http://httpd.apache.org/' => array('apache'), + 'http://nginx.org/' => array('nginx'), + 'http://lighttpd.net/' => array('lighttpd'), + 'http://gwan.com/' => array('g-wan', 'gwan'), + 'http://iis.net/' => array('iis', 'services'), + 'http://php.net/manual/en/features.commandline.webserver.php' => array('development'), + ); + if (isset($_SERVER['SERVER_SOFTWARE'])) { + foreach ($serverUrls as $url => $keywords) { + foreach ($keywords as $keyword) { + if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) { + return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . '</a>'; + } + } + } + } + return ''; + } + + /** + * Creates string containing HTML link which refers to the page with the current version + * of the framework and version number text. + * @return string framework version information hyperlink. + */ + public function createFrameworkVersionLink() + { + return '<a href="http://github.com/yiisoft/yii2/" target="_blank">' . $this->htmlEncode(Yii::getVersion()) . '</a>'; + } +} diff --git a/yii/base/Event.php b/framework/yii/base/Event.php similarity index 100% rename from yii/base/Event.php rename to framework/yii/base/Event.php diff --git a/framework/yii/base/Exception.php b/framework/yii/base/Exception.php new file mode 100644 index 0000000..64f1d1b --- /dev/null +++ b/framework/yii/base/Exception.php @@ -0,0 +1,53 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\base; + +/** + * Exception represents a generic exception for all purposes. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Exception extends \Exception implements Arrayable +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii', 'Exception'); + } + + /** + * Returns the array representation of this object. + * @return array the array representation of this object. + */ + public function toArray() + { + return $this->toArrayRecursive($this); + } + + /** + * Returns the array representation of the exception and all previous exceptions recursively. + * @param \Exception exception object + * @return array the array representation of the exception. + */ + protected function toArrayRecursive($exception) + { + $array = array( + 'type' => get_class($exception), + 'name' => $exception instanceof self ? $exception->getName() : 'Exception', + 'message' => $exception->getMessage(), + 'code' => $exception->getCode(), + ); + if (($prev = $exception->getPrevious()) !== null) { + $array['previous'] = $this->toArrayRecursive($prev); + } + return $array; + } +} diff --git a/framework/yii/base/Formatter.php b/framework/yii/base/Formatter.php new file mode 100644 index 0000000..84b4b8d --- /dev/null +++ b/framework/yii/base/Formatter.php @@ -0,0 +1,381 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\base; + +use Yii; +use DateTime; +use yii\helpers\HtmlPurifier; +use yii\helpers\Html; + +/** + * Formatter provides a set of commonly used data formatting methods. + * + * The formatting methods provided by Formatter are all named in the form of `asXyz()`. + * The behavior of some of them may be configured via the properties of Formatter. For example, + * by configuring [[dateFormat]], one may control how [[asDate()]] formats the value into a date string. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Formatter extends Component +{ + /** + * @var string the default format string to be used to format a date using PHP date() function. + */ + public $dateFormat = 'Y/m/d'; + /** + * @var string the default format string to be used to format a time using PHP date() function. + */ + public $timeFormat = 'h:i:s A'; + /** + * @var string the default format string to be used to format a date and time using PHP date() function. + */ + public $datetimeFormat = 'Y/m/d h:i:s A'; + /** + * @var string the text to be displayed when formatting a null. Defaults to '(not set)'. + */ + public $nullDisplay; + /** + * @var array the text to be displayed when formatting a boolean value. The first element corresponds + * to the text display for false, the second element for true. Defaults to `array('No', 'Yes')`. + */ + public $booleanFormat; + /** + * @var string the character displayed as the decimal point when formatting a number. + * If not set, "." will be used. + */ + public $decimalSeparator; + /** + * @var string the character displayed as the thousands separator character when formatting a number. + * If not set, "," will be used. + */ + public $thousandSeparator; + + + /** + * Initializes the component. + */ + public function init() + { + if (empty($this->booleanFormat)) { + $this->booleanFormat = array(Yii::t('yii', 'No'), Yii::t('yii', 'Yes')); + } + if ($this->nullDisplay === null) { + $this->nullDisplay = Yii::t('yii', '(not set)'); + } + } + + /** + * Formats the value based on the given format type. + * This method will call one of the "as" methods available in this class to do the formatting. + * For type "xyz", the method "asXyz" will be used. For example, if the format is "html", + * then [[asHtml()]] will be used. Format names are case insensitive. + * @param mixed $value the value to be formatted + * @param string|array $format the format of the value, e.g., "html", "text". To specify additional + * parameters of the formatting method, you may use an array. The first element of the array + * specifies the format name, while the rest of the elements will be used as the parameters to the formatting + * method. For example, a format of `array('date', 'Y-m-d')` will cause the invocation of `asDate($value, 'Y-m-d')`. + * @return string the formatting result + * @throws InvalidParamException if the type is not supported by this class. + */ + public function format($value, $format) + { + if (is_array($format)) { + if (!isset($format[0])) { + throw new InvalidParamException('The $format array must contain at least one element.'); + } + $f = $format[0]; + $format[0] = $value; + $params = $format; + $format = $f; + } else { + $params = array($value); + } + $method = 'as' . $format; + if (method_exists($this, $method)) { + return call_user_func_array(array($this, $method), $params); + } else { + throw new InvalidParamException("Unknown type: $format"); + } + } + + /** + * Formats the value as is without any formatting. + * This method simply returns back the parameter without any format. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asRaw($value) + { + if ($value === null) { + return $this->nullDisplay; + } + return $value; + } + + /** + * Formats the value as an HTML-encoded plain text. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asText($value) + { + if ($value === null) { + return $this->nullDisplay; + } + return Html::encode($value); + } + + /** + * Formats the value as an HTML-encoded plain text with newlines converted into breaks. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asNtext($value) + { + if ($value === null) { + return $this->nullDisplay; + } + return nl2br(Html::encode($value)); + } + + /** + * Formats the value as HTML-encoded text paragraphs. + * Each text paragraph is enclosed within a `<p>` tag. + * One or multiple consecutive empty lines divide two paragraphs. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asParagraphs($value) + { + if ($value === null) { + return $this->nullDisplay; + } + return str_replace('<p></p>', '', + '<p>' . preg_replace('/[\r\n]{2,}/', "</p>\n<p>", Html::encode($value)) . '</p>' + ); + } + + /** + * Formats the value as HTML text. + * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks. + * Use [[asRaw()]] if you do not want any purification of the value. + * @param mixed $value the value to be formatted + * @param array|null $config the configuration for the HTMLPurifier class. + * @return string the formatted result + */ + public function asHtml($value, $config = null) + { + if ($value === null) { + return $this->nullDisplay; + } + return HtmlPurifier::process($value, $config); + } + + /** + * Formats the value as a mailto link. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asEmail($value) + { + if ($value === null) { + return $this->nullDisplay; + } + return Html::mailto($value); + } + + /** + * Formats the value as an image tag. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asImage($value) + { + if ($value === null) { + return $this->nullDisplay; + } + return Html::img($value); + } + + /** + * Formats the value as a hyperlink. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asUrl($value) + { + if ($value === null) { + return $this->nullDisplay; + } + $url = $value; + if (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) { + $url = 'http://' . $url; + } + return Html::a(Html::encode($value), $url); + } + + /** + * Formats the value as a boolean. + * @param mixed $value the value to be formatted + * @return string the formatted result + * @see booleanFormat + */ + public function asBoolean($value) + { + if ($value === null) { + return $this->nullDisplay; + } + return $value ? $this->booleanFormat[1] : $this->booleanFormat[0]; + } + + /** + * Formats the value as a date. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[dateFormat]] will be used. The format string should be one + * that can be recognized by the PHP `date()` function. + * @return string the formatted result + * @see dateFormat + */ + public function asDate($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + $value = $this->normalizeDatetimeValue($value); + return date($format === null ? $this->dateFormat : $format, $value); + } + + /** + * Formats the value as a time. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[timeFormat]] will be used. The format string should be one + * that can be recognized by the PHP `date()` function. + * @return string the formatted result + * @see timeFormat + */ + public function asTime($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + $value = $this->normalizeDatetimeValue($value); + return date($format === null ? $this->timeFormat : $format, $value); + } + + /** + * Formats the value as a datetime. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[datetimeFormat]] will be used. The format string should be one + * that can be recognized by the PHP `date()` function. + * @return string the formatted result + * @see datetimeFormat + */ + public function asDatetime($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + $value = $this->normalizeDatetimeValue($value); + return date($format === null ? $this->datetimeFormat : $format, $value); + } + + /** + * Normalizes the given datetime value as one that can be taken by various date/time formatting methods. + * @param mixed $value the datetime value to be normalized. + * @return integer the normalized datetime value + */ + protected function normalizeDatetimeValue($value) + { + if (is_string($value)) { + return is_numeric($value) || $value === '' ? (int)$value : strtotime($value); + } elseif ($value instanceof DateTime) { + return $value->getTimestamp(); + } else { + return (int)$value; + } + } + + /** + * Formats the value as an integer. + * @param mixed $value the value to be formatted + * @return string the formatting result. + */ + public function asInteger($value) + { + if ($value === null) { + return $this->nullDisplay; + } + if (is_string($value) && preg_match('/^(-?\d+)/', $value, $matches)) { + return $matches[1]; + } else { + $value = (int)$value; + return "$value"; + } + } + + /** + * Formats the value as a double number. + * Property [[decimalSeparator]] will be used to represent the decimal point. + * @param mixed $value the value to be formatted + * @param integer $decimals the number of digits after the decimal point + * @return string the formatting result. + * @see decimalSeparator + */ + public function asDouble($value, $decimals = 2) + { + if ($value === null) { + return $this->nullDisplay; + } + if ($this->decimalSeparator === null) { + return sprintf("%.{$decimals}f", $value); + } else { + return str_replace('.', $this->decimalSeparator, sprintf("%.{$decimals}f", $value)); + } + } + + /** + * Formats the value as a number with decimal and thousand separators. + * This method calls the PHP number_format() function to do the formatting. + * @param mixed $value the value to be formatted + * @param integer $decimals the number of digits after the decimal point + * @return string the formatted result + * @see decimalSeparator + * @see thousandSeparator + */ + public function asNumber($value, $decimals = 0) + { + if ($value === null) { + return $this->nullDisplay; + } + $ds = isset($this->decimalSeparator) ? $this->decimalSeparator: '.'; + $ts = isset($this->thousandSeparator) ? $this->thousandSeparator: ','; + return number_format($value, $decimals, $ds, $ts); + } +} diff --git a/yii/base/InlineAction.php b/framework/yii/base/InlineAction.php similarity index 88% rename from yii/base/InlineAction.php rename to framework/yii/base/InlineAction.php index c5afe28..a669563 100644 --- a/yii/base/InlineAction.php +++ b/framework/yii/base/InlineAction.php @@ -7,6 +7,8 @@ namespace yii\base; +use Yii; + /** * InlineAction represents an action that is defined as a controller method. * @@ -39,11 +41,15 @@ class InlineAction extends Action * Runs this action with the specified parameters. * This method is mainly invoked by the controller. * @param array $params action parameters - * @return integer the exit status (0 means normal, non-zero means abnormal). + * @return mixed the result of the action */ public function runWithParams($params) { $args = $this->controller->bindActionParams($this, $params); - return (int)call_user_func_array(array($this->controller, $this->actionMethod), $args); + Yii::trace('Running action: ' . get_class($this->controller) . '::' . $this->actionMethod . '()', __METHOD__); + if (Yii::$app->requestedParams === null) { + Yii::$app->requestedParams = $args; + } + return call_user_func_array(array($this->controller, $this->actionMethod), $args); } } diff --git a/yii/base/InvalidCallException.php b/framework/yii/base/InvalidCallException.php similarity index 95% rename from yii/base/InvalidCallException.php rename to framework/yii/base/InvalidCallException.php index 9aefe14..73cb4b9 100644 --- a/yii/base/InvalidCallException.php +++ b/framework/yii/base/InvalidCallException.php @@ -20,7 +20,6 @@ class InvalidCallException extends Exception */ public function getName() { - return \Yii::t('yii|Invalid Call'); + return \Yii::t('yii', 'Invalid Call'); } } - diff --git a/yii/base/InvalidConfigException.php b/framework/yii/base/InvalidConfigException.php similarity index 95% rename from yii/base/InvalidConfigException.php rename to framework/yii/base/InvalidConfigException.php index 389737c..0a6b4c5 100644 --- a/yii/base/InvalidConfigException.php +++ b/framework/yii/base/InvalidConfigException.php @@ -20,7 +20,6 @@ class InvalidConfigException extends Exception */ public function getName() { - return \Yii::t('yii|Invalid Configuration'); + return \Yii::t('yii', 'Invalid Configuration'); } } - diff --git a/yii/base/InvalidParamException.php b/framework/yii/base/InvalidParamException.php similarity index 95% rename from yii/base/InvalidParamException.php rename to framework/yii/base/InvalidParamException.php index a8c96fd..44430a8 100644 --- a/yii/base/InvalidParamException.php +++ b/framework/yii/base/InvalidParamException.php @@ -20,7 +20,6 @@ class InvalidParamException extends Exception */ public function getName() { - return \Yii::t('yii|Invalid Parameter'); + return \Yii::t('yii', 'Invalid Parameter'); } } - diff --git a/yii/base/InvalidRouteException.php b/framework/yii/base/InvalidRouteException.php similarity index 95% rename from yii/base/InvalidRouteException.php rename to framework/yii/base/InvalidRouteException.php index 6d2256e..fbcc087 100644 --- a/yii/base/InvalidRouteException.php +++ b/framework/yii/base/InvalidRouteException.php @@ -20,7 +20,6 @@ class InvalidRouteException extends UserException */ public function getName() { - return \Yii::t('yii|Invalid Route'); + return \Yii::t('yii', 'Invalid Route'); } } - diff --git a/yii/base/Model.php b/framework/yii/base/Model.php similarity index 69% rename from yii/base/Model.php rename to framework/yii/base/Model.php index 7f55239..7fb8c35 100644 --- a/yii/base/Model.php +++ b/framework/yii/base/Model.php @@ -7,7 +7,13 @@ namespace yii\base; -use yii\helpers\StringHelper; +use Yii; +use ArrayAccess; +use ArrayObject; +use ArrayIterator; +use ReflectionClass; +use IteratorAggregate; +use yii\helpers\Inflector; use yii\validators\RequiredValidator; use yii\validators\Validator; @@ -30,18 +36,29 @@ use yii\validators\Validator; * You may directly use Model to store model data, or extend it with customization. * You may also customize Model by attaching [[ModelBehavior|model behaviors]]. * - * @property Vector $validators All the validators declared in the model. - * @property array $activeValidators The validators applicable to the current [[scenario]]. - * @property array $errors Errors for all attributes or the specified attribute. Empty array is returned if no error. - * @property array $attributes Attribute values (name=>value). - * @property string $scenario The scenario that this model is in. + * @property \yii\validators\Validator[] $activeValidators The validators applicable to the current + * [[scenario]]. This property is read-only. + * @property array $attributes Attribute values (name => value). + * @property array $errors An array of errors for all attributes. Empty array is returned if no error. The + * result is a two-dimensional array. See [[getErrors()]] for detailed description. This property is read-only. + * @property array $firstErrors The first errors. An empty array will be returned if there is no error. This + * property is read-only. + * @property ArrayIterator $iterator An iterator for traversing the items in the list. This property is + * read-only. + * @property string $scenario The scenario that this model is in. Defaults to [[DEFAULT_SCENARIO]]. + * @property ArrayObject $validators All the validators declared in the model. This property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Model extends Component implements \IteratorAggregate, \ArrayAccess +class Model extends Component implements IteratorAggregate, ArrayAccess { /** + * The name of the default scenario. + */ + const DEFAULT_SCENARIO = 'default'; + + /** * @event ModelEvent an event raised at the beginning of [[validate()]]. You may set * [[ModelEvent::isValid]] to be false to stop the validation. */ @@ -56,13 +73,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ private $_errors; /** - * @var Vector vector of validators + * @var ArrayObject list of validators */ private $_validators; /** * @var string current scenario */ - private $_scenario = 'default'; + private $_scenario = self::DEFAULT_SCENARIO; /** * Returns the validation rules for attributes. @@ -76,7 +93,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * array( * 'attribute list', * 'validator type', - * 'on'=>'scenario name', + * 'on' => 'scenario name', * ...other parameters... * ) * ~~~ @@ -99,6 +116,10 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * function validatorName($attribute, $params) * ~~~ * + * In the above `$attribute` refers to currently validated attribute name while `$params` contains an array of + * validator configuration options such as `max` in case of `string` validator. Currently validate attribute value + * can be accessed as `$this->[$attribute]`. + * * Yii also provides a set of [[Validator::builtInValidators|built-in validators]]. * They each has an alias name which can be used when specifying a validation rule. * @@ -108,14 +129,14 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * array( * // built-in "required" validator * array('username', 'required'), - * // built-in "length" validator customized with "min" and "max" properties - * array('username', 'length', 'min'=>3, 'max'=>12), + * // built-in "string" validator customized with "min" and "max" properties + * array('username', 'string', 'min' => 3, 'max' => 12), * // built-in "compare" validator that is used in "register" scenario only - * array('password', 'compare', 'compareAttribute'=>'password2', 'on'=>'register'), + * array('password', 'compare', 'compareAttribute' => 'password2', 'on' => 'register'), * // an inline validator defined via the "authenticate()" method in the model class - * array('password', 'authenticate', 'on'=>'login'), - * // a validator of class "CaptchaValidator" - * array('captcha', 'CaptchaValidator'), + * array('password', 'authenticate', 'on' => 'login'), + * // a validator of class "DateRangeValidator" + * array('dateRange', 'DateRangeValidator'), * ); * ~~~ * @@ -147,25 +168,39 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * If an attribute should NOT be massively assigned (thus considered unsafe), * please prefix the attribute with an exclamation character (e.g. '!rank'). * - * The default implementation of this method will return a 'default' scenario - * which corresponds to all attributes listed in the validation rules applicable - * to the 'default' scenario. + * The default implementation of this method will return all scenarios found in the [[rules()]] + * declaration. A special scenario named [[DEFAULT_SCENARIO]] will contain all attributes + * found in the [[rules()]]. Each scenario will be associated with the attributes that + * are being validated by the validation rules that apply to the scenario. * * @return array a list of scenarios and the corresponding active attributes. */ public function scenarios() { - $attributes = array(); - foreach ($this->getActiveValidators() as $validator) { - if ($validator->isActive('default')) { - foreach ($validator->attributes as $name) { - $attributes[$name] = true; + $scenarios = array(); + $defaults = array(); + /** @var $validator Validator */ + foreach ($this->getValidators() as $validator) { + if (empty($validator->on)) { + foreach ($validator->attributes as $attribute) { + $defaults[$attribute] = true; } + } else { + foreach ($validator->on as $scenario) { + foreach ($validator->attributes as $attribute) { + $scenarios[$scenario][$attribute] = true; + } + } + } + } + foreach ($scenarios as $scenario => $attributes) { + foreach (array_keys($defaults) as $attribute) { + $attributes[$attribute] = true; } + $scenarios[$scenario] = array_keys($attributes); } - return array( - 'default' => array_keys($attributes), - ); + $scenarios[self::DEFAULT_SCENARIO] = array_keys($defaults); + return $scenarios; } /** @@ -183,9 +218,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function formName() { - $class = get_class($this); - $pos = strrpos($class, '\\'); - return $pos === false ? $class : substr($class, $pos + 1); + $reflector = new ReflectionClass($this); + return $reflector->getShortName(); } /** @@ -196,7 +230,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function attributes() { - $class = new \ReflectionClass($this); + $class = new ReflectionClass($this); $names = array(); foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { $name = $property->getName(); @@ -220,7 +254,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * Note, in order to inherit labels defined in the parent class, a child class needs to * merge the parent labels with child labels using functions such as `array_merge()`. * - * @return array attribute labels (name=>label) + * @return array attribute labels (name => label) * @see generateAttributeLabel */ public function attributeLabels() @@ -241,17 +275,24 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * after the actual validation, respectively. If [[beforeValidate()]] returns false, * the validation will be cancelled and [[afterValidate()]] will not be called. * - * Errors found during the validation can be retrieved via [[getErrors()]] - * and [[getError()]]. + * Errors found during the validation can be retrieved via [[getErrors()]], + * [[getFirstErrors()]] and [[getFirstError()]]. * * @param array $attributes list of attributes that should be validated. * If this parameter is empty, it means any attribute listed in the applicable * validation rules should be validated. * @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation * @return boolean whether the validation is successful without any error. + * @throws InvalidParamException if the current scenario is unknown. */ public function validate($attributes = null, $clearErrors = true) { + $scenarios = $this->scenarios(); + $scenario = $this->getScenario(); + if (!isset($scenarios[$scenario])) { + throw new InvalidParamException("Unknown scenario: $scenario"); + } + if ($clearErrors) { $this->clearErrors(); } @@ -300,15 +341,15 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * This method differs from [[getActiveValidators()]] in that the latter * only returns the validators applicable to the current [[scenario]]. * - * Because this method returns a [[Vector]] object, you may + * Because this method returns an ArrayObject object, you may * manipulate it by inserting or removing validators (useful in model behaviors). * For example, * * ~~~ - * $model->validators->add($newValidator); + * $model->validators[] = $newValidator; * ~~~ * - * @return Vector all the validators declared in the model. + * @return ArrayObject all the validators declared in the model. */ public function getValidators() { @@ -340,18 +381,18 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Creates validator objects based on the validation rules specified in [[rules()]]. * Unlike [[getValidators()]], each time this method is called, a new list of validators will be returned. - * @return Vector validators + * @return ArrayObject validators * @throws InvalidConfigException if any validation rule configuration is invalid */ public function createValidators() { - $validators = new Vector; + $validators = new ArrayObject; foreach ($this->rules() as $rule) { if ($rule instanceof Validator) { - $validators->add($rule); + $validators->append($rule); } elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type $validator = Validator::createValidator($rule[1], $this, $rule[0], array_slice($rule, 2)); - $validators->add($validator); + $validators->append($validator); } else { throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.'); } @@ -381,6 +422,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * Returns a value indicating whether the attribute is safe for massive assignments. * @param string $attribute attribute name * @return boolean whether the attribute is safe for massive assignments + * @see safeAttributes() */ public function isAttributeSafe($attribute) { @@ -388,6 +430,17 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Returns a value indicating whether the attribute is active in the current scenario. + * @param string $attribute attribute name + * @return boolean whether the attribute is active in the current scenario + * @see activeAttributes() + */ + public function isAttributeActive($attribute) + { + return in_array($attribute, $this->activeAttributes(), true); + } + + /** * Returns the text label for the specified attribute. * @param string $attribute the attribute name * @return string the attribute label @@ -413,6 +466,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Returns the errors for all attribute or a single attribute. * @param string $attribute attribute name. Use null to retrieve errors for all attributes. + * @property array An array of errors for all attributes. Empty array is returned if no error. + * The result is a two-dimensional array. See [[getErrors()]] for detailed description. * @return array errors for all attributes or the specified attribute. Empty array is returned if no error. * Note that when returning errors for all attributes, the result is a two-dimensional array, like the following: * @@ -428,7 +483,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * ) * ~~~ * - * @see getError + * @see getFirstErrors + * @see getFirstError */ public function getErrors($attribute = null) { @@ -442,6 +498,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Returns the first error of every attribute in the model. * @return array the first errors. An empty array will be returned if there is no error. + * @see getErrors + * @see getFirstError */ public function getFirstErrors() { @@ -463,6 +521,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * @param string $attribute attribute name. * @return string the error message. Null is returned if no error. * @see getErrors + * @see getFirstErrors */ public function getFirstError($attribute) { @@ -474,7 +533,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * @param string $attribute attribute name * @param string $error new error message */ - public function addError($attribute, $error) + public function addError($attribute, $error = '') { $this->_errors[$attribute][] = $error; } @@ -502,7 +561,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function generateAttributeLabel($name) { - return StringHelper::camel2words($name, true); + return Inflector::camel2words($name, true); } /** @@ -511,7 +570,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * Defaults to null, meaning all attributes listed in [[attributes()]] will be returned. * If it is an array, only the attributes in the array will be returned. * @param array $except list of attributes whose value should NOT be returned. - * @return array attribute values (name=>value). + * @return array attribute values (name => value). */ public function getAttributes($names = null, $except = array()) { @@ -531,7 +590,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Sets the attribute values in a massive way. - * @param array $values attribute values (name=>value) to be assigned to the model. + * @param array $values attribute values (name => value) to be assigned to the model. * @param boolean $safeOnly whether the assignments should only be done to the safe attributes. * A safe attribute is one that is associated with a validation rule in the current [[scenario]]. * @see safeAttributes() @@ -561,7 +620,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) . "'.", __METHOD__); + Yii::trace("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __METHOD__); } } @@ -571,7 +630,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * Scenario affects how validation is performed and which attributes can * be massively assigned. * - * @return string the scenario that this model is in. Defaults to 'default'. + * @return string the scenario that this model is in. Defaults to [[DEFAULT_SCENARIO]]. */ public function getScenario() { @@ -580,8 +639,9 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Sets the scenario for the model. + * Note that this method does not check if the scenario exists or not. + * The method [[validate()]] will perform this check. * @param string $value the scenario that this model is in. - * @see getScenario */ public function setScenario($value) { @@ -590,18 +650,19 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Returns the attribute names that are safe to be massively assigned in the current scenario. - * @return array safe attribute names + * @return string[] safe attribute names */ public function safeAttributes() { $scenario = $this->getScenario(); $scenarios = $this->scenarios(); + if (!isset($scenarios[$scenario])) { + return array(); + } $attributes = array(); - if (isset($scenarios[$scenario])) { - foreach ($scenarios[$scenario] as $attribute) { - if ($attribute[0] !== '!') { - $attributes[] = $attribute; - } + foreach ($scenarios[$scenario] as $attribute) { + if ($attribute[0] !== '!') { + $attributes[] = $attribute; } } return $attributes; @@ -609,34 +670,117 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Returns the attribute names that are subject to validation in the current scenario. - * @return array safe attribute names + * @return string[] safe attribute names */ public function activeAttributes() { $scenario = $this->getScenario(); $scenarios = $this->scenarios(); - if (isset($scenarios[$scenario])) { - $attributes = $scenarios[$this->getScenario()]; - foreach ($attributes as $i => $attribute) { - if ($attribute[0] === '!') { - $attributes[$i] = substr($attribute, 1); - } + if (!isset($scenarios[$scenario])) { + return array(); + } + $attributes = $scenarios[$scenario]; + foreach ($attributes as $i => $attribute) { + if ($attribute[0] === '!') { + $attributes[$i] = substr($attribute, 1); } - return $attributes; + } + return $attributes; + } + + /** + * Populates the model with the data from end user. + * The data to be loaded is `$data[formName]`, where `formName` refers to the value of [[formName()]]. + * If [[formName()]] is empty, the whole `$data` array will be used to populate the model. + * The data being populated is subject to the safety check by [[setAttributes()]]. + * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array + * supplied by end user. + * @return boolean whether the model is successfully populated with some data. + */ + public function load($data) + { + $scope = $this->formName(); + if ($scope == '') { + $this->setAttributes($data); + return true; + } elseif (isset($data[$scope])) { + $this->setAttributes($data[$scope]); + return true; } else { - return array(); + return false; + } + } + + /** + * Populates a set of models with the data from end user. + * This method is mainly used to collect tabular data input. + * The data to be loaded for each model is `$data[formName][index]`, where `formName` + * refers to the value of [[formName()]], and `index` the index of the model in the `$models` array. + * If [[formName()]] is empty, `$data[index]` will be used to populate each model. + * The data being populated to each model is subject to the safety check by [[setAttributes()]]. + * @param array $models the models to be populated. Note that all models should have the same class. + * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array + * supplied by end user. + * @return boolean whether the model is successfully populated with some data. + */ + public static function loadMultiple($models, $data) + { + /** @var Model $model */ + $model = reset($models); + if ($model === false) { + return false; } + $success = false; + $scope = $model->formName(); + foreach ($models as $i => $model) { + if ($scope == '') { + if (isset($data[$i])) { + $model->setAttributes($data[$i]); + $success = true; + } + } elseif (isset($data[$scope][$i])) { + $model->setAttributes($data[$scope][$i]); + $success = true; + } + } + return $success; + } + + /** + * Validates multiple models. + * @param array $models the models to be validated + * @return boolean whether all models are valid. False will be returned if one + * or multiple models have validation error. + */ + public static function validateMultiple($models) + { + $valid = true; + /** @var Model $model */ + foreach ($models as $model) { + $valid = $model->validate() && $valid; + } + return $valid; + } + + /** + * Converts the object into an array. + * The default implementation will return [[attributes]]. + * @return array the array representation of the object + */ + public function toArray() + { + return $this->getAttributes(); } /** * Returns an iterator for traversing the attributes in the model. * This method is required by the interface IteratorAggregate. - * @return DictionaryIterator an iterator for traversing the items in the list. + * @return ArrayIterator an iterator for traversing the items in the list. */ public function getIterator() { $attributes = $this->getAttributes(); - return new DictionaryIterator($attributes); + return new ArrayIterator($attributes); } /** diff --git a/yii/base/ModelEvent.php b/framework/yii/base/ModelEvent.php similarity index 100% rename from yii/base/ModelEvent.php rename to framework/yii/base/ModelEvent.php diff --git a/yii/base/Module.php b/framework/yii/base/Module.php similarity index 77% rename from yii/base/Module.php rename to framework/yii/base/Module.php index b1597e2..7c29a8b 100644 --- a/yii/base/Module.php +++ b/framework/yii/base/Module.php @@ -1,650 +1,674 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\base; - -use Yii; -use yii\helpers\StringHelper; - -/** - * Module is the base class for module and application classes. - * - * A module represents a sub-application which contains MVC elements by itself, such as - * models, views, controllers, etc. - * - * A module may consist of [[modules|sub-modules]]. - * - * [[components|Components]] may be registered with the module so that they are globally - * accessible within the module. - * - * @property string $uniqueId An ID that uniquely identifies this module among all modules within - * the current application. - * @property string $basePath The root directory of the module. Defaults to the directory containing the module class. - * @property string $controllerPath The directory containing the controller classes. Defaults to "[[basePath]]/controllers". - * @property string $viewPath The directory containing the view files within this module. Defaults to "[[basePath]]/views". - * @property string $layoutPath The directory containing the layout view files within this module. Defaults to "[[viewPath]]/layouts". - * @property array $modules The configuration of the currently installed modules (module ID => configuration). - * @property array $components The components (indexed by their IDs) registered within this module. - * @property array $import List of aliases to be imported. This property is write-only. - * @property array $aliases List of aliases to be defined. This property is write-only. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -abstract class Module extends Component -{ - /** - * @event ActionEvent an event raised before executing a controller action. - * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. - */ - const EVENT_BEFORE_ACTION = 'beforeAction'; - /** - * @event ActionEvent an event raised after executing a controller action. - */ - const EVENT_AFTER_ACTION = 'afterAction'; - /** - * @var array custom module parameters (name => value). - */ - public $params = array(); - /** - * @var array the IDs of the components that should be preloaded when this module is created. - */ - public $preload = array(); - /** - * @var string an ID that uniquely identifies this module among other modules which have the same [[module|parent]]. - */ - public $id; - /** - * @var Module the parent module of this module. Null if this module does not have a parent. - */ - public $module; - /** - * @var string|boolean the layout that should be applied for views within this module. This refers to a view name - * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]] - * will be taken. If this is false, layout will be disabled within this module. - */ - public $layout; - /** - * @var array mapping from controller ID to controller configurations. - * Each name-value pair specifies the configuration of a single controller. - * A controller configuration can be either a string or an array. - * If the former, the string should be the class name or path alias of the controller. - * If the latter, the array must contain a 'class' element which specifies - * the controller's class name or path alias, and the rest of the name-value pairs - * in the array are used to initialize the corresponding controller properties. For example, - * - * ~~~ - * array( - * 'account' => '@app/controllers/UserController', - * 'article' => array( - * 'class' => '@app/controllers/PostController', - * 'pageTitle' => 'something new', - * ), - * ) - * ~~~ - */ - public $controllerMap = array(); - /** - * @var string the namespace that controller classes are in. Default is to use global namespace. - */ - public $controllerNamespace; - /** - * @return string the default route of this module. Defaults to 'default'. - * The route may consist of child module ID, controller ID, and/or action ID. - * For example, `help`, `post/create`, `admin/post/create`. - * If action ID is not given, it will take the default value as specified in - * [[Controller::defaultAction]]. - */ - public $defaultRoute = 'default'; - /** - * @var string the root directory of the module. - */ - private $_basePath; - /** - * @var string the root directory that contains view files for this module - */ - private $_viewPath; - /** - * @var string the root directory that contains layout view files for this module. - */ - private $_layoutPath; - /** - * @var string the directory containing controller classes in the module. - */ - private $_controllerPath; - /** - * @var array child modules of this module - */ - private $_modules = array(); - /** - * @var array components registered under this module - */ - private $_components = array(); - - /** - * Constructor. - * @param string $id the ID of this module - * @param Module $parent the parent module (if any) - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($id, $parent = null, $config = array()) - { - $this->id = $id; - $this->module = $parent; - parent::__construct($config); - } - - /** - * Getter magic method. - * This method is overridden to support accessing components - * like reading module properties. - * @param string $name component or property name - * @return mixed the named property value - */ - public function __get($name) - { - if ($this->hasComponent($name)) { - return $this->getComponent($name); - } else { - return parent::__get($name); - } - } - - /** - * Checks if a property value is null. - * This method overrides the parent implementation by checking - * if the named component is loaded. - * @param string $name the property name or the event name - * @return boolean whether the property value is null - */ - public function __isset($name) - { - if ($this->hasComponent($name)) { - return $this->getComponent($name) !== null; - } else { - return parent::__isset($name); - } - } - - /** - * Initializes the module. - * This method is called after the module is created and initialized with property values - * given in configuration. The default implement will create a path alias using the module [[id]] - * and then call [[preloadComponents()]] to load components that are declared in [[preload]]. - */ - public function init() - { - $this->preloadComponents(); - } - - /** - * Returns an ID that uniquely identifies this module among all modules within the current application. - * Note that if the module is an application, an empty string will be returned. - * @return string the unique ID of the module. - */ - public function getUniqueId() - { - if ($this instanceof Application) { - return ''; - } elseif ($this->module) { - return $this->module->getUniqueId() . '/' . $this->id; - } else { - return $this->id; - } - } - - /** - * Returns the root directory of the module. - * It defaults to the directory containing the module class file. - * @return string the root directory of the module. - */ - public function getBasePath() - { - if ($this->_basePath === null) { - $class = new \ReflectionClass($this); - $this->_basePath = dirname($class->getFileName()); - } - return $this->_basePath; - } - - /** - * Sets the root directory of the module. - * This method can only be invoked at the beginning of the constructor. - * @param string $path the root directory of the module. This can be either a directory name or a path alias. - * @throws InvalidParamException if the directory does not exist. - */ - public function setBasePath($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"); - } - } - - /** - * Returns the directory that contains the controller classes. - * Defaults to "[[basePath]]/controllers". - * @return string the directory that contains the controller classes. - */ - public function getControllerPath() - { - if ($this->_controllerPath !== null) { - return $this->_controllerPath; - } else { - return $this->_controllerPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'controllers'; - } - } - - /** - * Sets the directory that contains the controller classes. - * @param string $path the directory that contains the controller classes. - * This can be either a directory name or a path alias. - * @throws Exception if the directory is invalid - */ - public function setControllerPath($path) - { - $this->_controllerPath = Yii::getAlias($path); - } - - /** - * Returns the directory that contains the view files for this module. - * @return string the root directory of view files. Defaults to "[[basePath]]/view". - */ - public function getViewPath() - { - if ($this->_viewPath !== null) { - return $this->_viewPath; - } else { - return $this->_viewPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'views'; - } - } - - /** - * Sets the directory that contains the view files. - * @param string $path the root directory of view files. - * @throws Exception if the directory is invalid - */ - public function setViewPath($path) - { - $this->_viewPath = Yii::getAlias($path); - } - - /** - * Returns the directory that contains layout view files for this module. - * @return string the root directory of layout files. Defaults to "[[viewPath]]/layouts". - */ - public function getLayoutPath() - { - if ($this->_layoutPath !== null) { - return $this->_layoutPath; - } else { - return $this->_layoutPath = $this->getViewPath() . DIRECTORY_SEPARATOR . 'layouts'; - } - } - - /** - * Sets the directory that contains the layout files. - * @param string $path the root directory of layout files. - * @throws Exception if the directory is invalid - */ - public function setLayoutPath($path) - { - $this->_layoutPath = Yii::getAlias($path); - } - - /** - * Defines path aliases. - * This method calls [[Yii::setAlias()]] to register the path aliases. - * This method is provided so that you can define path aliases when configuring a module. - * @param array $aliases list of path aliases to be defined. The array keys are alias names - * (must start with '@') and the array values are the corresponding paths or aliases. - * For example, - * - * ~~~ - * array( - * '@models' => '@app/models', // an existing alias - * '@backend' => __DIR__ . '/../backend', // a directory - * ) - * ~~~ - */ - public function setAliases($aliases) - { - foreach ($aliases as $name => $alias) { - Yii::setAlias($name, $alias); - } - } - - /** - * Checks whether the named module exists. - * @param string $id module ID - * @return boolean whether the named module exists. Both loaded and unloaded modules - * are considered. - */ - public function hasModule($id) - { - return isset($this->_modules[$id]); - } - - /** - * Retrieves the named module. - * @param string $id module ID (case-sensitive) - * @param boolean $load whether to load the module if it is not yet loaded. - * @return Module|null the module instance, null if the module - * does not exist. - * @see hasModule() - */ - public function getModule($id, $load = true) - { - if (isset($this->_modules[$id])) { - if ($this->_modules[$id] instanceof Module) { - return $this->_modules[$id]; - } elseif ($load) { - Yii::trace("Loading module: $id", __METHOD__); - return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); - } - } - return null; - } - - /** - * Adds a sub-module to this module. - * @param string $id module ID - * @param Module|array|null $module the sub-module to be added to this module. This can - * be one of the followings: - * - * - a [[Module]] object - * - a configuration array: when [[getModule()]] is called initially, the array - * will be used to instantiate the sub-module - * - null: the named sub-module will be removed from this module - */ - public function setModule($id, $module) - { - if ($module === null) { - unset($this->_modules[$id]); - } else { - $this->_modules[$id] = $module; - } - } - - /** - * Returns the sub-modules in this module. - * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false, - * then all sub-modules registered in this module will be returned, whether they are loaded or not. - * Loaded modules will be returned as objects, while unloaded modules as configuration arrays. - * @return array the modules (indexed by their IDs) - */ - public function getModules($loadedOnly = false) - { - if ($loadedOnly) { - $modules = array(); - foreach ($this->_modules as $module) { - if ($module instanceof Module) { - $modules[] = $module; - } - } - return $modules; - } else { - return $this->_modules; - } - } - - /** - * Registers sub-modules in the current module. - * - * Each sub-module should be specified as a name-value pair, where - * name refers to the ID of the module and value the module or a configuration - * array that can be used to create the module. In the latter case, [[Yii::createObject()]] - * will be used to create the module. - * - * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently. - * - * The following is an example for registering two sub-modules: - * - * ~~~ - * array( - * 'comment' => array( - * 'class' => 'app\modules\CommentModule', - * 'db' => 'db', - * ), - * 'booking' => array( - * 'class' => 'app\modules\BookingModule', - * ), - * ) - * ~~~ - * - * @param array $modules modules (id => module configuration or instances) - */ - public function setModules($modules) - { - foreach ($modules as $id => $module) { - $this->_modules[$id] = $module; - } - } - - /** - * Checks whether the named component exists. - * @param string $id component ID - * @return boolean whether the named component exists. Both loaded and unloaded components - * are considered. - */ - public function hasComponent($id) - { - return isset($this->_components[$id]); - } - - /** - * Retrieves the named component. - * @param string $id component ID (case-sensitive) - * @param boolean $load whether to load the component if it is not yet loaded. - * @return Component|null the component instance, null if the component does not exist. - * @see hasComponent() - */ - public function getComponent($id, $load = true) - { - if (isset($this->_components[$id])) { - if ($this->_components[$id] instanceof Component) { - return $this->_components[$id]; - } elseif ($load) { - Yii::trace("Loading component: $id", __METHOD__); - return $this->_components[$id] = Yii::createObject($this->_components[$id]); - } - } - return null; - } - - /** - * Registers a component with this module. - * @param string $id component ID - * @param Component|array|null $component the component to be registered with the module. This can - * be one of the followings: - * - * - a [[Component]] object - * - a configuration array: when [[getComponent()]] is called initially for this component, the array - * will be used to instantiate the component via [[Yii::createObject()]]. - * - null: the named component will be removed from the module - */ - public function setComponent($id, $component) - { - if ($component === null) { - unset($this->_components[$id]); - } else { - $this->_components[$id] = $component; - } - } - - /** - * Returns the registered components. - * @param boolean $loadedOnly whether to return the loaded components only. If this is set false, - * then all components specified in the configuration will be returned, whether they are loaded or not. - * Loaded components will be returned as objects, while unloaded components as configuration arrays. - * @return array the components (indexed by their IDs) - */ - public function getComponents($loadedOnly = false) - { - if ($loadedOnly) { - $components = array(); - foreach ($this->_components as $component) { - if ($component instanceof Component) { - $components[] = $component; - } - } - return $components; - } else { - return $this->_components; - } - } - - /** - * Registers a set of components in this module. - * - * Each component should be specified as a name-value pair, where - * name refers to the ID of the component and value the component or a configuration - * array that can be used to create the component. In the latter case, [[Yii::createObject()]] - * will be used to create the component. - * - * If a new component has the same ID as an existing one, the existing one will be overwritten silently. - * - * The following is an example for setting two components: - * - * ~~~ - * array( - * 'db' => array( - * 'class' => 'yii\db\Connection', - * 'dsn' => 'sqlite:path/to/file.db', - * ), - * 'cache' => array( - * 'class' => 'yii\caching\DbCache', - * 'db' => 'db', - * ), - * ) - * ~~~ - * - * @param array $components components (id => component configuration or instance) - */ - public function setComponents($components) - { - foreach ($components as $id => $component) { - if (isset($this->_components[$id]['class']) && !isset($component['class'])) { - $component['class'] = $this->_components[$id]['class']; - } - $this->_components[$id] = $component; - } - } - - /** - * Loads components that are declared in [[preload]]. - */ - public function preloadComponents() - { - foreach ($this->preload as $id) { - $this->getComponent($id); - } - } - - /** - * Runs a controller action specified by a route. - * This method parses the specified route and creates the corresponding child module(s), controller and action - * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. - * If the route is empty, the method will use [[defaultRoute]]. - * @param string $route the route that specifies the action. - * @param array $params the parameters to be passed to the action - * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. - * @throws InvalidRouteException if the requested route cannot be resolved into an action successfully - */ - public function runAction($route, $params = array()) - { - $result = $this->createController($route); - if (is_array($result)) { - /** @var $controller Controller */ - list($controller, $actionID) = $result; - $oldController = Yii::$app->controller; - Yii::$app->controller = $controller; - $status = $controller->runAction($actionID, $params); - Yii::$app->controller = $oldController; - return $status; - } else { - throw new InvalidRouteException('Unable to resolve the request "' . trim($this->getUniqueId() . '/' . $route, '/') . '".'); - } - } - - /** - * Creates a controller instance based on the controller ID. - * - * The controller is created within this module. The method first attempts to - * create the controller based on the [[controllerMap]] of the module. If not available, - * it will look for the controller class under the [[controllerPath]] and create an - * instance of it. - * - * @param string $route the route consisting of module, controller and action IDs. - * @return array|boolean If the controller is created successfully, it will be returned together - * with the requested action ID. Otherwise false will be returned. - * @throws InvalidConfigException if the controller class and its file do not match. - */ - public function createController($route) - { - if ($route === '') { - $route = $this->defaultRoute; - } - if (($pos = strpos($route, '/')) !== false) { - $id = substr($route, 0, $pos); - $route = substr($route, $pos + 1); - } else { - $id = $route; - $route = ''; - } - - $module = $this->getModule($id); - if ($module !== null) { - return $module->createController($route); - } - - if (isset($this->controllerMap[$id])) { - $controller = Yii::createObject($this->controllerMap[$id], $id, $this); - } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { - $className = StringHelper::id2camel($id) . 'Controller'; - $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; - if (!is_file($classFile)) { - return false; - } - $className = ltrim($this->controllerNamespace . '\\' . $className, '\\'); - Yii::$classMap[$className] = $classFile; - 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."); - } - } - - return isset($controller) ? array($controller, $route) : false; - } - - /** - * This method is invoked right before an action is to be executed (after all possible filters.) - * You may override this method to do last-minute preparation for the action. - * @param Action $action the action to be executed. - * @return boolean whether the action should continue to be executed. - */ - public function beforeAction($action) - { - $event = new ActionEvent($action); - $this->trigger(self::EVENT_BEFORE_ACTION, $event); - return $event->isValid; - } - - /** - * This method is invoked right after an action is executed. - * You may override this method to do some postprocessing for the action. - * @param Action $action the action just executed. - */ - public function afterAction($action) - { - $this->trigger(self::EVENT_AFTER_ACTION, new ActionEvent($action)); - } -} +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\base; + +use Yii; + +/** + * Module is the base class for module and application classes. + * + * A module represents a sub-application which contains MVC elements by itself, such as + * models, views, controllers, etc. + * + * A module may consist of [[modules|sub-modules]]. + * + * [[components|Components]] may be registered with the module so that they are globally + * accessible within the module. + * + * @property array $aliases List of path aliases to be defined. The array keys are alias names (must start + * with '@') and the array values are the corresponding paths or aliases. See [[setAliases()]] for an example. + * This property is write-only. + * @property string $basePath The root directory of the module. + * @property array $components The components (indexed by their IDs). + * @property string $controllerPath The directory that contains the controller classes. + * @property string $layoutPath The root directory of layout files. Defaults to "[[viewPath]]/layouts". + * @property array $modules The modules (indexed by their IDs). + * @property string $uniqueId The unique ID of the module. This property is read-only. + * @property string $viewPath The root directory of view files. Defaults to "[[basePath]]/view". + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +abstract class Module extends Component +{ + /** + * @var array custom module parameters (name => value). + */ + public $params = array(); + /** + * @var array the IDs of the components or modules that should be preloaded when this module is created. + */ + public $preload = array(); + /** + * @var string an ID that uniquely identifies this module among other modules which have the same [[module|parent]]. + */ + public $id; + /** + * @var Module the parent module of this module. Null if this module does not have a parent. + */ + public $module; + /** + * @var string|boolean the layout that should be applied for views within this module. This refers to a view name + * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]] + * will be taken. If this is false, layout will be disabled within this module. + */ + public $layout; + /** + * @var array mapping from controller ID to controller configurations. + * Each name-value pair specifies the configuration of a single controller. + * A controller configuration can be either a string or an array. + * If the former, the string should be the class name or path alias of the controller. + * If the latter, the array must contain a 'class' element which specifies + * the controller's class name or path alias, and the rest of the name-value pairs + * in the array are used to initialize the corresponding controller properties. For example, + * + * ~~~ + * array( + * 'account' => '@app/controllers/UserController', + * 'article' => array( + * 'class' => '@app/controllers/PostController', + * 'pageTitle' => 'something new', + * ), + * ) + * ~~~ + */ + public $controllerMap = array(); + /** + * @var string the namespace that controller classes are in. If not set, + * it will use the "controllers" sub-namespace under the namespace of this module. + * For example, if the namespace of this module is "foo\bar", then the default + * controller namespace would be "foo\bar\controllers". + * If the module is an application, it will default to "app\controllers". + */ + public $controllerNamespace; + /** + * @return string the default route of this module. Defaults to 'default'. + * The route may consist of child module ID, controller ID, and/or action ID. + * For example, `help`, `post/create`, `admin/post/create`. + * If action ID is not given, it will take the default value as specified in + * [[Controller::defaultAction]]. + */ + public $defaultRoute = 'default'; + /** + * @var string the root directory of the module. + */ + private $_basePath; + /** + * @var string the root directory that contains view files for this module + */ + private $_viewPath; + /** + * @var string the root directory that contains layout view files for this module. + */ + private $_layoutPath; + /** + * @var string the directory containing controller classes in the module. + */ + private $_controllerPath; + /** + * @var array child modules of this module + */ + private $_modules = array(); + /** + * @var array components registered under this module + */ + private $_components = array(); + + /** + * Constructor. + * @param string $id the ID of this module + * @param Module $parent the parent module (if any) + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($id, $parent = null, $config = array()) + { + $this->id = $id; + $this->module = $parent; + parent::__construct($config); + } + + /** + * Getter magic method. + * This method is overridden to support accessing components + * like reading module properties. + * @param string $name component or property name + * @return mixed the named property value + */ + public function __get($name) + { + if ($this->hasComponent($name)) { + return $this->getComponent($name); + } else { + return parent::__get($name); + } + } + + /** + * Checks if a property value is null. + * This method overrides the parent implementation by checking + * if the named component is loaded. + * @param string $name the property name or the event name + * @return boolean whether the property value is null + */ + public function __isset($name) + { + if ($this->hasComponent($name)) { + return $this->getComponent($name) !== null; + } else { + return parent::__isset($name); + } + } + + /** + * Initializes the module. + * This method is called after the module is created and initialized with property values + * given in configuration. The default implement will create a path alias using the module [[id]] + * and then call [[preloadComponents()]] to load components that are declared in [[preload]]. + */ + public function init() + { + $this->preloadComponents(); + if ($this->controllerNamespace === null) { + if ($this instanceof Application) { + $this->controllerNamespace = 'app\\controllers'; + } else { + $class = get_class($this); + if (($pos = strrpos($class, '\\')) !== false) { + $this->controllerNamespace = substr($class, 0, $pos) . '\\controllers'; + } + } + } + } + + /** + * Returns an ID that uniquely identifies this module among all modules within the current application. + * Note that if the module is an application, an empty string will be returned. + * @return string the unique ID of the module. + */ + public function getUniqueId() + { + return $this->module ? ltrim($this->module->getUniqueId() . '/' . $this->id, '/') : $this->id; + } + + /** + * Returns the root directory of the module. + * It defaults to the directory containing the module class file. + * @return string the root directory of the module. + */ + public function getBasePath() + { + if ($this->_basePath === null) { + $class = new \ReflectionClass($this); + $this->_basePath = dirname($class->getFileName()); + } + return $this->_basePath; + } + + /** + * Sets the root directory of the module. + * This method can only be invoked at the beginning of the constructor. + * @param string $path the root directory of the module. This can be either a directory name or a path alias. + * @throws InvalidParamException if the directory does not exist. + */ + public function setBasePath($path) + { + $path = Yii::getAlias($path); + $p = realpath($path); + if ($p !== false && is_dir($p)) { + $this->_basePath = $p; + if ($this instanceof Application) { + Yii::setAlias('@app', $p); + } + } else { + throw new InvalidParamException("The directory does not exist: $path"); + } + } + + /** + * Returns the directory that contains the controller classes. + * Defaults to "[[basePath]]/controllers". + * @return string the directory that contains the controller classes. + */ + public function getControllerPath() + { + if ($this->_controllerPath !== null) { + return $this->_controllerPath; + } else { + return $this->_controllerPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'controllers'; + } + } + + /** + * Sets the directory that contains the controller classes. + * @param string $path the directory that contains the controller classes. + * This can be either a directory name or a path alias. + * @throws Exception if the directory is invalid + */ + public function setControllerPath($path) + { + $this->_controllerPath = Yii::getAlias($path); + } + + /** + * Returns the directory that contains the view files for this module. + * @return string the root directory of view files. Defaults to "[[basePath]]/view". + */ + public function getViewPath() + { + if ($this->_viewPath !== null) { + return $this->_viewPath; + } else { + return $this->_viewPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'views'; + } + } + + /** + * Sets the directory that contains the view files. + * @param string $path the root directory of view files. + * @throws Exception if the directory is invalid + */ + public function setViewPath($path) + { + $this->_viewPath = Yii::getAlias($path); + } + + /** + * Returns the directory that contains layout view files for this module. + * @return string the root directory of layout files. Defaults to "[[viewPath]]/layouts". + */ + public function getLayoutPath() + { + if ($this->_layoutPath !== null) { + return $this->_layoutPath; + } else { + return $this->_layoutPath = $this->getViewPath() . DIRECTORY_SEPARATOR . 'layouts'; + } + } + + /** + * Sets the directory that contains the layout files. + * @param string $path the root directory of layout files. + * @throws Exception if the directory is invalid + */ + public function setLayoutPath($path) + { + $this->_layoutPath = Yii::getAlias($path); + } + + /** + * 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. + * @property array list of path aliases to be defined. The array keys are alias names + * (must start with '@') and the array values are the corresponding paths or aliases. + * See [[setAliases()]] for an example. + * @param array $aliases list of path aliases to be defined. The array keys are alias names + * (must start with '@') and the array values are the corresponding paths or aliases. + * For example, + * + * ~~~ + * array( + * '@models' => '@app/models', // an existing alias + * '@backend' => __DIR__ . '/../backend', // a directory + * ) + * ~~~ + */ + public function setAliases($aliases) + { + foreach ($aliases as $name => $alias) { + Yii::setAlias($name, $alias); + } + } + + /** + * Checks whether the child module of the specified ID exists. + * This method supports checking the existence of both child and grand child modules. + * @param string $id module ID. For grand child modules, use ID path relative to this module (e.g. `admin/content`). + * @return boolean whether the named module exists. Both loaded and unloaded modules + * are considered. + */ + public function hasModule($id) + { + if (($pos = strpos($id, '/')) !== false) { + // sub-module + $module = $this->getModule(substr($id, 0, $pos)); + return $module === null ? false : $module->hasModule(substr($id, $pos + 1)); + } else { + return isset($this->_modules[$id]); + } + } + + /** + * Retrieves the child module of the specified ID. + * This method supports retrieving both child modules and grand child modules. + * @param string $id module ID (case-sensitive). To retrieve grand child modules, + * use ID path relative to this module (e.g. `admin/content`). + * @param boolean $load whether to load the module if it is not yet loaded. + * @return Module|null the module instance, null if the module does not exist. + * @see hasModule() + */ + public function getModule($id, $load = true) + { + if (($pos = strpos($id, '/')) !== false) { + // sub-module + $module = $this->getModule(substr($id, 0, $pos)); + return $module === null ? null : $module->getModule(substr($id, $pos + 1), $load); + } + + if (isset($this->_modules[$id])) { + if ($this->_modules[$id] instanceof Module) { + return $this->_modules[$id]; + } elseif ($load) { + Yii::trace("Loading module: $id", __METHOD__); + return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); + } + } + return null; + } + + /** + * Adds a sub-module to this module. + * @param string $id module ID + * @param Module|array|null $module the sub-module to be added to this module. This can + * be one of the followings: + * + * - a [[Module]] object + * - a configuration array: when [[getModule()]] is called initially, the array + * will be used to instantiate the sub-module + * - null: the named sub-module will be removed from this module + */ + public function setModule($id, $module) + { + if ($module === null) { + unset($this->_modules[$id]); + } else { + $this->_modules[$id] = $module; + } + } + + /** + * Returns the sub-modules in this module. + * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false, + * then all sub-modules registered in this module will be returned, whether they are loaded or not. + * Loaded modules will be returned as objects, while unloaded modules as configuration arrays. + * @return array the modules (indexed by their IDs) + */ + public function getModules($loadedOnly = false) + { + if ($loadedOnly) { + $modules = array(); + foreach ($this->_modules as $module) { + if ($module instanceof Module) { + $modules[] = $module; + } + } + return $modules; + } else { + return $this->_modules; + } + } + + /** + * Registers sub-modules in the current module. + * + * Each sub-module should be specified as a name-value pair, where + * name refers to the ID of the module and value the module or a configuration + * array that can be used to create the module. In the latter case, [[Yii::createObject()]] + * will be used to create the module. + * + * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently. + * + * The following is an example for registering two sub-modules: + * + * ~~~ + * array( + * 'comment' => array( + * 'class' => 'app\modules\comment\CommentModule', + * 'db' => 'db', + * ), + * 'booking' => array( + * 'class' => 'app\modules\booking\BookingModule', + * ), + * ) + * ~~~ + * + * @param array $modules modules (id => module configuration or instances) + */ + public function setModules($modules) + { + foreach ($modules as $id => $module) { + $this->_modules[$id] = $module; + } + } + + /** + * Checks whether the named component exists. + * @param string $id component ID + * @return boolean whether the named component exists. Both loaded and unloaded components + * are considered. + */ + public function hasComponent($id) + { + return isset($this->_components[$id]); + } + + /** + * Retrieves the named component. + * @param string $id component ID (case-sensitive) + * @param boolean $load whether to load the component if it is not yet loaded. + * @return Component|null the component instance, null if the component does not exist. + * @see hasComponent() + */ + public function getComponent($id, $load = true) + { + if (isset($this->_components[$id])) { + if ($this->_components[$id] instanceof Object) { + return $this->_components[$id]; + } elseif ($load) { + return $this->_components[$id] = Yii::createObject($this->_components[$id]); + } + } + return null; + } + + /** + * Registers a component with this module. + * @param string $id component ID + * @param Component|array|null $component the component to be registered with the module. This can + * be one of the followings: + * + * - a [[Component]] object + * - a configuration array: when [[getComponent()]] is called initially for this component, the array + * will be used to instantiate the component via [[Yii::createObject()]]. + * - null: the named component will be removed from the module + */ + public function setComponent($id, $component) + { + if ($component === null) { + unset($this->_components[$id]); + } else { + $this->_components[$id] = $component; + } + } + + /** + * Returns the registered components. + * @param boolean $loadedOnly whether to return the loaded components only. If this is set false, + * then all components specified in the configuration will be returned, whether they are loaded or not. + * Loaded components will be returned as objects, while unloaded components as configuration arrays. + * @return array the components (indexed by their IDs) + */ + public function getComponents($loadedOnly = false) + { + if ($loadedOnly) { + $components = array(); + foreach ($this->_components as $component) { + if ($component instanceof Component) { + $components[] = $component; + } + } + return $components; + } else { + return $this->_components; + } + } + + /** + * Registers a set of components in this module. + * + * Each component should be specified as a name-value pair, where + * name refers to the ID of the component and value the component or a configuration + * array that can be used to create the component. In the latter case, [[Yii::createObject()]] + * will be used to create the component. + * + * If a new component has the same ID as an existing one, the existing one will be overwritten silently. + * + * The following is an example for setting two components: + * + * ~~~ + * array( + * 'db' => array( + * 'class' => 'yii\db\Connection', + * 'dsn' => 'sqlite:path/to/file.db', + * ), + * 'cache' => array( + * 'class' => 'yii\caching\DbCache', + * 'db' => 'db', + * ), + * ) + * ~~~ + * + * @param array $components components (id => component configuration or instance) + */ + public function setComponents($components) + { + foreach ($components as $id => $component) { + if (isset($this->_components[$id]['class']) && !isset($component['class'])) { + $component['class'] = $this->_components[$id]['class']; + } + $this->_components[$id] = $component; + } + } + + /** + * Loads components that are declared in [[preload]]. + * @throws InvalidConfigException if a component or module to be preloaded is unknown + */ + public function preloadComponents() + { + foreach ($this->preload as $id) { + if ($this->hasComponent($id)) { + $this->getComponent($id); + } elseif ($this->hasModule($id)) { + $this->getModule($id); + } else { + throw new InvalidConfigException("Unknown component or module: $id"); + } + } + } + + /** + * Runs a controller action specified by a route. + * This method parses the specified route and creates the corresponding child module(s), controller and action + * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. + * If the route is empty, the method will use [[defaultRoute]]. + * @param string $route the route that specifies the action. + * @param array $params the parameters to be passed to the action + * @return mixed the result of the action. + * @throws InvalidRouteException if the requested route cannot be resolved into an action successfully + */ + public function runAction($route, $params = array()) + { + $parts = $this->createController($route); + if (is_array($parts)) { + /** @var $controller Controller */ + list($controller, $actionID) = $parts; + $oldController = Yii::$app->controller; + Yii::$app->controller = $controller; + $result = $controller->runAction($actionID, $params); + Yii::$app->controller = $oldController; + return $result; + } else { + throw new InvalidRouteException('Unable to resolve the request "' . trim($this->getUniqueId() . '/' . $route, '/') . '".'); + } + } + + /** + * Creates a controller instance based on the controller ID. + * + * The controller is created within this module. The method first attempts to + * create the controller based on the [[controllerMap]] of the module. If not available, + * it will look for the controller class under the [[controllerPath]] and create an + * instance of it. + * + * @param string $route the route consisting of module, controller and action IDs. + * @return array|boolean If the controller is created successfully, it will be returned together + * with the requested action ID. Otherwise false will be returned. + * @throws InvalidConfigException if the controller class and its file do not match. + */ + public function createController($route) + { + if ($route === '') { + $route = $this->defaultRoute; + } + if (($pos = strpos($route, '/')) !== false) { + $id = substr($route, 0, $pos); + $route = substr($route, $pos + 1); + } else { + $id = $route; + $route = ''; + } + + $module = $this->getModule($id); + if ($module !== null) { + return $module->createController($route); + } + + if (isset($this->controllerMap[$id])) { + $controller = Yii::createObject($this->controllerMap[$id], $id, $this); + } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { + $className = str_replace(' ', '', ucwords(str_replace('-', ' ', $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 (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."); + } + } + + return isset($controller) ? array($controller, $route) : false; + } + + /** + * This method is invoked right before an action of this module is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * Make sure you call the parent implementation so that the relevant event is triggered. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + return true; + } + + /** + * This method is invoked right after an action of this module has been executed. + * You may override this method to do some postprocessing for the action. + * Make sure you call the parent implementation so that the relevant event is triggered. + * @param Action $action the action just executed. + * @param mixed $result the action return result. + */ + public function afterAction($action, &$result) + { + } +} diff --git a/yii/base/NotSupportedException.php b/framework/yii/base/NotSupportedException.php similarity index 95% rename from yii/base/NotSupportedException.php rename to framework/yii/base/NotSupportedException.php index 2f08891..33f936f 100644 --- a/yii/base/NotSupportedException.php +++ b/framework/yii/base/NotSupportedException.php @@ -20,7 +20,6 @@ class NotSupportedException extends Exception */ public function getName() { - return \Yii::t('yii|Not Supported'); + return \Yii::t('yii', 'Not Supported'); } } - diff --git a/yii/base/Object.php b/framework/yii/base/Object.php similarity index 81% rename from yii/base/Object.php rename to framework/yii/base/Object.php index a90a231..55754de 100644 --- a/yii/base/Object.php +++ b/framework/yii/base/Object.php @@ -7,12 +7,14 @@ namespace yii\base; +use Yii; + /** * @include @yii/base/Object.md * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Object +class Object implements Arrayable { /** * @return string the fully qualified name of this class. @@ -38,8 +40,8 @@ class Object */ public function __construct($config = array()) { - foreach ($config as $name => $value) { - $this->$name = $value; + if (!empty($config)) { + Yii::configure($this, $config); } $this->init(); } @@ -59,8 +61,7 @@ class Object * Do not call this method directly as it is a PHP magic method that * will be implicitly called when executing `$value = $object->property;`. * @param string $name the property name - * @return mixed the property value, event handlers attached to the event, - * the named behavior, or the value of a behavior's property + * @return mixed the property value * @throws UnknownPropertyException if the property is not defined * @see __set */ @@ -69,6 +70,8 @@ class Object $getter = 'get' . $name; if (method_exists($this, $getter)) { return $this->$getter(); + } elseif (method_exists($this, 'set' . $name)) { + throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name); } else { throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); } @@ -140,8 +143,6 @@ class Object /** * Calls the named method which is not a class method. - * If the name refers to a component property whose value is - * an anonymous function, the method will execute the function. * * Do not call this method directly as it is a PHP magic method that * will be implicitly called when an unknown method is being invoked. @@ -152,13 +153,6 @@ class Object */ public function __call($name, $params) { - $getter = 'get' . $name; - if (method_exists($this, $getter)) { - $func = $this->$getter(); - if ($func instanceof \Closure) { - return call_user_func_array($func, $params); - } - } throw new UnknownMethodException('Unknown method: ' . get_class($this) . "::$name()"); } @@ -168,17 +162,17 @@ class Object * * - the class has a getter or setter method associated with the specified name * (in this case, property name is case-insensitive); - * - the class has a member variable with the specified name (when `$checkVar` is true); + * - the class has a member variable with the specified name (when `$checkVars` is true); * * @param string $name the property name - * @param boolean $checkVar whether to treat member variables as properties + * @param boolean $checkVars whether to treat member variables as properties * @return boolean whether the property is defined * @see canGetProperty * @see canSetProperty */ - public function hasProperty($name, $checkVar = true) + public function hasProperty($name, $checkVars = true) { - return $this->canGetProperty($name, $checkVar) || $this->canSetProperty($name, false); + return $this->canGetProperty($name, $checkVars) || $this->canSetProperty($name, false); } /** @@ -187,16 +181,16 @@ class Object * * - the class has a getter method associated with the specified name * (in this case, property name is case-insensitive); - * - the class has a member variable with the specified name (when `$checkVar` is true); + * - the class has a member variable with the specified name (when `$checkVars` is true); * * @param string $name the property name - * @param boolean $checkVar whether to treat member variables as properties + * @param boolean $checkVars whether to treat member variables as properties * @return boolean whether the property can be read * @see canSetProperty */ - public function canGetProperty($name, $checkVar = true) + public function canGetProperty($name, $checkVars = true) { - return method_exists($this, 'get' . $name) || $checkVar && property_exists($this, $name); + return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name); } /** @@ -205,15 +199,38 @@ class Object * * - the class has a setter method associated with the specified name * (in this case, property name is case-insensitive); - * - the class has a member variable with the specified name (when `$checkVar` is true); + * - the class has a member variable with the specified name (when `$checkVars` is true); * * @param string $name the property name - * @param boolean $checkVar whether to treat member variables as properties + * @param boolean $checkVars whether to treat member variables as properties * @return boolean whether the property can be written * @see canGetProperty */ - public function canSetProperty($name, $checkVar = true) + public function canSetProperty($name, $checkVars = true) + { + return method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name); + } + + /** + * Returns a value indicating whether a method is defined. + * + * The default implementation is a call to php function `method_exists()`. + * You may override this method when you implemented the php magic method `__call()`. + * @param string $name the property name + * @return boolean whether the property is defined + */ + public function hasMethod($name) + { + return method_exists($this, $name); + } + + /** + * Converts the object into an array. + * The default implementation will return all public property values as an array. + * @return array the array representation of the object + */ + public function toArray() { - return method_exists($this, 'set' . $name) || $checkVar && property_exists($this, $name); + return Yii::getObjectVars($this); } } diff --git a/yii/base/Request.php b/framework/yii/base/Request.php similarity index 97% rename from yii/base/Request.php rename to framework/yii/base/Request.php index 45556ab..0d660d6 100644 --- a/yii/base/Request.php +++ b/framework/yii/base/Request.php @@ -8,6 +8,10 @@ namespace yii\base; /** + * + * @property boolean $isConsoleRequest The value indicating whether the current request is made via console. + * @property string $scriptFile Entry script file path (processed w/ realpath()). + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ diff --git a/framework/yii/base/Response.php b/framework/yii/base/Response.php new file mode 100644 index 0000000..467de9e --- /dev/null +++ b/framework/yii/base/Response.php @@ -0,0 +1,28 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\base; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Response extends Component +{ + /** + * @var integer the exit status. Exit statuses should be in the range 0 to 254. + * The status 0 means the program terminates successfully. + */ + public $exitStatus = 0; + + /** + * Sends the response to client. + */ + public function send() + { + } +} diff --git a/yii/base/Theme.php b/framework/yii/base/Theme.php similarity index 86% rename from yii/base/Theme.php rename to framework/yii/base/Theme.php index a60d56e..658ada1 100644 --- a/yii/base/Theme.php +++ b/framework/yii/base/Theme.php @@ -21,11 +21,25 @@ use yii\helpers\FileHelper; * with its themed version if part of its path matches one of the keys in [[pathMap]]. * Then the matched part will be replaced with the corresponding array value. * - * For example, if [[pathMap]] is `array('/www/views' => '/www/themes/basic')`, - * then the themed version for a view file `/www/views/site/index.php` will be - * `/www/themes/basic/site/index.php`. + * For example, if [[pathMap]] is `array('/web/views' => '/web/themes/basic')`, + * then the themed version for a view file `/web/views/site/index.php` will be + * `/web/themes/basic/site/index.php`. * - * @property string $baseUrl the base URL for this theme. This is mainly used by [[getUrl()]]. + * To use a theme, you should configure the [[View::theme|theme]] property of the "view" application + * component like the following: + * + * ~~~ + * 'view' => array( + * 'theme' => array( + * 'basePath' => '@webroot/themes/basic', + * 'baseUrl' => '@web/themes/basic', + * ), + * ), + * ~~~ + * + * The above configuration specifies a theme located under the "themes/basic" directory of the Web folder + * that contains the entry script of the application. If your theme is designed to handle modules, + * you may configure the [[pathMap]] property like described above. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -58,7 +72,7 @@ class Theme extends Component */ public function init() { - parent::init(); + parent::init(); if (empty($this->pathMap)) { if ($this->basePath !== null) { $this->basePath = Yii::getAlias($this->basePath); diff --git a/yii/base/UnknownClassException.php b/framework/yii/base/UnknownClassException.php similarity index 87% rename from yii/base/UnknownClassException.php rename to framework/yii/base/UnknownClassException.php index ac44746..8ccd09c 100644 --- a/yii/base/UnknownClassException.php +++ b/framework/yii/base/UnknownClassException.php @@ -8,7 +8,7 @@ namespace yii\base; /** - * UnknownClassException represents an exception caused by accessing an unknown class. + * UnknownClassException represents an exception caused by using an unknown class. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -20,7 +20,6 @@ class UnknownClassException extends Exception */ public function getName() { - return \Yii::t('yii|Unknown Class'); + return \Yii::t('yii', 'Unknown Class'); } } - diff --git a/yii/base/UnknownMethodException.php b/framework/yii/base/UnknownMethodException.php similarity index 95% rename from yii/base/UnknownMethodException.php rename to framework/yii/base/UnknownMethodException.php index 440e76e..3b33659 100644 --- a/yii/base/UnknownMethodException.php +++ b/framework/yii/base/UnknownMethodException.php @@ -20,7 +20,6 @@ class UnknownMethodException extends Exception */ public function getName() { - return \Yii::t('yii|Unknown Method'); + return \Yii::t('yii', 'Unknown Method'); } } - diff --git a/yii/base/UnknownPropertyException.php b/framework/yii/base/UnknownPropertyException.php similarity index 95% rename from yii/base/UnknownPropertyException.php rename to framework/yii/base/UnknownPropertyException.php index 5ec3814..682fdfa 100644 --- a/yii/base/UnknownPropertyException.php +++ b/framework/yii/base/UnknownPropertyException.php @@ -20,7 +20,6 @@ class UnknownPropertyException extends Exception */ public function getName() { - return \Yii::t('yii|Unknown Property'); + return \Yii::t('yii', 'Unknown Property'); } } - diff --git a/yii/base/UserException.php b/framework/yii/base/UserException.php similarity index 100% rename from yii/base/UserException.php rename to framework/yii/base/UserException.php diff --git a/yii/base/View.php b/framework/yii/base/View.php similarity index 76% rename from yii/base/View.php rename to framework/yii/base/View.php index af65c49..f9cae98 100644 --- a/yii/base/View.php +++ b/framework/yii/base/View.php @@ -8,29 +8,44 @@ namespace yii\base; use Yii; -use yii\base\Application; use yii\helpers\FileHelper; use yii\helpers\Html; +use yii\web\JqueryAsset; +use yii\web\AssetBundle; +use yii\widgets\Block; +use yii\widgets\ContentDecorator; +use yii\widgets\FragmentCache; /** * View represents a view object in the MVC pattern. * * View provides a set of methods (e.g. [[render()]]) for rendering purpose. * + * @property \yii\web\AssetManager $assetManager The asset manager. Defaults to the "assetManager" application + * component. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ class View extends Component { /** - * @event ViewEvent an event that is triggered by [[beginPage()]]. + * @event Event an event that is triggered by [[beginPage()]]. */ const EVENT_BEGIN_PAGE = 'beginPage'; /** - * @event ViewEvent an event that is triggered by [[endPage()]]. + * @event Event an event that is triggered by [[endPage()]]. */ const EVENT_END_PAGE = 'endPage'; /** + * @event Event an event that is triggered by [[beginBody()]]. + */ + const EVENT_BEGIN_BODY = 'beginBody'; + /** + * @event Event an event that is triggered by [[endBody()]]. + */ + const EVENT_END_BODY = 'endBody'; + /** * @event ViewEvent an event that is triggered by [[renderFile()]] right before it renders a view file. */ const EVENT_BEFORE_RENDER = 'beforeRender'; @@ -55,17 +70,22 @@ class View extends Component */ const POS_END = 3; /** + * The location of registered JavaScript code block. + * This means the JavaScript code block will be enclosed within `jQuery(document).ready()`. + */ + const POS_READY = 4; + /** * This is internally used as the placeholder for receiving the content registered for the head section. */ - const PL_HEAD = '<![CDATA[YII-BLOCK-HEAD]]>'; + const PH_HEAD = '<![CDATA[YII-BLOCK-HEAD]]>'; /** * This is internally used as the placeholder for receiving the content registered for the beginning of the body section. */ - const PL_BODY_BEGIN = '<![CDATA[YII-BLOCK-BODY-BEGIN]]>'; + const PH_BODY_BEGIN = '<![CDATA[YII-BLOCK-BODY-BEGIN]]>'; /** * This is internally used as the placeholder for receiving the content registered for the end of the body section. */ - const PL_BODY_END = '<![CDATA[YII-BLOCK-BODY-END]]>'; + const PH_BODY_END = '<![CDATA[YII-BLOCK-BODY-END]]>'; /** @@ -76,25 +96,27 @@ class View extends Component /** * @var mixed custom parameters that are shared among view templates. */ - public $params; + public $params = array(); /** * @var array a list of available renderers indexed by their corresponding supported file extensions. * Each renderer may be a view renderer object or the configuration for creating the renderer object. - * The default setting supports both Smarty and Twig (their corresponding file extension is "tpl" - * and "twig" respectively. Please refer to [[SmartyRenderer]] and [[TwigRenderer]] on how to install - * the needed libraries for these template engines. + * For example, the following configuration enables both Smarty and Twig view renderers: + * + * ~~~ + * array( + * 'tpl' => array( + * 'class' => 'yii\smarty\ViewRenderer', + * ), + * 'twig' => array( + * 'class' => 'yii\twig\ViewRenderer', + * ), + * ) + * ~~~ * * If no renderer is available for the given view file, the view file will be treated as a normal PHP * and rendered via [[renderPhpFile()]]. */ - public $renderers = array( - 'tpl' => array( - 'class' => 'yii\renderers\SmartyRenderer', - ), - 'twig' => array( - 'class' => 'yii\renderers\TwigRenderer', - ), - ); + public $renderers; /** * @var Theme|array the theme object or the configuration array for creating the theme object. * If not set, it means theming is not enabled. @@ -108,12 +130,6 @@ class View extends Component */ 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 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 directly. * @internal @@ -126,11 +142,11 @@ class View extends Component */ public $dynamicPlaceholders = array(); /** - * @var array the registered asset bundles. The keys are the bundle names, and the values - * are the corresponding [[AssetBundle]] objects. + * @var AssetBundle[] list of the registered asset bundles. The keys are the bundle names, and the values + * are the registered [[AssetBundle]] objects. * @see registerAssetBundle */ - public $assetBundles; + public $assetBundles = array(); /** * @var string the page title */ @@ -174,6 +190,9 @@ class View extends Component { parent::init(); if (is_array($this->theme)) { + if (!isset($this->theme['class'])) { + $this->theme['class'] = 'yii\base\Theme'; + } $this->theme = Yii::createObject($this->theme); } } @@ -228,10 +247,10 @@ class View extends Component public function renderFile($viewFile, $params = array(), $context = null) { $viewFile = Yii::getAlias($viewFile); + if ($this->theme !== null) { + $viewFile = $this->theme->applyTo($viewFile); + } if (is_file($viewFile)) { - if ($this->theme !== null) { - $viewFile = $this->theme->applyTo($viewFile); - } $viewFile = FileHelper::localize($viewFile); } else { throw new InvalidParamException("The view file does not exist: $viewFile"); @@ -253,6 +272,7 @@ class View extends Component $renderer = $this->renderers[$ext]; $output = $renderer->render($this, $viewFile, $params); } else { + Yii::trace("Rendering view file: $viewFile", __METHOD__); $output = $this->renderPhpFile($viewFile, $params); } $this->afterRender($viewFile, $output); @@ -364,93 +384,19 @@ class View extends Component } /** - * Creates a widget. - * This method will use [[Yii::createObject()]] to create the widget. - * @param string $class the widget class name or path alias - * @param array $properties the initial property values of the widget. - * @return Widget the newly created widget instance - */ - public function createWidget($class, $properties = array()) - { - $properties['class'] = $class; - if (!isset($properties['view'])) { - $properties['view'] = $this; - } - return Yii::createObject($properties); - } - - /** - * Creates and runs a widget. - * Compared with [[createWidget()]], this method does one more thing: it will - * run the widget after it is created. - * @param string $class the widget class name or path alias - * @param array $properties the initial property values of the widget. - * @param boolean $captureOutput whether to capture the output of the widget and return it as a string - * @return string|Widget if $captureOutput is true, the output of the widget will be returned; - * otherwise the widget object will be returned. - */ - public function widget($class, $properties = array(), $captureOutput = false) - { - if ($captureOutput) { - ob_start(); - ob_implicit_flush(false); - $widget = $this->createWidget($class, $properties); - $widget->run(); - return ob_get_clean(); - } else { - $widget = $this->createWidget($class, $properties); - $widget->run(); - return $widget; - } - } - - /** - * Begins a widget. - * This method is similar to [[createWidget()]] except that it will expect a matching - * [[endWidget()]] call after this. - * @param string $class the widget class name or path alias - * @param array $properties the initial property values of the widget. - * @return Widget the widget instance - */ - public function beginWidget($class, $properties = array()) - { - $widget = $this->createWidget($class, $properties); - $this->widgetStack[] = $widget; - return $widget; - } - - /** - * Ends a widget. - * Note that the rendering result of the widget is directly echoed out. - * If you want to capture the rendering result of a widget, you may use - * [[createWidget()]] and [[Widget::run()]]. - * @return Widget the widget instance - * @throws InvalidCallException if [[beginWidget()]] and [[endWidget()]] calls are not properly nested - */ - public function endWidget() - { - $widget = array_pop($this->widgetStack); - if ($widget instanceof Widget) { - $widget->run(); - return $widget; - } else { - throw new InvalidCallException("Unmatched beginWidget() and endWidget() calls."); - } - } - - /** * Begins recording a block. - * This method is a shortcut to beginning [[yii\widgets\Block]] + * This method is a shortcut to beginning [[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 + * @return Block the Block widget instance */ public function beginBlock($id, $renderInPlace = false) { - return $this->beginWidget('yii\widgets\Block', array( + return Block::begin(array( 'id' => $id, 'renderInPlace' => $renderInPlace, + 'view' => $this, )); } @@ -459,31 +405,32 @@ class View extends Component */ public function endBlock() { - $this->endWidget(); + Block::end(); } /** * Begins the rendering of content that is to be decorated by the specified view. * 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: + * in another layout file specified as '@app/view/layouts/base.php' like the following: * * ~~~ - * <?php $this->beginContent('@app/view/layouts/base'); ?> + * <?php $this->beginContent('@app/view/layouts/base.php'); ?> * ...layout content here... * <?php $this->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 + * @param array $params the variables (name => value) to be extracted and made available in the decorative view. + * @return ContentDecorator the ContentDecorator widget instance + * @see ContentDecorator */ public function beginContent($viewFile, $params = array()) { - return $this->beginWidget('yii\widgets\ContentDecorator', array( + return ContentDecorator::begin(array( 'viewFile' => $viewFile, 'params' => $params, + 'view' => $this, )); } @@ -492,7 +439,7 @@ class View extends Component */ public function endContent() { - $this->endWidget(); + ContentDecorator::end(); } /** @@ -503,22 +450,23 @@ class View extends Component * A typical usage of fragment caching is as follows, * * ~~~ - * if($this->beginCache($id)) { + * if ($this->beginCache($id)) { * // ...generate content here * $this->endCache(); * } * ~~~ * * @param string $id a unique ID identifying the fragment to be cached. - * @param array $properties initial property values for [[\yii\widgets\FragmentCache]] + * @param array $properties initial property values for [[FragmentCache]] * @return boolean whether you should generate the content for caching. * False if the cached version is available. */ public function beginCache($id, $properties = array()) { $properties['id'] = $id; - /** @var $cache \yii\widgets\FragmentCache */ - $cache = $this->beginWidget('yii\widgets\FragmentCache', $properties); + $properties['view'] = $this; + /** @var $cache FragmentCache */ + $cache = FragmentCache::begin($properties); if ($cache->getCachedContent() !== false) { $this->endCache(); return false; @@ -532,7 +480,7 @@ class View extends Component */ public function endCache() { - $this->endWidget(); + FragmentCache::end(); } @@ -575,14 +523,16 @@ class View extends Component $this->trigger(self::EVENT_END_PAGE); $content = ob_get_clean(); + foreach(array_keys($this->assetBundles) as $bundle) { + $this->registerAssetFiles($bundle); + } echo strtr($content, array( - self::PL_HEAD => $this->renderHeadHtml(), - self::PL_BODY_BEGIN => $this->renderBodyBeginHtml(), - self::PL_BODY_END => $this->renderBodyEndHtml(), + self::PH_HEAD => $this->renderHeadHtml(), + self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(), + self::PH_BODY_END => $this->renderBodyEndHtml(), )); unset( - $this->assetBundles, $this->metaTags, $this->linkTags, $this->css, @@ -593,11 +543,30 @@ class View extends Component } /** + * Registers all files provided by an asset bundle including depending bundles files. + * Removes a bundle from [[assetBundles]] once registered. + * @param string $name name of the bundle to register + */ + private function registerAssetFiles($name) + { + if (!isset($this->assetBundles[$name])) { + return; + } + $bundle = $this->assetBundles[$name]; + foreach($bundle->depends as $dep) { + $this->registerAssetFiles($dep); + } + $bundle->registerAssets($this); + unset($this->assetBundles[$name]); + } + + /** * Marks the beginning of an HTML body section. */ public function beginBody() { - echo self::PL_BODY_BEGIN; + echo self::PH_BODY_BEGIN; + $this->trigger(self::EVENT_BEGIN_BODY); } /** @@ -605,7 +574,8 @@ class View extends Component */ public function endBody() { - echo self::PL_BODY_END; + $this->trigger(self::EVENT_END_BODY); + echo self::PH_BODY_END; } /** @@ -613,30 +583,51 @@ class View extends Component */ public function head() { - echo self::PL_HEAD; + echo self::PH_HEAD; } /** * Registers the named asset bundle. * All dependent asset bundles will be registered. * @param string $name the name of the asset bundle. + * @param integer|null $position if set, this forces a minimum position for javascript files. + * This will adjust depending assets javascript file position or fail if requirement can not be met. + * If this is null, asset bundles position settings will not be changed. + * See [[registerJsFile]] for more details on javascript position. + * @return AssetBundle the registered asset bundle instance * @throws InvalidConfigException if the asset bundle does not exist or a circular dependency is detected */ - public function registerAssetBundle($name) + public function registerAssetBundle($name, $position = null) { 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"); + $this->assetBundles[$name] = false; + // register dependencies + $pos = isset($bundle->jsOptions['position']) ? $bundle->jsOptions['position'] : null; + foreach ($bundle->depends as $dep) { + $this->registerAssetBundle($dep, $pos); } + $this->assetBundles[$name] = $bundle; } elseif ($this->assetBundles[$name] === false) { throw new InvalidConfigException("A circular dependency is detected for bundle '$name'."); + } else { + $bundle = $this->assetBundles[$name]; + } + + if ($position !== null) { + $pos = isset($bundle->jsOptions['position']) ? $bundle->jsOptions['position'] : null; + if ($pos === null) { + $bundle->jsOptions['position'] = $pos = $position; + } elseif ($pos > $position) { + throw new InvalidConfigException("An asset bundle that depends on '$name' has a higher javascript file position configured than '$name'."); + } + // update position for all dependencies + foreach ($bundle->depends as $dep) { + $this->registerAssetBundle($dep, $pos); + } } + return $bundle; } /** @@ -681,7 +672,7 @@ class View extends Component */ public function registerCss($css, $options = array(), $key = null) { - $key = $key ?: $css; + $key = $key ?: md5($css); $this->css[$key] = Html::style($css, $options); } @@ -702,28 +693,32 @@ class View extends Component /** * 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: + * @param integer $position the position at which the JS script tag should be inserted + * in a page. The possible values 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 + * - [[POS_READY]]: enclosed within jQuery(document).ready(). This is the default value. + * Note that by using this position, the method will automatically register the jQuery js file. * * @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) + public function registerJs($js, $position = self::POS_READY, $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); + $key = $key ?: md5($js); + $this->js[$position][$key] = $js; + if ($position === self::POS_READY) { + JqueryAsset::register($this); + } } /** * Registers a JS file. + * Please note that when this file depends on other JS files to be registered before, + * for example jQuery, you should use [[registerAssetBundle]] instead. * @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 @@ -731,7 +726,7 @@ class View extends Component * * - [[POS_HEAD]]: in the head section * - [[POS_BEGIN]]: at the beginning of the body section - * - [[POS_END]]: at the end of the body section + * - [[POS_END]]: at the end of the body section. This is the default value. * * @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 @@ -756,6 +751,13 @@ class View extends Component if (!empty($this->metaTags)) { $lines[] = implode("\n", $this->metaTags); } + + $request = Yii::$app->getRequest(); + if ($request instanceof \yii\web\Request && $request->enableCsrfValidation) { + $lines[] = Html::tag('meta', '', array('name' => 'csrf-var', 'content' => $request->csrfVar)); + $lines[] = Html::tag('meta', '', array('name' => 'csrf-token', 'content' => $request->getCsrfToken())); + } + if (!empty($this->linkTags)) { $lines[] = implode("\n", $this->linkTags); } @@ -769,9 +771,9 @@ class View extends Component $lines[] = implode("\n", $this->jsFiles[self::POS_HEAD]); } if (!empty($this->js[self::POS_HEAD])) { - $lines[] = implode("\n", $this->js[self::POS_HEAD]); + $lines[] = Html::script(implode("\n", $this->js[self::POS_HEAD]), array('type' => 'text/javascript')); } - return implode("\n", $lines); + return empty($lines) ? '' : implode("\n", $lines); } /** @@ -786,9 +788,9 @@ class View extends Component $lines[] = implode("\n", $this->jsFiles[self::POS_BEGIN]); } if (!empty($this->js[self::POS_BEGIN])) { - $lines[] = implode("\n", $this->js[self::POS_BEGIN]); + $lines[] = Html::script(implode("\n", $this->js[self::POS_BEGIN]), array('type' => 'text/javascript')); } - return implode("\n", $lines); + return empty($lines) ? '' : implode("\n", $lines); } /** @@ -803,8 +805,12 @@ class View extends Component $lines[] = implode("\n", $this->jsFiles[self::POS_END]); } if (!empty($this->js[self::POS_END])) { - $lines[] = implode("\n", $this->js[self::POS_END]); + $lines[] = Html::script(implode("\n", $this->js[self::POS_END]), array('type' => 'text/javascript')); + } + if (!empty($this->js[self::POS_READY])) { + $js = "jQuery(document).ready(function(){\n" . implode("\n", $this->js[self::POS_READY]) . "\n});"; + $lines[] = Html::script($js, array('type' => 'text/javascript')); } - return implode("\n", $lines); + return empty($lines) ? '' : implode("\n", $lines); } } diff --git a/yii/base/ViewEvent.php b/framework/yii/base/ViewEvent.php similarity index 100% rename from yii/base/ViewEvent.php rename to framework/yii/base/ViewEvent.php diff --git a/yii/base/ViewRenderer.php b/framework/yii/base/ViewRenderer.php similarity index 100% rename from yii/base/ViewRenderer.php rename to framework/yii/base/ViewRenderer.php diff --git a/yii/base/Widget.php b/framework/yii/base/Widget.php similarity index 51% rename from yii/base/Widget.php rename to framework/yii/base/Widget.php index c0c524f..943d007 100644 --- a/yii/base/Widget.php +++ b/framework/yii/base/Widget.php @@ -8,32 +8,90 @@ namespace yii\base; use Yii; -use yii\helpers\FileHelper; /** * Widget is the base class for widgets. * + * @property string $id ID of the widget. + * @property View $view The view object that can be used to render views or view files. + * @property string $viewPath The directory containing the view files for this widget. This property is + * read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ class Widget extends Component { /** - * @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()]]. + * @var integer a counter used to generate [[id]] for widgets. + * @internal */ - public $view; + public static $_counter = 0; /** - * @var string id of the widget. + * @var Widget[] the widgets that are currently being rendered (not ended). This property + * is maintained by [[begin()]] and [[end()]] methods. + * @internal */ - private $_id; + public static $_stack = array(); + + /** - * @var integer a counter used to generate IDs for widgets. + * Begins a widget. + * This method creates an instance of the calling class. It will apply the configuration + * to the created instance. A matching [[end()]] call should be called later. + * @param array $config name-value pairs that will be used to initialize the object properties + * @return Widget the newly created widget instance */ - private static $_counter = 0; + public static function begin($config = array()) + { + $config['class'] = get_called_class(); + /** @var Widget $widget */ + $widget = Yii::createObject($config); + self::$_stack[] = $widget; + return $widget; + } /** + * Ends a widget. + * Note that the rendering result of the widget is directly echoed out. + * @return Widget the widget instance that is ended. + * @throws InvalidCallException if [[begin()]] and [[end()]] calls are not properly nested + */ + public static function end() + { + if (!empty(self::$_stack)) { + $widget = array_pop(self::$_stack); + if (get_class($widget) === get_called_class()) { + $widget->run(); + return $widget; + } else { + throw new InvalidCallException("Expecting end() of " . get_class($widget) . ", found " . get_called_class()); + } + } else { + throw new InvalidCallException("Unexpected " . get_called_class() . '::end() call. A matching begin() is not found.'); + } + } + + /** + * Creates a widget instance and runs it. + * The widget rendering result is returned by this method. + * @param array $config name-value pairs that will be used to initialize the object properties + * @return string the rendering result of the widget. + */ + public static function widget($config = array()) + { + ob_start(); + ob_implicit_flush(false); + /** @var Widget $widget */ + $config['class'] = get_called_class(); + $widget = Yii::createObject($config); + $widget->run(); + return ob_get_clean(); + } + + private $_id; + + /** * 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. @@ -55,6 +113,32 @@ class Widget extends Component $this->_id = $value; } + private $_view; + + /** + * Returns the view object that can be used to render views or view files. + * The [[render()]] and [[renderFile()]] methods will use + * this view object to implement the actual view rendering. + * If not set, it will default to the "view" application component. + * @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 widget. + * @param View $view the view object that can be used to render views or view files. + */ + public function setView($view) + { + $this->_view = $view; + } + /** * Executes the widget. */ @@ -84,7 +168,7 @@ class Widget extends Component public function render($view, $params = array()) { $viewFile = $this->findViewFile($view); - return $this->view->renderFile($viewFile, $params, $this); + return $this->getView()->renderFile($viewFile, $params, $this); } /** @@ -96,7 +180,7 @@ class Widget extends Component */ public function renderFile($file, $params = array()) { - return $this->view->renderFile($file, $params, $this); + return $this->getView()->renderFile($file, $params, $this); } /** diff --git a/framework/yii/behaviors/AutoTimestamp.php b/framework/yii/behaviors/AutoTimestamp.php new file mode 100644 index 0000000..63a3ef0 --- /dev/null +++ b/framework/yii/behaviors/AutoTimestamp.php @@ -0,0 +1,104 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\behaviors; + +use yii\base\Behavior; +use yii\base\Event; +use yii\db\Expression; +use yii\db\ActiveRecord; + +/** + * AutoTimestamp will automatically fill the attributes about creation time and updating time. + * + * AutoTimestamp fills the attributes when the associated AR model is being inserted or updated. + * You may specify an AR to use this behavior like the following: + * + * ~~~ + * public function behaviors() + * { + * return array( + * 'timestamp' => array( + * 'class' => 'yii\behaviors\AutoTimestamp', + * ), + * ); + * } + * ~~~ + * + * By default, AutoTimestamp will fill the `create_time` attribute with the current timestamp + * when the associated AR object is being inserted; it will fill the `update_time` attribute + * with the timestamp when the AR object is being updated. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class AutoTimestamp extends Behavior +{ + /** + * @var array list of attributes that are to be automatically filled with timestamps. + * The array keys are the ActiveRecord events upon which the attributes are to be filled with timestamps, + * and the array values are the corresponding attribute to be updated. You can use a string to represent + * a single attribute, or an array to represent a list of attributes. + * The default setting is to update the `create_time` attribute upon AR insertion, + * and update the `update_time` attribute upon AR updating. + */ + public $attributes = array( + ActiveRecord::EVENT_BEFORE_INSERT => 'create_time', + ActiveRecord::EVENT_BEFORE_UPDATE => 'update_time', + ); + /** + * @var \Closure|Expression The expression that will be used for generating the timestamp. + * This can be either an anonymous function that returns the timestamp value, + * or an [[Expression]] object representing a DB expression (e.g. `new Expression('NOW()')`). + * If not set, it will use the value of `time()` to fill the attributes. + */ + public $timestamp; + + + /** + * Declares event handlers for the [[owner]]'s events. + * @return array events (array keys) and the corresponding event handler methods (array values). + */ + public function events() + { + $events = $this->attributes; + foreach ($events as $i => $event) { + $events[$i] = 'updateTimestamp'; + } + return $events; + } + + /** + * Updates the attributes with the current timestamp. + * @param Event $event + */ + public function updateTimestamp($event) + { + $attributes = isset($this->attributes[$event->name]) ? (array)$this->attributes[$event->name] : array(); + if (!empty($attributes)) { + $timestamp = $this->evaluateTimestamp(); + foreach ($attributes as $attribute) { + $this->owner->$attribute = $timestamp; + } + } + } + + /** + * Gets the current timestamp. + * @return mixed the timestamp value + */ + protected function evaluateTimestamp() + { + if ($this->timestamp instanceof Expression) { + return $this->timestamp; + } elseif ($this->timestamp !== null) { + return call_user_func($this->timestamp); + } else { + return time(); + } + } +} diff --git a/framework/yii/bootstrap/Alert.php b/framework/yii/bootstrap/Alert.php new file mode 100644 index 0000000..d57bcbe --- /dev/null +++ b/framework/yii/bootstrap/Alert.php @@ -0,0 +1,153 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use Yii; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * Alert renders an alert bootstrap component. + * + * For example, + * + * ```php + * echo Alert::widget(array( + * 'body' => 'Say hello...', + * 'closeButton' => array( + * 'label' => '×', + * 'tag' => 'a', + * ), + * )); + * ``` + * + * The following example will show the content enclosed between the [[begin()]] + * and [[end()]] calls within the alert box: + * + * ```php + * Alert::begin(array( + * 'closeButton' => array( + * 'label' => '×', + * ), + * )); + * + * echo 'Say hello...'; + * + * Alert::end(); + * ``` + * + * @see http://twitter.github.io/bootstrap/javascript.html#alerts + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @since 2.0 + */ +class Alert extends Widget +{ + /** + * @var string the body content in the alert component. Note that anything between + * the [[begin()]] and [[end()]] calls of the Alert widget will also be treated + * as the body content, and will be rendered before this. + */ + public $body; + /** + * @var array the options for rendering the close button tag. + * The close button is displayed in the header of the modal window. Clicking + * on the button will hide the modal window. If this is null, no close button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * - label: string, the label of the button. Defaults to '×'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Alert plugin help](http://twitter.github.com/bootstrap/javascript.html#alerts) + * for the supported HTML attributes. + */ + public $closeButton = array(); + + + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + + $this->initOptions(); + + echo Html::beginTag('div', $this->options) . "\n"; + echo $this->renderBodyBegin() . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo "\n" . $this->renderBodyEnd(); + echo "\n" . Html::endTag('div'); + + $this->registerPlugin('alert'); + } + + /** + * Renders the close button if any before rendering the content. + * @return string the rendering result + */ + protected function renderBodyBegin() + { + return $this->renderCloseButton(); + } + + /** + * Renders the alert body (if any). + * @return string the rendering result + */ + protected function renderBodyEnd() + { + return $this->body . "\n"; + } + + /** + * Renders the close button. + * @return string the rendering result + */ + protected function renderCloseButton() + { + if ($this->closeButton !== null) { + $tag = ArrayHelper::remove($this->closeButton, 'tag', 'button'); + $label = ArrayHelper::remove($this->closeButton, 'label', '×'); + if ($tag === 'button' && !isset($this->closeButton['type'])) { + $this->closeButton['type'] = 'button'; + } + return Html::tag($tag, $label, $this->closeButton); + } else { + return null; + } + } + + /** + * Initializes the widget options. + * This method sets the default values for various options. + */ + protected function initOptions() + { + $this->options = array_merge(array( + 'class' => 'fade in', + ), $this->options); + + Html::addCssClass($this->options, 'alert'); + + if ($this->closeButton !== null) { + $this->closeButton = array_merge(array( + 'data-dismiss' => 'alert', + 'aria-hidden' => 'true', + 'class' => 'close', + ), $this->closeButton); + } + } +} diff --git a/framework/yii/bootstrap/BootstrapAsset.php b/framework/yii/bootstrap/BootstrapAsset.php new file mode 100644 index 0000000..26dad31 --- /dev/null +++ b/framework/yii/bootstrap/BootstrapAsset.php @@ -0,0 +1,22 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class BootstrapAsset extends AssetBundle +{ + public $sourcePath = '@yii/bootstrap/assets'; + public $css = array( + 'css/bootstrap.css', + ); +} diff --git a/framework/yii/bootstrap/BootstrapPluginAsset.php b/framework/yii/bootstrap/BootstrapPluginAsset.php new file mode 100644 index 0000000..613e120 --- /dev/null +++ b/framework/yii/bootstrap/BootstrapPluginAsset.php @@ -0,0 +1,27 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use yii\web\AssetBundle; + +/** + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class BootstrapPluginAsset extends AssetBundle +{ + public $sourcePath = '@yii/bootstrap/assets'; + public $js = array( + 'js/bootstrap.js', + ); + public $depends = array( + 'yii\web\JqueryAsset', + 'yii\bootstrap\BootstrapAsset', + ); +} diff --git a/framework/yii/bootstrap/BootstrapThemeAsset.php b/framework/yii/bootstrap/BootstrapThemeAsset.php new file mode 100644 index 0000000..d21f308 --- /dev/null +++ b/framework/yii/bootstrap/BootstrapThemeAsset.php @@ -0,0 +1,27 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use yii\web\AssetBundle; + +/** + * Bootstrap 2 theme for Bootstrap 3 + * + * @author Alexander Makarov <sam@rmcreative.ru> + * @since 2.0 + */ +class BootstrapThemeAsset extends AssetBundle +{ + public $sourcePath = '@yii/bootstrap/assets'; + public $css = array( + 'css/bootstrap-theme.css', + ); + public $depends = array( + 'yii\bootstrap\BootstrapAsset', + ); +} diff --git a/framework/yii/bootstrap/Button.php b/framework/yii/bootstrap/Button.php new file mode 100644 index 0000000..5c45747 --- /dev/null +++ b/framework/yii/bootstrap/Button.php @@ -0,0 +1,62 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use yii\helpers\Html; + +/** + * Button renders a bootstrap button. + * + * For example, + * + * ```php + * echo Button::widget(array( + * 'label' => 'Action', + * 'options' => array('class' => 'btn-lg'), + * )); + * ``` + * @see http://twitter.github.io/bootstrap/javascript.html#buttons + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @since 2.0 + */ +class Button extends Widget +{ + /** + * @var string the tag to use to render the button + */ + public $tagName = 'button'; + /** + * @var string the button label + */ + public $label = 'Button'; + /** + * @var boolean whether the label should be HTML-encoded. + */ + public $encodeLabel = true; + + + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + $this->clientOptions = false; + Html::addCssClass($this->options, 'btn'); + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::tag($this->tagName, $this->encodeLabel ? Html::encode($this->label) : $this->label, $this->options); + $this->registerPlugin('button'); + } +} diff --git a/framework/yii/bootstrap/ButtonDropdown.php b/framework/yii/bootstrap/ButtonDropdown.php new file mode 100644 index 0000000..2e04cb6 --- /dev/null +++ b/framework/yii/bootstrap/ButtonDropdown.php @@ -0,0 +1,115 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use yii\helpers\Html; + +/** + * ButtonDropdown renders a group or split button dropdown bootstrap component. + * + * For example, + * + * ```php + * // a button group using Dropdown widget + * echo ButtonDropdown::widget(array( + * 'label' => 'Action', + * 'dropdown' => array( + * 'items' => array( + * array( + * 'label' => 'DropdownA', + * 'url' => '/', + * ), + * array( + * 'label' => 'DropdownB', + * 'url' => '#', + * ), + * ), + * ), + * )); + * ``` + * @see http://twitter.github.io/bootstrap/javascript.html#buttons + * @see http://twitter.github.io/bootstrap/components.html#buttonDropdowns + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @since 2.0 + */ +class ButtonDropdown extends Widget +{ + /** + * @var string the button label + */ + public $label = 'Button'; + /** + * @var array the HTML attributes of the button. + */ + public $options = array(); + /** + * @var array the configuration array for [[Dropdown]]. + */ + public $dropdown = array(); + /** + * @var boolean whether to display a group of split-styled button group. + */ + public $split = false; + + + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderButton() . "\n" . $this->renderDropdown(); + $this->registerPlugin('button'); + } + + /** + * Generates the button dropdown. + * @return string the rendering result. + */ + protected function renderButton() + { + Html::addCssClass($this->options, 'btn'); + if ($this->split) { + $tag = 'button'; + $options = $this->options; + $this->options['data-toggle'] = 'dropdown'; + Html::addCssClass($this->options, 'dropdown-toggle'); + $splitButton = Button::widget(array( + 'label' => '<span class="caret"></span>', + 'encodeLabel' => false, + 'options' => $this->options, + )); + } else { + $tag = 'a'; + $this->label .= ' <span class="caret"></span>'; + $options = $this->options; + if (!isset($options['href'])) { + $options['href'] = '#'; + } + Html::addCssClass($options, 'dropdown-toggle'); + $options['data-toggle'] = 'dropdown'; + $splitButton = ''; + } + return Button::widget(array( + 'tagName' => $tag, + 'label' => $this->label, + 'options' => $options, + 'encodeLabel' => false, + )) . "\n" . $splitButton; + } + + /** + * Generates the dropdown menu. + * @return string the rendering result. + */ + protected function renderDropdown() + { + $config = $this->dropdown; + $config['clientOptions'] = false; + return Dropdown::widget($config); + } +} diff --git a/framework/yii/bootstrap/ButtonGroup.php b/framework/yii/bootstrap/ButtonGroup.php new file mode 100644 index 0000000..f2656c0 --- /dev/null +++ b/framework/yii/bootstrap/ButtonGroup.php @@ -0,0 +1,97 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * ButtonGroup renders a button group bootstrap component. + * + * For example, + * + * ```php + * // a button group with items configuration + * echo ButtonGroup::::widget(array( + * 'buttons' => array( + * array('label' => 'A'), + * array('label' => 'B'), + * ) + * )); + * + * // button group with an item as a string + * echo ButtonGroup::::widget(array( + * 'buttons' => array( + * Button::widget(array('label' => 'A')), + * array('label' => 'B'), + * ) + * )); + * ``` + * @see http://twitter.github.io/bootstrap/javascript.html#buttons + * @see http://twitter.github.io/bootstrap/components.html#buttonGroups + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @since 2.0 + */ +class ButtonGroup extends Widget +{ + /** + * @var array list of buttons. Each array element represents a single button + * which can be specified as a string or an array of the following structure: + * + * - label: string, required, the button label. + * - options: array, optional, the HTML attributes of the button. + */ + public $buttons = array(); + /** + * @var boolean whether to HTML-encode the button labels. + */ + public $encodeLabels = true; + + + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, 'btn-group'); + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::tag('div', $this->renderButtons(), $this->options); + BootstrapAsset::register($this->getView()); + } + + /** + * Generates the buttons that compound the group as specified on [[items]]. + * @return string the rendering result. + */ + protected function renderButtons() + { + $buttons = array(); + foreach ($this->buttons as $button) { + if (is_array($button)) { + $label = ArrayHelper::getValue($button, 'label'); + $options = ArrayHelper::getValue($button, 'options'); + $buttons[] = Button::widget(array( + 'label' => $label, + 'options' => $options, + 'encodeLabel' => $this->encodeLabels + )); + } else { + $buttons[] = $button; + } + } + return implode("\n", $buttons); + } +} diff --git a/framework/yii/bootstrap/Carousel.php b/framework/yii/bootstrap/Carousel.php new file mode 100644 index 0000000..ec9a0c9 --- /dev/null +++ b/framework/yii/bootstrap/Carousel.php @@ -0,0 +1,172 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use yii\base\InvalidConfigException; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * Carousel renders a carousel bootstrap javascript component. + * + * For example: + * + * ```php + * echo Carousel::widget(array( + * 'items' => array( + * // the item contains only the image + * '<img src="http://twitter.github.io/bootstrap/assets/img/bootstrap-mdo-sfmoma-01.jpg"/>', + * // equivalent to the above + * array( + * 'content' => '<img src="http://twitter.github.io/bootstrap/assets/img/bootstrap-mdo-sfmoma-02.jpg"/>', + * ), + * // the item contains both the image and the caption + * array( + * 'content' => '<img src="http://twitter.github.io/bootstrap/assets/img/bootstrap-mdo-sfmoma-03.jpg"/>', + * 'caption' => '<h4>This is title</h4><p>This is the caption text</p>', + * 'options' => array(...), + * ), + * ) + * )); + * ``` + * + * @see http://twitter.github.io/bootstrap/javascript.html#carousel + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @since 2.0 + */ +class Carousel extends Widget +{ + /** + * @var array|boolean the labels for the previous and the next control buttons. + * If false, it means the previous and the next control buttons should not be displayed. + */ + public $controls = array('‹', '›'); + /** + * @var array list of slides in the carousel. Each array element represents a single + * slide with the following structure: + * + * ```php + * array( + * // required, slide content (HTML), such as an image tag + * 'content' => '<img src="http://twitter.github.io/bootstrap/assets/img/bootstrap-mdo-sfmoma-01.jpg"/>', + * // optional, the caption (HTML) of the slide + * 'caption' => '<h4>This is title</h4><p>This is the caption text</p>', + * // optional the HTML attributes of the slide container + * 'options' => array(), + * ) + * ``` + */ + public $items = array(); + + + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, 'carousel'); + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::beginTag('div', $this->options) . "\n"; + echo $this->renderIndicators() . "\n"; + echo $this->renderItems() . "\n"; + echo $this->renderControls() . "\n"; + echo Html::endTag('div') . "\n"; + $this->registerPlugin('carousel'); + } + + /** + * Renders carousel indicators. + * @return string the rendering result + */ + public function renderIndicators() + { + $indicators = array(); + for ($i = 0, $count = count($this->items); $i < $count; $i++) { + $options = array('data-target' => '#' . $this->options['id'], 'data-slide-to' => $i); + if ($i === 0) { + Html::addCssClass($options, 'active'); + } + $indicators[] = Html::tag('li', '', $options); + } + return Html::tag('ol', implode("\n", $indicators), array('class' => 'carousel-indicators')); + } + + /** + * Renders carousel items as specified on [[items]]. + * @return string the rendering result + */ + public function renderItems() + { + $items = array(); + for ($i = 0, $count = count($this->items); $i < $count; $i++) { + $items[] = $this->renderItem($this->items[$i], $i); + } + return Html::tag('div', implode("\n", $items), array('class' => 'carousel-inner')); + } + + /** + * Renders a single carousel item + * @param string|array $item a single item from [[items]] + * @param integer $index the item index as the first item should be set to `active` + * @return string the rendering result + * @throws InvalidConfigException if the item is invalid + */ + public function renderItem($item, $index) + { + if (is_string($item)) { + $content = $item; + $caption = null; + $options = array(); + } elseif (isset($item['content'])) { + $content = $item['content']; + $caption = ArrayHelper::getValue($item, 'caption'); + if ($caption !== null) { + $caption = Html::tag('div', $caption, array('class' => 'carousel-caption')); + } + $options = ArrayHelper::getValue($item, 'options', array()); + } else { + throw new InvalidConfigException('The "content" option is required.'); + } + + Html::addCssClass($options, 'item'); + if ($index === 0) { + Html::addCssClass($options, 'active'); + } + + return Html::tag('div', $content . "\n" . $caption, $options); + } + + /** + * Renders previous and next control buttons. + * @throws InvalidConfigException if [[controls]] is invalid. + */ + public function renderControls() + { + if (isset($this->controls[0], $this->controls[1])) { + return Html::a($this->controls[0], '#' . $this->options['id'], array( + 'class' => 'left carousel-control', + 'data-slide' => 'prev', + )) . "\n" + . Html::a($this->controls[1], '#' . $this->options['id'], array( + 'class' => 'right carousel-control', + 'data-slide' => 'next', + )); + } elseif ($this->controls === false) { + return ''; + } else { + throw new InvalidConfigException('The "controls" property must be either false or an array of two elements.'); + } + } +} diff --git a/framework/yii/bootstrap/Collapse.php b/framework/yii/bootstrap/Collapse.php new file mode 100644 index 0000000..77fdde7 --- /dev/null +++ b/framework/yii/bootstrap/Collapse.php @@ -0,0 +1,133 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use yii\base\InvalidConfigException; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * Collapse renders an accordion bootstrap javascript component. + * + * For example: + * + * ```php + * echo Collapse::widget(array( + * 'items' => array( + * // equivalent to the above + * 'Collapsible Group Item #1' => array( + * 'content' => 'Anim pariatur cliche...', + * // open its content by default + * 'contentOptions' => array('class' => 'in') + * ), + * // another group item + * 'Collapsible Group Item #2' => array( + * 'content' => 'Anim pariatur cliche...', + * 'contentOptions' => array(...), + * 'options' => array(...), + * ), + * ) + * )); + * ``` + * + * @see http://twitter.github.io/bootstrap/javascript.html#collapse + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @since 2.0 + */ +class Collapse extends Widget +{ + /** + * @var array list of groups in the collapse widget. Each array element represents a single + * group with the following structure: + * + * ```php + * // item key is the actual group header + * 'Collapsible Group Item #1' => array( + * // required, the content (HTML) of the group + * 'content' => 'Anim pariatur cliche...', + * // optional the HTML attributes of the content group + * 'contentOptions' => array(), + * // optional the HTML attributes of the group + * 'options' => array(), + * ) + * ``` + */ + public $items = array(); + + + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, 'accordion'); + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::beginTag('div', $this->options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag('div') . "\n"; + $this->registerPlugin('collapse'); + } + + /** + * Renders collapsible items as specified on [[items]]. + * @return string the rendering result + */ + public function renderItems() + { + $items = array(); + $index = 0; + foreach ($this->items as $header => $item) { + $options = ArrayHelper::getValue($item, 'options', array()); + Html::addCssClass($options, 'accordion-group'); + $items[] = Html::tag('div', $this->renderItem($header, $item, ++$index), $options); + } + + return implode("\n", $items); + } + + /** + * Renders a single collapsible item group + * @param string $header a label of the item group [[items]] + * @param array $item a single item from [[items]] + * @param integer $index the item index as each item group content must have an id + * @return string the rendering result + * @throws InvalidConfigException + */ + public function renderItem($header, $item, $index) + { + if (isset($item['content'])) { + $id = $this->options['id'] . '-collapse' . $index; + $options = ArrayHelper::getValue($item, 'contentOptions', array()); + $options['id'] = $id; + Html::addCssClass($options, 'accordion-body collapse'); + + $header = Html::a($header, '#' . $id, array( + 'class' => 'accordion-toggle', + 'data-toggle' => 'collapse', + 'data-parent' => '#' . $this->options['id'] + )) . "\n"; + + $content = Html::tag('div', $item['content'], array('class' => 'accordion-inner')) . "\n"; + } else { + throw new InvalidConfigException('The "content" option is required.'); + } + $group = array(); + + $group[] = Html::tag('div', $header, array('class' => 'accordion-heading')); + $group[] = Html::tag('div', $content, $options); + + return implode("\n", $group); + } +} diff --git a/framework/yii/bootstrap/Dropdown.php b/framework/yii/bootstrap/Dropdown.php new file mode 100644 index 0000000..1cc389e --- /dev/null +++ b/framework/yii/bootstrap/Dropdown.php @@ -0,0 +1,90 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use yii\base\InvalidConfigException; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * Dropdown renders a Bootstrap dropdown menu component. + * + * @see http://twitter.github.io/bootstrap/javascript.html#dropdowns + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @since 2.0 + */ +class Dropdown extends Widget +{ + /** + * @var array list of menu items in the dropdown. Each array element can be either an HTML string, + * or an array representing a single menu with the following structure: + * + * - label: string, required, the label of the item link + * - url: string, optional, the url of the item link. Defaults to "#". + * - visible: boolean, optional, whether this menu item is visible. Defaults to true. + * - linkOptions: array, optional, the HTML attributes of the item link. + * - options: array, optional, the HTML attributes of the item. + */ + public $items = array(); + /** + * @var boolean whether the labels for header items should be HTML-encoded. + */ + public $encodeLabels = true; + + + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, 'dropdown-menu'); + } + + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderItems($this->items); + $this->registerPlugin('dropdown'); + } + + /** + * Renders menu items. + * @param array $items the menu items to be rendered + * @return string the rendering result. + * @throws InvalidConfigException if the label option is not specified in one of the items. + */ + protected function renderItems($items) + { + $lines = array(); + foreach ($items as $i => $item) { + if (isset($item['visible']) && !$item['visible']) { + unset($items[$i]); + continue; + } + if (is_string($item)) { + $lines[] = $item; + continue; + } + if (!isset($item['label'])) { + throw new InvalidConfigException("The 'label' option is required."); + } + $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; + $options = ArrayHelper::getValue($item, 'options', array()); + $linkOptions = ArrayHelper::getValue($item, 'linkOptions', array()); + $linkOptions['tabindex'] = '-1'; + $content = Html::a($label, ArrayHelper::getValue($item, 'url', '#'), $linkOptions); + $lines[] = Html::tag('li', $content, $options); + } + + return Html::tag('ul', implode("\n", $lines), $this->options); + } +} diff --git a/framework/yii/bootstrap/Modal.php b/framework/yii/bootstrap/Modal.php new file mode 100644 index 0000000..e1f042f --- /dev/null +++ b/framework/yii/bootstrap/Modal.php @@ -0,0 +1,231 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use Yii; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * Modal renders a modal window that can be toggled by clicking on a button. + * + * The following example will show the content enclosed between the [[begin()]] + * and [[end()]] calls within the modal window: + * + * ~~~php + * Modal::begin(array( + * 'header' => '<h2>Hello world</h2>', + * 'toggleButton' => array( + * 'label' => 'click me', + * ), + * )); + * + * echo 'Say hello...'; + * + * Modal::end(); + * ~~~ + * + * @see http://twitter.github.io/bootstrap/javascript.html#modals + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Modal extends Widget +{ + /** + * @var string the header content in the modal window. + */ + public $header; + /** + * @var string the footer content in the modal window. + */ + public $footer; + /** + * @var array the options for rendering the close button tag. + * The close button is displayed in the header of the modal window. Clicking + * on the button will hide the modal window. If this is null, no close button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * - label: string, the label of the button. Defaults to '×'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Modal plugin help](http://twitter.github.com/bootstrap/javascript.html#modals) + * for the supported HTML attributes. + */ + public $closeButton = array(); + /** + * @var array the options for rendering the toggle button tag. + * The toggle button is used to toggle the visibility of the modal window. + * If this property is null, no toggle button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * - label: string, the label of the button. Defaults to 'Show'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Modal plugin help](http://twitter.github.com/bootstrap/javascript.html#modals) + * for the supported HTML attributes. + */ + public $toggleButton; + + + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + + $this->initOptions(); + + echo $this->renderToggleButton() . "\n"; + echo Html::beginTag('div', $this->options) . "\n"; + echo Html::beginTag('div', array('class' => 'modal-dialog')) . "\n"; + echo Html::beginTag('div', array('class' => 'modal-content')) . "\n"; + echo $this->renderHeader() . "\n"; + echo $this->renderBodyBegin() . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo "\n" . $this->renderBodyEnd(); + echo "\n" . $this->renderFooter(); + echo "\n" . Html::endTag('div'); // modal-content + echo "\n" . Html::endTag('div'); // modal-dialog + echo "\n" . Html::endTag('div'); + + $this->registerPlugin('modal'); + } + + /** + * Renders the header HTML markup of the modal + * @return string the rendering result + */ + protected function renderHeader() + { + $button = $this->renderCloseButton(); + if ($button !== null) { + $this->header = $button . "\n" . $this->header; + } + if ($this->header !== null) { + return Html::tag('div', "\n" . $this->header . "\n", array('class' => 'modal-header')); + } else { + return null; + } + } + + /** + * Renders the opening tag of the modal body. + * @return string the rendering result + */ + protected function renderBodyBegin() + { + return Html::beginTag('div', array('class' => 'modal-body')); + } + + /** + * Renders the closing tag of the modal body. + * @return string the rendering result + */ + protected function renderBodyEnd() + { + return Html::endTag('div'); + } + + /** + * Renders the HTML markup for the footer of the modal + * @return string the rendering result + */ + protected function renderFooter() + { + if ($this->footer !== null) { + return Html::tag('div', "\n" . $this->footer . "\n", array('class' => 'modal-footer')); + } else { + return null; + } + } + + /** + * Renders the toggle button. + * @return string the rendering result + */ + protected function renderToggleButton() + { + if ($this->toggleButton !== null) { + $tag = ArrayHelper::remove($this->toggleButton, 'tag', 'button'); + $label = ArrayHelper::remove($this->toggleButton, 'label', 'Show'); + if ($tag === 'button' && !isset($this->toggleButton['type'])) { + $this->toggleButton['type'] = 'button'; + } + return Html::tag($tag, $label, $this->toggleButton); + } else { + return null; + } + } + + /** + * Renders the close button. + * @return string the rendering result + */ + protected function renderCloseButton() + { + if ($this->closeButton !== null) { + $tag = ArrayHelper::remove($this->closeButton, 'tag', 'button'); + $label = ArrayHelper::remove($this->closeButton, 'label', '×'); + if ($tag === 'button' && !isset($this->closeButton['type'])) { + $this->closeButton['type'] = 'button'; + } + return Html::tag($tag, $label, $this->closeButton); + } else { + return null; + } + } + + /** + * Initializes the widget options. + * This method sets the default values for various options. + */ + protected function initOptions() + { + $this->options = array_merge(array( + 'class' => 'fade', + 'role' => 'dialog', + 'tabindex' => -1, + ), $this->options); + Html::addCssClass($this->options, 'modal'); + + if ($this->clientOptions !== false) { + $this->clientOptions = array_merge(array( + 'show' => false, + ), $this->clientOptions); + } + + if ($this->closeButton !== null) { + $this->closeButton = array_merge(array( + 'data-dismiss' => 'modal', + 'aria-hidden' => 'true', + 'class' => 'close', + ), $this->closeButton); + } + + if ($this->toggleButton !== null) { + $this->toggleButton = array_merge(array( + 'data-toggle' => 'modal', + ), $this->toggleButton); + if (!isset($this->toggleButton['data-target']) && !isset($this->toggleButton['href'])) { + $this->toggleButton['data-target'] = '#' . $this->options['id']; + } + } + } +} diff --git a/framework/yii/bootstrap/Nav.php b/framework/yii/bootstrap/Nav.php new file mode 100644 index 0000000..f24f729 --- /dev/null +++ b/framework/yii/bootstrap/Nav.php @@ -0,0 +1,220 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use Yii; +use yii\base\InvalidConfigException; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * Nav renders a nav HTML component. + * + * For example: + * + * ```php + * echo Nav::widget(array( + * 'items' => array( + * array( + * 'label' => 'Home', + * 'url' => array('site/index'), + * 'linkOptions' => array(...), + * ), + * array( + * 'label' => 'Dropdown', + * 'items' => array( + * array( + * 'label' => 'Level 1 -DropdownA', + * 'url' => '#', + * 'items' => array( + * array( + * 'label' => 'Level 2 -DropdownA', + * 'url' => '#', + * ), + * ), + * ), + * array( + * 'label' => 'Level 1 -DropdownB', + * 'url' => '#', + * ), + * ), + * ), + * ), + * )); + * ``` + * + * @see http://twitter.github.io/bootstrap/components.html#nav + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @since 2.0 + */ +class Nav extends Widget +{ + /** + * @var array list of items in the nav widget. Each array element represents a single + * menu item which can be either a string or an array with the following structure: + * + * - label: string, required, the nav item label. + * - url: optional, the item's URL. Defaults to "#". + * - visible: boolean, optional, whether this menu item is visible. Defaults to true. + * - linkOptions: array, optional, the HTML attributes of the item's link. + * - options: array, optional, the HTML attributes of the item container (LI). + * - active: boolean, optional, whether the item should be on active state or not. + * - items: array|string, optional, the configuration array for creating a [[Dropdown]] widget, + * or a string representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus. + * + * If a menu item is a string, it will be rendered directly without HTML encoding. + */ + public $items = array(); + /** + * @var boolean whether the nav items labels should be HTML-encoded. + */ + public $encodeLabels = true; + /** + * @var boolean whether to automatically activate items according to whether their route setting + * matches the currently requested route. + * @see isItemActive + */ + public $activateItems = true; + /** + * @var string the route used to determine if a menu item is active or not. + * If not set, it will use the route of the current request. + * @see params + * @see isItemActive + */ + public $route; + /** + * @var array the parameters used to determine if a menu item is active or not. + * If not set, it will use `$_GET`. + * @see route + * @see isItemActive + */ + public $params; + + + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + if ($this->route === null && Yii::$app->controller !== null) { + $this->route = Yii::$app->controller->getRoute(); + } + if ($this->params === null) { + $this->params = $_GET; + } + Html::addCssClass($this->options, 'nav'); + } + + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderItems(); + BootstrapAsset::register($this->getView()); + } + + /** + * Renders widget items. + */ + public function renderItems() + { + $items = array(); + foreach ($this->items as $i => $item) { + if (isset($item['visible']) && !$item['visible']) { + unset($items[$i]); + continue; + } + $items[] = $this->renderItem($item); + } + + return Html::tag('ul', implode("\n", $items), $this->options); + } + + /** + * Renders a widget's item. + * @param string|array $item the item to render. + * @return string the rendering result. + * @throws InvalidConfigException + */ + public function renderItem($item) + { + if (is_string($item)) { + return $item; + } + if (!isset($item['label'])) { + throw new InvalidConfigException("The 'label' option is required."); + } + $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; + $options = ArrayHelper::getValue($item, 'options', array()); + $items = ArrayHelper::getValue($item, 'items'); + $url = Html::url(ArrayHelper::getValue($item, 'url', '#')); + $linkOptions = ArrayHelper::getValue($item, 'linkOptions', array()); + + if (isset($item['active'])) { + $active = ArrayHelper::remove($item, 'active', false); + } else { + $active = $this->isItemActive($item); + } + + if ($active) { + Html::addCssClass($options, 'active'); + } + + if ($items !== null) { + $linkOptions['data-toggle'] = 'dropdown'; + Html::addCssClass($options, 'dropdown'); + Html::addCssClass($urlOptions, 'dropdown-toggle'); + $label .= ' ' . Html::tag('b', '', array('class' => 'caret')); + if (is_array($items)) { + $items = Dropdown::widget(array( + 'items' => $items, + 'encodeLabels' => $this->encodeLabels, + 'clientOptions' => false, + )); + } + } + + return Html::tag('li', Html::a($label, $url, $linkOptions) . $items, $options); + } + + + /** + * Checks whether a menu item is active. + * This is done by checking if [[route]] and [[params]] match that specified in the `url` option of the menu item. + * When the `url` option of a menu item is specified in terms of an array, its first element is treated + * as the route for the item and the rest of the elements are the associated parameters. + * Only when its route and parameters match [[route]] and [[params]], respectively, will a menu item + * be considered active. + * @param array $item the menu item to be checked + * @return boolean whether the menu item is active + */ + protected function isItemActive($item) + { + if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) { + $route = $item['url'][0]; + if ($route[0] !== '/' && Yii::$app->controller) { + $route = Yii::$app->controller->module->getUniqueId() . '/' . $route; + } + if (ltrim($route, '/') !== $this->route) { + return false; + } + unset($item['url']['#']); + if (count($item['url']) > 1) { + foreach (array_splice($item['url'], 1) as $name => $value) { + if (!isset($this->params[$name]) || $this->params[$name] != $value) { + return false; + } + } + } + return true; + } + return false; + } +} diff --git a/framework/yii/bootstrap/NavBar.php b/framework/yii/bootstrap/NavBar.php new file mode 100644 index 0000000..7aafbbb --- /dev/null +++ b/framework/yii/bootstrap/NavBar.php @@ -0,0 +1,108 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use yii\helpers\Html; + +/** + * NavBar renders a navbar HTML component. + * + * Any content enclosed between the [[begin()]] and [[end()]] calls of NavBar + * is treated as the content of the navbar. You may use widgets such as [[Nav]] + * or [[\yii\widgets\Menu]] to build up such content. For example, + * + * ```php + * use yii\bootstrap\NavBar; + * use yii\widgets\Menu; + * + * NavBar::begin(array('brandLabel' => 'NavBar Test')); + * echo Nav::widget(array( + * 'items' => array( + * array('label' => 'Home', 'url' => array('/site/index')), + * array('label' => 'About', 'url' => array('/site/about')), + * ), + * )); + * NavBar::end(); + * ``` + * + * @see http://twitter.github.io/bootstrap/components.html#navbar + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @since 2.0 + */ +class NavBar extends Widget +{ + /** + * @var string the text of the brand. Note that this is not HTML-encoded. + * @see http://twitter.github.io/bootstrap/components.html#navbar + */ + public $brandLabel; + /** + * @param array|string $url the URL for the brand's hyperlink tag. This parameter will be processed by [[Html::url()]] + * and will be used for the "href" attribute of the brand link. Defaults to site root. + */ + public $brandUrl = '/'; + /** + * @var array the HTML attributes of the brand link. + */ + public $brandOptions = array(); + + public $screenReaderToggleText = 'Toggle navigation'; + + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + $this->clientOptions = false; + Html::addCssClass($this->options, 'navbar navbar-default'); + Html::addCssClass($this->brandOptions, 'navbar-brand'); + if (empty($this->options['role'])) { + $this->options['role'] = 'navigation'; + } + + echo Html::beginTag('nav', $this->options); + echo Html::beginTag('div', array('class' => 'container')); + + echo Html::beginTag('div', array('class' => 'navbar-header')); + echo $this->renderToggleButton(); + if ($this->brandLabel !== null) { + echo Html::a($this->brandLabel, $this->brandUrl, $this->brandOptions); + } + echo Html::endTag('div'); + + echo Html::beginTag('div', array('class' => 'collapse navbar-collapse navbar-ex1-collapse')); + } + + /** + * Renders the widget. + */ + public function run() + { + + echo Html::endTag('div'); + echo Html::endTag('div'); + echo Html::endTag('nav'); + BootstrapPluginAsset::register($this->getView()); + } + + /** + * Renders collapsible toggle button. + * @return string the rendering toggle button. + */ + protected function renderToggleButton() + { + $bar = Html::tag('span', '', array('class' => 'icon-bar')); + $screenReader = '<span class="sr-only">'.$this->screenReaderToggleText.'</span>'; + return Html::button("{$screenReader}\n{$bar}\n{$bar}\n{$bar}", array( + 'class' => 'navbar-toggle', + 'data-toggle' => 'collapse', + 'data-target' => '.navbar-ex1-collapse', + )); + } +} diff --git a/framework/yii/bootstrap/Progress.php b/framework/yii/bootstrap/Progress.php new file mode 100644 index 0000000..d8e5a69 --- /dev/null +++ b/framework/yii/bootstrap/Progress.php @@ -0,0 +1,146 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use yii\base\InvalidConfigException; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * Progress renders a bootstrap progress bar component. + * + * For example, + * + * ```php + * // default with label + * echo Progress::widget(array( + * 'percent' => 60, + * 'label' => 'test', + * )); + * + * // styled + * echo Progress::widget(array( + * 'percent' => 65, + * 'barOptions' => array('class' => 'bar-danger') + * )); + * + * // striped + * echo Progress::widget(array( + * 'percent' => 70, + * 'barOptions' => array('class' => 'bar-warning'), + * 'options' => array('class' => 'progress-striped') + * )); + * + * // striped animated + * echo Progress::widget(array( + * 'percent' => 70, + * 'barOptions' => array('class' => 'bar-success'), + * 'options' => array('class' => 'active progress-striped') + * )); + * + * // stacked bars + * echo Progress::widget(array( + * 'bars' => array( + * array('percent' => 30, 'options' => array('class' => 'bar-danger')), + * array('percent' => 30, 'label' => 'test', 'options' => array('class' => 'bar-success')), + * array('percent' => 35, 'options' => array('class' => 'bar-warning')) + * ) + * )); + * ``` + * @see http://twitter.github.io/bootstrap/components.html#progress + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @since 2.0 + */ +class Progress extends Widget +{ + /** + * @var string the button label + */ + public $label; + /** + * @var integer the amount of progress as a percentage. + */ + public $percent = 0; + /** + * @var array the HTML attributes of the + */ + public $barOptions = array(); + /** + * @var array a set of bars that are stacked together to form a single progress bar. + * Each bar is an array of the following structure: + * + * ```php + * array( + * // required, the amount of progress as a percentage. + * 'percent' => 30, + * // optional, the label to be displayed on the bar + * 'label' => '30%', + * // optional, array, additional HTML attributes for the bar tag + * 'options' => array(), + * ) + */ + public $bars; + + + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, 'progress'); + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::beginTag('div', $this->options) . "\n"; + echo $this->renderProgress() . "\n"; + echo Html::endTag('div') . "\n"; + BootstrapAsset::register($this->getView()); + } + + /** + * Renders the progress. + * @return string the rendering result. + * @throws InvalidConfigException if the "percent" option is not set in a stacked progress bar. + */ + protected function renderProgress() + { + if (empty($this->bars)) { + return $this->renderBar($this->percent, $this->label, $this->barOptions); + } + $bars = array(); + foreach ($this->bars as $bar) { + $label = ArrayHelper::getValue($bar, 'label', ''); + if (!isset($bar['percent'])) { + throw new InvalidConfigException("The 'percent' option is required."); + } + $options = ArrayHelper::getValue($bar, 'options', array()); + $bars[] = $this->renderBar($bar['percent'], $label, $options); + } + return implode("\n", $bars); + } + + /** + * Generates a bar + * @param int $percent the percentage of the bar + * @param string $label, optional, the label to display at the bar + * @param array $options the HTML attributes of the bar + * @return string the rendering result. + */ + protected function renderBar($percent, $label = '', $options = array()) + { + Html::addCssClass($options, 'bar'); + $options['style'] = "width:{$percent}%"; + return Html::tag('div', $label, $options); + } +} diff --git a/framework/yii/bootstrap/Tabs.php b/framework/yii/bootstrap/Tabs.php new file mode 100644 index 0000000..d6ddd7b --- /dev/null +++ b/framework/yii/bootstrap/Tabs.php @@ -0,0 +1,195 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use yii\base\InvalidConfigException; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * Tabs renders a Tab bootstrap javascript component. + * + * For example: + * + * ```php + * echo Tabs::widget(array( + * 'items' => array( + * array( + * 'label' => 'One', + * 'content' => 'Anim pariatur cliche...', + * 'active' => true + * ), + * array( + * 'label' => 'Two', + * 'content' => 'Anim pariatur cliche...', + * 'headerOptions' => array(...), + * 'options' => array('id' => 'myveryownID'), + * ), + * array( + * 'label' => 'Dropdown', + * 'items' => array( + * array( + * 'label' => 'DropdownA', + * 'content' => 'DropdownA, Anim pariatur cliche...', + * ), + * array( + * 'label' => 'DropdownB', + * 'content' => 'DropdownB, Anim pariatur cliche...', + * ), + * ), + * ), + * ), + * )); + * ``` + * + * @see http://twitter.github.io/bootstrap/javascript.html#tabs + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @since 2.0 + */ +class Tabs extends Widget +{ + /** + * @var array list of tabs in the tabs widget. Each array element represents a single + * tab with the following structure: + * + * - label: string, required, the tab header label. + * - headerOptions: array, optional, the HTML attributes of the tab header. + * - content: array, required if `items` is not set. The content (HTML) of the tab pane. + * - options: array, optional, the HTML attributes of the tab pane container. + * - active: boolean, optional, whether the item tab header and pane should be visible or not. + * - items: array, optional, if not set then `content` will be required. The `items` specify a dropdown items + * configuration array. Each item can hold two extra keys, besides the above ones: + * * active: boolean, optional, whether the item tab header and pane should be visible or not. + * * content: string, required if `items` is not set. The content (HTML) of the tab pane. + * * contentOptions: optional, array, the HTML attributes of the tab content container. + */ + public $items = array(); + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the item container tags. + */ + public $itemOptions = array(); + /** + * @var array list of HTML attributes for the header container tags. This will be overwritten + * by the "headerOptions" set in individual [[items]]. + */ + public $headerOptions = array(); + /** + * @var boolean whether the labels for header items should be HTML-encoded. + */ + public $encodeLabels = true; + + + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, 'nav nav-tabs'); + } + + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderItems(); + $this->registerPlugin('tab'); + } + + /** + * Renders tab items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + protected function renderItems() + { + $headers = array(); + $panes = array(); + foreach ($this->items as $n => $item) { + if (!isset($item['label'])) { + throw new InvalidConfigException("The 'label' option is required."); + } + $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; + $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', array())); + + if (isset($item['items'])) { + $label .= ' <b class="caret"></b>'; + Html::addCssClass($headerOptions, 'dropdown'); + + if ($this->renderDropdown($item['items'], $panes)) { + Html::addCssClass($headerOptions, 'active'); + } + + $header = Html::a($label, "#", array('class' => 'dropdown-toggle', 'data-toggle' => 'dropdown')) . "\n" + . Dropdown::widget(array('items' => $item['items'], 'clientOptions' => false)); + } elseif (isset($item['content'])) { + $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', array())); + $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . '-tab' . $n); + + Html::addCssClass($options, 'tab-pane'); + if (ArrayHelper::remove($item, 'active')) { + Html::addCssClass($options, 'active'); + Html::addCssClass($headerOptions, 'active'); + } + $header = Html::a($label, '#' . $options['id'], array('data-toggle' => 'tab')); + $panes[] = Html::tag('div', $item['content'], $options); + } else { + throw new InvalidConfigException("Either the 'content' or 'items' option must be set."); + } + + $headers[] = Html::tag('li', $header, $headerOptions); + } + + return Html::tag('ul', implode("\n", $headers), $this->options) . "\n" + . Html::tag('div', implode("\n", $panes), array('class' => 'tab-content')); + } + + /** + * Normalizes dropdown item options by removing tab specific keys `content` and `contentOptions`, and also + * configure `panes` accordingly. + * @param array $items the dropdown items configuration. + * @param array $panes the panes reference array. + * @return boolean whether any of the dropdown items is `active` or not. + * @throws InvalidConfigException + */ + protected function renderDropdown(&$items, &$panes) + { + $itemActive = false; + + foreach ($items as $n => &$item) { + if (is_string($item)) { + continue; + } + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' option is required."); + } + + $content = ArrayHelper::remove($item, 'content'); + $options = ArrayHelper::remove($item, 'contentOptions', array()); + Html::addCssClass($options, 'tab-pane'); + if (ArrayHelper::remove($item, 'active')) { + Html::addCssClass($options, 'active'); + Html::addCssClass($item['options'], 'active'); + $itemActive = true; + } + + $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . '-dd-tab' . $n); + $item['url'] = '#' . $options['id']; + $item['linkOptions']['data-toggle'] = 'tab'; + + $panes[] = Html::tag('div', $content, $options); + + unset($item); + } + return $itemActive; + } +} diff --git a/framework/yii/bootstrap/Widget.php b/framework/yii/bootstrap/Widget.php new file mode 100644 index 0000000..2959f09 --- /dev/null +++ b/framework/yii/bootstrap/Widget.php @@ -0,0 +1,82 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\bootstrap; + +use Yii; +use yii\base\View; +use yii\helpers\Json; + +/** + * \yii\bootstrap\Widget is the base class for all bootstrap widgets. + * + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Widget extends \yii\base\Widget +{ + /** + * @var array the HTML attributes for the widget container tag. + */ + public $options = array(); + /** + * @var array the options for the underlying Bootstrap JS plugin. + * Please refer to the corresponding Bootstrap plugin Web page for possible options. + * For example, [this page](http://twitter.github.io/bootstrap/javascript.html#modals) shows + * how to use the "Modal" plugin and the supported options (e.g. "remote"). + */ + public $clientOptions = array(); + /** + * @var array the event handlers for the underlying Bootstrap JS plugin. + * Please refer to the corresponding Bootstrap plugin Web page for possible events. + * For example, [this page](http://twitter.github.io/bootstrap/javascript.html#modals) shows + * how to use the "Modal" plugin and the supported events (e.g. "shown"). + */ + public $clientEvents = array(); + + + /** + * Initializes the widget. + * This method will register the bootstrap asset bundle. If you override this method, + * make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + } + + /** + * Registers a specific Bootstrap plugin and the related events + * @param string $name the name of the Bootstrap plugin + */ + protected function registerPlugin($name) + { + $view = $this->getView(); + + BootstrapPluginAsset::register($view); + + $id = $this->options['id']; + + if ($this->clientOptions !== false) { + $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); + $js = "jQuery('#$id').$name($options);"; + $view->registerJs($js); + } + + if (!empty($this->clientEvents)) { + $js = array(); + foreach ($this->clientEvents as $event => $handler) { + $js[] = "jQuery('#$id').on('$event', $handler);"; + } + $view->registerJs(implode("\n", $js)); + } + } +} diff --git a/framework/yii/bootstrap/assets/css/bootstrap-theme.css b/framework/yii/bootstrap/assets/css/bootstrap-theme.css new file mode 100644 index 0000000..ad11735 --- /dev/null +++ b/framework/yii/bootstrap/assets/css/bootstrap-theme.css @@ -0,0 +1,384 @@ +.btn-default, +.btn-primary, +.btn-success, +.btn-info, +.btn-warning, +.btn-danger { + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.btn-default:active, +.btn-primary:active, +.btn-success:active, +.btn-info:active, +.btn-warning:active, +.btn-danger:active, +.btn-default.active, +.btn-primary.active, +.btn-success.active, +.btn-info.active, +.btn-warning.active, +.btn-danger.active { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +.btn:active, +.btn.active { + background-image: none; +} + +.btn-default { + text-shadow: 0 1px 0 #fff; + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, 0%, #e6e6e6, 100%); + background-image: -moz-linear-gradient(top, #ffffff 0%, #e6e6e6 100%); + background-image: linear-gradient(to bottom, #ffffff 0%, #e6e6e6 100%); + background-repeat: repeat-x; + border-color: #e0e0e0; + border-color: #ccc; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); +} + +.btn-default:active, +.btn-default.active { + background-color: #e6e6e6; + border-color: #e0e0e0; +} + +.btn-primary { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3071a9)); + background-image: -webkit-linear-gradient(top, #428bca, 0%, #3071a9, 100%); + background-image: -moz-linear-gradient(top, #428bca 0%, #3071a9 100%); + background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); + background-repeat: repeat-x; + border-color: #2d6ca2; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); +} + +.btn-primary:active, +.btn-primary.active { + background-color: #3071a9; + border-color: #2d6ca2; +} + +.btn-success { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5cb85c), to(#449d44)); + background-image: -webkit-linear-gradient(top, #5cb85c, 0%, #449d44, 100%); + background-image: -moz-linear-gradient(top, #5cb85c 0%, #449d44 100%); + background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); + background-repeat: repeat-x; + border-color: #419641; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); +} + +.btn-success:active, +.btn-success.active { + background-color: #449d44; + border-color: #419641; +} + +.btn-warning { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f0ad4e), to(#ec971f)); + background-image: -webkit-linear-gradient(top, #f0ad4e, 0%, #ec971f, 100%); + background-image: -moz-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); + background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); + background-repeat: repeat-x; + border-color: #eb9316; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); +} + +.btn-warning:active, +.btn-warning.active { + background-color: #ec971f; + border-color: #eb9316; +} + +.btn-danger { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9534f), to(#c9302c)); + background-image: -webkit-linear-gradient(top, #d9534f, 0%, #c9302c, 100%); + background-image: -moz-linear-gradient(top, #d9534f 0%, #c9302c 100%); + background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); + background-repeat: repeat-x; + border-color: #c12e2a; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); +} + +.btn-danger:active, +.btn-danger.active { + background-color: #c9302c; + border-color: #c12e2a; +} + +.btn-info { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5bc0de), to(#31b0d5)); + background-image: -webkit-linear-gradient(top, #5bc0de, 0%, #31b0d5, 100%); + background-image: -moz-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); + background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); + background-repeat: repeat-x; + border-color: #2aabd2; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); +} + +.btn-info:active, +.btn-info.active { + background-color: #31b0d5; + border-color: #2aabd2; +} + +.thumbnail, +.img-thumbnail { + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus, +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + background-color: #357ebd; + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#357ebd)); + background-image: -webkit-linear-gradient(top, #428bca, 0%, #357ebd, 100%); + background-image: -moz-linear-gradient(top, #428bca 0%, #357ebd 100%); + background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); +} + +.navbar { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ffffff), to(#f8f8f8)); + background-image: -webkit-linear-gradient(top, #ffffff, 0%, #f8f8f8, 100%); + background-image: -moz-linear-gradient(top, #ffffff 0%, #f8f8f8 100%); + background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%); + background-repeat: repeat-x; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); +} + +.navbar .navbar-nav > .active > a { + background-color: #f8f8f8; +} + +.navbar-brand, +.navbar-nav > li > a { + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25); +} + +.navbar-inverse { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#3c3c3c), to(#222222)); + background-image: -webkit-linear-gradient(top, #3c3c3c, 0%, #222222, 100%); + background-image: -moz-linear-gradient(top, #3c3c3c 0%, #222222 100%); + background-image: linear-gradient(to bottom, #3c3c3c 0%, #222222 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); +} + +.navbar-inverse .navbar-nav > .active > a { + background-color: #222222; +} + +.navbar-inverse .navbar-brand, +.navbar-inverse .navbar-nav > li > a { + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.navbar-static-top, +.navbar-fixed-top, +.navbar-fixed-bottom { + border-radius: 0; +} + +.alert { + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.alert-success { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#dff0d8), to(#c8e5bc)); + background-image: -webkit-linear-gradient(top, #dff0d8, 0%, #c8e5bc, 100%); + background-image: -moz-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); + background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); + background-repeat: repeat-x; + border-color: #b2dba1; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); +} + +.alert-info { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9edf7), to(#b9def0)); + background-image: -webkit-linear-gradient(top, #d9edf7, 0%, #b9def0, 100%); + background-image: -moz-linear-gradient(top, #d9edf7 0%, #b9def0 100%); + background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); + background-repeat: repeat-x; + border-color: #9acfea; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); +} + +.alert-warning { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#fcf8e3), to(#f8efc0)); + background-image: -webkit-linear-gradient(top, #fcf8e3, 0%, #f8efc0, 100%); + background-image: -moz-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); + background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); + background-repeat: repeat-x; + border-color: #f5e79e; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); +} + +.alert-danger { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f2dede), to(#e7c3c3)); + background-image: -webkit-linear-gradient(top, #f2dede, 0%, #e7c3c3, 100%); + background-image: -moz-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); + background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); + background-repeat: repeat-x; + border-color: #dca7a7; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); +} + +.progress { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ebebeb), to(#f5f5f5)); + background-image: -webkit-linear-gradient(top, #ebebeb, 0%, #f5f5f5, 100%); + background-image: -moz-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); + background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); +} + +.progress-bar { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3071a9)); + background-image: -webkit-linear-gradient(top, #428bca, 0%, #3071a9, 100%); + background-image: -moz-linear-gradient(top, #428bca 0%, #3071a9 100%); + background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); +} + +.progress-bar-success { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5cb85c), to(#449d44)); + background-image: -webkit-linear-gradient(top, #5cb85c, 0%, #449d44, 100%); + background-image: -moz-linear-gradient(top, #5cb85c 0%, #449d44 100%); + background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); +} + +.progress-bar-info { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5bc0de), to(#31b0d5)); + background-image: -webkit-linear-gradient(top, #5bc0de, 0%, #31b0d5, 100%); + background-image: -moz-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); + background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); +} + +.progress-bar-warning { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f0ad4e), to(#ec971f)); + background-image: -webkit-linear-gradient(top, #f0ad4e, 0%, #ec971f, 100%); + background-image: -moz-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); + background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); +} + +.progress-bar-danger { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9534f), to(#c9302c)); + background-image: -webkit-linear-gradient(top, #d9534f, 0%, #c9302c, 100%); + background-image: -moz-linear-gradient(top, #d9534f 0%, #c9302c 100%); + background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); +} + +.list-group { + border-radius: 4px; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); +} + +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + text-shadow: 0 -1px 0 #3071a9; + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3278b3)); + background-image: -webkit-linear-gradient(top, #428bca, 0%, #3278b3, 100%); + background-image: -moz-linear-gradient(top, #428bca 0%, #3278b3 100%); + background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); + background-repeat: repeat-x; + border-color: #3278b3; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); +} + +.panel { + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.panel-default > .panel-heading { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f5f5f5), to(#e8e8e8)); + background-image: -webkit-linear-gradient(top, #f5f5f5, 0%, #e8e8e8, 100%); + background-image: -moz-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); +} + +.panel-primary > .panel-heading { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#357ebd)); + background-image: -webkit-linear-gradient(top, #428bca, 0%, #357ebd, 100%); + background-image: -moz-linear-gradient(top, #428bca 0%, #357ebd 100%); + background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); +} + +.panel-success > .panel-heading { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#dff0d8), to(#d0e9c6)); + background-image: -webkit-linear-gradient(top, #dff0d8, 0%, #d0e9c6, 100%); + background-image: -moz-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); + background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); +} + +.panel-info > .panel-heading { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9edf7), to(#c4e3f3)); + background-image: -webkit-linear-gradient(top, #d9edf7, 0%, #c4e3f3, 100%); + background-image: -moz-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); + background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); +} + +.panel-warning > .panel-heading { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#fcf8e3), to(#faf2cc)); + background-image: -webkit-linear-gradient(top, #fcf8e3, 0%, #faf2cc, 100%); + background-image: -moz-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); + background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); +} + +.panel-danger > .panel-heading { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f2dede), to(#ebcccc)); + background-image: -webkit-linear-gradient(top, #f2dede, 0%, #ebcccc, 100%); + background-image: -moz-linear-gradient(top, #f2dede 0%, #ebcccc 100%); + background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); +} + +.well { + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#e8e8e8), to(#f5f5f5)); + background-image: -webkit-linear-gradient(top, #e8e8e8, 0%, #f5f5f5, 100%); + background-image: -moz-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); + background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); + background-repeat: repeat-x; + border-color: #dcdcdc; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); +} \ No newline at end of file diff --git a/framework/yii/bootstrap/assets/css/bootstrap.css b/framework/yii/bootstrap/assets/css/bootstrap.css new file mode 100644 index 0000000..bbda4ee --- /dev/null +++ b/framework/yii/bootstrap/assets/css/bootstrap.css @@ -0,0 +1,6805 @@ +/*! + * Bootstrap v3.0.0 + * + * Copyright 2013 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world by @mdo and @fat. + */ + +/*! normalize.css v2.1.0 | MIT License | git.io/normalize */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; +} + +audio, +canvas, +video { + display: inline-block; +} + +audio:not([controls]) { + display: none; + height: 0; +} + +[hidden] { + display: none; +} + +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + margin: 0; +} + +a:focus { + outline: thin dotted; +} + +a:active, +a:hover { + outline: 0; +} + +h1 { + margin: 0.67em 0; + font-size: 2em; +} + +abbr[title] { + border-bottom: 1px dotted; +} + +b, +strong { + font-weight: bold; +} + +dfn { + font-style: italic; +} + +hr { + height: 0; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +mark { + color: #000; + background: #ff0; +} + +code, +kbd, +pre, +samp { + font-family: monospace, serif; + font-size: 1em; +} + +pre { + white-space: pre-wrap; +} + +q { + quotes: "\201C" "\201D" "\2018" "\2019"; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + border: 0; +} + +svg:not(:root) { + overflow: hidden; +} + +figure { + margin: 0; +} + +fieldset { + padding: 0.35em 0.625em 0.75em; + margin: 0 2px; + border: 1px solid #c0c0c0; +} + +legend { + padding: 0; + border: 0; +} + +button, +input, +select, +textarea { + margin: 0; + font-family: inherit; + font-size: 100%; +} + +button, +input { + line-height: normal; +} + +button, +select { + text-transform: none; +} + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +button[disabled], +html input[disabled] { + cursor: default; +} + +input[type="checkbox"], +input[type="radio"] { + padding: 0; + box-sizing: border-box; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +@media print { + * { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + @page { + margin: 2cm .5cm; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + .navbar { + display: none; + } + .table td, + .table th { + background-color: #fff !important; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} + +*, +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +html { + font-size: 62.5%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.428571429; + color: #333333; + background-color: #ffffff; +} + +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input, +select[multiple], +textarea { + background-image: none; +} + +a { + color: #428bca; + text-decoration: none; +} + +a:hover, +a:focus { + color: #2a6496; + text-decoration: underline; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +img { + vertical-align: middle; +} + +.img-responsive { + display: block; + height: auto; + max-width: 100%; +} + +.img-rounded { + border-radius: 6px; +} + +.img-thumbnail { + display: inline-block; + height: auto; + max-width: 100%; + padding: 4px; + line-height: 1.428571429; + background-color: #ffffff; + border: 1px solid #dddddd; + border-radius: 4px; + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +.img-circle { + border-radius: 50%; +} + +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eeeeee; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + border: 0; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 16.099999999999998px; + font-weight: 200; + line-height: 1.4; +} + +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} + +small { + font-size: 85%; +} + +cite { + font-style: normal; +} + +.text-muted { + color: #999999; +} + +.text-primary { + color: #428bca; +} + +.text-warning { + color: #c09853; +} + +.text-danger { + color: #b94a48; +} + +.text-success { + color: #468847; +} + +.text-info { + color: #3a87ad; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 500; + line-height: 1.1; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small { + font-weight: normal; + line-height: 1; + color: #999999; +} + +h1, +h2, +h3 { + margin-top: 20px; + margin-bottom: 10px; +} + +h4, +h5, +h6 { + margin-top: 10px; + margin-bottom: 10px; +} + +h1, +.h1 { + font-size: 36px; +} + +h2, +.h2 { + font-size: 30px; +} + +h3, +.h3 { + font-size: 24px; +} + +h4, +.h4 { + font-size: 18px; +} + +h5, +.h5 { + font-size: 14px; +} + +h6, +.h6 { + font-size: 12px; +} + +h1 small, +.h1 small { + font-size: 24px; +} + +h2 small, +.h2 small { + font-size: 18px; +} + +h3 small, +.h3 small, +h4 small, +.h4 small { + font-size: 14px; +} + +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} + +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} + +dl { + margin-bottom: 20px; +} + +dt, +dd { + line-height: 1.428571429; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 0; +} + +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } + .dl-horizontal dd:before, + .dl-horizontal dd:after { + display: table; + content: " "; + } + .dl-horizontal dd:after { + clear: both; + } + .dl-horizontal dd:before, + .dl-horizontal dd:after { + display: table; + content: " "; + } + .dl-horizontal dd:after { + clear: both; + } +} + +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #999999; +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + font-size: 17.5px; + font-weight: 300; + line-height: 1.25; +} + +blockquote p:last-child { + margin-bottom: 0; +} + +blockquote small { + display: block; + line-height: 1.428571429; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +blockquote.pull-right small:before { + content: ''; +} + +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 20px; + font-style: normal; + line-height: 1.428571429; +} + +code, +pre { + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; +} + +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + white-space: nowrap; + background-color: #f9f2f4; + border-radius: 4px; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.428571429; + color: #333333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 20px; +} + +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +.container:before, +.container:after { + display: table; + content: " "; +} + +.container:after { + clear: both; +} + +.container:before, +.container:after { + display: table; + content: " "; +} + +.container:after { + clear: both; +} + +.row { + margin-right: -15px; + margin-left: -15px; +} + +.row:before, +.row:after { + display: table; + content: " "; +} + +.row:after { + clear: both; +} + +.row:before, +.row:after { + display: table; + content: " "; +} + +.row:after { + clear: both; +} + +.col-xs-1, +.col-xs-2, +.col-xs-3, +.col-xs-4, +.col-xs-5, +.col-xs-6, +.col-xs-7, +.col-xs-8, +.col-xs-9, +.col-xs-10, +.col-xs-11, +.col-xs-12, +.col-sm-1, +.col-sm-2, +.col-sm-3, +.col-sm-4, +.col-sm-5, +.col-sm-6, +.col-sm-7, +.col-sm-8, +.col-sm-9, +.col-sm-10, +.col-sm-11, +.col-sm-12, +.col-md-1, +.col-md-2, +.col-md-3, +.col-md-4, +.col-md-5, +.col-md-6, +.col-md-7, +.col-md-8, +.col-md-9, +.col-md-10, +.col-md-11, +.col-md-12, +.col-lg-1, +.col-lg-2, +.col-lg-3, +.col-lg-4, +.col-lg-5, +.col-lg-6, +.col-lg-7, +.col-lg-8, +.col-lg-9, +.col-lg-10, +.col-lg-11, +.col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} + +.col-xs-1, +.col-xs-2, +.col-xs-3, +.col-xs-4, +.col-xs-5, +.col-xs-6, +.col-xs-7, +.col-xs-8, +.col-xs-9, +.col-xs-10, +.col-xs-11 { + float: left; +} + +.col-xs-1 { + width: 8.333333333333332%; +} + +.col-xs-2 { + width: 16.666666666666664%; +} + +.col-xs-3 { + width: 25%; +} + +.col-xs-4 { + width: 33.33333333333333%; +} + +.col-xs-5 { + width: 41.66666666666667%; +} + +.col-xs-6 { + width: 50%; +} + +.col-xs-7 { + width: 58.333333333333336%; +} + +.col-xs-8 { + width: 66.66666666666666%; +} + +.col-xs-9 { + width: 75%; +} + +.col-xs-10 { + width: 83.33333333333334%; +} + +.col-xs-11 { + width: 91.66666666666666%; +} + +.col-xs-12 { + width: 100%; +} + +@media (min-width: 768px) { + .container { + max-width: 750px; + } + .col-sm-1, + .col-sm-2, + .col-sm-3, + .col-sm-4, + .col-sm-5, + .col-sm-6, + .col-sm-7, + .col-sm-8, + .col-sm-9, + .col-sm-10, + .col-sm-11 { + float: left; + } + .col-sm-1 { + width: 8.333333333333332%; + } + .col-sm-2 { + width: 16.666666666666664%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-4 { + width: 33.33333333333333%; + } + .col-sm-5 { + width: 41.66666666666667%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-7 { + width: 58.333333333333336%; + } + .col-sm-8 { + width: 66.66666666666666%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-10 { + width: 83.33333333333334%; + } + .col-sm-11 { + width: 91.66666666666666%; + } + .col-sm-12 { + width: 100%; + } + .col-sm-push-1 { + left: 8.333333333333332%; + } + .col-sm-push-2 { + left: 16.666666666666664%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-4 { + left: 33.33333333333333%; + } + .col-sm-push-5 { + left: 41.66666666666667%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-7 { + left: 58.333333333333336%; + } + .col-sm-push-8 { + left: 66.66666666666666%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-10 { + left: 83.33333333333334%; + } + .col-sm-push-11 { + left: 91.66666666666666%; + } + .col-sm-pull-1 { + right: 8.333333333333332%; + } + .col-sm-pull-2 { + right: 16.666666666666664%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-4 { + right: 33.33333333333333%; + } + .col-sm-pull-5 { + right: 41.66666666666667%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-7 { + right: 58.333333333333336%; + } + .col-sm-pull-8 { + right: 66.66666666666666%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-10 { + right: 83.33333333333334%; + } + .col-sm-pull-11 { + right: 91.66666666666666%; + } + .col-sm-offset-1 { + margin-left: 8.333333333333332%; + } + .col-sm-offset-2 { + margin-left: 16.666666666666664%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-4 { + margin-left: 33.33333333333333%; + } + .col-sm-offset-5 { + margin-left: 41.66666666666667%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-7 { + margin-left: 58.333333333333336%; + } + .col-sm-offset-8 { + margin-left: 66.66666666666666%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-10 { + margin-left: 83.33333333333334%; + } + .col-sm-offset-11 { + margin-left: 91.66666666666666%; + } +} + +@media (min-width: 992px) { + .container { + max-width: 970px; + } + .col-md-1, + .col-md-2, + .col-md-3, + .col-md-4, + .col-md-5, + .col-md-6, + .col-md-7, + .col-md-8, + .col-md-9, + .col-md-10, + .col-md-11 { + float: left; + } + .col-md-1 { + width: 8.333333333333332%; + } + .col-md-2 { + width: 16.666666666666664%; + } + .col-md-3 { + width: 25%; + } + .col-md-4 { + width: 33.33333333333333%; + } + .col-md-5 { + width: 41.66666666666667%; + } + .col-md-6 { + width: 50%; + } + .col-md-7 { + width: 58.333333333333336%; + } + .col-md-8 { + width: 66.66666666666666%; + } + .col-md-9 { + width: 75%; + } + .col-md-10 { + width: 83.33333333333334%; + } + .col-md-11 { + width: 91.66666666666666%; + } + .col-md-12 { + width: 100%; + } + .col-md-push-0 { + left: auto; + } + .col-md-push-1 { + left: 8.333333333333332%; + } + .col-md-push-2 { + left: 16.666666666666664%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-4 { + left: 33.33333333333333%; + } + .col-md-push-5 { + left: 41.66666666666667%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-7 { + left: 58.333333333333336%; + } + .col-md-push-8 { + left: 66.66666666666666%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-10 { + left: 83.33333333333334%; + } + .col-md-push-11 { + left: 91.66666666666666%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-pull-1 { + right: 8.333333333333332%; + } + .col-md-pull-2 { + right: 16.666666666666664%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-4 { + right: 33.33333333333333%; + } + .col-md-pull-5 { + right: 41.66666666666667%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-7 { + right: 58.333333333333336%; + } + .col-md-pull-8 { + right: 66.66666666666666%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-10 { + right: 83.33333333333334%; + } + .col-md-pull-11 { + right: 91.66666666666666%; + } + .col-md-offset-0 { + margin-left: 0; + } + .col-md-offset-1 { + margin-left: 8.333333333333332%; + } + .col-md-offset-2 { + margin-left: 16.666666666666664%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-4 { + margin-left: 33.33333333333333%; + } + .col-md-offset-5 { + margin-left: 41.66666666666667%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-7 { + margin-left: 58.333333333333336%; + } + .col-md-offset-8 { + margin-left: 66.66666666666666%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-10 { + margin-left: 83.33333333333334%; + } + .col-md-offset-11 { + margin-left: 91.66666666666666%; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1170px; + } + .col-lg-1, + .col-lg-2, + .col-lg-3, + .col-lg-4, + .col-lg-5, + .col-lg-6, + .col-lg-7, + .col-lg-8, + .col-lg-9, + .col-lg-10, + .col-lg-11 { + float: left; + } + .col-lg-1 { + width: 8.333333333333332%; + } + .col-lg-2 { + width: 16.666666666666664%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-4 { + width: 33.33333333333333%; + } + .col-lg-5 { + width: 41.66666666666667%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-7 { + width: 58.333333333333336%; + } + .col-lg-8 { + width: 66.66666666666666%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-10 { + width: 83.33333333333334%; + } + .col-lg-11 { + width: 91.66666666666666%; + } + .col-lg-12 { + width: 100%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-push-1 { + left: 8.333333333333332%; + } + .col-lg-push-2 { + left: 16.666666666666664%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-4 { + left: 33.33333333333333%; + } + .col-lg-push-5 { + left: 41.66666666666667%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-7 { + left: 58.333333333333336%; + } + .col-lg-push-8 { + left: 66.66666666666666%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-10 { + left: 83.33333333333334%; + } + .col-lg-push-11 { + left: 91.66666666666666%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-pull-1 { + right: 8.333333333333332%; + } + .col-lg-pull-2 { + right: 16.666666666666664%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-4 { + right: 33.33333333333333%; + } + .col-lg-pull-5 { + right: 41.66666666666667%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-7 { + right: 58.333333333333336%; + } + .col-lg-pull-8 { + right: 66.66666666666666%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-10 { + right: 83.33333333333334%; + } + .col-lg-pull-11 { + right: 91.66666666666666%; + } + .col-lg-offset-0 { + margin-left: 0; + } + .col-lg-offset-1 { + margin-left: 8.333333333333332%; + } + .col-lg-offset-2 { + margin-left: 16.666666666666664%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-4 { + margin-left: 33.33333333333333%; + } + .col-lg-offset-5 { + margin-left: 41.66666666666667%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-7 { + margin-left: 58.333333333333336%; + } + .col-lg-offset-8 { + margin-left: 66.66666666666666%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-10 { + margin-left: 83.33333333333334%; + } + .col-lg-offset-11 { + margin-left: 91.66666666666666%; + } +} + +table { + max-width: 100%; + background-color: transparent; +} + +th { + text-align: left; +} + +.table { + width: 100%; + margin-bottom: 20px; +} + +.table thead > tr > th, +.table tbody > tr > th, +.table tfoot > tr > th, +.table thead > tr > td, +.table tbody > tr > td, +.table tfoot > tr > td { + padding: 8px; + line-height: 1.428571429; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #dddddd; +} + +.table caption + thead tr:first-child th, +.table colgroup + thead tr:first-child th, +.table thead:first-child tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table .table { + background-color: #ffffff; +} + +.table-condensed thead > tr > th, +.table-condensed tbody > tr > th, +.table-condensed tfoot > tr > th, +.table-condensed thead > tr > td, +.table-condensed tbody > tr > td, +.table-condensed tfoot > tr > td { + padding: 5px; +} + +.table-bordered { + border: 1px solid #dddddd; +} + +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #dddddd; +} + +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} + +.table-striped > tbody > tr:nth-child(odd) > td, +.table-striped > tbody > tr:nth-child(odd) > th { + background-color: #f9f9f9; +} + +.table-hover > tbody > tr:hover > td, +.table-hover > tbody > tr:hover > th { + background-color: #f5f5f5; +} + +table col[class*="col-"] { + display: table-column; + float: none; +} + +table td[class*="col-"], +table th[class*="col-"] { + display: table-cell; + float: none; +} + +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} + +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td { + background-color: #d0e9c6; + border-color: #c9e2b3; +} + +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; + border-color: #eed3d7; +} + +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td { + background-color: #ebcccc; + border-color: #e6c1c7; +} + +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; + border-color: #fbeed5; +} + +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td { + background-color: #faf2cc; + border-color: #f8e5be; +} + +@media (max-width: 768px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-x: scroll; + overflow-y: hidden; + border: 1px solid #dddddd; + } + .table-responsive > .table { + margin-bottom: 0; + background-color: #fff; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > thead > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > thead > tr:last-child > td, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +label { + display: inline-block; + margin-bottom: 5px; + font-weight: bold; +} + +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + /* IE8-9 */ + + line-height: normal; +} + +input[type="file"] { + display: block; +} + +select[multiple], +select[size] { + height: auto; +} + +select optgroup { + font-family: inherit; + font-size: inherit; + font-style: inherit; +} + +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + height: auto; +} + +.form-control:-moz-placeholder { + color: #999999; +} + +.form-control::-moz-placeholder { + color: #999999; +} + +.form-control:-ms-input-placeholder { + color: #999999; +} + +.form-control::-webkit-input-placeholder { + color: #999999; +} + +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.428571429; + color: #555555; + vertical-align: middle; + background-color: #ffffff; + border: 1px solid #cccccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; +} + +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); +} + +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + cursor: not-allowed; + background-color: #eeeeee; +} + +textarea.form-control { + height: auto; +} + +.form-group { + margin-bottom: 15px; +} + +.radio, +.checkbox { + display: block; + min-height: 20px; + padding-left: 20px; + margin-top: 10px; + margin-bottom: 10px; + vertical-align: middle; +} + +.radio label, +.checkbox label { + display: inline; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; +} + +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + float: left; + margin-left: -20px; +} + +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} + +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + vertical-align: middle; + cursor: pointer; +} + +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +.radio[disabled], +.radio-inline[disabled], +.checkbox[disabled], +.checkbox-inline[disabled], +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"], +fieldset[disabled] .radio, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} + +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +select.input-sm { + height: 30px; + line-height: 30px; +} + +textarea.input-sm { + height: auto; +} + +.input-lg { + height: 45px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} + +select.input-lg { + height: 45px; + line-height: 45px; +} + +textarea.input-lg { + height: auto; +} + +.has-warning .help-block, +.has-warning .control-label { + color: #c09853; +} + +.has-warning .form-control { + border-color: #c09853; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.has-warning .form-control:focus { + border-color: #a47e3c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; +} + +.has-warning .input-group-addon { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.has-error .help-block, +.has-error .control-label { + color: #b94a48; +} + +.has-error .form-control { + border-color: #b94a48; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.has-error .form-control:focus { + border-color: #953b39; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; +} + +.has-error .input-group-addon { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.has-success .help-block, +.has-success .control-label { + color: #468847; +} + +.has-success .form-control { + border-color: #468847; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.has-success .form-control:focus { + border-color: #356635; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; +} + +.has-success .input-group-addon { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +.form-control-static { + padding-top: 7px; + margin-bottom: 0; +} + +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} + +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + float: none; + margin-left: 0; + } +} + +.form-horizontal .control-label, +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; +} + +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; +} + +.form-horizontal .form-group:before, +.form-horizontal .form-group:after { + display: table; + content: " "; +} + +.form-horizontal .form-group:after { + clear: both; +} + +.form-horizontal .form-group:before, +.form-horizontal .form-group:after { + display: table; + content: " "; +} + +.form-horizontal .form-group:after { + clear: both; +} + +@media (min-width: 768px) { + .form-horizontal .control-label { + text-align: right; + } +} + +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.428571429; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + border: 1px solid transparent; + border-radius: 4px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn:hover, +.btn:focus { + color: #333333; + text-decoration: none; +} + +.btn:active, +.btn.active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + pointer-events: none; + cursor: not-allowed; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; +} + +.btn-default { + color: #333333; + background-color: #ffffff; + border-color: #cccccc; +} + +.btn-default:hover, +.btn-default:focus, +.btn-default:active, +.btn-default.active, +.open .dropdown-toggle.btn-default { + color: #333333; + background-color: #ebebeb; + border-color: #adadad; +} + +.btn-default:active, +.btn-default.active, +.open .dropdown-toggle.btn-default { + background-image: none; +} + +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #ffffff; + border-color: #cccccc; +} + +.btn-primary { + color: #ffffff; + background-color: #428bca; + border-color: #357ebd; +} + +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active, +.btn-primary.active, +.open .dropdown-toggle.btn-primary { + color: #ffffff; + background-color: #3276b1; + border-color: #285e8e; +} + +.btn-primary:active, +.btn-primary.active, +.open .dropdown-toggle.btn-primary { + background-image: none; +} + +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #428bca; + border-color: #357ebd; +} + +.btn-warning { + color: #ffffff; + background-color: #f0ad4e; + border-color: #eea236; +} + +.btn-warning:hover, +.btn-warning:focus, +.btn-warning:active, +.btn-warning.active, +.open .dropdown-toggle.btn-warning { + color: #ffffff; + background-color: #ed9c28; + border-color: #d58512; +} + +.btn-warning:active, +.btn-warning.active, +.open .dropdown-toggle.btn-warning { + background-image: none; +} + +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #f0ad4e; + border-color: #eea236; +} + +.btn-danger { + color: #ffffff; + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-danger:hover, +.btn-danger:focus, +.btn-danger:active, +.btn-danger.active, +.open .dropdown-toggle.btn-danger { + color: #ffffff; + background-color: #d2322d; + border-color: #ac2925; +} + +.btn-danger:active, +.btn-danger.active, +.open .dropdown-toggle.btn-danger { + background-image: none; +} + +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-success { + color: #ffffff; + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-success:hover, +.btn-success:focus, +.btn-success:active, +.btn-success.active, +.open .dropdown-toggle.btn-success { + color: #ffffff; + background-color: #47a447; + border-color: #398439; +} + +.btn-success:active, +.btn-success.active, +.open .dropdown-toggle.btn-success { + background-image: none; +} + +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-info { + color: #ffffff; + background-color: #5bc0de; + border-color: #46b8da; +} + +.btn-info:hover, +.btn-info:focus, +.btn-info:active, +.btn-info.active, +.open .dropdown-toggle.btn-info { + color: #ffffff; + background-color: #39b3d7; + border-color: #269abc; +} + +.btn-info:active, +.btn-info.active, +.open .dropdown-toggle.btn-info { + background-image: none; +} + +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #5bc0de; + border-color: #46b8da; +} + +.btn-link { + font-weight: normal; + color: #428bca; + cursor: pointer; + border-radius: 0; +} + +.btn-link, +.btn-link:active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} + +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} + +.btn-link:hover, +.btn-link:focus { + color: #2a6496; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #999999; + text-decoration: none; +} + +.btn-lg { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} + +.btn-sm, +.btn-xs { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-xs { + padding: 1px 5px; +} + +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 0; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + display: none; +} + +.collapse.in { + display: block; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +@font-face { + font-family: 'Glyphicons Halflings'; + src: url('../fonts/glyphicons-halflings-regular.eot'); + src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg'); +} + +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + -webkit-font-smoothing: antialiased; + font-style: normal; + font-weight: normal; + line-height: 1; +} + +.glyphicon-asterisk:before { + content: "\2a"; +} + +.glyphicon-plus:before { + content: "\2b"; +} + +.glyphicon-euro:before { + content: "\20ac"; +} + +.glyphicon-minus:before { + content: "\2212"; +} + +.glyphicon-cloud:before { + content: "\2601"; +} + +.glyphicon-envelope:before { + content: "\2709"; +} + +.glyphicon-pencil:before { + content: "\270f"; +} + +.glyphicon-glass:before { + content: "\e001"; +} + +.glyphicon-music:before { + content: "\e002"; +} + +.glyphicon-search:before { + content: "\e003"; +} + +.glyphicon-heart:before { + content: "\e005"; +} + +.glyphicon-star:before { + content: "\e006"; +} + +.glyphicon-star-empty:before { + content: "\e007"; +} + +.glyphicon-user:before { + content: "\e008"; +} + +.glyphicon-film:before { + content: "\e009"; +} + +.glyphicon-th-large:before { + content: "\e010"; +} + +.glyphicon-th:before { + content: "\e011"; +} + +.glyphicon-th-list:before { + content: "\e012"; +} + +.glyphicon-ok:before { + content: "\e013"; +} + +.glyphicon-remove:before { + content: "\e014"; +} + +.glyphicon-zoom-in:before { + content: "\e015"; +} + +.glyphicon-zoom-out:before { + content: "\e016"; +} + +.glyphicon-off:before { + content: "\e017"; +} + +.glyphicon-signal:before { + content: "\e018"; +} + +.glyphicon-cog:before { + content: "\e019"; +} + +.glyphicon-trash:before { + content: "\e020"; +} + +.glyphicon-home:before { + content: "\e021"; +} + +.glyphicon-file:before { + content: "\e022"; +} + +.glyphicon-time:before { + content: "\e023"; +} + +.glyphicon-road:before { + content: "\e024"; +} + +.glyphicon-download-alt:before { + content: "\e025"; +} + +.glyphicon-download:before { + content: "\e026"; +} + +.glyphicon-upload:before { + content: "\e027"; +} + +.glyphicon-inbox:before { + content: "\e028"; +} + +.glyphicon-play-circle:before { + content: "\e029"; +} + +.glyphicon-repeat:before { + content: "\e030"; +} + +.glyphicon-refresh:before { + content: "\e031"; +} + +.glyphicon-list-alt:before { + content: "\e032"; +} + +.glyphicon-flag:before { + content: "\e034"; +} + +.glyphicon-headphones:before { + content: "\e035"; +} + +.glyphicon-volume-off:before { + content: "\e036"; +} + +.glyphicon-volume-down:before { + content: "\e037"; +} + +.glyphicon-volume-up:before { + content: "\e038"; +} + +.glyphicon-qrcode:before { + content: "\e039"; +} + +.glyphicon-barcode:before { + content: "\e040"; +} + +.glyphicon-tag:before { + content: "\e041"; +} + +.glyphicon-tags:before { + content: "\e042"; +} + +.glyphicon-book:before { + content: "\e043"; +} + +.glyphicon-print:before { + content: "\e045"; +} + +.glyphicon-font:before { + content: "\e047"; +} + +.glyphicon-bold:before { + content: "\e048"; +} + +.glyphicon-italic:before { + content: "\e049"; +} + +.glyphicon-text-height:before { + content: "\e050"; +} + +.glyphicon-text-width:before { + content: "\e051"; +} + +.glyphicon-align-left:before { + content: "\e052"; +} + +.glyphicon-align-center:before { + content: "\e053"; +} + +.glyphicon-align-right:before { + content: "\e054"; +} + +.glyphicon-align-justify:before { + content: "\e055"; +} + +.glyphicon-list:before { + content: "\e056"; +} + +.glyphicon-indent-left:before { + content: "\e057"; +} + +.glyphicon-indent-right:before { + content: "\e058"; +} + +.glyphicon-facetime-video:before { + content: "\e059"; +} + +.glyphicon-picture:before { + content: "\e060"; +} + +.glyphicon-map-marker:before { + content: "\e062"; +} + +.glyphicon-adjust:before { + content: "\e063"; +} + +.glyphicon-tint:before { + content: "\e064"; +} + +.glyphicon-edit:before { + content: "\e065"; +} + +.glyphicon-share:before { + content: "\e066"; +} + +.glyphicon-check:before { + content: "\e067"; +} + +.glyphicon-move:before { + content: "\e068"; +} + +.glyphicon-step-backward:before { + content: "\e069"; +} + +.glyphicon-fast-backward:before { + content: "\e070"; +} + +.glyphicon-backward:before { + content: "\e071"; +} + +.glyphicon-play:before { + content: "\e072"; +} + +.glyphicon-pause:before { + content: "\e073"; +} + +.glyphicon-stop:before { + content: "\e074"; +} + +.glyphicon-forward:before { + content: "\e075"; +} + +.glyphicon-fast-forward:before { + content: "\e076"; +} + +.glyphicon-step-forward:before { + content: "\e077"; +} + +.glyphicon-eject:before { + content: "\e078"; +} + +.glyphicon-chevron-left:before { + content: "\e079"; +} + +.glyphicon-chevron-right:before { + content: "\e080"; +} + +.glyphicon-plus-sign:before { + content: "\e081"; +} + +.glyphicon-minus-sign:before { + content: "\e082"; +} + +.glyphicon-remove-sign:before { + content: "\e083"; +} + +.glyphicon-ok-sign:before { + content: "\e084"; +} + +.glyphicon-question-sign:before { + content: "\e085"; +} + +.glyphicon-info-sign:before { + content: "\e086"; +} + +.glyphicon-screenshot:before { + content: "\e087"; +} + +.glyphicon-remove-circle:before { + content: "\e088"; +} + +.glyphicon-ok-circle:before { + content: "\e089"; +} + +.glyphicon-ban-circle:before { + content: "\e090"; +} + +.glyphicon-arrow-left:before { + content: "\e091"; +} + +.glyphicon-arrow-right:before { + content: "\e092"; +} + +.glyphicon-arrow-up:before { + content: "\e093"; +} + +.glyphicon-arrow-down:before { + content: "\e094"; +} + +.glyphicon-share-alt:before { + content: "\e095"; +} + +.glyphicon-resize-full:before { + content: "\e096"; +} + +.glyphicon-resize-small:before { + content: "\e097"; +} + +.glyphicon-exclamation-sign:before { + content: "\e101"; +} + +.glyphicon-gift:before { + content: "\e102"; +} + +.glyphicon-leaf:before { + content: "\e103"; +} + +.glyphicon-eye-open:before { + content: "\e105"; +} + +.glyphicon-eye-close:before { + content: "\e106"; +} + +.glyphicon-warning-sign:before { + content: "\e107"; +} + +.glyphicon-plane:before { + content: "\e108"; +} + +.glyphicon-random:before { + content: "\e110"; +} + +.glyphicon-comment:before { + content: "\e111"; +} + +.glyphicon-magnet:before { + content: "\e112"; +} + +.glyphicon-chevron-up:before { + content: "\e113"; +} + +.glyphicon-chevron-down:before { + content: "\e114"; +} + +.glyphicon-retweet:before { + content: "\e115"; +} + +.glyphicon-shopping-cart:before { + content: "\e116"; +} + +.glyphicon-folder-close:before { + content: "\e117"; +} + +.glyphicon-folder-open:before { + content: "\e118"; +} + +.glyphicon-resize-vertical:before { + content: "\e119"; +} + +.glyphicon-resize-horizontal:before { + content: "\e120"; +} + +.glyphicon-hdd:before { + content: "\e121"; +} + +.glyphicon-bullhorn:before { + content: "\e122"; +} + +.glyphicon-certificate:before { + content: "\e124"; +} + +.glyphicon-thumbs-up:before { + content: "\e125"; +} + +.glyphicon-thumbs-down:before { + content: "\e126"; +} + +.glyphicon-hand-right:before { + content: "\e127"; +} + +.glyphicon-hand-left:before { + content: "\e128"; +} + +.glyphicon-hand-up:before { + content: "\e129"; +} + +.glyphicon-hand-down:before { + content: "\e130"; +} + +.glyphicon-circle-arrow-right:before { + content: "\e131"; +} + +.glyphicon-circle-arrow-left:before { + content: "\e132"; +} + +.glyphicon-circle-arrow-up:before { + content: "\e133"; +} + +.glyphicon-circle-arrow-down:before { + content: "\e134"; +} + +.glyphicon-globe:before { + content: "\e135"; +} + +.glyphicon-tasks:before { + content: "\e137"; +} + +.glyphicon-filter:before { + content: "\e138"; +} + +.glyphicon-fullscreen:before { + content: "\e140"; +} + +.glyphicon-dashboard:before { + content: "\e141"; +} + +.glyphicon-heart-empty:before { + content: "\e143"; +} + +.glyphicon-link:before { + content: "\e144"; +} + +.glyphicon-phone:before { + content: "\e145"; +} + +.glyphicon-usd:before { + content: "\e148"; +} + +.glyphicon-gbp:before { + content: "\e149"; +} + +.glyphicon-sort:before { + content: "\e150"; +} + +.glyphicon-sort-by-alphabet:before { + content: "\e151"; +} + +.glyphicon-sort-by-alphabet-alt:before { + content: "\e152"; +} + +.glyphicon-sort-by-order:before { + content: "\e153"; +} + +.glyphicon-sort-by-order-alt:before { + content: "\e154"; +} + +.glyphicon-sort-by-attributes:before { + content: "\e155"; +} + +.glyphicon-sort-by-attributes-alt:before { + content: "\e156"; +} + +.glyphicon-unchecked:before { + content: "\e157"; +} + +.glyphicon-expand:before { + content: "\e158"; +} + +.glyphicon-collapse-down:before { + content: "\e159"; +} + +.glyphicon-collapse-up:before { + content: "\e160"; +} + +.glyphicon-log-in:before { + content: "\e161"; +} + +.glyphicon-flash:before { + content: "\e162"; +} + +.glyphicon-log-out:before { + content: "\e163"; +} + +.glyphicon-new-window:before { + content: "\e164"; +} + +.glyphicon-record:before { + content: "\e165"; +} + +.glyphicon-save:before { + content: "\e166"; +} + +.glyphicon-open:before { + content: "\e167"; +} + +.glyphicon-saved:before { + content: "\e168"; +} + +.glyphicon-import:before { + content: "\e169"; +} + +.glyphicon-export:before { + content: "\e170"; +} + +.glyphicon-send:before { + content: "\e171"; +} + +.glyphicon-floppy-disk:before { + content: "\e172"; +} + +.glyphicon-floppy-saved:before { + content: "\e173"; +} + +.glyphicon-floppy-remove:before { + content: "\e174"; +} + +.glyphicon-floppy-save:before { + content: "\e175"; +} + +.glyphicon-floppy-open:before { + content: "\e176"; +} + +.glyphicon-credit-card:before { + content: "\e177"; +} + +.glyphicon-transfer:before { + content: "\e178"; +} + +.glyphicon-cutlery:before { + content: "\e179"; +} + +.glyphicon-header:before { + content: "\e180"; +} + +.glyphicon-compressed:before { + content: "\e181"; +} + +.glyphicon-earphone:before { + content: "\e182"; +} + +.glyphicon-phone-alt:before { + content: "\e183"; +} + +.glyphicon-tower:before { + content: "\e184"; +} + +.glyphicon-stats:before { + content: "\e185"; +} + +.glyphicon-sd-video:before { + content: "\e186"; +} + +.glyphicon-hd-video:before { + content: "\e187"; +} + +.glyphicon-subtitles:before { + content: "\e188"; +} + +.glyphicon-sound-stereo:before { + content: "\e189"; +} + +.glyphicon-sound-dolby:before { + content: "\e190"; +} + +.glyphicon-sound-5-1:before { + content: "\e191"; +} + +.glyphicon-sound-6-1:before { + content: "\e192"; +} + +.glyphicon-sound-7-1:before { + content: "\e193"; +} + +.glyphicon-copyright-mark:before { + content: "\e194"; +} + +.glyphicon-registration-mark:before { + content: "\e195"; +} + +.glyphicon-cloud-download:before { + content: "\e197"; +} + +.glyphicon-cloud-upload:before { + content: "\e198"; +} + +.glyphicon-tree-conifer:before { + content: "\e199"; +} + +.glyphicon-tree-deciduous:before { + content: "\e200"; +} + +.glyphicon-briefcase:before { + content: "\1f4bc"; +} + +.glyphicon-calendar:before { + content: "\1f4c5"; +} + +.glyphicon-pushpin:before { + content: "\1f4cc"; +} + +.glyphicon-paperclip:before { + content: "\1f4ce"; +} + +.glyphicon-camera:before { + content: "\1f4f7"; +} + +.glyphicon-lock:before { + content: "\1f512"; +} + +.glyphicon-bell:before { + content: "\1f514"; +} + +.glyphicon-bookmark:before { + content: "\1f516"; +} + +.glyphicon-fire:before { + content: "\1f525"; +} + +.glyphicon-wrench:before { + content: "\1f527"; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-bottom: 0 dotted; + border-left: 4px solid transparent; + content: ""; +} + +.dropdown { + position: relative; +} + +.dropdown-toggle:focus { + outline: 0; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + list-style: none; + background-color: #ffffff; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} + +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.428571429; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: #ffffff; + text-decoration: none; + background-color: #428bca; +} + +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #ffffff; + text-decoration: none; + background-color: #428bca; + outline: 0; +} + +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #999999; +} + +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.open > .dropdown-menu { + display: block; +} + +.open > a { + outline: 0; +} + +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.428571429; + color: #999999; +} + +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0 dotted; + border-bottom: 4px solid #000000; + content: ""; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } +} + +.btn-default .caret { + border-top-color: #333333; +} + +.btn-primary .caret, +.btn-success .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret { + border-top-color: #fff; +} + +.dropup .btn-default .caret { + border-bottom-color: #333333; +} + +.dropup .btn-primary .caret, +.dropup .btn-success .caret, +.dropup .btn-warning .caret, +.dropup .btn-danger .caret, +.dropup .btn-info .caret { + border-bottom-color: #fff; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} + +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus { + outline: none; +} + +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} + +.btn-toolbar:before, +.btn-toolbar:after { + display: table; + content: " "; +} + +.btn-toolbar:after { + clear: both; +} + +.btn-toolbar:before, +.btn-toolbar:after { + display: table; + content: " "; +} + +.btn-toolbar:after { + clear: both; +} + +.btn-toolbar .btn-group { + float: left; +} + +.btn-toolbar > .btn + .btn, +.btn-toolbar > .btn-group + .btn, +.btn-toolbar > .btn + .btn-group, +.btn-toolbar > .btn-group + .btn-group { + margin-left: 5px; +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +.btn-group > .btn:first-child { + margin-left: 0; +} + +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.btn-group > .btn-group { + float: left; +} + +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.btn-group > .btn-group:first-child > .btn:last-child, +.btn-group > .btn-group:first-child > .dropdown-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn-group:last-child > .btn:first-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group-xs > .btn { + padding: 5px 10px; + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} + +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} + +.btn-group > .btn-lg + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +.btn .caret { + margin-left: 0; +} + +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} + +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} + +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group { + display: block; + float: none; + width: 100%; + max-width: 100%; +} + +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after { + display: table; + content: " "; +} + +.btn-group-vertical > .btn-group:after { + clear: both; +} + +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after { + display: table; + content: " "; +} + +.btn-group-vertical > .btn-group:after { + clear: both; +} + +.btn-group-vertical > .btn-group > .btn { + float: none; +} + +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-right-radius: 0; + border-bottom-left-radius: 4px; + border-top-left-radius: 0; +} + +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.btn-group-vertical > .btn-group:first-child > .btn:last-child, +.btn-group-vertical > .btn-group:first-child > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn-group:last-child > .btn:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.btn-group-justified { + display: table; + width: 100%; + border-collapse: separate; + table-layout: fixed; +} + +.btn-group-justified .btn { + display: table-cell; + float: none; + width: 1%; +} + +[data-toggle="buttons"] > .btn > input[type="radio"], +[data-toggle="buttons"] > .btn > input[type="checkbox"] { + display: none; +} + +.input-group { + position: relative; + display: table; + border-collapse: separate; +} + +.input-group.col { + float: none; + padding-right: 0; + padding-left: 0; +} + +.input-group .form-control { + width: 100%; + margin-bottom: 0; +} + +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 45px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} + +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 45px; + line-height: 45px; +} + +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn { + height: auto; +} + +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} + +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn { + height: auto; +} + +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} + +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} + +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + text-align: center; + background-color: #eeeeee; + border: 1px solid #cccccc; + border-radius: 4px; +} + +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} + +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} + +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} + +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group-addon:first-child { + border-right: 0; +} + +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.input-group-addon:last-child { + border-left: 0; +} + +.input-group-btn { + position: relative; + white-space: nowrap; +} + +.input-group-btn > .btn { + position: relative; +} + +.input-group-btn > .btn + .btn { + margin-left: -4px; +} + +.input-group-btn > .btn:hover, +.input-group-btn > .btn:active { + z-index: 2; +} + +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav:before, +.nav:after { + display: table; + content: " "; +} + +.nav:after { + clear: both; +} + +.nav:before, +.nav:after { + display: table; + content: " "; +} + +.nav:after { + clear: both; +} + +.nav > li { + position: relative; + display: block; +} + +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} + +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > li.disabled > a { + color: #999999; +} + +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #999999; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} + +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #eeeeee; + border-color: #428bca; +} + +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} + +.nav > li > a > img { + max-width: none; +} + +.nav-tabs { + border-bottom: 1px solid #dddddd; +} + +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} + +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.428571429; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555555; + cursor: default; + background-color: #ffffff; + border: 1px solid #dddddd; + border-bottom-color: transparent; +} + +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} + +.nav-tabs.nav-justified > li { + float: none; +} + +.nav-tabs.nav-justified > li > a { + text-align: center; +} + +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } +} + +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-bottom: 1px solid #dddddd; +} + +.nav-tabs.nav-justified > .active > a { + border-bottom-color: #ffffff; +} + +.nav-pills > li { + float: left; +} + +.nav-pills > li > a { + border-radius: 5px; +} + +.nav-pills > li + li { + margin-left: 2px; +} + +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #ffffff; + background-color: #428bca; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} + +.nav-justified { + width: 100%; +} + +.nav-justified > li { + float: none; +} + +.nav-justified > li > a { + text-align: center; +} + +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } +} + +.nav-tabs-justified { + border-bottom: 0; +} + +.nav-tabs-justified > li > a { + margin-right: 0; + border-bottom: 1px solid #dddddd; +} + +.nav-tabs-justified > .active > a { + border-bottom-color: #ffffff; +} + +.tabbable:before, +.tabbable:after { + display: table; + content: " "; +} + +.tabbable:after { + clear: both; +} + +.tabbable:before, +.tabbable:after { + display: table; + content: " "; +} + +.tabbable:after { + clear: both; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.nav .caret { + border-top-color: #428bca; + border-bottom-color: #428bca; +} + +.nav a:hover .caret { + border-top-color: #2a6496; + border-bottom-color: #2a6496; +} + +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.navbar { + position: relative; + z-index: 1000; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; +} + +.navbar:before, +.navbar:after { + display: table; + content: " "; +} + +.navbar:after { + clear: both; +} + +.navbar:before, +.navbar:after { + display: table; + content: " "; +} + +.navbar:after { + clear: both; +} + +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} + +.navbar-header:before, +.navbar-header:after { + display: table; + content: " "; +} + +.navbar-header:after { + clear: both; +} + +.navbar-header:before, +.navbar-header:after { + display: table; + content: " "; +} + +.navbar-header:after { + clear: both; +} + +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} + +.navbar-collapse { + max-height: 340px; + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + -webkit-overflow-scrolling: touch; +} + +.navbar-collapse:before, +.navbar-collapse:after { + display: table; + content: " "; +} + +.navbar-collapse:after { + clear: both; +} + +.navbar-collapse:before, +.navbar-collapse:after { + display: table; + content: " "; +} + +.navbar-collapse:after { + clear: both; +} + +.navbar-collapse.in { + overflow-y: auto; +} + +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-collapse .navbar-nav.navbar-left:first-child { + margin-left: -15px; + } + .navbar-collapse .navbar-nav.navbar-right:last-child { + margin-right: -15px; + } + .navbar-collapse .navbar-text:last-child { + margin-right: 0; + } +} + +.container > .navbar-header, +.container > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} + +@media (min-width: 768px) { + .container > .navbar-header, + .container > .navbar-collapse { + margin-right: 0; + margin-left: 0; + } +} + +.navbar-static-top { + border-width: 0 0 1px; +} + +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + border-width: 0 0 1px; +} + +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} + +.navbar-fixed-top { + top: 0; + z-index: 1030; +} + +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; +} + +.navbar-brand { + float: left; + padding: 15px 15px; + font-size: 18px; + line-height: 20px; +} + +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} + +@media (min-width: 768px) { + .navbar > .container .navbar-brand { + margin-left: -15px; + } +} + +.navbar-toggle { + position: relative; + float: right; + padding: 9px 10px; + margin-top: 8px; + margin-right: 15px; + margin-bottom: 8px; + background-color: transparent; + border: 1px solid transparent; + border-radius: 4px; +} + +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} + +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} + +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} + +.navbar-nav { + margin: 7.5px -15px; +} + +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} + +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} + +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + } +} + +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + } +} + +.navbar-form { + padding: 10px 15px; + margin-top: 8px; + margin-right: -15px; + margin-bottom: 8px; + margin-left: -15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); +} + +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + } + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + float: none; + margin-left: 0; + } +} + +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } +} + +@media (min-width: 768px) { + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} + +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.navbar-nav.pull-right > li > .dropdown-menu, +.navbar-nav > li > .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; +} + +.navbar-text { + float: left; + margin-top: 15px; + margin-bottom: 15px; +} + +@media (min-width: 768px) { + .navbar-text { + margin-right: 15px; + margin-left: 15px; + } +} + +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} + +.navbar-default .navbar-brand { + color: #777777; +} + +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #5e5e5e; + background-color: transparent; +} + +.navbar-default .navbar-text { + color: #777777; +} + +.navbar-default .navbar-nav > li > a { + color: #777777; +} + +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #333333; + background-color: transparent; +} + +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555555; + background-color: #e7e7e7; +} + +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #cccccc; + background-color: transparent; +} + +.navbar-default .navbar-toggle { + border-color: #dddddd; +} + +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #dddddd; +} + +.navbar-default .navbar-toggle .icon-bar { + background-color: #cccccc; +} + +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e6e6e6; +} + +.navbar-default .navbar-nav > .dropdown > a:hover .caret, +.navbar-default .navbar-nav > .dropdown > a:focus .caret { + border-top-color: #333333; + border-bottom-color: #333333; +} + +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + color: #555555; + background-color: #e7e7e7; +} + +.navbar-default .navbar-nav > .open > a .caret, +.navbar-default .navbar-nav > .open > a:hover .caret, +.navbar-default .navbar-nav > .open > a:focus .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar-default .navbar-nav > .dropdown > a .caret { + border-top-color: #777777; + border-bottom-color: #777777; +} + +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #777777; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #333333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555555; + background-color: #e7e7e7; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #cccccc; + background-color: transparent; + } +} + +.navbar-default .navbar-link { + color: #777777; +} + +.navbar-default .navbar-link:hover { + color: #333333; +} + +.navbar-inverse { + background-color: #222222; + border-color: #080808; +} + +.navbar-inverse .navbar-brand { + color: #999999; +} + +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .navbar-text { + color: #999999; +} + +.navbar-inverse .navbar-nav > li > a { + color: #999999; +} + +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #ffffff; + background-color: #080808; +} + +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444444; + background-color: transparent; +} + +.navbar-inverse .navbar-toggle { + border-color: #333333; +} + +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #333333; +} + +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #ffffff; +} + +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} + +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + color: #ffffff; + background-color: #080808; +} + +.navbar-inverse .navbar-nav > .dropdown > a:hover .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .navbar-nav > .dropdown > a .caret { + border-top-color: #999999; + border-bottom-color: #999999; +} + +.navbar-inverse .navbar-nav > .open > a .caret, +.navbar-inverse .navbar-nav > .open > a:hover .caret, +.navbar-inverse .navbar-nav > .open > a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #999999; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #ffffff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #ffffff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444444; + background-color: transparent; + } +} + +.navbar-inverse .navbar-link { + color: #999999; +} + +.navbar-inverse .navbar-link:hover { + color: #ffffff; +} + +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} + +.breadcrumb > li { + display: inline-block; +} + +.breadcrumb > li + li:before { + padding: 0 5px; + color: #cccccc; + content: "/\00a0"; +} + +.breadcrumb > .active { + color: #999999; +} + +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} + +.pagination > li { + display: inline; +} + +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.428571429; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; +} + +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-bottom-left-radius: 4px; + border-top-left-radius: 4px; +} + +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + background-color: #eeeeee; +} + +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 2; + color: #ffffff; + cursor: default; + background-color: #428bca; + border-color: #428bca; +} + +.pagination > .disabled > span, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #999999; + cursor: not-allowed; + background-color: #ffffff; + border-color: #dddddd; +} + +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; +} + +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-bottom-left-radius: 6px; + border-top-left-radius: 6px; +} + +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; +} + +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; +} + +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} + +.pager:before, +.pager:after { + display: table; + content: " "; +} + +.pager:after { + clear: both; +} + +.pager:before, +.pager:after { + display: table; + content: " "; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #ffffff; + border: 1px solid #dddddd; + border-radius: 15px; +} + +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.pager .next > a, +.pager .next > span { + float: right; +} + +.pager .previous > a, +.pager .previous > span { + float: left; +} + +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #999999; + cursor: not-allowed; + background-color: #ffffff; +} + +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #ffffff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + +.label[href]:hover, +.label[href]:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label:empty { + display: none; +} + +.label-default { + background-color: #999999; +} + +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #808080; +} + +.label-primary { + background-color: #428bca; +} + +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #3071a9; +} + +.label-success { + background-color: #5cb85c; +} + +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} + +.label-info { + background-color: #5bc0de; +} + +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} + +.label-warning { + background-color: #f0ad4e; +} + +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} + +.label-danger { + background-color: #d9534f; +} + +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} + +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #ffffff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; + border-radius: 10px; +} + +.badge:empty { + display: none; +} + +a.badge:hover, +a.badge:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.btn .badge { + position: relative; + top: -1px; +} + +a.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #428bca; + background-color: #ffffff; +} + +.nav-pills > li > a > .badge { + margin-left: 3px; +} + +.jumbotron { + padding: 30px; + margin-bottom: 30px; + font-size: 21px; + font-weight: 200; + line-height: 2.1428571435; + color: inherit; + background-color: #eeeeee; +} + +.jumbotron h1 { + line-height: 1; + color: inherit; +} + +.jumbotron p { + line-height: 1.4; +} + +.container .jumbotron { + border-radius: 6px; +} + +@media screen and (min-width: 768px) { + .jumbotron { + padding-top: 48px; + padding-bottom: 48px; + } + .container .jumbotron { + padding-right: 60px; + padding-left: 60px; + } + .jumbotron h1 { + font-size: 63px; + } +} + +.thumbnail { + display: inline-block; + display: block; + height: auto; + max-width: 100%; + padding: 4px; + line-height: 1.428571429; + background-color: #ffffff; + border: 1px solid #dddddd; + border-radius: 4px; + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +.thumbnail > img { + display: block; + height: auto; + max-width: 100%; +} + +a.thumbnail:hover, +a.thumbnail:focus { + border-color: #428bca; +} + +.thumbnail > img { + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #333333; +} + +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert h4 { + margin-top: 0; + color: inherit; +} + +.alert .alert-link { + font-weight: bold; +} + +.alert > p, +.alert > ul { + margin-bottom: 0; +} + +.alert > p + p { + margin-top: 5px; +} + +.alert-dismissable { + padding-right: 35px; +} + +.alert-dismissable .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} + +.alert-success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-success hr { + border-top-color: #c9e2b3; +} + +.alert-success .alert-link { + color: #356635; +} + +.alert-info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-info hr { + border-top-color: #a6e1ec; +} + +.alert-info .alert-link { + color: #2d6987; +} + +.alert-warning { + color: #c09853; + background-color: #fcf8e3; + border-color: #fbeed5; +} + +.alert-warning hr { + border-top-color: #f8e5be; +} + +.alert-warning .alert-link { + color: #a47e3c; +} + +.alert-danger { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-danger hr { + border-top-color: #e6c1c7; +} + +.alert-danger .alert-link { + color: #953b39; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + color: #ffffff; + text-align: center; + background-color: #428bca; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress-striped .progress-bar { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 40px 40px; +} + +.progress.active .progress-bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-bar-success { + background-color: #5cb85c; +} + +.progress-striped .progress-bar-success { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-bar-info { + background-color: #5bc0de; +} + +.progress-striped .progress-bar-info { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-bar-warning { + background-color: #f0ad4e; +} + +.progress-striped .progress-bar-warning { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-bar-danger { + background-color: #d9534f; +} + +.progress-striped .progress-bar-danger { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.media, +.media-body { + overflow: hidden; + zoom: 1; +} + +.media, +.media .media { + margin-top: 15px; +} + +.media:first-child { + margin-top: 0; +} + +.media-object { + display: block; +} + +.media-heading { + margin: 0 0 5px; +} + +.media > .pull-left { + margin-right: 10px; +} + +.media > .pull-right { + margin-left: 10px; +} + +.media-list { + padding-left: 0; + list-style: none; +} + +.list-group { + padding-left: 0; + margin-bottom: 20px; +} + +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #ffffff; + border: 1px solid #dddddd; +} + +.list-group-item:first-child { + border-top-right-radius: 4px; + border-top-left-radius: 4px; +} + +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} + +.list-group-item > .badge { + float: right; +} + +.list-group-item > .badge + .badge { + margin-right: 5px; +} + +a.list-group-item { + color: #555555; +} + +a.list-group-item .list-group-item-heading { + color: #333333; +} + +a.list-group-item:hover, +a.list-group-item:focus { + text-decoration: none; + background-color: #f5f5f5; +} + +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: #ffffff; + background-color: #428bca; + border-color: #428bca; +} + +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading { + color: inherit; +} + +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #e1edf7; +} + +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} + +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} + +.panel { + margin-bottom: 20px; + background-color: #ffffff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.panel-body { + padding: 15px; +} + +.panel-body:before, +.panel-body:after { + display: table; + content: " "; +} + +.panel-body:after { + clear: both; +} + +.panel-body:before, +.panel-body:after { + display: table; + content: " "; +} + +.panel-body:after { + clear: both; +} + +.panel > .list-group { + margin-bottom: 0; +} + +.panel > .list-group .list-group-item { + border-width: 1px 0; +} + +.panel > .list-group .list-group-item:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.panel > .list-group .list-group-item:last-child { + border-bottom: 0; +} + +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} + +.panel > .table { + margin-bottom: 0; +} + +.panel > .panel-body + .table { + border-top: 1px solid #dddddd; +} + +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} + +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; +} + +.panel-title > a { + color: inherit; +} + +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #dddddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} + +.panel-group .panel { + margin-bottom: 0; + overflow: hidden; + border-radius: 4px; +} + +.panel-group .panel + .panel { + margin-top: 5px; +} + +.panel-group .panel-heading { + border-bottom: 0; +} + +.panel-group .panel-heading + .panel-collapse .panel-body { + border-top: 1px solid #dddddd; +} + +.panel-group .panel-footer { + border-top: 0; +} + +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #dddddd; +} + +.panel-default { + border-color: #dddddd; +} + +.panel-default > .panel-heading { + color: #333333; + background-color: #f5f5f5; + border-color: #dddddd; +} + +.panel-default > .panel-heading + .panel-collapse .panel-body { + border-top-color: #dddddd; +} + +.panel-default > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #dddddd; +} + +.panel-primary { + border-color: #428bca; +} + +.panel-primary > .panel-heading { + color: #ffffff; + background-color: #428bca; + border-color: #428bca; +} + +.panel-primary > .panel-heading + .panel-collapse .panel-body { + border-top-color: #428bca; +} + +.panel-primary > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #428bca; +} + +.panel-success { + border-color: #d6e9c6; +} + +.panel-success > .panel-heading { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.panel-success > .panel-heading + .panel-collapse .panel-body { + border-top-color: #d6e9c6; +} + +.panel-success > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #d6e9c6; +} + +.panel-warning { + border-color: #fbeed5; +} + +.panel-warning > .panel-heading { + color: #c09853; + background-color: #fcf8e3; + border-color: #fbeed5; +} + +.panel-warning > .panel-heading + .panel-collapse .panel-body { + border-top-color: #fbeed5; +} + +.panel-warning > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #fbeed5; +} + +.panel-danger { + border-color: #eed3d7; +} + +.panel-danger > .panel-heading { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.panel-danger > .panel-heading + .panel-collapse .panel-body { + border-top-color: #eed3d7; +} + +.panel-danger > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #eed3d7; +} + +.panel-info { + border-color: #bce8f1; +} + +.panel-info > .panel-heading { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.panel-info > .panel-heading + .panel-collapse .panel-body { + border-top-color: #bce8f1; +} + +.panel-info > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #bce8f1; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-lg { + padding: 24px; + border-radius: 6px; +} + +.well-sm { + padding: 9px; + border-radius: 3px; +} + +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover, +.close:focus { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.5; + filter: alpha(opacity=50); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.modal-open { + overflow: hidden; +} + +body.modal-open, +.modal-open .navbar-fixed-top, +.modal-open .navbar-fixed-bottom { + margin-right: 15px; +} + +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + display: none; + overflow: auto; + overflow-y: scroll; +} + +.modal.fade .modal-dialog { + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + transform: translate(0, -25%); + -webkit-transition: -webkit-transform 0.3s ease-out; + -moz-transition: -moz-transform 0.3s ease-out; + -o-transition: -o-transform 0.3s ease-out; + transition: transform 0.3s ease-out; +} + +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); +} + +.modal-dialog { + z-index: 1050; + width: auto; + padding: 10px; + margin-right: auto; + margin-left: auto; +} + +.modal-content { + position: relative; + background-color: #ffffff; + border: 1px solid #999999; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + outline: none; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + background-clip: padding-box; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; + filter: alpha(opacity=0); +} + +.modal-backdrop.in { + opacity: 0.5; + filter: alpha(opacity=50); +} + +.modal-header { + min-height: 16.428571429px; + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} + +.modal-header .close { + margin-top: -2px; +} + +.modal-title { + margin: 0; + line-height: 1.428571429; +} + +.modal-body { + position: relative; + padding: 20px; +} + +.modal-footer { + padding: 19px 20px 20px; + margin-top: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} + +@media screen and (min-width: 768px) { + .modal-dialog { + right: auto; + left: 50%; + width: 600px; + padding-top: 30px; + padding-bottom: 30px; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + } +} + +.tooltip { + position: absolute; + z-index: 1030; + display: block; + font-size: 12px; + line-height: 1.4; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.9; + filter: alpha(opacity=90); +} + +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} + +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} + +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} + +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.top-left .tooltip-arrow { + bottom: 0; + left: 5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.top-right .tooltip-arrow { + right: 5px; + bottom: 0; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-right-color: #000000; + border-width: 5px 5px 5px 0; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-left-color: #000000; + border-width: 5px 0 5px 5px; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.tooltip.bottom-left .tooltip-arrow { + top: 0; + left: 5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.tooltip.bottom-right .tooltip-arrow { + top: 0; + right: 5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 276px; + padding: 1px; + text-align: left; + white-space: normal; + background-color: #ffffff; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + background-clip: padding-box; +} + +.popover.top { + margin-top: -10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-left: -10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +.popover .arrow, +.popover .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover .arrow { + border-width: 11px; +} + +.popover .arrow:after { + border-width: 10px; + content: ""; +} + +.popover.top .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999999; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} + +.popover.top .arrow:after { + bottom: 1px; + margin-left: -10px; + border-top-color: #ffffff; + border-bottom-width: 0; + content: " "; +} + +.popover.right .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999999; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} + +.popover.right .arrow:after { + bottom: -10px; + left: 1px; + border-right-color: #ffffff; + border-left-width: 0; + content: " "; +} + +.popover.bottom .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-bottom-color: #999999; + border-bottom-color: rgba(0, 0, 0, 0.25); + border-top-width: 0; +} + +.popover.bottom .arrow:after { + top: 1px; + margin-left: -10px; + border-bottom-color: #ffffff; + border-top-width: 0; + content: " "; +} + +.popover.left .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-left-color: #999999; + border-left-color: rgba(0, 0, 0, 0.25); + border-right-width: 0; +} + +.popover.left .arrow:after { + right: 1px; + bottom: -10px; + border-left-color: #ffffff; + border-right-width: 0; + content: " "; +} + +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + height: auto; + max-width: 100%; + line-height: 1; +} + +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} + +.carousel-inner > .active { + left: 0; +} + +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel-inner > .next { + left: 100%; +} + +.carousel-inner > .prev { + left: -100%; +} + +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} + +.carousel-inner > .active.left { + left: -100%; +} + +.carousel-inner > .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #ffffff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.left { + background-image: -webkit-gradient(linear, 0 top, 100% top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.0001))); + background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.5) 0), color-stop(rgba(0, 0, 0, 0.0001) 100%)); + background-image: -moz-linear-gradient(left, rgba(0, 0, 0, 0.5) 0, rgba(0, 0, 0, 0.0001) 100%); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0, rgba(0, 0, 0, 0.0001) 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); +} + +.carousel-control.right { + right: 0; + left: auto; + background-image: -webkit-gradient(linear, 0 top, 100% top, from(rgba(0, 0, 0, 0.0001)), to(rgba(0, 0, 0, 0.5))); + background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.0001) 0), color-stop(rgba(0, 0, 0, 0.5) 100%)); + background-image: -moz-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0, rgba(0, 0, 0, 0.5) 100%); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0, rgba(0, 0, 0, 0.5) 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); +} + +.carousel-control:hover, +.carousel-control:focus { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + left: 50%; + z-index: 5; + display: inline-block; +} + +.carousel-control .icon-prev, +.carousel-control .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + margin-left: -10px; + font-family: serif; +} + +.carousel-control .icon-prev:before { + content: '\2039'; +} + +.carousel-control .icon-next:before { + content: '\203a'; +} + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} + +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + border: 1px solid #ffffff; + border-radius: 10px; +} + +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #ffffff; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #ffffff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); +} + +.carousel-caption .btn { + text-shadow: none; +} + +@media screen and (min-width: 768px) { + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + margin-left: -15px; + font-size: 30px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} + +.clearfix:before, +.clearfix:after { + display: table; + content: " "; +} + +.clearfix:after { + clear: both; +} + +.pull-right { + float: right !important; +} + +.pull-left { + float: left !important; +} + +.hide { + display: none !important; +} + +.show { + display: block !important; +} + +.invisible { + visibility: hidden; +} + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.affix { + position: fixed; +} + +@-ms-viewport { + width: device-width; +} + +@media screen and (max-width: 400px) { + @-ms-viewport { + width: 320px; + } +} + +.hidden { + display: none !important; + visibility: hidden !important; +} + +.visible-xs { + display: none !important; +} + +tr.visible-xs { + display: none !important; +} + +th.visible-xs, +td.visible-xs { + display: none !important; +} + +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + tr.visible-xs { + display: table-row !important; + } + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-xs.visible-sm { + display: block !important; + } + tr.visible-xs.visible-sm { + display: table-row !important; + } + th.visible-xs.visible-sm, + td.visible-xs.visible-sm { + display: table-cell !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-xs.visible-md { + display: block !important; + } + tr.visible-xs.visible-md { + display: table-row !important; + } + th.visible-xs.visible-md, + td.visible-xs.visible-md { + display: table-cell !important; + } +} + +@media (min-width: 1200px) { + .visible-xs.visible-lg { + display: block !important; + } + tr.visible-xs.visible-lg { + display: table-row !important; + } + th.visible-xs.visible-lg, + td.visible-xs.visible-lg { + display: table-cell !important; + } +} + +.visible-sm { + display: none !important; +} + +tr.visible-sm { + display: none !important; +} + +th.visible-sm, +td.visible-sm { + display: none !important; +} + +@media (max-width: 767px) { + .visible-sm.visible-xs { + display: block !important; + } + tr.visible-sm.visible-xs { + display: table-row !important; + } + th.visible-sm.visible-xs, + td.visible-sm.visible-xs { + display: table-cell !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + tr.visible-sm { + display: table-row !important; + } + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-sm.visible-md { + display: block !important; + } + tr.visible-sm.visible-md { + display: table-row !important; + } + th.visible-sm.visible-md, + td.visible-sm.visible-md { + display: table-cell !important; + } +} + +@media (min-width: 1200px) { + .visible-sm.visible-lg { + display: block !important; + } + tr.visible-sm.visible-lg { + display: table-row !important; + } + th.visible-sm.visible-lg, + td.visible-sm.visible-lg { + display: table-cell !important; + } +} + +.visible-md { + display: none !important; +} + +tr.visible-md { + display: none !important; +} + +th.visible-md, +td.visible-md { + display: none !important; +} + +@media (max-width: 767px) { + .visible-md.visible-xs { + display: block !important; + } + tr.visible-md.visible-xs { + display: table-row !important; + } + th.visible-md.visible-xs, + td.visible-md.visible-xs { + display: table-cell !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-md.visible-sm { + display: block !important; + } + tr.visible-md.visible-sm { + display: table-row !important; + } + th.visible-md.visible-sm, + td.visible-md.visible-sm { + display: table-cell !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + tr.visible-md { + display: table-row !important; + } + th.visible-md, + td.visible-md { + display: table-cell !important; + } +} + +@media (min-width: 1200px) { + .visible-md.visible-lg { + display: block !important; + } + tr.visible-md.visible-lg { + display: table-row !important; + } + th.visible-md.visible-lg, + td.visible-md.visible-lg { + display: table-cell !important; + } +} + +.visible-lg { + display: none !important; +} + +tr.visible-lg { + display: none !important; +} + +th.visible-lg, +td.visible-lg { + display: none !important; +} + +@media (max-width: 767px) { + .visible-lg.visible-xs { + display: block !important; + } + tr.visible-lg.visible-xs { + display: table-row !important; + } + th.visible-lg.visible-xs, + td.visible-lg.visible-xs { + display: table-cell !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-lg.visible-sm { + display: block !important; + } + tr.visible-lg.visible-sm { + display: table-row !important; + } + th.visible-lg.visible-sm, + td.visible-lg.visible-sm { + display: table-cell !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-lg.visible-md { + display: block !important; + } + tr.visible-lg.visible-md { + display: table-row !important; + } + th.visible-lg.visible-md, + td.visible-lg.visible-md { + display: table-cell !important; + } +} + +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + tr.visible-lg { + display: table-row !important; + } + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } +} + +.hidden-xs { + display: block !important; +} + +tr.hidden-xs { + display: table-row !important; +} + +th.hidden-xs, +td.hidden-xs { + display: table-cell !important; +} + +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } + tr.hidden-xs { + display: none !important; + } + th.hidden-xs, + td.hidden-xs { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .hidden-xs.hidden-sm { + display: none !important; + } + tr.hidden-xs.hidden-sm { + display: none !important; + } + th.hidden-xs.hidden-sm, + td.hidden-xs.hidden-sm { + display: none !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-xs.hidden-md { + display: none !important; + } + tr.hidden-xs.hidden-md { + display: none !important; + } + th.hidden-xs.hidden-md, + td.hidden-xs.hidden-md { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-xs.hidden-lg { + display: none !important; + } + tr.hidden-xs.hidden-lg { + display: none !important; + } + th.hidden-xs.hidden-lg, + td.hidden-xs.hidden-lg { + display: none !important; + } +} + +.hidden-sm { + display: block !important; +} + +tr.hidden-sm { + display: table-row !important; +} + +th.hidden-sm, +td.hidden-sm { + display: table-cell !important; +} + +@media (max-width: 767px) { + .hidden-sm.hidden-xs { + display: none !important; + } + tr.hidden-sm.hidden-xs { + display: none !important; + } + th.hidden-sm.hidden-xs, + td.hidden-sm.hidden-xs { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } + tr.hidden-sm { + display: none !important; + } + th.hidden-sm, + td.hidden-sm { + display: none !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-sm.hidden-md { + display: none !important; + } + tr.hidden-sm.hidden-md { + display: none !important; + } + th.hidden-sm.hidden-md, + td.hidden-sm.hidden-md { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-sm.hidden-lg { + display: none !important; + } + tr.hidden-sm.hidden-lg { + display: none !important; + } + th.hidden-sm.hidden-lg, + td.hidden-sm.hidden-lg { + display: none !important; + } +} + +.hidden-md { + display: block !important; +} + +tr.hidden-md { + display: table-row !important; +} + +th.hidden-md, +td.hidden-md { + display: table-cell !important; +} + +@media (max-width: 767px) { + .hidden-md.hidden-xs { + display: none !important; + } + tr.hidden-md.hidden-xs { + display: none !important; + } + th.hidden-md.hidden-xs, + td.hidden-md.hidden-xs { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .hidden-md.hidden-sm { + display: none !important; + } + tr.hidden-md.hidden-sm { + display: none !important; + } + th.hidden-md.hidden-sm, + td.hidden-md.hidden-sm { + display: none !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } + tr.hidden-md { + display: none !important; + } + th.hidden-md, + td.hidden-md { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-md.hidden-lg { + display: none !important; + } + tr.hidden-md.hidden-lg { + display: none !important; + } + th.hidden-md.hidden-lg, + td.hidden-md.hidden-lg { + display: none !important; + } +} + +.hidden-lg { + display: block !important; +} + +tr.hidden-lg { + display: table-row !important; +} + +th.hidden-lg, +td.hidden-lg { + display: table-cell !important; +} + +@media (max-width: 767px) { + .hidden-lg.hidden-xs { + display: none !important; + } + tr.hidden-lg.hidden-xs { + display: none !important; + } + th.hidden-lg.hidden-xs, + td.hidden-lg.hidden-xs { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .hidden-lg.hidden-sm { + display: none !important; + } + tr.hidden-lg.hidden-sm { + display: none !important; + } + th.hidden-lg.hidden-sm, + td.hidden-lg.hidden-sm { + display: none !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-lg.hidden-md { + display: none !important; + } + tr.hidden-lg.hidden-md { + display: none !important; + } + th.hidden-lg.hidden-md, + td.hidden-lg.hidden-md { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } + tr.hidden-lg { + display: none !important; + } + th.hidden-lg, + td.hidden-lg { + display: none !important; + } +} + +.visible-print { + display: none !important; +} + +tr.visible-print { + display: none !important; +} + +th.visible-print, +td.visible-print { + display: none !important; +} + +@media print { + .visible-print { + display: block !important; + } + tr.visible-print { + display: table-row !important; + } + th.visible-print, + td.visible-print { + display: table-cell !important; + } + .hidden-print { + display: none !important; + } + tr.hidden-print { + display: none !important; + } + th.hidden-print, + td.hidden-print { + display: none !important; + } +} \ No newline at end of file diff --git a/framework/yii/bootstrap/assets/fonts/glyphicons-halflings-regular.eot b/framework/yii/bootstrap/assets/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 0000000..87eaa43 Binary files /dev/null and b/framework/yii/bootstrap/assets/fonts/glyphicons-halflings-regular.eot differ diff --git a/framework/yii/bootstrap/assets/fonts/glyphicons-halflings-regular.svg b/framework/yii/bootstrap/assets/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 0000000..5fee068 --- /dev/null +++ b/framework/yii/bootstrap/assets/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,228 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > +<svg xmlns="http://www.w3.org/2000/svg"> +<metadata></metadata> +<defs> +<font id="glyphicons_halflingsregular" horiz-adv-x="1200" > +<font-face units-per-em="1200" ascent="960" descent="-240" /> +<missing-glyph horiz-adv-x="500" /> +<glyph /> +<glyph /> +<glyph unicode=" " /> +<glyph unicode="*" d="M1100 500h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200z" /> +<glyph unicode="+" d="M1100 400h-400v-400h-300v400h-400v300h400v400h300v-400h400v-300z" /> +<glyph unicode=" " /> +<glyph unicode=" " horiz-adv-x="652" /> +<glyph unicode=" " horiz-adv-x="1304" /> +<glyph unicode=" " horiz-adv-x="652" /> +<glyph unicode=" " horiz-adv-x="1304" /> +<glyph unicode=" " horiz-adv-x="434" /> +<glyph unicode=" " horiz-adv-x="326" /> +<glyph unicode=" " horiz-adv-x="217" /> +<glyph unicode=" " horiz-adv-x="217" /> +<glyph unicode=" " horiz-adv-x="163" /> +<glyph unicode=" " horiz-adv-x="260" /> +<glyph unicode=" " horiz-adv-x="72" /> +<glyph unicode=" " horiz-adv-x="260" /> +<glyph unicode=" " horiz-adv-x="326" /> +<glyph unicode="€" d="M800 500h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257 q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406z" /> +<glyph unicode="−" d="M1100 700h-900v-300h900v300z" /> +<glyph unicode="☁" d="M178 300h750q120 0 205 86t85 208q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5q0 -80 56.5 -137t135.5 -57z" /> +<glyph unicode="✉" d="M1200 1100h-1200l600 -603zM300 600l-300 -300v600zM1200 900v-600l-300 300zM800 500l400 -400h-1200l400 400l200 -200z" /> +<glyph unicode="✏" d="M1101 889l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13l-94 -97zM401 189l614 614l-214 214l-614 -614zM-13 -13l333 112l-223 223z" /> +<glyph unicode="" horiz-adv-x="500" d="M0 0z" /> +<glyph unicode="" d="M700 100h300v-100h-800v100h300v550l-500 550h1200l-500 -550v-550z" /> +<glyph unicode="" d="M1000 934v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q17 -55 85.5 -75.5t147.5 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7q-79 -25 -122.5 -82t-25.5 -112t86 -75.5t147 5.5 q65 21 109 69t44 90v606z" /> +<glyph unicode="" d="M913 432l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342t142 342t342 142t342 -142t142 -342q0 -142 -78 -261zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" /> +<glyph unicode="" d="M649 949q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5t-94 124.5t-33.5 117.5q0 64 28 123t73 100.5t104.5 64t119 20.5 t120 -38.5t104.5 -104.5z" /> +<glyph unicode="" d="M791 522l145 -449l-384 275l-382 -275l146 447l-388 280h479l146 400h2l146 -400h472zM168 71l2 1z" /> +<glyph unicode="" d="M791 522l145 -449l-384 275l-382 -275l146 447l-388 280h479l146 400h2l146 -400h472zM747 331l-74 229l193 140h-235l-77 211l-78 -211h-239l196 -142l-73 -226l192 140zM168 71l2 1z" /> +<glyph unicode="" d="M1200 143v-143h-1200v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100z" /> +<glyph unicode="" d="M1200 1100v-1100h-1200v1100h1200zM200 1000h-100v-100h100v100zM900 1000h-600v-400h600v400zM1100 1000h-100v-100h100v100zM200 800h-100v-100h100v100zM1100 800h-100v-100h100v100zM200 600h-100v-100h100v100zM1100 600h-100v-100h100v100zM900 500h-600v-400h600 v400zM200 400h-100v-100h100v100zM1100 400h-100v-100h100v100zM200 200h-100v-100h100v100zM1100 200h-100v-100h100v100z" /> +<glyph unicode="" d="M500 1050v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5zM1100 1050v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h400 q21 0 35.5 -14.5t14.5 -35.5zM500 450v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5zM1100 450v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400 q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5z" /> +<glyph unicode="" d="M300 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM700 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5zM1100 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM300 650v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200 q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM700 650v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM1100 650v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM300 250v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM700 250v-200 q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM1100 250v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5 t14.5 -35.5z" /> +<glyph unicode="" d="M300 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM1200 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h700 q21 0 35.5 -14.5t14.5 -35.5zM300 450v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-200q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5zM1200 650v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v200 q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5zM300 250v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM1200 250v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700 q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5z" /> +<glyph unicode="" d="M448 34l818 820l-212 212l-607 -607l-206 207l-212 -212z" /> +<glyph unicode="" d="M882 106l-282 282l-282 -282l-212 212l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282z" /> +<glyph unicode="" d="M913 432l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342t142 342t342 142t342 -142t142 -342q0 -142 -78 -261zM507 363q137 0 233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5t-234 -97t-97 -233 t97 -233t234 -97zM600 800h100v-200h-100v-100h-200v100h-100v200h100v100h200v-100z" /> +<glyph unicode="" d="M913 432l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 299q-120 -77 -261 -77q-200 0 -342 142t-142 342t142 342t342 142t342 -142t142 -342q0 -141 -78 -262zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 801v-200h400v200h-400z" /> +<glyph unicode="" d="M700 750v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5zM800 975v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123 t-123 184t-45.5 224.5q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155z" /> +<glyph unicode="" d="M1200 1h-200v1200h200v-1200zM900 1h-200v800h200v-800zM600 1h-200v500h200v-500zM300 301h-200v-300h200v300z" /> +<glyph unicode="" d="M488 183l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5 q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39zM600 815q89 0 152 -63 t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152q0 88 63 151t152 63z" /> +<glyph unicode="" d="M900 1100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100zM800 1100v100h-300v-100h300zM200 900h900v-800q0 -41 -29.5 -71 t-70.5 -30h-700q-41 0 -70.5 30t-29.5 71v800zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" /> +<glyph unicode="" d="M1301 601h-200v-600h-300v400h-300v-400h-300v600h-200l656 644z" /> +<glyph unicode="" d="M600 700h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18v1150q0 11 7 18t18 7h475v-500zM1000 800h-300v300z" /> +<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM600 600h200 v-100h-300v400h100v-300z" /> +<glyph unicode="" d="M721 400h-242l-40 -400h-539l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538zM712 500l-27 300h-170l-27 -300h224z" /> +<glyph unicode="" d="M1100 400v-400h-1100v400h490l-290 300h200v500h300v-500h200l-290 -300h490zM988 300h-175v-100h175v100z" /> +<glyph unicode="" d="M600 1199q122 0 233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233t47.5 233t127.5 191t191 127.5t233 47.5zM600 1012q-170 0 -291 -121t-121 -291t121 -291t291 -121t291 121 t121 291t-121 291t-291 121zM700 600h150l-250 -300l-250 300h150v300h200v-300z" /> +<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM850 600h-150 v-300h-200v300h-150l250 300z" /> +<glyph unicode="" d="M0 500l200 700h800q199 -700 200 -700v-475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18v475zM903 1000h-606l-97 -500h200l50 -200h300l50 200h200z" /> +<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5zM797 598 l-297 -201v401z" /> +<glyph unicode="" d="M1177 600h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123t-123 -184t-45.5 -224.5t45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123 t123 184t45.5 224.5z" /> +<glyph unicode="" d="M700 800l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400zM500 400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122l-145 -145v400h400z" /> +<glyph unicode="" d="M100 1200v-1200h1100v1200h-1100zM1100 100h-900v900h900v-900zM400 800h-100v100h100v-100zM1000 800h-500v100h500v-100zM400 600h-100v100h100v-100zM1000 600h-500v100h500v-100zM400 400h-100v100h100v-100zM1000 400h-500v100h500v-100zM400 200h-100v100h100v-100 zM1000 300h-500v-100h500v100z" /> +<glyph unicode="" d="M200 0h-100v1100h100v-1100zM1100 600v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5z" /> +<glyph unicode="" d="M1200 275v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5t-49.5 -227v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50 q11 0 18 7t7 18zM400 480v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14zM1000 480v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14z" /> +<glyph unicode="" d="M0 800v-400h300l300 -200v800l-300 -200h-300zM971 600l141 -141l-71 -71l-141 141l-141 -141l-71 71l141 141l-141 141l71 71l141 -141l141 141l71 -71z" /> +<glyph unicode="" d="M0 800v-400h300l300 -200v800l-300 -200h-300zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" /> +<glyph unicode="" d="M974 186l6 8q142 178 142 405q0 230 -144 408l-6 8l-83 -64l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8zM300 801l300 200v-800l-300 200h-300v400h300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257z" /> +<glyph unicode="" d="M100 700h400v100h100v100h-100v300h-500v-600h100v100zM1200 700v500h-600v-200h100v-300h200v-300h300v200h-200v100h200zM100 1100h300v-300h-300v300zM800 800v300h300v-300h-300zM200 900h100v100h-100v-100zM900 1000h100v-100h-100v100zM300 600h-100v-100h-200 v-500h500v500h-200v100zM900 200v-100h-200v100h-100v100h100v200h-200v100h300v-300h200v-100h-100zM400 400v-300h-300v300h300zM300 200h-100v100h100v-100zM1100 300h100v-100h-100v100zM600 100h100v-100h-100v100zM1200 100v-100h-300v100h300z" /> +<glyph unicode="" d="M100 1200h-100v-1000h100v1000zM300 200h-100v1000h100v-1000zM700 200h-200v1000h200v-1000zM900 200h-100v1000h100v-1000zM1200 1200v-1000h-200v1000h200zM400 100v-100h-300v100h300zM500 91h100v-91h-100v91zM700 91h100v-91h-100v91zM1100 91v-91h-200v91h200z " /> +<glyph unicode="" d="M1200 500l-500 -500l-699 700v475q0 10 7.5 17.5t17.5 7.5h474zM320 882q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71t29 -71q30 -30 71.5 -30t71.5 30z" /> +<glyph unicode="" d="M1201 500l-500 -500l-699 700v475q0 11 7 18t18 7h474zM1501 500l-500 -500l-50 50l450 450l-700 700h100zM320 882q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71t30 -71q29 -30 71 -30t71 30z" /> +<glyph unicode="" d="M1200 1200v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900v1025l175 175h925z" /> +<glyph unicode="" d="M947 829l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18l-94 -346l40 -124h592zM1200 800v-700h-200v200h-800v-200h-200v700h200l100 -200h600l100 200h200zM881 176l38 -152q2 -10 -3.5 -17t-15.5 -7h-600q-10 0 -15.5 7t-3.5 17l38 152q2 10 11.5 17t19.5 7 h500q10 0 19.5 -7t11.5 -17z" /> +<glyph unicode="" d="M1200 0v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417zM416 521l178 457l46 -140l116 -317 h-340z" /> +<glyph unicode="" d="M100 1199h471q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111t-162 -38.5h-500v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21 t-29 14t-49 14.5v70zM400 1079v-379h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400z" /> +<glyph unicode="" d="M877 1200l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425z" /> +<glyph unicode="" d="M1150 1200h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49v300h150h700zM100 1000v-800h75l-125 -167l-125 167h75v800h-75l125 167 l125 -167h-75z" /> +<glyph unicode="" d="M950 1201h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50v300h150h700zM200 101h800v75l167 -125l-167 -125v75h-800v-75l-167 125l167 125 v-75z" /> +<glyph unicode="" d="M700 950v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35zM1100 650v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h1000 q21 0 35.5 15t14.5 35zM900 350v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35zM1200 50v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35 t35.5 -15h1100q21 0 35.5 15t14.5 35z" /> +<glyph unicode="" d="M1000 950v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35zM1200 650v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h1100 q21 0 35.5 15t14.5 35zM1000 350v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35zM1200 50v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35 t35.5 -15h1100q21 0 35.5 15t14.5 35z" /> +<glyph unicode="" d="M500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" /> +<glyph unicode="" d="M0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" /> +<glyph unicode="" d="M0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35zM0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" /> +<glyph unicode="" d="M400 1100h-100v-1100h100v1100zM700 950v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35zM1100 650v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15 h500q20 0 35 15t15 35zM100 425v75h-201v100h201v75l166 -125zM900 350v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35zM1200 50v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5 v-100q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35z" /> +<glyph unicode="" d="M201 950v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35zM801 1100h100v-1100h-100v1100zM601 650v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15 h500q20 0 35 15t15 35zM1101 425v75h200v100h-200v75l-167 -125zM401 350v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35zM701 50v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5 v-100q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35z" /> +<glyph unicode="" d="M900 925v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53zM1200 300l-300 300l300 300v-600z" /> +<glyph unicode="" d="M1200 1056v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31zM1100 1000h-1000v-737l247 182l298 -131l-74 156l293 318l236 -288v500zM476 750q0 -56 -39 -95t-95 -39t-95 39t-39 95t39 95t95 39t95 -39 t39 -95z" /> +<glyph unicode="" d="M600 1213q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262q0 124 60.5 231.5t165 172t226.5 64.5zM599 514q107 0 182.5 75.5t75.5 182.5t-75.5 182 t-182.5 75t-182 -75.5t-75 -181.5q0 -107 75.5 -182.5t181.5 -75.5z" /> +<glyph unicode="" d="M600 1199q122 0 233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233t47.5 233t127.5 191t191 127.5t233 47.5zM600 173v854q-176 0 -301.5 -125t-125.5 -302t125.5 -302t301.5 -125z " /> +<glyph unicode="" d="M554 1295q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 138.5t-64 210.5q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5zM455 296q-7 6 -18 17 t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156q14 -82 59.5 -136t136.5 -80z" /> +<glyph unicode="" d="M1108 902l113 113l-21 85l-92 28l-113 -113zM1100 625v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5 t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125zM436 341l161 50l412 412l-114 113l-405 -405z" /> +<glyph unicode="" d="M1100 453v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5z M813 431l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209z" /> +<glyph unicode="" d="M1100 569v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5v300q0 165 117.5 282.5t282.5 117.5h300q60 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69z M625 348l566 567l-136 137l-430 -431l-147 147l-136 -136z" /> +<glyph unicode="" d="M900 303v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198l-300 300l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296z" /> +<glyph unicode="" d="M900 0l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100z" /> +<glyph unicode="" d="M1200 0l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100z" /> +<glyph unicode="" d="M1200 0l-500 488v-488l-564 550l564 550v-487l500 487v-1100z" /> +<glyph unicode="" d="M1100 550l-900 550v-1100z" /> +<glyph unicode="" d="M500 150v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5zM900 150v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800q0 -21 14.5 -35.5t35.5 -14.5h200 q21 0 35.5 14.5t14.5 35.5z" /> +<glyph unicode="" d="M1100 150v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35z" /> +<glyph unicode="" d="M500 0v488l-500 -488v1100l500 -487v487l564 -550z" /> +<glyph unicode="" d="M1050 1100h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488l-500 -488v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M850 1100h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M650 1064l-550 -564h1100zM1200 350v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5z" /> +<glyph unicode="" d="M777 7l240 240l-353 353l353 353l-240 240l-592 -594z" /> +<glyph unicode="" d="M513 -46l-241 240l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1z" /> +<glyph unicode="" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM500 900v-200h-200v-200h200v-200h200v200h200v200h-200v200h-200z" /> +<glyph unicode="" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM300 700v-200h600v200h-600z" /> +<glyph unicode="" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM247 741l141 -141l-142 -141l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141 l-141 142z" /> +<glyph unicode="" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM546 623l-102 102l-174 -174l276 -277l411 411l-175 174z" /> +<glyph unicode="" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM500 500h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3 q-105 0 -172 -56t-67 -183h144q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5q19 0 30 -10t11 -26q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5zM500 400v-100h200v100h-200z" /> +<glyph unicode="" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM500 900v-100h200v100h-200zM400 700v-100h100v-200h-100v-100h400v100h-100v300h-300z" /> +<glyph unicode="" d="M1200 700v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194v200h194q15 60 36 104.5t55.5 86t88 69t126.5 40.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203zM700 500v-206q149 48 201 206h-201v200h200 q-25 74 -76 127.5t-124 76.5v-204h-200v203q-75 -24 -130 -77.5t-79 -125.5h209v-200h-210q24 -73 79.5 -127.5t130.5 -78.5v206h200z" /> +<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM844 735 l-135 -135l135 -135l-109 -109l-135 135l-135 -135l-109 109l135 135l-135 135l109 109l135 -135l135 135z" /> +<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM896 654 l-346 -345l-228 228l141 141l87 -87l204 205z" /> +<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM248 385l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5q0 -115 62 -215zM955 809l-564 -564q97 -59 209 -59q171 0 292.5 121.5 t121.5 292.5q0 112 -59 209z" /> +<glyph unicode="" d="M1200 400h-600v-301l-600 448l600 453v-300h600v-300z" /> +<glyph unicode="" d="M600 400h-600v300h600v300l600 -453l-600 -448v301z" /> +<glyph unicode="" d="M1098 600h-298v-600h-300v600h-296l450 600z" /> +<glyph unicode="" d="M998 600l-449 -600l-445 600h296v600h300v-600h298z" /> +<glyph unicode="" d="M600 199v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453z" /> +<glyph unicode="" d="M1200 1200h-400l129 -129l-294 -294l142 -142l294 294l129 -129v400zM565 423l-294 -294l129 -129h-400v400l129 -129l294 294z" /> +<glyph unicode="" d="M871 730l129 -130h-400v400l129 -129l295 295l142 -141zM200 600h400v-400l-129 130l-295 -295l-142 141l295 295z" /> +<glyph unicode="" d="M600 1177q118 0 224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5t45.5 224.5t123 184t184 123t224.5 45.5zM686 549l58 302q4 20 -8 34.5t-33 14.5h-207q-20 0 -32 -14.5t-8 -34.5 l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5t21.5 34.5zM700 400h-200v-100h200v100z" /> +<glyph unicode="" d="M1200 900h-111v6t-1 15t-3 18l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6h-111v-100h100v-200h400v300h200v-300h400v200h100v100z M731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269zM481 900h-281q-3 0 14 48t35 96l18 47zM100 0h400v400h-400v-400zM700 400h400v-400h-400v400z" /> +<glyph unicode="" d="M0 121l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55l-201 -202 v143zM692 611q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5t86.5 76.5q55 66 367 234z" /> +<glyph unicode="" d="M1261 600l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30l-26 40l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5 t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30zM600 240q64 0 123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212 q0 85 46 158q-102 -87 -226 -258q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5zM484 762l-107 -106q49 -124 154 -191l105 105q-37 24 -75 72t-57 84z" /> +<glyph unicode="" d="M906 1200l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43l-26 40l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148zM1261 600l-26 -40q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5 t-124 -100t-146.5 -79l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52zM513 264l37 141q-107 18 -178.5 101.5t-71.5 193.5q0 85 46 158q-102 -87 -226 -258q210 -282 393 -336z M484 762l-107 -106q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68z" /> +<glyph unicode="" d="M-47 0h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 33 -48 36t-48 -29l-642 -1066q-21 -32 -7.5 -66t50.5 -34zM700 200v100h-200v-100h-345l445 723l445 -723h-345zM700 700h-200v-100l100 -300l100 300v100z" /> +<glyph unicode="" d="M800 711l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -21 -13 -29t-32 1l-94 78h-222l-94 -78q-19 -9 -32 -1t-13 29v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5v41q0 20 11 44.5t26 38.5 l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339z" /> +<glyph unicode="" d="M941 800l-600 -600h-341v200h259l600 600h241v198l300 -295l-300 -300v197h-159zM381 678l141 142l-181 180h-341v-200h259zM1100 598l300 -295l-300 -300v197h-241l-181 181l141 142l122 -123h159v198z" /> +<glyph unicode="" d="M100 1100h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5z" /> +<glyph unicode="" d="M400 900h-300v300h300v-300zM1100 900h-300v300h300v-300zM1100 800v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5t-58 109.5t-31.5 116t-15 104t-3 83v200h300v-250q0 -113 6 -145 q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300z" /> +<glyph unicode="" d="M902 184l226 227l-578 579l-580 -579l227 -227l352 353z" /> +<glyph unicode="" d="M650 218l578 579l-226 227l-353 -353l-352 353l-227 -227z" /> +<glyph unicode="" d="M1198 400v600h-796l215 -200h381v-400h-198l299 -283l299 283h-200zM-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196z" /> +<glyph unicode="" d="M1050 1200h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35 q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43l-100 475q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5z" /> +<glyph unicode="" d="M1200 1000v-100h-1200v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500zM0 800h1200v-800h-1200v800z" /> +<glyph unicode="" d="M201 800l-200 -400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000zM1501 700l-300 -700h-1200l300 700h1200z" /> +<glyph unicode="" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" /> +<glyph unicode="" d="M900 303v197h-600v-197l-300 297l300 298v-198h600v198l300 -298z" /> +<glyph unicode="" d="M31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM100 300h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM900 200h-100v-100h100v100z M1100 200h-100v-100h100v100z" /> +<glyph unicode="" d="M1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35zM325 800l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35q-56 337 -56 351v250v5 q0 13 0.5 18.5t2.5 13t8 10.5t15 3h200zM-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5z" /> +<glyph unicode="" d="M445 1180l-45 -233l-224 78l78 -225l-233 -44l179 -156l-179 -155l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180z" /> +<glyph unicode="" d="M700 1200h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400q0 -75 100 -75h61q123 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5zM700 925l-50 -225h450 v-125l-250 -375h-214l-136 100h-100v375l150 212l100 213h50v-175zM0 800v-600h200v600h-200z" /> +<glyph unicode="" d="M700 0h-50q-27 0 -51 20t-38 48l-96 198l-145 196q-20 26 -20 63v400q0 75 100 75h61q123 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5zM200 400h-200v600h200 v-600zM700 275l-50 225h450v125l-250 375h-214l-136 -100h-100v-375l150 -212l100 -213h50v175z" /> +<glyph unicode="" d="M364 873l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341q-7 0 -90 81t-83 94v525q0 17 14 35.5t28 28.5zM408 792v-503 l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83zM208 200h-200v600h200v-600z" /> +<glyph unicode="" d="M475 1104l365 -230q7 -4 16.5 -10.5t26 -26t16.5 -36.5v-526q0 -13 -85.5 -93.5t-93.5 -80.5h-342q-15 0 -28.5 20t-19.5 41l-131 339h-106q-84 0 -139 39t-55 111t54 110t139 37h302l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6zM370 946 l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100h222q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l106 89v502l-342 237zM1199 201h-200v600h200v-600z" /> +<glyph unicode="" d="M1100 473v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90zM911 400h-503l-236 339 l83 86l183 -146q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6v7.5v7v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294zM1000 200v-200h-600v200h600z" /> +<glyph unicode="" d="M305 1104v200h600v-200h-600zM605 310l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15l-230 -362q-15 -31 7 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85l-1 -302q0 -84 38.5 -138t110.5 -54t111 55t39 139v106z M905 804v-294l-340 -130q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146l-83 86l237 339h503z" /> +<glyph unicode="" d="M603 1195q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5zM598 701h-298v-201h300l-2 -194l402 294l-402 298v-197z" /> +<glyph unicode="" d="M597 1195q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5zM200 600l400 -294v194h302v201h-300v197z" /> +<glyph unicode="" d="M603 1195q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5zM300 600h200v-300h200v300h200l-300 400z" /> +<glyph unicode="" d="M603 1195q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5zM500 900v-300h-200l300 -400l300 400h-200v300h-200z" /> +<glyph unicode="" d="M603 1195q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5zM627 1101q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6 q-15 -3 -45.5 0.5t-45.5 -2.5q-21 -7 -52 -26.5t-34 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60q12 0 23 -5.5t23 -15t20 -13.5q24 -12 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55 t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q102 -2 221 112q30 29 47 47t34.5 49t20.5 62q-14 9 -37 9.5t-36 7.5q-14 7 -49 15t-52 19q-9 0 -39.5 -0.5t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7 q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5 t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 41 1 44q31 -13 58.5 -14.5t39.5 3.5l11 4q6 36 -17 53.5t-64 28.5t-56 23q-19 -3 -37 0zM613 994q0 -18 8 -42.5t16.5 -44t9.5 -23.5q-9 2 -31 5t-36 5t-32 8t-30 14q3 12 16 30t16 25q10 -10 18.5 -10 t14 6t14.5 14.5t16 12.5z" /> +<glyph unicode="" horiz-adv-x="1220" d="M100 1196h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 1096h-200v-100h200v100zM100 796h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 696h-500v-100h500v100zM100 396h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 296h-300v-100h300v100z " /> +<glyph unicode="" d="M1100 1200v-100h-1000v100h1000zM150 1000h900l-350 -500v-300l-200 -200v500z" /> +<glyph unicode="" d="M329 729l142 142l-200 200l129 129h-400v-400l129 129zM1200 1200v-400l-129 129l-200 -200l-142 142l200 200l-129 129h400zM271 129l129 -129h-400v400l129 -129l200 200l142 -142zM1071 271l129 129v-400h-400l129 129l-200 200l142 142z" /> +<glyph unicode="" d="M596 1192q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM596 1010q-171 0 -292.5 -121.5t-121.5 -292.5q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5zM455 905 q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5t16 38.5t39 16.5zM708 821l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-14 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5 q0 32 20.5 56.5t51.5 29.5zM855 709q23 0 38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39q0 22 16 38t39 16zM345 709q23 0 39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39t15.5 38.5t38.5 15.5z" /> +<glyph unicode="" d="M649 54l-16 22q-90 125 -293 323q-71 70 -104.5 105.5t-77 89.5t-61 99t-17.5 91q0 131 98.5 229.5t230.5 98.5q143 0 241 -129q103 129 246 129q129 0 226 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-203 -198 -293 -323zM844 524l12 12 q64 62 97.5 97t64.5 79t31 72q0 71 -48 119t-105 48q-74 0 -132 -82l-118 -171l-114 174q-51 79 -123 79q-60 0 -109.5 -49t-49.5 -118q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203z" /> +<glyph unicode="" d="M476 406l19 -17l105 105l-212 212l389 389l247 -247l-95 -96l18 -18q46 -46 77 -99l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159q0 -93 66 -159zM123 193l141 -141q66 -66 159 -66q95 0 159 66 l283 283q66 66 66 159t-66 159l-141 141q-12 12 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159q0 -94 66 -160z" /> +<glyph unicode="" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM900 1000h-600v-700h600v700zM600 46q43 0 73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5t-73.5 -30.5t-30.5 -73.5 t30.5 -73.5t73.5 -30.5z" /> +<glyph unicode="" d="M700 1029v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5h139q5 -77 48.5 -126.5t117.5 -64.5v335l-27 7q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5 t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5zM600 755v274q-61 -8 -97.5 -37.5t-36.5 -102.5q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3zM700 548 v-311q170 18 170 151q0 64 -44 99.5t-126 60.5z" /> +<glyph unicode="" d="M866 300l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5t-30 142.5h-221v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5 t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -11 2.5 -24.5t5.5 -24t9.5 -26.5t10.5 -25t14 -27.5t14 -25.5t15.5 -27t13.5 -24h242v-100h-197q8 -50 -2.5 -115t-31.5 -94 q-41 -59 -99 -113q35 11 84 18t70 7q32 1 102 -16t104 -17q76 0 136 30z" /> +<glyph unicode="" d="M300 0l298 300h-198v900h-200v-900h-198zM900 1200l298 -300h-198v-900h-200v900h-198z" /> +<glyph unicode="" d="M400 300h198l-298 -300l-298 300h198v900h200v-900zM1000 1200v-500h-100v100h-100v-100h-100v500h300zM901 1100h-100v-200h100v200zM700 500h300v-200h-99v-100h-100v100h99v100h-200v100zM800 100h200v-100h-300v200h100v-100z" /> +<glyph unicode="" d="M400 300h198l-298 -300l-298 300h198v900h200v-900zM1000 1200v-200h-99v-100h-100v100h99v100h-200v100h300zM800 800h200v-100h-300v200h100v-100zM700 500h300v-500h-100v100h-100v-100h-100v500zM801 200h100v200h-100v-200z" /> +<glyph unicode="" d="M300 0l298 300h-198v900h-200v-900h-198zM900 1100h-100v100h200v-500h-100v400zM1100 500v-500h-100v100h-200v400h300zM1001 400h-100v-200h100v200z" /> +<glyph unicode="" d="M300 0l298 300h-198v900h-200v-900h-198zM1100 1200v-500h-100v100h-200v400h300zM1001 1100h-100v-200h100v200zM900 400h-100v100h200v-500h-100v400z" /> +<glyph unicode="" d="M300 0l298 300h-198v900h-200v-900h-198zM900 1000h-200v200h200v-200zM1000 700h-300v200h300v-200zM1100 400h-400v200h400v-200zM1200 100h-500v200h500v-200z" /> +<glyph unicode="" d="M300 0l298 300h-198v900h-200v-900h-198zM1200 1000h-500v200h500v-200zM1100 700h-400v200h400v-200zM1000 400h-300v200h300v-200zM900 100h-200v200h200v-200z" /> +<glyph unicode="" d="M400 1100h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5v300q0 165 117.5 282.5t282.5 117.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5 t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5z" /> +<glyph unicode="" d="M700 0h-300q-163 0 -281.5 117.5t-118.5 282.5v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5 t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5zM400 800v-500l333 250z" /> +<glyph unicode="" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM900 300v500q0 41 -29.5 70.5t-70.5 29.5h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5 t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5zM800 700h-500l250 -333z" /> +<glyph unicode="" d="M1100 700v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5zM900 300v500q0 41 -29.5 70.5t-70.5 29.5h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5 t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5zM550 733l-250 -333h500z" /> +<glyph unicode="" d="M500 1100h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200zM700 550l-400 -350v200h-300v300h300v200z" /> +<glyph unicode="" d="M403 2l9 -1q13 0 26 16l538 630q15 19 6 36q-8 18 -32 16h-300q1 4 78 219.5t79 227.5q2 17 -6 27l-8 8h-9q-16 0 -25 -15q-4 -5 -98.5 -111.5t-228 -257t-209.5 -238.5q-17 -19 -7 -40q10 -19 32 -19h302q-155 -438 -160 -458q-5 -21 4 -32z" /> +<glyph unicode="" d="M800 200h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185zM900 200v200h-300v300h300v200l400 -350z" /> +<glyph unicode="" d="M1200 700l-149 149l-342 -353l-213 213l353 342l-149 149h500v-500zM1022 571l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5v-300 q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98z" /> +<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM600 794 q80 0 137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137t57 137t137 57z" /> +<glyph unicode="" d="M700 800v400h-300v-400h-300l445 -500l450 500h-295zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" /> +<glyph unicode="" d="M400 700v-300h300v300h295l-445 500l-450 -500h300zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" /> +<glyph unicode="" d="M405 400l596 596l-154 155l-442 -442l-150 151l-155 -155zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" /> +<glyph unicode="" d="M409 1103l-97 97l-212 -212l97 -98zM650 861l-149 149l-212 -212l149 -149l-238 -248h700v699zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" /> +<glyph unicode="" d="M539 950l-149 -149l212 -212l149 148l248 -237v700h-699zM297 709l-97 -97l212 -212l98 97zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" /> +<glyph unicode="" d="M1200 1199v-1079l-475 272l-310 -393v416h-392zM1166 1148l-672 -712v-226z" /> +<glyph unicode="" d="M1100 1000v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1200h-100v-200h100v200z" /> +<glyph unicode="" d="M578 500h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120zM700 1200h-100v-200h100v200zM1300 538l-475 -476l-244 244l123 123l120 -120l353 352z" /> +<glyph unicode="" d="M529 500h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170zM700 1200h-100v-200h100v200zM1167 6l-170 170l-170 -170l-127 127l170 170l-170 170l127 127l170 -170l170 170l127 -128 l-170 -169l170 -170z" /> +<glyph unicode="" d="M700 500h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200zM700 1000h-100v200h100v-200zM1000 600h-200v-300h-200l300 -300l300 300h-200v300z" /> +<glyph unicode="" d="M602 500h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200zM700 1000h-100v200h100v-200zM1000 300h200l-300 300l-300 -300h200v-300h200v300z" /> +<glyph unicode="" d="M1200 900v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150h1200zM0 800v-550q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200zM100 500h400v-200h-400v200z" /> +<glyph unicode="" d="M500 1000h400v198l300 -298l-300 -298v198h-400v200zM100 800v200h100v-200h-100zM400 800h-100v200h100v-200zM700 300h-400v-198l-300 298l300 298v-198h400v-200zM800 500h100v-200h-100v200zM1000 500v-200h100v200h-100z" /> +<glyph unicode="" d="M1200 50v1106q0 31 -18 40.5t-44 -7.5l-276 -117q-25 -16 -43.5 -50.5t-18.5 -65.5v-359q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5zM550 1200l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447l-100 203v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300z" /> +<glyph unicode="" d="M1100 106v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394 q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5z" /> +<glyph unicode="" d="M675 1000l-100 100h-375l-100 -100h400l200 -200v-98l295 98h105v200h-425zM500 300v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5zM100 800h300v-200h-300v200zM700 565l400 133 v-163l-400 -133v163zM100 500h300v-200h-300v200zM805 300l295 98v-298h-425l-100 -100h-375l-100 100h400l200 200h105z" /> +<glyph unicode="" d="M179 1169l-162 -162q-1 -11 -0.5 -32.5t16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q16 17 13 40.5t-22 37.5l-192 136q-19 14 -45 12t-42 -19l-119 -118q-143 103 -267 227q-126 126 -227 268l118 118 q17 17 20 41.5t-11 44.5l-139 194q-14 19 -36.5 22t-40.5 -14z" /> +<glyph unicode="" d="M1200 712v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40t-53.5 -36.5t-31 -27.5l-9 -10v-200q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38 t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5zM800 650l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -15 -35.5t-35 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5 t30 -27.5t12 -24l1 -10v-50z" /> +<glyph unicode="" d="M175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250zM1200 100v-100h-1100v100h1100z" /> +<glyph unicode="" d="M600 1100h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300v1000q0 41 29.5 70.5t70.5 29.5zM1000 800h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300v700q0 41 29.5 70.5t70.5 29.5zM400 0v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400h300z" /> +<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM200 800v-300h200v-100h-200v-100h300v300h-200v100h200v100h-300zM800 800h-200v-500h200v100h100v300h-100 v100zM800 700v-300h-100v300h100z" /> +<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM400 600h-100v200h-100v-500h100v200h100v-200h100v500h-100v-200zM800 800h-200v-500h200v100h100v300h-100 v100zM800 700v-300h-100v300h100z" /> +<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM200 800v-500h300v100h-200v300h200v100h-300zM600 800v-500h300v100h-200v300h200v100h-300z" /> +<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM500 700l-300 -150l300 -150v300zM600 400l300 150l-300 150v-300z" /> +<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM900 800v-500h-700v500h700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM800 700h-130 q-38 0 -66.5 -43t-28.5 -108t27 -107t68 -42h130v300z" /> +<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM200 800v-300h200v-100h-200v-100h300v300h-200v100h200v100h-300zM800 300h100v500h-200v-100h100v-400z M601 300h100v100h-100v-100z" /> +<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM300 700v100h-100v-500h300v400h-200zM800 300h100v500h-200v-100h100v-400zM401 400h-100v200h100v-200z M601 300h100v100h-100v-100z" /> +<glyph unicode="" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM1000 900h-900v-700h900v700zM400 700h-200v100h300v-300h-99v-100h-100v100h99v200zM800 700h-100v100h200v-500h-100v400zM201 400h100v-100 h-100v100zM701 300h-100v100h100v-100z" /> +<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM800 700h-300 v-200h300v-100h-300l-100 100v200l100 100h300v-100z" /> +<glyph unicode="" d="M596 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM596 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM800 700v-100 h-100v100h-200v-100h200v-100h-200v-100h-100v400h300zM800 400h-100v100h100v-100z" /> +<glyph unicode="" d="M800 300h128q120 0 205 86t85 208q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5q0 -80 56.5 -137t135.5 -57h222v300h400v-300zM700 200h200l-300 -300 l-300 300h200v300h200v-300z" /> +<glyph unicode="" d="M600 714l403 -403q94 26 154.5 104t60.5 178q0 121 -85 207.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5q0 -80 56.5 -137t135.5 -57h8zM700 -100h-200v300h-200l300 300 l300 -300h-200v-300z" /> +<glyph unicode="" d="M700 200h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170l-270 -300h400v-155l-75 -45h350l-75 45v155z" /> +<glyph unicode="" d="M700 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -12t1 -11q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5 q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350z" /> +<glyph unicode="💼" d="M800 1000h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100zM500 1000h200v100h-200v-100zM1200 400v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5v200h1200z" /> +<glyph unicode="📅" d="M1100 900v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150h1100zM0 800v-750q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100zM100 600h100v-100h-100v100zM300 600h100v-100h-100v100z M500 600h100v-100h-100v100zM700 600h100v-100h-100v100zM900 600h100v-100h-100v100zM100 400h100v-100h-100v100zM300 400h100v-100h-100v100zM500 400h100v-100h-100v100zM700 400h100v-100h-100v100zM900 400h100v-100h-100v100zM100 200h100v-100h-100v100zM300 200 h100v-100h-100v100zM500 200h100v-100h-100v100zM700 200h100v-100h-100v100zM900 200h100v-100h-100v100z" /> +<glyph unicode="📌" d="M902 1185l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207l-380 -303l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15z" /> +<glyph unicode="📎" d="M518 119l69 -60l517 511q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163t35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84 t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -79.5 -17t-67.5 -51l-388 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23q38 0 53 -36q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348 q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256z" /> +<glyph unicode="📷" d="M1200 200v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5z M1000 700h-100v100h100v-100zM844 500q0 -100 -72 -172t-172 -72t-172 72t-72 172t72 172t172 72t172 -72t72 -172zM706 500q0 44 -31 75t-75 31t-75 -31t-31 -75t31 -75t75 -31t75 31t31 75z" /> +<glyph unicode="🔒" d="M900 800h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" /> +<glyph unicode="🔔" d="M1062 400h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-22 -9 -63 -23t-167.5 -37t-251.5 -23t-245.5 20.5t-178.5 41.5l-58 20q-18 7 -31 27.5t-13 40.5q0 21 13.5 35.5t33.5 14.5h17l118 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94 q20 0 29 -10.5t3 -29.5l-18 -37q83 -19 144 -82.5t76 -140.5l63 -327zM600 104q-54 0 -103 6q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6z" /> +<glyph unicode="🔖" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" /> +<glyph unicode="🔥" d="M400 755q2 -12 8 -41.5t8 -43t6 -39.5t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85t5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5 q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129 q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5z" /> +<glyph unicode="🔧" d="M948 778l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5t15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138z" /> +</font> +</defs></svg> \ No newline at end of file diff --git a/framework/yii/bootstrap/assets/fonts/glyphicons-halflings-regular.ttf b/framework/yii/bootstrap/assets/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000..be784dc Binary files /dev/null and b/framework/yii/bootstrap/assets/fonts/glyphicons-halflings-regular.ttf differ diff --git a/framework/yii/bootstrap/assets/fonts/glyphicons-halflings-regular.woff b/framework/yii/bootstrap/assets/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 0000000..2cc3e48 Binary files /dev/null and b/framework/yii/bootstrap/assets/fonts/glyphicons-halflings-regular.woff differ diff --git a/framework/yii/bootstrap/assets/js/bootstrap.js b/framework/yii/bootstrap/assets/js/bootstrap.js new file mode 100644 index 0000000..2c64257 --- /dev/null +++ b/framework/yii/bootstrap/assets/js/bootstrap.js @@ -0,0 +1,1999 @@ +/** +* bootstrap.js v3.0.0 by @fat and @mdo +* Copyright 2013 Twitter Inc. +* http://www.apache.org/licenses/LICENSE-2.0 +*/ +if (!jQuery) { throw new Error("Bootstrap requires jQuery") } + +/* ======================================================================== + * Bootstrap: transition.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#transitions + * ======================================================================== + * Copyright 2013 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + + function transitionEnd() { + var el = document.createElement('bootstrap') + + var transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd' + , 'MozTransition' : 'transitionend' + , 'OTransition' : 'oTransitionEnd otransitionend' + , 'transition' : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false, $el = this + $(this).one($.support.transition.end, function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } + + $(function () { + $.support.transition = transitionEnd() + }) + +}(window.jQuery); + +/* ======================================================================== + * Bootstrap: alert.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#alerts + * ======================================================================== + * Copyright 2013 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // ALERT CLASS DEFINITION + // ====================== + + var dismiss = '[data-dismiss="alert"]' + var Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = $(selector) + + if (e) e.preventDefault() + + if (!$parent.length) { + $parent = $this.hasClass('alert') ? $this : $this.parent() + } + + $parent.trigger(e = $.Event('close.bs.alert')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent.trigger('closed.bs.alert').remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent + .one($.support.transition.end, removeElement) + .emulateTransitionEnd(150) : + removeElement() + } + + + // ALERT PLUGIN DEFINITION + // ======================= + + var old = $.fn.alert + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.alert') + + if (!data) $this.data('bs.alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + // ALERT NO CONFLICT + // ================= + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + // ALERT DATA-API + // ============== + + $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) + +}(window.jQuery); + +/* ======================================================================== + * Bootstrap: button.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#buttons + * ======================================================================== + * Copyright 2013 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // BUTTON PUBLIC CLASS DEFINITION + // ============================== + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Button.DEFAULTS, options) + } + + Button.DEFAULTS = { + loadingText: 'loading...' + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + var $el = this.$element + var val = $el.is('input') ? 'val' : 'html' + var data = $el.data() + + state = state + 'Text' + + if (!data.resetText) $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout(function () { + state == 'loadingText' ? + $el.addClass(d).attr(d, d) : + $el.removeClass(d).removeAttr(d); + }, 0) + } + + Button.prototype.toggle = function () { + var $parent = this.$element.closest('[data-toggle="buttons"]') + + if ($parent.length) { + var $input = this.$element.find('input') + .prop('checked', !this.$element.hasClass('active')) + .trigger('change') + if ($input.prop('type') === 'radio') $parent.find('.active').removeClass('active') + } + + this.$element.toggleClass('active') + } + + + // BUTTON PLUGIN DEFINITION + // ======================== + + var old = $.fn.button + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.button') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.button', (data = new Button(this, options))) + + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.Constructor = Button + + + // BUTTON NO CONFLICT + // ================== + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + // BUTTON DATA-API + // =============== + + $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + e.preventDefault() + }) + +}(window.jQuery); + +/* ======================================================================== + * Bootstrap: carousel.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#carousel + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // CAROUSEL CLASS DEFINITION + // ========================= + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.paused = + this.sliding = + this.interval = + this.$active = + this.$items = null + + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.DEFAULTS = { + interval: 5000 + , pause: 'hover' + , wrap: true + } + + Carousel.prototype.cycle = function (e) { + e || (this.paused = false) + + this.interval && clearInterval(this.interval) + + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + + return this + } + + Carousel.prototype.getActiveIndex = function () { + this.$active = this.$element.find('.item.active') + this.$items = this.$active.parent().children() + + return this.$items.index(this.$active) + } + + Carousel.prototype.to = function (pos) { + var that = this + var activeIndex = this.getActiveIndex() + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) return this.$element.one('slid', function () { that.to(pos) }) + if (activeIndex == pos) return this.pause().cycle() + + return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) + } + + Carousel.prototype.pause = function (e) { + e || (this.paused = true) + + if (this.$element.find('.next, .prev').length && $.support.transition.end) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + + this.interval = clearInterval(this.interval) + + return this + } + + Carousel.prototype.next = function () { + if (this.sliding) return + return this.slide('next') + } + + Carousel.prototype.prev = function () { + if (this.sliding) return + return this.slide('prev') + } + + Carousel.prototype.slide = function (type, next) { + var $active = this.$element.find('.item.active') + var $next = next || $active[type]() + var isCycling = this.interval + var direction = type == 'next' ? 'left' : 'right' + var fallback = type == 'next' ? 'first' : 'last' + var that = this + + if (!$next.length) { + if (!this.options.wrap) return + $next = this.$element.find('.item')[fallback]() + } + + this.sliding = true + + isCycling && this.pause() + + var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction }) + + if ($next.hasClass('active')) return + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + this.$element.one('slid', function () { + var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) + $nextIndicator && $nextIndicator.addClass('active') + }) + } + + if ($.support.transition && this.$element.hasClass('slide')) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + $active + .one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid') }, 0) + }) + .emulateTransitionEnd(600) + } else { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid') + } + + isCycling && this.cycle() + + return this + } + + + // CAROUSEL PLUGIN DEFINITION + // ========================== + + var old = $.fn.carousel + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.carousel') + var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) + var action = typeof option == 'string' ? option : options.slide + + if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + $.fn.carousel.Constructor = Carousel + + + // CAROUSEL NO CONFLICT + // ==================== + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + + // CAROUSEL DATA-API + // ================= + + $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { + var $this = $(this), href + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + var options = $.extend({}, $target.data(), $this.data()) + var slideIndex = $this.attr('data-slide-to') + if (slideIndex) options.interval = false + + $target.carousel(options) + + if (slideIndex = $this.attr('data-slide-to')) { + $target.data('bs.carousel').to(slideIndex) + } + + e.preventDefault() + }) + + $(window).on('load', function () { + $('[data-ride="carousel"]').each(function () { + var $carousel = $(this) + $carousel.carousel($carousel.data()) + }) + }) + +}(window.jQuery); + +/* ======================================================================== + * Bootstrap: collapse.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#collapse + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // COLLAPSE PUBLIC CLASS DEFINITION + // ================================ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Collapse.DEFAULTS, options) + this.transitioning = null + + if (this.options.parent) this.$parent = $(this.options.parent) + if (this.options.toggle) this.toggle() + } + + Collapse.DEFAULTS = { + toggle: true + } + + Collapse.prototype.dimension = function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + Collapse.prototype.show = function () { + if (this.transitioning || this.$element.hasClass('in')) return + + var startEvent = $.Event('show.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var actives = this.$parent && this.$parent.find('> .panel > .in') + + if (actives && actives.length) { + var hasData = actives.data('bs.collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('bs.collapse', null) + } + + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + .addClass('collapsing') + [dimension](0) + + this.transitioning = 1 + + var complete = function () { + this.$element + .removeClass('collapsing') + .addClass('in') + [dimension]('auto') + this.transitioning = 0 + this.$element.trigger('shown.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + var scrollSize = $.camelCase(['scroll', dimension].join('-')) + + this.$element + .one($.support.transition.end, $.proxy(complete, this)) + .emulateTransitionEnd(350) + [dimension](this.$element[0][scrollSize]) + } + + Collapse.prototype.hide = function () { + if (this.transitioning || !this.$element.hasClass('in')) return + + var startEvent = $.Event('hide.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var dimension = this.dimension() + + this.$element + [dimension](this.$element[dimension]()) + [0].offsetHeight + + this.$element + .addClass('collapsing') + .removeClass('collapse') + .removeClass('in') + + this.transitioning = 1 + + var complete = function () { + this.transitioning = 0 + this.$element + .trigger('hidden.bs.collapse') + .removeClass('collapsing') + .addClass('collapse') + } + + if (!$.support.transition) return complete.call(this) + + this.$element + [dimension](0) + .one($.support.transition.end, $.proxy(complete, this)) + .emulateTransitionEnd(350) + } + + Collapse.prototype.toggle = function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + + // COLLAPSE PLUGIN DEFINITION + // ========================== + + var old = $.fn.collapse + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.collapse') + var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.Constructor = Collapse + + + // COLLAPSE NO CONFLICT + // ==================== + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + // COLLAPSE DATA-API + // ================= + + $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + var target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + var $target = $(target) + var data = $target.data('bs.collapse') + var option = data ? 'toggle' : $this.data() + var parent = $this.attr('data-parent') + var $parent = parent && $(parent) + + if (!data || !data.transitioning) { + if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed') + $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + } + + $target.collapse(option) + }) + +}(window.jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#dropdowns + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle=dropdown]' + var Dropdown = function (element) { + var $el = $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we we use a backdrop because click events don't delegate + $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus) + } + + $parent.trigger(e = $.Event('show.bs.dropdown')) + + if (e.isDefaultPrevented()) return + + $parent + .toggleClass('open') + .trigger('shown.bs.dropdown') + + $this.focus() + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27)/.test(e.keyCode)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) { + if (e.which == 27) $parent.find(toggle).focus() + return $this.click() + } + + var $items = $('[role=menu] li:not(.divider):visible a', $parent) + + if (!$items.length) return + + var index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index=0 + + $items.eq(index).focus() + } + + function clearMenus() { + $(backdrop).remove() + $(toggle).each(function (e) { + var $parent = getParent($(this)) + if (!$parent.hasClass('open')) return + $parent.trigger(e = $.Event('hide.bs.dropdown')) + if (e.isDefaultPrevented()) return + $parent.removeClass('open').trigger('hidden.bs.dropdown') + }) + } + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + var old = $.fn.dropdown + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('dropdown') + + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + +}(window.jQuery); + +/* ======================================================================== + * Bootstrap: modal.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#modals + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // MODAL CLASS DEFINITION + // ====================== + + var Modal = function (element, options) { + this.options = options + this.$element = $(element) + this.$backdrop = + this.isShown = null + + if (this.options.remote) this.$element.load(this.options.remote) + } + + Modal.DEFAULTS = { + backdrop: true + , keyboard: true + , show: true + } + + Modal.prototype.toggle = function (_relatedTarget) { + return this[!this.isShown ? 'show' : 'hide'](_relatedTarget) + } + + Modal.prototype.show = function (_relatedTarget) { + var that = this + var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.escape() + + this.$element.on('click.dismiss.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(document.body) // don't move modals dom position + } + + that.$element.show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element + .addClass('in') + .attr('aria-hidden', false) + + that.enforceFocus() + + var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) + + transition ? + that.$element.find('.modal-dialog') // wait for modal to slide in + .one($.support.transition.end, function () { + that.$element.focus().trigger(e) + }) + .emulateTransitionEnd(300) : + that.$element.focus().trigger(e) + }) + } + + Modal.prototype.hide = function (e) { + if (e) e.preventDefault() + + e = $.Event('hide.bs.modal') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + + $(document).off('focusin.bs.modal') + + this.$element + .removeClass('in') + .attr('aria-hidden', true) + .off('click.dismiss.modal') + + $.support.transition && this.$element.hasClass('fade') ? + this.$element + .one($.support.transition.end, $.proxy(this.hideModal, this)) + .emulateTransitionEnd(300) : + this.hideModal() + } + + Modal.prototype.enforceFocus = function () { + $(document) + .off('focusin.bs.modal') // guard against infinite focus loop + .on('focusin.bs.modal', $.proxy(function (e) { + if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { + this.$element.focus() + } + }, this)) + } + + Modal.prototype.escape = function () { + if (this.isShown && this.options.keyboard) { + this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) { + e.which == 27 && this.hide() + }, this)) + } else if (!this.isShown) { + this.$element.off('keyup.dismiss.bs.modal') + } + } + + Modal.prototype.hideModal = function () { + var that = this + this.$element.hide() + this.backdrop(function () { + that.removeBackdrop() + that.$element.trigger('hidden.bs.modal') + }) + } + + Modal.prototype.removeBackdrop = function () { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } + + Modal.prototype.backdrop = function (callback) { + var that = this + var animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') + .appendTo(document.body) + + this.$element.on('click.dismiss.modal', $.proxy(function (e) { + if (e.target !== e.currentTarget) return + this.options.backdrop == 'static' + ? this.$element[0].focus.call(this.$element[0]) + : this.hide.call(this) + }, this)) + + if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + + this.$backdrop.addClass('in') + + if (!callback) return + + doAnimate ? + this.$backdrop + .one($.support.transition.end, callback) + .emulateTransitionEnd(150) : + callback() + + } else if (!this.isShown && this.$backdrop) { + this.$backdrop.removeClass('in') + + $.support.transition && this.$element.hasClass('fade')? + this.$backdrop + .one($.support.transition.end, callback) + .emulateTransitionEnd(150) : + callback() + + } else if (callback) { + callback() + } + } + + + // MODAL PLUGIN DEFINITION + // ======================= + + var old = $.fn.modal + + $.fn.modal = function (option, _relatedTarget) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.modal') + var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data) $this.data('bs.modal', (data = new Modal(this, options))) + if (typeof option == 'string') data[option](_relatedTarget) + else if (options.show) data.show(_relatedTarget) + }) + } + + $.fn.modal.Constructor = Modal + + + // MODAL NO CONFLICT + // ================= + + $.fn.modal.noConflict = function () { + $.fn.modal = old + return this + } + + + // MODAL DATA-API + // ============== + + $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { + var $this = $(this) + var href = $this.attr('href') + var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7 + var option = $target.data('modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) + + e.preventDefault() + + $target + .modal(option, this) + .one('hide', function () { + $this.is(':visible') && $this.focus() + }) + }) + + $(document) + .on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') }) + .on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') }) + +}(window.jQuery); + +/* ======================================================================== + * Bootstrap: tooltip.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#tooltip + * Inspired by the original jQuery.tipsy by Jason Frame + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // TOOLTIP PUBLIC CLASS DEFINITION + // =============================== + + var Tooltip = function (element, options) { + this.type = + this.options = + this.enabled = + this.timeout = + this.hoverState = + this.$element = null + + this.init('tooltip', element, options) + } + + Tooltip.DEFAULTS = { + animation: true + , placement: 'top' + , selector: false + , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' + , trigger: 'hover focus' + , title: '' + , delay: 0 + , html: false + , container: false + } + + Tooltip.prototype.init = function (type, element, options) { + this.enabled = true + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + + var triggers = this.options.trigger.split(' ') + + for (var i = triggers.length; i--;) { + var trigger = triggers[i] + + if (trigger == 'click') { + this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) + } else if (trigger != 'manual') { + var eventIn = trigger == 'hover' ? 'mouseenter' : 'focus' + var eventOut = trigger == 'hover' ? 'mouseleave' : 'blur' + + this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) + } + } + + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + Tooltip.prototype.getDefaults = function () { + return Tooltip.DEFAULTS + } + + Tooltip.prototype.getOptions = function (options) { + options = $.extend({}, this.getDefaults(), this.$element.data(), options) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay + , hide: options.delay + } + } + + return options + } + + Tooltip.prototype.getDelegateOptions = function () { + var options = {} + var defaults = this.getDefaults() + + this._options && $.each(this._options, function (key, value) { + if (defaults[key] != value) options[key] = value + }) + + return options + } + + Tooltip.prototype.enter = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) + + clearTimeout(self.timeout) + + self.hoverState = 'in' + + if (!self.options.delay || !self.options.delay.show) return self.show() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + Tooltip.prototype.leave = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) + + clearTimeout(self.timeout) + + self.hoverState = 'out' + + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + Tooltip.prototype.show = function () { + var e = $.Event('show.bs.'+ this.type) + + if (this.hasContent() && this.enabled) { + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + + var $tip = this.tip() + + this.setContent() + + if (this.options.animation) $tip.addClass('fade') + + var placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + var autoToken = /\s?auto?\s?/i + var autoPlace = autoToken.test(placement) + if (autoPlace) placement = placement.replace(autoToken, '') || 'top' + + $tip + .detach() + .css({ top: 0, left: 0, display: 'block' }) + .addClass(placement) + + this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) + + var pos = this.getPosition() + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (autoPlace) { + var $parent = this.$element.parent() + + var orgPlacement = placement + var docScroll = document.documentElement.scrollTop || document.body.scrollTop + var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth() + var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight() + var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left + + placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' : + placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' : + placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' : + placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' : + placement + + $tip + .removeClass(orgPlacement) + .addClass(placement) + } + + var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) + + this.applyPlacement(calculatedOffset, placement) + this.$element.trigger('shown.bs.' + this.type) + } + } + + Tooltip.prototype.applyPlacement = function(offset, placement) { + var replace + var $tip = this.tip() + var width = $tip[0].offsetWidth + var height = $tip[0].offsetHeight + + // manually read margins because getBoundingClientRect includes difference + var marginTop = parseInt($tip.css('margin-top'), 10) + var marginLeft = parseInt($tip.css('margin-left'), 10) + + // we must check for NaN for ie 8/9 + if (isNaN(marginTop)) marginTop = 0 + if (isNaN(marginLeft)) marginLeft = 0 + + offset.top = offset.top + marginTop + offset.left = offset.left + marginLeft + + $tip + .offset(offset) + .addClass('in') + + // check to see if placing tip in new offset caused the tip to resize itself + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (placement == 'top' && actualHeight != height) { + replace = true + offset.top = offset.top + height - actualHeight + } + + if (/bottom|top/.test(placement)) { + var delta = 0 + + if (offset.left < 0) { + delta = offset.left * -2 + offset.left = 0 + + $tip.offset(offset) + + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + } + + this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') + } else { + this.replaceArrow(actualHeight - height, actualHeight, 'top') + } + + if (replace) $tip.offset(offset) + } + + Tooltip.prototype.replaceArrow = function(delta, dimension, position) { + this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + "%") : '') + } + + Tooltip.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + Tooltip.prototype.hide = function () { + var that = this + var $tip = this.tip() + var e = $.Event('hide.bs.' + this.type) + + function complete() { + if (that.hoverState != 'in') $tip.detach() + } + + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + + $tip.removeClass('in') + + $.support.transition && this.$tip.hasClass('fade') ? + $tip + .one($.support.transition.end, complete) + .emulateTransitionEnd(150) : + complete() + + this.$element.trigger('hidden.bs.' + this.type) + + return this + } + + Tooltip.prototype.fixTitle = function () { + var $e = this.$element + if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') + } + } + + Tooltip.prototype.hasContent = function () { + return this.getTitle() + } + + Tooltip.prototype.getPosition = function () { + var el = this.$element[0] + return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { + width: el.offsetWidth + , height: el.offsetHeight + }, this.$element.offset()) + } + + Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { + return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : + /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } + } + + Tooltip.prototype.getTitle = function () { + var title + var $e = this.$element + var o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + Tooltip.prototype.tip = function () { + return this.$tip = this.$tip || $(this.options.template) + } + + Tooltip.prototype.arrow = function () { + return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow') + } + + Tooltip.prototype.validate = function () { + if (!this.$element[0].parentNode) { + this.hide() + this.$element = null + this.options = null + } + } + + Tooltip.prototype.enable = function () { + this.enabled = true + } + + Tooltip.prototype.disable = function () { + this.enabled = false + } + + Tooltip.prototype.toggleEnabled = function () { + this.enabled = !this.enabled + } + + Tooltip.prototype.toggle = function (e) { + var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this + self.tip().hasClass('in') ? self.leave(self) : self.enter(self) + } + + Tooltip.prototype.destroy = function () { + this.hide().$element.off('.' + this.type).removeData('bs.' + this.type) + } + + + // TOOLTIP PLUGIN DEFINITION + // ========================= + + var old = $.fn.tooltip + + $.fn.tooltip = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.tooltip') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tooltip.Constructor = Tooltip + + + // TOOLTIP NO CONFLICT + // =================== + + $.fn.tooltip.noConflict = function () { + $.fn.tooltip = old + return this + } + +}(window.jQuery); + +/* ======================================================================== + * Bootstrap: popover.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#popovers + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // POPOVER PUBLIC CLASS DEFINITION + // =============================== + + var Popover = function (element, options) { + this.init('popover', element, options) + } + + if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') + + Popover.DEFAULTS = $.extend({} , $.fn.tooltip.Constructor.DEFAULTS, { + placement: 'right' + , trigger: 'click' + , content: '' + , template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' + }) + + + // NOTE: POPOVER EXTENDS tooltip.js + // ================================ + + Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) + + Popover.prototype.constructor = Popover + + Popover.prototype.getDefaults = function () { + return Popover.DEFAULTS + } + + Popover.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + var content = this.getContent() + + $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) + $tip.find('.popover-content')[this.options.html ? 'html' : 'text'](content) + + $tip.removeClass('fade top bottom left right in') + + // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do + // this manually by checking the contents. + if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() + } + + Popover.prototype.hasContent = function () { + return this.getTitle() || this.getContent() + } + + Popover.prototype.getContent = function () { + var $e = this.$element + var o = this.options + + return $e.attr('data-content') + || (typeof o.content == 'function' ? + o.content.call($e[0]) : + o.content) + } + + Popover.prototype.arrow = function () { + return this.$arrow = this.$arrow || this.tip().find('.arrow') + } + + Popover.prototype.tip = function () { + if (!this.$tip) this.$tip = $(this.options.template) + return this.$tip + } + + + // POPOVER PLUGIN DEFINITION + // ========================= + + var old = $.fn.popover + + $.fn.popover = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.popover') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.popover', (data = new Popover(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.popover.Constructor = Popover + + + // POPOVER NO CONFLICT + // =================== + + $.fn.popover.noConflict = function () { + $.fn.popover = old + return this + } + +}(window.jQuery); + +/* ======================================================================== + * Bootstrap: scrollspy.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#scrollspy + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // SCROLLSPY CLASS DEFINITION + // ========================== + + function ScrollSpy(element, options) { + var href + var process = $.proxy(this.process, this) + + this.$element = $(element).is('body') ? $(window) : $(element) + this.$body = $('body') + this.$scrollElement = this.$element.on('scroll.bs.scroll-spy.data-api', process) + this.options = $.extend({}, ScrollSpy.DEFAULTS, options) + this.selector = (this.options.target + || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + || '') + ' .nav li > a' + this.offsets = $([]) + this.targets = $([]) + this.activeTarget = null + + this.refresh() + this.process() + } + + ScrollSpy.DEFAULTS = { + offset: 10 + } + + ScrollSpy.prototype.refresh = function () { + var offsetMethod = this.$element[0] == window ? 'offset' : 'position' + + this.offsets = $([]) + this.targets = $([]) + + var self = this + var $targets = this.$body + .find(this.selector) + .map(function () { + var $el = $(this) + var href = $el.data('target') || $el.attr('href') + var $href = /^#\w/.test(href) && $(href) + + return ($href + && $href.length + && [[ $href[offsetMethod]().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]]) || null + }) + .sort(function (a, b) { return a[0] - b[0] }) + .each(function () { + self.offsets.push(this[0]) + self.targets.push(this[1]) + }) + } + + ScrollSpy.prototype.process = function () { + var scrollTop = this.$scrollElement.scrollTop() + this.options.offset + var scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight + var maxScroll = scrollHeight - this.$scrollElement.height() + var offsets = this.offsets + var targets = this.targets + var activeTarget = this.activeTarget + var i + + if (scrollTop >= maxScroll) { + return activeTarget != (i = targets.last()[0]) && this.activate(i) + } + + for (i = offsets.length; i--;) { + activeTarget != targets[i] + && scrollTop >= offsets[i] + && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) + && this.activate( targets[i] ) + } + } + + ScrollSpy.prototype.activate = function (target) { + this.activeTarget = target + + $(this.selector) + .parents('.active') + .removeClass('active') + + var selector = this.selector + + '[data-target="' + target + '"],' + + this.selector + '[href="' + target + '"]' + + var active = $(selector) + .parents('li') + .addClass('active') + + if (active.parent('.dropdown-menu').length) { + active = active + .closest('li.dropdown') + .addClass('active') + } + + active.trigger('activate') + } + + + // SCROLLSPY PLUGIN DEFINITION + // =========================== + + var old = $.fn.scrollspy + + $.fn.scrollspy = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.scrollspy') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.scrollspy.Constructor = ScrollSpy + + + // SCROLLSPY NO CONFLICT + // ===================== + + $.fn.scrollspy.noConflict = function () { + $.fn.scrollspy = old + return this + } + + + // SCROLLSPY DATA-API + // ================== + + $(window).on('load', function () { + $('[data-spy="scroll"]').each(function () { + var $spy = $(this) + $spy.scrollspy($spy.data()) + }) + }) + +}(window.jQuery); + +/* ======================================================================== + * Bootstrap: tab.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#tabs + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // TAB CLASS DEFINITION + // ==================== + + var Tab = function (element) { + this.element = $(element) + } + + Tab.prototype.show = function () { + var $this = this.element + var $ul = $this.closest('ul:not(.dropdown-menu)') + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + if ($this.parent('li').hasClass('active')) return + + var previous = $ul.find('.active:last a')[0] + var e = $.Event('show.bs.tab', { + relatedTarget: previous + }) + + $this.trigger(e) + + if (e.isDefaultPrevented()) return + + var $target = $(selector) + + this.activate($this.parent('li'), $ul) + this.activate($target, $target.parent(), function () { + $this.trigger({ + type: 'shown.bs.tab' + , relatedTarget: previous + }) + }) + } + + Tab.prototype.activate = function (element, container, callback) { + var $active = container.find('> .active') + var transition = callback + && $.support.transition + && $active.hasClass('fade') + + function next() { + $active + .removeClass('active') + .find('> .dropdown-menu > .active') + .removeClass('active') + + element.addClass('active') + + if (transition) { + element[0].offsetWidth // reflow for transition + element.addClass('in') + } else { + element.removeClass('fade') + } + + if (element.parent('.dropdown-menu')) { + element.closest('li.dropdown').addClass('active') + } + + callback && callback() + } + + transition ? + $active + .one($.support.transition.end, next) + .emulateTransitionEnd(150) : + next() + + $active.removeClass('in') + } + + + // TAB PLUGIN DEFINITION + // ===================== + + var old = $.fn.tab + + $.fn.tab = function ( option ) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.tab') + + if (!data) $this.data('bs.tab', (data = new Tab(this))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tab.Constructor = Tab + + + // TAB NO CONFLICT + // =============== + + $.fn.tab.noConflict = function () { + $.fn.tab = old + return this + } + + + // TAB DATA-API + // ============ + + $(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { + e.preventDefault() + $(this).tab('show') + }) + +}(window.jQuery); + +/* ======================================================================== + * Bootstrap: affix.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#affix + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // AFFIX CLASS DEFINITION + // ====================== + + var Affix = function (element, options) { + this.options = $.extend({}, Affix.DEFAULTS, options) + this.$window = $(window) + .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) + .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) + + this.$element = $(element) + this.affixed = + this.unpin = null + + this.checkPosition() + } + + Affix.RESET = 'affix affix-top affix-bottom' + + Affix.DEFAULTS = { + offset: 0 + } + + Affix.prototype.checkPositionWithEventLoop = function () { + setTimeout($.proxy(this.checkPosition, this), 1) + } + + Affix.prototype.checkPosition = function () { + if (!this.$element.is(':visible')) return + + var scrollHeight = $(document).height() + var scrollTop = this.$window.scrollTop() + var position = this.$element.offset() + var offset = this.options.offset + var offsetTop = offset.top + var offsetBottom = offset.bottom + + if (typeof offset != 'object') offsetBottom = offsetTop = offset + if (typeof offsetTop == 'function') offsetTop = offset.top() + if (typeof offsetBottom == 'function') offsetBottom = offset.bottom() + + var affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? false : + offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' : + offsetTop != null && (scrollTop <= offsetTop) ? 'top' : false + + if (this.affixed === affix) return + if (this.unpin) this.$element.css('top', '') + + this.affixed = affix + this.unpin = affix == 'bottom' ? position.top - scrollTop : null + + this.$element.removeClass(Affix.RESET).addClass('affix' + (affix ? '-' + affix : '')) + + if (affix == 'bottom') { + this.$element.offset({ top: document.body.offsetHeight - offsetBottom - this.$element.height() }) + } + } + + + // AFFIX PLUGIN DEFINITION + // ======================= + + var old = $.fn.affix + + $.fn.affix = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.affix') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.affix', (data = new Affix(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.affix.Constructor = Affix + + + // AFFIX NO CONFLICT + // ================= + + $.fn.affix.noConflict = function () { + $.fn.affix = old + return this + } + + + // AFFIX DATA-API + // ============== + + $(window).on('load', function () { + $('[data-spy="affix"]').each(function () { + var $spy = $(this) + var data = $spy.data() + + data.offset = data.offset || {} + + if (data.offsetBottom) data.offset.bottom = data.offsetBottom + if (data.offsetTop) data.offset.top = data.offsetTop + + $spy.affix(data) + }) + }) + +}(window.jQuery); diff --git a/yii/caching/ApcCache.php b/framework/yii/caching/ApcCache.php similarity index 89% rename from yii/caching/ApcCache.php rename to framework/yii/caching/ApcCache.php index 391851d..6c2754a 100644 --- a/yii/caching/ApcCache.php +++ b/framework/yii/caching/ApcCache.php @@ -21,10 +21,26 @@ namespace yii\caching; class ApcCache extends Cache { /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + $key = $this->buildKey($key); + return apc_exists($key); + } + + /** * Retrieves a value from cache with a specified key. * This is the implementation of the method declared in the parent class. * @param string $key a unique key identifying the cached value - * @return string the value stored in cache, false if the value is not in the cache or expired. + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. */ protected function getValue($key) { diff --git a/yii/caching/Cache.php b/framework/yii/caching/Cache.php similarity index 76% rename from yii/caching/Cache.php rename to framework/yii/caching/Cache.php index 78f2854..56c6d96 100644 --- a/yii/caching/Cache.php +++ b/framework/yii/caching/Cache.php @@ -7,7 +7,9 @@ namespace yii\caching; +use Yii; use yii\base\Component; +use yii\base\InvalidConfigException; use yii\helpers\StringHelper; /** @@ -45,17 +47,18 @@ use yii\helpers\StringHelper; * - [[deleteValue()]]: delete the value with the specified key from cache * - [[flushValues()]]: delete all values from cache * - * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ abstract class Cache extends Component implements \ArrayAccess { /** - * @var string a string prefixed to every cache key so that it is unique. Defaults to null, meaning using - * the value of [[Application::id]] as the key prefix. You may set this property to be an empty string + * @var string a string prefixed to every cache key so that it is unique. If not set, + * it will use a prefix generated from [[Application::id]]. You may set this property to be an empty string * if you don't want to use key prefix. It is recommended that you explicitly set this property to some * static value if the cached data needs to be shared among multiple applications. + * + * To ensure interoperability, only use alphanumeric characters should be used. */ public $keyPrefix; /** @@ -78,46 +81,42 @@ abstract class Cache extends Component implements \ArrayAccess { parent::init(); if ($this->keyPrefix === null) { - $this->keyPrefix = \Yii::$app->id; + $this->keyPrefix = substr(md5(Yii::$app->id), 0, 5); + } elseif (!ctype_alnum($this->keyPrefix)) { + throw new InvalidConfigException(get_class($this) . '::keyPrefix should only contain alphanumeric characters.'); } } /** * Builds a normalized cache key from a given key. * - * The generated key contains letters and digits only, and its length is no more than 32. - * * If the given key is a string containing alphanumeric characters only and no more than 32 characters, - * then the key will be returned back without change. Otherwise, a normalized key - * is generated by serializing the given key and applying MD5 hashing. + * then the key will be returned back prefixed with [[keyPrefix]]. Otherwise, a normalized key + * is generated by serializing the given key, applying MD5 hashing, and prefixing with [[keyPrefix]]. * - * The following example builds a cache key using three parameters: - * - * ~~~ - * $key = $cache->buildKey($className, $method, $id); - * ~~~ - * - * @param array|string $key the key to be normalized + * @param mixed $key the key to be normalized * @return string the generated cache key */ - public function buildKey($key) + protected function buildKey($key) { if (is_string($key)) { - return ctype_alnum($key) && StringHelper::strlen($key) <= 32 ? $key : md5($key); + $key = ctype_alnum($key) && StringHelper::strlen($key) <= 32 ? $key : md5($key); } else { - return md5(json_encode($key)); + $key = md5(json_encode($key)); } + return $this->keyPrefix . $key; } /** * Retrieves a value from cache with a specified key. - * @param string $key a key identifying the cached value + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. * @return mixed the value stored in cache, false if the value is not in the cache, expired, * or the dependency associated with the cached data has changed. */ public function get($key) { - $key = $this->keyPrefix . $this->buildKey($key); + $key = $this->buildKey($key); $value = $this->getValue($key); if ($value === false || $this->serializer === false) { return $value; @@ -126,7 +125,7 @@ abstract class Cache extends Component implements \ArrayAccess } else { $value = call_user_func($this->serializer[1], $value); } - if (is_array($value) && !($value[1] instanceof Dependency && $value[1]->getHasChanged())) { + if (is_array($value) && !($value[1] instanceof Dependency && $value[1]->getHasChanged($this))) { return $value[0]; } else { return false; @@ -134,20 +133,39 @@ abstract class Cache extends Component implements \ArrayAccess } /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * In case a cache does not support this feature natively, this method will try to simulate it + * but has no performance improvement over getting it. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + $key = $this->buildKey($key); + $value = $this->getValue($key); + return $value !== false; + } + + /** * Retrieves multiple values from cache with the specified keys. * Some caches (such as memcache, apc) allow retrieving multiple cached values at the same time, * which may improve the performance. In case a cache does not support this feature natively, * this method will try to simulate it. * @param array $keys list of keys identifying the cached values * @return array list of cached values corresponding to the specified keys. The array - * is returned in terms of (key,value) pairs. + * is returned in terms of (key, value) pairs. * If a value is not cached or expired, the corresponding array value will be false. */ public function mget($keys) { $keyMap = array(); foreach ($keys as $key) { - $keyMap[$key] = $this->keyPrefix . $this->buildKey($key); + $keyMap[$key] = $this->buildKey($key); } $values = $this->getValues(array_values($keyMap)); $results = array(); @@ -160,7 +178,7 @@ abstract class Cache extends Component implements \ArrayAccess $value = $this->serializer === null ? unserialize($values[$newKey]) : call_user_func($this->serializer[1], $values[$newKey]); - if (is_array($value) && !($value[1] instanceof Dependency && $value[1]->getHasChanged())) { + if (is_array($value) && !($value[1] instanceof Dependency && $value[1]->getHasChanged($this))) { $results[$key] = $value[0]; } } @@ -174,7 +192,8 @@ abstract class Cache extends Component implements \ArrayAccess * If the cache already contains such a key, the existing value and * expiration time will be replaced with the new ones, respectively. * - * @param string $key the key identifying the value to be cached + * @param mixed $key a key identifying the value to be cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. * @param mixed $value the value to be cached * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. * @param Dependency $dependency dependency of the cached item. If the dependency changes, @@ -185,21 +204,22 @@ abstract class Cache extends Component implements \ArrayAccess public function set($key, $value, $expire = 0, $dependency = null) { if ($dependency !== null && $this->serializer !== false) { - $dependency->evaluateDependency(); + $dependency->evaluateDependency($this); } if ($this->serializer === null) { $value = serialize(array($value, $dependency)); } elseif ($this->serializer !== false) { $value = call_user_func($this->serializer[0], array($value, $dependency)); } - $key = $this->keyPrefix . $this->buildKey($key); + $key = $this->buildKey($key); return $this->setValue($key, $value, $expire); } /** * Stores a value identified by a key into cache if the cache does not contain this key. * Nothing will be done if the cache already contains the key. - * @param string $key the key identifying the value to be cached + * @param mixed $key a key identifying the value to be cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. * @param mixed $value the value to be cached * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. * @param Dependency $dependency dependency of the cached item. If the dependency changes, @@ -210,25 +230,26 @@ abstract class Cache extends Component implements \ArrayAccess public function add($key, $value, $expire = 0, $dependency = null) { if ($dependency !== null && $this->serializer !== false) { - $dependency->evaluateDependency(); + $dependency->evaluateDependency($this); } if ($this->serializer === null) { $value = serialize(array($value, $dependency)); } elseif ($this->serializer !== false) { $value = call_user_func($this->serializer[0], array($value, $dependency)); } - $key = $this->keyPrefix . $this->buildKey($key); + $key = $this->buildKey($key); return $this->addValue($key, $value, $expire); } /** * Deletes a value with the specified key from cache - * @param string $key the key of the value to be deleted + * @param mixed $key a key identifying the value to be deleted from cache. This can be a simple string or + * a complex data structure consisting of factors representing the key. * @return boolean if no error happens during deletion */ public function delete($key) { - $key = $this->keyPrefix . $this->buildKey($key); + $key = $this->buildKey($key); return $this->deleteValue($key); } @@ -247,7 +268,7 @@ abstract class Cache extends Component implements \ArrayAccess * This method should be implemented by child classes to retrieve the data * from specific cache storage. * @param string $key a unique key identifying the cached value - * @return string the value stored in cache, false if the value is not in the cache or expired. + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. */ abstract protected function getValue($key); diff --git a/yii/caching/ChainedDependency.php b/framework/yii/caching/ChainedDependency.php similarity index 89% rename from yii/caching/ChainedDependency.php rename to framework/yii/caching/ChainedDependency.php index 7c7058e..d3b9953 100644 --- a/yii/caching/ChainedDependency.php +++ b/framework/yii/caching/ChainedDependency.php @@ -14,8 +14,6 @@ namespace yii\caching; * considered changed; When [[dependOnAll]] is false, if one of the dependencies has NOT changed, * this dependency is considered NOT changed. * - * @property boolean $hasChanged Whether the dependency is changed or not. - * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -48,20 +46,22 @@ class ChainedDependency extends Dependency /** * Evaluates the dependency by generating and saving the data related with dependency. + * @param Cache $cache the cache component that is currently evaluating this dependency */ - public function evaluateDependency() + public function evaluateDependency($cache) { foreach ($this->dependencies as $dependency) { - $dependency->evaluateDependency(); + $dependency->evaluateDependency($cache); } } /** * Generates the data needed to determine if dependency has been changed. * This method does nothing in this class. + * @param Cache $cache the cache component that is currently evaluating this dependency * @return mixed the data needed to determine if dependency has been changed. */ - protected function generateDependencyData() + protected function generateDependencyData($cache) { return null; } @@ -70,14 +70,15 @@ class ChainedDependency extends Dependency * Performs the actual dependency checking. * This method returns true if any of the dependency objects * reports a dependency change. + * @param Cache $cache the cache component that is currently evaluating this dependency * @return boolean whether the dependency is changed or not. */ - public function getHasChanged() + public function getHasChanged($cache) { foreach ($this->dependencies as $dependency) { - if ($this->dependOnAll && $dependency->getHasChanged()) { + if ($this->dependOnAll && $dependency->getHasChanged($cache)) { return true; - } elseif (!$this->dependOnAll && !$dependency->getHasChanged()) { + } elseif (!$this->dependOnAll && !$dependency->getHasChanged($cache)) { return false; } } diff --git a/yii/caching/DbCache.php b/framework/yii/caching/DbCache.php similarity index 84% rename from yii/caching/DbCache.php rename to framework/yii/caching/DbCache.php index dee8c7a..b7b6692 100644 --- a/yii/caching/DbCache.php +++ b/framework/yii/caching/DbCache.php @@ -89,10 +89,39 @@ class DbCache extends Cache } /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + $key = $this->buildKey($key); + + $query = new Query; + $query->select(array('COUNT(*)')) + ->from($this->cacheTable) + ->where('[[id]] = :id AND ([[expire]] = 0 OR [[expire]] >' . time() . ')', array(':id' => $key)); + if ($this->db->enableQueryCache) { + // temporarily disable and re-enable query caching + $this->db->enableQueryCache = false; + $result = $query->createCommand($this->db)->queryScalar(); + $this->db->enableQueryCache = true; + } else { + $result = $query->createCommand($this->db)->queryScalar(); + } + return $result > 0; + } + + /** * Retrieves a value from cache with a specified key. * This is the implementation of the method declared in the parent class. * @param string $key a unique key identifying the cached value - * @return string the value stored in cache, false if the value is not in the cache or expired. + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. */ protected function getValue($key) { @@ -170,7 +199,7 @@ class DbCache extends Cache } else { return $this->addValue($key, $value, $expire); } - } + } /** * Stores a value identified by a key into cache if the cache does not contain this key. diff --git a/yii/caching/DbDependency.php b/framework/yii/caching/DbDependency.php similarity index 87% rename from yii/caching/DbDependency.php rename to framework/yii/caching/DbDependency.php index 7d45223..0b559d7 100644 --- a/yii/caching/DbDependency.php +++ b/framework/yii/caching/DbDependency.php @@ -32,14 +32,14 @@ class DbDependency extends Dependency */ public $sql; /** - * @var array the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. + * @var array the parameters (name => value) to be bound to the SQL statement specified by [[sql]]. */ public $params; /** * 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 $params the parameters (name => value) to be bound to the SQL statement specified by [[sql]]. * @param array $config name-value pairs that will be used to initialize the object properties */ public function __construct($sql, $params = array(), $config = array()) @@ -52,10 +52,11 @@ 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 + * @param Cache $cache the cache component that is currently evaluating this dependency * @return mixed the data needed to determine if dependency has been changed. + * @throws InvalidConfigException if [[db]] is not a valid application component ID */ - protected function generateDependencyData() + protected function generateDependencyData($cache) { $db = Yii::$app->getComponent($this->db); if (!$db instanceof Connection) { @@ -65,10 +66,10 @@ class DbDependency extends Dependency if ($db->enableQueryCache) { // temporarily disable and re-enable query caching $db->enableQueryCache = false; - $result = $db->createCommand($this->sql, $this->params)->queryRow(); + $result = $db->createCommand($this->sql, $this->params)->queryOne(); $db->enableQueryCache = true; } else { - $result = $db->createCommand($this->sql, $this->params)->queryRow(); + $result = $db->createCommand($this->sql, $this->params)->queryOne(); } return $result; } diff --git a/yii/caching/Dependency.php b/framework/yii/caching/Dependency.php similarity index 56% rename from yii/caching/Dependency.php rename to framework/yii/caching/Dependency.php index d1428fc..91c0568 100644 --- a/yii/caching/Dependency.php +++ b/framework/yii/caching/Dependency.php @@ -13,8 +13,6 @@ namespace yii\caching; * Child classes should override its [[generateDependencyData()]] for generating * the actual dependency data. * - * @property boolean $hasChanged Whether the dependency has changed. - * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -25,28 +23,77 @@ abstract class Dependency extends \yii\base\Object * latest dependency data. */ public $data; + /** + * @var boolean whether this dependency is reusable or not. True value means that dependent + * data for this cache dependency will be generated only once per request. This allows you + * to use the same cache dependency for multiple separate cache calls while generating the same + * page without an overhead of re-evaluating dependency data each time. Defaults to false. + */ + public $reusable = false; + + /** + * @var array static storage of cached data for reusable dependencies. + */ + private static $_reusableData = array(); + /** + * @var string a unique hash value for this cache dependency. + */ + private $_hash; + /** * Evaluates the dependency by generating and saving the data related with dependency. * This method is invoked by cache before writing data into it. + * @param Cache $cache the cache component that is currently evaluating this dependency */ - public function evaluateDependency() + public function evaluateDependency($cache) { - $this->data = $this->generateDependencyData(); + if (!$this->reusable) { + $this->data = $this->generateDependencyData($cache); + } else { + if ($this->_hash === null) { + $this->_hash = sha1(serialize($this)); + } + if (!array_key_exists($this->_hash, self::$_reusableData)) { + self::$_reusableData[$this->_hash] = $this->generateDependencyData($cache); + } + $this->data = self::$_reusableData[$this->_hash]; + } } /** + * Returns a value indicating whether the dependency has changed. + * @param Cache $cache the cache component that is currently evaluating this dependency * @return boolean whether the dependency has changed. */ - public function getHasChanged() + public function getHasChanged($cache) + { + if (!$this->reusable) { + return $this->generateDependencyData($cache) !== $this->data; + } else { + if ($this->_hash === null) { + $this->_hash = sha1(serialize($this)); + } + if (!array_key_exists($this->_hash, self::$_reusableData)) { + self::$_reusableData[$this->_hash] = $this->generateDependencyData($cache); + } + return self::$_reusableData[$this->_hash] !== $this->data; + } + } + + /** + * Resets all cached data for reusable dependencies. + */ + public static function resetReusableData() { - return $this->generateDependencyData() !== $this->data; + self::$_reusableData = array(); } /** * Generates the data needed to determine if dependency has been changed. * Derived classes should override this method to generate the actual dependency data. + * @param Cache $cache the cache component that is currently evaluating this dependency * @return mixed the data needed to determine if dependency has been changed. */ - abstract protected function generateDependencyData(); + abstract protected function generateDependencyData($cache); } diff --git a/yii/caching/DummyCache.php b/framework/yii/caching/DummyCache.php similarity index 97% rename from yii/caching/DummyCache.php rename to framework/yii/caching/DummyCache.php index 359fa7c..8d900df 100644 --- a/yii/caching/DummyCache.php +++ b/framework/yii/caching/DummyCache.php @@ -24,7 +24,7 @@ class DummyCache extends Cache * Retrieves a value from cache with a specified key. * This is the implementation of the method declared in the parent class. * @param string $key a unique key identifying the cached value - * @return string the value stored in cache, false if the value is not in the cache or expired. + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. */ protected function getValue($key) { diff --git a/yii/caching/ExpressionDependency.php b/framework/yii/caching/ExpressionDependency.php similarity index 81% rename from yii/caching/ExpressionDependency.php rename to framework/yii/caching/ExpressionDependency.php index ec4736c..533c2ab 100644 --- a/yii/caching/ExpressionDependency.php +++ b/framework/yii/caching/ExpressionDependency.php @@ -24,35 +24,36 @@ class ExpressionDependency extends Dependency { /** * @var string the string representation of a PHP expression whose result is used to determine the dependency. - * A PHP expression can be any PHP code that has a value. To learn more about what an expression is, + * A PHP expression can be any PHP code that evaluates to a value. To learn more about what an expression is, * please refer to the [php manual](http://www.php.net/manual/en/language.expressions.php). */ public $expression; /** - * @var mixed custom data associated with this dependency. In [[expression]], you may compare the value of - * this property with the latest data to determine if the dependency has changed or not. + * @var mixed custom parameters associated with this dependency. You may get the value + * of this property in [[expression]] using `$this->params`. */ - public $data; + public $params; /** * Constructor. * @param string $expression the PHP expression whose result is used to determine the dependency. - * @param mixed $data the custom data associated with this dependency + * @param mixed $params the custom parameters associated with this dependency * @param array $config name-value pairs that will be used to initialize the object properties */ - public function __construct($expression = 'true', $data = null, $config = array()) + public function __construct($expression = 'true', $params = null, $config = array()) { $this->expression = $expression; - $this->data = $data; + $this->params = $params; parent::__construct($config); } /** * Generates the data needed to determine if dependency has been changed. * This method returns the result of the PHP expression. + * @param Cache $cache the cache component that is currently evaluating this dependency * @return mixed the data needed to determine if dependency has been changed. */ - protected function generateDependencyData() + protected function generateDependencyData($cache) { return eval("return {$this->expression};"); } diff --git a/yii/caching/FileCache.php b/framework/yii/caching/FileCache.php similarity index 77% rename from yii/caching/FileCache.php rename to framework/yii/caching/FileCache.php index cc1a871..32cdf58 100644 --- a/yii/caching/FileCache.php +++ b/framework/yii/caching/FileCache.php @@ -8,6 +8,7 @@ namespace yii\caching; use Yii; +use yii\helpers\FileHelper; /** * FileCache implements a cache component using files. @@ -25,8 +26,9 @@ class FileCache extends Cache { /** * @var string the directory to store cache files. You may use path alias here. + * If not set, it will use the "cache" subdirectory under the application runtime path. */ - public $cachePath = '@app/runtime/cache'; + public $cachePath = '@runtime/cache'; /** * @var string cache file suffix. Defaults to '.bin'. */ @@ -44,6 +46,20 @@ class FileCache extends Cache * This number should be between 0 and 1000000. A value 0 means no GC will be performed at all. **/ public $gcProbability = 10; + /** + * @var integer the permission to be set for newly created cache files. + * This value will be used by PHP chmod() function. No umask will be applied. + * If not set, the permission will be determined by the current environment. + */ + public $fileMode; + /** + * @var integer the permission to be set for newly created directories. + * This value will be used by PHP chmod() function. No umask will be applied. + * Defaults to 0775, meaning the directory is read-writable by owner and group, + * but read-only for other users. + */ + public $dirMode = 0775; + /** * Initializes this component by ensuring the existence of the cache path. @@ -53,20 +69,36 @@ class FileCache extends Cache parent::init(); $this->cachePath = Yii::getAlias($this->cachePath); if (!is_dir($this->cachePath)) { - mkdir($this->cachePath, 0777, true); + FileHelper::createDirectory($this->cachePath, $this->dirMode, true); } } /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + $cacheFile = $this->getCacheFile($this->buildKey($key)); + return @filemtime($cacheFile) > time(); + } + + /** * Retrieves a value from cache with a specified key. * This is the implementation of the method declared in the parent class. * @param string $key a unique key identifying the cached value - * @return string the value stored in cache, false if the value is not in the cache or expired. + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. */ protected function getValue($key) { $cacheFile = $this->getCacheFile($key); - if (($time = @filemtime($cacheFile)) > time()) { + if (@filemtime($cacheFile) > time()) { return @file_get_contents($cacheFile); } else { return false; @@ -91,10 +123,12 @@ class FileCache extends Cache $cacheFile = $this->getCacheFile($key); if ($this->directoryLevel > 0) { - @mkdir(dirname($cacheFile), 0777, true); + @FileHelper::createDirectory(dirname($cacheFile), $this->dirMode, true); } if (@file_put_contents($cacheFile, $value, LOCK_EX) !== false) { - @chmod($cacheFile, 0777); + if ($this->fileMode !== null) { + @chmod($cacheFile, $this->fileMode); + } return @touch($cacheFile, $expire); } else { return false; diff --git a/yii/caching/FileDependency.php b/framework/yii/caching/FileDependency.php similarity index 95% rename from yii/caching/FileDependency.php rename to framework/yii/caching/FileDependency.php index 3797dde..bcc48a8 100644 --- a/yii/caching/FileDependency.php +++ b/framework/yii/caching/FileDependency.php @@ -38,9 +38,10 @@ class FileDependency extends Dependency /** * Generates the data needed to determine if dependency has been changed. * This method returns the file's last modification time. + * @param Cache $cache the cache component that is currently evaluating this dependency * @return mixed the data needed to determine if dependency has been changed. */ - protected function generateDependencyData() + protected function generateDependencyData($cache) { return @filemtime($this->fileName); } diff --git a/framework/yii/caching/GroupDependency.php b/framework/yii/caching/GroupDependency.php new file mode 100644 index 0000000..d63cee4 --- /dev/null +++ b/framework/yii/caching/GroupDependency.php @@ -0,0 +1,75 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\caching; + +/** + * GroupDependency marks a cached data item with a group name. + * + * You may invalidate the cached data items with the same group name all at once + * by calling [[invalidate()]]. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class GroupDependency extends Dependency +{ + /** + * @var string the group name + */ + public $group; + + /** + * Constructor. + * @param string $group the group name + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($group, $config = array()) + { + $this->group = $group; + parent::__construct($config); + } + + /** + * Generates the data needed to determine if dependency has been changed. + * This method does nothing in this class. + * @param Cache $cache the cache component that is currently evaluating this dependency + * @return mixed the data needed to determine if dependency has been changed. + */ + protected function generateDependencyData($cache) + { + $version = $cache->get(array(__CLASS__, $this->group)); + if ($version === false) { + $version = $this->invalidate($cache, $this->group); + } + return $version; + } + + /** + * Performs the actual dependency checking. + * @param Cache $cache the cache component that is currently evaluating this dependency + * @return boolean whether the dependency is changed or not. + */ + public function getHasChanged($cache) + { + $version = $cache->get(array(__CLASS__, $this->group)); + return $version === false || $version !== $this->data; + } + + /** + * Invalidates all of the cached data items that have the same [[group]]. + * @param Cache $cache the cache component that caches the data items + * @param string $group the group name + * @return string the current version number + */ + public static function invalidate($cache, $group) + { + $version = microtime(); + $cache->set(array(__CLASS__, $group), $version); + return $version; + } +} diff --git a/yii/caching/MemCache.php b/framework/yii/caching/MemCache.php similarity index 70% rename from yii/caching/MemCache.php rename to framework/yii/caching/MemCache.php index 20aff21..69a90b4 100644 --- a/yii/caching/MemCache.php +++ b/framework/yii/caching/MemCache.php @@ -7,7 +7,6 @@ namespace yii\caching; -use yii\base\Exception; use yii\base\InvalidConfigException; /** @@ -21,7 +20,7 @@ use yii\base\InvalidConfigException; * MemCache can be configured with a list of memcache servers by settings its [[servers]] property. * By default, MemCache assumes there is a memcache server running on localhost at port 11211. * - * See [[Cache]] for common cache operations that ApcCache supports. + * See [[Cache]] for common cache operations that MemCache supports. * * Note, there is no security measure to protected data in memcache. * All data in memcache can be accessed by any process running in the system. @@ -30,19 +29,19 @@ use yii\base\InvalidConfigException; * * ~~~ * array( - * 'components'=>array( - * 'cache'=>array( - * 'class'=>'MemCache', - * 'servers'=>array( + * 'components' => array( + * 'cache' => array( + * 'class' => 'MemCache', + * 'servers' => array( * array( - * 'host'=>'server1', - * 'port'=>11211, - * 'weight'=>60, + * 'host' => 'server1', + * 'port' => 11211, + * 'weight' => 60, * ), * array( - * 'host'=>'server2', - * 'port'=>11211, - * 'weight'=>40, + * 'host' => 'server2', + * 'port' => 11211, + * 'weight' => 40, * ), * ), * ), @@ -53,8 +52,10 @@ use yii\base\InvalidConfigException; * In the above, two memcache servers are used: server1 and server2. You can configure more properties of * each server, such as `persistent`, `weight`, `timeout`. Please see [[MemCacheServer]] for available options. * - * @property \Memcache|\Memcached $memCache The memcache instance (or memcached if [[useMemcached]] is true) used by this component. - * @property MemCacheServer[] $servers List of memcache server configurations. + * @property \Memcache|\Memcached $memcache The memcache (or memcached) object used by this cache component. + * This property is read-only. + * @property MemCacheServer[] $servers List of memcache server configurations. Note that the type of this + * property differs in getter and setter. See [[getServers()]] and [[setServers()]] for details. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -86,20 +87,38 @@ class MemCache extends Cache parent::init(); $servers = $this->getServers(); $cache = $this->getMemCache(); - if (count($servers)) { + if (empty($servers)) { + $cache->addServer('127.0.0.1', 11211); + } else { + if (!$this->useMemcached) { + // different version of memcache may have different number of parameters for the addServer method. + $class = new \ReflectionClass($cache); + $paramCount = $class->getMethod('addServer')->getNumberOfParameters(); + } foreach ($servers as $server) { if ($server->host === null) { - throw new Exception("The 'host' property must be specified for every memcache server."); + throw new InvalidConfigException("The 'host' property must be specified for every memcache server."); } if ($this->useMemcached) { $cache->addServer($server->host, $server->port, $server->weight); } else { - $cache->addServer($server->host, $server->port, $server->persistent, - $server->weight, $server->timeout, $server->retryInterval, $server->status); + // $timeout is used for memcache versions that do not have timeoutms parameter + $timeout = (int) ($server->timeout / 1000) + (($server->timeout % 1000 > 0) ? 1 : 0); + if ($paramCount === 9) { + $cache->addServer( + $server->host, $server->port, $server->persistent, + $server->weight, $timeout, $server->retryInterval, + $server->status, $server->failureCallback, $server->timeout + ); + } else { + $cache->addServer( + $server->host, $server->port, $server->persistent, + $server->weight, $timeout, $server->retryInterval, + $server->status, $server->failureCallback + ); + } } } - } else { - $cache->addServer('127.0.0.1', 11211); } } @@ -145,7 +164,7 @@ class MemCache extends Cache * Retrieves a value from cache with a specified key. * This is the implementation of the method declared in the parent class. * @param string $key a unique key identifying the cached value - * @return string the value stored in cache, false if the value is not in the cache or expired. + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. */ protected function getValue($key) { diff --git a/yii/caching/MemCacheServer.php b/framework/yii/caching/MemCacheServer.php similarity index 85% rename from yii/caching/MemCacheServer.php rename to framework/yii/caching/MemCacheServer.php index dc0de08..b1b7727 100644 --- a/yii/caching/MemCacheServer.php +++ b/framework/yii/caching/MemCacheServer.php @@ -35,9 +35,11 @@ class MemCacheServer extends \yii\base\Object */ public $persistent = true; /** - * @var integer value in seconds which will be used for connecting to the server. This is used by memcache only. + * @var integer timeout in milliseconds which will be used for connecting to the server. + * This is used by memcache only. For old versions of memcache that only support specifying + * timeout in seconds this will be rounded up to full seconds. */ - public $timeout = 15; + public $timeout = 1000; /** * @var integer how often a failed server will be retried (in seconds). This is used by memcache only. */ @@ -46,4 +48,11 @@ class MemCacheServer extends \yii\base\Object * @var boolean if the server should be flagged as online upon a failure. This is used by memcache only. */ public $status = true; + /** + * @var \Closure this callback function will run upon encountering an error. + * The callback is run before fail over is attempted. The function takes two parameters, + * the [[host]] and the [[port]] of the failed server. + * This is used by memcache only. + */ + public $failureCallback; } diff --git a/framework/yii/caching/RedisCache.php b/framework/yii/caching/RedisCache.php new file mode 100644 index 0000000..7cb6451 --- /dev/null +++ b/framework/yii/caching/RedisCache.php @@ -0,0 +1,213 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\caching; + +use yii\redis\Connection; + +/** + * RedisCache implements a cache application component based on [redis](http://redis.io/) version 2.6 or higher. + * + * RedisCache needs to be configured with [[hostname]], [[port]] and [[database]] of the server + * to connect to. By default RedisCache assumes there is a redis server running on localhost at + * port 6379 and uses the database number 0. + * + * RedisCache also supports [the AUTH command](http://redis.io/commands/auth) of redis. + * When the server needs authentication, you can set the [[password]] property to + * authenticate with the server after connect. + * + * See [[Cache]] manual for common cache operations that RedisCache supports. + * Unlike the [[CCache]], RedisCache allows the expire parameter of + * [[set]] and [[add]] to be a floating point number, so you may specify the time in milliseconds. + * + * To use RedisCache as the cache application component, configure the application as follows, + * + * ~~~ + * array( + * 'components'=>array( + * 'cache'=>array( + * 'class'=>'RedisCache', + * 'hostname'=>'localhost', + * 'port'=>6379, + * 'database'=>0, + * ), + * ), + * ) + * ~~~ + * + * @property Connection $connection The redis connection object. This property is read-only. + * + * @author Carsten Brandt <mail@cebe.cc> + * @since 2.0 + */ +class RedisCache extends Cache +{ + /** + * @var string hostname to use for connecting to the redis server. Defaults to 'localhost'. + */ + public $hostname = 'localhost'; + /** + * @var int the port to use for connecting to the redis server. Default port is 6379. + */ + public $port = 6379; + /** + * @var string the password to use to authenticate with the redis server. If not set, no AUTH command will be sent. + */ + public $password; + /** + * @var int the redis database to use. This is an integer value starting from 0. Defaults to 0. + */ + public $database = 0; + /** + * @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout") + */ + public $connectionTimeout = null; + /** + * @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used. + */ + public $dataTimeout = null; + /** + * @var Connection the redis connection + */ + private $_connection; + + + /** + * Initializes the cache component by establishing a connection to the redis server. + */ + public function init() + { + parent::init(); + $this->getConnection(); + } + + /** + * Returns the redis connection object. + * Establishes a connection to the redis server if it does not already exists. + * @return Connection the redis connection object. + */ + public function getConnection() + { + if ($this->_connection === null) { + $this->_connection = new Connection(array( + 'dsn' => 'redis://' . $this->hostname . ':' . $this->port . '/' . $this->database, + 'password' => $this->password, + 'connectionTimeout' => $this->connectionTimeout, + 'dataTimeout' => $this->dataTimeout, + )); + } + return $this->_connection; + } + + /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + return (bool) $this->_connection->executeCommand('EXISTS', array($this->buildKey($key))); + } + + /** + * Retrieves a value from cache with a specified key. + * This is the implementation of the method declared in the parent class. + * @param string $key a unique key identifying the cached value + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + return $this->_connection->executeCommand('GET', array($key)); + } + + /** + * Retrieves multiple values from cache with the specified keys. + * @param array $keys a list of keys identifying the cached values + * @return array a list of cached values indexed by the keys + */ + protected function getValues($keys) + { + $response = $this->_connection->executeCommand('MGET', $keys); + $result = array(); + $i = 0; + foreach($keys as $key) { + $result[$key] = $response[$i++]; + } + return $result; + } + + /** + * Stores a value identified by a key in cache. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param float $expire the number of seconds in which the cached value will expire. 0 means never expire. + * This can be a floating point number to specify the time in milliseconds. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key,$value,$expire) + { + if ($expire == 0) { + return (bool) $this->_connection->executeCommand('SET', array($key, $value)); + } else { + $expire = (int) ($expire * 1000); + return (bool) $this->_connection->executeCommand('PSETEX', array($key, $expire, $value)); + } + } + + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param float $expire the number of seconds in which the cached value will expire. 0 means never expire. + * This can be a floating point number to specify the time in milliseconds. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key,$value,$expire) + { + if ($expire == 0) { + return (bool) $this->_connection->executeCommand('SETNX', array($key, $value)); + } else { + // TODO consider requiring redis version >= 2.6.12 that supports this in one command + $expire = (int) ($expire * 1000); + $this->_connection->executeCommand('MULTI'); + $this->_connection->executeCommand('SETNX', array($key, $value)); + $this->_connection->executeCommand('PEXPIRE', array($key, $expire)); + $response = $this->_connection->executeCommand('EXEC'); + return (bool) $response[0]; + } + } + + /** + * Deletes a value with the specified key from cache + * This is the implementation of the method declared in the parent class. + * @param string $key the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + return (bool) $this->_connection->executeCommand('DEL', array($key)); + } + + /** + * Deletes all values from cache. + * This is the implementation of the method declared in the parent class. + * @return boolean whether the flush operation was successful. + */ + protected function flushValues() + { + return $this->_connection->executeCommand('FLUSHDB'); + } +} diff --git a/yii/caching/WinCache.php b/framework/yii/caching/WinCache.php similarity index 88% rename from yii/caching/WinCache.php rename to framework/yii/caching/WinCache.php index 4e07c7f..3679884 100644 --- a/yii/caching/WinCache.php +++ b/framework/yii/caching/WinCache.php @@ -8,7 +8,7 @@ namespace yii\caching; /** - * WinCache provides XCache caching in terms of an application component. + * WinCache provides Windows Cache caching in terms of an application component. * * To use this application component, the [WinCache PHP extension](http://www.iis.net/expand/wincacheforphp) * must be loaded. Also note that "wincache.ucenabled" should be set to "On" in your php.ini file. @@ -21,10 +21,26 @@ namespace yii\caching; class WinCache extends Cache { /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + $key = $this->buildKey($key); + return wincache_ucache_exists($key); + } + + /** * Retrieves a value from cache with a specified key. * This is the implementation of the method declared in the parent class. * @param string $key a unique key identifying the cached value - * @return string the value stored in cache, false if the value is not in the cache or expired. + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. */ protected function getValue($key) { diff --git a/yii/caching/XCache.php b/framework/yii/caching/XCache.php similarity index 89% rename from yii/caching/XCache.php rename to framework/yii/caching/XCache.php index 2108c4f..4221a39 100644 --- a/yii/caching/XCache.php +++ b/framework/yii/caching/XCache.php @@ -22,10 +22,26 @@ namespace yii\caching; class XCache extends Cache { /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + $key = $this->buildKey($key); + return xcache_isset($key); + } + + /** * Retrieves a value from cache with a specified key. * This is the implementation of the method declared in the parent class. * @param string $key a unique key identifying the cached value - * @return string the value stored in cache, false if the value is not in the cache or expired. + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. */ protected function getValue($key) { @@ -86,4 +102,3 @@ class XCache extends Cache return true; } } - diff --git a/yii/caching/ZendDataCache.php b/framework/yii/caching/ZendDataCache.php similarity index 97% rename from yii/caching/ZendDataCache.php rename to framework/yii/caching/ZendDataCache.php index 5b41a8d..9ff2fd0 100644 --- a/yii/caching/ZendDataCache.php +++ b/framework/yii/caching/ZendDataCache.php @@ -24,7 +24,7 @@ class ZendDataCache extends Cache * Retrieves a value from cache with a specified key. * This is the implementation of the method declared in the parent class. * @param string $key a unique key identifying the cached value - * @return string the value stored in cache, false if the value is not in the cache or expired. + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. */ protected function getValue($key) { diff --git a/framework/yii/captcha/Captcha.php b/framework/yii/captcha/Captcha.php new file mode 100644 index 0000000..fc1d881 --- /dev/null +++ b/framework/yii/captcha/Captcha.php @@ -0,0 +1,141 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\captcha; + +use Yii; +use yii\base\InvalidConfigException; +use yii\helpers\Html; +use yii\helpers\Json; +use yii\widgets\InputWidget; + +/** + * Captcha renders a CAPTCHA image and an input field that takes user-entered verification code. + * + * Captcha is used together with [[CaptchaAction]] provide [CAPTCHA](http://en.wikipedia.org/wiki/Captcha) + * - a way of preventing Website spamming. + * + * The image element rendered by Captcha will display a CAPTCHA image generated by + * an action whose route is specified by [[captchaAction]]. This action must be an instance of [[CaptchaAction]]. + * + * When the user clicks on the CAPTCHA image, it will cause the CAPTCHA image + * to be refreshed with a new CAPTCHA. + * + * You may use [[\yii\validators\CaptchaValidator]] to validate the user input matches + * the current CAPTCHA verification code. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Captcha extends InputWidget +{ + /** + * @var string the route of the action that generates the CAPTCHA images. + * The action represented by this route must be an action of [[CaptchaAction]]. + */ + public $captchaAction = 'site/captcha'; + /** + * @var array HTML attributes to be applied to the text input field. + */ + public $options = array(); + /** + * @var array HTML attributes to be applied to the CAPTCHA image tag. + */ + public $imageOptions = array(); + /** + * @var string the template for arranging the CAPTCHA image tag and the text input tag. + * In this template, the token `{image}` will be replaced with the actual image tag, + * while `{input}` will be replaced with the text input tag. + */ + public $template = '{image} {input}'; + + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + + $this->checkRequirements(); + + if (!isset($this->options['id'])) { + $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId(); + } + if (!isset($this->imageOptions['id'])) { + $this->imageOptions['id'] = $this->options['id'] . '-image'; + } + } + + /** + * Renders the widget. + */ + public function run() + { + $this->registerClientScript(); + if ($this->hasModel()) { + $input = Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + $input = Html::textInput($this->name, $this->value, $this->options); + } + $url = Yii::$app->getUrlManager()->createUrl($this->captchaAction, array('v' => uniqid())); + $image = Html::img($url, $this->imageOptions); + echo strtr($this->template, array( + '{input}' => $input, + '{image}' => $image, + )); + } + + /** + * Registers the needed JavaScript. + */ + public function registerClientScript() + { + $options = $this->getClientOptions(); + $options = empty($options) ? '' : Json::encode($options); + $id = $this->imageOptions['id']; + $view = $this->getView(); + CaptchaAsset::register($view); + $view->registerJs("jQuery('#$id').yiiCaptcha($options);"); + } + + /** + * Returns the options for the captcha JS widget. + * @return array the options + */ + protected function getClientOptions() + { + $options = array( + 'refreshUrl' => Html::url(array($this->captchaAction, CaptchaAction::REFRESH_GET_VAR => 1)), + 'hashKey' => "yiiCaptcha/{$this->captchaAction}", + ); + return $options; + } + + /** + * Checks if there is graphic extension available to generate CAPTCHA images. + * This method will check the existence of ImageMagick and GD extensions. + * @return string the name of the graphic extension, either "imagick" or "gd". + * @throws InvalidConfigException if neither ImageMagick nor GD is installed. + */ + public static function checkRequirements() + { + if (extension_loaded('imagick')) { + $imagick = new \Imagick(); + $imagickFormats = $imagick->queryFormats('PNG'); + if (in_array('PNG', $imagickFormats)) { + return 'imagick'; + } + } + if (extension_loaded('gd')) { + $gdInfo = gd_info(); + if (!empty($gdInfo['FreeType Support'])) { + return 'gd'; + } + } + throw new InvalidConfigException('GD with FreeType or ImageMagick PHP extensions are required.'); + } +} diff --git a/yii/web/CaptchaAction.php b/framework/yii/captcha/CaptchaAction.php similarity index 83% rename from yii/web/CaptchaAction.php rename to framework/yii/captcha/CaptchaAction.php index e3d6eaa..e1761a1 100644 --- a/yii/web/CaptchaAction.php +++ b/framework/yii/captcha/CaptchaAction.php @@ -5,12 +5,11 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\web; +namespace yii\captcha; use Yii; use yii\base\Action; use yii\base\InvalidConfigException; -use yii\widgets\Captcha; /** * CaptchaAction renders a CAPTCHA image. @@ -30,6 +29,8 @@ use yii\widgets\Captcha; * to be validated by the 'captcha' validator. * 3. In the controller view, insert a [[Captcha]] widget in the form. * + * @property string $verifyCode The verification code. This property is read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -85,9 +86,9 @@ class CaptchaAction extends Action /** * @var string the TrueType font file. This can be either a file path or path alias. */ - public $fontFile = '@yii/web/SpicyRice.ttf'; + public $fontFile = '@yii/captcha/SpicyRice.ttf'; /** - * @var string the fixed verification code. When this is property is set, + * @var string the fixed verification code. When this property is set, * [[getVerifyCode()]] will always return the value of this property. * This is mainly used in automated tests where we want to be able to reproduce * the same verification code each time we run the tests. @@ -116,17 +117,19 @@ class CaptchaAction extends Action if (isset($_GET[self::REFRESH_GET_VAR])) { // AJAX request for regenerating code $code = $this->getVerifyCode(true); - echo json_encode(array( + /** @var \yii\web\Controller $controller */ + $controller = $this->controller; + return json_encode(array( 'hash1' => $this->generateValidationHash($code), 'hash2' => $this->generateValidationHash(strtolower($code)), // we add a random 'v' parameter so that FireFox can refresh the image // when src attribute of image tag is changed - 'url' => $this->controller->createUrl($this->id, array('v' => uniqid())), + 'url' => $controller->createUrl($this->id, array('v' => uniqid())), )); } else { - $this->renderImage($this->getVerifyCode()); + $this->setHttpHeaders(); + return $this->renderImage($this->getVerifyCode()); } - Yii::$app->end(); } /** @@ -153,7 +156,7 @@ class CaptchaAction extends Action return $this->fixedVerifyCode; } - $session = Yii::$app->session; + $session = Yii::$app->getSession(); $session->open(); $name = $this->getSessionKey(); if ($session[$name] === null || $regenerate) { @@ -173,11 +176,11 @@ class CaptchaAction extends Action { $code = $this->getVerifyCode(); $valid = $caseSensitive ? ($input === $code) : strcasecmp($input, $code) === 0; - $session = Yii::$app->session; + $session = Yii::$app->getSession(); $session->open(); $name = $this->getSessionKey() . 'count'; $session[$name] = $session[$name] + 1; - if ($session[$name] > $this->testLimit && $this->testLimit > 0) { + if ($valid || $session[$name] > $this->testLimit && $this->testLimit > 0) { $this->getVerifyCode(true); } return $valid; @@ -189,15 +192,15 @@ class CaptchaAction extends Action */ protected function generateVerifyCode() { + if ($this->minLength > $this->maxLength) { + $this->maxLength = $this->minLength; + } if ($this->minLength < 3) { $this->minLength = 3; } if ($this->maxLength > 20) { $this->maxLength = 20; } - if ($this->minLength > $this->maxLength) { - $this->maxLength = $this->minLength; - } $length = mt_rand($this->minLength, $this->maxLength); $letters = 'bcdfghjklmnpqrstvwxyz'; @@ -226,19 +229,21 @@ class CaptchaAction extends Action /** * Renders the CAPTCHA image. * @param string $code the verification code + * @return string image contents */ protected function renderImage($code) { if (Captcha::checkRequirements() === 'gd') { - $this->renderImageByGD($code); + return $this->renderImageByGD($code); } else { - $this->renderImageByImagick($code); + return $this->renderImageByImagick($code); } } /** * Renders the CAPTCHA image based on the code using GD library. * @param string $code the verification code + * @return string image contents */ protected function renderImageByGD($code) { @@ -260,10 +265,6 @@ class CaptchaAction extends Action (int)($this->foreColor % 0x10000 / 0x100), $this->foreColor % 0x100); - if ($this->fontFile === null) { - $this->fontFile = dirname(__FILE__) . '/SpicyRice.ttf'; - } - $length = strlen($code); $box = imagettfbbox(30, 0, $this->fontFile, $code); $w = $box[4] - $box[0] + $this->offset * ($length - 1); @@ -281,18 +282,16 @@ class CaptchaAction extends Action imagecolordeallocate($image, $foreColor); - header('Pragma: public'); - header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header('Content-Transfer-Encoding: binary'); - header("Content-type: image/png"); + ob_start(); imagepng($image); imagedestroy($image); + return ob_get_clean(); } /** * Renders the CAPTCHA image based on the code using ImageMagick library. * @param string $code the verification code + * @return \Imagick image instance. Can be used as string. In this case it will contain image contents. */ protected function renderImageByImagick($code) { @@ -302,10 +301,6 @@ class CaptchaAction extends Action $image = new \Imagick(); $image->newImage($this->width, $this->height, $backColor); - if ($this->fontFile === null) { - $this->fontFile = dirname(__FILE__) . '/SpicyRice.ttf'; - } - $draw = new \ImagickDraw(); $draw->setFont($this->fontFile); $draw->setFontSize(30); @@ -327,12 +322,20 @@ class CaptchaAction extends Action $x += (int)($fontMetrics['textWidth']) + $this->offset; } - header('Pragma: public'); - header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header('Content-Transfer-Encoding: binary'); - header("Content-type: image/png"); $image->setImageFormat('png'); - echo $image; + return $image; + } + + /** + * Sets the HTTP headers needed by image response. + */ + protected function setHttpHeaders() + { + Yii::$app->getResponse()->getHeaders() + ->set('Pragma', 'public') + ->set('Expires', '0') + ->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->set('Content-Transfer-Encoding', 'binary') + ->set('Content-type', 'image/png'); } } diff --git a/framework/yii/captcha/CaptchaAsset.php b/framework/yii/captcha/CaptchaAsset.php new file mode 100644 index 0000000..50c75b7 --- /dev/null +++ b/framework/yii/captcha/CaptchaAsset.php @@ -0,0 +1,25 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\captcha; + +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class CaptchaAsset extends AssetBundle +{ + public $sourcePath = '@yii/assets'; + public $js = array( + 'yii.captcha.js', + ); + public $depends = array( + 'yii\web\YiiAsset', + ); +} diff --git a/yii/validators/CaptchaValidator.php b/framework/yii/captcha/CaptchaValidator.php similarity index 91% rename from yii/validators/CaptchaValidator.php rename to framework/yii/captcha/CaptchaValidator.php index 2e58cf2..97bfd1b 100644 --- a/yii/validators/CaptchaValidator.php +++ b/framework/yii/captcha/CaptchaValidator.php @@ -5,22 +5,29 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\validators; +namespace yii\captcha; use Yii; use yii\base\InvalidConfigException; use yii\helpers\Html; +use yii\validators\ValidationAsset; +use yii\validators\Validator; /** * CaptchaValidator validates that the attribute value is the same as the verification code displayed in the CAPTCHA. * * CaptchaValidator should be used together with [[CaptchaAction]]. * + * @property \yii\captcha\CaptchaAction $captchaAction The action object. This property is read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ class CaptchaValidator extends Validator { + /** + * @var boolean whether to skip this validator if the input is empty. + */ public $skipOnEmpty = false; /** * @var boolean whether the comparison is case sensitive. Defaults to false. @@ -39,7 +46,7 @@ class CaptchaValidator extends Validator { parent::init(); if ($this->message === null) { - $this->message = Yii::t('yii|The verification code is incorrect.'); + $this->message = Yii::t('yii', 'The verification code is incorrect.'); } } @@ -71,7 +78,7 @@ class CaptchaValidator extends Validator /** * Returns the CAPTCHA action object. * @throws InvalidConfigException - * @return \yii\web\CaptchaAction the action object + * @return \yii\captcha\CaptchaAction the action object */ public function getCaptchaAction() { @@ -91,9 +98,11 @@ class CaptchaValidator extends Validator * 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. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $captcha = $this->getCaptchaAction(); $code = $captcha->getVerifyCode(false); @@ -111,7 +120,7 @@ class CaptchaValidator extends Validator $options['skipOnEmpty'] = 1; } + ValidationAsset::register($view); return 'yii.validation.captcha(value, messages, ' . json_encode($options) . ');'; } } - diff --git a/yii/web/SpicyRice.md b/framework/yii/captcha/SpicyRice.md similarity index 100% rename from yii/web/SpicyRice.md rename to framework/yii/captcha/SpicyRice.md index d99f3dc..7049bd1 100644 --- a/yii/web/SpicyRice.md +++ b/framework/yii/captcha/SpicyRice.md @@ -1,11 +1,11 @@ -## Spicy Rice font - -* **Author:** Brian J. Bonislawsky, Astigmatic (AOETI, Astigmatic One Eye Typographic Institute) -* **License:** SIL Open Font License (OFL), version 1.1, [notes and FAQ](http://scripts.sil.org/OFL) - -## Links - -* [Astigmatic](http://www.astigmatic.com/) -* [Google WebFonts](http://www.google.com/webfonts/specimen/Spicy+Rice) -* [fontsquirrel.com](http://www.fontsquirrel.com/fonts/spicy-rice) -* [fontspace.com](http://www.fontspace.com/astigmatic-one-eye-typographic-institute/spicy-rice) +## Spicy Rice font + +* **Author:** Brian J. Bonislawsky, Astigmatic (AOETI, Astigmatic One Eye Typographic Institute) +* **License:** SIL Open Font License (OFL), version 1.1, [notes and FAQ](http://scripts.sil.org/OFL) + +## Links + +* [Astigmatic](http://www.astigmatic.com/) +* [Google WebFonts](http://www.google.com/webfonts/specimen/Spicy+Rice) +* [fontsquirrel.com](http://www.fontsquirrel.com/fonts/spicy-rice) +* [fontspace.com](http://www.fontspace.com/astigmatic-one-eye-typographic-institute/spicy-rice) diff --git a/yii/web/SpicyRice.ttf b/framework/yii/captcha/SpicyRice.ttf similarity index 100% rename from yii/web/SpicyRice.ttf rename to framework/yii/captcha/SpicyRice.ttf Binary files a/yii/web/SpicyRice.ttf and b/framework/yii/captcha/SpicyRice.ttf differ diff --git a/framework/yii/classes.php b/framework/yii/classes.php new file mode 100644 index 0000000..78cce95 --- /dev/null +++ b/framework/yii/classes.php @@ -0,0 +1,244 @@ +<?php +/** + * Yii core class map. + * + * This file is automatically generated by the "build classmap" command under the "build" folder. + * Do not modify it directly. + * + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +return array( + 'yii\base\Action' => YII_PATH . '/base/Action.php', + 'yii\base\ActionEvent' => YII_PATH . '/base/ActionEvent.php', + 'yii\base\ActionFilter' => YII_PATH . '/base/ActionFilter.php', + 'yii\base\Application' => YII_PATH . '/base/Application.php', + 'yii\base\Arrayable' => YII_PATH . '/base/Arrayable.php', + 'yii\base\Behavior' => YII_PATH . '/base/Behavior.php', + 'yii\base\Component' => YII_PATH . '/base/Component.php', + 'yii\base\Controller' => YII_PATH . '/base/Controller.php', + 'yii\base\ErrorException' => YII_PATH . '/base/ErrorException.php', + 'yii\base\ErrorHandler' => YII_PATH . '/base/ErrorHandler.php', + 'yii\base\Event' => YII_PATH . '/base/Event.php', + 'yii\base\Exception' => YII_PATH . '/base/Exception.php', + 'yii\base\Formatter' => YII_PATH . '/base/Formatter.php', + 'yii\base\InlineAction' => YII_PATH . '/base/InlineAction.php', + 'yii\base\InvalidCallException' => YII_PATH . '/base/InvalidCallException.php', + 'yii\base\InvalidConfigException' => YII_PATH . '/base/InvalidConfigException.php', + 'yii\base\InvalidParamException' => YII_PATH . '/base/InvalidParamException.php', + 'yii\base\InvalidRouteException' => YII_PATH . '/base/InvalidRouteException.php', + 'yii\base\Model' => YII_PATH . '/base/Model.php', + 'yii\base\ModelEvent' => YII_PATH . '/base/ModelEvent.php', + 'yii\base\Module' => YII_PATH . '/base/Module.php', + 'yii\base\NotSupportedException' => YII_PATH . '/base/NotSupportedException.php', + 'yii\base\Object' => YII_PATH . '/base/Object.php', + 'yii\base\Request' => YII_PATH . '/base/Request.php', + 'yii\base\Response' => YII_PATH . '/base/Response.php', + 'yii\base\Theme' => YII_PATH . '/base/Theme.php', + 'yii\base\UnknownClassException' => YII_PATH . '/base/UnknownClassException.php', + 'yii\base\UnknownMethodException' => YII_PATH . '/base/UnknownMethodException.php', + 'yii\base\UnknownPropertyException' => YII_PATH . '/base/UnknownPropertyException.php', + 'yii\base\UserException' => YII_PATH . '/base/UserException.php', + 'yii\base\View' => YII_PATH . '/base/View.php', + 'yii\base\ViewEvent' => YII_PATH . '/base/ViewEvent.php', + 'yii\base\ViewRenderer' => YII_PATH . '/base/ViewRenderer.php', + 'yii\base\Widget' => YII_PATH . '/base/Widget.php', + 'yii\behaviors\AutoTimestamp' => YII_PATH . '/behaviors/AutoTimestamp.php', + 'yii\bootstrap\Alert' => YII_PATH . '/bootstrap/Alert.php', + 'yii\bootstrap\BootstrapAsset' => YII_PATH . '/bootstrap/BootstrapAsset.php', + 'yii\bootstrap\BootstrapPluginAsset' => YII_PATH . '/bootstrap/BootstrapPluginAsset.php', + 'yii\bootstrap\BootstrapThemeAsset' => YII_PATH . '/bootstrap/BootstrapThemeAsset.php', + 'yii\bootstrap\Button' => YII_PATH . '/bootstrap/Button.php', + 'yii\bootstrap\ButtonDropdown' => YII_PATH . '/bootstrap/ButtonDropdown.php', + 'yii\bootstrap\ButtonGroup' => YII_PATH . '/bootstrap/ButtonGroup.php', + 'yii\bootstrap\Carousel' => YII_PATH . '/bootstrap/Carousel.php', + 'yii\bootstrap\Collapse' => YII_PATH . '/bootstrap/Collapse.php', + 'yii\bootstrap\Dropdown' => YII_PATH . '/bootstrap/Dropdown.php', + 'yii\bootstrap\Modal' => YII_PATH . '/bootstrap/Modal.php', + 'yii\bootstrap\Nav' => YII_PATH . '/bootstrap/Nav.php', + 'yii\bootstrap\NavBar' => YII_PATH . '/bootstrap/NavBar.php', + 'yii\bootstrap\Progress' => YII_PATH . '/bootstrap/Progress.php', + 'yii\bootstrap\Tabs' => YII_PATH . '/bootstrap/Tabs.php', + 'yii\bootstrap\Widget' => YII_PATH . '/bootstrap/Widget.php', + 'yii\caching\ApcCache' => YII_PATH . '/caching/ApcCache.php', + 'yii\caching\Cache' => YII_PATH . '/caching/Cache.php', + 'yii\caching\ChainedDependency' => YII_PATH . '/caching/ChainedDependency.php', + 'yii\caching\DbCache' => YII_PATH . '/caching/DbCache.php', + 'yii\caching\DbDependency' => YII_PATH . '/caching/DbDependency.php', + 'yii\caching\Dependency' => YII_PATH . '/caching/Dependency.php', + 'yii\caching\DummyCache' => YII_PATH . '/caching/DummyCache.php', + 'yii\caching\ExpressionDependency' => YII_PATH . '/caching/ExpressionDependency.php', + 'yii\caching\FileCache' => YII_PATH . '/caching/FileCache.php', + 'yii\caching\FileDependency' => YII_PATH . '/caching/FileDependency.php', + 'yii\caching\GroupDependency' => YII_PATH . '/caching/GroupDependency.php', + 'yii\caching\MemCache' => YII_PATH . '/caching/MemCache.php', + 'yii\caching\MemCacheServer' => YII_PATH . '/caching/MemCacheServer.php', + 'yii\caching\RedisCache' => YII_PATH . '/caching/RedisCache.php', + 'yii\caching\WinCache' => YII_PATH . '/caching/WinCache.php', + 'yii\caching\XCache' => YII_PATH . '/caching/XCache.php', + 'yii\caching\ZendDataCache' => YII_PATH . '/caching/ZendDataCache.php', + 'yii\captcha\Captcha' => YII_PATH . '/captcha/Captcha.php', + 'yii\captcha\CaptchaAction' => YII_PATH . '/captcha/CaptchaAction.php', + 'yii\captcha\CaptchaAsset' => YII_PATH . '/captcha/CaptchaAsset.php', + 'yii\captcha\CaptchaValidator' => YII_PATH . '/captcha/CaptchaValidator.php', + 'yii\data\ActiveDataProvider' => YII_PATH . '/data/ActiveDataProvider.php', + 'yii\data\ArrayDataProvider' => YII_PATH . '/data/ArrayDataProvider.php', + 'yii\data\BaseDataProvider' => YII_PATH . '/data/BaseDataProvider.php', + 'yii\data\DataProviderInterface' => YII_PATH . '/data/DataProviderInterface.php', + 'yii\data\Pagination' => YII_PATH . '/data/Pagination.php', + 'yii\data\Sort' => YII_PATH . '/data/Sort.php', + 'yii\db\ActiveQuery' => YII_PATH . '/db/ActiveQuery.php', + 'yii\db\ActiveRecord' => YII_PATH . '/db/ActiveRecord.php', + 'yii\db\ActiveRelation' => YII_PATH . '/db/ActiveRelation.php', + 'yii\db\ColumnSchema' => YII_PATH . '/db/ColumnSchema.php', + 'yii\db\Command' => YII_PATH . '/db/Command.php', + 'yii\db\Connection' => YII_PATH . '/db/Connection.php', + 'yii\db\DataReader' => YII_PATH . '/db/DataReader.php', + 'yii\db\Exception' => YII_PATH . '/db/Exception.php', + 'yii\db\Expression' => YII_PATH . '/db/Expression.php', + 'yii\db\Migration' => YII_PATH . '/db/Migration.php', + 'yii\db\Query' => YII_PATH . '/db/Query.php', + 'yii\db\QueryBuilder' => YII_PATH . '/db/QueryBuilder.php', + 'yii\db\Schema' => YII_PATH . '/db/Schema.php', + 'yii\db\StaleObjectException' => YII_PATH . '/db/StaleObjectException.php', + 'yii\db\TableSchema' => YII_PATH . '/db/TableSchema.php', + 'yii\db\Transaction' => YII_PATH . '/db/Transaction.php', + 'yii\db\cubrid\QueryBuilder' => YII_PATH . '/db/cubrid/QueryBuilder.php', + 'yii\db\cubrid\Schema' => YII_PATH . '/db/cubrid/Schema.php', + 'yii\db\mssql\PDO' => YII_PATH . '/db/mssql/PDO.php', + 'yii\db\mssql\QueryBuilder' => YII_PATH . '/db/mssql/QueryBuilder.php', + 'yii\db\mssql\Schema' => YII_PATH . '/db/mssql/Schema.php', + 'yii\db\mssql\SqlsrvPDO' => YII_PATH . '/db/mssql/SqlsrvPDO.php', + 'yii\db\mssql\TableSchema' => YII_PATH . '/db/mssql/TableSchema.php', + 'yii\db\mysql\QueryBuilder' => YII_PATH . '/db/mysql/QueryBuilder.php', + 'yii\db\mysql\Schema' => YII_PATH . '/db/mysql/Schema.php', + 'yii\db\pgsql\QueryBuilder' => YII_PATH . '/db/pgsql/QueryBuilder.php', + 'yii\db\pgsql\Schema' => YII_PATH . '/db/pgsql/Schema.php', + 'yii\db\sqlite\QueryBuilder' => YII_PATH . '/db/sqlite/QueryBuilder.php', + 'yii\db\sqlite\Schema' => YII_PATH . '/db/sqlite/Schema.php', + 'yii\grid\ActionColumn' => YII_PATH . '/grid/ActionColumn.php', + 'yii\grid\CheckboxColumn' => YII_PATH . '/grid/CheckboxColumn.php', + 'yii\grid\Column' => YII_PATH . '/grid/Column.php', + 'yii\grid\DataColumn' => YII_PATH . '/grid/DataColumn.php', + 'yii\grid\GridView' => YII_PATH . '/grid/GridView.php', + 'yii\grid\GridViewAsset' => YII_PATH . '/grid/GridViewAsset.php', + 'yii\grid\SerialColumn' => YII_PATH . '/grid/SerialColumn.php', + 'yii\helpers\ArrayHelper' => YII_PATH . '/helpers/ArrayHelper.php', + 'yii\helpers\BaseArrayHelper' => YII_PATH . '/helpers/BaseArrayHelper.php', + 'yii\helpers\BaseConsole' => YII_PATH . '/helpers/BaseConsole.php', + 'yii\helpers\BaseFileHelper' => YII_PATH . '/helpers/BaseFileHelper.php', + 'yii\helpers\BaseHtml' => YII_PATH . '/helpers/BaseHtml.php', + 'yii\helpers\BaseHtmlPurifier' => YII_PATH . '/helpers/BaseHtmlPurifier.php', + 'yii\helpers\BaseInflector' => YII_PATH . '/helpers/BaseInflector.php', + 'yii\helpers\BaseJson' => YII_PATH . '/helpers/BaseJson.php', + 'yii\helpers\BaseMarkdown' => YII_PATH . '/helpers/BaseMarkdown.php', + 'yii\helpers\BaseSecurity' => YII_PATH . '/helpers/BaseSecurity.php', + 'yii\helpers\BaseStringHelper' => YII_PATH . '/helpers/BaseStringHelper.php', + 'yii\helpers\BaseVarDumper' => YII_PATH . '/helpers/BaseVarDumper.php', + 'yii\helpers\Console' => YII_PATH . '/helpers/Console.php', + 'yii\helpers\FileHelper' => YII_PATH . '/helpers/FileHelper.php', + 'yii\helpers\Html' => YII_PATH . '/helpers/Html.php', + 'yii\helpers\HtmlPurifier' => YII_PATH . '/helpers/HtmlPurifier.php', + 'yii\helpers\Inflector' => YII_PATH . '/helpers/Inflector.php', + 'yii\helpers\Json' => YII_PATH . '/helpers/Json.php', + 'yii\helpers\Markdown' => YII_PATH . '/helpers/Markdown.php', + 'yii\helpers\Security' => YII_PATH . '/helpers/Security.php', + 'yii\helpers\StringHelper' => YII_PATH . '/helpers/StringHelper.php', + 'yii\helpers\VarDumper' => YII_PATH . '/helpers/VarDumper.php', + 'yii\i18n\DbMessageSource' => YII_PATH . '/i18n/DbMessageSource.php', + 'yii\i18n\Formatter' => YII_PATH . '/i18n/Formatter.php', + 'yii\i18n\GettextFile' => YII_PATH . '/i18n/GettextFile.php', + 'yii\i18n\GettextMessageSource' => YII_PATH . '/i18n/GettextMessageSource.php', + 'yii\i18n\GettextMoFile' => YII_PATH . '/i18n/GettextMoFile.php', + 'yii\i18n\GettextPoFile' => YII_PATH . '/i18n/GettextPoFile.php', + 'yii\i18n\I18N' => YII_PATH . '/i18n/I18N.php', + 'yii\i18n\MessageSource' => YII_PATH . '/i18n/MessageSource.php', + 'yii\i18n\MissingTranslationEvent' => YII_PATH . '/i18n/MissingTranslationEvent.php', + 'yii\i18n\PhpMessageSource' => YII_PATH . '/i18n/PhpMessageSource.php', + 'yii\log\DbTarget' => YII_PATH . '/log/DbTarget.php', + 'yii\log\EmailTarget' => YII_PATH . '/log/EmailTarget.php', + 'yii\log\FileTarget' => YII_PATH . '/log/FileTarget.php', + 'yii\log\Logger' => YII_PATH . '/log/Logger.php', + 'yii\log\Target' => YII_PATH . '/log/Target.php', + 'yii\rbac\Assignment' => YII_PATH . '/rbac/Assignment.php', + 'yii\rbac\DbManager' => YII_PATH . '/rbac/DbManager.php', + 'yii\rbac\Item' => YII_PATH . '/rbac/Item.php', + 'yii\rbac\Manager' => YII_PATH . '/rbac/Manager.php', + 'yii\rbac\PhpManager' => YII_PATH . '/rbac/PhpManager.php', + 'yii\redis\Connection' => YII_PATH . '/redis/Connection.php', + 'yii\redis\Transaction' => YII_PATH . '/redis/Transaction.php', + 'yii\requirements\YiiRequirementChecker' => YII_PATH . '/requirements/YiiRequirementChecker.php', + 'yii\validators\BooleanValidator' => YII_PATH . '/validators/BooleanValidator.php', + 'yii\validators\CompareValidator' => YII_PATH . '/validators/CompareValidator.php', + 'yii\validators\DateValidator' => YII_PATH . '/validators/DateValidator.php', + 'yii\validators\DefaultValueValidator' => YII_PATH . '/validators/DefaultValueValidator.php', + 'yii\validators\EmailValidator' => YII_PATH . '/validators/EmailValidator.php', + 'yii\validators\ExistValidator' => YII_PATH . '/validators/ExistValidator.php', + 'yii\validators\FileValidator' => YII_PATH . '/validators/FileValidator.php', + 'yii\validators\FilterValidator' => YII_PATH . '/validators/FilterValidator.php', + 'yii\validators\InlineValidator' => YII_PATH . '/validators/InlineValidator.php', + 'yii\validators\NumberValidator' => YII_PATH . '/validators/NumberValidator.php', + 'yii\validators\PunycodeAsset' => YII_PATH . '/validators/PunycodeAsset.php', + 'yii\validators\RangeValidator' => YII_PATH . '/validators/RangeValidator.php', + 'yii\validators\RegularExpressionValidator' => YII_PATH . '/validators/RegularExpressionValidator.php', + 'yii\validators\RequiredValidator' => YII_PATH . '/validators/RequiredValidator.php', + 'yii\validators\SafeValidator' => YII_PATH . '/validators/SafeValidator.php', + 'yii\validators\StringValidator' => YII_PATH . '/validators/StringValidator.php', + 'yii\validators\UniqueValidator' => YII_PATH . '/validators/UniqueValidator.php', + 'yii\validators\UrlValidator' => YII_PATH . '/validators/UrlValidator.php', + 'yii\validators\ValidationAsset' => YII_PATH . '/validators/ValidationAsset.php', + 'yii\validators\Validator' => YII_PATH . '/validators/Validator.php', + 'yii\web\AccessControl' => YII_PATH . '/web/AccessControl.php', + 'yii\web\AccessRule' => YII_PATH . '/web/AccessRule.php', + 'yii\web\Application' => YII_PATH . '/web/Application.php', + 'yii\web\AssetBundle' => YII_PATH . '/web/AssetBundle.php', + 'yii\web\AssetConverter' => YII_PATH . '/web/AssetConverter.php', + 'yii\web\AssetConverterInterface' => YII_PATH . '/web/AssetConverterInterface.php', + 'yii\web\AssetManager' => YII_PATH . '/web/AssetManager.php', + 'yii\web\CacheSession' => YII_PATH . '/web/CacheSession.php', + 'yii\web\Controller' => YII_PATH . '/web/Controller.php', + 'yii\web\Cookie' => YII_PATH . '/web/Cookie.php', + 'yii\web\CookieCollection' => YII_PATH . '/web/CookieCollection.php', + 'yii\web\DbSession' => YII_PATH . '/web/DbSession.php', + 'yii\web\ErrorAction' => YII_PATH . '/web/ErrorAction.php', + 'yii\web\HeaderCollection' => YII_PATH . '/web/HeaderCollection.php', + 'yii\web\HttpCache' => YII_PATH . '/web/HttpCache.php', + 'yii\web\HttpException' => YII_PATH . '/web/HttpException.php', + 'yii\web\IdentityInterface' => YII_PATH . '/web/IdentityInterface.php', + 'yii\web\JqueryAsset' => YII_PATH . '/web/JqueryAsset.php', + 'yii\web\JsExpression' => YII_PATH . '/web/JsExpression.php', + 'yii\web\PageCache' => YII_PATH . '/web/PageCache.php', + 'yii\web\Request' => YII_PATH . '/web/Request.php', + 'yii\web\Response' => YII_PATH . '/web/Response.php', + 'yii\web\ResponseEvent' => YII_PATH . '/web/ResponseEvent.php', + 'yii\web\ResponseFormatterInterface' => YII_PATH . '/web/ResponseFormatterInterface.php', + 'yii\web\Session' => YII_PATH . '/web/Session.php', + 'yii\web\SessionIterator' => YII_PATH . '/web/SessionIterator.php', + 'yii\web\UploadedFile' => YII_PATH . '/web/UploadedFile.php', + 'yii\web\UrlManager' => YII_PATH . '/web/UrlManager.php', + 'yii\web\UrlRule' => YII_PATH . '/web/UrlRule.php', + 'yii\web\User' => YII_PATH . '/web/User.php', + 'yii\web\UserEvent' => YII_PATH . '/web/UserEvent.php', + 'yii\web\VerbFilter' => YII_PATH . '/web/VerbFilter.php', + 'yii\web\XmlResponseFormatter' => YII_PATH . '/web/XmlResponseFormatter.php', + 'yii\web\YiiAsset' => YII_PATH . '/web/YiiAsset.php', + 'yii\widgets\ActiveField' => YII_PATH . '/widgets/ActiveField.php', + 'yii\widgets\ActiveForm' => YII_PATH . '/widgets/ActiveForm.php', + 'yii\widgets\ActiveFormAsset' => YII_PATH . '/widgets/ActiveFormAsset.php', + 'yii\widgets\BaseListView' => YII_PATH . '/widgets/BaseListView.php', + 'yii\widgets\Block' => YII_PATH . '/widgets/Block.php', + 'yii\widgets\Breadcrumbs' => YII_PATH . '/widgets/Breadcrumbs.php', + 'yii\widgets\ContentDecorator' => YII_PATH . '/widgets/ContentDecorator.php', + 'yii\widgets\DetailView' => YII_PATH . '/widgets/DetailView.php', + 'yii\widgets\FragmentCache' => YII_PATH . '/widgets/FragmentCache.php', + 'yii\widgets\InputWidget' => YII_PATH . '/widgets/InputWidget.php', + 'yii\widgets\LinkPager' => YII_PATH . '/widgets/LinkPager.php', + 'yii\widgets\LinkSorter' => YII_PATH . '/widgets/LinkSorter.php', + 'yii\widgets\ListView' => YII_PATH . '/widgets/ListView.php', + 'yii\widgets\MaskedInput' => YII_PATH . '/widgets/MaskedInput.php', + 'yii\widgets\MaskedInputAsset' => YII_PATH . '/widgets/MaskedInputAsset.php', + 'yii\widgets\Menu' => YII_PATH . '/widgets/Menu.php', + 'yii\widgets\Spaceless' => YII_PATH . '/widgets/Spaceless.php', +); diff --git a/yii/console/Application.php b/framework/yii/console/Application.php similarity index 81% rename from yii/console/Application.php rename to framework/yii/console/Application.php index 2f28cac..9ca7393 100644 --- a/yii/console/Application.php +++ b/framework/yii/console/Application.php @@ -9,6 +9,7 @@ namespace yii\console; +use Yii; use yii\base\InvalidRouteException; /** @@ -30,7 +31,7 @@ use yii\base\InvalidRouteException; * To run the console application, enter the following on the command line: * * ~~~ - * yiic <route> [--param1=value1 --param2 ...] + * yii <route> [--param1=value1 --param2 ...] * ~~~ * * where `<route>` refers to a controller route in the form of `ModuleID/ControllerID/ActionID` @@ -42,9 +43,11 @@ use yii\base\InvalidRouteException; * To use this command, simply type: * * ~~~ - * yiic help + * yii help * ~~~ * + * @property Response $response The response component. This property is read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -60,6 +63,10 @@ class Application extends \yii\base\Application * Defaults to true. */ public $enableCoreCommands = true; + /** + * @var Controller the currently active controller instance + */ + public $controller; /** * Initialize the application. @@ -81,24 +88,34 @@ class Application extends \yii\base\Application } /** - * Processes the request. - * The request is represented in terms of a controller route and action parameters. - * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) - * @throws Exception if the script is not running from the command line + * Handles the specified request. + * @param Request $request the request to be handled + * @return Response the resulting response */ - public function processRequest() + public function handleRequest($request) { - /** @var $request Request */ - $request = $this->getRequest(); - if ($request->getIsConsoleRequest()) { - list ($route, $params) = $request->resolve(); - return $this->runAction($route, $params); + list ($route, $params) = $request->resolve(); + $this->requestedRoute = $route; + $result = $this->runAction($route, $params); + if ($result instanceof Response) { + return $result; } else { - throw new Exception(\Yii::t('yii|This script must be run from the command line.')); + $response = $this->getResponse(); + $response->exitStatus = (int)$result; + return $response; } } /** + * Returns the response component. + * @return Response the response component + */ + public function getResponse() + { + return $this->getComponent('response'); + } + + /** * Runs a controller action specified by a route. * This method parses the specified route and creates the corresponding child module(s), controller and action * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. @@ -113,7 +130,7 @@ class Application extends \yii\base\Application try { return parent::runAction($route, $params); } catch (InvalidRouteException $e) { - throw new Exception(\Yii::t('yii|Unknown command "{command}".', array('{command}' => $route))); + throw new Exception(Yii::t('yii', 'Unknown command "{command}".', array('command' => $route)), 0, $e); } } @@ -127,7 +144,6 @@ class Application extends \yii\base\Application 'message' => 'yii\console\controllers\MessageController', 'help' => 'yii\console\controllers\HelpController', 'migrate' => 'yii\console\controllers\MigrateController', - 'app' => 'yii\console\controllers\AppController', 'cache' => 'yii\console\controllers\CacheController', 'asset' => 'yii\console\controllers\AssetController', ); diff --git a/yii/console/Controller.php b/framework/yii/console/Controller.php similarity index 73% rename from yii/console/Controller.php rename to framework/yii/console/Controller.php index c07d92d..73b74f8 100644 --- a/yii/console/Controller.php +++ b/framework/yii/console/Controller.php @@ -18,10 +18,10 @@ use yii\helpers\Console; * * A controller consists of one or several actions known as sub-commands. * Users call a console command by specifying the corresponding route which identifies a controller action. - * The `yiic` program is used when calling a console command, like the following: + * The `yii` program is used when calling a console command, like the following: * * ~~~ - * yiic <route> [--param1=value1 --param2 ...] + * yii <route> [--param1=value1 --param2 ...] * ~~~ * * @author Qiang Xue <qiang.xue@gmail.com> @@ -30,18 +30,29 @@ use yii\helpers\Console; class Controller extends \yii\base\Controller { /** - * @var boolean whether the call of [[confirm()]] requires a user input. - * If false, [[confirm()]] will always return true no matter what user enters or not. + * @var boolean whether to run the command interactively. */ public $interactive = true; /** - * @var bool whether to enable ANSI style in output. - * Setting this will affect [[ansiFormat()]], [[stdout()]] and [[stderr()]]. - * If not set it will be auto detected using [[yii\helpers\Console::streamSupportsAnsiColors()]] with STDOUT - * for [[ansiFormat()]] and [[stdout()]] and STDERR for [[stderr()]]. + * @var boolean whether to enable ANSI color in the output. + * If not set, ANSI color will only be enabled for terminals that support it. */ - public $colors; + public $color; + + /** + * Returns a value indicating whether ANSI color is enabled. + * + * ANSI color is enabled only if [[color]] is set true or is not set + * and the terminal supports ANSI color. + * + * @param resource $stream the stream to check. + * @return boolean Whether to enable ANSI style in output. + */ + public function isColorEnabled($stream = STDOUT) + { + return $this->color === null ? Console::streamSupportsAnsiColors($stream) : $this->color; + } /** * Runs an action with the specified action ID and parameters. @@ -78,24 +89,22 @@ class Controller extends \yii\base\Controller */ public function bindActionParams($action, $params) { + $args = array(); if (!empty($params)) { $options = $this->globalOptions(); foreach ($params as $name => $value) { if (in_array($name, $options, true)) { $this->$name = $value; - unset($params[$name]); + } elseif (is_int($name)) { + $args[] = $value; + } else { + throw new Exception(Yii::t('yii', 'Unknown option: --{name}', array( + 'name' => $name, + ))); } } } - $args = isset($params[Request::ANONYMOUS_PARAMS]) ? $params[Request::ANONYMOUS_PARAMS] : array(); - unset($params[Request::ANONYMOUS_PARAMS]); - if (!empty($params)) { - throw new Exception(Yii::t('yii|Unknown options: {params}', array( - '{params}' => implode(', ', array_keys($params)), - ))); - } - if ($action instanceof InlineAction) { $method = new \ReflectionMethod($this, $action->actionMethod); } else { @@ -115,8 +124,8 @@ class Controller extends \yii\base\Controller } if (!empty($missing)) { - throw new Exception(Yii::t('yii|Missing required arguments: {params}', array( - '{params}' => implode(', ', $missing), + throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', array( + 'params' => implode(', ', $missing), ))); } @@ -126,11 +135,12 @@ class Controller extends \yii\base\Controller /** * Formats a string with ANSI codes * - * You may pass additional parameters using the constants defined in [[yii\helpers\base\Console]]. + * You may pass additional parameters using the constants defined in [[yii\helpers\Console]]. * * Example: + * * ~~~ - * $this->ansiFormat('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); + * echo $this->ansiFormat('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); * ~~~ * * @param string $string the string to be formatted @@ -138,7 +148,7 @@ class Controller extends \yii\base\Controller */ public function ansiFormat($string) { - if ($this->ansi === true || $this->ansi === null && Console::streamSupportsAnsiColors(STDOUT)) { + if ($this->isColorEnabled()) { $args = func_get_args(); array_shift($args); $string = Console::ansiFormat($string, $args); @@ -150,9 +160,10 @@ class Controller extends \yii\base\Controller * Prints a string to STDOUT * * You may optionally format the string with ANSI codes by - * passing additional parameters using the constants defined in [[yii\helpers\base\Console]]. + * passing additional parameters using the constants defined in [[yii\helpers\Console]]. * * Example: + * * ~~~ * $this->stdout('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); * ~~~ @@ -162,7 +173,7 @@ class Controller extends \yii\base\Controller */ public function stdout($string) { - if ($this->ansi === true || $this->ansi === null && Console::streamSupportsAnsiColors(STDOUT)) { + if ($this->isColorEnabled()) { $args = func_get_args(); array_shift($args); $string = Console::ansiFormat($string, $args); @@ -174,9 +185,10 @@ class Controller extends \yii\base\Controller * Prints a string to STDERR * * You may optionally format the string with ANSI codes by - * passing additional parameters using the constants defined in [[yii\helpers\base\Console]]. + * passing additional parameters using the constants defined in [[yii\helpers\Console]]. * * Example: + * * ~~~ * $this->stderr('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); * ~~~ @@ -186,7 +198,7 @@ class Controller extends \yii\base\Controller */ public function stderr($string) { - if ($this->ansi === true || $this->ansi === null && Console::streamSupportsAnsiColors(STDERR)) { + if ($this->isColorEnabled(STDERR)) { $args = func_get_args(); array_shift($args); $string = Console::ansiFormat($string, $args); @@ -199,6 +211,7 @@ class Controller extends \yii\base\Controller * * @param string $text prompt string * @param array $options the options to validate the input: + * * - required: whether it is required or not * - default: default value if no input is inserted by the user * - pattern: regular expression pattern to validate user input @@ -221,7 +234,8 @@ class Controller extends \yii\base\Controller * * @param string $message to echo out before waiting for user input * @param boolean $default this value is returned if no selection is made. - * @return boolean whether user confirmed + * @return boolean whether user confirmed. + * Will return true if [[interactive]] is false. */ public function confirm($message, $default = false) { @@ -259,6 +273,6 @@ class Controller extends \yii\base\Controller */ public function globalOptions() { - return array(); + return array('color', 'interactive'); } } diff --git a/yii/console/Exception.php b/framework/yii/console/Exception.php similarity index 96% rename from yii/console/Exception.php rename to framework/yii/console/Exception.php index cb10c19..f272bde 100644 --- a/yii/console/Exception.php +++ b/framework/yii/console/Exception.php @@ -22,7 +22,6 @@ class Exception extends UserException */ public function getName() { - return \Yii::t('yii|Error'); + return \Yii::t('yii', 'Error'); } } - diff --git a/yii/console/Request.php b/framework/yii/console/Request.php similarity index 66% rename from yii/console/Request.php rename to framework/yii/console/Request.php index ed477e9..dca0240 100644 --- a/yii/console/Request.php +++ b/framework/yii/console/Request.php @@ -8,16 +8,40 @@ namespace yii\console; /** + * + * @property array $params The command line arguments. It does not include the entry script name. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ class Request extends \yii\base\Request { - const ANONYMOUS_PARAMS = '-args'; + private $_params; - public function getRawParams() + /** + * Returns the command line arguments. + * @return array the command line arguments. It does not include the entry script name. + */ + public function getParams() { - return isset($_SERVER['argv']) ? $_SERVER['argv'] : array(); + if (!isset($this->_params)) { + if (isset($_SERVER['argv'])) { + $this->_params = $_SERVER['argv']; + array_shift($this->_params); + } else { + $this->_params = array(); + } + } + return $this->_params; + } + + /** + * Sets the command line arguments. + * @param array $params the command line arguments + */ + public function setParams($params) + { + $this->_params = $params; } /** @@ -26,9 +50,7 @@ class Request extends \yii\base\Request */ public function resolve() { - $rawParams = $this->getRawParams(); - array_shift($rawParams); // the 1st argument is the yiic script name - + $rawParams = $this->getParams(); if (isset($rawParams[0])) { $route = $rawParams[0]; array_shift($rawParams); @@ -36,13 +58,13 @@ class Request extends \yii\base\Request $route = ''; } - $params = array(self::ANONYMOUS_PARAMS => array()); + $params = array(); foreach ($rawParams as $param) { if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) { $name = $matches[1]; $params[$name] = isset($matches[3]) ? $matches[3] : true; } else { - $params[self::ANONYMOUS_PARAMS][] = $param; + $params[] = $param; } } diff --git a/yii/debug/Module.php b/framework/yii/console/Response.php similarity index 82% rename from yii/debug/Module.php rename to framework/yii/console/Response.php index 3421d95..9c23e83 100644 --- a/yii/debug/Module.php +++ b/framework/yii/console/Response.php @@ -5,13 +5,12 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\debug; +namespace yii\console; /** * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Module extends \yii\base\Module +class Response extends \yii\base\Response { - public $controllerNamespace = 'yii\debug\controllers'; -} \ No newline at end of file +} diff --git a/framework/yii/console/controllers/AssetController.php b/framework/yii/console/controllers/AssetController.php new file mode 100644 index 0000000..1e95dd8 --- /dev/null +++ b/framework/yii/console/controllers/AssetController.php @@ -0,0 +1,627 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\console\controllers; + +use Yii; +use yii\console\Exception; +use yii\console\Controller; + +/** + * This command allows you to combine and compress your JavaScript and CSS files. + * + * Usage: + * 1. Create a configuration file using 'template' action: + * yii asset/template /path/to/myapp/config.php + * 2. Edit the created config file, adjusting it for your web application needs. + * 3. Run the 'compress' action, using created config: + * yii asset /path/to/myapp/config.php /path/to/myapp/config/assets_compressed.php + * 4. Adjust your web application config to use compressed assets. + * + * Note: in the console environment some path aliases like '@webroot' and '@web' may not exist, + * so corresponding paths inside the configuration should be specified directly. + * + * Note: by default this command relies on an external tools to perform actual files compression, + * check [[jsCompressor]] and [[cssCompressor]] for more details. + * + * @property \yii\web\AssetManager $assetManager Asset manager instance. Note that the type of this property + * differs in getter and setter. See [[getAssetManager()]] and [[setAssetManager()]] for details. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class AssetController extends Controller +{ + /** + * @var string controller default action ID. + */ + public $defaultAction = 'compress'; + /** + * @var array list of asset bundles to be compressed. + */ + public $bundles = array(); + /** + * @var array list of asset bundles, which represents output compressed files. + * You can specify the name of the output compressed file using 'css' and 'js' keys: + * For example: + * + * ~~~ + * 'app\config\AllAsset' => array( + * 'js' => 'js/all-{ts}.js', + * 'css' => 'css/all-{ts}.css', + * 'depends' => array( ... ), + * ) + * ~~~ + * + * File names can contain placeholder "{ts}", which will be filled by current timestamp, while + * file creation. + */ + public $targets = array(); + /** + * @var string|callback JavaScript file compressor. + * If a string, it is treated as shell command template, which should contain + * placeholders {from} - source file name - and {to} - output file name. + * Otherwise, it is treated as PHP callback, which should perform the compression. + * + * Default value relies on usage of "Closure Compiler" + * @see https://developers.google.com/closure/compiler/ + */ + public $jsCompressor = 'java -jar compiler.jar --js {from} --js_output_file {to}'; + /** + * @var string|callback CSS file compressor. + * If a string, it is treated as shell command template, which should contain + * placeholders {from} - source file name - and {to} - output file name. + * Otherwise, it is treated as PHP callback, which should perform the compression. + * + * Default value relies on usage of "YUI Compressor" + * @see https://github.com/yui/yuicompressor/ + */ + public $cssCompressor = 'java -jar yuicompressor.jar {from} -o {to}'; + + /** + * @var array|\yii\web\AssetManager [[yii\web\AssetManager]] instance or its array configuration, which will be used + * for assets processing. + */ + private $_assetManager = array(); + + /** + * Returns the asset manager instance. + * @throws \yii\console\Exception on invalid configuration. + * @return \yii\web\AssetManager asset manager instance. + */ + public function getAssetManager() + { + if (!is_object($this->_assetManager)) { + $options = $this->_assetManager; + if (!isset($options['class'])) { + $options['class'] = 'yii\\web\\AssetManager'; + } + if (!isset($options['basePath'])) { + throw new Exception("Please specify 'basePath' for the 'assetManager' option."); + } + if (!isset($options['baseUrl'])) { + throw new Exception("Please specify 'baseUrl' for the 'assetManager' option."); + } + $this->_assetManager = Yii::createObject($options); + } + return $this->_assetManager; + } + + /** + * Sets asset manager instance or configuration. + * @param \yii\web\AssetManager|array $assetManager asset manager instance or its array configuration. + * @throws \yii\console\Exception on invalid argument type. + */ + public function setAssetManager($assetManager) + { + if (is_scalar($assetManager)) { + throw new Exception('"' . get_class($this) . '::assetManager" should be either object or array - "' . gettype($assetManager) . '" given.'); + } + $this->_assetManager = $assetManager; + } + + /** + * Combines and compresses the asset files according to the given configuration. + * During the process new asset bundle configuration file will be created. + * You should replace your original asset bundle configuration with this file in order to use compressed files. + * @param string $configFile configuration file name. + * @param string $bundleFile output asset bundles configuration file name. + */ + public function actionCompress($configFile, $bundleFile) + { + $this->loadConfiguration($configFile); + $bundles = $this->loadBundles($this->bundles); + $targets = $this->loadTargets($this->targets, $bundles); + $this->publishBundles($bundles, $this->assetManager); + $timestamp = time(); + foreach ($targets as $name => $target) { + echo "Creating output bundle '{$name}':\n"; + if (!empty($target->js)) { + $this->buildTarget($target, 'js', $bundles, $timestamp); + } + if (!empty($target->css)) { + $this->buildTarget($target, 'css', $bundles, $timestamp); + } + echo "\n"; + } + + $targets = $this->adjustDependency($targets, $bundles); + $this->saveTargets($targets, $bundleFile); + } + + /** + * Applies configuration from the given file to self instance. + * @param string $configFile configuration file name. + * @throws \yii\console\Exception on failure. + */ + protected function loadConfiguration($configFile) + { + echo "Loading configuration from '{$configFile}'...\n"; + foreach (require($configFile) as $name => $value) { + if (property_exists($this, $name) || $this->canSetProperty($name)) { + $this->$name = $value; + } else { + throw new Exception("Unknown configuration option: $name"); + } + } + + $this->getAssetManager(); // check if asset manager configuration is correct + } + + /** + * Creates full list of source asset bundles. + * @param string[] $bundles list of asset bundle names + * @return \yii\web\AssetBundle[] list of source asset bundles. + */ + protected function loadBundles($bundles) + { + echo "Collecting source bundles information...\n"; + + $am = $this->getAssetManager(); + $result = array(); + foreach ($bundles as $name) { + $result[$name] = $am->getBundle($name); + } + foreach ($result as $bundle) { + $this->loadDependency($bundle, $result); + } + + return $result; + } + + /** + * Loads asset bundle dependencies recursively. + * @param \yii\web\AssetBundle $bundle bundle instance + * @param array $result already loaded bundles list. + * @throws Exception on failure. + */ + protected function loadDependency($bundle, &$result) + { + $am = $this->getAssetManager(); + foreach ($bundle->depends as $name) { + if (!isset($result[$name])) { + $dependencyBundle = $am->getBundle($name); + $result[$name] = false; + $this->loadDependency($dependencyBundle, $result); + $result[$name] = $dependencyBundle; + } elseif ($result[$name] === false) { + throw new Exception("A circular dependency is detected for bundle '$name'."); + } + } + } + + /** + * Creates full list of output asset bundles. + * @param array $targets output asset bundles configuration. + * @param \yii\web\AssetBundle[] $bundles list of source asset bundles. + * @return \yii\web\AssetBundle[] list of output asset bundles. + * @throws Exception on failure. + */ + 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'] = $name; + $targets[$name] = Yii::createObject($target); + } + return $targets; + } + + /** + * Publishes given asset bundles. + * @param \yii\web\AssetBundle[] $bundles asset bundles to be published. + */ + protected function publishBundles($bundles) + { + echo "\nPublishing bundles:\n"; + $assetManager = $this->getAssetManager(); + foreach ($bundles as $name => $bundle) { + $bundle->publish($assetManager); + echo " '".$name."' published.\n"; + } + echo "\n"; + } + + /** + * Builds output asset bundle. + * @param \yii\web\AssetBundle $target output asset bundle + * @param string $type either 'js' or 'css'. + * @param \yii\web\AssetBundle[] $bundles source asset bundles. + * @param integer $timestamp current timestamp. + * @throws Exception on failure. + */ + 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); + } + + /** + * Adjust dependencies between asset bundles in the way source bundles begin to depend on output ones. + * @param \yii\web\AssetBundle[] $targets output asset bundles. + * @param \yii\web\AssetBundle[] $bundles source asset bundles. + * @return \yii\web\AssetBundle[] output asset bundles. + */ + protected function adjustDependency($targets, $bundles) + { + echo "Creating new bundle configuration...\n"; + + $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; + } + + /** + * Registers asset bundles including their dependencies. + * @param \yii\web\AssetBundle[] $bundles asset bundles list. + * @param string $name bundle name. + * @param array $registered stores already registered names. + * @throws Exception if circular dependency is detected. + */ + 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'."); + } + } + + /** + * Saves new asset bundles configuration. + * @param \yii\web\AssetBundle[] $targets list of asset bundles to be saved. + * @param string $bundleFile output file name. + * @throws \yii\console\Exception on failure. + */ + 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()); + $bundleFileContent = <<<EOD +<?php +/** + * This file is generated by the "yii {$this->id}" command. + * DO NOT MODIFY THIS FILE DIRECTLY. + * @version {$version} + */ +return {$array}; +EOD; + if (!file_put_contents($bundleFile, $bundleFileContent)) { + throw new Exception("Unable to write output bundle configuration at '{$bundleFile}'."); + } + echo "Output bundle configuration created at '{$bundleFile}'.\n"; + } + + /** + * Compresses given JavaScript files and combines them into the single one. + * @param array $inputFiles list of source file names. + * @param string $outputFile output file name. + * @throws \yii\console\Exception on failure + */ + protected function compressJsFiles($inputFiles, $outputFile) + { + if (empty($inputFiles)) { + return; + } + echo " Compressing JavaScript files...\n"; + if (is_string($this->jsCompressor)) { + $tmpFile = $outputFile . '.tmp'; + $this->combineJsFiles($inputFiles, $tmpFile); + echo shell_exec(strtr($this->jsCompressor, array( + '{from}' => escapeshellarg($tmpFile), + '{to}' => escapeshellarg($outputFile), + ))); + @unlink($tmpFile); + } else { + call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile); + } + if (!file_exists($outputFile)) { + throw new Exception("Unable to compress JavaScript files into '{$outputFile}'."); + } + echo " JavaScript files compressed into '{$outputFile}'.\n"; + } + + /** + * Compresses given CSS files and combines them into the single one. + * @param array $inputFiles list of source file names. + * @param string $outputFile output file name. + * @throws \yii\console\Exception on failure + */ + protected function compressCssFiles($inputFiles, $outputFile) + { + if (empty($inputFiles)) { + return; + } + echo " Compressing CSS files...\n"; + if (is_string($this->cssCompressor)) { + $tmpFile = $outputFile . '.tmp'; + $this->combineCssFiles($inputFiles, $tmpFile); + echo shell_exec(strtr($this->cssCompressor, array( + '{from}' => escapeshellarg($tmpFile), + '{to}' => escapeshellarg($outputFile), + ))); + @unlink($tmpFile); + } else { + call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile); + } + if (!file_exists($outputFile)) { + throw new Exception("Unable to compress CSS files into '{$outputFile}'."); + } + echo " CSS files compressed into '{$outputFile}'.\n"; + } + + /** + * Combines JavaScript files into a single one. + * @param array $inputFiles source file names. + * @param string $outputFile output file name. + * @throws \yii\console\Exception on failure. + */ + public function combineJsFiles($inputFiles, $outputFile) + { + $content = ''; + foreach ($inputFiles as $file) { + $content .= "/*** BEGIN FILE: $file ***/\n" + . file_get_contents($file) + . "/*** END FILE: $file ***/\n"; + } + if (!file_put_contents($outputFile, $content)) { + throw new Exception("Unable to write output JavaScript file '{$outputFile}'."); + } + } + + /** + * Combines CSS files into a single one. + * @param array $inputFiles source file names. + * @param string $outputFile output file name. + * @throws \yii\console\Exception on failure. + */ + public function combineCssFiles($inputFiles, $outputFile) + { + $content = ''; + foreach ($inputFiles as $file) { + $content .= "/*** BEGIN FILE: $file ***/\n" + . $this->adjustCssUrl(file_get_contents($file), dirname($file), dirname($outputFile)) + . "/*** END FILE: $file ***/\n"; + } + if (!file_put_contents($outputFile, $content)) { + throw new Exception("Unable to write output CSS file '{$outputFile}'."); + } + } + + /** + * Adjusts CSS content allowing URL references pointing to the original resources. + * @param string $cssContent source CSS content. + * @param string $inputFilePath input CSS file name. + * @param string $outputFilePath output CSS file name. + * @return string adjusted CSS content. + */ + protected function adjustCssUrl($cssContent, $inputFilePath, $outputFilePath) + { + $sharedPathParts = array(); + $inputFilePathParts = explode('/', $inputFilePath); + $inputFilePathPartsCount = count($inputFilePathParts); + $outputFilePathParts = explode('/', $outputFilePath); + $outputFilePathPartsCount = count($outputFilePathParts); + for ($i =0; $i < $inputFilePathPartsCount && $i < $outputFilePathPartsCount; $i++) { + if ($inputFilePathParts[$i] == $outputFilePathParts[$i]) { + $sharedPathParts[] = $inputFilePathParts[$i]; + } else { + break; + } + } + $sharedPath = implode('/', $sharedPathParts); + + $inputFileRelativePath = trim(str_replace($sharedPath, '', $inputFilePath), '/'); + $outputFileRelativePath = trim(str_replace($sharedPath, '', $outputFilePath), '/'); + $inputFileRelativePathParts = explode('/', $inputFileRelativePath); + $outputFileRelativePathParts = explode('/', $outputFileRelativePath); + + $callback = function ($matches) use ($inputFileRelativePathParts, $outputFileRelativePathParts) { + $fullMatch = $matches[0]; + $inputUrl = $matches[1]; + + if (preg_match('/https?:\/\//is', $inputUrl)) { + return $fullMatch; + } + + $outputUrlParts = array_fill(0, count($outputFileRelativePathParts), '..'); + $outputUrlParts = array_merge($outputUrlParts, $inputFileRelativePathParts); + + if (strpos($inputUrl, '/') !== false) { + $inputUrlParts = explode('/', $inputUrl); + foreach ($inputUrlParts as $key => $inputUrlPart) { + if ($inputUrlPart == '..') { + array_pop($outputUrlParts); + unset($inputUrlParts[$key]); + } + } + $outputUrlParts[] = implode('/', $inputUrlParts); + } else { + $outputUrlParts[] = $inputUrl; + } + $outputUrl = implode('/', $outputUrlParts); + + return str_replace($inputUrl, $outputUrl, $fullMatch); + }; + + $cssContent = preg_replace_callback('/url\(["\']?([^"]*)["\']?\)/is', $callback, $cssContent); + + return $cssContent; + } + + /** + * Creates template of configuration file for [[actionCompress]]. + * @param string $configFile output file name. + * @throws \yii\console\Exception on failure. + */ + public function actionTemplate($configFile) + { + $template = <<<EOD +<?php +/** + * Configuration file for the "yii asset" console command. + * Note that in the console environment, some path aliases like '@webroot' and '@web' may not exist. + * Please define these missing path aliases. + */ +return array( + // The list of asset bundles to compress: + 'bundles' => array( + // 'yii\web\YiiAsset', + // 'yii\web\JqueryAsset', + ), + // Asset bundle for compression output: + 'targets' => array( + 'app\config\AllAsset' => array( + 'basePath' => 'path/to/web', + 'baseUrl' => '', + 'js' => 'js/all-{ts}.js', + 'css' => 'css/all-{ts}.css', + ), + ), + // Asset manager configuration: + 'assetManager' => array( + 'basePath' => __DIR__, + 'baseUrl' => '', + ), +); +EOD; + if (file_exists($configFile)) { + if (!$this->confirm("File '{$configFile}' already exists. Do you wish to overwrite it?")) { + return; + } + } + if (!file_put_contents($configFile, $template)) { + throw new Exception("Unable to write template file '{$configFile}'."); + } else { + echo "Configuration file template created at '{$configFile}'.\n\n"; + } + } +} diff --git a/yii/console/controllers/CacheController.php b/framework/yii/console/controllers/CacheController.php similarity index 72% rename from yii/console/controllers/CacheController.php rename to framework/yii/console/controllers/CacheController.php index 6765f9b..95765fa 100644 --- a/yii/console/controllers/CacheController.php +++ b/framework/yii/console/controllers/CacheController.php @@ -7,6 +7,7 @@ namespace yii\console\controllers; +use Yii; use yii\console\Controller; use yii\console\Exception; use yii\caching\Cache; @@ -19,9 +20,28 @@ use yii\caching\Cache; */ class CacheController extends Controller { + /** + * Lists the caches that can be flushed. + */ public function actionIndex() { - $this->forward('help/index', array('-args' => array('cache/flush'))); + $caches = array(); + $components = Yii::$app->getComponents(); + foreach ($components as $name => $component) { + if ($component instanceof Cache) { + $caches[$name] = get_class($component); + } elseif (is_array($component) && isset($component['class']) && strpos($component['class'], 'Cache') !== false) { + $caches[$name] = $component['class']; + } + } + if (!empty($caches)) { + echo "The following caches can be flushed:\n\n"; + foreach ($caches as $name => $class) { + echo " * $name: $class\n"; + } + } else { + echo "No cache is used.\n"; + } } /** @@ -33,12 +53,12 @@ class CacheController extends Controller public function actionFlush($component = 'cache') { /** @var $cache Cache */ - $cache = \Yii::$app->getComponent($component); - if(!$cache || !$cache instanceof Cache) { + $cache = Yii::$app->getComponent($component); + if (!$cache || !$cache instanceof Cache) { throw new Exception('Application component "'.$component.'" is not defined or not a cache.'); } - if(!$cache->flush()) { + if (!$cache->flush()) { throw new Exception('Unable to flush cache.'); } diff --git a/yii/console/controllers/HelpController.php b/framework/yii/console/controllers/HelpController.php similarity index 81% rename from yii/console/controllers/HelpController.php rename to framework/yii/console/controllers/HelpController.php index 6a66fd0..678d9f9 100644 --- a/yii/console/controllers/HelpController.php +++ b/framework/yii/console/controllers/HelpController.php @@ -12,8 +12,8 @@ use yii\base\Application; use yii\base\InlineAction; use yii\console\Controller; use yii\console\Exception; -use yii\console\Request; -use yii\helpers\StringHelper; +use yii\helpers\Console; +use yii\helpers\Inflector; /** * This command provides help information about console commands. @@ -25,12 +25,14 @@ use yii\helpers\StringHelper; * This command can be used as follows on command line: * * ~~~ - * yiic help [command name] + * yii help [command name] * ~~~ * * In the above, if the command name is not provided, all * available commands will be displayed. * + * @property array $commands All available command names. This property is read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -41,8 +43,8 @@ class HelpController extends Controller * about a particular command. For example, * * ~~~ - * yiic help # list available commands - * yiic help message # display help info about "message" + * yii help # list available commands + * yii help message # display help info about "message" * ~~~ * * @param string $command The name of the command to show help about. @@ -55,8 +57,8 @@ class HelpController extends Controller if ($command !== null) { $result = Yii::$app->createController($command); if ($result === false) { - throw new Exception(Yii::t('yii|No help for unknown command "{command}".', array( - '{command}' => $command, + throw new Exception(Yii::t('yii', 'No help for unknown command "{command}".', array( + 'command' => $this->ansiFormat($command, Console::FG_YELLOW), ))); } @@ -96,7 +98,7 @@ class HelpController extends Controller foreach ($class->getMethods() as $method) { $name = $method->getName(); if ($method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0 && $name !== 'actions') { - $actions[] = StringHelper::camel2id(substr($name, 6)); + $actions[] = Inflector::camel2id(substr($name, 6)); } } sort($actions); @@ -129,7 +131,7 @@ class HelpController extends Controller $files = scandir($module->getControllerPath()); foreach ($files as $file) { if (strcmp(substr($file, -14), 'Controller.php') === 0) { - $commands[] = $prefix . lcfirst(substr(basename($file), 0, -14)); + $commands[] = $prefix . Inflector::camel2id(substr(basename($file), 0, -14)); } } @@ -143,14 +145,15 @@ class HelpController extends Controller { $commands = $this->getCommands(); if (!empty($commands)) { - echo "The following commands are available:\n\n"; + $this->stdout("\nThe following commands are available:\n\n", Console::BOLD); foreach ($commands as $command) { - echo "* $command\n"; + echo "- " . $this->ansiFormat($command, Console::FG_YELLOW) . "\n"; } - echo "\nTo see the help of each command, enter:\n"; - echo "\n yiic help <command-name>\n\n"; + $this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD); + echo "\n yii " . $this->ansiFormat('help', Console::FG_YELLOW) . ' ' + . $this->ansiFormat('<command-name>', Console::FG_CYAN) . "\n\n"; } else { - echo "\nNo commands are found.\n"; + $this->stdout("\nNo commands are found.\n\n", Console::BOLD); } } @@ -167,19 +170,18 @@ class HelpController extends Controller } if ($comment !== '') { - echo "\nDESCRIPTION\n"; - echo "\n" . $comment . "\n\n"; + $this->stdout("\nDESCRIPTION\n", Console::BOLD); + echo "\n" . Console::renderColoredString($comment) . "\n\n"; } $actions = $this->getActions($controller); if (!empty($actions)) { - echo "\nSUB-COMMANDS\n\n"; + $this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD); $prefix = $controller->getUniqueId(); foreach ($actions as $action) { + echo '- ' . $this->ansiFormat($prefix.'/'.$action, Console::FG_YELLOW); if ($action === $controller->defaultAction) { - echo "* $prefix/$action (default)"; - } else { - echo "* $prefix/$action"; + $this->stdout(' (default)', Console::FG_GREEN); } $summary = $this->getActionSummary($controller, $action); if ($summary !== '') { @@ -187,8 +189,9 @@ class HelpController extends Controller } echo "\n"; } - echo "\n\nTo see the detailed information about individual sub-commands, enter:\n"; - echo "\n yiic help <sub-command>\n\n"; + echo "\nTo see the detailed information about individual sub-commands, enter:\n"; + echo "\n yii " . $this->ansiFormat('help', Console::FG_YELLOW) . ' ' + . $this->ansiFormat('<sub-command>', Console::FG_CYAN) . "\n\n"; } } @@ -239,8 +242,8 @@ class HelpController extends Controller { $action = $controller->createAction($actionID); if ($action === null) { - throw new Exception(Yii::t('yii|No help for unknown sub-command "{command}".', array( - '{command}' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'), + throw new Exception(Yii::t('yii', 'No help for unknown sub-command "{command}".', array( + 'command' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'), ))); } if ($action instanceof InlineAction) { @@ -253,25 +256,25 @@ class HelpController extends Controller $options = $this->getOptionHelps($controller); if ($tags['description'] !== '') { - echo "\nDESCRIPTION"; - echo "\n\n" . $tags['description'] . "\n\n"; + $this->stdout("\nDESCRIPTION\n", Console::BOLD); + echo "\n" . Console::renderColoredString($tags['description']) . "\n\n"; } - echo "\nUSAGE\n\n"; + $this->stdout("\nUSAGE\n\n", Console::BOLD); if ($action->id === $controller->defaultAction) { - echo 'yiic ' . $controller->getUniqueId(); + echo 'yii ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW); } else { - echo "yiic " . $action->getUniqueId(); + echo 'yii ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW); } list ($required, $optional) = $this->getArgHelps($method, isset($tags['param']) ? $tags['param'] : array()); - if (!empty($required)) { - echo ' <' . implode('> <', array_keys($required)) . '>'; + foreach ($required as $arg => $description) { + $this->stdout(' <' . $arg . '>', Console::FG_CYAN); } - if (!empty($optional)) { - echo ' [' . implode('] [', array_keys($optional)) . ']'; + foreach ($optional as $arg => $description) { + $this->stdout(' [' . $arg . ']', Console::FG_CYAN); } if (!empty($options)) { - echo ' [...options...]'; + $this->stdout(' [...options...]', Console::FG_RED); } echo "\n\n"; @@ -281,7 +284,7 @@ class HelpController extends Controller $options = $this->getOptionHelps($controller); if (!empty($options)) { - echo "\nOPTIONS\n\n"; + $this->stdout("\nOPTIONS\n\n", Console::BOLD); echo implode("\n\n", $options) . "\n\n"; } } @@ -310,9 +313,9 @@ class HelpController extends Controller $comment = $tag; } if ($param->isDefaultValueAvailable()) { - $optional[$name] = $this->formatOptionHelp('* ' . $name, false, $type, $param->getDefaultValue(), $comment); + $optional[$name] = $this->formatOptionHelp('- ' . $this->ansiFormat($name, Console::FG_CYAN), false, $type, $param->getDefaultValue(), $comment); } else { - $required[$name] = $this->formatOptionHelp('* ' . $name, true, $type, null, $comment); + $required[$name] = $this->formatOptionHelp('- ' . $this->ansiFormat($name, Console::FG_CYAN), true, $type, null, $comment); } } @@ -352,9 +355,9 @@ class HelpController extends Controller $type = null; $comment = $doc; } - $options[$name] = $this->formatOptionHelp('--' . $name, false, $type, $defaultValue, $comment); + $options[$name] = $this->formatOptionHelp($this->ansiFormat('--' . $name, Console::FG_RED), false, $type, $defaultValue, $comment); } else { - $options[$name] = $this->formatOptionHelp('--' . $name, false, null, $defaultValue, ''); + $options[$name] = $this->formatOptionHelp($this->ansiFormat('--' . $name, Console::FG_RED), false, null, $defaultValue, ''); } } ksort($options); @@ -404,6 +407,10 @@ class HelpController extends Controller if ($type === null) { $type = gettype($defaultValue); } + if (is_bool($defaultValue)) { + // show as integer to avoid confusion + $defaultValue = (int)$defaultValue; + } $doc = "$type (defaults to " . var_export($defaultValue, true) . ")"; } elseif (trim($type) !== '') { $doc = $type; diff --git a/framework/yii/console/controllers/MessageController.php b/framework/yii/console/controllers/MessageController.php new file mode 100644 index 0000000..306eb41 --- /dev/null +++ b/framework/yii/console/controllers/MessageController.php @@ -0,0 +1,237 @@ +<?php +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\console\controllers; + +use Yii; +use yii\console\Controller; +use yii\console\Exception; +use yii\helpers\FileHelper; + +/** + * This command extracts messages to be translated from source files. + * The extracted messages are saved as PHP message source files + * under the specified directory. + * + * Usage: + * 1. Create a configuration file using the 'message/config' command: + * yii message/config /path/to/myapp/messages/config.php + * 2. Edit the created config file, adjusting it for your web application needs. + * 3. Run the 'message/extract' extract, using created config: + * yii message /path/to/myapp/messages/config.php + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class MessageController extends Controller +{ + /** + * @var string controller default action ID. + */ + public $defaultAction = 'extract'; + + + /** + * Creates a configuration file for the "extract" command. + * + * The generated configuration file contains detailed instructions on + * how to customize it to fit for your needs. After customization, + * you may use this configuration file with the "extract" command. + * + * @param string $filePath output file name. + * @throws Exception on failure. + */ + public function actionConfig($filePath) + { + if (file_exists($filePath)) { + if (!$this->confirm("File '{$filePath}' already exists. Do you wish to overwrite it?")) { + return; + } + } + copy(Yii::getAlias('@yii/views/messageConfig.php'), $filePath); + echo "Configuration file template created at '{$filePath}'.\n\n"; + } + + /** + * Extracts messages to be translated from source code. + * + * This command will search through source code files and extract + * messages that need to be translated in different languages. + * + * @param string $configFile the path of the configuration file. + * You may use the "yii message/config" command to generate + * this file and then customize it for your needs. + * @throws Exception on failure. + */ + public function actionExtract($configFile) + { + if (!is_file($configFile)) { + throw new Exception("The configuration file does not exist: $configFile"); + } + + $config = array_merge(array( + 'translator' => 'Yii::t', + 'overwrite' => false, + 'removeUnused' => false, + 'sort' => false, + ), require($configFile)); + + if (!isset($config['sourcePath'], $config['messagePath'], $config['languages'])) { + throw new Exception('The configuration file must specify "sourcePath", "messagePath" and "languages".'); + } + if (!is_dir($config['sourcePath'])) { + throw new Exception("The source path {$config['sourcePath']} is not a valid directory."); + } + if (!is_dir($config['messagePath'])) { + throw new Exception("The message path {$config['messagePath']} is not a valid directory."); + } + if (empty($config['languages'])) { + throw new Exception("Languages cannot be empty."); + } + + $files = FileHelper::findFiles(realpath($config['sourcePath']), $config); + + $messages = array(); + foreach ($files as $file) { + $messages = array_merge_recursive($messages, $this->extractMessages($file, $config['translator'])); + } + + foreach ($config['languages'] as $language) { + $dir = $config['messagePath'] . DIRECTORY_SEPARATOR . $language; + if (!is_dir($dir)) { + @mkdir($dir); + } + foreach ($messages as $category => $msgs) { + $msgs = array_values(array_unique($msgs)); + $this->generateMessageFile($msgs, $dir . DIRECTORY_SEPARATOR . $category . '.php', + $config['overwrite'], + $config['removeUnused'], + $config['sort']); + } + } + } + + /** + * Extracts messages from a file + * + * @param string $fileName name of the file to extract messages from + * @param string $translator name of the function used to translate messages + * @return array + */ + protected function extractMessages($fileName, $translator) + { + echo "Extracting messages from $fileName...\n"; + $subject = file_get_contents($fileName); + $messages = array(); + if (!is_array($translator)) { + $translator = array($translator); + } + foreach ($translator as $currentTranslator) { + $n = preg_match_all( + '/\b' . $currentTranslator . '\s*\(\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*,\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*[,\)]/s', + $subject, $matches, PREG_SET_ORDER); + for ($i = 0; $i < $n; ++$i) { + if (($pos = strpos($matches[$i][1], '.')) !== false) { + $category = substr($matches[$i][1], $pos + 1, -1); + } else { + $category = substr($matches[$i][1], 1, -1); + } + $message = $matches[$i][2]; + $messages[$category][] = eval("return $message;"); // use eval to eliminate quote escape + } + } + return $messages; + } + + /** + * Writes messages into file + * + * @param array $messages + * @param string $fileName name of the file to write to + * @param boolean $overwrite if existing file should be overwritten without backup + * @param boolean $removeUnused if obsolete translations should be removed + * @param boolean $sort if translations should be sorted + */ + protected function generateMessageFile($messages, $fileName, $overwrite, $removeUnused, $sort) + { + echo "Saving messages to $fileName..."; + if (is_file($fileName)) { + $translated = require($fileName); + sort($messages); + ksort($translated); + if (array_keys($translated) == $messages) { + echo "nothing new...skipped.\n"; + return; + } + $merged = array(); + $untranslated = array(); + foreach ($messages as $message) { + if (array_key_exists($message, $translated) && strlen($translated[$message]) > 0) { + $merged[$message] = $translated[$message]; + } else { + $untranslated[] = $message; + } + } + ksort($merged); + sort($untranslated); + $todo = array(); + foreach ($untranslated as $message) { + $todo[$message] = ''; + } + ksort($translated); + foreach ($translated as $message => $translation) { + if (!isset($merged[$message]) && !isset($todo[$message]) && !$removeUnused) { + if (substr($translation, 0, 2) === '@@' && substr($translation, -2) === '@@') { + $todo[$message] = $translation; + } else { + $todo[$message] = '@@' . $translation . '@@'; + } + } + } + $merged = array_merge($todo, $merged); + if ($sort) { + ksort($merged); + } + if (false === $overwrite) { + $fileName .= '.merged'; + } + echo "translation merged.\n"; + } else { + $merged = array(); + foreach ($messages as $message) { + $merged[$message] = ''; + } + ksort($merged); + echo "saved.\n"; + } + $array = str_replace("\r", '', var_export($merged, true)); + $content = <<<EOD +<?php +/** + * Message translations. + * + * This file is automatically generated by 'yii {$this->id}' command. + * It contains the localizable messages extracted from source code. + * You may modify this file by translating the extracted messages. + * + * Each array element represents the translation (value) of a message (key). + * If the value is empty, the message is considered as not translated. + * Messages that no longer need translation will have their translations + * enclosed between a pair of '@@' marks. + * + * Message string can be used with plural forms format. Check i18n section + * of the guide for details. + * + * NOTE: this file must be saved in UTF-8 encoding. + */ +return $array; + +EOD; + file_put_contents($fileName, $content); + } +} diff --git a/yii/console/controllers/MigrateController.php b/framework/yii/console/controllers/MigrateController.php similarity index 90% rename from yii/console/controllers/MigrateController.php rename to framework/yii/console/controllers/MigrateController.php index fb06c66..e9e3973 100644 --- a/yii/console/controllers/MigrateController.php +++ b/framework/yii/console/controllers/MigrateController.php @@ -1,633 +1,637 @@ -<?php -/** - * @author Qiang Xue <qiang.xue@gmail.com> - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\console\controllers; - -use Yii; -use yii\console\Exception; -use yii\console\Controller; -use yii\db\Connection; -use yii\db\Query; -use yii\helpers\ArrayHelper; - -/** - * This command manages application migrations. - * - * A migration means a set of persistent changes to the application environment - * that is shared among different developers. For example, in an application - * backed by a database, a migration may refer to a set of changes to - * the database, such as creating a new table, adding a new table column. - * - * This command provides support for tracking the migration history, upgrading - * or downloading with migrations, and creating new migration skeletons. - * - * The migration history is stored in a database table named - * as [[migrationTable]]. The table will be automatically created the first time - * this command is executed, if it does not exist. You may also manually - * create it as follows: - * - * ~~~ - * CREATE TABLE tbl_migration ( - * version varchar(255) PRIMARY KEY, - * apply_time integer - * ) - * ~~~ - * - * Below are some common usages of this command: - * - * ~~~ - * # creates a new migration named 'create_user_table' - * yiic migrate/create create_user_table - * - * # applies ALL new migrations - * yiic migrate - * - * # reverts the last applied migration - * yiic migrate/down - * ~~~ - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class MigrateController extends Controller -{ - /** - * The name of the dummy migration that marks the beginning of the whole migration history. - */ - const BASE_MIGRATION = 'm000000_000000_base'; - - /** - * @var string the default command action. - */ - public $defaultAction = 'up'; - /** - * @var string the directory storing the migration classes. This can be either - * a path alias or a directory. - */ - public $migrationPath = '@app/migrations'; - /** - * @var string the name of the table for keeping applied migration information. - */ - public $migrationTable = 'tbl_migration'; - /** - * @var string the template file for generating new migrations. - * This can be either a path alias (e.g. "@app/migrations/template.php") - * or a file path. - */ - public $templateFile = '@yii/views/migration.php'; - /** - * @var boolean whether to execute the migration in an interactive mode. - */ - public $interactive = true; - /** - * @var Connection|string the DB connection object or the application - * component ID of the DB connection. - */ - public $db = 'db'; - - /** - * Returns the names of the global options for this command. - * @return array the names of the global options for this command. - */ - public function globalOptions() - { - return array('migrationPath', 'migrationTable', 'db', 'templateFile', 'interactive'); - } - - /** - * This method is invoked right before an action is to be executed (after all possible filters.) - * It checks the existence of the [[migrationPath]]. - * @param \yii\base\Action $action the action to be executed. - * @return boolean whether the action should continue to be executed. - * @throws Exception if the migration directory does not exist. - */ - public function beforeAction($action) - { - if (parent::beforeAction($action)) { - $path = Yii::getAlias($this->migrationPath); - if (!is_dir($path)) { - throw new Exception("The migration directory \"{$this->migrationPath}\" does not exist."); - } - $this->migrationPath = $path; - - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new Exception("The 'db' option must refer to the application component ID of a DB connection."); - } - - $version = Yii::getVersion(); - echo "Yii Migration Tool (based on Yii v{$version})\n\n"; - return true; - } else { - return false; - } - } - - /** - * Upgrades the application by applying new migrations. - * For example, - * - * ~~~ - * yiic migrate # apply all new migrations - * yiic migrate 3 # apply the first 3 new migrations - * ~~~ - * - * @param integer $limit the number of new migrations to be applied. If 0, it means - * applying all available new migrations. - */ - public function actionUp($limit = 0) - { - $migrations = $this->getNewMigrations(); - if (empty($migrations)) { - echo "No new migration found. Your system is up-to-date.\n"; - Yii::$app->end(); - } - - $total = count($migrations); - $limit = (int)$limit; - if ($limit > 0) { - $migrations = array_slice($migrations, 0, $limit); - } - - $n = count($migrations); - if ($n === $total) { - echo "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n"; - } else { - echo "Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n"; - } - - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if (!$this->migrateUp($migration)) { - echo "\nMigration failed. The rest of the migrations are canceled.\n"; - return; - } - } - echo "\nMigrated up successfully.\n"; - } - } - - /** - * Downgrades the application by reverting old migrations. - * For example, - * - * ~~~ - * yiic migrate/down # revert the last migration - * yiic migrate/down 3 # revert the last 3 migrations - * ~~~ - * - * @param integer $limit the number of migrations to be reverted. Defaults to 1, - * meaning the last applied migration will be reverted. - * @throws Exception if the number of the steps specified is less than 1. - */ - public function actionDown($limit = 1) - { - $limit = (int)$limit; - if ($limit < 1) { - throw new Exception("The step argument must be greater than 0."); - } - - $migrations = $this->getMigrationHistory($limit); - if (empty($migrations)) { - echo "No migration has been done before.\n"; - return; - } - $migrations = array_keys($migrations); - - $n = count($migrations); - echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n"; - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if (!$this->migrateDown($migration)) { - echo "\nMigration failed. The rest of the migrations are canceled.\n"; - return; - } - } - echo "\nMigrated down successfully.\n"; - } - } - - /** - * Redoes the last few migrations. - * - * This command will first revert the specified migrations, and then apply - * them again. For example, - * - * ~~~ - * yiic migrate/redo # redo the last applied migration - * yiic migrate/redo 3 # redo the last 3 applied migrations - * ~~~ - * - * @param integer $limit the number of migrations to be redone. Defaults to 1, - * meaning the last applied migration will be redone. - * @throws Exception if the number of the steps specified is less than 1. - */ - public function actionRedo($limit = 1) - { - $limit = (int)$limit; - if ($limit < 1) { - throw new Exception("The step argument must be greater than 0."); - } - - $migrations = $this->getMigrationHistory($limit); - if (empty($migrations)) { - echo "No migration has been done before.\n"; - return; - } - $migrations = array_keys($migrations); - - $n = count($migrations); - echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n"; - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if (!$this->migrateDown($migration)) { - echo "\nMigration failed. The rest of the migrations are canceled.\n"; - return; - } - } - foreach (array_reverse($migrations) as $migration) { - if (!$this->migrateUp($migration)) { - echo "\nMigration failed. The rest of the migrations migrations are canceled.\n"; - return; - } - } - echo "\nMigration redone successfully.\n"; - } - } - - /** - * Upgrades or downgrades till the specified version. - * - * This command will first revert the specified migrations, and then apply - * them again. For example, - * - * ~~~ - * yiic migrate/to 101129_185401 # using timestamp - * yiic migrate/to m101129_185401_create_user_table # using full name - * ~~~ - * - * @param string $version the version name that the application should be migrated to. - * This can be either the timestamp or the full name of the migration. - * @throws Exception if the version argument is invalid - */ - public function actionTo($version) - { - $originalVersion = $version; - if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { - $version = 'm' . $matches[1]; - } else { - throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); - } - - // try migrate up - $migrations = $this->getNewMigrations(); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - $this->actionUp($i + 1); - return; - } - } - - // try migrate down - $migrations = array_keys($this->getMigrationHistory(-1)); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($i === 0) { - echo "Already at '$originalVersion'. Nothing needs to be done.\n"; - } else { - $this->actionDown($i); - } - return; - } - } - - throw new Exception("Unable to find the version '$originalVersion'."); - } - - /** - * Modifies the migration history to the specified version. - * - * No actual migration will be performed. - * - * ~~~ - * yiic migrate/mark 101129_185401 # using timestamp - * yiic migrate/mark m101129_185401_create_user_table # using full name - * ~~~ - * - * @param string $version the version at which the migration history should be marked. - * This can be either the timestamp or the full name of the migration. - * @throws Exception if the version argument is invalid or the version cannot be found. - */ - public function actionMark($version) - { - $originalVersion = $version; - if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { - $version = 'm' . $matches[1]; - } else { - throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); - } - - // try mark up - $migrations = $this->getNewMigrations(); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($this->confirm("Set migration history at $originalVersion?")) { - $command = $this->db->createCommand(); - for ($j = 0; $j <= $i; ++$j) { - $command->insert($this->migrationTable, array( - 'version' => $migrations[$j], - 'apply_time' => time(), - ))->execute(); - } - echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; - } - return; - } - } - - // try mark down - $migrations = array_keys($this->getMigrationHistory(-1)); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($i === 0) { - echo "Already at '$originalVersion'. Nothing needs to be done.\n"; - } else { - if ($this->confirm("Set migration history at $originalVersion?")) { - $command = $this->db->createCommand(); - for ($j = 0; $j < $i; ++$j) { - $command->delete($this->migrationTable, array( - 'version' => $migrations[$j], - ))->execute(); - } - echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; - } - } - return; - } - } - - throw new Exception("Unable to find the version '$originalVersion'."); - } - - /** - * Displays the migration history. - * - * This command will show the list of migrations that have been applied - * so far. For example, - * - * ~~~ - * yiic migrate/history # showing the last 10 migrations - * yiic migrate/history 5 # showing the last 5 migrations - * yiic migrate/history 0 # showing the whole history - * ~~~ - * - * @param integer $limit the maximum number of migrations to be displayed. - * If it is 0, the whole migration history will be displayed. - */ - public function actionHistory($limit = 10) - { - $limit = (int)$limit; - $migrations = $this->getMigrationHistory($limit); - if (empty($migrations)) { - echo "No migration has been done before.\n"; - } else { - $n = count($migrations); - if ($limit > 0) { - echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } else { - echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n"; - } - foreach ($migrations as $version => $time) { - echo " (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n"; - } - } - } - - /** - * Displays the un-applied new migrations. - * - * This command will show the new migrations that have not been applied. - * For example, - * - * ~~~ - * yiic migrate/new # showing the first 10 new migrations - * yiic migrate/new 5 # showing the first 5 new migrations - * yiic migrate/new 0 # showing all new migrations - * ~~~ - * - * @param integer $limit the maximum number of new migrations to be displayed. - * If it is 0, all available new migrations will be displayed. - */ - public function actionNew($limit = 10) - { - $limit = (int)$limit; - $migrations = $this->getNewMigrations(); - if (empty($migrations)) { - echo "No new migrations found. Your system is up-to-date.\n"; - } else { - $n = count($migrations); - if ($limit > 0 && $n > $limit) { - $migrations = array_slice($migrations, 0, $limit); - echo "Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } else { - echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } - - foreach ($migrations as $migration) { - echo " " . $migration . "\n"; - } - } - } - - /** - * Creates a new migration. - * - * This command creates a new migration using the available migration template. - * After using this command, developers should modify the created migration - * skeleton by filling up the actual migration logic. - * - * ~~~ - * yiic migrate/create create_user_table - * ~~~ - * - * @param string $name the name of the new migration. This should only contain - * letters, digits and/or underscores. - * @throws Exception if the name argument is invalid. - */ - public function actionCreate($name) - { - if (!preg_match('/^\w+$/', $name)) { - throw new Exception("The migration name should contain letters, digits and/or underscore characters only."); - } - - $name = 'm' . gmdate('ymd_His') . '_' . $name; - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php'; - - if ($this->confirm("Create new migration '$file'?")) { - $content = $this->renderFile(Yii::getAlias($this->templateFile), array( - 'className' => $name, - )); - file_put_contents($file, $content); - echo "New migration created successfully.\n"; - } - } - - /** - * Upgrades with the specified migration class. - * @param string $class the migration class name - * @return boolean whether the migration is successful - */ - protected function migrateUp($class) - { - if ($class === self::BASE_MIGRATION) { - return true; - } - - echo "*** applying $class\n"; - $start = microtime(true); - $migration = $this->createMigration($class); - if ($migration->up() !== false) { - $this->db->createCommand()->insert($this->migrationTable, array( - 'version' => $class, - 'apply_time' => time(), - ))->execute(); - $time = microtime(true) - $start; - echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return true; - } else { - $time = microtime(true) - $start; - echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return false; - } - } - - /** - * Downgrades with the specified migration class. - * @param string $class the migration class name - * @return boolean whether the migration is successful - */ - protected function migrateDown($class) - { - if ($class === self::BASE_MIGRATION) { - return true; - } - - echo "*** reverting $class\n"; - $start = microtime(true); - $migration = $this->createMigration($class); - if ($migration->down() !== false) { - $this->db->createCommand()->delete($this->migrationTable, array( - 'version' => $class, - ))->execute(); - $time = microtime(true) - $start; - echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return true; - } else { - $time = microtime(true) - $start; - echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return false; - } - } - - /** - * Creates a new migration instance. - * @param string $class the migration class name - * @return \yii\db\Migration the migration instance - */ - protected function createMigration($class) - { - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; - require_once($file); - return new $class(array( - 'db' => $this->db, - )); - } - - /** - * Returns the migration history. - * @param integer $limit the maximum number of records in the history to be returned - * @return array the migration history - */ - protected function getMigrationHistory($limit) - { - if ($this->db->schema->getTableSchema($this->migrationTable) === null) { - $this->createMigrationHistoryTable(); - } - $query = new Query; - $rows = $query->select(array('version', 'apply_time')) - ->from($this->migrationTable) - ->orderBy('version DESC') - ->limit($limit) - ->createCommand() - ->queryAll(); - $history = ArrayHelper::map($rows, 'version', 'apply_time'); - unset($history[self::BASE_MIGRATION]); - return $history; - } - - /** - * Creates the migration history table. - */ - protected function createMigrationHistoryTable() - { - echo 'Creating migration history table "' . $this->migrationTable . '"...'; - $this->db->createCommand()->createTable($this->migrationTable, array( - 'version' => 'varchar(255) NOT NULL PRIMARY KEY', - 'apply_time' => 'integer', - ))->execute(); - $this->db->createCommand()->insert($this->migrationTable, array( - 'version' => self::BASE_MIGRATION, - 'apply_time' => time(), - ))->execute(); - echo "done.\n"; - } - - /** - * Returns the migrations that are not applied. - * @return array list of new migrations - */ - protected function getNewMigrations() - { - $applied = array(); - foreach ($this->getMigrationHistory(-1) as $version => $time) { - $applied[substr($version, 1, 13)] = true; - } - - $migrations = array(); - $handle = opendir($this->migrationPath); - while (($file = readdir($handle)) !== false) { - if ($file === '.' || $file === '..') { - continue; - } - $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file; - if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches[2]])) { - $migrations[] = $matches[1]; - } - } - closedir($handle); - sort($migrations); - return $migrations; - } -} +<?php +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\console\controllers; + +use Yii; +use yii\console\Exception; +use yii\console\Controller; +use yii\db\Connection; +use yii\db\Query; +use yii\helpers\ArrayHelper; + +/** + * This command manages application migrations. + * + * A migration means a set of persistent changes to the application environment + * that is shared among different developers. For example, in an application + * backed by a database, a migration may refer to a set of changes to + * the database, such as creating a new table, adding a new table column. + * + * This command provides support for tracking the migration history, upgrading + * or downloading with migrations, and creating new migration skeletons. + * + * The migration history is stored in a database table named + * as [[migrationTable]]. The table will be automatically created the first time + * this command is executed, if it does not exist. You may also manually + * create it as follows: + * + * ~~~ + * CREATE TABLE tbl_migration ( + * version varchar(255) PRIMARY KEY, + * apply_time integer + * ) + * ~~~ + * + * Below are some common usages of this command: + * + * ~~~ + * # creates a new migration named 'create_user_table' + * yii migrate/create create_user_table + * + * # applies ALL new migrations + * yii migrate + * + * # reverts the last applied migration + * yii migrate/down + * ~~~ + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class MigrateController extends Controller +{ + /** + * The name of the dummy migration that marks the beginning of the whole migration history. + */ + const BASE_MIGRATION = 'm000000_000000_base'; + + /** + * @var string the default command action. + */ + public $defaultAction = 'up'; + /** + * @var string the directory storing the migration classes. This can be either + * a path alias or a directory. + */ + public $migrationPath = '@app/migrations'; + /** + * @var string the name of the table for keeping applied migration information. + */ + public $migrationTable = 'tbl_migration'; + /** + * @var string the template file for generating new migrations. + * This can be either a path alias (e.g. "@app/migrations/template.php") + * or a file path. + */ + public $templateFile = '@yii/views/migration.php'; + /** + * @var boolean whether to execute the migration in an interactive mode. + */ + public $interactive = true; + /** + * @var Connection|string the DB connection object or the application + * component ID of the DB connection. + */ + public $db = 'db'; + + /** + * Returns the names of the global options for this command. + * @return array the names of the global options for this command. + */ + public function globalOptions() + { + return array_merge(parent::globalOptions(), array( + 'migrationPath', 'migrationTable', 'db', 'templateFile', 'interactive', 'color' + )); + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * It checks the existence of the [[migrationPath]]. + * @param \yii\base\Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + * @throws Exception if the migration directory does not exist. + */ + public function beforeAction($action) + { + if (parent::beforeAction($action)) { + $path = Yii::getAlias($this->migrationPath); + if (!is_dir($path)) { + throw new Exception("The migration directory \"{$this->migrationPath}\" does not exist."); + } + $this->migrationPath = $path; + + if ($action->id !== 'create') { + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new Exception("The 'db' option must refer to the application component ID of a DB connection."); + } + } + + $version = Yii::getVersion(); + echo "Yii Migration Tool (based on Yii v{$version})\n\n"; + return true; + } else { + return false; + } + } + + /** + * Upgrades the application by applying new migrations. + * For example, + * + * ~~~ + * yii migrate # apply all new migrations + * yii migrate 3 # apply the first 3 new migrations + * ~~~ + * + * @param integer $limit the number of new migrations to be applied. If 0, it means + * applying all available new migrations. + */ + public function actionUp($limit = 0) + { + $migrations = $this->getNewMigrations(); + if (empty($migrations)) { + echo "No new migration found. Your system is up-to-date.\n"; + return; + } + + $total = count($migrations); + $limit = (int)$limit; + if ($limit > 0) { + $migrations = array_slice($migrations, 0, $limit); + } + + $n = count($migrations); + if ($n === $total) { + echo "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n"; + } else { + echo "Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n"; + } + + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateUp($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + return; + } + } + echo "\nMigrated up successfully.\n"; + } + } + + /** + * Downgrades the application by reverting old migrations. + * For example, + * + * ~~~ + * yii migrate/down # revert the last migration + * yii migrate/down 3 # revert the last 3 migrations + * ~~~ + * + * @param integer $limit the number of migrations to be reverted. Defaults to 1, + * meaning the last applied migration will be reverted. + * @throws Exception if the number of the steps specified is less than 1. + */ + public function actionDown($limit = 1) + { + $limit = (int)$limit; + if ($limit < 1) { + throw new Exception("The step argument must be greater than 0."); + } + + $migrations = $this->getMigrationHistory($limit); + if (empty($migrations)) { + echo "No migration has been done before.\n"; + return; + } + $migrations = array_keys($migrations); + + $n = count($migrations); + echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n"; + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateDown($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + return; + } + } + echo "\nMigrated down successfully.\n"; + } + } + + /** + * Redoes the last few migrations. + * + * This command will first revert the specified migrations, and then apply + * them again. For example, + * + * ~~~ + * yii migrate/redo # redo the last applied migration + * yii migrate/redo 3 # redo the last 3 applied migrations + * ~~~ + * + * @param integer $limit the number of migrations to be redone. Defaults to 1, + * meaning the last applied migration will be redone. + * @throws Exception if the number of the steps specified is less than 1. + */ + public function actionRedo($limit = 1) + { + $limit = (int)$limit; + if ($limit < 1) { + throw new Exception("The step argument must be greater than 0."); + } + + $migrations = $this->getMigrationHistory($limit); + if (empty($migrations)) { + echo "No migration has been done before.\n"; + return; + } + $migrations = array_keys($migrations); + + $n = count($migrations); + echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n"; + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateDown($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + return; + } + } + foreach (array_reverse($migrations) as $migration) { + if (!$this->migrateUp($migration)) { + echo "\nMigration failed. The rest of the migrations migrations are canceled.\n"; + return; + } + } + echo "\nMigration redone successfully.\n"; + } + } + + /** + * Upgrades or downgrades till the specified version. + * + * This command will first revert the specified migrations, and then apply + * them again. For example, + * + * ~~~ + * yii migrate/to 101129_185401 # using timestamp + * yii migrate/to m101129_185401_create_user_table # using full name + * ~~~ + * + * @param string $version the version name that the application should be migrated to. + * This can be either the timestamp or the full name of the migration. + * @throws Exception if the version argument is invalid + */ + public function actionTo($version) + { + $originalVersion = $version; + if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { + $version = 'm' . $matches[1]; + } else { + throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); + } + + // try migrate up + $migrations = $this->getNewMigrations(); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + $this->actionUp($i + 1); + return; + } + } + + // try migrate down + $migrations = array_keys($this->getMigrationHistory(-1)); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($i === 0) { + echo "Already at '$originalVersion'. Nothing needs to be done.\n"; + } else { + $this->actionDown($i); + } + return; + } + } + + throw new Exception("Unable to find the version '$originalVersion'."); + } + + /** + * Modifies the migration history to the specified version. + * + * No actual migration will be performed. + * + * ~~~ + * yii migrate/mark 101129_185401 # using timestamp + * yii migrate/mark m101129_185401_create_user_table # using full name + * ~~~ + * + * @param string $version the version at which the migration history should be marked. + * This can be either the timestamp or the full name of the migration. + * @throws Exception if the version argument is invalid or the version cannot be found. + */ + public function actionMark($version) + { + $originalVersion = $version; + if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { + $version = 'm' . $matches[1]; + } else { + throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); + } + + // try mark up + $migrations = $this->getNewMigrations(); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($this->confirm("Set migration history at $originalVersion?")) { + $command = $this->db->createCommand(); + for ($j = 0; $j <= $i; ++$j) { + $command->insert($this->migrationTable, array( + 'version' => $migrations[$j], + 'apply_time' => time(), + ))->execute(); + } + echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; + } + return; + } + } + + // try mark down + $migrations = array_keys($this->getMigrationHistory(-1)); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($i === 0) { + echo "Already at '$originalVersion'. Nothing needs to be done.\n"; + } else { + if ($this->confirm("Set migration history at $originalVersion?")) { + $command = $this->db->createCommand(); + for ($j = 0; $j < $i; ++$j) { + $command->delete($this->migrationTable, array( + 'version' => $migrations[$j], + ))->execute(); + } + echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; + } + } + return; + } + } + + throw new Exception("Unable to find the version '$originalVersion'."); + } + + /** + * Displays the migration history. + * + * This command will show the list of migrations that have been applied + * so far. For example, + * + * ~~~ + * yii migrate/history # showing the last 10 migrations + * yii migrate/history 5 # showing the last 5 migrations + * yii migrate/history 0 # showing the whole history + * ~~~ + * + * @param integer $limit the maximum number of migrations to be displayed. + * If it is 0, the whole migration history will be displayed. + */ + public function actionHistory($limit = 10) + { + $limit = (int)$limit; + $migrations = $this->getMigrationHistory($limit); + if (empty($migrations)) { + echo "No migration has been done before.\n"; + } else { + $n = count($migrations); + if ($limit > 0) { + echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } else { + echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n"; + } + foreach ($migrations as $version => $time) { + echo " (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n"; + } + } + } + + /** + * Displays the un-applied new migrations. + * + * This command will show the new migrations that have not been applied. + * For example, + * + * ~~~ + * yii migrate/new # showing the first 10 new migrations + * yii migrate/new 5 # showing the first 5 new migrations + * yii migrate/new 0 # showing all new migrations + * ~~~ + * + * @param integer $limit the maximum number of new migrations to be displayed. + * If it is 0, all available new migrations will be displayed. + */ + public function actionNew($limit = 10) + { + $limit = (int)$limit; + $migrations = $this->getNewMigrations(); + if (empty($migrations)) { + echo "No new migrations found. Your system is up-to-date.\n"; + } else { + $n = count($migrations); + if ($limit > 0 && $n > $limit) { + $migrations = array_slice($migrations, 0, $limit); + echo "Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } else { + echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } + + foreach ($migrations as $migration) { + echo " " . $migration . "\n"; + } + } + } + + /** + * Creates a new migration. + * + * This command creates a new migration using the available migration template. + * After using this command, developers should modify the created migration + * skeleton by filling up the actual migration logic. + * + * ~~~ + * yii migrate/create create_user_table + * ~~~ + * + * @param string $name the name of the new migration. This should only contain + * letters, digits and/or underscores. + * @throws Exception if the name argument is invalid. + */ + public function actionCreate($name) + { + if (!preg_match('/^\w+$/', $name)) { + throw new Exception("The migration name should contain letters, digits and/or underscore characters only."); + } + + $name = 'm' . gmdate('ymd_His') . '_' . $name; + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php'; + + if ($this->confirm("Create new migration '$file'?")) { + $content = $this->renderFile(Yii::getAlias($this->templateFile), array( + 'className' => $name, + )); + file_put_contents($file, $content); + echo "New migration created successfully.\n"; + } + } + + /** + * Upgrades with the specified migration class. + * @param string $class the migration class name + * @return boolean whether the migration is successful + */ + protected function migrateUp($class) + { + if ($class === self::BASE_MIGRATION) { + return true; + } + + echo "*** applying $class\n"; + $start = microtime(true); + $migration = $this->createMigration($class); + if ($migration->up() !== false) { + $this->db->createCommand()->insert($this->migrationTable, array( + 'version' => $class, + 'apply_time' => time(), + ))->execute(); + $time = microtime(true) - $start; + echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return true; + } else { + $time = microtime(true) - $start; + echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return false; + } + } + + /** + * Downgrades with the specified migration class. + * @param string $class the migration class name + * @return boolean whether the migration is successful + */ + protected function migrateDown($class) + { + if ($class === self::BASE_MIGRATION) { + return true; + } + + echo "*** reverting $class\n"; + $start = microtime(true); + $migration = $this->createMigration($class); + if ($migration->down() !== false) { + $this->db->createCommand()->delete($this->migrationTable, array( + 'version' => $class, + ))->execute(); + $time = microtime(true) - $start; + echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return true; + } else { + $time = microtime(true) - $start; + echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return false; + } + } + + /** + * Creates a new migration instance. + * @param string $class the migration class name + * @return \yii\db\Migration the migration instance + */ + protected function createMigration($class) + { + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; + require_once($file); + return new $class(array( + 'db' => $this->db, + )); + } + + /** + * Returns the migration history. + * @param integer $limit the maximum number of records in the history to be returned + * @return array the migration history + */ + protected function getMigrationHistory($limit) + { + if ($this->db->schema->getTableSchema($this->migrationTable, true) === null) { + $this->createMigrationHistoryTable(); + } + $query = new Query; + $rows = $query->select(array('version', 'apply_time')) + ->from($this->migrationTable) + ->orderBy('version DESC') + ->limit($limit) + ->createCommand($this->db) + ->queryAll(); + $history = ArrayHelper::map($rows, 'version', 'apply_time'); + unset($history[self::BASE_MIGRATION]); + return $history; + } + + /** + * Creates the migration history table. + */ + protected function createMigrationHistoryTable() + { + echo 'Creating migration history table "' . $this->migrationTable . '"...'; + $this->db->createCommand()->createTable($this->migrationTable, array( + 'version' => 'varchar(255) NOT NULL PRIMARY KEY', + 'apply_time' => 'integer', + ))->execute(); + $this->db->createCommand()->insert($this->migrationTable, array( + 'version' => self::BASE_MIGRATION, + 'apply_time' => time(), + ))->execute(); + echo "done.\n"; + } + + /** + * Returns the migrations that are not applied. + * @return array list of new migrations + */ + protected function getNewMigrations() + { + $applied = array(); + foreach ($this->getMigrationHistory(-1) as $version => $time) { + $applied[substr($version, 1, 13)] = true; + } + + $migrations = array(); + $handle = opendir($this->migrationPath); + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file; + if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches[2]])) { + $migrations[] = $matches[1]; + } + } + closedir($handle); + sort($migrations); + return $migrations; + } +} diff --git a/framework/yii/console/runtime/.gitignore b/framework/yii/console/runtime/.gitignore new file mode 100644 index 0000000..f59ec20 --- /dev/null +++ b/framework/yii/console/runtime/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/framework/yii/data/ActiveDataProvider.php b/framework/yii/data/ActiveDataProvider.php new file mode 100644 index 0000000..bd822f9 --- /dev/null +++ b/framework/yii/data/ActiveDataProvider.php @@ -0,0 +1,182 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\data; + +use Yii; +use yii\base\InvalidConfigException; +use yii\base\Model; +use yii\db\Query; +use yii\db\ActiveQuery; +use yii\db\Connection; + +/** + * ActiveDataProvider implements a data provider based on [[Query]] and [[ActiveQuery]]. + * + * ActiveDataProvider provides data by performing DB queries using [[query]]. + * + * The following is an example of using ActiveDataProvider to provide ActiveRecord instances: + * + * ~~~ + * $provider = new ActiveDataProvider(array( + * 'query' => Post::find(), + * 'pagination' => array( + * 'pageSize' => 20, + * ), + * )); + * + * // get the posts in the current page + * $posts = $provider->getModels(); + * ~~~ + * + * And the following example shows how to use ActiveDataProvider without ActiveRecord: + * + * ~~~ + * $query = new Query; + * $provider = new ActiveDataProvider(array( + * 'query' => $query->from('tbl_post'), + * 'pagination' => array( + * 'pageSize' => 20, + * ), + * )); + * + * // get the posts in the current page + * $posts = $provider->getModels(); + * ~~~ + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ActiveDataProvider extends BaseDataProvider +{ + /** + * @var Query the query that is used to fetch data models and [[totalCount]] + * if it is not explicitly set. + */ + public $query; + /** + * @var string|callable the column that is used as the key of the data models. + * This can be either a column name, or a callable that returns the key value of a given data model. + * + * If this is not set, the following rules will be used to determine the keys of the data models: + * + * - If [[query]] is an [[ActiveQuery]] instance, the primary keys of [[ActiveQuery::modelClass]] will be used. + * - Otherwise, the keys of the [[models]] array will be used. + * + * @see getKeys() + */ + public $key; + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * If not set, the default DB connection will be used. + */ + public $db; + + /** + * Initializes the DB connection component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + if (!$this->db instanceof Connection) { + throw new InvalidConfigException('The "db" property must be a valid DB Connection application component.'); + } + } + } + + /** + * @inheritdoc + */ + protected function prepareModels() + { + if (!$this->query instanceof Query) { + throw new InvalidConfigException('The "query" property must be an instance of Query or its subclass.'); + } + if (($pagination = $this->getPagination()) !== false) { + $pagination->totalCount = $this->getTotalCount(); + $this->query->limit($pagination->getLimit())->offset($pagination->getOffset()); + } + if (($sort = $this->getSort()) !== false) { + $this->query->addOrderBy($sort->getOrders()); + } + return $this->query->all($this->db); + } + + /** + * @inheritdoc + */ + protected function prepareKeys($models) + { + $keys = array(); + if ($this->key !== null) { + foreach ($models as $model) { + if (is_string($this->key)) { + $keys[] = $model[$this->key]; + } else { + $keys[] = call_user_func($this->key, $model); + } + } + return $keys; + } elseif ($this->query instanceof ActiveQuery) { + /** @var \yii\db\ActiveRecord $class */ + $class = $this->query->modelClass; + $pks = $class::primaryKey(); + if (count($pks) === 1) { + $pk = $pks[0]; + foreach ($models as $model) { + $keys[] = $model[$pk]; + } + } else { + foreach ($models as $model) { + $kk = array(); + foreach ($pks as $pk) { + $kk[] = $model[$pk]; + } + $keys[] = json_encode($kk); + } + } + return $keys; + } else { + return array_keys($models); + } + } + + /** + * @inheritdoc + */ + protected function prepareTotalCount() + { + if (!$this->query instanceof Query) { + throw new InvalidConfigException('The "query" property must be an instance of Query or its subclass.'); + } + $query = clone $this->query; + return $query->limit(-1)->offset(-1)->count('*', $this->db); + } + + /** + * @inheritdoc + */ + public function setSort($value) + { + parent::setSort($value); + if (($sort = $this->getSort()) !== false && empty($sort->attributes) && $this->query instanceof ActiveQuery) { + /** @var Model $model */ + $model = new $this->query->modelClass; + foreach($model->attributes() as $attribute) { + $sort->attributes[$attribute] = array( + 'asc' => array($attribute => Sort::ASC), + 'desc' => array($attribute => Sort::DESC), + 'label' => $model->getAttributeLabel($attribute), + ); + } + } + } +} diff --git a/framework/yii/data/ArrayDataProvider.php b/framework/yii/data/ArrayDataProvider.php new file mode 100644 index 0000000..77f31cd --- /dev/null +++ b/framework/yii/data/ArrayDataProvider.php @@ -0,0 +1,133 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\data; + +use yii\helpers\ArrayHelper; + +/** + * ArrayDataProvider implements a data provider based on a data array. + * + * The [[allModels]] property contains all data models that may be sorted and/or paginated. + * ArrayDataProvider will provide the data after sorting and/or pagination. + * You may configure the [[sort]] and [[pagination]] properties to + * customize the sorting and pagination behaviors. + * + * Elements in the [[allModels]] array may be either objects (e.g. model objects) + * or associative arrays (e.g. query results of DAO). + * Make sure to set the [[key]] property to the name of the field that uniquely + * identifies a data record or false if you do not have such a field. + * + * Compared to [[ActiveDataProvider]], ArrayDataProvider could be less efficient + * because it needs to have [[allModels]] ready. + * + * ArrayDataProvider may be used in the following way: + * + * ~~~ + * $query = new Query; + * $provider = new ArrayDataProvider(array( + * 'allModels' => $query->from('tbl_post')->all(), + * 'sort' => array( + * 'attributes' => array( + * 'id', 'username', 'email', + * ), + * ), + * 'pagination' => array( + * 'pageSize' => 10, + * ), + * )); + * // get the posts in the current page + * $posts = $provider->getModels(); + * ~~~ + * + * Note: if you want to use the sorting feature, you must configure the [[sort]] property + * so that the provider knows which columns can be sorted. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ArrayDataProvider extends BaseDataProvider +{ + /** + * @var string|callable the column that is used as the key of the data models. + * This can be either a column name, or a callable that returns the key value of a given data model. + * If this is not set, the index of the [[models]] array will be used. + * @see getKeys() + */ + public $key; + /** + * @var array the data that is not paginated or sorted. When pagination is enabled, + * this property usually contains more elements than [[models]]. + * The array elements must use zero-based integer keys. + */ + public $allModels; + + + /** + * @inheritdoc + */ + protected function prepareModels() + { + if (($models = $this->allModels) === null) { + return array(); + } + + if (($sort = $this->getSort()) !== false) { + $models = $this->sortModels($models, $sort); + } + + if (($pagination = $this->getPagination()) !== false) { + $pagination->totalCount = $this->getTotalCount(); + $models = array_slice($models, $pagination->getOffset(), $pagination->getLimit()); + } + + return $models; + } + + /** + * @inheritdoc + */ + protected function prepareKeys($models) + { + if ($this->key !== null) { + $keys = array(); + foreach ($models as $model) { + if (is_string($this->key)) { + $keys[] = $model[$this->key]; + } else { + $keys[] = call_user_func($this->key, $model); + } + } + return $keys; + } else { + return array_keys($models); + } + } + + /** + * @inheritdoc + */ + protected function prepareTotalCount() + { + return count($this->allModels); + } + + /** + * Sorts the data models according to the given sort definition + * @param array $models the models to be sorted + * @param Sort $sort the sort definition + * @return array the sorted data models + */ + protected function sortModels($models, $sort) + { + $orders = $sort->getOrders(); + if (!empty($orders)) { + ArrayHelper::multisort($models, array_keys($orders), array_values($orders)); + } + return $models; + } +} diff --git a/framework/yii/data/BaseDataProvider.php b/framework/yii/data/BaseDataProvider.php new file mode 100644 index 0000000..3b0669d --- /dev/null +++ b/framework/yii/data/BaseDataProvider.php @@ -0,0 +1,256 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\data; + +use Yii; +use yii\base\Component; +use yii\base\InvalidParamException; + +/** + * BaseDataProvider provides a base class that implements the [[DataProviderInterface]]. + * + * @property integer $count The number of data models in the current page. This property is read-only. + * @property array $keys The list of key values corresponding to [[models]]. Each data model in [[models]] is + * uniquely identified by the corresponding key value in this array. + * @property array $models The list of data models in the current page. + * @property Pagination|boolean $pagination The pagination object. If this is false, it means the pagination + * is disabled. Note that the type of this property differs in getter and setter. See [[getPagination()]] and + * [[setPagination()]] for details. + * @property Sort|boolean $sort The sorting object. If this is false, it means the sorting is disabled. Note + * that the type of this property differs in getter and setter. See [[getSort()]] and [[setSort()]] for details. + * @property integer $totalCount Total number of possible data models. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +abstract class BaseDataProvider extends Component implements DataProviderInterface +{ + /** + * @var string an ID that uniquely identifies the data provider among all data providers. + * You should set this property if the same page contains two or more different data providers. + * Otherwise, the [[pagination]] and [[sort]] mainly not work properly. + */ + public $id; + + private $_sort; + private $_pagination; + private $_keys; + private $_models; + private $_totalCount; + + + /** + * Prepares the data models that will be made available in the current page. + * @return array the available data models + */ + abstract protected function prepareModels(); + + /** + * Prepares the keys associated with the currently available data models. + * @param array $models the available data models + * @return array the keys + */ + abstract protected function prepareKeys($models); + + /** + * Returns a value indicating the total number of data models in this data provider. + * @return integer total number of data models in this data provider. + */ + abstract protected function prepareTotalCount(); + + /** + * Prepares the data models and keys. + * + * This method will prepare the data models and keys that can be retrieved via + * [[getModels()]] and [[getKeys()]]. + * + * This method will be implicitly called by [[getModels()]] and [[getKeys()]] if it has not been called before. + * + * @param boolean $forcePrepare whether to force data preparation even if it has been done before. + */ + public function prepare($forcePrepare = false) + { + if ($forcePrepare || $this->_models === null) { + $this->_models = $this->prepareModels(); + } + if ($forcePrepare || $this->_keys === null) { + $this->_keys = $this->prepareKeys($this->_models); + } + } + + /** + * Returns the data models in the current page. + * @return array the list of data models in the current page. + */ + public function getModels() + { + $this->prepare(); + return $this->_models; + } + + /** + * Sets the data models in the current page. + * @param array $models the models in the current page + */ + public function setModels($models) + { + $this->_models = $models; + } + + /** + * Returns the key values associated with the data models. + * @return array the list of key values corresponding to [[models]]. Each data model in [[models]] + * is uniquely identified by the corresponding key value in this array. + */ + public function getKeys() + { + $this->prepare(); + return $this->_keys; + } + + /** + * Sets the key values associated with the data models. + * @param array $keys the list of key values corresponding to [[models]]. + */ + public function setKeys($keys) + { + $this->_keys = $keys; + } + + /** + * Returns the number of data models in the current page. + * @return integer the number of data models in the current page. + */ + public function getCount() + { + return count($this->getModels()); + } + + /** + * Returns the total number of data models. + * When [[pagination]] is false, this returns the same value as [[count]]. + * Otherwise, it will call [[prepareTotalCount()]] to get the count. + * @return integer total number of possible data models. + */ + public function getTotalCount() + { + if ($this->getPagination() === false) { + return $this->getCount(); + } elseif ($this->_totalCount === null) { + $this->_totalCount = $this->prepareTotalCount(); + } + return $this->_totalCount; + } + + /** + * Sets the total number of data models. + * @param integer $value the total number of data models. + */ + public function setTotalCount($value) + { + $this->_totalCount = $value; + } + + /** + * Returns the pagination object used by this data provider. + * Note that you should call [[prepare()]] or [[getModels()]] first to get correct values + * of [[Pagination::totalCount]] and [[Pagination::pageCount]]. + * @return Pagination|boolean the pagination object. If this is false, it means the pagination is disabled. + */ + public function getPagination() + { + if ($this->_pagination === null) { + $this->_pagination = new Pagination; + if ($this->id !== null) { + $this->_pagination->pageVar = $this->id . '-page'; + } + } + return $this->_pagination; + } + + /** + * Sets the pagination for this data provider. + * @param array|Pagination|boolean $value the pagination to be used by this data provider. + * This can be one of the following: + * + * - a configuration array for creating the pagination object. The "class" element defaults + * to 'yii\data\Pagination' + * - an instance of [[Pagination]] or its subclass + * - false, if pagination needs to be disabled. + * + * @throws InvalidParamException + */ + public function setPagination($value) + { + if (is_array($value)) { + $config = array( + 'class' => Pagination::className(), + ); + if ($this->id !== null) { + $config['pageVar'] = $this->id . '-page'; + } + $this->_pagination = Yii::createObject(array_merge($config, $value)); + } elseif ($value instanceof Pagination || $value === false) { + $this->_pagination = $value; + } else { + throw new InvalidParamException('Only Pagination instance, configuration array or false is allowed.'); + } + } + + /** + * @return Sort|boolean the sorting object. If this is false, it means the sorting is disabled. + */ + public function getSort() + { + if ($this->_sort === null) { + $this->setSort(array()); + } + return $this->_sort; + } + + /** + * Sets the sort definition for this data provider. + * @param array|Sort|boolean $value the sort definition to be used by this data provider. + * This can be one of the following: + * + * - a configuration array for creating the sort definition object. The "class" element defaults + * to 'yii\data\Sort' + * - an instance of [[Sort]] or its subclass + * - false, if sorting needs to be disabled. + * + * @throws InvalidParamException + */ + public function setSort($value) + { + if (is_array($value)) { + $config = array( + 'class' => Sort::className(), + ); + if ($this->id !== null) { + $config['sortVar'] = $this->id . '-sort'; + } + $this->_sort = Yii::createObject(array_merge($config, $value)); + } elseif ($value instanceof Sort || $value === false) { + $this->_sort = $value; + } else { + throw new InvalidParamException('Only Sort instance, configuration array or false is allowed.'); + } + } + + /** + * Refreshes the data provider. + * After calling this method, if [[getModels()]], [[getKeys()]] or [[getTotalCount()]] is called again, + * they will re-execute the query and return the latest data available. + */ + public function refresh() + { + $this->_totalCount = null; + $this->_models = null; + $this->_keys = null; + } +} diff --git a/framework/yii/data/DataProviderInterface.php b/framework/yii/data/DataProviderInterface.php new file mode 100644 index 0000000..1dea1e6 --- /dev/null +++ b/framework/yii/data/DataProviderInterface.php @@ -0,0 +1,70 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\data; + +/** + * DataProviderInterface is the interface that must be implemented by data provider classes. + * + * Data providers are components that sort and paginate data, and provide them to widgets + * such as [[GridView]], [[ListView]]. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +interface DataProviderInterface +{ + /** + * Prepares the data models and keys. + * + * This method will prepare the data models and keys that can be retrieved via + * [[getModels()]] and [[getKeys()]]. + * + * This method will be implicitly called by [[getModels()]] and [[getKeys()]] if it has not been called before. + * + * @param boolean $forcePrepare whether to force data preparation even if it has been done before. + */ + public function prepare($forcePrepare = false); + + /** + * Returns the number of data models in the current page. + * This is equivalent to `count($provider->getModels())`. + * When [[pagination]] is false, this is the same as [[totalCount]]. + * @return integer the number of data models in the current page. + */ + public function getCount(); + + /** + * Returns the total number of data models. + * When [[pagination]] is false, this is the same as [[count]]. + * @return integer total number of possible data models. + */ + public function getTotalCount(); + + /** + * Returns the data models in the current page. + * @return array the list of data models in the current page. + */ + public function getModels(); + + /** + * Returns the key values associated with the data models. + * @return array the list of key values corresponding to [[models]]. Each data model in [[models]] + * is uniquely identified by the corresponding key value in this array. + */ + public function getKeys(); + + /** + * @return Sort the sorting object. If this is false, it means the sorting is disabled. + */ + public function getSort(); + + /** + * @return Pagination the pagination object. If this is false, it means the pagination is disabled. + */ + public function getPagination(); +} diff --git a/yii/web/Pagination.php b/framework/yii/data/Pagination.php similarity index 75% rename from yii/web/Pagination.php rename to framework/yii/data/Pagination.php index 3d4e242..04af828 100644 --- a/yii/web/Pagination.php +++ b/framework/yii/data/Pagination.php @@ -5,15 +5,16 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\web; +namespace yii\data; use Yii; +use yii\base\Object; /** * Pagination represents information relevant to pagination of data items. * * When data needs to be rendered in multiple pages, Pagination can be used to - * represent information such as [[itemCount|total item count]], [[pageSize|page size]], + * represent information such as [[totalCount|total item count]], [[pageSize|page size]], * [[page|current page]], etc. These information can be passed to [[yii\widgets\Pager|pagers]] * to render pagination buttons or links. * @@ -27,12 +28,12 @@ use Yii; * { * $query = Article::find()->where(array('status' => 1)); * $countQuery = clone $query; - * $pages = new Pagination($countQuery->count()); + * $pages = new Pagination(array('totalCount' => $countQuery->count())); * $models = $query->offset($pages->offset) * ->limit($pages->limit) * ->all(); * - * $this->render('index', array( + * return $this->render('index', array( * 'models' => $models, * 'pages' => $pages, * )); @@ -42,27 +43,28 @@ use Yii; * View: * * ~~~ - * foreach($models as $model) { + * foreach ($models as $model) { * // display $model here * } * * // display pagination - * $this->widget('yii\widgets\LinkPager', array( - * 'pages' => $pages, + * echo LinkPager::widget(array( + * 'pagination' => $pages, * )); * ~~~ * - * @property integer $pageCount Number of pages. - * @property integer $page The zero-based index of the current page. - * @property integer $offset The offset of the data. This may be used to set the - * OFFSET value for a SQL statement for fetching the current page of data. - * @property integer $limit The limit of the data. This may be used to set the - * LIMIT value for a SQL statement for fetching the current page of data. + * @property integer $limit The limit of the data. This may be used to set the LIMIT value for a SQL statement + * for fetching the current page of data. Note that if the page size is infinite, a value -1 will be returned. + * This property is read-only. + * @property integer $offset The offset of the data. This may be used to set the OFFSET value for a SQL + * statement for fetching the current page of data. This property is read-only. + * @property integer $page The zero-based current page number. + * @property integer $pageCount Number of pages. This property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Pagination extends \yii\base\Object +class Pagination extends Object { /** * @var string name of the parameter storing the current page index. Defaults to 'page'. @@ -80,7 +82,7 @@ class Pagination extends \yii\base\Object */ public $route; /** - * @var array parameters (name=>value) that should be used to obtain the current page number + * @var array parameters (name => value) that should be used to obtain the current page number * and to create new pagination URLs. If not set, $_GET will be used instead. * * The array element indexed by [[pageVar]] is considered to be the current page number. @@ -90,31 +92,21 @@ class Pagination extends \yii\base\Object /** * @var boolean whether to check if [[page]] is within valid range. * When this property is true, the value of [[page]] will always be between 0 and ([[pageCount]]-1). - * Because [[pageCount]] relies on the correct value of [[itemCount]] which may not be available + * Because [[pageCount]] relies on the correct value of [[totalCount]] which may not be available * in some cases (e.g. MongoDB), you may want to set this property to be false to disable the page * number validation. By doing so, [[page]] will return the value indexed by [[pageVar]] in [[params]]. */ public $validatePage = true; /** - * @var integer number of items on each page. Defaults to 10. + * @var integer number of items on each page. Defaults to 20. * If it is less than 1, it means the page size is infinite, and thus a single page contains all items. */ - public $pageSize = 10; + public $pageSize = 20; /** * @var integer total number of items. */ - public $itemCount; + public $totalCount = 0; - /** - * Constructor. - * @param integer $itemCount total number of items. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($itemCount, $config = array()) - { - $this->itemCount = $itemCount; - parent::__construct($config); - } /** * @return integer number of pages @@ -122,10 +114,10 @@ class Pagination extends \yii\base\Object public function getPageCount() { if ($this->pageSize < 1) { - return $this->itemCount > 0 ? 1 : 0; + return $this->totalCount > 0 ? 1 : 0; } else { - $itemCount = $this->itemCount < 0 ? 0 : (int)$this->itemCount; - return (int)(($itemCount + $this->pageSize - 1) / $this->pageSize); + $totalCount = $this->totalCount < 0 ? 0 : (int)$this->totalCount; + return (int)(($totalCount + $this->pageSize - 1) / $this->pageSize); } } diff --git a/yii/web/Sort.php b/framework/yii/data/Sort.php similarity index 68% rename from yii/web/Sort.php rename to framework/yii/data/Sort.php index 324e733..5eb031e 100644 --- a/yii/web/Sort.php +++ b/framework/yii/data/Sort.php @@ -5,10 +5,13 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\web; +namespace yii\data; use Yii; +use yii\base\InvalidConfigException; +use yii\base\Object; use yii\helpers\Html; +use yii\helpers\Inflector; /** * Sort represents information relevant to sorting. @@ -26,8 +29,10 @@ use yii\helpers\Html; * 'attributes' => array( * 'age', * 'name' => array( - * 'asc' => array('last_name', 'first_name'), - * 'desc' => array('last_name' => true, 'first_name' => true), + * 'asc' => array('first_name' => Sort::ASC, 'last_name' => Sort::ASC), + * 'desc' => array('first_name' => Sort::DESC, 'last_name' => Sort::DESC), + * 'default' => Sort::DESC, + * 'label' => 'Name', * ), * ), * )); @@ -37,7 +42,7 @@ use yii\helpers\Html; * ->orderBy($sort->orders) * ->all(); * - * $this->render('index', array( + * return $this->render('index', array( * 'models' => $models, * 'sort' => $sort, * )); @@ -48,9 +53,9 @@ use yii\helpers\Html; * * ~~~ * // display links leading to sort actions - * echo $sort->link('name', 'Name') . ' | ' . $sort->link('age', 'Age'); + * echo $sort->link('name') . ' | ' . $sort->link('age'); * - * foreach($models as $model) { + * foreach ($models as $model) { * // display $model here * } * ~~~ @@ -60,15 +65,15 @@ use yii\helpers\Html; * sorted by the orders specified by the Sort object. In the view, we show two hyperlinks * that can lead to pages with the data sorted by the corresponding attributes. * - * @property array $orders Sort directions indexed by column names. The sort direction - * can be either [[Sort::ASC]] for ascending order or [[Sort::DESC]] for descending order. - * @property array $attributeOrders Sort directions indexed by attribute names. The sort - * direction can be either [[Sort::ASC]] for ascending order or [[Sort::DESC]] for descending order. + * @property array $attributeOrders Sort directions indexed by attribute names. Sort direction can be either + * [[Sort::ASC]] for ascending order or [[Sort::DESC]] for descending order. This property is read-only. + * @property array $orders The columns (keys) and their corresponding sort directions (values). This can be + * passed to [[\yii\db\Query::orderBy()]] to construct a DB query. This property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Sort extends \yii\base\Object +class Sort extends Object { /** * Sort ascending @@ -93,10 +98,11 @@ class Sort extends \yii\base\Object * ~~~ * array( * 'age', - * 'user' => array( + * 'name' => array( * 'asc' => array('first_name' => Sort::ASC, 'last_name' => Sort::ASC), * 'desc' => array('first_name' => Sort::DESC, 'last_name' => Sort::DESC), - * 'default' => 'desc', + * 'default' => Sort::DESC, + * 'label' => 'Name', * ), * ) * ~~~ @@ -108,6 +114,8 @@ class Sort extends \yii\base\Object * 'age' => array( * 'asc' => array('age' => Sort::ASC), * 'desc' => array('age' => Sort::DESC), + * 'default' => Sort::ASC, + * 'label' => Inflector::camel2words('age'), * ) * ~~~ * @@ -118,8 +126,14 @@ class Sort extends \yii\base\Object * - The "asc" and "desc" elements specify how to sort by the attribute in ascending * and descending orders, respectively. Their values represent the actual columns and * the directions by which the data should be sorted by. - * - And the "default" element specifies if the attribute is not sorted currently, - * in which direction it should be sorted (the default value is ascending order). + * - The "default" element specifies by which direction the attribute should be sorted + * if it is not currently sorted (the default value is ascending order). + * - The "label" element specifies what label should be used when calling [[link()]] to create + * a sort link. If not set, [[Inflector::camel2words()]] will be called to get a label. + * Note that it will not be HTML-encoded. + * + * Note that if the Sort object is already created, you can only use the full format + * to configure every attribute. Each attribute must include these elements: asc and desc. */ public $attributes = array(); /** @@ -146,7 +160,7 @@ class Sort extends \yii\base\Object * * @see attributeOrders */ - public $defaults; + public $defaultOrder; /** * @var string the route of the controller action for displaying the sorted contents. * If not set, it means using the currently requested route. @@ -156,9 +170,9 @@ class Sort extends \yii\base\Object * @var array separators used in the generated URL. This must be an array consisting of * two elements. The first element specifies the character separating different * attributes, while the second element specifies the character separating attribute name - * and the corresponding sort direction. Defaults to `array('-', '.')`. + * and the corresponding sort direction. Defaults to `array('.', '-')`. */ - public $separators = array('-', '.'); + public $separators = array('.', '-'); /** * @var array parameters (name => value) that should be used to obtain the current sort directions * and to create new sort URLs. If not set, $_GET will be used instead. @@ -167,21 +181,51 @@ class Sort extends \yii\base\Object * If the element does not exist, the [[defaults|default order]] will be used. * * @see sortVar - * @see defaults + * @see defaultOrder */ public $params; + /** + * @var \yii\web\UrlManager the URL manager used for creating sort URLs. If not set, + * the "urlManager" application component will be used. + */ + public $urlManager; + + /** + * Normalizes the [[attributes]] property. + */ + public function init() + { + $attributes = array(); + foreach ($this->attributes as $name => $attribute) { + if (!is_array($attribute)) { + $attributes[$attribute] = array( + 'asc' => array($attribute => self::ASC), + 'desc' => array($attribute => self::DESC), + ); + } elseif (!isset($attribute['asc'], $attribute['desc'])) { + $attributes[$name] = array_merge(array( + 'asc' => array($name => self::ASC), + 'desc' => array($name => self::DESC), + ), $attribute); + } else { + $attributes[$name] = $attribute; + } + } + $this->attributes = $attributes; + } /** * Returns the columns and their corresponding sort directions. + * @param boolean $recalculate whether to recalculate the sort directions * @return array the columns (keys) and their corresponding sort directions (values). * This can be passed to [[\yii\db\Query::orderBy()]] to construct a DB query. */ - public function getOrders() + public function getOrders($recalculate = false) { - $attributeOrders = $this->getAttributeOrders(); + $attributeOrders = $this->getAttributeOrders($recalculate); $orders = array(); foreach ($attributeOrders as $attribute => $direction) { - $definition = $this->getAttribute($attribute); + $definition = $this->attributes[$attribute]; $columns = $definition[$direction === self::ASC ? 'asc' : 'desc']; foreach ($columns as $name => $dir) { $orders[$name] = $dir; @@ -191,34 +235,8 @@ class Sort extends \yii\base\Object } /** - * Generates a hyperlink that links to the sort action to sort by the specified attribute. - * Based on the sort direction, the CSS class of the generated hyperlink will be appended - * with "asc" or "desc". - * @param string $attribute the attribute name by which the data should be sorted by. - * @param string $label the link label. Note that the label will not be HTML-encoded. - * @param array $htmlOptions additional HTML attributes for the hyperlink tag - * @return string the generated hyperlink + * @var array the currently requested sort order as computed by [[getAttributeOrders]]. */ - public function link($attribute, $label, $htmlOptions = array()) - { - if (($definition = $this->getAttribute($attribute)) === false) { - return $label; - } - - if (($direction = $this->getAttributeOrder($attribute)) !== null) { - $class = $direction ? 'desc' : 'asc'; - if (isset($htmlOptions['class'])) { - $htmlOptions['class'] .= ' ' . $class; - } else { - $htmlOptions['class'] = $class; - } - } - - $url = $this->createUrl($attribute); - - return Html::a($label, $url, $htmlOptions); - } - private $_attributeOrders; /** @@ -243,7 +261,7 @@ class Sort extends \yii\base\Object } } - if (($this->getAttribute($attribute)) !== false) { + if (isset($this->attributes[$attribute])) { $this->_attributeOrders[$attribute] = $descending; if (!$this->enableMultiSort) { return $this->_attributeOrders; @@ -251,8 +269,8 @@ class Sort extends \yii\base\Object } } } - if (empty($this->_attributeOrders) && is_array($this->defaults)) { - $this->_attributeOrders = $this->defaults; + if (empty($this->_attributeOrders) && is_array($this->defaultOrder)) { + $this->_attributeOrders = $this->defaultOrder; } } return $this->_attributeOrders; @@ -267,8 +285,48 @@ class Sort extends \yii\base\Object */ public function getAttributeOrder($attribute) { - $this->getAttributeOrders(); - return isset($this->_attributeOrders[$attribute]) ? $this->_attributeOrders[$attribute] : null; + $orders = $this->getAttributeOrders(); + return isset($orders[$attribute]) ? $orders[$attribute] : null; + } + + /** + * Generates a hyperlink that links to the sort action to sort by the specified attribute. + * Based on the sort direction, the CSS class of the generated hyperlink will be appended + * with "asc" or "desc". + * @param string $attribute the attribute name by which the data should be sorted by. + * @param array $options additional HTML attributes for the hyperlink tag. + * There is one special attribute `label` which will be used as the label of the hyperlink. + * If this is not set, the label defined in [[attributes]] will be used. + * If no label is defined, [[yii\helpers\Inflector::camel2words()]] will be called to get a label. + * Note that it will not be HTML-encoded. + * @return string the generated hyperlink + * @throws InvalidConfigException if the attribute is unknown + */ + public function link($attribute, $options = array()) + { + if (($direction = $this->getAttributeOrder($attribute)) !== null) { + $class = $direction ? 'desc' : 'asc'; + if (isset($options['class'])) { + $options['class'] .= ' ' . $class; + } else { + $options['class'] = $class; + } + } + + $url = $this->createUrl($attribute); + $options['data-sort'] = $this->createSortVar($attribute); + + if (isset($options['label'])) { + $label = $options['label']; + unset($options['label']); + } else { + if (isset($this->attributes[$attribute]['label'])) { + $label = $this->attributes[$attribute]['label']; + } else { + $label = Inflector::camel2words($attribute); + } + } + return Html::a($label, $url, $options); } /** @@ -277,23 +335,40 @@ class Sort extends \yii\base\Object * For example, if the current page already sorts the data by the specified attribute in ascending order, * then the URL created will lead to a page that sorts the data by the specified attribute in descending order. * @param string $attribute the attribute name - * @return string|boolean the URL for sorting. False if the attribute is invalid. + * @return string the URL for sorting. False if the attribute is invalid. + * @throws InvalidConfigException if the attribute is unknown * @see attributeOrders * @see params */ public function createUrl($attribute) { - if (($definition = $this->getAttribute($attribute)) === false) { - return false; + $params = $this->params === null ? $_GET : $this->params; + $params[$this->sortVar] = $this->createSortVar($attribute); + $route = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; + $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager; + return $urlManager->createUrl($route, $params); + } + + /** + * Creates the sort variable for the specified attribute. + * The newly created sort variable can be used to create a URL that will lead to + * sorting by the specified attribute. + * @param string $attribute the attribute name + * @return string the value of the sort variable + * @throws InvalidConfigException if the specified attribute is not defined in [[attributes]] + */ + public function createSortVar($attribute) + { + if (!isset($this->attributes[$attribute])) { + throw new InvalidConfigException("Unknown attribute: $attribute"); } + $definition = $this->attributes[$attribute]; $directions = $this->getAttributeOrders(); if (isset($directions[$attribute])) { $descending = !$directions[$attribute]; unset($directions[$attribute]); - } elseif (isset($definition['default'])) { - $descending = $definition['default'] === 'desc'; } else { - $descending = false; + $descending = !empty($definition['default']); } if ($this->enableMultiSort) { @@ -306,31 +381,16 @@ class Sort extends \yii\base\Object foreach ($directions as $attribute => $descending) { $sorts[] = $descending ? $attribute . $this->separators[1] . $this->descTag : $attribute; } - $params = $this->params === null ? $_GET : $this->params; - $params[$this->sortVar] = implode($this->separators[0], $sorts); - $route = $this->route === null ? Yii::$app->controller->route : $this->route; - - return Yii::$app->getUrlManager()->createUrl($route, $params); + return implode($this->separators[0], $sorts); } /** - * Returns the attribute definition of the specified name. + * Returns a value indicating whether the sort definition supports sorting by the named attribute. * @param string $name the attribute name - * @return array|boolean the sort definition (column names => sort directions). - * False is returned if the attribute cannot be sorted. - * @see attributes + * @return boolean whether the sort definition supports sorting by the named attribute. */ - public function getAttribute($name) + public function hasAttribute($name) { - if (isset($this->attributes[$name])) { - return $this->attributes[$name]; - } elseif (in_array($name, $this->attributes, true)) { - return array( - 'asc' => array($name => self::ASC), - 'desc' => array($name => self::DESC), - ); - } else { - return false; - } + return isset($this->attributes[$name]); } } diff --git a/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php similarity index 71% rename from yii/db/ActiveQuery.php rename to framework/yii/db/ActiveQuery.php index dac94c8..375e91f 100644 --- a/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -8,11 +8,6 @@ namespace yii\db; -use yii\db\Connection; -use yii\db\Command; -use yii\db\QueryBuilder; -use yii\db\Expression; - /** * ActiveQuery represents a DB query associated with an Active Record class. * @@ -60,11 +55,6 @@ class ActiveQuery extends Query */ public $with; /** - * @var string the name of the column by which query results should be indexed by. - * This is only used when the query result is returned as an array when calling [[all()]]. - */ - public $indexBy; - /** * @var boolean whether to return each record as an array. If false (default), an object * of [[modelClass]] will be created to represent each record. */ @@ -97,11 +87,13 @@ class ActiveQuery extends Query /** * Executes query and returns all results as an array. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. * @return array the query results. If the query results in nothing, an empty array will be returned. */ - public function all() + public function all($db = null) { - $command = $this->createCommand(); + $command = $this->createCommand($db); $rows = $command->queryAll(); if (!empty($rows)) { $models = $this->createModels($rows); @@ -116,18 +108,24 @@ class ActiveQuery extends Query /** * Executes query and returns a single row of result. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], * the query result may be either an array or an ActiveRecord object. Null will be returned * if the query results in nothing. */ - public function one() + public function one($db = null) { - $command = $this->createCommand(); - $row = $command->queryRow(); - if ($row !== false && !$this->asArray) { - /** @var $class ActiveRecord */ - $class = $this->modelClass; - $model = $class::create($row); + $command = $this->createCommand($db); + $row = $command->queryOne(); + if ($row !== false) { + if ($this->asArray) { + $model = $row; + } else { + /** @var $class ActiveRecord */ + $class = $this->modelClass; + $model = $class::create($row); + } if (!empty($this->with)) { $models = array($model); $this->populateRelations($models, $this->with); @@ -135,92 +133,11 @@ class ActiveQuery extends Query } return $model; } else { - return $row === false ? null : $row; + return null; } } /** - * Returns the number of records. - * @param string $q the COUNT expression. Defaults to '*'. - * Make sure you properly quote column names. - * @return integer number of records - */ - public function count($q = '*') - { - $this->select = array("COUNT($q)"); - return $this->createCommand()->queryScalar(); - } - - /** - * Returns the sum of the specified column values. - * @param string $q the column name or expression. - * Make sure you properly quote column names. - * @return integer the sum of the specified column values - */ - public function sum($q) - { - $this->select = array("SUM($q)"); - return $this->createCommand()->queryScalar(); - } - - /** - * Returns the average of the specified column values. - * @param string $q the column name or expression. - * Make sure you properly quote column names. - * @return integer the average of the specified column values. - */ - public function average($q) - { - $this->select = array("AVG($q)"); - return $this->createCommand()->queryScalar(); - } - - /** - * Returns the minimum of the specified column values. - * @param string $q the column name or expression. - * Make sure you properly quote column names. - * @return integer the minimum of the specified column values. - */ - public function min($q) - { - $this->select = array("MIN($q)"); - return $this->createCommand()->queryScalar(); - } - - /** - * Returns the maximum of the specified column values. - * @param string $q the column name or expression. - * Make sure you properly quote column names. - * @return integer the maximum of the specified column values. - */ - public function max($q) - { - $this->select = array("MAX($q)"); - return $this->createCommand()->queryScalar(); - } - - /** - * Returns the query result as a scalar value. - * The value returned will be the first column in the first row of the query results. - * @return string|boolean the value of the first column in the first row of the query result. - * False is returned if the query result is empty. - */ - public function scalar() - { - return $this->createCommand()->queryScalar(); - } - - /** - * Returns a value indicating whether the query result contains any row of data. - * @return boolean whether the query result contains any row of data. - */ - public function exists() - { - $this->select = array(new Expression('1')); - return $this->scalar() !== false; - } - - /** * Creates a DB command that can be used to execute this query. * @param Connection $db the DB connection used to create the DB command. * If null, the DB connection returned by [[modelClass]] will be used. @@ -233,22 +150,25 @@ class ActiveQuery extends Query if ($db === null) { $db = $modelClass::getDb(); } + + $params = $this->params; if ($this->sql === null) { if ($this->from === null) { $tableName = $modelClass::tableName(); + if ($this->select === null && !empty($this->join)) { + $this->select = array("$tableName.*"); + } $this->from = array($tableName); } - /** @var $qb QueryBuilder */ - $qb = $db->getQueryBuilder(); - $this->sql = $qb->build($this); + list ($this->sql, $params) = $db->getQueryBuilder()->build($this); } - return $db->createCommand($this->sql, $this->params); + return $db->createCommand($this->sql, $params); } /** * Sets the [[asArray]] property. * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. - * @return ActiveQuery the query object itself + * @return static the query object itself */ public function asArray($value = true) { @@ -276,7 +196,7 @@ class ActiveQuery extends Query * ))->all(); * ~~~ * - * @return ActiveQuery the query object itself + * @return static the query object itself */ public function with() { @@ -290,13 +210,24 @@ class ActiveQuery extends Query /** * Sets the [[indexBy]] property. - * @param string $column the name of the column by which the query results should be indexed by. - * @return ActiveQuery the query object itself + * @param string|callable $column the name of the column by which the query results should be indexed by. + * This can also be a callable (e.g. anonymous function) that returns the index value based on the given + * row or model data. The signature of the callable should be: + * + * ~~~ + * // $model is an AR instance when `asArray` is false, + * // or an array of column values when `asArray` is true. + * function ($model) + * { + * // return the index value corresponding to $model + * } + * ~~~ + * + * @return static the query object itself */ public function indexBy($column) { - $this->indexBy = $column; - return $this; + return parent::indexBy($column); } private function createModels($rows) @@ -307,7 +238,12 @@ class ActiveQuery extends Query return $rows; } foreach ($rows as $row) { - $models[$row[$this->indexBy]] = $row; + if (is_string($this->indexBy)) { + $key = $row[$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row); + } + $models[$key] = $row; } } else { /** @var $class ActiveRecord */ @@ -319,7 +255,12 @@ class ActiveQuery extends Query } else { foreach ($rows as $row) { $model = $class::create($row); - $models[$model->{$this->indexBy}] = $model; + if (is_string($this->indexBy)) { + $key = $model->{$this->indexBy}; + } else { + $key = call_user_func($this->indexBy, $model); + } + $models[$key] = $model; } } } diff --git a/yii/db/ActiveRecord.php b/framework/yii/db/ActiveRecord.php similarity index 76% rename from yii/db/ActiveRecord.php rename to framework/yii/db/ActiveRecord.php index 356819d..e1c4b4f 100644 --- a/yii/db/ActiveRecord.php +++ b/framework/yii/db/ActiveRecord.php @@ -14,23 +14,24 @@ use yii\base\InvalidParamException; use yii\base\ModelEvent; use yii\base\UnknownMethodException; use yii\base\InvalidCallException; -use yii\db\Connection; -use yii\db\TableSchema; -use yii\db\Expression; use yii\helpers\StringHelper; +use yii\helpers\Inflector; /** * ActiveRecord is the base class for classes representing relational data in terms of objects. * * @include @yii/db/ActiveRecord.md * - * @property Connection $db the database connection used by this AR class. - * @property TableSchema $tableSchema the schema information of the DB table associated with this AR class. - * @property array $oldAttributes the old attribute values (name-value pairs). - * @property array $dirtyAttributes the changed attribute values (name-value pairs). - * @property boolean $isNewRecord whether the record is new and should be inserted when calling [[save()]]. - * @property mixed $primaryKey the primary key value. - * @property mixed $oldPrimaryKey the old primary key value. + * @property array $dirtyAttributes The changed attribute values (name-value pairs). This property is + * read-only. + * @property boolean $isNewRecord Whether the record is new and should be inserted when calling [[save()]]. + * @property array $oldAttributes The old attribute values (name-value pairs). + * @property mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is + * returned if the primary key is composite or `$asArray` is true. A string is returned otherwise (null will be + * returned if the key value is null). This property is read-only. + * @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if + * the primary key is composite or `$asArray` is true. A string is returned otherwise (null will be returned if + * the key value is null). This property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -74,6 +75,24 @@ class ActiveRecord extends Model const EVENT_AFTER_DELETE = 'afterDelete'; /** + * The insert operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. + */ + const OP_INSERT = 0x01; + /** + * The update operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. + */ + const OP_UPDATE = 0x02; + /** + * The delete operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. + */ + const OP_DELETE = 0x04; + /** + * All three operations: insert, update, delete. + * This is a shortcut of the expression: OP_INSERT | OP_UPDATE | OP_DELETE. + */ + const OP_ALL = 0x07; + + /** * @var array attribute values indexed by attribute names */ private $_attributes = array(); @@ -169,7 +188,7 @@ class ActiveRecord extends Model * @param array $attributes attribute values (name-value pairs) to be saved into the table * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. + * @param array $params the parameters (name => value) to be bound to the query. * @return integer the number of rows updated */ public static function updateAll($attributes, $condition = '', $params = array()) @@ -191,7 +210,7 @@ class ActiveRecord extends Model * Use negative values if you want to decrement the counters. * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. + * @param array $params the parameters (name => value) to be bound to the query. * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method. * @return integer the number of rows updated */ @@ -219,7 +238,7 @@ class ActiveRecord extends Model * * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL. * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. + * @param array $params the parameters (name => value) to be bound to the query. * @return integer the number of rows deleted */ public static function deleteAll($condition = '', $params = array()) @@ -245,23 +264,29 @@ class ActiveRecord extends Model /** * Declares the name of the database table associated with this AR class. - * By default this method returns the class name as the table name by calling [[StringHelper::camel2id()]] + * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]] * with prefix 'tbl_'. For example, 'Customer' becomes 'tbl_customer', and 'OrderItem' becomes * 'tbl_order_item'. You may override this method if the table is not named after this convention. * @return string the table name */ public static function tableName() { - return 'tbl_' . StringHelper::camel2id(StringHelper::basename(get_called_class()), '_'); + return 'tbl_' . Inflector::camel2id(StringHelper::basename(get_called_class()), '_'); } /** * Returns the schema information of the DB table associated with this AR class. * @return TableSchema the schema information of the DB table associated with this AR class. + * @throws InvalidConfigException if the table for the AR class does not exist. */ public static function getTableSchema() { - return static::getDb()->getTableSchema(static::tableName()); + $schema = static::getDb()->getTableSchema(static::tableName()); + if ($schema !== null) { + return $schema; + } else { + throw new InvalidConfigException("The table does not exist: " . static::tableName()); + } } /** @@ -311,6 +336,38 @@ class ActiveRecord extends Model } /** + * Declares which DB operations should be performed within a transaction in different scenarios. + * The supported DB operations are: [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]], + * which correspond to the [[insert()]], [[update()]] and [[delete()]] methods, respectively. + * By default, these methods are NOT enclosed in a DB transaction. + * + * In some scenarios, to ensure data consistency, you may want to enclose some or all of them + * in transactions. You can do so by overriding this method and returning the operations + * that need to be transactional. For example, + * + * ~~~ + * return array( + * 'admin' => self::OP_INSERT, + * 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE, + * // the above is equivalent to the following: + * // 'api' => self::OP_ALL, + * + * ); + * ~~~ + * + * The above declaration specifies that in the "admin" scenario, the insert operation ([[insert()]]) + * should be done in a transaction; and in the "api" scenario, all the operations should be done + * in a transaction. + * + * @return array the declarations of transactional operations. The array keys are scenarios names, + * and the array values are the corresponding transaction operations. + */ + public function transactions() + { + return array(); + } + + /** * PHP getter magic method. * This method is overridden so that attributes and related objects can be accessed like properties. * @param string $name property name @@ -345,7 +402,7 @@ class ActiveRecord extends Model */ public function __set($name, $value) { - if (isset($this->_attributes[$name]) || isset($this->getTableSchema()->columns[$name])) { + if ($this->hasAttribute($name)) { $this->_attributes[$name] = $value; } else { parent::__set($name, $value); @@ -503,11 +560,26 @@ class ActiveRecord extends Model * Sets the named attribute value. * @param string $name the attribute name * @param mixed $value the attribute value. + * @throws InvalidParamException if the named attribute does not exist. * @see hasAttribute */ public function setAttribute($name, $value) { - $this->_attributes[$name] = $value; + if ($this->hasAttribute($name)) { + $this->_attributes[$name] = $value; + } else { + throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".'); + } + } + + /** + * Returns a value indicating whether the model has an attribute with the specified name. + * @param string $name the name of the attribute + * @return boolean whether the model has an attribute with the specified name. + */ + public function hasAttribute($name) + { + return isset($this->_attributes[$name]) || isset($this->getTableSchema()->columns[$name]); } /** @@ -547,11 +619,16 @@ class ActiveRecord extends Model * Sets the old value of the named attribute. * @param string $name the attribute name * @param mixed $value the old attribute value. + * @throws InvalidParamException if the named attribute does not exist. * @see hasAttribute */ public function setOldAttribute($name, $value) { - $this->_oldAttributes[$name] = $value; + if (isset($this->_oldAttributes[$name]) || isset($this->getTableSchema()->columns[$name])) { + $this->_oldAttributes[$name] = $value; + } else { + throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".'); + } } /** @@ -564,7 +641,7 @@ class ActiveRecord extends Model if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) { return $this->_attributes[$name] !== $this->_oldAttributes[$name]; } else { - return isset($this->_attributes[$name]) || isset($this->_oldAttributes); + return isset($this->_attributes[$name]) || isset($this->_oldAttributes[$name]); } } @@ -664,10 +741,39 @@ class ActiveRecord extends Model * @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 inserted successfully. + * @throws \Exception in case insert failed. */ public function insert($runValidation = true, $attributes = null) { - if ($runValidation && !$this->validate($attributes) || !$this->beforeSave(true)) { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + $db = static::getDb(); + if ($this->isTransactional(self::OP_INSERT) && $db->getTransaction() === null) { + $transaction = $db->beginTransaction(); + try { + $result = $this->insertInternal($attributes); + if ($result === false) { + $transaction->rollback(); + } else { + $transaction->commit(); + } + } catch (\Exception $e) { + $transaction->rollback(); + throw $e; + } + } else { + $result = $this->insertInternal($attributes); + } + return $result; + } + + /** + * @see ActiveRecord::insert() + */ + private function insertInternal($attributes = null) + { + if (!$this->beforeSave(true)) { return false; } $values = $this->getDirtyAttributes($attributes); @@ -678,22 +784,23 @@ class ActiveRecord extends Model } $db = static::getDb(); $command = $db->createCommand()->insert($this->tableName(), $values); - if ($command->execute()) { - $table = $this->getTableSchema(); - if ($table->sequenceName !== null) { - foreach ($table->primaryKey as $name) { - if (!isset($this->_attributes[$name])) { - $this->_oldAttributes[$name] = $this->_attributes[$name] = $db->getLastInsertID($table->sequenceName); - break; - } + if (!$command->execute()) { + return false; + } + $table = $this->getTableSchema(); + if ($table->sequenceName !== null) { + foreach ($table->primaryKey as $name) { + if (!isset($this->_attributes[$name])) { + $this->_oldAttributes[$name] = $this->_attributes[$name] = $db->getLastInsertID($table->sequenceName); + break; } } - foreach ($values as $name => $value) { - $this->_oldAttributes[$name] = $value; - } - $this->afterSave(true); - return true; } + foreach ($values as $name => $value) { + $this->_oldAttributes[$name] = $value; + } + $this->afterSave(true); + return true; } /** @@ -744,39 +851,68 @@ class ActiveRecord extends Model * or [[beforeSave()]] stops the updating process. * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data * being updated is outdated. + * @throws \Exception in case update failed. */ public function update($runValidation = true, $attributes = null) { - if ($runValidation && !$this->validate($attributes) || !$this->beforeSave(false)) { + if ($runValidation && !$this->validate($attributes)) { return false; } - $values = $this->getDirtyAttributes($attributes); - if (!empty($values)) { - $condition = $this->getOldPrimaryKey(true); - $lock = $this->optimisticLock(); - if ($lock !== null) { - if (!isset($values[$lock])) { - $values[$lock] = $this->$lock + 1; + $db = static::getDb(); + if ($this->isTransactional(self::OP_UPDATE) && $db->getTransaction() === null) { + $transaction = $db->beginTransaction(); + try { + $result = $this->updateInternal($attributes); + if ($result === false) { + $transaction->rollback(); + } else { + $transaction->commit(); } - $condition[$lock] = $this->$lock; + } catch (\Exception $e) { + $transaction->rollback(); + throw $e; } - // 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, $condition); + } else { + $result = $this->updateInternal($attributes); + } + return $result; + } - if ($lock !== null && !$rows) { - throw new StaleObjectException('The object being updated is outdated.'); + /** + * @see CActiveRecord::update() + * @throws StaleObjectException + */ + private function updateInternal($attributes = null) + { + if (!$this->beforeSave(false)) { + return false; + } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + $this->afterSave(false); + return 0; + } + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + 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. + $rows = $this->updateAll($values, $condition); - foreach ($values as $name => $value) { - $this->_oldAttributes[$name] = $this->_attributes[$name]; - } + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being updated is outdated.'); + } - $this->afterSave(false); - return $rows; - } else { - return 0; + foreach ($values as $name => $value) { + $this->_oldAttributes[$name] = $this->_attributes[$name]; } + $this->afterSave(false); + return $rows; } /** @@ -826,27 +962,43 @@ class ActiveRecord extends Model * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data * being deleted is outdated. + * @throws \Exception in case delete failed. */ 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 - $condition = $this->getOldPrimaryKey(true); - $lock = $this->optimisticLock(); - if ($lock !== null) { - $condition[$lock] = $this->$lock; + $db = static::getDb(); + $transaction = $this->isTransactional(self::OP_DELETE) && $db->getTransaction() === null ? $db->beginTransaction() : null; + try { + $result = false; + 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 + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + $condition[$lock] = $this->$lock; + } + $result = $this->deleteAll($condition); + if ($lock !== null && !$result) { + throw new StaleObjectException('The object being deleted is outdated.'); + } + $this->_oldAttributes = null; + $this->afterDelete(); } - $rows = $this->deleteAll($condition); - if ($lock !== null && !$rows) { - throw new StaleObjectException('The object being deleted is outdated.'); + if ($transaction !== null) { + if ($result === false) { + $transaction->rollback(); + } else { + $transaction->commit(); + } } - $this->_oldAttributes = null; - $this->afterDelete(); - return $rows; - } else { - return false; + } catch (\Exception $e) { + if ($transaction !== null) { + $transaction->rollback(); + } + throw $e; } + return $result; } /** @@ -859,6 +1011,16 @@ class ActiveRecord extends Model } /** + * Sets the value indicating whether the record is new. + * @param boolean $value whether the record is new and should be inserted when calling [[save()]]. + * @see getIsNewRecord + */ + public function setIsNewRecord($value) + { + $this->_oldAttributes = $value ? null : $this->_attributes; + } + + /** * Initializes the object. * This method is called at the end of the constructor. * The default implementation will trigger an [[EVENT_INIT]] event. @@ -883,16 +1045,6 @@ class ActiveRecord extends Model } /** - * Sets the value indicating whether the record is new. - * @param boolean $value whether the record is new and should be inserted when calling [[save()]]. - * @see getIsNewRecord - */ - public function setIsNewRecord($value) - { - $this->_oldAttributes = $value ? null : $this->_attributes; - } - - /** * This method is called at the beginning of inserting or updating a record. * The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is true, * or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is false. @@ -1014,7 +1166,7 @@ class ActiveRecord extends Model * @param boolean $asArray whether to return the primary key value as an array. If true, * the return value will be an array with column names as keys and column values as values. * Note that for composite primary keys, an array will always be returned regardless of this parameter value. - * @return mixed the primary key value. An array (column name=>column value) is returned if the primary key + * @return mixed the primary key value. An array (column name => column value) is returned if the primary key * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if * the key value is null). */ @@ -1040,7 +1192,7 @@ class ActiveRecord extends Model * @param boolean $asArray whether to return the primary key value as an array. If true, * the return value will be an array with column name as key and column value as value. * If this is false (default), a scalar value will be returned for non-composite primary key. - * @return mixed the old primary key value. An array (column name=>column value) is returned if the primary key + * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if * the key value is null). */ @@ -1061,7 +1213,7 @@ class ActiveRecord extends Model /** * Creates an active record object using a row of data. * This method is called by [[ActiveQuery]] to populate the query results - * into Active Records. + * into Active Records. It is not meant to be used to create new records. * @param array $row attribute values (name => value) * @return ActiveRecord the newly created active record. */ @@ -1124,7 +1276,7 @@ class ActiveRecord extends Model return $relation; } } catch (UnknownMethodException $e) { - throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); + throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e); } } @@ -1152,6 +1304,9 @@ class ActiveRecord extends Model $relation = $this->getRelation($name); if ($relation->via !== null) { + if ($this->getIsNewRecord() || $model->getIsNewRecord()) { + throw new InvalidCallException('Unable to link models: both models must NOT be newly created.'); + } if (is_array($relation->via)) { /** @var $viaRelation ActiveRelation */ list($viaName, $viaRelation) = $relation->via; @@ -1293,15 +1448,14 @@ class ActiveRecord extends Model * @param string $class the class name to be namespaced * @return string the namespaced class name */ - protected function getNamespacedClass($class) + protected static function getNamespacedClass($class) { if (strpos($class, '\\') === false) { - $primaryClass = get_class($this); - if (($pos = strrpos($primaryClass, '\\')) !== false) { - return substr($primaryClass, 0, $pos + 1) . $class; - } + $reflector = new \ReflectionClass(static::className()); + return $reflector->getNamespaceName() . '\\' . $class; + } else { + return $class; } - return $class; } /** @@ -1336,4 +1490,16 @@ class ActiveRecord extends Model } return true; } + + /** + * Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. + * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. + * @return boolean whether the specified operation is transactional in the current [[scenario]]. + */ + public function isTransactional($operation) + { + $scenario = $this->getScenario(); + $transactions = $this->transactions(); + return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation); + } } diff --git a/yii/db/ActiveRelation.php b/framework/yii/db/ActiveRelation.php similarity index 90% rename from yii/db/ActiveRelation.php rename to framework/yii/db/ActiveRelation.php index 42ae0e7..c1c8b2d 100644 --- a/yii/db/ActiveRelation.php +++ b/framework/yii/db/ActiveRelation.php @@ -8,8 +8,6 @@ namespace yii\db; -use yii\db\Connection; -use yii\db\Command; use yii\base\InvalidConfigException; /** @@ -53,11 +51,22 @@ class ActiveRelation extends ActiveQuery public $via; /** + * Clones internal objects. + */ + public function __clone() + { + if (is_object($this->via)) { + // make a clone of "via" object so that the same query object can be reused multiple times + $this->via = clone $this->via; + } + } + + /** * Specifies the relation associated with the pivot table. * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. * Its signature should be `function($query)`, where `$query` is the query to be customized. - * @return ActiveRelation the relation object itself. + * @return static the relation object itself. */ public function via($relationName, $callable = null) { @@ -77,7 +86,7 @@ class ActiveRelation extends ActiveQuery * in the [[primaryModel]] table. * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. * Its signature should be `function($query)`, where `$query` is the query to be customized. - * @return ActiveRelation + * @return static */ public function viaTable($tableName, $link, $callable = null) { @@ -270,7 +279,9 @@ class ActiveRelation extends ActiveQuery // single key $attribute = reset($this->link); foreach ($models as $model) { - $values[] = $model[$attribute]; + if (($value = $model[$attribute]) !== null) { + $values[] = $value; + } } } else { // composite keys @@ -298,7 +309,7 @@ class ActiveRelation extends ActiveQuery /** @var $primaryModel ActiveRecord */ $primaryModel = reset($primaryModels); $db = $primaryModel->getDb(); - $sql = $db->getQueryBuilder()->build($this); - return $db->createCommand($sql, $this->params)->queryAll(); + list ($sql, $params) = $db->getQueryBuilder()->build($this); + return $db->createCommand($sql, $params)->queryAll(); } } diff --git a/yii/db/ColumnSchema.php b/framework/yii/db/ColumnSchema.php similarity index 96% rename from yii/db/ColumnSchema.php rename to framework/yii/db/ColumnSchema.php index ffdafd4..cd2d9fa 100644 --- a/yii/db/ColumnSchema.php +++ b/framework/yii/db/ColumnSchema.php @@ -7,13 +7,15 @@ namespace yii\db; +use yii\base\Object; + /** * ColumnSchema class describes the metadata of a column in a database table. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class ColumnSchema extends \yii\base\Component +class ColumnSchema extends Object { /** * @var string name of this column (without quotes). @@ -88,6 +90,9 @@ class ColumnSchema extends \yii\base\Component if ($value === null || gettype($value) === $this->phpType || $value instanceof Expression) { return $value; } + if ($value === '' && $this->type !== Schema::TYPE_TEXT && $this->type !== Schema::TYPE_STRING) { + return null; + } switch ($this->phpType) { case 'string': return (string)$value; diff --git a/yii/db/Command.php b/framework/yii/db/Command.php similarity index 83% rename from yii/db/Command.php rename to framework/yii/db/Command.php index a117685..bd18e1f 100644 --- a/yii/db/Command.php +++ b/framework/yii/db/Command.php @@ -19,7 +19,7 @@ use yii\caching\Cache; * * To execute a non-query SQL (such as INSERT, DELETE, UPDATE), call [[execute()]]. * To execute a SQL statement that returns result data set (such as SELECT), - * use [[queryAll()]], [[queryRow()]], [[queryColumn()]], [[queryScalar()]], or [[query()]]. + * use [[queryAll()]], [[queryOne()]], [[queryColumn()]], [[queryScalar()]], or [[query()]]. * For example, * * ~~~ @@ -44,7 +44,9 @@ use yii\caching\Cache; * * To build SELECT SQL statements, please use [[QueryBuilder]] instead. * - * @property string $sql the SQL statement to be executed + * @property string $rawSql The raw SQL with parameter values inserted into the corresponding placeholders in + * [[sql]]. This property is read-only. + * @property string $sql The SQL statement to be executed. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -60,7 +62,7 @@ class Command extends \yii\base\Component */ public $pdoStatement; /** - * @var mixed the default fetch mode for this command. + * @var integer the default fetch mode for this command. * @see http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php */ public $fetchMode = \PDO::FETCH_ASSOC; @@ -69,7 +71,7 @@ class Command extends \yii\base\Component */ private $_sql; /** - * @var array the parameter log information (name=>value) + * @var array the parameter log information (name => value) */ private $_params = array(); @@ -86,7 +88,7 @@ class Command extends \yii\base\Component * Specifies the SQL statement to be executed. * 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 + * @return static this command instance */ public function setSql($sql) { @@ -102,7 +104,7 @@ 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 + * @return string the raw SQL with parameter values inserted into the corresponding placeholders in [[sql]]. */ public function getRawSql() { @@ -146,9 +148,9 @@ class Command extends \yii\base\Component try { $this->pdoStatement = $this->db->pdo->prepare($sql); } catch (\Exception $e) { - Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __METHOD__); + $message = $e->getMessage() . "\nFailed to prepare SQL: $sql"; $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($e->getMessage(), $errorInfo, (int)$e->getCode()); + throw new Exception($message, $errorInfo, (int)$e->getCode(), $e); } } } @@ -172,15 +174,16 @@ class Command extends \yii\base\Component * @param integer $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the value. * @param integer $length length of the data type * @param mixed $driverOptions the driver-specific options - * @return Command the current command being executed + * @return static the current command being executed * @see http://www.php.net/manual/en/function.PDOStatement-bindParam.php */ public function bindParam($name, &$value, $dataType = null, $length = null, $driverOptions = null) { $this->prepare(); if ($dataType === null) { - $this->pdoStatement->bindParam($name, $value, $this->getPdoType($value)); - } elseif ($length === null) { + $dataType = $this->db->getSchema()->getPdoType($value); + } + if ($length === null) { $this->pdoStatement->bindParam($name, $value, $dataType); } elseif ($driverOptions === null) { $this->pdoStatement->bindParam($name, $value, $dataType, $length); @@ -199,17 +202,16 @@ class Command extends \yii\base\Component * placeholders, this will be the 1-indexed position of the parameter. * @param mixed $value The value to bind to the parameter * @param integer $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the value. - * @return Command the current command being executed + * @return static the current command being executed * @see http://www.php.net/manual/en/function.PDOStatement-bindValue.php */ public function bindValue($name, $value, $dataType = null) { $this->prepare(); if ($dataType === null) { - $this->pdoStatement->bindValue($name, $value, $this->getPdoType($value)); - } else { - $this->pdoStatement->bindValue($name, $value, $dataType); + $dataType = $this->db->getSchema()->getPdoType($value); } + $this->pdoStatement->bindValue($name, $value, $dataType); $this->_params[$name] = $value; return $this; } @@ -220,10 +222,10 @@ class Command extends \yii\base\Component * Note that the SQL data type of each value is determined by its PHP type. * @param array $values the values to be bound. This must be given in terms of an associative * array with array keys being the parameter names, and array values the corresponding parameter values, - * e.g. `array(':name'=>'John', ':age'=>25)`. By default, the PDO type of each value is determined + * e.g. `array(':name' => 'John', ':age' => 25)`. By default, the PDO type of each value is determined * by its PHP type. You may explicitly specify the PDO type by using an array: `array(value, type)`, - * e.g. `array(':name'=>'John', ':profile'=>array($profile, \PDO::PARAM_LOB))`. - * @return Command the current command being executed + * e.g. `array(':name' => 'John', ':profile' => array($profile, \PDO::PARAM_LOB))`. + * @return static the current command being executed */ public function bindValues($values) { @@ -234,7 +236,7 @@ class Command extends \yii\base\Component $type = $value[1]; $value = $value[0]; } else { - $type = $this->getPdoType($value); + $type = $this->db->getSchema()->getPdoType($value); } $this->pdoStatement->bindValue($name, $value, $type); $this->_params[$name] = $value; @@ -244,25 +246,6 @@ class Command extends \yii\base\Component } /** - * Determines the PDO type for the give PHP data value. - * @param mixed $data the data whose PDO type is to be determined - * @return integer the PDO type - * @see http://www.php.net/manual/en/pdo.constants.php - */ - private function getPdoType($data) - { - static $typeMap = array( - 'boolean' => \PDO::PARAM_BOOL, - 'integer' => \PDO::PARAM_INT, - 'string' => \PDO::PARAM_STR, - 'resource' => \PDO::PARAM_LOB, - 'NULL' => \PDO::PARAM_NULL, - ); - $type = gettype($data); - return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; - } - - /** * Executes the SQL statement. * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs. * No result set will be returned. @@ -275,36 +258,27 @@ class Command extends \yii\base\Component $rawSql = $this->getRawSql(); - Yii::trace("Executing SQL: $rawSql", __METHOD__); + Yii::trace($rawSql, __METHOD__); if ($sql == '') { return 0; } + $token = $rawSql; try { - $token = "SQL: $sql"; - if ($this->db->enableProfiling) { - Yii::beginProfile($token, __METHOD__); - } + Yii::beginProfile($token, __METHOD__); $this->prepare(); $this->pdoStatement->execute(); $n = $this->pdoStatement->rowCount(); - if ($this->db->enableProfiling) { - Yii::endProfile($token, __METHOD__); - } + Yii::endProfile($token, __METHOD__); return $n; } catch (\Exception $e) { - if ($this->db->enableProfiling) { - Yii::endProfile($token, __METHOD__); - } - $message = $e->getMessage(); - - Yii::error("$message\nFailed to execute SQL: $rawSql", __METHOD__); - + Yii::endProfile($token, __METHOD__); + $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql"; $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($message, $errorInfo, (int)$e->getCode()); + throw new Exception($message, $errorInfo, (int)$e->getCode(), $e); } } @@ -321,7 +295,7 @@ class Command extends \yii\base\Component /** * Executes the SQL statement and returns ALL rows at once. - * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) + * @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. * @return array all rows of the query result. Each array element is an array representing a row of data. * An empty array is returned if the query results in nothing. @@ -335,13 +309,13 @@ class Command extends \yii\base\Component /** * Executes the SQL statement and returns the first row of the result. * This method is best used when only the first row of result is needed for a query. - * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) + * @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query * results in nothing. * @throws Exception execution failed */ - public function queryRow($fetchMode = null) + public function queryOne($fetchMode = null) { return $this->queryInternal('fetch', $fetchMode); } @@ -378,7 +352,7 @@ class Command extends \yii\base\Component /** * Performs the actual DB query of a SQL statement. * @param string $method method of PDOStatement to be called - * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) + * @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. * @return mixed the method execution result * @throws Exception if the query causes any problem @@ -386,10 +360,9 @@ class Command extends \yii\base\Component private function queryInternal($method, $fetchMode = null) { $db = $this->db; - $sql = $this->getSql(); $rawSql = $this->getRawSql(); - Yii::trace("Querying SQL: $rawSql", __METHOD__); + Yii::trace($rawSql, __METHOD__); /** @var $cache \yii\caching\Cache */ if ($db->enableQueryCache && $method !== '') { @@ -397,23 +370,21 @@ class Command extends \yii\base\Component } if (isset($cache) && $cache instanceof Cache) { - $cacheKey = $cache->buildKey(array( + $cacheKey = array( __CLASS__, $db->dsn, $db->username, $rawSql, - )); + ); if (($result = $cache->get($cacheKey)) !== false) { Yii::trace('Query result served from cache', __METHOD__); return $result; } } + $token = $rawSql; try { - $token = "SQL: $sql"; - if ($db->enableProfiling) { - Yii::beginProfile($token, __METHOD__); - } + Yii::beginProfile($token, __METHOD__); $this->prepare(); $this->pdoStatement->execute(); @@ -428,9 +399,7 @@ class Command extends \yii\base\Component $this->pdoStatement->closeCursor(); } - if ($db->enableProfiling) { - Yii::endProfile($token, __METHOD__); - } + Yii::endProfile($token, __METHOD__); if (isset($cache, $cacheKey) && $cache instanceof Cache) { $cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency); @@ -439,13 +408,10 @@ class Command extends \yii\base\Component return $result; } catch (\Exception $e) { - if ($db->enableProfiling) { - Yii::endProfile($token, __METHOD__); - } - $message = $e->getMessage(); - Yii::error("$message\nCommand::$method() failed: $rawSql", __METHOD__); + Yii::endProfile($token, __METHOD__); + $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql"; $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($message, $errorInfo, (int)$e->getCode()); + throw new Exception($message, $errorInfo, (int)$e->getCode(), $e); } } @@ -465,7 +431,7 @@ class Command extends \yii\base\Component * Note that the created command is not executed until [[execute()]] is called. * * @param string $table the table that new rows will be inserted into. - * @param array $columns the column data (name=>value) to be inserted into the table. + * @param array $columns the column data (name => value) to be inserted into the table. * @return Command the command object itself */ public function insert($table, $columns) @@ -487,7 +453,7 @@ class Command extends \yii\base\Component * ))->execute(); * ~~~ * - * Not that the values in each row must match the corresponding column names. + * Note that the values in each row must match the corresponding column names. * * @param string $table the table that new rows will be inserted into. * @param array $columns the column names @@ -515,8 +481,8 @@ class Command extends \yii\base\Component * Note that the created command is not executed until [[execute()]] is called. * * @param string $table the table to be updated. - * @param array $columns the column data (name=>value) to be updated. - * @param mixed $condition the condition that will be put in the WHERE part. Please + * @param array $columns the column data (name => value) to be updated. + * @param string|array $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 parameters to be bound to the command * @return Command the command object itself @@ -540,7 +506,7 @@ class Command extends \yii\base\Component * Note that the created command is not executed until [[execute()]] is called. * * @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 + * @param string|array $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 parameters to be bound to the command * @return Command the command object itself @@ -555,7 +521,7 @@ class Command extends \yii\base\Component /** * Creates a SQL command for creating a new DB table. * - * The columns in the new table should be specified as name-definition pairs (e.g. 'name'=>'string'), + * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'), * where name stands for a column name which will be properly quoted by the method, and definition * stands for the column type which can contain an abstract DB type. * The method [[QueryBuilder::getColumnType()]] will be called @@ -566,7 +532,7 @@ class Command extends \yii\base\Component * inserted into the generated SQL. * * @param string $table the name of the table to be created. The name will be properly quoted by the method. - * @param array $columns the columns (name=>definition) in the new table. + * @param array $columns the columns (name => definition) in the new table. * @param string $options additional SQL fragment that will be appended to the generated SQL. * @return Command the command object itself */ @@ -666,6 +632,32 @@ class Command extends \yii\base\Component } /** + * Creates a SQL command for adding a primary key constraint to an existing table. + * The method will properly quote the table and column names. + * @param string $name the name of the primary key constraint. + * @param string $table the table that the primary key constraint will be added to. + * @param string|array $columns comma separated string or array of columns that the primary key will consist of. + * @return Command the command object itself. + */ + public function addPrimaryKey($name, $table, $columns) + { + $sql = $this->db->getQueryBuilder()->addPrimaryKey($name, $table, $columns); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for removing a primary key constraint to an existing table. + * @param string $name the name of the primary key constraint to be removed. + * @param string $table the table that the primary key constraint will be removed from. + * @return Command the command object itself + */ + public function dropPrimaryKey($name, $table) + { + $sql = $this->db->getQueryBuilder()->dropPrimaryKey($name, $table); + return $this->setSql($sql); + } + + /** * Creates a SQL command for adding a foreign key constraint to an existing table. * The method will properly quote the table and column names. * @param string $name the name of the foreign key constraint. diff --git a/yii/db/Connection.php b/framework/yii/db/Connection.php similarity index 83% rename from yii/db/Connection.php rename to framework/yii/db/Connection.php index 37a5307..b79146d 100644 --- a/yii/db/Connection.php +++ b/framework/yii/db/Connection.php @@ -7,6 +7,8 @@ namespace yii\db; +use PDO; +use Yii; use yii\base\Component; use yii\base\InvalidConfigException; use yii\base\NotSupportedException; @@ -87,13 +89,16 @@ use yii\caching\Cache; * ) * ~~~ * + * @property string $driverName Name of the DB driver. This property is read-only. * @property boolean $isActive Whether the DB connection is established. This property is read-only. - * @property Transaction $transaction The currently active transaction. Null if no active transaction. - * @property Schema $schema The database schema information for the current connection. - * @property QueryBuilder $queryBuilder The query builder. - * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the sequence object. - * @property string $driverName Name of the DB driver currently being used. - * @property array $querySummary The statistical results of SQL queries. + * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the + * sequence object. This property is read-only. + * @property QueryBuilder $queryBuilder The query builder for the current DB connection. This property is + * read-only. + * @property Schema $schema The schema information for the database opened by this connection. This property + * is read-only. + * @property Transaction $transaction The currently active transaction. Null if no active transaction. This + * property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -121,14 +126,14 @@ class Connection extends Component */ public $password = ''; /** - * @var array PDO attributes (name=>value) that should be set when calling [[open()]] + * @var array PDO attributes (name => value) that should be set when calling [[open()]] * to establish a DB connection. Please refer to the * [PHP manual](http://www.php.net/manual/en/function.PDO-setAttribute.php) for * details about available attributes. */ public $attributes; /** - * @var \PDO the PHP PDO instance associated with this DB connection. + * @var PDO the PHP PDO instance associated with this DB connection. * This property is mainly managed by [[open()]] and [[close()]] methods. * When a DB connection is active, this property will represent a PDO instance; * otherwise, it will be null. @@ -196,7 +201,7 @@ class Connection extends Component public $queryCache = 'cache'; /** * @var string the charset used for database connection. The property is only used - * for MySQL and PostgreSQL databases. Defaults to null, meaning using default charset + * for MySQL, PostgreSQL and CUBRID databases. Defaults to null, meaning using default charset * as specified by the database. * * Note that if you're using GBK or BIG5 then it's highly recommended to @@ -213,13 +218,6 @@ class Connection extends Component */ public $emulatePrepare; /** - * @var boolean whether to enable profiling for the SQL statements being executed. - * Defaults to false. This should be mainly enabled and used during development - * to find out the bottleneck of SQL executions. - * @see getStats - */ - public $enableProfiling = false; - /** * @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 @@ -229,7 +227,7 @@ class Connection extends Component /** * @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 + * schema class name or configuration. Please refer to [[Yii::createObject()]] for * details on how to specify a configuration. * * This property is mainly used by [[getSchema()]] when fetching the database schema information. @@ -242,10 +240,11 @@ class Connection extends Component 'mysql' => 'yii\db\mysql\Schema', // MySQL 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3 'sqlite2' => 'yii\db\sqlite\Schema', // sqlite 2 - 'mssql' => 'yii\db\dao\mssql\Schema', // Mssql driver on windows hosts - 'sqlsrv' => 'yii\db\mssql\Schema', // Mssql + 'sqlsrv' => 'yii\db\mssql\Schema', // newer MSSQL driver on MS Windows hosts 'oci' => 'yii\db\oci\Schema', // Oracle driver - 'dblib' => 'yii\db\mssql\Schema', // dblib drivers on linux (and maybe others os) hosts + 'mssql' => 'yii\db\mssql\Schema', // older MSSQL driver on MS Windows hosts + 'dblib' => 'yii\db\mssql\Schema', // dblib drivers on GNU/Linux (and maybe other OSes) hosts + 'cubrid' => 'yii\db\cubrid\Schema', // CUBRID ); /** * @var Transaction the currently active transaction @@ -256,15 +255,6 @@ class Connection extends Component */ private $_schema; - /** - * Closes the connection when this component is being serialized. - * @return array - */ - public function __sleep() - { - $this->close(); - return array_keys(get_object_vars($this)); - } /** * Returns a value indicating whether the DB connection is established. @@ -312,15 +302,16 @@ class Connection extends Component if (empty($this->dsn)) { throw new InvalidConfigException('Connection::dsn cannot be empty.'); } + $token = 'Opening DB connection: ' . $this->dsn; try { - \Yii::trace('Opening DB connection: ' . $this->dsn, __METHOD__); + Yii::trace($token, __METHOD__); + Yii::beginProfile($token, __METHOD__); $this->pdo = $this->createPdoInstance(); $this->initConnection(); - } - catch (\PDOException $e) { - \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()); + Yii::endProfile($token, __METHOD__); + } catch (\PDOException $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), $e->errorInfo, (int)$e->getCode(), $e); } } } @@ -332,7 +323,7 @@ class Connection extends Component public function close() { if ($this->pdo !== null) { - \Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__); + Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__); $this->pdo = null; $this->_schema = null; $this->_transaction = null; @@ -344,11 +335,11 @@ class Connection extends Component * This method is called by [[open]] to establish a DB connection. * The default implementation will create a PHP PDO instance. * You may override this method if the default PDO needs to be adapted for certain DBMS. - * @return \PDO the pdo instance + * @return PDO the pdo instance */ protected function createPdoInstance() { - $pdoClass = '\PDO'; + $pdoClass = 'PDO'; if (($pos = strpos($this->dsn, ':')) !== false) { $driver = strtolower(substr($this->dsn, 0, $pos)); if ($driver === 'mssql' || $driver === 'dblib' || $driver === 'sqlsrv') { @@ -367,11 +358,11 @@ class Connection extends Component */ protected function initConnection() { - $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); - if ($this->emulatePrepare !== null && constant('\PDO::ATTR_EMULATE_PREPARES')) { - $this->pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare); + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + if ($this->emulatePrepare !== null && constant('PDO::ATTR_EMULATE_PREPARES')) { + $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare); } - if ($this->charset !== null && in_array($this->getDriverName(), array('pgsql', 'mysql', 'mysqli'))) { + if ($this->charset !== null && in_array($this->getDriverName(), array('pgsql', 'mysql', 'mysqli', 'cubrid'))) { $this->pdo->exec('SET NAMES ' . $this->pdo->quote($this->charset)); } $this->trigger(self::EVENT_AFTER_OPEN); @@ -428,7 +419,7 @@ class Connection extends Component } else { $driver = $this->getDriverName(); if (isset($this->schemaMap[$driver])) { - $this->_schema = \Yii::createObject($this->schemaMap[$driver]); + $this->_schema = Yii::createObject($this->schemaMap[$driver]); $this->_schema->db = $this; return $this->_schema; } else { @@ -510,19 +501,19 @@ 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]]. + * Also, the percentage character "%" at the beginning or ending of 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) { + return preg_replace_callback('/(\\{\\{(%?[\w\-\. ]+%?)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/', + function ($matches) { if (isset($matches[3])) { - return $db->quoteColumnName($matches[3]); + return $this->quoteColumnName($matches[3]); } else { - return str_replace('%', $db->tablePrefix, $db->quoteTableName($matches[2])); + return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2])); } }, $sql); } @@ -536,28 +527,8 @@ class Connection extends Component if (($pos = strpos($this->dsn, ':')) !== false) { return strtolower(substr($this->dsn, 0, $pos)); } else { - return strtolower($this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME)); - } - } - - /** - * Returns the statistical results of SQL queries. - * The results returned include the number of SQL statements executed and - * the total time spent. - * In order to use this method, [[enableProfiling]] has to be set true. - * @return array the first element indicates the number of SQL statements executed, - * and the second element the total time spent in SQL execution. - * @see \yii\logging\Logger::getProfiling() - */ - public function getQuerySummary() - { - $logger = \Yii::getLogger(); - $timings = $logger->getProfiling(array('yii\db\Command::query', 'yii\db\Command::execute')); - $count = count($timings); - $time = 0; - foreach ($timings as $timing) { - $time += $timing[1]; + $this->open(); + return strtolower($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME)); } - return array($count, $time); } } diff --git a/yii/db/DataReader.php b/framework/yii/db/DataReader.php similarity index 91% rename from yii/db/DataReader.php rename to framework/yii/db/DataReader.php index 20444e7..f2990c1 100644 --- a/yii/db/DataReader.php +++ b/framework/yii/db/DataReader.php @@ -24,7 +24,7 @@ use yii\base\InvalidCallException; * } * * // equivalent to: - * foreach($reader as $row) { + * foreach ($reader as $row) { * $rows[] = $row; * } * @@ -39,10 +39,10 @@ use yii\base\InvalidCallException; * [[fetchMode]]. See the [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for more details about possible fetch mode. * - * @property boolean $isClosed whether the reader is closed or not. - * @property integer $rowCount number of rows contained in the result. - * @property integer $columnCount the number of columns in the result set. - * @property mixed $fetchMode fetch mode used when retrieving the data. + * @property integer $columnCount The number of columns in the result set. This property is read-only. + * @property integer $fetchMode Fetch mode. This property is write-only. + * @property boolean $isClosed Whether the reader is closed or not. This property is read-only. + * @property integer $rowCount Number of rows contained in the result. This property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -73,7 +73,7 @@ class DataReader extends \yii\base\Object implements \Iterator, \Countable * Binds a column to a PHP variable. * When rows of data are being fetched, the corresponding column value * will be set in the variable. Note, the fetch mode must include PDO::FETCH_BOUND. - * @param mixed $column Number of the column (1-indexed) or name of the column + * @param integer|string $column Number of the column (1-indexed) or name of the column * in the result set. If using the column name, be aware that the name * should match the case of the column, as returned by the driver. * @param mixed $value Name of the PHP variable to which the column will be bound. @@ -91,7 +91,7 @@ class DataReader extends \yii\base\Object implements \Iterator, \Countable /** * Set the default fetch mode for this statement - * @param mixed $mode fetch mode + * @param integer $mode fetch mode * @see http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php */ public function setFetchMode($mode) @@ -112,7 +112,7 @@ class DataReader extends \yii\base\Object implements \Iterator, \Countable /** * Returns a single column from the next row of a result set. * @param integer $columnIndex zero-based column index - * @return mixed the column of the current row, false if no more row available + * @return mixed the column of the current row, false if no more rows available */ public function readColumn($columnIndex) { diff --git a/yii/db/Exception.php b/framework/yii/db/Exception.php similarity index 86% rename from yii/db/Exception.php rename to framework/yii/db/Exception.php index b7a60b4..25ae39f 100644 --- a/yii/db/Exception.php +++ b/framework/yii/db/Exception.php @@ -16,19 +16,19 @@ namespace yii\db; class Exception extends \yii\base\Exception { /** - * @var mixed the error info provided by a PDO exception. This is the same as returned + * @var array the error info provided by a PDO exception. This is the same as returned * by [PDO::errorInfo](http://www.php.net/manual/en/pdo.errorinfo.php). */ - public $errorInfo; + public $errorInfo = array(); /** * Constructor. * @param string $message PDO error message - * @param mixed $errorInfo PDO error info + * @param array $errorInfo PDO error info * @param integer $code PDO error code * @param \Exception $previous The previous exception used for the exception chaining. */ - public function __construct($message, $errorInfo = null, $code = 0, \Exception $previous = null) + public function __construct($message, $errorInfo = array(), $code = 0, \Exception $previous = null) { $this->errorInfo = $errorInfo; parent::__construct($message, $code, $previous); @@ -39,6 +39,6 @@ class Exception extends \yii\base\Exception */ public function getName() { - return \Yii::t('yii|Database Exception'); + return \Yii::t('yii', 'Database Exception'); } } diff --git a/yii/db/Expression.php b/framework/yii/db/Expression.php similarity index 100% rename from yii/db/Expression.php rename to framework/yii/db/Expression.php diff --git a/yii/db/Migration.php b/framework/yii/db/Migration.php similarity index 92% rename from yii/db/Migration.php rename to framework/yii/db/Migration.php index f51e597..7368788 100644 --- a/yii/db/Migration.php +++ b/framework/yii/db/Migration.php @@ -10,14 +10,14 @@ namespace yii\db; /** * Migration is the base class for representing a database migration. * - * Migration is designed to be used together with the "yiic migrate" command. + * Migration is designed to be used together with the "yii migrate" command. * * Each child class of Migration represents an individual database migration which * is identified by the child class name. * * Within each migration, the [[up()]] method should be overwritten to contain the logic * for "upgrading" the database; while the [[down()]] method for the "downgrading" - * logic. The "yiic migrate" command manages all available migrations in an application. + * logic. The "yii migrate" command manages all available migrations in an application. * * If the database supports transactions, you may also overwrite [[safeUp()]] and * [[safeDown()]] so that if anything wrong happens during the upgrading or downgrading, @@ -132,7 +132,8 @@ class Migration extends \yii\base\Component * Executes a SQL statement. * This method executes the specified SQL statement using [[db]]. * @param string $sql the SQL statement to be executed - * @param array $params input parameters (name=>value) for the SQL execution. See [[Command::execute()]] for more details. + * @param array $params input parameters (name => value) for the SQL execution. + * See [[Command::execute()]] for more details. */ public function execute($sql, $params = array()) { @@ -146,7 +147,7 @@ class Migration extends \yii\base\Component * Creates and executes an INSERT SQL statement. * The method will properly escape the column names, and bind the values to be inserted. * @param string $table the table that new rows will be inserted into. - * @param array $columns the column data (name=>value) to be inserted into the table. + * @param array $columns the column data (name => value) to be inserted into the table. */ public function insert($table, $columns) { @@ -160,8 +161,8 @@ class Migration extends \yii\base\Component * Creates and executes an UPDATE SQL statement. * The method will properly escape the column names and bind the values to be updated. * @param string $table the table to be updated. - * @param array $columns the column data (name=>value) to be updated. - * @param mixed $condition the conditions that will be put in the WHERE part. Please + * @param array $columns the column data (name => value) to be updated. + * @param array|string $condition the conditions that will be put in the WHERE part. Please * refer to [[Query::where()]] on how to specify conditions. * @param array $params the parameters to be bound to the query. */ @@ -176,7 +177,7 @@ class Migration extends \yii\base\Component /** * Creates and executes a DELETE SQL statement. * @param string $table the table where the data will be deleted from. - * @param mixed $condition the conditions that will be put in the WHERE part. Please + * @param array|string $condition the conditions that will be put in the WHERE part. Please * refer to [[Query::where()]] on how to specify conditions. * @param array $params the parameters to be bound to the query. */ @@ -191,7 +192,7 @@ class Migration extends \yii\base\Component /** * Builds and executes a SQL statement for creating a new DB table. * - * The columns in the new table should be specified as name-definition pairs (e.g. 'name'=>'string'), + * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'), * where name stands for a column name which will be properly quoted by the method, and definition * stands for the column type which can contain an abstract DB type. * @@ -201,7 +202,7 @@ class Migration extends \yii\base\Component * put into the generated SQL. * * @param string $table the name of the table to be created. The name will be properly quoted by the method. - * @param array $columns the columns (name=>definition) in the new table. + * @param array $columns the columns (name => definition) in the new table. * @param string $options additional SQL fragment that will be appended to the generated SQL. */ public function createTable($table, $columns, $options = null) @@ -309,6 +310,35 @@ class Migration extends \yii\base\Component } /** + * Builds and executes a SQL statement for creating a primary key. + * The method will properly quote the table and column names. + * @param string $name the name of the primary key constraint. + * @param string $table the table that the primary key constraint will be added to. + * @param string|array $columns comma separated string or array of columns that the primary key will consist of. + */ + public function addPrimaryKey($name, $table, $columns) + { + echo " > add primary key $name on $table (".(is_array($columns) ? implode(',', $columns) : $columns).") ..."; + $time = microtime(true); + $this->db->createCommand()->addPrimaryKey($name, $table, $columns)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for dropping a primary key. + * @param string $name the name of the primary key constraint to be removed. + * @param string $table the table that the primary key constraint will be removed from. + * @return Command the command object itself + */ + public function dropPrimaryKey($name, $table) + { + echo " > drop primary key $name ..."; + $time = microtime(true); + $this->db->createCommand()->dropPrimaryKey($name, $table)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** * Builds a SQL statement for adding a foreign key constraint to an existing table. * The method will properly quote the table and column names. * @param string $name the name of the foreign key constraint. diff --git a/yii/db/Query.php b/framework/yii/db/Query.php similarity index 77% rename from yii/db/Query.php rename to framework/yii/db/Query.php index b1fc718..dbe0424 100644 --- a/yii/db/Query.php +++ b/framework/yii/db/Query.php @@ -7,6 +7,9 @@ namespace yii\db; +use Yii; +use yii\base\Component; + /** * Query represents a SELECT SQL statement in a way that is independent of DBMS. * @@ -32,7 +35,7 @@ namespace yii\db; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Query extends \yii\base\Component +class Query extends Component { /** * Sort ascending @@ -40,7 +43,7 @@ class Query extends \yii\base\Component */ const SORT_ASC = false; /** - * Sort ascending + * Sort descending * @see orderBy */ const SORT_DESC = true; @@ -124,9 +127,16 @@ class Query extends \yii\base\Component public $union; /** * @var array list of query parameter values indexed by parameter placeholders. - * For example, `array(':name'=>'Dan', ':age'=>31)`. + * For example, `array(':name' => 'Dan', ':age' => 31)`. */ public $params; + /** + * @var string|callable $column the name of the column by which the query results should be indexed by. + * This can also be a callable (e.g. anonymous function) that returns the index value based on the given + * row data. For more details, see [[indexBy()]]. This property is only used by [[all()]]. + */ + public $indexBy; + /** * Creates a DB command that can be used to execute this query. @@ -137,10 +147,173 @@ class Query extends \yii\base\Component public function createCommand($db = null) { if ($db === null) { - $db = \Yii::$app->db; + $db = Yii::$app->getDb(); } - $sql = $db->getQueryBuilder()->build($this); - return $db->createCommand($sql, $this->params); + list ($sql, $params) = $db->getQueryBuilder()->build($this); + return $db->createCommand($sql, $params); + } + + /** + * Sets the [[indexBy]] property. + * @param string|callable $column the name of the column by which the query results should be indexed by. + * This can also be a callable (e.g. anonymous function) that returns the index value based on the given + * row data. The signature of the callable should be: + * + * ~~~ + * function ($row) + * { + * // return the index value corresponding to $row + * } + * ~~~ + * + * @return static the query object itself + */ + public function indexBy($column) + { + $this->indexBy = $column; + return $this; + } + + /** + * Executes the query and returns all results as an array. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null) + { + $rows = $this->createCommand($db)->queryAll(); + if ($this->indexBy === null) { + return $rows; + } + $result = array(); + foreach ($rows as $row) { + if (is_string($this->indexBy)) { + $key = $row[$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row); + } + $result[$key] = $row; + } + return $result; + } + + /** + * Executes the query and returns a single row of result. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * results in nothing. + */ + public function one($db = null) + { + return $this->createCommand($db)->queryOne(); + } + + /** + * Returns the query result as a scalar value. + * The value returned will be the first column in the first row of the query results. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return string|boolean the value of the first column in the first row of the query result. + * False is returned if the query result is empty. + */ + public function scalar($db = null) + { + return $this->createCommand($db)->queryScalar(); + } + + /** + * Executes the query and returns the first column of the result. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return array the first column of the query result. An empty array is returned if the query results in nothing. + */ + public function column($db = null) + { + return $this->createCommand($db)->queryColumn(); + } + + /** + * Returns the number of records. + * @param string $q the COUNT expression. Defaults to '*'. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return integer number of records + */ + public function count($q = '*', $db = null) + { + $this->select = array("COUNT($q)"); + return $this->createCommand($db)->queryScalar(); + } + + /** + * Returns the sum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return integer the sum of the specified column values + */ + public function sum($q, $db = null) + { + $this->select = array("SUM($q)"); + return $this->createCommand($db)->queryScalar(); + } + + /** + * Returns the average of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return integer the average of the specified column values. + */ + public function average($q, $db = null) + { + $this->select = array("AVG($q)"); + return $this->createCommand($db)->queryScalar(); + } + + /** + * Returns the minimum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return integer the minimum of the specified column values. + */ + public function min($q, $db = null) + { + $this->select = array("MIN($q)"); + return $this->createCommand($db)->queryScalar(); + } + + /** + * Returns the maximum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return integer the maximum of the specified column values. + */ + public function max($q, $db = null) + { + $this->select = array("MAX($q)"); + return $this->createCommand($db)->queryScalar(); + } + + /** + * Returns a value indicating whether the query result contains any row of data. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return boolean whether the query result contains any row of data. + */ + public function exists($db = null) + { + $this->select = array(new Expression('1')); + return $this->scalar($db) !== false; } /** @@ -152,7 +325,7 @@ class Query extends \yii\base\Component * (which means the column contains a DB expression). * @param string $option additional option that should be appended to the 'SELECT' keyword. For example, * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. - * @return Query the query object itself + * @return static the query object itself */ public function select($columns, $option = null) { @@ -167,7 +340,7 @@ class Query extends \yii\base\Component /** * Sets the value indicating whether to SELECT DISTINCT or not. * @param bool $value whether to SELECT DISTINCT or not. - * @return Query the query object itself + * @return static the query object itself */ public function distinct($value = true) { @@ -182,7 +355,7 @@ class Query extends \yii\base\Component * Table names can contain schema prefixes (e.g. `'public.tbl_user'`) and/or table aliases (e.g. `'tbl_user u'`). * The method will automatically quote the table names unless it contains some parenthesis * (which means the table is given as a sub-query or DB expression). - * @return Query the query object itself + * @return static the query object itself */ public function from($tables) { @@ -210,9 +383,9 @@ class Query extends \yii\base\Component * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used * in the generated expression. Below are some examples: * - * - `array('type'=>1, 'status'=>2)` generates `(type=1) AND (status=2)`. - * - `array('id'=>array(1,2,3), 'status'=>2)` generates `(id IN (1,2,3)) AND (status=2)`. - * - `array('status'=>null) generates `status IS NULL`. + * - `array('type' => 1, 'status' => 2)` generates `(type = 1) AND (status = 2)`. + * - `array('id' => array(1, 2, 3), 'status' => 2)` generates `(id IN (1, 2, 3)) AND (status = 2)`. + * - `array('status' => null) generates `status IS NULL`. * * A condition in operator format generates the SQL expression according to the specified operator, which * can be one of the followings: @@ -234,7 +407,7 @@ class Query extends \yii\base\Component * * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing * the range of the values that the column or DB expression should be in. For example, - * `array('in', 'id', array(1,2,3))` will generate `id IN (1,2,3)`. + * `array('in', 'id', array(1, 2, 3))` will generate `id IN (1, 2, 3)`. * The method will properly quote the column name and escape values in the range. * * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. @@ -257,8 +430,8 @@ class Query extends \yii\base\Component * the `NOT LIKE` predicates. * * @param string|array $condition the conditions that should be put in the WHERE part. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself * @see andWhere() * @see orWhere() */ @@ -274,8 +447,8 @@ class Query extends \yii\base\Component * The new condition and the existing one will be joined using the 'AND' operator. * @param string|array $condition the new WHERE condition. Please refer to [[where()]] * on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself * @see where() * @see orWhere() */ @@ -295,8 +468,8 @@ class Query extends \yii\base\Component * The new condition and the existing one will be joined using the 'OR' operator. * @param string|array $condition the new WHERE condition. Please refer to [[where()]] * on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself * @see where() * @see andWhere() */ @@ -321,7 +494,7 @@ class Query extends \yii\base\Component * (which means the table is given as a sub-query or DB expression). * @param string|array $on the join condition that should appear in the ON part. * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. + * @param array $params the parameters (name => value) to be bound to the query. * @return Query the query object itself */ public function join($type, $table, $on = '', $params = array()) @@ -338,7 +511,7 @@ class Query extends \yii\base\Component * (which means the table is given as a sub-query or DB expression). * @param string|array $on the join condition that should appear in the ON part. * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. + * @param array $params the parameters (name => value) to be bound to the query. * @return Query the query object itself */ public function innerJoin($table, $on = '', $params = array()) @@ -355,7 +528,7 @@ class Query extends \yii\base\Component * (which means the table is given as a sub-query or DB expression). * @param string|array $on the join condition that should appear in the ON part. * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query + * @param array $params the parameters (name => value) to be bound to the query * @return Query the query object itself */ public function leftJoin($table, $on = '', $params = array()) @@ -372,7 +545,7 @@ class Query extends \yii\base\Component * (which means the table is given as a sub-query or DB expression). * @param string|array $on the join condition that should appear in the ON part. * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query + * @param array $params the parameters (name => value) to be bound to the query * @return Query the query object itself */ public function rightJoin($table, $on = '', $params = array()) @@ -387,7 +560,7 @@ class Query extends \yii\base\Component * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). - * @return Query the query object itself + * @return static the query object itself * @see addGroupBy() */ public function groupBy($columns) @@ -405,7 +578,7 @@ class Query extends \yii\base\Component * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). - * @return Query the query object itself + * @return static the query object itself * @see groupBy() */ public function addGroupBy($columns) @@ -425,8 +598,8 @@ class Query extends \yii\base\Component * Sets the HAVING part of the query. * @param string|array $condition the conditions to be put after HAVING. * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself * @see andHaving() * @see orHaving() */ @@ -442,8 +615,8 @@ class Query extends \yii\base\Component * The new condition and the existing one will be joined using the 'AND' operator. * @param string|array $condition the new HAVING condition. Please refer to [[where()]] * on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself * @see having() * @see orHaving() */ @@ -463,8 +636,8 @@ class Query extends \yii\base\Component * The new condition and the existing one will be joined using the 'OR' operator. * @param string|array $condition the new HAVING condition. Please refer to [[where()]] * on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself * @see having() * @see andHaving() */ @@ -486,7 +659,7 @@ class Query extends \yii\base\Component * (e.g. `array('id' => Query::SORT_ASC, 'name' => Query::SORT_DESC)`). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). - * @return Query the query object itself + * @return static the query object itself * @see addOrderBy() */ public function orderBy($columns) @@ -502,7 +675,7 @@ class Query extends \yii\base\Component * (e.g. `array('id' => Query::SORT_ASC, 'name' => Query::SORT_DESC)`). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). - * @return Query the query object itself + * @return static the query object itself * @see orderBy() */ public function addOrderBy($columns) @@ -536,8 +709,8 @@ class Query extends \yii\base\Component /** * Sets the LIMIT part of the query. - * @param integer $limit the limit - * @return Query the query object itself + * @param integer $limit the limit. Use null or negative value to disable limit. + * @return static the query object itself */ public function limit($limit) { @@ -547,8 +720,8 @@ class Query extends \yii\base\Component /** * Sets the OFFSET part of the query. - * @param integer $offset the offset - * @return Query the query object itself + * @param integer $offset the offset. Use null or negative value to disable offset. + * @return static the query object itself */ public function offset($offset) { @@ -559,7 +732,7 @@ class Query extends \yii\base\Component /** * Appends a SQL statement using UNION operator. * @param string|Query $sql the SQL statement to be appended using UNION - * @return Query the query object itself + * @return static the query object itself */ public function union($sql) { @@ -570,8 +743,8 @@ class Query extends \yii\base\Component /** * Sets the parameters to be bound to the query. * @param array $params list of query parameter values indexed by parameter placeholders. - * For example, `array(':name'=>'Dan', ':age'=>31)`. - * @return Query the query object itself + * For example, `array(':name' => 'Dan', ':age' => 31)`. + * @return static the query object itself * @see addParams() */ public function params($params) @@ -583,8 +756,8 @@ class Query extends \yii\base\Component /** * Adds additional parameters to be bound to the query. * @param array $params list of query parameter values indexed by parameter placeholders. - * For example, `array(':name'=>'Dan', ':age'=>31)`. - * @return Query the query object itself + * For example, `array(':name' => 'Dan', ':age' => 31)`. + * @return static the query object itself * @see params() */ public function addParams($params) diff --git a/yii/db/QueryBuilder.php b/framework/yii/db/QueryBuilder.php similarity index 85% rename from yii/db/QueryBuilder.php rename to framework/yii/db/QueryBuilder.php index a4e30ea..f210f65 100644 --- a/yii/db/QueryBuilder.php +++ b/framework/yii/db/QueryBuilder.php @@ -7,7 +7,6 @@ namespace yii\db; -use yii\db\Exception; use yii\base\NotSupportedException; /** @@ -56,22 +55,24 @@ class QueryBuilder extends \yii\base\Object /** * Generates a SELECT SQL statement from a [[Query]] object. * @param Query $query the [[Query]] object from which the SQL statement will be generated - * @return string the generated SQL statement + * @return array the generated SQL statement (the first array element) and the corresponding + * parameters to be bound to the SQL statement (the second array element). */ public function build($query) { + $params = $query->params; $clauses = array( $this->buildSelect($query->select, $query->distinct, $query->selectOption), $this->buildFrom($query->from), - $this->buildJoin($query->join, $query->params), - $this->buildWhere($query->where, $query->params), + $this->buildJoin($query->join, $params), + $this->buildWhere($query->where, $params), $this->buildGroupBy($query->groupBy), - $this->buildHaving($query->having, $query->params), - $this->buildUnion($query->union, $query->params), + $this->buildHaving($query->having, $params), + $this->buildUnion($query->union, $params), $this->buildOrderBy($query->orderBy), $this->buildLimit($query->limit, $query->offset), ); - return implode($this->separator, array_filter($clauses)); + return array(implode($this->separator, array_filter($clauses)), $params); } /** @@ -88,13 +89,18 @@ class QueryBuilder extends \yii\base\Object * The method will properly escape the table and column names. * * @param string $table the table that new rows will be inserted into. - * @param array $columns the column data (name=>value) to be inserted into the table. + * @param array $columns the column data (name => value) to be inserted into the table. * @param array $params the binding parameters that will be generated by this method. * They should be bound to the DB command later. * @return string the INSERT SQL */ public function insert($table, $columns, &$params) { + if (($tableSchema = $this->db->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = array(); + } $names = array(); $placeholders = array(); foreach ($columns as $name => $value) { @@ -107,7 +113,7 @@ class QueryBuilder extends \yii\base\Object } else { $phName = self::PARAM_PREFIX . count($params); $placeholders[] = $phName; - $params[$phName] = $value; + $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($value) : $value; } } @@ -128,7 +134,7 @@ class QueryBuilder extends \yii\base\Object * ))->execute(); * ~~~ * - * Not that the values in each row must match the corresponding column names. + * Note that the values in each row must match the corresponding column names. * * @param string $table the table that new rows will be inserted into. * @param array $columns the column names @@ -156,8 +162,8 @@ class QueryBuilder extends \yii\base\Object * The method will properly escape the table and column names. * * @param string $table the table to be updated. - * @param array $columns the column data (name=>value) to be updated. - * @param mixed $condition the condition that will be put in the WHERE part. Please + * @param array $columns the column data (name => value) to be updated. + * @param array|string $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. @@ -165,6 +171,12 @@ class QueryBuilder extends \yii\base\Object */ public function update($table, $columns, $condition, &$params) { + if (($tableSchema = $this->db->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = array(); + } + $lines = array(); foreach ($columns as $name => $value) { if ($value instanceof Expression) { @@ -175,7 +187,7 @@ class QueryBuilder extends \yii\base\Object } else { $phName = self::PARAM_PREFIX . count($params); $lines[] = $this->db->quoteColumnName($name) . '=' . $phName; - $params[$phName] = $value; + $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($value) : $value; } } @@ -195,7 +207,7 @@ class QueryBuilder extends \yii\base\Object * The method will properly escape the table and column names. * * @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 + * @param array|string $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. @@ -211,7 +223,7 @@ class QueryBuilder extends \yii\base\Object /** * Builds a SQL statement for creating a new DB table. * - * The columns in the new table should be specified as name-definition pairs (e.g. 'name'=>'string'), + * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'), * where name stands for a column name which will be properly quoted by the method, and definition * stands for the column type which can contain an abstract DB type. * The [[getColumnType()]] method will be invoked to convert any abstract type into a physical one. @@ -230,7 +242,7 @@ class QueryBuilder extends \yii\base\Object * ~~~ * * @param string $table the name of the table to be created. The name will be properly quoted by the method. - * @param array $columns the columns (name=>definition) in the new table. + * @param array $columns the columns (name => definition) in the new table. * @param string $options additional SQL fragment that will be appended to the generated SQL. * @return string the SQL statement for creating a new DB table. */ @@ -268,6 +280,40 @@ class QueryBuilder extends \yii\base\Object { return "DROP TABLE " . $this->db->quoteTableName($table); } + + /** + * Builds a SQL statement for adding a primary key constraint to an existing table. + * @param string $name the name of the primary key constraint. + * @param string $table the table that the primary key constraint will be added to. + * @param string|array $columns comma separated string or array of columns that the primary key will consist of. + * @return string the SQL statement for adding a primary key constraint to an existing table. + */ + public function addPrimaryKey($name, $table, $columns) + { + if (is_string($columns)) { + $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY); + } + + foreach ($columns as $i => $col) { + $columns[$i] = $this->db->quoteColumnName($col); + } + + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT ' + . $this->db->quoteColumnName($name) . ' PRIMARY KEY (' + . implode(', ', $columns). ' )'; + } + + /** + * Builds a SQL statement for removing a primary key constraint to an existing table. + * @param string $name the name of the primary key constraint to be removed. + * @param string $table the table that the primary key constraint will be removed from. + * @return string the SQL statement for removing a primary key constraint from an existing table. * + */ + public function dropPrimaryKey($name, $table) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) + . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name); + } /** * Builds a SQL statement for truncating a DB table. @@ -415,7 +461,7 @@ class QueryBuilder extends \yii\base\Object * The sequence will be reset such that the primary key of the next new row inserted * will have the specified value or 1. * @param string $table the name of the table whose primary key sequence will be reset - * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * @param array|string $value the value for the primary key of the next new row inserted. If this is not set, * the next new row's primary key will have a value 1. * @return string the SQL statement for resetting sequence * @throws NotSupportedException if this is not supported by the underlying DBMS @@ -429,10 +475,11 @@ class QueryBuilder extends \yii\base\Object * Builds a SQL statement for enabling or disabling integrity check. * @param boolean $check whether to turn on or off the integrity check. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @param string $table the table name. Defaults to empty string, meaning that no table will be changed. * @return string the SQL statement for checking integrity * @throws NotSupportedException if this is not supported by the underlying DBMS */ - public function checkIntegrity($check = true, $schema = '') + public function checkIntegrity($check = true, $schema = '', $table = '') { throw new NotSupportedException($this->db->getDriverName() . ' does not support enabling/disabling integrity check.'); } @@ -444,6 +491,7 @@ class QueryBuilder extends \yii\base\Object * physical types): * * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY" + * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY" * - `string`: string type, will be converted into "varchar(255)" * - `text`: a long string type, will be converted into "text" * - `smallint`: a small integer type, will be converted into "smallint(6)" @@ -463,6 +511,12 @@ class QueryBuilder extends \yii\base\Object * the first part will be converted, and the rest of the parts will be appended to the converted result. * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'. * + * For some of the abstract types you can also specify a length or precision constraint + * by prepending it in round brackets directly to the type. + * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. + * If the underlying DBMS does not support these kind of constraints for a type it will + * be ignored. + * * If a type cannot be found in [[typeMap]], it will be returned without any change. * @param string $type abstract column type * @return string physical column type. @@ -471,6 +525,10 @@ class QueryBuilder extends \yii\base\Object { if (isset($this->typeMap[$type])) { return $this->typeMap[$type]; + } elseif (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) { + if (isset($this->typeMap[$matches[1]])) { + return preg_replace('/\(.+\)/', '(' . $matches[2] . ')', $this->typeMap[$matches[1]]) . $matches[3]; + } } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) { if (isset($this->typeMap[$matches[1]])) { return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type); @@ -527,7 +585,7 @@ class QueryBuilder extends \yii\base\Object foreach ($tables as $i => $table) { if (strpos($table, '(') === false) { - if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/i', $table, $matches)) { // with alias + if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias $tables[$i] = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); } else { $tables[$i] = $this->db->quoteTableName($table); @@ -561,7 +619,7 @@ class QueryBuilder extends \yii\base\Object // 0:join type, 1:table name, 2:on-condition $table = $join[1]; if (strpos($table, '(') === false) { - if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $table, $matches)) { // with alias + if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias $table = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); } else { $table = $this->db->quoteTableName($table); @@ -663,9 +721,11 @@ class QueryBuilder extends \yii\base\Object } foreach ($unions as $i => $union) { if ($union instanceof Query) { + // save the original parameters so that we can restore them later to prevent from modifying the query object + $originalParams = $union->params; $union->addParams($params); - $unions[$i] = $this->build($union); - $params = $union->params; + list ($unions[$i], $params) = $this->build($union); + $union->params = $originalParams; } } return "UNION (\n" . implode("\n) UNION (\n", $unions) . "\n)"; @@ -734,7 +794,7 @@ class QueryBuilder extends \yii\base\Object } else { throw new Exception('Found unknown operator in query: ' . $operator); } - } else { // hash format: 'column1'=>'value1', 'column2'=>'value2', ... + } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... return $this->buildHashCondition($condition, $params); } } @@ -744,7 +804,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), $params); + $parts[] = $this->buildInCondition('IN', array($column, $value), $params); } else { if (strpos($column, '(') === false) { $column = $this->db->quoteColumnName($column); @@ -796,8 +856,8 @@ class QueryBuilder extends \yii\base\Object $column = $this->db->quoteColumnName($column); } $phName1 = self::PARAM_PREFIX . count($params); - $phName2 = self::PARAM_PREFIX . count($params); $params[$phName1] = $value1; + $phName2 = self::PARAM_PREFIX . count($params); $params[$phName2] = $value2; return "$column $operator $phName1 AND $phName2"; @@ -853,11 +913,6 @@ class QueryBuilder extends \yii\base\Object 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(); @@ -872,6 +927,11 @@ class QueryBuilder extends \yii\base\Object } $vss[] = '(' . implode(', ', $vs) . ')'; } + foreach ($columns as $i => $column) { + if (strpos($column, '(') === false) { + $columns[$i] = $this->db->quoteColumnName($column); + } + } return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; } diff --git a/yii/db/Schema.php b/framework/yii/db/Schema.php similarity index 80% rename from yii/db/Schema.php rename to framework/yii/db/Schema.php index 9538e4c..e32917f 100644 --- a/yii/db/Schema.php +++ b/framework/yii/db/Schema.php @@ -8,28 +8,34 @@ namespace yii\db; use Yii; +use yii\base\Object; use yii\base\NotSupportedException; use yii\base\InvalidCallException; use yii\caching\Cache; +use yii\caching\GroupDependency; /** * Schema is the base class for concrete DBMS-specific schema classes. * * Schema represents the database schema information that is DBMS specific. * - * @property QueryBuilder $queryBuilder the query builder for the DBMS represented by this schema - * @property array $tableNames the names of all tables in this database. - * @property array $tableSchemas the schema information for all tables in this database. + * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the + * sequence object. This property is read-only. + * @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only. + * @property string[] $tableNames All table names in the database. This property is read-only. + * @property TableSchema[] $tableSchemas The metadata for all tables in the database. Each array element is an + * instance of [[TableSchema]] or its child class. This property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -abstract class Schema extends \yii\base\Object +abstract class Schema extends Object { /** * The followings are the supported abstract column data types. */ const TYPE_PK = 'pk'; + const TYPE_BIGPK = 'bigpk'; const TYPE_STRING = 'string'; const TYPE_TEXT = 'text'; const TYPE_SMALLINT = 'smallint'; @@ -89,11 +95,11 @@ abstract class Schema extends \yii\base\Object /** @var $cache Cache */ $cache = is_string($db->schemaCache) ? Yii::$app->getComponent($db->schemaCache) : $db->schemaCache; if ($cache instanceof Cache) { - $key = $this->getCacheKey($cache, $name); + $key = $this->getCacheKey($name); if ($refresh || ($table = $cache->get($key)) === false) { $table = $this->loadTableSchema($realName); if ($table !== null) { - $cache->set($key, $table, $db->schemaCacheDuration); + $cache->set($key, $table, $db->schemaCacheDuration, new GroupDependency($this->getCacheGroup())); } } return $this->_tables[$name] = $table; @@ -104,18 +110,31 @@ abstract class Schema extends \yii\base\Object /** * Returns the cache key for the specified table name. - * @param Cache $cache the cache component * @param string $name the table name - * @return string the cache key + * @return mixed the cache key */ - public function getCacheKey($cache, $name) + protected function getCacheKey($name) { - return $cache->buildKey(array( + return array( __CLASS__, $this->db->dsn, $this->db->username, $name, - )); + ); + } + + /** + * Returns the cache group name. + * This allows [[refresh()]] to invalidate all cached table schemas. + * @return string the cache group name + */ + protected function getCacheGroup() + { + return md5(serialize(array( + __CLASS__, + $this->db->dsn, + $this->db->username, + ))); } /** @@ -123,7 +142,7 @@ abstract class Schema extends \yii\base\Object * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. * @param boolean $refresh whether to fetch the latest available table schemas. If this is false, * cached data may be returned if available. - * @return array the metadata for all tables in the database. + * @return TableSchema[] the metadata for all tables in the database. * Each array element is an instance of [[TableSchema]] or its child class. */ public function getTableSchemas($schema = '', $refresh = false) @@ -146,7 +165,7 @@ abstract class Schema extends \yii\base\Object * If not empty, the returned table names will be prefixed with the schema name. * @param boolean $refresh whether to fetch the latest available table names. If this is false, * table names fetched previously (if available) will be returned. - * @return array all table names in the database. + * @return string[] all table names in the database. */ public function getTableNames($schema = '', $refresh = false) { @@ -168,6 +187,26 @@ abstract class Schema extends \yii\base\Object } /** + * Determines the PDO type for the given PHP data value. + * @param mixed $data the data whose PDO type is to be determined + * @return integer the PDO type + * @see http://www.php.net/manual/en/pdo.constants.php + */ + public function getPdoType($data) + { + static $typeMap = array( + // php type => PDO type + 'boolean' => \PDO::PARAM_BOOL, + 'integer' => \PDO::PARAM_INT, + 'string' => \PDO::PARAM_STR, + 'resource' => \PDO::PARAM_LOB, + 'NULL' => \PDO::PARAM_NULL, + ); + $type = gettype($data); + return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; + } + + /** * Refreshes the schema. * This method cleans up all cached table schemas so that they can be re-created later * to reflect the database schema change. @@ -177,9 +216,7 @@ abstract class Schema extends \yii\base\Object /** @var $cache Cache */ $cache = is_string($this->db->schemaCache) ? Yii::$app->getComponent($this->db->schemaCache) : $this->db->schemaCache; if ($this->db->enableSchemaCache && $cache instanceof Cache) { - foreach ($this->_tables as $name => $table) { - $cache->delete($this->getCacheKey($cache, $name)); - } + GroupDependency::invalidate($cache, $this->getCacheGroup()); } $this->_tableNames = array(); $this->_tables = array(); @@ -200,7 +237,7 @@ abstract class Schema extends \yii\base\Object * This method should be overridden by child classes in order to support this feature * because the default implementation simply throws an exception. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO the schema name prefix. + * @return array all table names in the database. The names have NO schema name prefix. * @throws NotSupportedException if this method is called */ protected function findTableNames($schema = '') diff --git a/yii/db/StaleObjectException.php b/framework/yii/db/StaleObjectException.php similarity index 95% rename from yii/db/StaleObjectException.php rename to framework/yii/db/StaleObjectException.php index 0a04bd3..dc88ceb 100644 --- a/yii/db/StaleObjectException.php +++ b/framework/yii/db/StaleObjectException.php @@ -18,6 +18,6 @@ class StaleObjectException extends Exception */ public function getName() { - return \Yii::t('yii|Stale Object Exception'); + return \Yii::t('yii', 'Stale Object Exception'); } } diff --git a/yii/db/TableSchema.php b/framework/yii/db/TableSchema.php similarity index 93% rename from yii/db/TableSchema.php rename to framework/yii/db/TableSchema.php index 1065b51..910061d 100644 --- a/yii/db/TableSchema.php +++ b/framework/yii/db/TableSchema.php @@ -7,25 +7,20 @@ namespace yii\db; +use yii\base\Object; use yii\base\InvalidParamException; /** * TableSchema represents the metadata of a database table. * - * @property array $columnNames list of column names + * @property array $columnNames List of column names. This property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class TableSchema extends \yii\base\Object +class TableSchema extends Object { /** - * @var string name of the catalog (database) that this table belongs to. - * Defaults to null, meaning no catalog (or the current database). - * This property is only meaningful for MSSQL. - */ - public $catalogName; - /** * @var string name of the schema that this table belongs to. */ public $schemaName; diff --git a/yii/db/Transaction.php b/framework/yii/db/Transaction.php similarity index 97% rename from yii/db/Transaction.php rename to framework/yii/db/Transaction.php index 195a8c8..e0b90d9 100644 --- a/yii/db/Transaction.php +++ b/framework/yii/db/Transaction.php @@ -29,7 +29,8 @@ use yii\base\InvalidConfigException; * } * ~~~ * - * @property boolean $isActive Whether the transaction is active. This property is read-only. + * @property boolean $isActive Whether this transaction is active. Only an active transaction can [[commit()]] + * or [[rollback()]]. This property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 diff --git a/framework/yii/db/cubrid/QueryBuilder.php b/framework/yii/db/cubrid/QueryBuilder.php new file mode 100644 index 0000000..4b7ef43 --- /dev/null +++ b/framework/yii/db/cubrid/QueryBuilder.php @@ -0,0 +1,117 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\cubrid; + +use yii\base\InvalidParamException; + +/** + * QueryBuilder is the query builder for CUBRID databases (version 9.1.x and higher). + * + * @author Carsten Brandt <mail@cebe.cc> + * @since 2.0 + */ +class QueryBuilder extends \yii\db\QueryBuilder +{ + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = array( + Schema::TYPE_PK => 'int NOT NULL AUTO_INCREMENT PRIMARY KEY', + Schema::TYPE_BIGPK => 'bigint NOT NULL AUTO_INCREMENT PRIMARY KEY', + Schema::TYPE_STRING => 'varchar(255)', + Schema::TYPE_TEXT => 'varchar', + Schema::TYPE_SMALLINT => 'smallint', + Schema::TYPE_INTEGER => 'int', + Schema::TYPE_BIGINT => 'bigint', + Schema::TYPE_FLOAT => 'float(7)', + Schema::TYPE_DECIMAL => 'decimal(10,0)', + Schema::TYPE_DATETIME => 'datetime', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'blob', + Schema::TYPE_BOOLEAN => 'smallint', + Schema::TYPE_MONEY => 'decimal(19,4)', + ); + + /** + * Creates a SQL statement for resetting the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param string $tableName the name of the table whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @return string the SQL statement for resetting sequence + * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. + */ + public function resetSequence($tableName, $value = null) + { + $table = $this->db->getTableSchema($tableName); + if ($table !== null && $table->sequenceName !== null) { + $tableName = $this->db->quoteTableName($tableName); + if ($value === null) { + $key = reset($table->primaryKey); + $value = (int)$this->db->createCommand("SELECT MAX(`$key`) FROM " . $this->db->schema->quoteTableName($tableName))->queryScalar() + 1; + } else { + $value = (int)$value; + } + return "ALTER TABLE " . $this->db->schema->quoteTableName($tableName) . " AUTO_INCREMENT=$value;"; + } elseif ($table === null) { + throw new InvalidParamException("Table not found: $tableName"); + } else { + throw new InvalidParamException("There is not sequence associated with table '$tableName'."); + } + } + + /** + * Generates a batch INSERT SQL statement. + * For example, + * + * ~~~ + * $connection->createCommand()->batchInsert('tbl_user', array('name', 'age'), array( + * array('Tom', 30), + * array('Jane', 20), + * array('Linda', 25), + * ))->execute(); + * ~~~ + * + * Note that the values in each row must match the corresponding column names. + * + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column names + * @param array $rows the rows to be batch inserted into the table + * @return string the batch INSERT SQL statement + */ + public function batchInsert($table, $columns, $rows) + { + if (($tableSchema = $this->db->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = array(); + } + + foreach ($columns as $i => $name) { + $columns[$i] = $this->db->quoteColumnName($name); + } + + $values = array(); + foreach ($rows as $row) { + $vs = array(); + foreach ($row as $i => $value) { + if (!is_array($value) && isset($columnSchemas[$columns[$i]])) { + $value = $columnSchemas[$columns[$i]]->typecast($value); + } + $vs[] = is_string($value) ? $this->db->quoteValue($value) : $value; + } + $values[] = '(' . implode(', ', $vs) . ')'; + } + + return 'INSERT INTO ' . $this->db->quoteTableName($table) + . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values); + } +} diff --git a/framework/yii/db/cubrid/Schema.php b/framework/yii/db/cubrid/Schema.php new file mode 100644 index 0000000..a789694 --- /dev/null +++ b/framework/yii/db/cubrid/Schema.php @@ -0,0 +1,261 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\cubrid; + +use yii\db\Expression; +use yii\db\TableSchema; +use yii\db\ColumnSchema; + +/** + * Schema is the class for retrieving metadata from a CUBRID database (version 9.1.x and higher). + * + * @author Carsten Brandt <mail@cebe.cc> + * @since 2.0 + */ +class Schema extends \yii\db\Schema +{ + /** + * @var array mapping from physical column types (keys) to abstract column types (values) + * Please refer to [CUBRID manual](http://www.cubrid.org/manual/91/en/sql/datatype.html) for + * details on data types. + */ + public $typeMap = array( + // Numeric data types + 'short' => self::TYPE_SMALLINT, + 'smallint' => self::TYPE_SMALLINT, + 'int' => self::TYPE_INTEGER, + 'integer' => self::TYPE_INTEGER, + 'bigint' => self::TYPE_BIGINT, + 'numeric' => self::TYPE_DECIMAL, + 'decimal' => self::TYPE_DECIMAL, + 'float' => self::TYPE_FLOAT, + 'real' => self::TYPE_FLOAT, + 'double' => self::TYPE_FLOAT, + 'double precision' => self::TYPE_FLOAT, + 'monetary' => self::TYPE_MONEY, + // Date/Time data types + 'date' => self::TYPE_DATE, + 'time' => self::TYPE_TIME, + 'timestamp' => self::TYPE_TIMESTAMP, + 'datetime' => self::TYPE_DATETIME, + // String data types + 'char' => self::TYPE_STRING, + 'varchar' => self::TYPE_STRING, + 'char varying' => self::TYPE_STRING, + 'nchar' => self::TYPE_STRING, + 'nchar varying' => self::TYPE_STRING, + 'string' => self::TYPE_STRING, + // BLOB/CLOB data types + 'blob' => self::TYPE_BINARY, + 'clob' => self::TYPE_BINARY, + // Bit string data types + 'bit' => self::TYPE_STRING, + 'bit varying' => self::TYPE_STRING, + // Collection data types (considered strings for now) + 'set' => self::TYPE_STRING, + 'multiset' => self::TYPE_STRING, + 'list' => self::TYPE_STRING, + 'sequence' => self::TYPE_STRING, + 'enum' => self::TYPE_STRING, + ); + + /** + * Quotes a table name for use in a query. + * A simple table name has no schema prefix. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteSimpleTableName($name) + { + return strpos($name, '"') !== false ? $name : '"' . $name . '"'; + } + + /** + * Quotes a column name for use in a query. + * A simple column name has no prefix. + * @param string $name column name + * @return string the properly quoted column name + */ + public function quoteSimpleColumnName($name) + { + return strpos($name, '"') !== false || $name === '*' ? $name : '"' . $name . '"'; + } + + /** + * Quotes a string value for use in a query. + * Note that if the parameter is not a string, it will be returned without change. + * @param string $str string to be quoted + * @return string the properly quoted string + * @see http://www.php.net/manual/en/function.PDO-quote.php + */ + public function quoteValue($str) + { + if (!is_string($str)) { + return $str; + } + + $this->db->open(); + // workaround for broken PDO::quote() implementation in CUBRID 9.1.0 http://jira.cubrid.org/browse/APIS-658 + $version = $this->db->pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION); + if (version_compare($version, '8.4.4.0002', '<') || $version[0] == '9' && version_compare($version, '9.2.0.0002', '<=')) { + return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'"; + } else { + return $this->db->pdo->quote($str); + } + } + + /** + * Creates a query builder for the CUBRID database. + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema driver dependent table metadata. Null if the table does not exist. + */ + protected function loadTableSchema($name) + { + $this->db->open(); + $tableInfo = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_TABLE, $name); + + if (isset($tableInfo[0]['NAME'])) { + $table = new TableSchema(); + $table->name = $tableInfo[0]['NAME']; + + $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteSimpleTableName($table->name); + $columns = $this->db->createCommand($sql)->queryAll(); + + foreach ($columns as $info) { + $column = $this->loadColumnSchema($info); + $table->columns[$column->name] = $column; + if ($column->isPrimaryKey) { + $table->primaryKey[] = $column->name; + if ($column->autoIncrement) { + $table->sequenceName = ''; + } + } + } + + $foreignKeys = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_IMPORTED_KEYS, $table->name); + foreach($foreignKeys as $key) { + if (isset($table->foreignKeys[$key['FK_NAME']])) { + $table->foreignKeys[$key['FK_NAME']][$key['FKCOLUMN_NAME']] = $key['PKCOLUMN_NAME']; + } else { + $table->foreignKeys[$key['FK_NAME']] = array( + $key['PKTABLE_NAME'], + $key['FKCOLUMN_NAME'] => $key['PKCOLUMN_NAME'] + ); + } + } + $table->foreignKeys = array_values($table->foreignKeys); + + return $table; + } else { + return null; + } + } + + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema(); + + $column->name = $info['Field']; + $column->allowNull = $info['Null'] === 'YES'; + $column->isPrimaryKey = strpos($info['Key'], 'PRI') !== false; + $column->autoIncrement = stripos($info['Extra'], 'auto_increment') !== false; + + $column->dbType = strtolower($info['Type']); + $column->unsigned = strpos($column->dbType, 'unsigned') !== false; + + $column->type = self::TYPE_STRING; + if (preg_match('/^([\w ]+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { + $type = $matches[1]; + if (isset($this->typeMap[$type])) { + $column->type = $this->typeMap[$type]; + } + if (!empty($matches[2])) { + if ($type === 'enum') { + $values = explode(',', $matches[2]); + foreach ($values as $i => $value) { + $values[$i] = trim($value, "'"); + } + $column->enumValues = $values; + } else { + $values = explode(',', $matches[2]); + $column->size = $column->precision = (int)$values[0]; + if (isset($values[1])) { + $column->scale = (int)$values[1]; + } + } + } + } + + $column->phpType = $this->getColumnPhpType($column); + + if ($column->type === 'timestamp' && $info['Default'] === 'CURRENT_TIMESTAMP' || + $column->type === 'datetime' && $info['Default'] === 'SYS_DATETIME' || + $column->type === 'date' && $info['Default'] === 'SYS_DATE' || + $column->type === 'time' && $info['Default'] === 'SYS_TIME' + ) { + $column->defaultValue = new Expression($info['Default']); + } else { + $column->defaultValue = $column->typecast($info['Default']); + } + + return $column; + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @return array all table names in the database. The names have NO schema name prefix. + */ + protected function findTableNames($schema = '') + { + $this->db->open(); + $tables = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_TABLE); + $tableNames = array(); + foreach($tables as $table) { + // do not list system tables + if ($table['TYPE'] != 0) { + $tableNames[] = $table['NAME']; + } + } + return $tableNames; + } + + /** + * Determines the PDO type for the given PHP data value. + * @param mixed $data the data whose PDO type is to be determined + * @return integer the PDO type + * @see http://www.php.net/manual/en/pdo.constants.php + */ + public function getPdoType($data) + { + static $typeMap = array( + // php type => PDO type + 'boolean' => \PDO::PARAM_INT, // PARAM_BOOL is not supported by CUBRID PDO + 'integer' => \PDO::PARAM_INT, + 'string' => \PDO::PARAM_STR, + 'resource' => \PDO::PARAM_LOB, + 'NULL' => \PDO::PARAM_NULL, + ); + $type = gettype($data); + return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; + } +} diff --git a/framework/yii/db/mssql/PDO.php b/framework/yii/db/mssql/PDO.php new file mode 100644 index 0000000..c29cd4c --- /dev/null +++ b/framework/yii/db/mssql/PDO.php @@ -0,0 +1,61 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\mssql; + +/** + * This is an extension of the default PDO class of MSSQL and DBLIB drivers. + * It provides workarounds for improperly implemented functionalities of the MSSQL and DBLIB drivers. + * + * @author Timur Ruziev <resurtm@gmail.com> + * @since 2.0 + */ +class PDO extends \PDO +{ + /** + * Returns value of the last inserted ID. + * @param string|null $sequence the sequence name. Defaults to null. + * @return integer last inserted ID value. + */ + public function lastInsertId($sequence = null) + { + return $this->query('SELECT CAST(COALESCE(SCOPE_IDENTITY(), @@IDENTITY) AS bigint)')->fetchColumn(); + } + + /** + * Starts a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not + * natively support transactions. + * @return boolean the result of a transaction start. + */ + public function beginTransaction() + { + $this->exec('BEGIN TRANSACTION'); + return true; + } + + /** + * Commits a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not + * natively support transactions. + * @return boolean the result of a transaction commit. + */ + public function commit() + { + $this->exec('COMMIT TRANSACTION'); + return true; + } + + /** + * Rollbacks a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not + * natively support transactions. + * @return boolean the result of a transaction rollback. + */ + public function rollBack() + { + $this->exec('ROLLBACK TRANSACTION'); + return true; + } +} diff --git a/framework/yii/db/mssql/QueryBuilder.php b/framework/yii/db/mssql/QueryBuilder.php new file mode 100644 index 0000000..aeb5be8 --- /dev/null +++ b/framework/yii/db/mssql/QueryBuilder.php @@ -0,0 +1,82 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\mssql; + +use yii\base\InvalidParamException; + +/** + * QueryBuilder is the query builder for MS SQL Server databases (version 2008 and above). + * + * @author Timur Ruziev <resurtm@gmail.com> + * @since 2.0 + */ +class QueryBuilder extends \yii\db\QueryBuilder +{ + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = array( + Schema::TYPE_PK => 'int IDENTITY PRIMARY KEY', + Schema::TYPE_BIGPK => 'bigint IDENTITY PRIMARY KEY', + Schema::TYPE_STRING => 'varchar(255)', + Schema::TYPE_TEXT => 'text', + Schema::TYPE_SMALLINT => 'smallint(6)', + Schema::TYPE_INTEGER => 'int(11)', + Schema::TYPE_BIGINT => 'bigint(20)', + Schema::TYPE_FLOAT => 'float', + Schema::TYPE_DECIMAL => 'decimal(10,0)', + Schema::TYPE_DATETIME => 'datetime', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'binary', + Schema::TYPE_BOOLEAN => 'tinyint(1)', + Schema::TYPE_MONEY => 'decimal(19,4)', + ); + +// public function update($table, $columns, $condition, &$params) +// { +// return ''; +// } + +// public function delete($table, $condition, &$params) +// { +// return ''; +// } + +// public function buildLimit($limit, $offset) +// { +// return ''; +// } + +// public function resetSequence($table, $value = null) +// { +// return ''; +// } + + /** + * Builds a SQL statement for enabling or disabling integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @param string $table the table name. Defaults to empty string, meaning that no table will be changed. + * @return string the SQL statement for checking integrity + * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. + */ + public function checkIntegrity($check = true, $schema = '', $table = '') + { + if ($schema !== '') { + $table = "{$schema}.{$table}"; + } + $table = $this->db->quoteTableName($table); + if ($this->db->getTableSchema($table) === null) { + throw new InvalidParamException("Table not found: $table"); + } + $enable = $check ? 'CHECK' : 'NOCHECK'; + return "ALTER TABLE {$table} {$enable} CONSTRAINT ALL"; + } +} diff --git a/framework/yii/db/mssql/Schema.php b/framework/yii/db/mssql/Schema.php new file mode 100644 index 0000000..9def3b4 --- /dev/null +++ b/framework/yii/db/mssql/Schema.php @@ -0,0 +1,357 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\mssql; + +use yii\db\ColumnSchema; + +/** + * Schema is the class for retrieving metadata from a MS SQL Server databases (version 2008 and above). + * + * @author Timur Ruziev <resurtm@gmail.com> + * @since 2.0 + */ +class Schema extends \yii\db\Schema +{ + /** + * Default schema name to be used. + */ + const DEFAULT_SCHEMA = 'dbo'; + + /** + * @var array mapping from physical column types (keys) to abstract column types (values) + */ + public $typeMap = array( + // exact numerics + 'bigint' => self::TYPE_BIGINT, + 'numeric' => self::TYPE_DECIMAL, + 'bit' => self::TYPE_SMALLINT, + 'smallint' => self::TYPE_SMALLINT, + 'decimal' => self::TYPE_DECIMAL, + 'smallmoney' => self::TYPE_MONEY, + 'int' => self::TYPE_INTEGER, + 'tinyint' => self::TYPE_SMALLINT, + 'money' => self::TYPE_MONEY, + + // approximate numerics + 'float' => self::TYPE_FLOAT, + 'real' => self::TYPE_FLOAT, + + // date and time + 'date' => self::TYPE_DATE, + 'datetimeoffset' => self::TYPE_DATETIME, + 'datetime2' => self::TYPE_DATETIME, + 'smalldatetime' => self::TYPE_DATETIME, + 'datetime' => self::TYPE_DATETIME, + 'time' => self::TYPE_TIME, + + // character strings + 'char' => self::TYPE_STRING, + 'varchar' => self::TYPE_STRING, + 'text' => self::TYPE_TEXT, + + // unicode character strings + 'nchar' => self::TYPE_STRING, + 'nvarchar' => self::TYPE_STRING, + 'ntext' => self::TYPE_TEXT, + + // binary strings + 'binary' => self::TYPE_BINARY, + 'varbinary' => self::TYPE_BINARY, + 'image' => self::TYPE_BINARY, + + // other data types + // 'cursor' type cannot be used with tables + 'timestamp' => self::TYPE_TIMESTAMP, + 'hierarchyid' => self::TYPE_STRING, + 'uniqueidentifier' => self::TYPE_STRING, + 'sql_variant' => self::TYPE_STRING, + 'xml' => self::TYPE_STRING, + 'table' => self::TYPE_STRING, + ); + + /** + * Quotes a table name for use in a query. + * A simple table name has no schema prefix. + * @param string $name table name. + * @return string the properly quoted table name. + */ + public function quoteSimpleTableName($name) + { + return strpos($name, '[') === false ? "[{$name}]" : $name; + } + + /** + * Quotes a column name for use in a query. + * A simple column name has no prefix. + * @param string $name column name. + * @return string the properly quoted column name. + */ + public function quoteSimpleColumnName($name) + { + return strpos($name, '[') === false && $name !== '*' ? "[{$name}]" : $name; + } + + /** + * Creates a query builder for the MSSQL database. + * @return QueryBuilder query builder interface. + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema|null driver dependent table metadata. Null if the table does not exist. + */ + public function loadTableSchema($name) + { + $table = new TableSchema(); + $this->resolveTableNames($table, $name); + $this->findPrimaryKeys($table); + if ($this->findColumns($table)) { + $this->findForeignKeys($table); + return $table; + } + } + + /** + * Resolves the table name and schema name (if any). + * @param TableSchema $table the table metadata object + * @param string $name the table name + */ + protected function resolveTableNames($table, $name) + { + $parts = explode('.', str_replace(array('[', ']'), '', $name)); + $partCount = count($parts); + if ($partCount == 3) { + // catalog name, schema name and table name passed + $table->catalogName = $parts[0]; + $table->schemaName = $parts[1]; + $table->name = $parts[2]; + } elseif ($partCount == 2) { + // only schema name and table name passed + $table->schemaName = $parts[0]; + $table->name = $parts[1]; + } else { + // only schema name passed + $table->schemaName = static::DEFAULT_SCHEMA; + $table->name = $parts[0]; + } + } + + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema(); + + $column->name = $info['column_name']; + $column->allowNull = $info['is_nullable'] == 'YES'; + $column->dbType = $info['data_type']; + $column->enumValues = array(); // mssql has only vague equivalents to enum + $column->isPrimaryKey = null; // primary key will be determined in findColumns() method + $column->autoIncrement = $info['is_identity'] == 1; + $column->unsigned = stripos($column->dbType, 'unsigned') !== false; + $column->comment = $info['comment'] === null ? '' : $info['comment']; + + $column->type = self::TYPE_STRING; + if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { + $type = $matches[1]; + if (isset($this->typeMap[$type])) { + $column->type = $this->typeMap[$type]; + } + if (!empty($matches[2])) { + $values = explode(',', $matches[2]); + $column->size = $column->precision = (int)$values[0]; + if (isset($values[1])) { + $column->scale = (int)$values[1]; + } + if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) { + $column->type = 'boolean'; + } elseif ($type === 'bit') { + if ($column->size > 32) { + $column->type = 'bigint'; + } elseif ($column->size === 32) { + $column->type = 'integer'; + } + } + } + } + + $column->phpType = $this->getColumnPhpType($column); + + if ($info['column_default'] == '(NULL)') { + $info['column_default'] = null; + } + if ($column->type !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP') { + $column->defaultValue = $column->typecast($info['column_default']); + } + + return $column; + } + + /** + * Collects the metadata of table columns. + * @param TableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $columnsTableName = 'information_schema.columns'; + $whereSql = "[t1].[table_name] = '{$table->name}'"; + if ($table->catalogName !== null) { + $columnsTableName = "{$table->catalogName}.{$columnsTableName}"; + $whereSql .= " AND [t1].[table_catalog] = '{$table->catalogName}'"; + } + if ($table->schemaName !== null) { + $whereSql .= " AND [t1].[table_schema] = '{$table->schemaName}'"; + } + $columnsTableName = $this->quoteTableName($columnsTableName); + + $sql = <<<SQL +SELECT + [t1].[column_name], [t1].[is_nullable], [t1].[data_type], [t1].[column_default], + COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsIdentity') AS is_identity, + CONVERT(VARCHAR, [t2].[value]) AS comment +FROM {$columnsTableName} AS [t1] +LEFT OUTER JOIN [sys].[extended_properties] AS [t2] ON + [t1].[ordinal_position] = [t2].[minor_id] AND + OBJECT_NAME([t2].[major_id]) = [t1].[table_name] AND + [t2].[class] = 1 AND + [t2].[class_desc] = 'OBJECT_OR_COLUMN' AND + [t2].[name] = 'MS_Description' +WHERE {$whereSql} +SQL; + + try { + $columns = $this->db->createCommand($sql)->queryAll(); + } catch (\Exception $e) { + return false; + } + foreach ($columns as $column) { + $column = $this->loadColumnSchema($column); + foreach ($table->primaryKey as $primaryKey) { + if (strcasecmp($column->name, $primaryKey) === 0) { + $column->isPrimaryKey = true; + break; + } + } + if ($column->isPrimaryKey && $column->autoIncrement) { + $table->sequenceName = ''; + } + $table->columns[$column->name] = $column; + } + return true; + } + + /** + * Collects the primary key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findPrimaryKeys($table) + { + $keyColumnUsageTableName = 'information_schema.key_column_usage'; + $tableConstraintsTableName = 'information_schema.table_constraints'; + if ($table->catalogName !== null) { + $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; + $tableConstraintsTableName = $table->catalogName . '.' . $tableConstraintsTableName; + } + $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); + $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName); + + $sql = <<<SQL +SELECT + [kcu].[column_name] AS [field_name] +FROM {$keyColumnUsageTableName} AS [kcu] +LEFT JOIN {$tableConstraintsTableName} AS [tc] ON + [kcu].[table_name] = [tc].[table_name] AND + [kcu].[constraint_name] = [tc].[constraint_name] +WHERE + [tc].[constraint_type] = 'PRIMARY KEY' AND + [kcu].[table_name] = :tableName AND + [kcu].[table_schema] = :schemaName +SQL; + + $table->primaryKey = $this->db + ->createCommand($sql, array(':tableName' => $table->name, ':schemaName' => $table->schemaName)) + ->queryColumn(); + } + + /** + * Collects the foreign key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findForeignKeys($table) + { + $referentialConstraintsTableName = 'information_schema.referential_constraints'; + $keyColumnUsageTableName = 'information_schema.key_column_usage'; + if ($table->catalogName !== null) { + $referentialConstraintsTableName = $table->catalogName . '.' . $referentialConstraintsTableName; + $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; + } + $referentialConstraintsTableName = $this->quoteTableName($referentialConstraintsTableName); + $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); + + // please refer to the following page for more details: + // http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx + $sql = <<<SQL +SELECT + [kcu1].[column_name] AS [fk_column_name], + [kcu2].[table_name] AS [uq_table_name], + [kcu2].[column_name] AS [uq_column_name] +FROM {$referentialConstraintsTableName} AS [rc] +JOIN {$keyColumnUsageTableName} AS [kcu1] ON + [kcu1].[constraint_catalog] = [rc].[constraint_catalog] AND + [kcu1].[constraint_schema] = [rc].[constraint_schema] AND + [kcu1].[constraint_name] = [rc].[constraint_name] +JOIN {$keyColumnUsageTableName} AS [kcu2] ON + [kcu2].[constraint_catalog] = [rc].[constraint_catalog] AND + [kcu2].[constraint_schema] = [rc].[constraint_schema] AND + [kcu2].[constraint_name] = [rc].[constraint_name] AND + [kcu2].[ordinal_position] = [kcu1].[ordinal_position] +WHERE [kcu1].[table_name] = :tableName +SQL; + + $rows = $this->db->createCommand($sql, array(':tableName' => $table->name))->queryAll(); + $table->foreignKeys = array(); + foreach ($rows as $row) { + $table->foreignKeys[] = array($row['uq_table_name'], $row['fk_column_name'] => $row['uq_column_name']); + } + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @return array all table names in the database. The names have NO schema name prefix. + */ + protected function findTableNames($schema = '') + { + if ($schema === '') { + $schema = static::DEFAULT_SCHEMA; + } + + $sql = <<<SQL +SELECT [t].[table] +FROM [information_schema].[tables] AS [t] +WHERE [t].[table_schema] = :schema AND [t].[table_type] = 'BASE TABLE' +SQL; + + $names = $this->db->createCommand($sql, array(':schema' => $schema))->queryColumn(); + if ($schema !== static::DEFAULT_SCHEMA) { + foreach ($names as $index => $name) { + $names[$index] = $schema . '.' . $name; + } + } + return $names; + } +} diff --git a/framework/yii/db/mssql/SqlsrvPDO.php b/framework/yii/db/mssql/SqlsrvPDO.php new file mode 100644 index 0000000..29444c5 --- /dev/null +++ b/framework/yii/db/mssql/SqlsrvPDO.php @@ -0,0 +1,33 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\mssql; + +/** + * This is an extension of the default PDO class of SQLSRV driver. + * It provides workarounds for improperly implemented functionalities of the SQLSRV driver. + * + * @author Timur Ruziev <resurtm@gmail.com> + * @since 2.0 + */ +class SqlsrvPDO extends \PDO +{ + /** + * Returns value of the last inserted ID. + * + * SQLSRV driver implements [[PDO::lastInsertId()]] method but with a single peculiarity: + * when `$sequence` value is a null or an empty string it returns an empty string. + * But when parameter is not specified it works as expected and returns actual + * last inserted ID (like the other PDO drivers). + * @param string|null $sequence the sequence name. Defaults to null. + * @return integer last inserted ID value. + */ + public function lastInsertId($sequence = null) + { + return !$sequence ? parent::lastInsertId() : parent::lastInsertId($sequence); + } +} diff --git a/framework/yii/db/mssql/TableSchema.php b/framework/yii/db/mssql/TableSchema.php new file mode 100644 index 0000000..67ad85c --- /dev/null +++ b/framework/yii/db/mssql/TableSchema.php @@ -0,0 +1,23 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\mssql; + +/** + * TableSchema represents the metadata of a database table. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class TableSchema extends \yii\db\TableSchema +{ + /** + * @var string name of the catalog (database) that this table belongs to. + * Defaults to null, meaning no catalog (or the current database). + */ + public $catalogName; +} diff --git a/yii/db/mysql/QueryBuilder.php b/framework/yii/db/mysql/QueryBuilder.php similarity index 84% rename from yii/db/mysql/QueryBuilder.php rename to framework/yii/db/mysql/QueryBuilder.php index a078b9a..386de2f 100644 --- a/yii/db/mysql/QueryBuilder.php +++ b/framework/yii/db/mysql/QueryBuilder.php @@ -23,13 +23,14 @@ class QueryBuilder extends \yii\db\QueryBuilder */ public $typeMap = array( Schema::TYPE_PK => 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY', + Schema::TYPE_BIGPK => 'bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY', Schema::TYPE_STRING => 'varchar(255)', Schema::TYPE_TEXT => 'text', Schema::TYPE_SMALLINT => 'smallint(6)', Schema::TYPE_INTEGER => 'int(11)', Schema::TYPE_BIGINT => 'bigint(20)', Schema::TYPE_FLOAT => 'float', - Schema::TYPE_DECIMAL => 'decimal', + Schema::TYPE_DECIMAL => 'decimal(10,0)', Schema::TYPE_DATETIME => 'datetime', Schema::TYPE_TIMESTAMP => 'timestamp', Schema::TYPE_TIME => 'time', @@ -50,7 +51,7 @@ class QueryBuilder extends \yii\db\QueryBuilder public function renameColumn($table, $oldName, $newName) { $quotedTable = $this->db->quoteTableName($table); - $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryRow(); + $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryOne(); if ($row === false) { throw new Exception("Unable to find column '$oldName' in table '$table'."); } @@ -89,6 +90,17 @@ class QueryBuilder extends \yii\db\QueryBuilder } /** + * Builds a SQL statement for removing a primary key constraint to an existing table. + * @param string $name the name of the primary key constraint to be removed. + * @param string $table the table that the primary key constraint will be removed from. + * @return string the SQL statement for removing a primary key constraint from an existing table. + */ + public function dropPrimaryKey($name, $table) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' DROP PRIMARY KEY'; + } + + /** * Creates a SQL statement for resetting the sequence value of a table's primary key. * The sequence will be reset such that the primary key of the next new row inserted * will have the specified value or 1. @@ -113,19 +125,20 @@ class QueryBuilder extends \yii\db\QueryBuilder } elseif ($table === null) { throw new InvalidParamException("Table not found: $tableName"); } else { - throw new InvalidParamException("There is not sequence associated with table '$tableName'.'"); + throw new InvalidParamException("There is not sequence associated with table '$tableName'."); } } /** * Builds a SQL statement for enabling or disabling integrity check. * @param boolean $check whether to turn on or off the integrity check. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @param string $table the table name. Meaningless for MySQL. + * @param string $schema the schema of the tables. Meaningless for MySQL. * @return string the SQL statement for checking integrity */ - public function checkIntegrity($check = true, $schema = '') + public function checkIntegrity($check = true, $schema = '', $table = '') { - return 'SET FOREIGN_KEY_CHECKS=' . ($check ? 1 : 0); + return 'SET FOREIGN_KEY_CHECKS = ' . ($check ? 1 : 0); } /** @@ -140,7 +153,7 @@ class QueryBuilder extends \yii\db\QueryBuilder * ))->execute(); * ~~~ * - * Not that the values in each row must match the corresponding column names. + * Note that the values in each row must match the corresponding column names. * * @param string $table the table that new rows will be inserted into. * @param array $columns the column names @@ -149,17 +162,29 @@ class QueryBuilder extends \yii\db\QueryBuilder */ public function batchInsert($table, $columns, $rows) { + if (($tableSchema = $this->db->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = array(); + } + + foreach ($columns as $i => $name) { + $columns[$i] = $this->db->quoteColumnName($name); + } + $values = array(); foreach ($rows as $row) { $vs = array(); - foreach ($row as $value) { + foreach ($row as $i => $value) { + if (!is_array($value) && isset($columnSchemas[$columns[$i]])) { + $value = $columnSchemas[$columns[$i]]->typecast($value); + } $vs[] = is_string($value) ? $this->db->quoteValue($value) : $value; } - $values[] = $vs; + $values[] = '(' . implode(', ', $vs) . ')'; } return 'INSERT INTO ' . $this->db->quoteTableName($table) - . ' (' . implode(', ', $columns) . ') VALUES (' - . implode(', ', $values) . ')'; + . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values); } } diff --git a/yii/db/mysql/Schema.php b/framework/yii/db/mysql/Schema.php similarity index 96% rename from yii/db/mysql/Schema.php rename to framework/yii/db/mysql/Schema.php index 501149a..998f49a 100644 --- a/yii/db/mysql/Schema.php +++ b/framework/yii/db/mysql/Schema.php @@ -178,6 +178,7 @@ class Schema extends \yii\db\Schema * Collects the metadata of table columns. * @param TableSchema $table the table metadata * @return boolean whether the table exists in the database + * @throws \Exception if DB query fails */ protected function findColumns($table) { @@ -185,7 +186,12 @@ class Schema extends \yii\db\Schema try { $columns = $this->db->createCommand($sql)->queryAll(); } catch (\Exception $e) { - return false; + $previous = $e->getPrevious(); + if ($previous instanceof \PDOException && $previous->getCode() == '42S02') { + // table does not exist + return false; + } + throw $e; } foreach ($columns as $info) { $column = $this->loadColumnSchema($info); @@ -206,7 +212,7 @@ class Schema extends \yii\db\Schema */ protected function findConstraints($table) { - $row = $this->db->createCommand('SHOW CREATE TABLE ' . $this->quoteSimpleTableName($table->name))->queryRow(); + $row = $this->db->createCommand('SHOW CREATE TABLE ' . $this->quoteSimpleTableName($table->name))->queryOne(); if (isset($row['Create Table'])) { $sql = $row['Create Table']; } else { @@ -230,10 +236,8 @@ class Schema extends \yii\db\Schema /** * Returns all table names in the database. - * This method should be overridden by child classes in order to support this feature - * because the default implementation simply throws an exception. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO the schema name prefix. + * @return array all table names in the database. The names have NO schema name prefix. */ protected function findTableNames($schema = '') { diff --git a/framework/yii/db/pgsql/QueryBuilder.php b/framework/yii/db/pgsql/QueryBuilder.php new file mode 100644 index 0000000..667fa43 --- /dev/null +++ b/framework/yii/db/pgsql/QueryBuilder.php @@ -0,0 +1,80 @@ +<?php + +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\pgsql; + +/** + * QueryBuilder is the query builder for PostgreSQL databases. + * + * @author Gevik Babakhani <gevikb@gmail.com> + * @since 2.0 + */ +class QueryBuilder extends \yii\db\QueryBuilder +{ + + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = array( + Schema::TYPE_PK => 'serial NOT NULL PRIMARY KEY', + Schema::TYPE_BIGPK => 'bigserial NOT NULL PRIMARY KEY', + Schema::TYPE_STRING => 'varchar(255)', + Schema::TYPE_TEXT => 'text', + Schema::TYPE_SMALLINT => 'smallint', + Schema::TYPE_INTEGER => 'integer', + Schema::TYPE_BIGINT => 'bigint', + Schema::TYPE_FLOAT => 'double precision', + Schema::TYPE_DECIMAL => 'numeric(10,0)', + Schema::TYPE_DATETIME => 'timestamp', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'bytea', + Schema::TYPE_BOOLEAN => 'boolean', + Schema::TYPE_MONEY => 'numeric(19,4)', + ); + + /** + * Builds a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping an index. + */ + public function dropIndex($name, $table) + { + return 'DROP INDEX ' . $this->db->quoteTableName($name); + } + + /** + * Builds a SQL statement for renaming a DB table. + * @param string $oldName the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB table. + */ + public function renameTable($oldName, $newName) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($oldName) . ' RENAME TO ' . $this->db->quoteTableName($newName); + } + + /** + * Builds a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract + * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept + * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' + * will become 'varchar(255) not null'. + * @return string the SQL statement for changing the definition of a column. + */ + public function alterColumn($table, $column, $type) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN ' + . $this->db->quoteColumnName($column) . ' TYPE ' + . $this->getColumnType($type); + } +} diff --git a/framework/yii/db/pgsql/Schema.php b/framework/yii/db/pgsql/Schema.php new file mode 100644 index 0000000..d131342 --- /dev/null +++ b/framework/yii/db/pgsql/Schema.php @@ -0,0 +1,320 @@ +<?php + +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\pgsql; + +use yii\db\TableSchema; +use yii\db\ColumnSchema; + +/** + * Schema is the class for retrieving metadata from a PostgreSQL database + * (version 9.x and above). + * + * @author Gevik Babakhani <gevikb@gmail.com> + * @since 2.0 + */ +class Schema extends \yii\db\Schema +{ + + /** + * The default schema used for the current session. + * @var string + */ + public $defaultSchema = 'public'; + + /** + * @var array mapping from physical column types (keys) to abstract + * column types (values) + */ + public $typeMap = array( + 'abstime' => self::TYPE_TIMESTAMP, + 'bit' => self::TYPE_STRING, + 'boolean' => self::TYPE_BOOLEAN, + 'box' => self::TYPE_STRING, + 'character' => self::TYPE_STRING, + 'bytea' => self::TYPE_BINARY, + 'char' => self::TYPE_STRING, + 'cidr' => self::TYPE_STRING, + 'circle' => self::TYPE_STRING, + 'date' => self::TYPE_DATE, + 'real' => self::TYPE_FLOAT, + 'decimal' => self::TYPE_DECIMAL, + 'double precision' => self::TYPE_DECIMAL, + 'inet' => self::TYPE_STRING, + 'smallint' => self::TYPE_SMALLINT, + 'integer' => self::TYPE_INTEGER, + 'bigint' => self::TYPE_BIGINT, + 'interval' => self::TYPE_STRING, + 'json' => self::TYPE_STRING, + 'line' => self::TYPE_STRING, + 'macaddr' => self::TYPE_STRING, + 'money' => self::TYPE_MONEY, + 'name' => self::TYPE_STRING, + 'numeric' => self::TYPE_STRING, + 'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal! + 'path' => self::TYPE_STRING, + 'point' => self::TYPE_STRING, + 'polygon' => self::TYPE_STRING, + 'text' => self::TYPE_TEXT, + 'time without time zone' => self::TYPE_TIME, + 'timestamp without time zone' => self::TYPE_TIMESTAMP, + 'timestamp with time zone' => self::TYPE_TIMESTAMP, + 'time with time zone' => self::TYPE_TIMESTAMP, + 'unknown' => self::TYPE_STRING, + 'uuid' => self::TYPE_STRING, + 'bit varying' => self::TYPE_STRING, + 'character varying' => self::TYPE_STRING, + 'xml' => self::TYPE_STRING + ); + + /** + * Creates a query builder for the PostgreSQL database. + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Resolves the table name and schema name (if any). + * @param TableSchema $table the table metadata object + * @param string $name the table name + */ + protected function resolveTableNames($table, $name) + { + $parts = explode('.', str_replace('"', '', $name)); + if (isset($parts[1])) { + $table->schemaName = $parts[0]; + $table->name = $parts[1]; + } else { + $table->name = $parts[0]; + } + if ($table->schemaName === null) { + $table->schemaName = $this->defaultSchema; + } + } + + /** + * Quotes a table name for use in a query. + * A simple table name has no schema prefix. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteSimpleTableName($name) + { + return strpos($name, '"') !== false ? $name : '"' . $name . '"'; + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema|null driver dependent table metadata. Null if the table does not exist. + */ + public function loadTableSchema($name) + { + $table = new TableSchema(); + $this->resolveTableNames($table, $name); + if ($this->findColumns($table)) { + $this->findConstraints($table); + return $table; + } else { + return null; + } + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * If not empty, the returned table names will be prefixed with the schema name. + * @return array all table names in the database. + */ + protected function findTableNames($schema = '') + { + if ($schema === '') { + $schema = $this->defaultSchema; + } + $sql = <<<EOD +SELECT table_name, table_schema FROM information_schema.tables +WHERE table_schema=:schema AND table_type='BASE TABLE' +EOD; + $command = $this->db->createCommand($sql); + $command->bindParam(':schema', $schema); + $rows = $command->queryAll(); + $names = array(); + foreach ($rows as $row) { + if ($schema === $this->defaultSchema) { + $names[] = $row['table_name']; + } else { + $names[] = $row['table_schema'] . '.' . $row['table_name']; + } + } + return $names; + } + + /** + * Collects the foreign key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findConstraints($table) + { + + $tableName = $this->quoteValue($table->name); + $tableSchema = $this->quoteValue($table->schemaName); + + //We need to extract the constraints de hard way since: + //http://www.postgresql.org/message-id/26677.1086673982@sss.pgh.pa.us + + $sql = <<<SQL +select + (select string_agg(attname,',') attname from pg_attribute where attrelid=ct.conrelid and attnum = any(ct.conkey)) as columns, + fc.relname as foreign_table_name, + fns.nspname as foreign_table_schema, + (select string_agg(attname,',') attname from pg_attribute where attrelid=ct.confrelid and attnum = any(ct.confkey)) as foreign_columns +from + pg_constraint ct + inner join pg_class c on c.oid=ct.conrelid + inner join pg_namespace ns on c.relnamespace=ns.oid + left join pg_class fc on fc.oid=ct.confrelid + left join pg_namespace fns on fc.relnamespace=fns.oid + +where + ct.contype='f' + and c.relname={$tableName} + and ns.nspname={$tableSchema} +SQL; + + $constraints = $this->db->createCommand($sql)->queryAll(); + foreach ($constraints as $constraint) { + $columns = explode(',', $constraint['columns']); + $fcolumns = explode(',', $constraint['foreign_columns']); + if ($constraint['foreign_table_schema'] !== $this->defaultSchema) { + $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name']; + } else { + $foreignTable = $constraint['foreign_table_name']; + } + $citem = array($foreignTable); + foreach ($columns as $idx => $column) { + $citem[$fcolumns[$idx]] = $column; + } + $table->foreignKeys[] = $citem; + } + } + + /** + * Collects the metadata of table columns. + * @param TableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $tableName = $this->db->quoteValue($table->name); + $schemaName = $this->db->quoteValue($table->schemaName); + $sql = <<<SQL +SELECT + current_database() as table_catalog, + d.nspname AS table_schema, + c.relname AS table_name, + a.attname AS column_name, + t.typname AS data_type, + a.attlen AS character_maximum_length, + pg_catalog.col_description(c.oid, a.attnum) AS column_comment, + a.atttypmod AS modifier, + a.attnotnull = false AS is_nullable, + CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default, + coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) AS is_autoinc, + array_to_string((select array_agg(enumlabel) from pg_enum where enumtypid=a.atttypid)::varchar[],',') as enum_values, + CASE atttypid + WHEN 21 /*int2*/ THEN 16 + WHEN 23 /*int4*/ THEN 32 + WHEN 20 /*int8*/ THEN 64 + WHEN 1700 /*numeric*/ THEN + CASE WHEN atttypmod = -1 + THEN null + ELSE ((atttypmod - 4) >> 16) & 65535 + END + WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/ + WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/ + ELSE null + END AS numeric_precision, + CASE + WHEN atttypid IN (21, 23, 20) THEN 0 + WHEN atttypid IN (1700) THEN + CASE + WHEN atttypmod = -1 THEN null + ELSE (atttypmod - 4) & 65535 + END + ELSE null + END AS numeric_scale, + CAST( + information_schema._pg_char_max_length(information_schema._pg_truetypid(a, t), information_schema._pg_truetypmod(a, t)) + AS numeric + ) AS size, + a.attnum = any (ct.conkey) as is_pkey +FROM + pg_class c + LEFT JOIN pg_attribute a ON a.attrelid = c.oid + LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum + LEFT JOIN pg_type t ON a.atttypid = t.oid + LEFT JOIN pg_namespace d ON d.oid = c.relnamespace + LEFT join pg_constraint ct on ct.conrelid=c.oid and ct.contype='p' +WHERE + a.attnum > 0 + and c.relname = {$tableName} + and d.nspname = {$schemaName} +ORDER BY + a.attnum; +SQL; + + $columns = $this->db->createCommand($sql)->queryAll(); + if (empty($columns)) { + return false; + } + foreach ($columns as $column) { + $column = $this->loadColumnSchema($column); + $table->columns[$column->name] = $column; + if ($column->isPrimaryKey === true) { + $table->primaryKey[] = $column->name; + if ($table->sequenceName === null && preg_match("/nextval\('\w+'(::regclass)?\)/", $column->defaultValue) === 1) { + $table->sequenceName = preg_replace(array('/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'), '', $column->defaultValue); + } + } + } + return true; + } + + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema(); + $column->allowNull = $info['is_nullable']; + $column->autoIncrement = $info['is_autoinc']; + $column->comment = $info['column_comment']; + $column->dbType = $info['data_type']; + $column->defaultValue = $info['column_default']; + $column->enumValues = explode(',', str_replace(array("''"), array("'"), $info['enum_values'])); + $column->unsigned = false; // has no meanining in PG + $column->isPrimaryKey = $info['is_pkey']; + $column->name = $info['column_name']; + $column->precision = $info['numeric_precision']; + $column->scale = $info['numeric_scale']; + $column->size = $info['size']; + + if (isset($this->typeMap[$column->dbType])) { + $column->type = $this->typeMap[$column->dbType]; + } else { + $column->type = self::TYPE_STRING; + } + $column->phpType = $this->getColumnPhpType($column); + return $column; + } +} diff --git a/yii/db/sqlite/QueryBuilder.php b/framework/yii/db/sqlite/QueryBuilder.php similarity index 85% rename from yii/db/sqlite/QueryBuilder.php rename to framework/yii/db/sqlite/QueryBuilder.php index 3aa89e7..4e210f8 100644 --- a/yii/db/sqlite/QueryBuilder.php +++ b/framework/yii/db/sqlite/QueryBuilder.php @@ -24,19 +24,20 @@ class QueryBuilder extends \yii\db\QueryBuilder */ public $typeMap = array( Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', + Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', Schema::TYPE_STRING => 'varchar(255)', Schema::TYPE_TEXT => 'text', Schema::TYPE_SMALLINT => 'smallint', Schema::TYPE_INTEGER => 'integer', Schema::TYPE_BIGINT => 'bigint', Schema::TYPE_FLOAT => 'float', - Schema::TYPE_DECIMAL => 'decimal', + Schema::TYPE_DECIMAL => 'decimal(10,0)', Schema::TYPE_DATETIME => 'datetime', Schema::TYPE_TIMESTAMP => 'timestamp', Schema::TYPE_TIME => 'time', Schema::TYPE_DATE => 'date', Schema::TYPE_BINARY => 'blob', - Schema::TYPE_BOOLEAN => 'tinyint(1)', + Schema::TYPE_BOOLEAN => 'boolean', Schema::TYPE_MONEY => 'decimal(19,4)', ); @@ -77,10 +78,11 @@ class QueryBuilder extends \yii\db\QueryBuilder /** * Enables or disables integrity check. * @param boolean $check whether to turn on or off the integrity check. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @param string $schema the schema of the tables. Meaningless for SQLite. + * @param string $table the table name. Meaningless for SQLite. * @throws NotSupportedException this is not supported by SQLite */ - public function checkIntegrity($check = true, $schema = '') + public function checkIntegrity($check = true, $schema = '', $table = '') { throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); } @@ -178,4 +180,29 @@ class QueryBuilder extends \yii\db\QueryBuilder { throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); } + + /** + * Builds a SQL statement for adding a primary key constraint to an existing table. + * @param string $name the name of the primary key constraint. + * @param string $table the table that the primary key constraint will be added to. + * @param string|array $columns comma separated string or array of columns that the primary key will consist of. + * @return string the SQL statement for adding a primary key constraint to an existing table. + * @throws NotSupportedException this is not supported by SQLite + */ + public function addPrimaryKey($name, $table, $columns) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } + + /** + * Builds a SQL statement for removing a primary key constraint to an existing table. + * @param string $name the name of the primary key constraint to be removed. + * @param string $table the table that the primary key constraint will be removed from. + * @return string the SQL statement for removing a primary key constraint from an existing table. + * @throws NotSupportedException this is not supported by SQLite * + */ + public function dropPrimaryKey($name, $table) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } } diff --git a/yii/db/sqlite/Schema.php b/framework/yii/db/sqlite/Schema.php similarity index 95% rename from yii/db/sqlite/Schema.php rename to framework/yii/db/sqlite/Schema.php index d4fb245..bca26c1 100644 --- a/yii/db/sqlite/Schema.php +++ b/framework/yii/db/sqlite/Schema.php @@ -126,7 +126,13 @@ class Schema extends \yii\db\Schema $sql = "PRAGMA foreign_key_list(" . $this->quoteSimpleTableName($table->name) . ')'; $keys = $this->db->createCommand($sql)->queryAll(); foreach ($keys as $key) { - $table->foreignKeys[] = array($key['table'], $key['from'] => $key['to']); + $id = (int)$key['id']; + if (!isset($table->foreignKeys[$id])) { + $table->foreignKeys[$id] = array($key['table'], $key['from'] => $key['to']); + } else { + // composite FK + $table->foreignKeys[$id][$key['from']] = $key['to']; + } } } diff --git a/framework/yii/debug/DebugAsset.php b/framework/yii/debug/DebugAsset.php new file mode 100644 index 0000000..4f3ee89 --- /dev/null +++ b/framework/yii/debug/DebugAsset.php @@ -0,0 +1,25 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\debug; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DebugAsset extends AssetBundle +{ + public $sourcePath = '@yii/debug/assets'; + public $css = array( + 'main.css', + ); + public $depends = array( + 'yii\web\YiiAsset', + 'yii\bootstrap\BootstrapAsset', + ); +} diff --git a/framework/yii/debug/LogTarget.php b/framework/yii/debug/LogTarget.php new file mode 100644 index 0000000..30392f0 --- /dev/null +++ b/framework/yii/debug/LogTarget.php @@ -0,0 +1,105 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\debug; + +use Yii; +use yii\log\Target; + +/** + * The debug LogTarget is used to store logs for later use in the debugger tool + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class LogTarget extends Target +{ + /** + * @var Module + */ + public $module; + public $tag; + + /** + * @param \yii\debug\Module $module + * @param array $config + */ + public function __construct($module, $config = array()) + { + parent::__construct($config); + $this->module = $module; + $this->tag = uniqid(); + } + + /** + * Exports log messages to a specific destination. + * Child classes must implement this method. + */ + public function export() + { + $path = $this->module->dataPath; + if (!is_dir($path)) { + mkdir($path); + } + $indexFile = "$path/index.data"; + if (!is_file($indexFile)) { + $manifest = array(); + } else { + $manifest = unserialize(file_get_contents($indexFile)); + } + $request = Yii::$app->getRequest(); + $manifest[$this->tag] = $summary = array( + 'tag' => $this->tag, + 'url' => $request->getAbsoluteUrl(), + 'ajax' => $request->getIsAjax(), + 'method' => $request->getMethod(), + 'ip' => $request->getUserIP(), + 'time' => time(), + ); + $this->gc($manifest); + + $dataFile = "$path/{$this->tag}.data"; + $data = array(); + foreach ($this->module->panels as $id => $panel) { + $data[$id] = $panel->save(); + } + $data['summary'] = $summary; + file_put_contents($dataFile, serialize($data)); + file_put_contents($indexFile, serialize($manifest)); + } + + /** + * Processes the given log messages. + * This method will filter the given messages with [[levels]] and [[categories]]. + * And if requested, it will also export the filtering result to specific medium (e.g. email). + * @param array $messages log messages to be processed. See [[Logger::messages]] for the structure + * of each message. + * @param boolean $final whether this method is called at the end of the current application + */ + public function collect($messages, $final) + { + $this->messages = array_merge($this->messages, $messages); + if ($final) { + $this->export($this->messages); + } + } + + protected function gc(&$manifest) + { + if (count($manifest) > $this->module->historySize + 10) { + $n = count($manifest) - $this->module->historySize; + foreach (array_keys($manifest) as $tag) { + $file = $this->module->dataPath . "/$tag.data"; + @unlink($file); + unset($manifest[$tag]); + if (--$n <= 0) { + break; + } + } + } + } +} diff --git a/framework/yii/debug/Module.php b/framework/yii/debug/Module.php new file mode 100644 index 0000000..260a981 --- /dev/null +++ b/framework/yii/debug/Module.php @@ -0,0 +1,132 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\debug; + +use Yii; +use yii\base\Application; +use yii\base\View; +use yii\web\HttpException; + +/** + * The Yii Debug Module provides the debug toolbar and debugger + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Module extends \yii\base\Module +{ + /** + * @var array the list of IPs that are allowed to access this module. + * Each array element represents a single IP filter which can be either an IP address + * or an address with wildcard (e.g. 192.168.0.*) to represent a network segment. + * The default value is `array('127.0.0.1', '::1')`, which means the module can only be accessed + * by localhost. + */ + public $allowedIPs = array('127.0.0.1', '::1'); + /** + * @var string the namespace that controller classes are in. + */ + public $controllerNamespace = 'yii\debug\controllers'; + /** + * @var LogTarget + */ + public $logTarget; + /** + * @var array|Panel[] + */ + public $panels = array(); + /** + * @var string the directory storing the debugger data files. This can be specified using a path alias. + */ + public $dataPath = '@runtime/debug'; + /** + * @var integer the maximum number of debug data files to keep. If there are more files generated, + * the oldest ones will be removed. + */ + public $historySize = 50; + + + public function init() + { + parent::init(); + $this->dataPath = Yii::getAlias($this->dataPath); + $this->logTarget = Yii::$app->getLog()->targets['debug'] = new LogTarget($this); + // do not initialize view component before application is ready (needed when debug in preload) + Yii::$app->on(Application::EVENT_BEFORE_ACTION, function() { + Yii::$app->getView()->on(View::EVENT_END_BODY, array($this, 'renderToolbar')); + }); + + foreach (array_merge($this->corePanels(), $this->panels) as $id => $config) { + $config['module'] = $this; + $config['id'] = $id; + $this->panels[$id] = Yii::createObject($config); + } + } + + public function beforeAction($action) + { + Yii::$app->getView()->off(View::EVENT_END_BODY, array($this, 'renderToolbar')); + unset(Yii::$app->getLog()->targets['debug']); + $this->logTarget = null; + + if ($this->checkAccess($action)) { + return parent::beforeAction($action); + } elseif ($action->id === 'toolbar') { + return false; + } else { + throw new HttpException(403, 'You are not allowed to access this page.'); + } + } + + public function renderToolbar($event) + { + if (!$this->checkAccess()) { + return; + } + $url = Yii::$app->getUrlManager()->createUrl($this->id . '/default/toolbar', array( + 'tag' => $this->logTarget->tag, + )); + echo '<div id="yii-debug-toolbar" data-url="' . $url . '" style="display:none"></div>'; + /** @var View $view */ + $view = $event->sender; + echo '<style>' . $view->renderPhpFile(__DIR__ . '/views/default/toolbar.css') . '</style>'; + echo '<script>' . $view->renderPhpFile(__DIR__ . '/views/default/toolbar.js') . '</script>'; + } + + protected function checkAccess() + { + $ip = Yii::$app->getRequest()->getUserIP(); + foreach ($this->allowedIPs as $filter) { + if ($filter === '*' || $filter === $ip || (($pos = strpos($filter, '*')) !== false && !strncmp($ip, $filter, $pos))) { + return true; + } + } + return false; + } + + protected function corePanels() + { + return array( + 'config' => array( + 'class' => 'yii\debug\panels\ConfigPanel', + ), + 'request' => array( + 'class' => 'yii\debug\panels\RequestPanel', + ), + 'log' => array( + 'class' => 'yii\debug\panels\LogPanel', + ), + 'profiling' => array( + 'class' => 'yii\debug\panels\ProfilingPanel', + ), + 'db' => array( + 'class' => 'yii\debug\panels\DbPanel', + ), + ); + } +} diff --git a/framework/yii/debug/Panel.php b/framework/yii/debug/Panel.php new file mode 100644 index 0000000..6782264 --- /dev/null +++ b/framework/yii/debug/Panel.php @@ -0,0 +1,85 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\debug; + +use Yii; +use yii\base\Component; + +/** + * Panel is a base class for debugger panel classes. It defines how data should be collected, + * what should be displayed at debug toolbar and on debugger details view. + * + * @property string $detail Content that is displayed in debugger detail view. This property is read-only. + * @property string $name Name of the panel. This property is read-only. + * @property string $summary Content that is displayed at debug toolbar. This property is read-only. + * @property string $url URL pointing to panel detail view. This property is read-only. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Panel extends Component +{ + public $id; + public $tag; + /** + * @var Module + */ + public $module; + public $data; + + /** + * @return string name of the panel + */ + public function getName() + { + return ''; + } + + /** + * @return string content that is displayed at debug toolbar + */ + public function getSummary() + { + return ''; + } + + /** + * @return string content that is displayed in debugger detail view + */ + public function getDetail() + { + return ''; + } + + /** + * Saves data to be later used in debugger detail view. + * This method is called on every page where debugger is enabled. + * + * @return mixed data to be saved + */ + public function save() + { + return null; + } + + public function load($data) + { + $this->data = $data; + } + + /** + * @return string URL pointing to panel detail view + */ + public function getUrl() + { + return Yii::$app->getUrlManager()->createUrl($this->module->id . '/default/view', array( + 'panel' => $this->id, + 'tag' => $this->tag, + )); + } +} diff --git a/framework/yii/debug/assets/main.css b/framework/yii/debug/assets/main.css new file mode 100644 index 0000000..7953873 --- /dev/null +++ b/framework/yii/debug/assets/main.css @@ -0,0 +1,153 @@ +body { + padding-top: 60px; +} + +#yii-debug-toolbar { + position: fixed; + top: 0; + left: 0; + right: 0; + margin: 0 0 20px 0; + padding: 0; + z-index: 1000000; + font: 11px Verdana, Arial, sans-serif; + text-align: left; + height: 40px; + border-bottom: 1px solid #e4e4e4; + background: rgb(237,237,237); + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2VkZWRlZCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjUzJSIgc3RvcC1jb2xvcj0iI2Y2ZjZmNiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmZmZmZmYiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); + background: -moz-linear-gradient(top, rgba(237,237,237,1) 0%, rgba(246,246,246,1) 53%, rgba(255,255,255,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(237,237,237,1)), color-stop(53%,rgba(246,246,246,1)), color-stop(100%,rgba(255,255,255,1))); + background: -webkit-linear-gradient(top, rgba(237,237,237,1) 0%,rgba(246,246,246,1) 53%,rgba(255,255,255,1) 100%); + background: -o-linear-gradient(top, rgba(237,237,237,1) 0%,rgba(246,246,246,1) 53%,rgba(255,255,255,1) 100%); + background: -ms-linear-gradient(top, rgba(237,237,237,1) 0%,rgba(246,246,246,1) 53%,rgba(255,255,255,1) 100%); + background: linear-gradient(to bottom, rgba(237,237,237,1) 0%,rgba(246,246,246,1) 53%,rgba(255,255,255,1) 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ededed', endColorstr='#ffffff',GradientType=0 ); +} + +.yii-debug-toolbar-block { + float: left; + margin: 0; + border-right: 1px solid #e4e4e4; + padding: 4px 8px; + line-height: 32px; +} + +.yii-debug-toolbar-block.title { + font-size: 1.4em; +} + +.yii-debug-toolbar-block a { + text-decoration: none; + color: black; +} + +.yii-debug-toolbar-block span { +} + +.yii-debug-toolbar-block img { + vertical-align: middle; +} + +#yii-debug-toolbar .label { + display: inline-block; + padding: 2px 4px; + font-size: 11.844px; + font-weight: normal; + line-height: 14px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; +} + +#yii-debug-toolbar .label { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +#yii-debug-toolbar .label:empty { + display: none; +} + +#yii-debug-toolbar a.label:hover, +#yii-debug-toolbar a.label:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +#yii-debug-toolbar .label-important { + background-color: #b94a48; +} + +#yii-debug-toolbar .label-important[href] { + background-color: #953b39; +} + +#yii-debug-toolbar .label-warning, +#yii-debug-toolbar .badge-warning { + background-color: #f89406; +} + +#yii-debug-toolbar .label-warning[href] { + background-color: #c67605; +} + +#yii-debug-toolbar .label-success { + background-color: #468847; +} + +#yii-debug-toolbar .label-success[href] { + background-color: #356635; +} + +#yii-debug-toolbar .label-info { + background-color: #3a87ad; +} + +#yii-debug-toolbar .label-info[href] { + background-color: #2d6987; +} + +#yii-debug-toolbar .label-inverse, +#yii-debug-toolbar .badge-inverse { + background-color: #333333; +} + +#yii-debug-toolbar .label-inverse[href], +#yii-debug-toolbar .badge-inverse[href] { + background-color: #1a1a1a; +} +span.indent { + color: #ccc; +} + +ul.trace { + font-size: 12px; + color: #999; + margin: 2px 0 0 0; + padding: 0; + list-style: none; + white-space: normal; +} + +.callout-danger { + background-color: #fcf2f2; + border-color: #dFb5b4; +} +.callout { + margin: 0 0 10px 0; + padding: 5px; +} + +.list-group .glyphicon { + float: right; +} + +td, th { + white-space: pre; + word-wrap: break-word; +} diff --git a/framework/yii/debug/controllers/DefaultController.php b/framework/yii/debug/controllers/DefaultController.php new file mode 100644 index 0000000..2026dc7 --- /dev/null +++ b/framework/yii/debug/controllers/DefaultController.php @@ -0,0 +1,107 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\debug\controllers; + +use Yii; +use yii\web\Controller; +use yii\web\HttpException; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DefaultController extends Controller +{ + public $layout = 'main'; + /** + * @var \yii\debug\Module + */ + public $module; + /** + * @var array the summary data (e.g. URL, time) + */ + public $summary; + + public function actionIndex() + { + return $this->render('index', array( + 'manifest' => $this->getManifest(), + )); + } + + public function actionView($tag = null, $panel = null) + { + if ($tag === null) { + $tags = array_keys($this->getManifest()); + $tag = reset($tags); + } + $this->loadData($tag); + if (isset($this->module->panels[$panel])) { + $activePanel = $this->module->panels[$panel]; + } else { + $activePanel = $this->module->panels['request']; + } + return $this->render('view', array( + 'tag' => $tag, + 'summary' => $this->summary, + 'manifest' => $this->getManifest(), + 'panels' => $this->module->panels, + 'activePanel' => $activePanel, + )); + } + + public function actionToolbar($tag) + { + $this->loadData($tag); + return $this->renderPartial('toolbar', array( + 'tag' => $tag, + 'panels' => $this->module->panels, + )); + } + + public function actionPhpinfo() + { + phpinfo(); + } + + private $_manifest; + + protected function getManifest() + { + if ($this->_manifest === null) { + $indexFile = $this->module->dataPath . '/index.php'; + if (is_file($indexFile)) { + $this->_manifest = array_reverse(unserialize(file_get_contents($indexFile)), true); + } else { + $this->_manifest = array(); + } + } + return $this->_manifest; + } + + protected function loadData($tag) + { + $manifest = $this->getManifest(); + if (isset($manifest[$tag])) { + $dataFile = $this->module->dataPath . "/$tag.php"; + $data = unserialize(file_get_contents($dataFile)); + foreach ($this->module->panels as $id => $panel) { + if (isset($data[$id])) { + $panel->tag = $tag; + $panel->load($data[$id]); + } else { + // remove the panel since it has not received any data + unset($this->module->panels[$id]); + } + } + $this->summary = $data['summary']; + } else { + throw new HttpException(404, "Unable to find debug data tagged with '$tag'."); + } + } +} diff --git a/framework/yii/debug/panels/ConfigPanel.php b/framework/yii/debug/panels/ConfigPanel.php new file mode 100644 index 0000000..eb0d325 --- /dev/null +++ b/framework/yii/debug/panels/ConfigPanel.php @@ -0,0 +1,115 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\debug\panels; + +use Yii; +use yii\debug\Panel; +use yii\helpers\Html; + +/** + * Debugger panel that collects and displays application configuration and environment. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ConfigPanel extends Panel +{ + public function getName() + { + return 'Configuration'; + } + + public static function getYiiLogo() + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAeCAYAAADQBxWhAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAADcZJREFUeAEAtg1J8gHaKRUAAP8AAAEGAQACDgMAAQgDAAIIAQABCQMAAQgEAAIKAwACCAQAAQgCAAIJBQACCQQAAgkFAAIJBwAQCwkA0hgAANAO+gAM/AEAAQABAPn++wD2/PkA+f38Of3+/Wb+//9S/v/+sQABAV4DAAEAAAAAAAQAAAAAAAAAAAD/AAAA/wAAAP8AGwD/ABoA/wAAAP8A5gD/AOUA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAAAAAB+BgIEgf8BAwD9//4A/v/9Xfz+/hEAAABNAAAAAAQA/wAAAP8AMgADAGEAAwE7AQUBJwAGAgoBBQIAAQUB9gADAdoAAQDFAP//sQD//7sAAAAAAAAAAAAAAAAAAAAA/wD/AP///wD//wAA/wD/AAAAAH4HAwWBAP8AAP7//wD9//4A/f8DAAIB/Uz9//1mAQECmgHaKRUAAAIApwMPBFgCDAQAAQsCAAIJAwABBwMAAgkDAAEHAwABBQIAAQYDAAEFA8cCCAOEAggFtgIKBwAQCgkAzhsBANQO+wAP+wEA/gD/QP///78AAAAA/gD+AP7+/wD9//4A/P/+AP39/gD7//zp/gD/GAQCCAIAAAQA5wAJAgABCAIAAQcCAAEGAwACCAIAAAcCAAEHAgABBgQAAgcEAAIGAjkABAK1AAEAnwD//2f8AP77FPwHABACAwAEBAAAAP/+jgD//wD/AAAA/f/+AP4B/gD9//4AAv79APwB/QAA/f8X/P7+PgQCCgMAAAIBzgAGAQABBgEAAgUCAAIGAQABBgIAAQYDAAIFBAAABwQAAQcCAAEGAwABBQUAAQQCYQEDAiv6Af9HFvgD8A/+AQD2A/4hBf4CMQAAAQD/AP4A/v//AP7+/gD8//4AAgECAAL/AAAB/wAAAgD+RgQACwMAAP8AwwIFAQABBgIAAQYCAAAHAwABBgMAAQUDAAEHAwABBgIAAgYDAAEGBQACBgQAAgUEAAAFAjb9AwG+CPz+ORv6BfndDgMsBvsBAAAAAAD/AP4A/v/+APwB/gAC//0AAv4CAAL+AAAAAwEAAAH8FAICBgEAAgYA4QAEAscBBQIAAQYCAAEFAgAABAIAAQUDAAEFAwACBgMAAQYFAAIGBAABBwQAAAgEAAIHBQACCAYx/gMBpR7zAAAP/wbaBAUHAAcEBQAGAwYABgMGAAcDBQAFAwUABAMDAAQCBQAFAgMABAED/wICDAQAAgwFAAIGAngBAwEAAAUCAAEDAQACBQIAAQUCAAEFAgABBQQAAQYDAAEHBAACBgQAAgUDAAEGAwACBwUA/wn+U/0FHlULABjZBQX74AYDBwAGBAUABQMFAAUDBAAGAgUABQIEAAUCAwAEAQQABAID6AIABQEAAAYBAAAEAcIAAwGZAQMBAAAEAgAABAMAAgUCAAEEAgABBAIAAgQDAAEEAwABBQIAAQYDAAIHBQACBgQAAwYEAP8KAKIHAhEABwQChgYEBQAGAgUABwMFAAUCBQADAgMABQIEAAMCAwADAgMAAwIEugIA/wAAAP8AAAD+/wAAAABoAAMBqgIEAgABBAIAAAMBAAEEAwAABAMAAQUDAAEFAgAABAMAAgUEAAEFBAABBgUAAAcKAAUG8QgH/A93B/4amwYF/f8FAwYABAIDAAUDBAAEAgMAAwIDAAMBAgACAQHkBQIDxwIAAAAAAAAAAAAAAAAAAAAAAQABVwACAnsBAwH0AQMCAAEEAgABBAIAAAMCAAEDAgACBAMAAQUDAAEEAwABBQQAAgcFAP4FBQADAPqABfwaAQQDBbEEAwUAAwMFAAMCAwAEAgMAAwECAAMBAgACAQKaBAIDAAIAAAAAAAAAAAAAAAAAAAAAAAAAAP8A/4YAAAAvAQIBhQABAcoBAgIAAgMCAAEDAgABBAMAAAMDAAEEAwABBQQAAAcCAPwECwD9AgAIAf8LUQQBEaYGAwEAAwIEAAICAgACAgIAAQECAAECAvEDAgOTBAIDAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADeAAAAfAABADcAAgAx/wIAdwACArUAAgL3AQICAAEDAwABAwMAAAYCAPkCDgD8AgoA/QAIbP//Ec0EBAD7AgECAAIBAgACAAIAAgABAAEAAXEEAgPwBQIFAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAAigAAAEwBAAAxAAIBYgACArMDA/v8AAXzAPcADwD9AgkA/gIJQf//BBsCAfrZAf8CAAAAAAAAAAAA/wAAuAEBAp8FAgUABAIEAAIA//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAwAHAffZBgHtnwQD8k4ABPQp1vVFpvYCFgANCPUA/QIIAPr9Eyb8/AOx/wH7AP///wD+//7nAQEAWQUCBAAEAgQABAIEAAT98esAAQYJAAMLEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAIACQH3AAME7AD4AigA3/0sANPtLAD5Ag3c5AE5GxcK8QAzEsgAFAnzAPH7ESMC/ATg/v/1AP3+AP7///9PAQAB8AIBAQAAAAAAAAAAAAOGorAA+9zPAPvg1wADFBQAAgYCAAEGAwAABQYACwT4AA0F9AAIB/UA8QIXANf8LgCp+WIAtvNKAOP3GwDu/BmLEAPuWvT8CgDh9iYABwX+ABUN+PD8++EL+/zuNP3/A08A//+//wD/AAAAAAAAAAAAAAAAAAH///8A+ubdAOdzRQD/7t8AESYZAA0UCAACCPwA8A4iANsJLwDRBC8A2P0rAN37IgAIAfYABv70AA0LBkERCwe+BwQCAAkHAAAAAwkA+wMRADEZ7N0qCYfF9/jR0/4CFoz///wA/f3+AAAAAAAAAAAAAAAAAAH///8A/gAAAPXn4QD90bsA58y1APH38wAIEAkApA5sANICMgD//QAACQD1AA0C8wD//wAABAICEQsIBN4IBgQQBwUCAAkGAwAJBgIAAwQGAP0DFgAuEqk+FQbDw/j+GAD///0A/v7+AAAAAAAAAAAAAAAAAAH///8A+vv7AP4FBQAIAAAAlL7hAJC+6AAZEgwA/gACAAr/9AABAAAAAQD/AP8AAQD+//8ADQgFqw0IBlQIBQMACAYDAAgFBAAHBgMACgYBAAYFBP8BBA0XAwH+6/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAQAAAP729gAACgYA6/T4AOf1+gAQDPYAA//8APIA/wAAAAEAAAAAAP3//wAGAAE5BQECVAEA+wD8/v8A/f7/AP7+/wD9//4A/v8EAAr+/OYD/fLr/v8CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAA/v0AOx0HAIkA+ADf7/sABgMBAAAAAAAAAAAAAAAAAP4A/wAMBAR+AP8AAP3//wD8/v8A/f7+AP7+/wD9/v8A/v7/AP//BLgC/P4A//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAD+/P4ANyENABgPAwDh4/QAAAsEAAAAAAAAAAAAAP8AAAAAABTyCAVI/f//ABX//gAO/v4A/v4EAP3+/wD8//8A/v//AP8AAJICAQEA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAD2+v0ADxAIAEUlEACc0+0AAAwEAAD+AAAAAAAAAAEAAAD//1b49/oA5P3/APn+/wAW/v8AD/3/AP3+/wD8/v4A/f4Fofn8/dL//v8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAD/AAAAAPv9AE4oEQCcCwQAAP3/AAACAQAAAAAAAAAAAAABATAAAP8A/fr7AOj9/gD3/gYAGP//ABH+/gAK/QTfBgMCZwEAAgD9/v8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAABAAAA8fH5AE0zFwA3MRYAAOv3AAAAAAAA/v8AAP7/AAD/ABgAAgEAAAMBAAD9/QDx8/gA4fT5AOX3+tv4/P4/AwEB+QMCAQADAgEAAwIBAAMCAQADAgEAAwIBAAMCAQADAgEAAwIBAAIAAAAAAQEBAAAAAAAAAAAA9/7/AAAAAACGRB0AAAQDAAD5/wAA/gEAAP4BAAAAAA4A/gAAAPr/AAD4/wAC+fwA+Pb4qfH7/jgDAgHjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAT///8G/Pz8+gAAAAD///8AAgAAAPHt9wBCKBEAdFIfAMbZ7AARCwYADQkCAM7d9xzg6foABQ0D8SkVA7spHA+grNnxtfv8/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAT9/f3+////AQQEBAEAAAABBAQE/f0BAQAABQcASiMNAN3g5wAbDQQADAf/AOgNAXosEgkMAQgAsA4GAe4SEAUA/P8BAAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAP//vNz1LVdvDhUAAAAASUVORK5CYII='; + } + + public function getSummary() + { + $yiiLogo = $this->getYiiLogo(); + $url = $this->getUrl(); + $phpUrl = Yii::$app->getUrlManager()->createUrl($this->module->id . '/default/phpinfo'); + return <<<EOD +<div class="yii-debug-toolbar-block"> + <a href="$url"> + <img width="29" height="30" alt="" src="$yiiLogo"> + <span>{$this->data['application']['yii']}</span> + </a> +</div> +<div class="yii-debug-toolbar-block"> + <a href="$phpUrl" title="Show phpinfo()">PHP {$this->data['php']['version']}</a> +</div> +EOD; + } + + public function getDetail() + { + $app = array( + 'Yii Version' => $this->data['application']['yii'], + 'Application Name' => $this->data['application']['name'], + 'Environment' => $this->data['application']['env'], + 'Debug Mode' => $this->data['application']['debug'] ? 'Yes' : 'No', + ); + $php = array( + 'PHP Version' => $this->data['php']['version'], + 'Xdebug' => $this->data['php']['xdebug'] ? 'Enabled' : 'Disabled', + 'APC' => $this->data['php']['apc'] ? 'Enabled' : 'Disabled', + 'Memcache' => $this->data['php']['memcache'] ? 'Enabled' : 'Disabled', + ); + return "<h1>Configuration</h1>\n" + . $this->renderData('Application Configuration', $app) . "\n" + . $this->renderData('PHP Configuration', $php) . "\n" + . $this->getPhpInfo(); + } + + protected function getPhpInfo() + { + return '<div>' . Html::a('Show phpinfo »', array('phpinfo'), array('class' => 'btn btn-primary')) . "</div>\n"; + } + + protected function renderData($caption, $values) + { + if (empty($values)) { + return "<h3>$caption</h3>\n<p>Empty.</p>"; + } + $rows = array(); + foreach ($values as $name => $value) { + $rows[] = '<tr><th style="width:200px;">' . Html::encode($name) . '</th><td style="overflow:auto">' . Html::encode($value) . '</td></tr>'; + } + $rows = implode("\n", $rows); + return <<<EOD +<h3>$caption</h3> +<table class="table table-condensed table-bordered table-striped table-hover" style="table-layout: fixed;"> +<thead><tr><th style="width: 200px;">Name</th><th>Value</th></tr></thead> +<tbody> +$rows +</tbody> +</table> +EOD; + } + + public function save() + { + return array( + 'phpVersion' => PHP_VERSION, + 'yiiVersion' => Yii::getVersion(), + 'application' => array( + 'yii' => Yii::getVersion(), + 'name' => Yii::$app->name, + 'env' => YII_ENV, + 'debug' => YII_DEBUG, + ), + 'php' => array( + 'version' => PHP_VERSION, + 'xdebug' => extension_loaded('xdebug'), + 'apc' => extension_loaded('apc'), + 'memcache' => extension_loaded('memcache'), + ), + ); + } +} diff --git a/framework/yii/debug/panels/DbPanel.php b/framework/yii/debug/panels/DbPanel.php new file mode 100644 index 0000000..8dd2c9e --- /dev/null +++ b/framework/yii/debug/panels/DbPanel.php @@ -0,0 +1,125 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\debug\panels; + +use yii\debug\Panel; +use yii\helpers\ArrayHelper; +use yii\log\Logger; +use yii\helpers\Html; + +/** + * Debugger panel that collects and displays database queries performed. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DbPanel extends Panel +{ + public function getName() + { + return 'Database'; + } + + public function getSummary() + { + $timings = $this->calculateTimings(); + $queryCount = count($timings); + $queryTime = 0; + foreach ($timings as $timing) { + $queryTime += $timing[3]; + } + $queryTime = number_format($queryTime * 1000) . ' ms'; + $url = $this->getUrl(); + $output = <<<EOD +<div class="yii-debug-toolbar-block"> + <a href="$url" title="Executed $queryCount database queries which took $queryTime."> + DB <span class="label">$queryCount</span> <span class="label">$queryTime</span> + </a> +</div> +EOD; + return $queryCount > 0 ? $output : ''; + } + + public function getDetail() + { + $timings = $this->calculateTimings(); + ArrayHelper::multisort($timings, 3, true); + $rows = array(); + foreach ($timings as $timing) { + $duration = sprintf('%.1f ms', $timing[3] * 1000); + $procedure = Html::encode($timing[1]); + $traces = $timing[4]; + if (!empty($traces)) { + $procedure .= Html::ul($traces, array( + 'class' => 'trace', + 'item' => function ($trace) { + return "<li>{$trace['file']}({$trace['line']})</li>"; + }, + )); + } + $rows[] = "<tr><td style=\"width: 80px;\">$duration</td><td>$procedure</td>"; + } + $rows = implode("\n", $rows); + + return <<<EOD +<h1>Database Queries</h1> + +<table class="table table-condensed table-bordered table-striped table-hover" style="table-layout: fixed;"> +<thead> +<tr> + <th style="width: 80px;">Time</th> + <th>Query</th> +</tr> +</thead> +<tbody> +$rows +</tbody> +</table> +EOD; + } + + private $_timings; + + protected function calculateTimings() + { + if ($this->_timings !== null) { + return $this->_timings; + } + $messages = $this->data['messages']; + $timings = array(); + $stack = array(); + foreach ($messages as $i => $log) { + list($token, $level, $category, $timestamp) = $log; + $log[5] = $i; + if ($level == Logger::LEVEL_PROFILE_BEGIN) { + $stack[] = $log; + } elseif ($level == Logger::LEVEL_PROFILE_END) { + if (($last = array_pop($stack)) !== null && $last[0] === $token) { + $timings[$last[5]] = array(count($stack), $token, $last[3], $timestamp - $last[3], $last[4]); + } + } + } + + $now = microtime(true); + while (($last = array_pop($stack)) !== null) { + $delta = $now - $last[3]; + $timings[$last[5]] = array(count($stack), $last[0], $last[2], $delta, $last[4]); + } + ksort($timings); + return $this->_timings = $timings; + } + + public function save() + { + $target = $this->module->logTarget; + $messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, array('yii\db\Command::queryInternal')); + return array( + 'messages' => $messages, + ); + } +} diff --git a/framework/yii/debug/panels/LogPanel.php b/framework/yii/debug/panels/LogPanel.php new file mode 100644 index 0000000..c562006 --- /dev/null +++ b/framework/yii/debug/panels/LogPanel.php @@ -0,0 +1,107 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\debug\panels; + +use Yii; +use yii\debug\Panel; +use yii\helpers\Html; +use yii\log\Logger; +use yii\log\Target; + +/** + * Debugger panel that collects and displays logs. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class LogPanel extends Panel +{ + public function getName() + { + return 'Logs'; + } + + public function getSummary() + { + $output = array('<span class="label">' . count($this->data['messages']) . '</span>'); + $title = 'Logged ' . count($this->data['messages']) . ' messages'; + $errorCount = count(Target::filterMessages($this->data['messages'], Logger::LEVEL_ERROR)); + if ($errorCount) { + $output[] = '<span class="label label-important">' . $errorCount . '</span>'; + $title .= ", $errorCount errors"; + } + $warningCount = count(Target::filterMessages($this->data['messages'], Logger::LEVEL_WARNING)); + if ($warningCount) { + $output[] = '<span class="label label-warning">' . $warningCount . '</span>'; + $title .= ", $warningCount warnings"; + } + $log = implode(' ', $output); + $url = $this->getUrl(); + return <<<EOD +<div class="yii-debug-toolbar-block"> + <a href="$url" title="$title">Log $log</a> +</div> +EOD; + } + + public function getDetail() + { + $rows = array(); + foreach ($this->data['messages'] as $log) { + list ($message, $level, $category, $time, $traces) = $log; + $time = date('H:i:s.', $time) . sprintf('%03d', (int)(($time - (int)$time) * 1000)); + $message = nl2br(Html::encode($message)); + if (!empty($traces)) { + $message .= Html::ul($traces, array( + 'class' => 'trace', + 'item' => function ($trace) { + return "<li>{$trace['file']}({$trace['line']})</li>"; + }, + )); + } + if ($level == Logger::LEVEL_ERROR) { + $class = ' class="danger"'; + } elseif ($level == Logger::LEVEL_WARNING) { + $class = ' class="warning"'; + } elseif ($level == Logger::LEVEL_INFO) { + $class = ' class="success"'; + } else { + $class = ''; + } + $level = Logger::getLevelName($level); + $rows[] = "<tr$class><td style=\"width: 100px;\">$time</td><td style=\"width: 100px;\">$level</td><td style=\"width: 250px;\">$category</td><td><div>$message</div></td></tr>"; + } + $rows = implode("\n", $rows); + return <<<EOD +<h1>Log Messages</h1> + +<table class="table table-condensed table-bordered table-striped table-hover" style="table-layout: fixed;"> +<thead> +<tr> + <th style="width: 100px;">Time</th> + <th style="width: 65px;">Level</th> + <th style="width: 250px;">Category</th> + <th>Message</th> +</tr> +</thead> +<tbody> +$rows +</tbody> +</table> +EOD; + } + + public function save() + { + $target = $this->module->logTarget; + $messages = $target->filterMessages($target->messages, Logger::LEVEL_ERROR | Logger::LEVEL_INFO | Logger::LEVEL_WARNING | Logger::LEVEL_TRACE); + return array( + 'messages' => $messages, + ); + } +} diff --git a/framework/yii/debug/panels/ProfilingPanel.php b/framework/yii/debug/panels/ProfilingPanel.php new file mode 100644 index 0000000..c462e40 --- /dev/null +++ b/framework/yii/debug/panels/ProfilingPanel.php @@ -0,0 +1,107 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\debug\panels; + +use Yii; +use yii\debug\Panel; +use yii\helpers\Html; +use yii\log\Logger; + +/** + * Debugger panel that collects and displays performance profiling info. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ProfilingPanel extends Panel +{ + public function getName() + { + return 'Profiling'; + } + + public function getSummary() + { + $memory = sprintf('%.1f MB', $this->data['memory'] / 1048576); + $time = number_format($this->data['time'] * 1000) . ' ms'; + $url = $this->getUrl(); + + return <<<EOD +<div class="yii-debug-toolbar-block"> + <a href="$url" title="Total request processing time was $time">Time <span class="label">$time</span></a> +</div> +<div class="yii-debug-toolbar-block"> + <a href="$url" title="Peak memory consumption">Memory <span class="label">$memory</span></a> +</div> +EOD; + } + + public function getDetail() + { + $messages = $this->data['messages']; + $timings = array(); + $stack = array(); + foreach ($messages as $i => $log) { + list($token, $level, $category, $timestamp, $traces) = $log; + if ($level == Logger::LEVEL_PROFILE_BEGIN) { + $stack[] = $log; + } elseif ($level == Logger::LEVEL_PROFILE_END) { + if (($last = array_pop($stack)) !== null && $last[0] === $token) { + $timings[] = array(count($stack), $token, $category, $timestamp - $last[3], $traces); + } + } + } + + $now = microtime(true); + while (($last = array_pop($stack)) !== null) { + $timings[] = array(count($stack), $last[0], $last[2], $now - $last[3], $last[4]); + } + + $rows = array(); + foreach ($timings as $timing) { + $time = sprintf('%.1f ms', $timing[3] * 1000); + $procedure = str_repeat('<span class="indent">→</span>', $timing[0]) . Html::encode($timing[1]); + $category = Html::encode($timing[2]); + $rows[] = "<tr><td style=\"width: 80px;\">$time</td><td style=\"width: 220px;\">$category</td><td>$procedure</td>"; + } + $rows = implode("\n", $rows); + + $memory = sprintf('%.1f MB', $this->data['memory'] / 1048576); + $time = number_format($this->data['time'] * 1000) . ' ms'; + + return <<<EOD +<h2>Performance Profiling</h2> + +<p>Total processing time: <b>$time</b>; Peak memory: <b>$memory</b>.</p> + +<table class="table table-condensed table-bordered table-striped table-hover" style="table-layout: fixed;"> +<thead> +<tr> + <th style="width: 80px;">Time</th> + <th style="width: 220px;">Category</th> + <th>Procedure</th> +</tr> +</thead> +<tbody> +$rows +</tbody> +</table> +EOD; + } + + public function save() + { + $target = $this->module->logTarget; + $messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE); + return array( + 'memory' => memory_get_peak_usage(), + 'time' => microtime(true) - YII_BEGIN_TIME, + 'messages' => $messages, + ); + } +} diff --git a/framework/yii/debug/panels/RequestPanel.php b/framework/yii/debug/panels/RequestPanel.php new file mode 100644 index 0000000..58256e4 --- /dev/null +++ b/framework/yii/debug/panels/RequestPanel.php @@ -0,0 +1,167 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\debug\panels; + +use Yii; +use yii\base\InlineAction; +use yii\bootstrap\Tabs; +use yii\debug\Panel; +use yii\helpers\Html; +use yii\web\Response; + +/** + * Debugger panel that collects and displays request data. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class RequestPanel extends Panel +{ + public function getName() + { + return 'Request'; + } + + public function getSummary() + { + $url = $this->getUrl(); + $statusCode = $this->data['statusCode']; + if ($statusCode === null) { + $statusCode = 200; + } + if ($statusCode >= 200 && $statusCode < 300) { + $class = 'label-success'; + } elseif ($statusCode >= 100 && $statusCode < 200) { + $class = 'label-info'; + } else { + $class = 'label-important'; + } + $statusText = Html::encode(isset(Response::$httpStatuses[$statusCode]) ? Response::$httpStatuses[$statusCode] : ''); + + return <<<EOD +<div class="yii-debug-toolbar-block"> + <a href="$url" title="Status code: $statusCode $statusText">Status <span class="label $class">$statusCode</span></a> +</div> +<div class="yii-debug-toolbar-block"> + <a href="$url">Action <span class="label">{$this->data['action']}</span></a> +</div> +EOD; + } + + public function getDetail() + { + $data = array( + 'Route' => $this->data['route'], + 'Action' => $this->data['action'], + 'Parameters' => $this->data['actionParams'], + ); + return Tabs::widget(array( + 'items' => array( + array( + 'label' => 'Parameters', + 'content' => $this->renderData('Routing', $data) + . $this->renderData('$_GET', $this->data['GET']) + . $this->renderData('$_POST', $this->data['POST']) + . $this->renderData('$_FILES', $this->data['FILES']) + . $this->renderData('$_COOKIE', $this->data['COOKIE']), + 'active' => true, + ), + array( + 'label' => 'Headers', + 'content' => $this->renderData('Request Headers', $this->data['requestHeaders']) + . $this->renderData('Response Headers', $this->data['responseHeaders']), + ), + array( + 'label' => 'Session', + 'content' => $this->renderData('$_SESSION', $this->data['SESSION']) + . $this->renderData('Flashes', $this->data['flashes']), + ), + array( + 'label' => '$_SERVER', + 'content' => $this->renderData('$_SERVER', $this->data['SERVER']), + ), + ), + )); + } + + public function save() + { + if (function_exists('apache_request_headers')) { + $requestHeaders = apache_request_headers(); + } elseif (function_exists('http_get_request_headers')) { + $requestHeaders = http_get_request_headers(); + } else { + $requestHeaders = array(); + } + $responseHeaders = array(); + foreach (headers_list() as $header) { + if (($pos = strpos($header, ':')) !== false) { + $name = substr($header, 0, $pos); + $value = trim(substr($header, $pos + 1)); + if (isset($responseHeaders[$name])) { + if (!is_array($responseHeaders[$name])) { + $responseHeaders[$name] = array($responseHeaders[$name], $value); + } else { + $responseHeaders[$name][] = $value; + } + } else { + $responseHeaders[$name] = $value; + } + } else { + $responseHeaders[] = $header; + } + } + if (Yii::$app->requestedAction) { + if (Yii::$app->requestedAction instanceof InlineAction) { + $action = get_class(Yii::$app->requestedAction->controller) . '::' . Yii::$app->requestedAction->actionMethod . '()'; + } else { + $action = get_class(Yii::$app->requestedAction) . '::run()'; + } + } else { + $action = null; + } + /** @var \yii\web\Session $session */ + $session = Yii::$app->getComponent('session', false); + return array( + 'flashes' => $session ? $session->getAllFlashes() : array(), + 'statusCode' => Yii::$app->getResponse()->getStatusCode(), + 'requestHeaders' => $requestHeaders, + 'responseHeaders' => $responseHeaders, + 'route' => Yii::$app->requestedAction ? Yii::$app->requestedAction->getUniqueId() : Yii::$app->requestedRoute, + 'action' => $action, + 'actionParams' => Yii::$app->requestedParams, + 'SERVER' => empty($_SERVER) ? array() : $_SERVER, + 'GET' => empty($_GET) ? array() : $_GET, + 'POST' => empty($_POST) ? array() : $_POST, + 'COOKIE' => empty($_COOKIE) ? array() : $_COOKIE, + 'FILES' => empty($_FILES) ? array() : $_FILES, + 'SESSION' => empty($_SESSION) ? array() : $_SESSION, + ); + } + + protected function renderData($caption, $values) + { + if (empty($values)) { + return "<h3>$caption</h3>\n<p>Empty.</p>"; + } + $rows = array(); + foreach ($values as $name => $value) { + $rows[] = '<tr><th style="width: 200px;">' . Html::encode($name) . '</th><td>' . htmlspecialchars(var_export($value, true), ENT_QUOTES|ENT_SUBSTITUTE, \Yii::$app->charset, TRUE) . '</td></tr>'; + } + $rows = implode("\n", $rows); + return <<<EOD +<h3>$caption</h3> +<table class="table table-condensed table-bordered table-striped table-hover" style="table-layout: fixed;"> +<thead><tr><th style="width: 200px;">Name</th><th>Value</th></tr></thead> +<tbody> +$rows +</tbody> +</table> +EOD; + } +} diff --git a/framework/yii/debug/views/default/index.php b/framework/yii/debug/views/default/index.php new file mode 100644 index 0000000..59d60b4 --- /dev/null +++ b/framework/yii/debug/views/default/index.php @@ -0,0 +1,46 @@ +<?php + +use yii\helpers\Html; + +/** + * @var \yii\base\View $this + * @var array $manifest + */ + +$this->title = 'Yii Debugger'; +?> +<div class="default-index"> + <div id="yii-debug-toolbar"> + <div class="yii-debug-toolbar-block title"> + Yii Debugger + </div> + </div> + + <div class="container"> + <div class="row"> + <h1>Available Debug Data</h1> + <table class="table table-condensed table-bordered table-striped table-hover" style="table-layout: fixed;"> + <thead> + <tr> + <th style="width: 120px;">Tag</th> + <th style="width: 170px;">Time</th> + <th style="width: 120px;">IP</th> + <th style="width: 70px;">Method</th> + <th>URL</th> + </tr> + </thead> + <tbody> + <?php foreach ($manifest as $tag => $data): ?> + <tr> + <td><?php echo Html::a($tag, array('view', 'tag' => $tag)); ?></td> + <td><?php echo date('Y-m-d h:i:sa', $data['time']); ?></td> + <td><?php echo $data['ip']; ?></td> + <td><?php echo $data['method']; ?></td> + <td><?php echo $data['url']; ?></td> + </tr> + <?php endforeach; ?> + </tbody> + </table> + </div> + </div> +</div> diff --git a/framework/yii/debug/views/default/toolbar.css b/framework/yii/debug/views/default/toolbar.css new file mode 100644 index 0000000..72bf3bf --- /dev/null +++ b/framework/yii/debug/views/default/toolbar.css @@ -0,0 +1,177 @@ +#yii-debug-toolbar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + margin: 0; + padding: 0; + z-index: 1000000; + font: 11px Verdana, Arial, sans-serif; + text-align: left; + height: 40px; + border-top: 1px solid #ccc; + background: rgb(237,237,237); + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2VkZWRlZCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjUzJSIgc3RvcC1jb2xvcj0iI2Y2ZjZmNiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmZmZmZmYiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); + background: -moz-linear-gradient(top, rgba(237,237,237,1) 0%, rgba(246,246,246,1) 53%, rgba(255,255,255,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(237,237,237,1)), color-stop(53%,rgba(246,246,246,1)), color-stop(100%,rgba(255,255,255,1))); + background: -webkit-linear-gradient(top, rgba(237,237,237,1) 0%,rgba(246,246,246,1) 53%,rgba(255,255,255,1) 100%); + background: -o-linear-gradient(top, rgba(237,237,237,1) 0%,rgba(246,246,246,1) 53%,rgba(255,255,255,1) 100%); + background: -ms-linear-gradient(top, rgba(237,237,237,1) 0%,rgba(246,246,246,1) 53%,rgba(255,255,255,1) 100%); + background: linear-gradient(to bottom, rgba(237,237,237,1) 0%,rgba(246,246,246,1) 53%,rgba(255,255,255,1) 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ededed', endColorstr='#ffffff',GradientType=0 ); +} + +.yii-debug-toolbar-block { + float: left; + margin: 0; + border-right: 1px solid #e4e4e4; + padding: 4px 8px; + line-height: 32px; +} + +.yii-debug-toolbar-block a { + text-decoration: none; + color: black; +} + +.yii-debug-toolbar-block span { +} + +.yii-debug-toolbar-block img { + vertical-align: middle; +} + +#yii-debug-toolbar .label { + display: inline-block; + padding: 2px 4px; + font-size: 11.844px; + font-weight: normal; + line-height: 14px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; +} + +#yii-debug-toolbar .label { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +#yii-debug-toolbar .label:empty { + display: none; +} + +#yii-debug-toolbar a.label:hover, +#yii-debug-toolbar a.label:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +#yii-debug-toolbar .label-important { + background-color: #b94a48; +} + +#yii-debug-toolbar .label-important[href] { + background-color: #953b39; +} + +#yii-debug-toolbar .label-warning, +#yii-debug-toolbar .badge-warning { + background-color: #f89406; +} + +#yii-debug-toolbar .label-warning[href] { + background-color: #c67605; +} + +#yii-debug-toolbar .label-success { + background-color: #468847; +} + +#yii-debug-toolbar .label-success[href] { + background-color: #356635; +} + +#yii-debug-toolbar .label-info { + background-color: #3a87ad; +} + +#yii-debug-toolbar .label-info[href] { + background-color: #2d6987; +} + +#yii-debug-toolbar .label-inverse, +#yii-debug-toolbar .badge-inverse { + background-color: #333333; +} + +#yii-debug-toolbar .label-inverse[href], +#yii-debug-toolbar .badge-inverse[href] { + background-color: #1a1a1a; +} + +.yii-debug-toolbar-toggler { + cursor: pointer; + position: absolute; + right: 10px; + bottom: 4px; + width: 15px; + height: 30px; + font-size: 25px; + font-weight: 100; + line-height: 28px; + color: #ffffff; + text-align: center; + background: #666666; + -webkit-border-radius: 12px; + -moz-border-radius: 12px; + border-radius: 12px; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.yii-debug-toolbar-toggler:hover, +.yii-debug-toolbar-toggler:focus { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +#yii-debug-toolbar-min { + display: none; + position: fixed; + right: 0; + bottom: 0; + margin: 0; + padding: 0; + z-index: 1000000; + font: 11px Verdana, Arial, sans-serif; + text-align: left; + width: 63px; + height: 38px; + border-top: 1px solid #ccc; + border-left: 1px solid #ccc; + -webkit-border-top-left-radius: 6px; + -moz-border-top-left-radius: 6px; + border-top-left-radius: 6px; + background: rgb(237,237,237); + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2VkZWRlZCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjUzJSIgc3RvcC1jb2xvcj0iI2Y2ZjZmNiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmZmZmZmYiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); + background: -moz-linear-gradient(top, rgba(237,237,237,1) 0%, rgba(246,246,246,1) 53%, rgba(255,255,255,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(237,237,237,1)), color-stop(53%,rgba(246,246,246,1)), color-stop(100%,rgba(255,255,255,1))); + background: -webkit-linear-gradient(top, rgba(237,237,237,1) 0%,rgba(246,246,246,1) 53%,rgba(255,255,255,1) 100%); + background: -o-linear-gradient(top, rgba(237,237,237,1) 0%,rgba(246,246,246,1) 53%,rgba(255,255,255,1) 100%); + background: -ms-linear-gradient(top, rgba(237,237,237,1) 0%,rgba(246,246,246,1) 53%,rgba(255,255,255,1) 100%); + background: linear-gradient(to bottom, rgba(237,237,237,1) 0%,rgba(246,246,246,1) 53%,rgba(255,255,255,1) 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ededed', endColorstr='#ffffff',GradientType=0 ); +} + +#yii-debug-toolbar-logo { + position: fixed; + right: 31px; + bottom: 4px; +} diff --git a/framework/yii/debug/views/default/toolbar.js b/framework/yii/debug/views/default/toolbar.js new file mode 100644 index 0000000..0dca1de --- /dev/null +++ b/framework/yii/debug/views/default/toolbar.js @@ -0,0 +1,41 @@ +(function() { + var ajax = function(url, settings) { + var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); + settings = settings || {}; + xhr.open(settings.method || 'GET', url, true); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + xhr.onreadystatechange = function(state) { + if (xhr.readyState == 4) { + if (xhr.status == 200 && settings.success) { + settings.success(xhr); + } else if (xhr.status != 200 && settings.error) { + settings.error(xhr); + } + } + }; + xhr.send(settings.data || ''); + }; + + var e = document.getElementById('yii-debug-toolbar'); + if (e) { + e.style.display = 'block'; + var url = e.getAttribute('data-url'); + ajax(url, { + success: function(xhr) { + var div = document.createElement('div'); + div.innerHTML = xhr.responseText; + e.parentNode.replaceChild(div, e); + if (window.localStorage) { + var pref = localStorage.getItem('yii-debug-toolbar'); + if (pref == 'minimized') { + document.getElementById('yii-debug-toolbar').style.display = 'none'; + document.getElementById('yii-debug-toolbar-min').style.display = 'block'; + } + } + }, + error: function(xhr) { + e.innerHTML = xhr.responseText; + } + }); + } +})(); diff --git a/framework/yii/debug/views/default/toolbar.php b/framework/yii/debug/views/default/toolbar.php new file mode 100644 index 0000000..ac238fa --- /dev/null +++ b/framework/yii/debug/views/default/toolbar.php @@ -0,0 +1,38 @@ +<?php +/** + * @var \yii\base\View $this + * @var \yii\debug\Panel[] $panels + * @var string $tag + */ +use yii\debug\panels\ConfigPanel; + +$minJs = <<<EOD +document.getElementById('yii-debug-toolbar').style.display = 'none'; +document.getElementById('yii-debug-toolbar-min').style.display = 'block'; +if (window.localStorage) { + localStorage.setItem('yii-debug-toolbar', 'minimized'); +} +EOD; + +$maxJs = <<<EOD +document.getElementById('yii-debug-toolbar-min').style.display = 'none'; +document.getElementById('yii-debug-toolbar').style.display = 'block'; +if (window.localStorage) { + localStorage.setItem('yii-debug-toolbar', 'maximized'); +} +EOD; + +$url = $panels['request']->getUrl(); +?> +<div id="yii-debug-toolbar"> + <?php foreach ($panels as $panel): ?> + <?php echo $panel->getSummary(); ?> + <?php endforeach; ?> + <span class="yii-debug-toolbar-toggler" onclick="<?php echo $minJs; ?>">›</span> +</div> +<div id="yii-debug-toolbar-min"> + <a href="<?php echo $url; ?>" title="Open Yii Debugger" id="yii-debug-toolbar-logo"> + <img width="29" height="30" alt="" src="<?php echo ConfigPanel::getYiiLogo(); ?>"> + </a> + <span class="yii-debug-toolbar-toggler" onclick="<?php echo $maxJs; ?>">‹</span> +</div> diff --git a/framework/yii/debug/views/default/view.php b/framework/yii/debug/views/default/view.php new file mode 100644 index 0000000..190bbe2 --- /dev/null +++ b/framework/yii/debug/views/default/view.php @@ -0,0 +1,78 @@ +<?php + +use yii\bootstrap\ButtonDropdown; +use yii\bootstrap\ButtonGroup; +use yii\helpers\Html; + +/** + * @var \yii\base\View $this + * @var array $summary + * @var string $tag + * @var array $manifest + * @var \yii\debug\Panel[] $panels + * @var \yii\debug\Panel $activePanel + */ + +$this->title = 'Yii Debugger'; +?> +<div class="default-view"> + <div id="yii-debug-toolbar"> + <div class="yii-debug-toolbar-block title"> + Yii Debugger + </div> + <?php foreach ($panels as $panel): ?> + <?php echo $panel->getSummary(); ?> + <?php endforeach; ?> + </div> + + <div class="container"> + <div class="row"> + <div class="col-lg-2"> + <div class="list-group"> + <?php + foreach ($panels as $id => $panel) { + $label = '<i class="glyphicon glyphicon-chevron-right"></i>' . Html::encode($panel->getName()); + echo Html::a($label, array('view', 'tag' => $tag, 'panel' => $id), array( + 'class' => $panel === $activePanel ? 'list-group-item active' : 'list-group-item', + )); + } + ?> + </div> + </div> + <div class="col-lg-10"> + <div class="callout callout-danger"> + <?php + $count = 0; + $items = array(); + foreach ($manifest as $meta) { + $label = $meta['tag'] . ': ' . $meta['method'] . ' ' . $meta['url'] . ($meta['ajax'] ? ' (AJAX)' : '') + . ', ' . date('Y-m-d h:i:s a', $meta['time']) + . ', ' . $meta['ip']; + $url = array('view', 'tag' => $meta['tag'], 'panel' => $activePanel->id); + $items[] = array( + 'label' => $label, + 'url' => $url, + ); + if (++$count >= 10) { + break; + } + } + echo ButtonGroup::widget(array( + 'buttons' => array( + Html::a('All', array('index'), array('class' => 'btn btn-default')), + ButtonDropdown::widget(array( + 'label' => 'Last 10', + 'options' => array('class' => 'btn-default'), + 'dropdown' => array('items' => $items), + )), + ), + )); + echo "\n" . $summary['tag'] . ': ' . $summary['method'] . ' ' . Html::a(Html::encode($summary['url']), $summary['url']); + echo ' at ' . date('Y-m-d h:i:s a', $summary['time']) . ' by ' . $summary['ip']; + ?> + </div> + <?php echo $activePanel->getDetail(); ?> + </div> + </div> + </div> +</div> diff --git a/framework/yii/debug/views/layouts/main.php b/framework/yii/debug/views/layouts/main.php new file mode 100644 index 0000000..8875878 --- /dev/null +++ b/framework/yii/debug/views/layouts/main.php @@ -0,0 +1,23 @@ +<?php +/** + * @var \yii\base\View $this + * @var string $content + */ +use yii\helpers\Html; + +yii\debug\DebugAsset::register($this); +?> +<!DOCTYPE html> +<html> +<?php $this->beginPage(); ?> +<head> + <title><?php echo Html::encode($this->title); ?></title> + <?php $this->head(); ?> +</head> +<body> +<?php $this->beginBody(); ?> +<?php echo $content; ?> +<?php $this->endBody(); ?> +</body> +<?php $this->endPage(); ?> +</html> diff --git a/framework/yii/gii/CodeFile.php b/framework/yii/gii/CodeFile.php new file mode 100644 index 0000000..2f21b4c --- /dev/null +++ b/framework/yii/gii/CodeFile.php @@ -0,0 +1,155 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\gii; + +use Yii; +use yii\base\Object; +use yii\gii\components\TextDiff; +use yii\helpers\Html; +use yii\helpers\StringHelper; + +/** + * CodeFile represents a code file to be generated. + * + * @property string $relativePath The code file path relative to the application base path. This property is + * read-only. + * @property string $type The code file extension (e.g. php, txt). This property is read-only. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class CodeFile extends Object +{ + /** + * The code file is new. + */ + const OP_NEW = 'new'; + /** + * The code file already exists, and the new one may need to overwrite it. + */ + const OP_OVERWRITE = 'overwrite'; + /** + * The new code file and the existing one are identical. + */ + const OP_SKIP = 'skip'; + + /** + * @var string an ID that uniquely identifies this code file. + */ + public $id; + /** + * @var string the file path that the new code should be saved to. + */ + public $path; + /** + * @var string the newly generated code content + */ + public $content; + /** + * @var string the operation to be performed. This can be [[OP_NEW]], [[OP_OVERWRITE]] or [[OP_SKIP]]. + */ + public $operation; + + /** + * Constructor. + * @param string $path the file path that the new code should be saved to. + * @param string $content the newly generated code content. + */ + public function __construct($path, $content) + { + $this->path = strtr($path, array('/' => DIRECTORY_SEPARATOR, '\\' => DIRECTORY_SEPARATOR)); + $this->content = $content; + $this->id = md5($this->path); + if (is_file($path)) { + $this->operation = file_get_contents($path) === $content ? self::OP_SKIP : self::OP_OVERWRITE; + } else { + $this->operation = self::OP_NEW; + } + } + + /** + * Saves the code into the file specified by [[path]]. + * @return string|boolean the error occurred while saving the code file, or true if no error. + */ + public function save() + { + $module = Yii::$app->controller->module; + if ($this->operation === self::OP_NEW) { + $dir = dirname($this->path); + if (!is_dir($dir)) { + $mask = @umask(0); + $result = @mkdir($dir, $module->newDirMode, true); + @umask($mask); + if (!$result) { + return "Unable to create the directory '$dir'."; + } + } + } + if (@file_put_contents($this->path, $this->content) === false) { + return "Unable to write the file '{$this->path}'."; + } else { + $mask = @umask(0); + @chmod($this->path, $module->newFileMode); + @umask($mask); + } + return true; + } + + /** + * @return string the code file path relative to the application base path. + */ + public function getRelativePath() + { + if (strpos($this->path, Yii::$app->basePath) === 0) { + return substr($this->path, strlen(Yii::$app->basePath) + 1); + } else { + return $this->path; + } + } + + /** + * @return string the code file extension (e.g. php, txt) + */ + public function getType() + { + if (($pos = strrpos($this->path, '.')) !== false) { + return substr($this->path, $pos + 1); + } else { + return 'unknown'; + } + } + + public function preview() + { + if (($pos = strrpos($this->path, '.')) !== false) { + $type = substr($this->path, $pos + 1); + } else { + $type = 'unknown'; + } + + if ($type === 'php') { + return highlight_string($this->content, true); + } elseif (!in_array($type, array('jpg', 'gif', 'png', 'exe'))) { + return nl2br(Html::encode($this->content)); + } else { + return false; + } + } + + public function diff() + { + $type = strtolower($this->getType()); + if (in_array($type, array('jpg', 'gif', 'png', 'exe'))) { + return false; + } elseif ($this->operation === self::OP_OVERWRITE) { + return StringHelper::diff(file($this->path), $this->content); + } else { + return ''; + } + } +} diff --git a/framework/yii/gii/Generator.php b/framework/yii/gii/Generator.php new file mode 100644 index 0000000..eb5b8b4 --- /dev/null +++ b/framework/yii/gii/Generator.php @@ -0,0 +1,438 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\gii; + +use Yii; +use ReflectionClass; +use yii\base\InvalidConfigException; +use yii\base\Model; +use yii\base\View; + + +/** + * This is the base class for all generator classes. + * + * A generator instance is responsible for taking user inputs, validating them, + * and using them to generate the corresponding code based on a set of code template files. + * + * A generator class typically needs to implement the following methods: + * + * - [[getName()]]: returns the name of the generator + * - [[getDescription()]]: returns the detailed description of the generator + * - [[generate()]]: generates the code based on the current user input and the specified code template files. + * This is the place where main code generation code resides. + * + * @property string $description The detailed description of the generator. This property is read-only. + * @property string $stickyDataFile The file path that stores the sticky attribute values. This property is + * read-only. + * @property string $templatePath The root path of the template files that are currently being used. This + * property is read-only. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +abstract class Generator extends Model +{ + /** + * @var array a list of available code templates. The array keys are the template names, + * and the array values are the corresponding template paths or path aliases. + */ + public $templates = array(); + /** + * @var string the name of the code template that the user has selected. + * The value of this property is internally managed by this class. + */ + public $template; + + /** + * @return string name of the code generator + */ + abstract public function getName(); + /** + * Generates the code based on the current user input and the specified code template files. + * This is the main method that child classes should implement. + * Please refer to [[\yii\gii\generators\controller\Generator::generate()]] as an example + * on how to implement this method. + * @return CodeFile[] a list of code files to be created. + */ + abstract public function generate(); + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if (!isset($this->templates['default'])) { + $this->templates['default'] = $this->defaultTemplate(); + } + foreach ($this->templates as $i => $template) { + $this->templates[$i] = Yii::getAlias($template); + } + } + + /** + * Returns a list of code template files that are required. + * Derived classes usually should override this method if they require the existence of + * certain template files. + * @return array list of code template files that are required. They should be file paths + * relative to [[templatePath]]. + */ + public function requiredTemplates() + { + return array(); + } + + /** + * Returns the list of sticky attributes. + * A sticky attribute will remember its value and will initialize the attribute with this value + * when the generator is restarted. + * @return array list of sticky attributes + */ + public function stickyAttributes() + { + return array('template'); + } + + /** + * Returns the list of hint messages. + * The array keys are the attribute names, and the array values are the corresponding hint messages. + * Hint messages will be displayed to end users when they are filling the form for the generator. + * @return array the list of hint messages + */ + public function hints() + { + return array(); + } + + /** + * Returns the message to be displayed when the newly generated code is saved successfully. + * Child classes may override this method to customize the message. + * @return string the message to be displayed when the newly generated code is saved successfully. + */ + public function successMessage() + { + return 'The code has been generated successfully.'; + } + + /** + * Returns the view file for the input form of the generator. + * The default implementation will return the "form.php" file under the directory + * that contains the generator class file. + * @return string the view file for the input form of the generator. + */ + public function formView() + { + $class = new ReflectionClass($this); + return dirname($class->getFileName()) . '/form.php'; + } + + /** + * Returns the root path to the default code template files. + * The default implementation will return the "templates" subdirectory of the + * directory containing the generator class file. + * @return string the root path to the default code template files. + */ + public function defaultTemplate() + { + $class = new ReflectionClass($this); + return dirname($class->getFileName()) . '/templates'; + } + + /** + * @return string the detailed description of the generator. + */ + public function getDescription() + { + return ''; + } + + /** + * @inheritdoc + * + * Child classes should override this method like the following so that the parent + * rules are included: + * + * ~~~ + * return array_merge(parent::rules(), array( + * ...rules for the child class... + * )); + * ~~~ + */ + public function rules() + { + return array( + array('template', 'required', 'message' => 'A code template must be selected.'), + array('template', 'validateTemplate'), + ); + } + + /** + * Loads sticky attributes from an internal file and populates them into the generator. + * @internal + */ + public function loadStickyAttributes() + { + $stickyAttributes = $this->stickyAttributes(); + $attributes[] = 'template'; + $path = $this->getStickyDataFile(); + if (is_file($path)) { + $result = @include($path); + if (is_array($result)) { + foreach ($stickyAttributes as $name) { + if (isset($result[$name])) { + $this->$name = $result[$name]; + } + } + } + } + } + + /** + * Saves sticky attributes into an internal file. + * @internal + */ + public function saveStickyAttributes() + { + $stickyAttributes = $this->stickyAttributes(); + $stickyAttributes[] = 'template'; + $values = array(); + foreach ($stickyAttributes as $name) { + $values[$name] = $this->$name; + } + $path = $this->getStickyDataFile(); + @mkdir(dirname($path), 0755, true); + file_put_contents($path, "<?php\nreturn " . var_export($values, true) . ";\n"); + } + + /** + * @return string the file path that stores the sticky attribute values. + * @internal + */ + public function getStickyDataFile() + { + return Yii::$app->getRuntimePath() . '/gii-' . Yii::getVersion() . '/' . str_replace('\\', '-', get_class($this)) . '.php'; + } + + /** + * Saves the generated code into files. + * @param CodeFile[] $files the code files to be saved + * @param array $answers + * @param string $results this parameter receives a value from this method indicating the log messages + * generated while saving the code files. + * @return boolean whether there is any error while saving the code files. + */ + public function save($files, $answers, &$results) + { + $lines = array('Generating code using template "' . $this->getTemplatePath() . '"...'); + $hasError = false; + foreach ($files as $file) { + $relativePath = $file->getRelativePath(); + if (isset($answers[$file->id]) && $file->operation !== CodeFile::OP_SKIP) { + $error = $file->save(); + if (is_string($error)) { + $hasError = true; + $lines[] = "generating $relativePath\n<span class=\"error\">$error</span>"; + } else { + $lines[] = $file->operation === CodeFile::OP_NEW ? " generated $relativePath" : " overwrote $relativePath"; + } + } else { + $lines[] = " skipped $relativePath"; + } + } + $lines[] = "done!\n"; + $results = implode("\n", $lines); + + return $hasError; + } + + /** + * @return string the root path of the template files that are currently being used. + * @throws InvalidConfigException if [[template]] is invalid + */ + public function getTemplatePath() + { + if (isset($this->templates[$this->template])) { + return $this->templates[$this->template]; + } else { + throw new InvalidConfigException("Unknown template: {$this->template}"); + } + } + + /** + * Generates code using the specified code template and parameters. + * Note that the code template will be used as a PHP file. + * @param string $template the code template file. This must be specified as a file path + * relative to [[templatePath]]. + * @param array $params list of parameters to be passed to the template file. + * @return string the generated code + */ + public function render($template, $params = array()) + { + $view = new View; + $params['generator'] = $this; + return $view->renderFile($this->getTemplatePath() . '/' . $template, $params, $this); + } + + /** + * Validates the template selection. + * This method validates whether the user selects an existing template + * and the template contains all required template files as specified in [[requiredTemplates()]]. + */ + public function validateTemplate() + { + $templates = $this->templates; + if (!isset($templates[$this->template])) { + $this->addError('template', 'Invalid template selection.'); + } else { + $templatePath = $this->templates[$this->template]; + foreach ($this->requiredTemplates() as $template) { + if (!is_file($templatePath . '/' . $template)) { + $this->addError('template', "Unable to find the required code template file '$template'."); + } + } + } + } + + /** + * An inline validator that checks if the attribute value refers to an existing class name. + * If the `extends` option is specified, it will also check if the class is a child class + * of the class represented by the `extends` option. + * @param string $attribute the attribute being validated + * @param array $params the validation options + */ + public function validateClass($attribute, $params) + { + $class = $this->$attribute; + try { + if (class_exists($class)) { + if (isset($params['extends'])) { + if (ltrim($class, '\\') !== ltrim($params['extends'], '\\') && !is_subclass_of($class, $params['extends'])) { + $this->addError($attribute, "'$class' must extend from {$params['extends']} or its child class."); + } + } + } else { + $this->addError($attribute, "Class '$class' does not exist or has syntax error."); + } + } catch (\Exception $e) { + $this->addError($attribute, "Class '$class' does not exist or has syntax error."); + } + } + + /** + * An inline validator that checks if the attribute value refers to a valid namespaced class name. + * The validator will check if the directory containing the new class file exist or not. + * @param string $attribute the attribute being validated + * @param array $params the validation options + */ + public function validateNewClass($attribute, $params) + { + $class = ltrim($this->$attribute, '\\'); + if (($pos = strrpos($class, '\\')) === false) { + $this->addError($attribute, "The class name must contain fully qualified namespace name."); + } else { + $ns = substr($class, 0, $pos); + $path = Yii::getAlias('@' . str_replace('\\', '/', $ns), false); + if ($path === false) { + $this->addError($attribute, "The class namespace is invalid: $ns"); + } elseif (!is_dir($path)) { + $this->addError($attribute, "Please make sure the directory containing this class exists: $path"); + } + } + } + + /** + * @param string $value the attribute to be validated + * @return boolean whether the value is a reserved PHP keyword. + */ + public function isReservedKeyword($value) + { + static $keywords = array( + '__class__', + '__dir__', + '__file__', + '__function__', + '__line__', + '__method__', + '__namespace__', + '__trait__', + 'abstract', + 'and', + 'array', + 'as', + 'break', + 'case', + 'catch', + 'callable', + 'cfunction', + 'class', + 'clone', + 'const', + 'continue', + 'declare', + 'default', + 'die', + 'do', + 'echo', + 'else', + 'elseif', + 'empty', + 'enddeclare', + 'endfor', + 'endforeach', + 'endif', + 'endswitch', + 'endwhile', + 'eval', + 'exception', + 'exit', + 'extends', + 'final', + 'finally', + 'for', + 'foreach', + 'function', + 'global', + 'goto', + 'if', + 'implements', + 'include', + 'include_once', + 'instanceof', + 'insteadof', + 'interface', + 'isset', + 'list', + 'namespace', + 'new', + 'old_function', + 'or', + 'parent', + 'php_user_filter', + 'print', + 'private', + 'protected', + 'public', + 'require', + 'require_once', + 'return', + 'static', + 'switch', + 'this', + 'throw', + 'trait', + 'try', + 'unset', + 'use', + 'var', + 'while', + 'xor', + ); + return in_array(strtolower($value), $keywords, true); + } +} diff --git a/framework/yii/gii/GiiAsset.php b/framework/yii/gii/GiiAsset.php new file mode 100644 index 0000000..a5fc0d0 --- /dev/null +++ b/framework/yii/gii/GiiAsset.php @@ -0,0 +1,44 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\gii; + +use yii\web\AssetBundle; + +/** + * This declares the asset files required by Gii. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class GiiAsset extends AssetBundle +{ + /** + * @inheritdoc + */ + public $sourcePath = '@yii/gii/assets'; + /** + * @inheritdoc + */ + public $css = array( + 'main.css', + ); + /** + * @inheritdoc + */ + public $js = array( + 'gii.js', + ); + /** + * @inheritdoc + */ + public $depends = array( + 'yii\web\YiiAsset', + 'yii\bootstrap\BootstrapAsset', + 'yii\bootstrap\BootstrapPluginAsset', + ); +} diff --git a/framework/yii/gii/Module.php b/framework/yii/gii/Module.php new file mode 100644 index 0000000..f82e3dd --- /dev/null +++ b/framework/yii/gii/Module.php @@ -0,0 +1,157 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\gii; + +use Yii; +use yii\web\HttpException; + +/** + * This is the main module class for the Gii module. + * + * To use Gii, include it as a module in the application configuration like the following: + * + * ~~~ + * return array( + * ...... + * 'modules' => array( + * 'gii' => array( + * 'class' => 'yii\gii\Module', + * ), + * ), + * ) + * ~~~ + * + * Because Gii generates new code files on the server, you should only use it on your own + * development machine. To prevent other people from using this module, by default, Gii + * can only be accessed by localhost. You may configure its [[allowedIPs]] property if + * you want to make it accessible on other machines. + * + * With the above configuration, you will be able to access GiiModule in your browser using + * the URL `http://localhost/path/to/index.php?r=gii` + * + * If your application enables [[UrlManager::enablePrettyUrl|pretty URLs]] and you have defined + * custom URL rules or enabled [[UrlManager::enableStrictParsing], you may need to add + * the following URL rules at the beginning of your URL rule set in your application configuration + * in order to access Gii: + * + * ~~~ + * 'rules'=>array( + * 'gii' => 'gii', + * 'gii/<controller>' => 'gii/<controller>', + * 'gii/<controller>/<action>' => 'gii/<controller>/<action>', + * ... + * ), + * ~~~ + * + * You can then access Gii via URL: `http://localhost/path/to/index.php/gii` + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Module extends \yii\base\Module +{ + /** + * @inheritdoc + */ + public $controllerNamespace = 'yii\gii\controllers'; + /** + * @var array the list of IPs that are allowed to access this module. + * Each array element represents a single IP filter which can be either an IP address + * or an address with wildcard (e.g. 192.168.0.*) to represent a network segment. + * The default value is `array('127.0.0.1', '::1')`, which means the module can only be accessed + * by localhost. + */ + public $allowedIPs = array('127.0.0.1', '::1'); + /** + * @var array|Generator[] a list of generator configurations or instances. The array keys + * are the generator IDs (e.g. "crud"), and the array elements are the corresponding generator + * configurations or the instances. + * + * After the module is initialized, this property will become an array of generator instances + * which are created based on the configurations previously taken by this property. + * + * Newly assigned generators will be merged with the [[coreGenerators()|core ones]], and the former + * takes precedence in case when they have the same generator ID. + */ + public $generators = array(); + /** + * @var integer the permission to be set for newly generated code files. + * This value will be used by PHP chmod function. + * Defaults to 0666, meaning the file is read-writable by all users. + */ + public $newFileMode = 0666; + /** + * @var integer the permission to be set for newly generated 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. + */ + public $newDirMode = 0777; + + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + foreach (array_merge($this->coreGenerators(), $this->generators) as $id => $config) { + $this->generators[$id] = Yii::createObject($config); + } + } + + /** + * @inheritdoc + */ + public function beforeAction($action) + { + if ($this->checkAccess()) { + return parent::beforeAction($action); + } else { + throw new HttpException(403, 'You are not allowed to access this page.'); + } + } + + /** + * @return boolean whether the module can be accessed by the current user + */ + protected function checkAccess() + { + $ip = Yii::$app->getRequest()->getUserIP(); + foreach ($this->allowedIPs as $filter) { + if ($filter === '*' || $filter === $ip || (($pos = strpos($filter, '*')) !== false && !strncmp($ip, $filter, $pos))) { + return true; + } + } + return false; + } + + /** + * Returns the list of the core code generator configurations. + * @return array the list of the core code generator configurations. + */ + protected function coreGenerators() + { + return array( + 'model' => array( + 'class' => 'yii\gii\generators\model\Generator', + ), + 'crud' => array( + 'class' => 'yii\gii\generators\crud\Generator', + ), + 'controller' => array( + 'class' => 'yii\gii\generators\controller\Generator', + ), + 'form' => array( + 'class' => 'yii\gii\generators\form\Generator', + ), + 'module' => array( + 'class' => 'yii\gii\generators\module\Generator', + ), + ); + } +} diff --git a/framework/yii/gii/assets/gii.js b/framework/yii/gii/assets/gii.js new file mode 100644 index 0000000..b581d3b --- /dev/null +++ b/framework/yii/gii/assets/gii.js @@ -0,0 +1,99 @@ +yii.gii = (function ($) { + var isActive = $('.default-view').length > 0; + + var initHintBlocks = function () { + $('.hint-block').each(function () { + var $hint = $(this); + $hint.parent().find('label').addClass('help').popover({ + html: true, + trigger: 'hover', + placement: 'right', + content: $hint.html() + }); + }); + }; + + var initStickyInputs = function () { + $('.sticky:not(.error) input[type="text"],select,textarea').each(function () { + var value; + if (this.tagName === 'SELECT') { + value = this.options[this.selectedIndex].text; + } else if (this.tagName === 'TEXTAREA') { + value = $(this).html(); + } else { + value = $(this).val(); + } + if (value === '') { + value = '[empty]'; + } + $(this).before('<div class="sticky-value">' + value + '</div>').hide(); + }); + $('.sticky-value').on('click', function () { + $(this).hide(); + $(this).next().show().get(0).focus(); + }); + }; + + var initPreviewDiffLinks = function () { + $('.preview-code,.diff-code').on('click', function () { + var $modal = $('#preview-modal'); + var $link = $(this); + $modal.find('.modal-title').text($link.data('title')); + $modal.find('.modal-body').html('Loading ...'); + $modal.modal('show'); + $.ajax({ + type: 'POST', + cache: false, + url: $link.prop('href'), + data: $('.default-view form').serializeArray(), + success: function (data) { + $modal.find('.modal-body').html(data); + $modal.find('.content').css('max-height', ($(window).height() - 200) + 'px'); + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + $modal.find('.modal-body').html('<div class="error">' + XMLHttpRequest.responseText + '</div>'); + } + }); + return false; + }); + }; + + var initConfirmationCheckboxes = function () { + var $checkAll = $('#check-all'); + $checkAll.click(function () { + $('.default-view-files table .check input').prop('checked', this.checked); + }); + $('.default-view-files table .check input').click(function () { + $checkAll.prop('checked', !$('.default-view-files table .check input:not(:checked)').length); + }); + $checkAll.prop('checked', !$('.default-view-files table .check input:not(:checked)').length); + }; + + return { + init: function () { + initHintBlocks(); + initStickyInputs(); + initPreviewDiffLinks(); + initConfirmationCheckboxes(); + + // model generator: hide class name input when table name input contains * + $('#model-generator #generator-tablename').on('change', function () { + $('#model-generator .field-generator-modelclass').toggle($(this).val().indexOf('*') == -1); + }).change(); + + // hide Generate button if any input is changed + $('.default-view .form-group input,select,textarea').change(function () { + $('.default-view-results,.default-view-files').hide(); + $('.default-view button[name="generate"]').hide(); + }); + + $('.module-form #generator-moduleclass').change(function () { + var value = $(this).val().match(/(\w+)\\\w+$/); + var $idInput = $('#generator-moduleid'); + if (value && value[1] && $idInput.val() == '') { + $idInput.val(value[1]); + } + }); + } + }; +})(jQuery); diff --git a/framework/yii/gii/assets/logo.png b/framework/yii/gii/assets/logo.png new file mode 100644 index 0000000..e48b5aa Binary files /dev/null and b/framework/yii/gii/assets/logo.png differ diff --git a/framework/yii/gii/assets/main.css b/framework/yii/gii/assets/main.css new file mode 100644 index 0000000..8efc56c --- /dev/null +++ b/framework/yii/gii/assets/main.css @@ -0,0 +1,203 @@ +body { + padding-top: 70px; +} + +.footer { + border-top: 1px solid #ddd; + margin-top: 30px; + padding: 15px 0 30px; +} + +.jumbotron { + text-align: center; + background-color: transparent; +} + +.jumbotron .btn { + font-size: 21px; + padding: 14px 24px; +} + +.navbar-brand { + padding: 0; + margin: 0; +} + +.default-index .generator { + min-height: 200px; + margin-bottom: 20px; +} + +.list-group .glyphicon { + float: right; +} + +.popover { + max-width: 400px; + width: 400px; +} + +.hint-block { + display: none; +} + +.default-view .sticky-value { + padding: 6px 12px; + background: lightyellow; + white-space: pre; + word-wrap: break-word; +} + +.default-view .form-group label.help { + border-bottom: 1px dashed #888; + cursor: help; +} + +.default-view .modal-dialog { + width: 800px; +} + +.default-view .modal-dialog .error { + color: #d9534f; +} + +.default-view .modal-dialog .content { + background: #fafafa; + border-left: #eee 5px solid; + padding: 5px 10px; + overflow: auto; +} + +.default-view .modal-dialog code { + background: transparent; +} + +.default-view-files table .action { + width: 100px; +} + +.default-view-files table .check { + width: 25px; + text-align: center; +} + +.default-view-results pre { + overflow: auto; + background-color: #333; + max-height: 300px; + color: white; + padding: 10px; + border-radius: 0; + white-space: nowrap; +} + +.default-view-results pre .error { + background: #FFE0E1; + color: black; + padding: 1px; +} + +.default-view-results .alert pre { + background: white; +} + +.default-diff pre { + padding: 0; + margin: 0; + background: transparent; + border: none; +} + +.default-diff pre del { + background: pink; +} + +.default-diff pre ins { + background: lightgreen; + text-decoration: none; +} + + +.Differences { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + empty-cells: show; +} + +.Differences thead { + display: none; +} + +.Differences tbody th { + text-align: right; + background: #FAFAFA; + padding: 1px 2px; + border-right: 1px solid #eee; + vertical-align: top; + font-size: 13px; + font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; + font-weight: normal; + color: #999; + width: 5px; +} + +.Differences td { + padding: 1px 2px; + font-size: 13px; + font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; +} + +.DifferencesSideBySide .ChangeInsert td.Left { + background: #dfd; +} + +.DifferencesSideBySide .ChangeInsert td.Right { + background: #cfc; +} + +.DifferencesSideBySide .ChangeDelete td.Left { + background: #f88; +} + +.DifferencesSideBySide .ChangeDelete td.Right { + background: #faa; +} + +.DifferencesSideBySide .ChangeReplace .Left { + background: #fe9; +} + +.DifferencesSideBySide .ChangeReplace .Right { + background: #fd8; +} + +.Differences ins, .Differences del { + text-decoration: none; +} + +.DifferencesSideBySide .ChangeReplace ins, .DifferencesSideBySide .ChangeReplace del { + background: #fc0; +} + +.Differences .Skipped { + background: #f7f7f7; +} + +.DifferencesInline .ChangeReplace .Left, +.DifferencesInline .ChangeDelete .Left { + background: #fdd; +} + +.DifferencesInline .ChangeReplace .Right, +.DifferencesInline .ChangeInsert .Right { + background: #dfd; +} + +.DifferencesInline .ChangeReplace ins { + background: #9e9; +} + +.DifferencesInline .ChangeReplace del { + background: #e99; +} diff --git a/framework/yii/gii/components/ActiveField.php b/framework/yii/gii/components/ActiveField.php new file mode 100644 index 0000000..8bb67a9 --- /dev/null +++ b/framework/yii/gii/components/ActiveField.php @@ -0,0 +1,44 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\gii\components; + +use yii\gii\Generator; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ActiveField extends \yii\widgets\ActiveField +{ + /** + * @var Generator + */ + public $model; + + public function init() + { + $stickyAttributes = $this->model->stickyAttributes(); + if (in_array($this->attribute, $stickyAttributes)) { + $this->sticky(); + } + $hints = $this->model->hints(); + if (isset($hints[$this->attribute])) { + $this->hint($hints[$this->attribute]); + } + } + + /** + * Makes filed remember its value between page reloads + * @return static the field object itself + */ + public function sticky() + { + $this->options['class'] .= ' sticky'; + return $this; + } +} diff --git a/framework/yii/gii/controllers/DefaultController.php b/framework/yii/gii/controllers/DefaultController.php new file mode 100644 index 0000000..305ef35 --- /dev/null +++ b/framework/yii/gii/controllers/DefaultController.php @@ -0,0 +1,152 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\gii\controllers; + +use Yii; +use yii\web\Controller; +use yii\web\HttpException; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DefaultController extends Controller +{ + public $layout = 'generator'; + /** + * @var \yii\gii\Module + */ + public $module; + /** + * @var \yii\gii\Generator + */ + public $generator; + + public function actionIndex() + { + $this->layout = 'main'; + return $this->render('index'); + } + + public function actionView($id) + { + $generator = $this->loadGenerator($id); + $params = array('generator' => $generator, 'id' => $id); + if (isset($_POST['preview']) || isset($_POST['generate'])) { + if ($generator->validate()) { + $generator->saveStickyAttributes(); + $files = $generator->generate(); + if (isset($_POST['generate']) && !empty($_POST['answers'])) { + $params['hasError'] = $generator->save($files, (array)$_POST['answers'], $results); + $params['results'] = $results; + } else { + $params['files'] = $files; + $params['answers'] = isset($_POST['answers']) ? $_POST['answers'] : null; + } + } + } + + return $this->render('view', $params); + } + + public function actionPreview($id, $file) + { + $generator = $this->loadGenerator($id); + if ($generator->validate()) { + foreach ($generator->generate() as $f) { + if ($f->id === $file) { + $content = $f->preview(); + if ($content !== false) { + return '<div class="content">' . $content . '</content>'; + } else { + return '<div class="error">Preview is not available for this file type.</div>'; + } + } + } + } + throw new HttpException(404, "Code file not found: $file"); + } + + public function actionDiff($id, $file) + { + $generator = $this->loadGenerator($id); + if ($generator->validate()) { + foreach ($generator->generate() as $f) { + if ($f->id === $file) { + return $this->renderPartial('diff', array( + 'diff' => $f->diff(), + )); + } + } + } + throw new HttpException(404, "Code file not found: $file"); + } + + /** + * Runs an action defined in the generator. + * Given an action named "xyz", the method "actionXyz()" in the generator will be called. + * If the method does not exist, a 400 HTTP exception will be thrown. + * @param string $id the ID of the generator + * @param string $name the action name + * @return mixed the result of the action. + * @throws HttpException if the action method does not exist. + */ + public function actionAction($id, $name) + { + $generator = $this->loadGenerator($id); + $method = 'action' . $name; + if (method_exists($generator, $method)) { + return $generator->$method(); + } else { + throw new HttpException(400, "Unknown generator action: $name"); + } + } + + public function createUrl($route, $params = array()) + { + if (!isset($params['id']) && $this->generator !== null) { + foreach ($this->module->generators as $id => $generator) { + if ($generator === $this->generator) { + $params['id'] = $id; + break; + } + } + } + return parent::createUrl($route, $params); + } + + public function createActionUrl($name, $params = array()) + { + foreach ($this->module->generators as $id => $generator) { + if ($generator === $this->generator) { + $params['id'] = $id; + break; + } + } + $params['name'] = $name; + return parent::createUrl('action', $params); + } + + /** + * Loads the generator with the specified ID. + * @param string $id the ID of the generator to be loaded. + * @return \yii\gii\Generator the loaded generator + * @throws \yii\web\HttpException + */ + protected function loadGenerator($id) + { + if (isset($this->module->generators[$id])) { + $this->generator = $this->module->generators[$id]; + $this->generator->loadStickyAttributes(); + $this->generator->load($_POST); + return $this->generator; + } else { + throw new HttpException(404, "Code generator not found: $id"); + } + } +} diff --git a/framework/yii/gii/generators/controller/Generator.php b/framework/yii/gii/generators/controller/Generator.php new file mode 100644 index 0000000..b7c4095 --- /dev/null +++ b/framework/yii/gii/generators/controller/Generator.php @@ -0,0 +1,227 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\gii\generators\controller; + +use Yii; +use yii\gii\CodeFile; +use yii\helpers\Html; +use yii\helpers\Inflector; + +/** + * This generator will generate a controller and one or a few action view files. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Generator extends \yii\gii\Generator +{ + /** + * @var string the controller ID + */ + public $controller; + /** + * @var string the base class of the controller + */ + public $baseClass = 'yii\web\Controller'; + /** + * @var string the namespace of the controller class + */ + public $ns = 'app\controllers'; + /** + * @var string list of action IDs separated by commas or spaces + */ + public $actions = 'index'; + + /** + * @inheritdoc + */ + public function getName() + { + return 'Controller Generator'; + } + + /** + * @inheritdoc + */ + public function getDescription() + { + return 'This generator helps you to quickly generate a new controller class, + one or several controller actions and their corresponding views.'; + } + + /** + * @inheritdoc + */ + public function rules() + { + return array_merge(parent::rules(), array( + array('controller, actions, baseClass, ns', 'filter', 'filter' => 'trim'), + array('controller, baseClass', 'required'), + array('controller', 'match', 'pattern' => '/^[a-z\\-\\/]*$/', 'message' => 'Only a-z, dashes (-) and slashes (/) are allowed.'), + array('actions', 'match', 'pattern' => '/^[a-z\\-,\\s]*$/', 'message' => 'Only a-z, dashes (-), spaces and commas are allowed.'), + array('baseClass', 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'), + array('ns', 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'), + )); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return array( + 'baseClass' => 'Base Class', + 'controller' => 'Controller ID', + 'actions' => 'Action IDs', + 'ns' => 'Controller Namespace', + ); + } + + /** + * @inheritdoc + */ + public function requiredTemplates() + { + return array( + 'controller.php', + 'view.php', + ); + } + + /** + * @inheritdoc + */ + public function stickyAttributes() + { + return array('ns', 'baseClass'); + } + + /** + * @inheritdoc + */ + public function hints() + { + return array( + 'controller' => 'Controller ID should be in lower case and may contain module ID(s) separated by slashes. For example: + <ul> + <li><code>order</code> generates <code>OrderController.php</code></li> + <li><code>order-item</code> generates <code>OrderItemController.php</code></li> + <li><code>admin/user</code> generates <code>UserController.php</code> within the <code>admin</code> module.</li> + </ul>', + 'actions' => 'Provide one or multiple action IDs to generate empty action method(s) in the controller. Separate multiple action IDs with commas or spaces. + Action IDs should be in lower case. For example: + <ul> + <li><code>index</code> generates <code>actionIndex()</code></li> + <li><code>create-order</code> generates <code>actionCreateOrder()</code></li> + </ul>', + 'ns' => 'This is the namespace that the new controller class will use.', + 'baseClass' => 'This is the class that the new controller class will extend from. Please make sure the class exists and can be autoloaded.', + ); + } + + /** + * @inheritdoc + */ + public function successMessage() + { + $actions = $this->getActionIDs(); + if (in_array('index', $actions)) { + $route = $this->controller . '/index'; + } else { + $route = $this->controller . '/' . reset($actions); + } + $link = Html::a('try it now', Yii::$app->getUrlManager()->createUrl($route), array('target' => '_blank')); + return "The controller has been generated successfully. You may $link."; + } + + /** + * @inheritdoc + */ + public function generate() + { + $files = array(); + + $files[] = new CodeFile( + $this->getControllerFile(), + $this->render('controller.php') + ); + + foreach ($this->getActionIDs() as $action) { + $files[] = new CodeFile( + $this->getViewFile($action), + $this->render('view.php', array('action' => $action)) + ); + } + + return $files; + } + + /** + * Normalizes [[actions]] into an array of action IDs. + * @return array an array of action IDs entered by the user + */ + public function getActionIDs() + { + $actions = array_unique(preg_split('/[\s,]+/', $this->actions, -1, PREG_SPLIT_NO_EMPTY)); + sort($actions); + return $actions; + } + + /** + * @return string the controller class name without the namespace part. + */ + public function getControllerClass() + { + return Inflector::id2camel($this->getControllerID()) . 'Controller'; + } + + /** + * @return string the controller ID (without the module ID prefix) + */ + public function getControllerID() + { + if (($pos = strrpos($this->controller, '/')) !== false) { + return substr($this->controller, $pos + 1); + } else { + return $this->controller; + } + } + + /** + * @return \yii\base\Module the module that the new controller belongs to + */ + public function getModule() + { + if (($pos = strrpos($this->controller, '/')) !== false) { + $id = substr($this->controller, 0, $pos); + if (($module = Yii::$app->getModule($id)) !== null) { + return $module; + } + } + return Yii::$app; + } + + /** + * @return string the controller class file path + */ + public function getControllerFile() + { + $module = $this->getModule(); + return $module->getControllerPath() . '/' . $this->getControllerClass() . '.php'; + } + + /** + * @param string $action the action ID + * @return string the action view file path + */ + public function getViewFile($action) + { + $module = $this->getModule(); + return $module->getViewPath() . '/' . $this->getControllerID() . '/' . $action . '.php'; + } +} diff --git a/framework/yii/gii/generators/controller/form.php b/framework/yii/gii/generators/controller/form.php new file mode 100644 index 0000000..e4d2947 --- /dev/null +++ b/framework/yii/gii/generators/controller/form.php @@ -0,0 +1,10 @@ +<?php +/** + * @var yii\base\View $this + * @var yii\widgets\ActiveForm $form + * @var yii\gii\generators\controller\Generator $generator + */ +echo $form->field($generator, 'controller'); +echo $form->field($generator, 'actions'); +echo $form->field($generator, 'ns'); +echo $form->field($generator, 'baseClass'); diff --git a/framework/yii/gii/generators/controller/templates/controller.php b/framework/yii/gii/generators/controller/templates/controller.php new file mode 100644 index 0000000..3829d54 --- /dev/null +++ b/framework/yii/gii/generators/controller/templates/controller.php @@ -0,0 +1,28 @@ +<?php + +use yii\helpers\Inflector; + +/** + * This is the template for generating a controller class file. + * + * @var yii\base\View $this + * @var yii\gii\generators\controller\Generator $generator + */ + +echo "<?php\n"; +?> + +<?php if (!empty($generator->ns)): ?> +namespace <?php echo $generator->ns; ?>; +<?php endif; ?> + +class <?php echo $generator->getControllerClass(); ?> extends <?php echo '\\' . trim($generator->baseClass, '\\') . "\n"; ?> +{ +<?php foreach($generator->getActionIDs() as $action): ?> + public function action<?php echo Inflector::id2camel($action); ?>() + { + return $this->render('<?php echo $action; ?>'); + } + +<?php endforeach; ?> +} diff --git a/framework/yii/gii/generators/controller/templates/view.php b/framework/yii/gii/generators/controller/templates/view.php new file mode 100644 index 0000000..4b75d7a --- /dev/null +++ b/framework/yii/gii/generators/controller/templates/view.php @@ -0,0 +1,22 @@ +<?php +/** + * This is the template for generating an action view file. + * + * @var yii\base\View $this + * @var yii\gii\generators\controller\Generator $generator + * @var string $action the action ID + */ + +echo "<?php\n"; +?> +/** + * @var yii\base\View $this + */ +<?php echo "?>"; ?> + +<h1><?php echo $generator->getControllerID() . '/' . $action; ?></h1> + +<p> + You may change the content of this page by modifying + the file <code><?php echo '<?php'; ?> echo __FILE__; ?></code>. +</p> diff --git a/framework/yii/gii/generators/crud/Generator.php b/framework/yii/gii/generators/crud/Generator.php new file mode 100644 index 0000000..9bce7ff --- /dev/null +++ b/framework/yii/gii/generators/crud/Generator.php @@ -0,0 +1,389 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\gii\generators\crud; + +use Yii; +use yii\base\Model; +use yii\db\ActiveRecord; +use yii\db\Schema; +use yii\gii\CodeFile; +use yii\helpers\Inflector; +use yii\web\Controller; + +/** + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Generator extends \yii\gii\Generator +{ + public $modelClass; + public $moduleID; + public $controllerClass; + public $baseControllerClass = 'yii\web\Controller'; + public $indexWidgetType = 'grid'; + public $searchModelClass; + + public function getName() + { + return 'CRUD Generator'; + } + + public function getDescription() + { + return 'This generator generates a controller and views that implement CRUD (Create, Read, Update, Delete) + operations for the specified data model.'; + } + + public function rules() + { + return array_merge(parent::rules(), array( + array('moduleID, controllerClass, modelClass, searchModelClass, baseControllerClass', 'filter', 'filter' => 'trim'), + array('modelClass, searchModelClass, controllerClass, baseControllerClass, indexWidgetType', 'required'), + array('modelClass, controllerClass, baseControllerClass, searchModelClass', 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'), + array('modelClass', 'validateClass', 'params' => array('extends' => ActiveRecord::className())), + array('baseControllerClass', 'validateClass', 'params' => array('extends' => Controller::className())), + array('controllerClass', 'match', 'pattern' => '/Controller$/', 'message' => 'Controller class name must be suffixed with "Controller".'), + array('controllerClass, searchModelClass', 'validateNewClass'), + array('indexWidgetType', 'in', 'range' => array('grid', 'list')), + array('modelClass', 'validateModelClass'), + array('moduleID', 'validateModuleID'), + )); + } + + public function attributeLabels() + { + return array_merge(parent::attributeLabels(), array( + 'modelClass' => 'Model Class', + 'moduleID' => 'Module ID', + 'controllerClass' => 'Controller Class', + 'baseControllerClass' => 'Base Controller Class', + 'indexWidgetType' => 'Widget Used in Index Page', + 'searchModelClass' => 'Search Model Class', + )); + } + + /** + * @inheritdoc + */ + public function hints() + { + return array( + 'modelClass' => 'This is the ActiveRecord class associated with the table that CRUD will be built upon. + You should provide a fully qualified class name, e.g., <code>app\models\Post</code>.', + 'controllerClass' => 'This is the name of the controller class to be generated. You should + provide a fully qualified namespaced class, .e.g, <code>app\controllers\PostController</code>.', + 'baseControllerClass' => 'This is the class that the new CRUD controller class will extend from. + You should provide a fully qualified class name, e.g., <code>yii\web\Controller</code>.', + 'moduleID' => 'This is the ID of the module that the generated controller will belong to. + If not set, it means the controller will belong to the application.', + 'indexWidgetType' => 'This is the widget type to be used in the index page to display list of the models. + You may choose either <code>GridView</code> or <code>ListView</code>', + 'searchModelClass' => 'This is the class representing the data being collecting in the search form. + A fully qualified namespaced class name is required, e.g., <code>app\models\search\PostSearch</code>.', + ); + } + + public function requiredTemplates() + { + return array( + 'controller.php', + ); + } + + /** + * @inheritdoc + */ + public function stickyAttributes() + { + return array('baseControllerClass', 'moduleID', 'indexWidgetType'); + } + + public function validateModelClass() + { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $pk = $class::primaryKey(); + if (empty($pk)) { + $this->addError('modelClass', "The table associated with $class must have primary key(s)."); + } + } + + public function validateModuleID() + { + if (!empty($this->moduleID)) { + $module = Yii::$app->getModule($this->moduleID); + if ($module === null) { + $this->addError('moduleID', "Module '{$this->moduleID}' does not exist."); + } + } + } + + /** + * @inheritdoc + */ + public function generate() + { + $controllerFile = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->controllerClass, '\\')) . '.php'); + $searchModel = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->searchModelClass, '\\') . '.php')); + $files = array( + new CodeFile($controllerFile, $this->render('controller.php')), + new CodeFile($searchModel, $this->render('search.php')), + ); + + $viewPath = $this->getViewPath(); + $templatePath = $this->getTemplatePath() . '/views'; + foreach (scandir($templatePath) as $file) { + if (is_file($templatePath . '/' . $file) && pathinfo($file, PATHINFO_EXTENSION) === 'php') { + $files[] = new CodeFile("$viewPath/$file", $this->render("views/$file")); + } + } + + + return $files; + } + + /** + * @return string the controller ID (without the module ID prefix) + */ + public function getControllerID() + { + $pos = strrpos($this->controllerClass, '\\'); + $class = substr(substr($this->controllerClass, $pos + 1), 0, -10); + return Inflector::camel2id($class); + } + + /** + * @return string the action view file path + */ + public function getViewPath() + { + $module = empty($this->moduleID) ? Yii::$app : Yii::$app->getModule($this->moduleID); + return $module->getViewPath() . '/' . $this->getControllerID() ; + } + + public function getNameAttribute() + { + /** @var \yii\db\ActiveRecord $class */ + $class = $this->modelClass; + foreach ($class::getTableSchema()->columnNames as $name) { + if (!strcasecmp($name, 'name') || !strcasecmp($name, 'title')) { + return $name; + } + } + $pk = $class::primaryKey(); + return $pk[0]; + } + + /** + * @param string $attribute + * @return string + */ + public function generateActiveField($attribute) + { + $tableSchema = $this->getTableSchema(); + if (!isset($tableSchema->columns[$attribute])) { + return "\$form->field(\$model, '$attribute');"; + } + $column = $tableSchema->columns[$attribute]; + if ($column->phpType === 'boolean') { + return "\$form->field(\$model, '$attribute')->checkbox();"; + } elseif ($column->type === 'text') { + return "\$form->field(\$model, '$attribute')->textarea(array('rows' => 6));"; + } else { + if (preg_match('/^(password|pass|passwd|passcode)$/i', $column->name)) { + $input = 'passwordInput'; + } else { + $input = 'textInput'; + } + if ($column->phpType !== 'string' || $column->size === null) { + return "\$form->field(\$model, '$attribute')->$input();"; + } else { + return "\$form->field(\$model, '$attribute')->$input(array('maxlength' => $column->size));"; + } + } + } + + /** + * @param string $attribute + * @return string + */ + public function generateActiveSearchField($attribute) + { + $tableSchema = $this->getTableSchema(); + $column = $tableSchema->columns[$attribute]; + if ($column->phpType === 'boolean') { + return "\$form->field(\$model, '$attribute')->checkbox();"; + } else { + return "\$form->field(\$model, '$attribute');"; + } + } + + /** + * @param \yii\db\ColumnSchema $column + * @return string + */ + public function generateColumnFormat($column) + { + if ($column->phpType === 'boolean') { + return 'boolean'; + } elseif ($column->type === 'text') { + return 'ntext'; + } elseif (stripos($column->name, 'time') !== false && $column->phpType === 'integer') { + return 'datetime'; + } elseif (stripos($column->name, 'email') !== false) { + return 'email'; + } elseif (stripos($column->name, 'url') !== false) { + return 'url'; + } else { + return 'text'; + } + } + + /** + * Generates validation rules for the search model. + * @return array the generated validation rules + */ + public function generateSearchRules() + { + $table = $this->getTableSchema(); + $types = array(); + foreach ($table->columns as $column) { + switch ($column->type) { + case Schema::TYPE_SMALLINT: + case Schema::TYPE_INTEGER: + case Schema::TYPE_BIGINT: + $types['integer'][] = $column->name; + break; + case Schema::TYPE_BOOLEAN: + $types['boolean'][] = $column->name; + break; + case Schema::TYPE_FLOAT: + case Schema::TYPE_DECIMAL: + case Schema::TYPE_MONEY: + $types['number'][] = $column->name; + break; + case Schema::TYPE_DATE: + case Schema::TYPE_TIME: + case Schema::TYPE_DATETIME: + case Schema::TYPE_TIMESTAMP: + default: + $types['safe'][] = $column->name; + break; + } + } + + $rules = array(); + foreach ($types as $type => $columns) { + $rules[] = "array('" . implode(', ', $columns) . "', '$type')"; + } + + return $rules; + } + + public function getSearchAttributes() + { + return $this->getTableSchema()->getColumnNames(); + } + + /** + * Generates the attribute labels for the search model. + * @return array the generated attribute labels (name => label) + */ + public function generateSearchLabels() + { + $table = $this->getTableSchema(); + $labels = array(); + foreach ($table->columns as $column) { + if (!strcasecmp($column->name, 'id')) { + $labels[$column->name] = 'ID'; + } else { + $label = Inflector::camel2words($column->name); + if (strcasecmp(substr($label, -3), ' id') === 0) { + $label = substr($label, 0, -3) . ' ID'; + } + $labels[$column->name] = $label; + } + } + return $labels; + } + + public function generateSearchConditions() + { + $table = $this->getTableSchema(); + $conditions = array(); + foreach ($table->columns as $column) { + switch ($column->type) { + case Schema::TYPE_SMALLINT: + case Schema::TYPE_INTEGER: + case Schema::TYPE_BIGINT: + case Schema::TYPE_BOOLEAN: + case Schema::TYPE_FLOAT: + case Schema::TYPE_DECIMAL: + case Schema::TYPE_MONEY: + case Schema::TYPE_DATE: + case Schema::TYPE_TIME: + case Schema::TYPE_DATETIME: + case Schema::TYPE_TIMESTAMP: + $conditions[] = "\$this->addCondition(\$query, '{$column->name}');"; + break; + default: + $conditions[] = "\$this->addCondition(\$query, '{$column->name}', true);"; + break; + } + } + + return $conditions; + } + + public function generateUrlParams() + { + $pks = $this->getTableSchema()->primaryKey; + if (count($pks) === 1) { + return "'id' => \$model->{$pks[0]}"; + } else { + $params = array(); + foreach ($pks as $pk) { + $params[] = "'$pk' => \$model->$pk"; + } + return implode(', ', $params); + } + } + + public function generateActionParams() + { + $pks = $this->getTableSchema()->primaryKey; + if (count($pks) === 1) { + return '$id'; + } else { + return '$' . implode(', $', $pks); + } + } + + public function generateActionParamComments() + { + $table = $this->getTableSchema(); + $pks = $table->primaryKey; + if (count($pks) === 1) { + return array('@param ' . $table->columns[$pks[0]]->phpType . ' $id'); + } else { + $params = array(); + foreach ($pks as $pk) { + $params[] = '@param ' . $table->columns[$pk]->phpType . ' $' . $pk; + } + return $params; + } + } + + public function getTableSchema() + { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + return $class::getTableSchema(); + } +} diff --git a/framework/yii/gii/generators/crud/form.php b/framework/yii/gii/generators/crud/form.php new file mode 100644 index 0000000..829b8a3 --- /dev/null +++ b/framework/yii/gii/generators/crud/form.php @@ -0,0 +1,16 @@ +<?php +/** + * @var yii\base\View $this + * @var yii\widgets\ActiveForm $form + * @var yii\gii\generators\crud\Generator $generator + */ + +echo $form->field($generator, 'modelClass'); +echo $form->field($generator, 'searchModelClass'); +echo $form->field($generator, 'controllerClass'); +echo $form->field($generator, 'baseControllerClass'); +echo $form->field($generator, 'moduleID'); +echo $form->field($generator, 'indexWidgetType')->dropDownList(array( + 'grid' => 'GridView', + 'list' => 'ListView', +)); diff --git a/framework/yii/gii/generators/crud/templates/controller.php b/framework/yii/gii/generators/crud/templates/controller.php new file mode 100644 index 0000000..f53f819 --- /dev/null +++ b/framework/yii/gii/generators/crud/templates/controller.php @@ -0,0 +1,152 @@ +<?php + +use yii\helpers\StringHelper; + +/** + * This is the template for generating a CRUD controller class file. + * + * @var yii\base\View $this + * @var yii\gii\generators\crud\Generator $generator + */ + +$controllerClass = StringHelper::basename($generator->controllerClass); +$modelClass = StringHelper::basename($generator->modelClass); +$searchModelClass = StringHelper::basename($generator->searchModelClass); + +$pks = $generator->getTableSchema()->primaryKey; +$urlParams = $generator->generateUrlParams(); +$actionParams = $generator->generateActionParams(); +$actionParamComments = $generator->generateActionParamComments(); + +echo "<?php\n"; +?> + +namespace <?php echo StringHelper::dirname(ltrim($generator->controllerClass, '\\')); ?>; + +use <?php echo ltrim($generator->modelClass, '\\'); ?>; +use <?php echo ltrim($generator->searchModelClass, '\\'); ?>; +use yii\data\ActiveDataProvider; +use <?php echo ltrim($generator->baseControllerClass, '\\'); ?>; +use yii\web\HttpException; +use yii\web\VerbFilter; + +/** + * <?php echo $controllerClass; ?> implements the CRUD actions for <?php echo $modelClass; ?> model. + */ +class <?php echo $controllerClass; ?> extends <?php echo StringHelper::basename($generator->baseControllerClass) . "\n"; ?> +{ + public function behaviors() + { + return array( + 'verbs' => array( + 'class' => VerbFilter::className(), + 'actions' => array( + 'delete' => array('post'), + ), + ), + ); + } + + /** + * Lists all <?php echo $modelClass; ?> models. + * @return mixed + */ + public function actionIndex() + { + $searchModel = new <?php echo $searchModelClass; ?>; + $dataProvider = $searchModel->search($_GET); + + return $this->render('index', array( + 'dataProvider' => $dataProvider, + 'searchModel' => $searchModel, + )); + } + + /** + * Displays a single <?php echo $modelClass; ?> model. + * <?php echo implode("\n\t * ", $actionParamComments) . "\n"; ?> + * @return mixed + */ + public function actionView(<?php echo $actionParams; ?>) + { + return $this->render('view', array( + 'model' => $this->findModel(<?php echo $actionParams; ?>), + )); + } + + /** + * Creates a new <?php echo $modelClass; ?> model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return mixed + */ + public function actionCreate() + { + $model = new <?php echo $modelClass; ?>; + + if ($model->load($_POST) && $model->save()) { + return $this->redirect(array('view', <?php echo $urlParams; ?>)); + } else { + return $this->render('create', array( + 'model' => $model, + )); + } + } + + /** + * Updates an existing <?php echo $modelClass; ?> model. + * If update is successful, the browser will be redirected to the 'view' page. + * <?php echo implode("\n\t * ", $actionParamComments) . "\n"; ?> + * @return mixed + */ + public function actionUpdate(<?php echo $actionParams; ?>) + { + $model = $this->findModel(<?php echo $actionParams; ?>); + + if ($model->load($_POST) && $model->save()) { + return $this->redirect(array('view', <?php echo $urlParams; ?>)); + } else { + return $this->render('update', array( + 'model' => $model, + )); + } + } + + /** + * Deletes an existing <?php echo $modelClass; ?> model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * <?php echo implode("\n\t * ", $actionParamComments) . "\n"; ?> + * @return mixed + */ + public function actionDelete(<?php echo $actionParams; ?>) + { + $this->findModel(<?php echo $actionParams; ?>)->delete(); + return $this->redirect(array('index')); + } + + /** + * Finds the <?php echo $modelClass; ?> model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * <?php echo implode("\n\t * ", $actionParamComments) . "\n"; ?> + * @return <?php echo $modelClass; ?> the loaded model + * @throws HttpException if the model cannot be found + */ + protected function findModel(<?php echo $actionParams; ?>) + { +<?php +if (count($pks) === 1) { + $condition = '$id'; +} else { + $condition = array(); + foreach ($pks as $pk) { + $condition[] = "'$pk' => \$$pk"; + } + $condition = 'array(' . implode(', ', $condition) . ')'; +} +?> + if (($model = <?php echo $modelClass; ?>::find(<?php echo $condition; ?>)) !== null) { + return $model; + } else { + throw new HttpException(404, 'The requested page does not exist.'); + } + } +} diff --git a/framework/yii/gii/generators/crud/templates/search.php b/framework/yii/gii/generators/crud/templates/search.php new file mode 100644 index 0000000..467cee5 --- /dev/null +++ b/framework/yii/gii/generators/crud/templates/search.php @@ -0,0 +1,83 @@ +<?php + +use yii\helpers\StringHelper; + +/** + * This is the template for generating a CRUD controller class file. + * + * @var yii\base\View $this + * @var yii\gii\generators\crud\Generator $generator + */ + +$modelClass = StringHelper::basename($generator->modelClass); +$searchModelClass = StringHelper::basename($generator->searchModelClass); +$rules = $generator->generateSearchRules(); +$labels = $generator->generateSearchLabels(); +$searchAttributes = $generator->getSearchAttributes(); +$searchConditions = $generator->generateSearchConditions(); + +echo "<?php\n"; +?> + +namespace <?php echo StringHelper::dirname(ltrim($generator->searchModelClass, '\\')); ?>; + +use yii\base\Model; +use yii\data\ActiveDataProvider; +use <?php echo ltrim($generator->modelClass, '\\'); ?>; + +/** + * <?php echo $searchModelClass; ?> represents the model behind the search form about <?php echo $modelClass; ?>. + */ +class <?php echo $searchModelClass; ?> extends Model +{ + public $<?php echo implode(";\n\tpublic $", $searchAttributes); ?>; + + public function rules() + { + return array( + <?php echo implode(",\n\t\t\t", $rules); ?>, + ); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return array( +<?php foreach ($labels as $name => $label): ?> + <?php echo "'$name' => '" . addslashes($label) . "',\n"; ?> +<?php endforeach; ?> + ); + } + + public function search($params) + { + $query = <?php echo $modelClass; ?>::find(); + $dataProvider = new ActiveDataProvider(array( + 'query' => $query, + )); + + if (!($this->load($params) && $this->validate())) { + return $dataProvider; + } + + <?php echo implode("\n\t\t", $searchConditions); ?> + + return $dataProvider; + } + + protected function addCondition($query, $attribute, $partialMatch = false) + { + $value = $this->$attribute; + if (trim($value) === '') { + return; + } + if ($partialMatch) { + $value = '%' . strtr($value, array('%'=>'\%', '_'=>'\_', '\\'=>'\\\\')) . '%'; + $query->andWhere(array('like', $attribute, $value)); + } else { + $query->andWhere(array($attribute => $value)); + } + } +} diff --git a/framework/yii/gii/generators/crud/templates/views/_form.php b/framework/yii/gii/generators/crud/templates/views/_form.php new file mode 100644 index 0000000..529d3ef --- /dev/null +++ b/framework/yii/gii/generators/crud/templates/views/_form.php @@ -0,0 +1,44 @@ +<?php + +use yii\helpers\Inflector; +use yii\helpers\StringHelper; + +/** + * @var yii\base\View $this + * @var yii\gii\generators\crud\Generator $generator + */ + +/** @var \yii\db\ActiveRecord $model */ +$model = new $generator->modelClass; +$safeAttributes = $model->safeAttributes(); +if (empty($safeAttributes)) { + $safeAttributes = $model->getTableSchema()->columnNames; +} + +echo "<?php\n"; +?> + +use yii\helpers\Html; +use yii\widgets\ActiveForm; + +/** + * @var yii\base\View $this + * @var <?php echo ltrim($generator->modelClass, '\\'); ?> $model + * @var yii\widgets\ActiveForm $form + */ +?> + +<div class="<?php echo Inflector::camel2id(StringHelper::basename($generator->modelClass)); ?>-form"> + + <?php echo '<?php'; ?> $form = ActiveForm::begin(); ?> + +<?php foreach ($safeAttributes as $attribute) { + echo "\t\t<?php echo " . $generator->generateActiveField($attribute) . " ?>\n\n"; +} ?> + <div class="form-group"> + <?php echo '<?php'; ?> echo Html::submitButton($model->isNewRecord ? 'Create' : 'Update', array('class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary')); ?> + </div> + + <?php echo '<?php'; ?> ActiveForm::end(); ?> + +</div> diff --git a/framework/yii/gii/generators/crud/templates/views/_search.php b/framework/yii/gii/generators/crud/templates/views/_search.php new file mode 100644 index 0000000..13e2b82 --- /dev/null +++ b/framework/yii/gii/generators/crud/templates/views/_search.php @@ -0,0 +1,48 @@ +<?php + +use yii\helpers\Inflector; +use yii\helpers\StringHelper; + +/** + * @var yii\base\View $this + * @var yii\gii\generators\crud\Generator $generator + */ + +echo "<?php\n"; +?> + +use yii\helpers\Html; +use yii\widgets\ActiveForm; + +/** + * @var yii\base\View $this + * @var <?php echo ltrim($generator->searchModelClass, '\\'); ?> $model + * @var yii\widgets\ActiveForm $form + */ +?> + +<div class="<?php echo Inflector::camel2id(StringHelper::basename($generator->modelClass)); ?>-search"> + + <?php echo '<?php'; ?> $form = ActiveForm::begin(array( + 'action' => array('index'), + 'method' => 'get', + )); ?> + +<?php +$count = 0; +foreach ($generator->getTableSchema()->getColumnNames() as $attribute) { + if (++$count < 6) { + echo "\t\t<?php echo " . $generator->generateActiveSearchField($attribute) . " ?>\n"; + } else { + echo "\t\t<?php // echo " . $generator->generateActiveSearchField($attribute) . " ?>\n"; + } +} +?> + <div class="form-group"> + <?php echo '<?php'; ?> echo Html::submitButton('Search', array('class' => 'btn btn-primary')); ?> + <?php echo '<?php'; ?> echo Html::resetButton('Reset', array('class' => 'btn btn-default')); ?> + </div> + + <?php echo '<?php'; ?> ActiveForm::end(); ?> + +</div> diff --git a/framework/yii/gii/generators/crud/templates/views/create.php b/framework/yii/gii/generators/crud/templates/views/create.php new file mode 100644 index 0000000..669b99a --- /dev/null +++ b/framework/yii/gii/generators/crud/templates/views/create.php @@ -0,0 +1,33 @@ +<?php + +use yii\helpers\Inflector; +use yii\helpers\StringHelper; + +/** + * @var yii\base\View $this + * @var yii\gii\generators\crud\Generator $generator + */ + +echo "<?php\n"; +?> + +use yii\helpers\Html; + +/** + * @var yii\base\View $this + * @var <?php echo ltrim($generator->modelClass, '\\'); ?> $model + */ + +$this->title = 'Create <?php echo Inflector::camel2words(StringHelper::basename($generator->modelClass)); ?>'; +$this->params['breadcrumbs'][] = array('label' => '<?php echo Inflector::pluralize(Inflector::camel2words(StringHelper::basename($generator->modelClass))); ?>', 'url' => array('index')); +$this->params['breadcrumbs'][] = $this->title; +?> +<div class="<?php echo Inflector::camel2id(StringHelper::basename($generator->modelClass)); ?>-create"> + + <h1><?php echo "<?php"; ?> echo Html::encode($this->title); ?></h1> + + <?php echo "<?php"; ?> echo $this->render('_form', array( + 'model' => $model, + )); ?> + +</div> diff --git a/framework/yii/gii/generators/crud/templates/views/index.php b/framework/yii/gii/generators/crud/templates/views/index.php new file mode 100644 index 0000000..96f8390 --- /dev/null +++ b/framework/yii/gii/generators/crud/templates/views/index.php @@ -0,0 +1,73 @@ +<?php + +use yii\helpers\Inflector; +use yii\helpers\StringHelper; + +/** + * @var yii\base\View $this + * @var yii\gii\generators\crud\Generator $generator + */ + +$urlParams = $generator->generateUrlParams(); +$nameAttribute = $generator->getNameAttribute(); + +echo "<?php\n"; +?> + +use yii\helpers\Html; +use <?php echo $generator->indexWidgetType === 'grid' ? 'yii\grid\GridView' : 'yii\widgets\ListView'; ?>; + +/** + * @var yii\base\View $this + * @var yii\data\ActiveDataProvider $dataProvider + * @var <?php echo ltrim($generator->searchModelClass, '\\'); ?> $searchModel + */ + +$this->title = '<?php echo Inflector::pluralize(Inflector::camel2words(StringHelper::basename($generator->modelClass))); ?>'; +$this->params['breadcrumbs'][] = $this->title; +?> +<div class="<?php echo Inflector::camel2id(StringHelper::basename($generator->modelClass)); ?>-index"> + + <h1><?php echo "<?php"; ?> echo Html::encode($this->title); ?></h1> + + <?php echo '<?php' . ($generator->indexWidgetType === 'grid' ? ' //' : ''); ?> echo $this->render('_search', array('model' => $searchModel)); ?> + + <p> + <?php echo '<?php'; ?> echo Html::a('Create <?php echo StringHelper::basename($generator->modelClass); ?>', array('create'), array('class' => 'btn btn-success')); ?> + </p> + +<?php if ($generator->indexWidgetType === 'grid'): ?> + <?php echo "<?php"; ?> echo GridView::widget(array( + 'dataProvider' => $dataProvider, + 'filterModel' => $searchModel, + 'columns' => array( + array('class' => 'yii\grid\SerialColumn'), + +<?php +$count = 0; +foreach ($generator->getTableSchema()->columns as $column) { + $format = $generator->generateColumnFormat($column); + if (++$count < 6) { + echo "\t\t\t'" . $column->name . ($format === 'text' ? '' : ':' . $format) . "',\n"; + } else { + echo "\t\t\t// '" . $column->name . ($format === 'text' ? '' : ':' . $format) . "',\n"; + } +} +?> + + array('class' => 'yii\grid\ActionColumn'), + ), + )); ?> +<?php else: ?> + <?php echo "<?php"; ?> echo ListView::widget(array( + 'dataProvider' => $dataProvider, + 'itemOptions' => array( + 'class' => 'item', + ), + 'itemView' => function ($model, $key, $index, $widget) { + return Html::a(Html::encode($model-><?php echo $nameAttribute; ?>), array('view', <?php echo $urlParams; ?>)); + }, + )); ?> +<?php endif; ?> + +</div> diff --git a/framework/yii/gii/generators/crud/templates/views/update.php b/framework/yii/gii/generators/crud/templates/views/update.php new file mode 100644 index 0000000..cb892c2 --- /dev/null +++ b/framework/yii/gii/generators/crud/templates/views/update.php @@ -0,0 +1,36 @@ +<?php + +use yii\helpers\Inflector; +use yii\helpers\StringHelper; + +/** + * @var yii\base\View $this + * @var yii\gii\generators\crud\Generator $generator + */ + +$urlParams = $generator->generateUrlParams(); + +echo "<?php\n"; +?> + +use yii\helpers\Html; + +/** + * @var yii\base\View $this + * @var <?php echo ltrim($generator->modelClass, '\\'); ?> $model + */ + +$this->title = 'Update <?php echo Inflector::camel2words(StringHelper::basename($generator->modelClass)); ?>: ' . $model-><?php echo $generator->getNameAttribute(); ?>; +$this->params['breadcrumbs'][] = array('label' => '<?php echo Inflector::pluralize(Inflector::camel2words(StringHelper::basename($generator->modelClass))); ?>', 'url' => array('index')); +$this->params['breadcrumbs'][] = array('label' => $model-><?php echo $generator->getNameAttribute(); ?>, 'url' => array('view', <?php echo $urlParams; ?>)); +$this->params['breadcrumbs'][] = 'Update'; +?> +<div class="<?php echo Inflector::camel2id(StringHelper::basename($generator->modelClass)); ?>-update"> + + <h1><?php echo "<?php"; ?> echo Html::encode($this->title); ?></h1> + + <?php echo "<?php"; ?> echo $this->render('_form', array( + 'model' => $model, + )); ?> + +</div> diff --git a/framework/yii/gii/generators/crud/templates/views/view.php b/framework/yii/gii/generators/crud/templates/views/view.php new file mode 100644 index 0000000..4f302ed --- /dev/null +++ b/framework/yii/gii/generators/crud/templates/views/view.php @@ -0,0 +1,53 @@ +<?php + +use yii\helpers\Inflector; +use yii\helpers\StringHelper; + +/** + * @var yii\base\View $this + * @var yii\gii\generators\crud\Generator $generator + */ + +$urlParams = $generator->generateUrlParams(); + +echo "<?php\n"; +?> + +use yii\helpers\Html; +use yii\widgets\DetailView; + +/** + * @var yii\base\View $this + * @var <?php echo ltrim($generator->modelClass, '\\'); ?> $model + */ + +$this->title = $model-><?php echo $generator->getNameAttribute(); ?>; +$this->params['breadcrumbs'][] = array('label' => '<?php echo Inflector::pluralize(Inflector::camel2words(StringHelper::basename($generator->modelClass))); ?>', 'url' => array('index')); +$this->params['breadcrumbs'][] = $this->title; +?> +<div class="<?php echo Inflector::camel2id(StringHelper::basename($generator->modelClass)); ?>-view"> + + <h1><?php echo "<?php"; ?> echo Html::encode($this->title); ?></h1> + + <p> + <?php echo '<?php'; ?> echo Html::a('Update', array('update', <?php echo $urlParams; ?>), array('class' => 'btn btn-primary')); ?> + <?php echo '<?php'; ?> echo Html::a('Delete', array('delete', <?php echo $urlParams; ?>), array( + 'class' => 'btn btn-danger', + 'data-confirm' => Yii::t('app', 'Are you sure to delete this item?'), + 'data-method' => 'post', + )); ?> + </p> + + <?php echo '<?php'; ?> echo DetailView::widget(array( + 'model' => $model, + 'attributes' => array( +<?php +foreach ($generator->getTableSchema()->columns as $column) { + $format = $generator->generateColumnFormat($column); + echo "\t\t\t'" . $column->name . ($format === 'text' ? '' : ':' . $format) . "',\n"; +} +?> + ), + )); ?> + +</div> diff --git a/framework/yii/gii/generators/form/Generator.php b/framework/yii/gii/generators/form/Generator.php new file mode 100644 index 0000000..b0edb2a --- /dev/null +++ b/framework/yii/gii/generators/form/Generator.php @@ -0,0 +1,155 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\gii\generators\form; + +use Yii; +use yii\base\Model; +use yii\gii\CodeFile; + +/** + * This generator will generate an action view file based on the specified model class. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Generator extends \yii\gii\Generator +{ + public $modelClass; + public $viewPath = '@app/views'; + public $viewName; + public $scenarioName; + + + /** + * @inheritdoc + */ + public function getName() + { + return 'Form Generator'; + } + + /** + * @inheritdoc + */ + public function getDescription() + { + return 'This generator generates a view script file that displays a form to collect input for the specified model class.'; + } + + /** + * @inheritdoc + */ + public function generate() + { + $files = array(); + $files[] = new CodeFile( + Yii::getAlias($this->viewPath) . '/' . $this->viewName . '.php', + $this->render('form.php') + ); + return $files; + } + + /** + * @inheritdoc + */ + public function rules() + { + return array_merge(parent::rules(), array( + array('modelClass, viewName, scenarioName, viewPath', 'filter', 'filter' => 'trim'), + array('modelClass, viewName, viewPath', 'required'), + array('modelClass', 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'), + array('modelClass', 'validateClass', 'params' => array('extends' => Model::className())), + array('viewName', 'match', 'pattern' => '/^\w+[\\-\\/\w]*$/', 'message' => 'Only word characters, dashes and slashes are allowed.'), + array('viewPath', 'match', 'pattern' => '/^@?\w+[\\-\\/\w]*$/', 'message' => 'Only word characters, dashes, slashes and @ are allowed.'), + array('viewPath', 'validateViewPath'), + array('scenarioName', 'match', 'pattern' => '/^[\w\\-]+$/', 'message' => 'Only word characters and dashes are allowed.'), + )); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return array( + 'modelClass' => 'Model Class', + 'viewName' => 'View Name', + 'viewPath' => 'View Path', + 'scenarioName' => 'Scenario', + ); + } + + /** + * @inheritdoc + */ + public function requiredTemplates() + { + return array( + 'form.php', + 'action.php', + ); + } + + /** + * @inheritdoc + */ + public function stickyAttributes() + { + return array('viewPath', 'scenarioName'); + } + + /** + * @inheritdoc + */ + public function hints() + { + return array( + 'modelClass' => 'This is the model class for collecting the form input. You should provide a fully qualified class name, e.g., <code>app\models\Post</code>.', + 'viewName' => 'This is the view name with respect to the view path. For example, <code>site/index</code> would generate a <code>site/index.php</code> view file under the view path.', + 'viewPath' => 'This is the root view path to keep the generated view files. You may provide either a directory or a path alias, e.g., <code>@app/views</code>.', + 'scenarioName' => 'This is the scenario to be used by the model when collecting the form input. If empty, the default scenario will be used.', + ); + } + + /** + * @inheritdoc + */ + public function successMessage() + { + $code = highlight_string($this->render('action.php'), true); + return <<<EOD +<p>The form has been generated successfully.</p> +<p>You may add the following code in an appropriate controller class to invoke the view:</p> +<pre>$code</pre> +EOD; + } + + /** + * Validates [[viewPath]] to make sure it is a valid path or path alias and exists. + */ + public function validateViewPath() + { + $path = Yii::getAlias($this->viewPath, false); + if ($path === false || !is_dir($path)) { + $this->addError('viewPath', 'View path does not exist.'); + } + } + + /** + * @return array list of safe attributes of [[modelClass]] + */ + public function getModelAttributes() + { + /** @var Model $model */ + $model = new $this->modelClass; + if (!empty($this->scenarioName)) { + $model->setScenario($this->scenarioName); + } + return $model->safeAttributes(); + } +} diff --git a/framework/yii/gii/generators/form/form.php b/framework/yii/gii/generators/form/form.php new file mode 100644 index 0000000..c04a26e --- /dev/null +++ b/framework/yii/gii/generators/form/form.php @@ -0,0 +1,10 @@ +<?php +/** + * @var yii\base\View $this + * @var yii\widgets\ActiveForm $form + * @var yii\gii\generators\form\Generator $generator + */ +echo $form->field($generator, 'viewName'); +echo $form->field($generator, 'modelClass'); +echo $form->field($generator, 'scenarioName'); +echo $form->field($generator, 'viewPath'); diff --git a/framework/yii/gii/generators/form/templates/action.php b/framework/yii/gii/generators/form/templates/action.php new file mode 100644 index 0000000..fc5830e --- /dev/null +++ b/framework/yii/gii/generators/form/templates/action.php @@ -0,0 +1,28 @@ +<?php + +use yii\helpers\Inflector; + +/** + * This is the template for generating an action view file. + * + * @var yii\base\View $this + * @var yii\gii\generators\form\Generator $generator + */ + +echo "<?php\n"; +?> + +public function action<?php echo Inflector::id2camel(trim(basename($generator->viewName), '_')); ?>() +{ + $model = new <?php echo $generator->modelClass; ?><?php echo empty($generator->scenarioName) ? '' : "(array('scenario' => '{$generator->scenarioName}'))"; ?>; + + if ($model->load($_POST)) { + if($model->validate()) { + // form inputs are valid, do something here + return; + } + } + return $this->render('<?php echo $generator->viewName; ?>', array( + 'model' => $model, + )); +} diff --git a/framework/yii/gii/generators/form/templates/form.php b/framework/yii/gii/generators/form/templates/form.php new file mode 100644 index 0000000..ad52fe9 --- /dev/null +++ b/framework/yii/gii/generators/form/templates/form.php @@ -0,0 +1,35 @@ +<?php +/** + * This is the template for generating an action view file. + * + * @var yii\base\View $this + * @var yii\gii\generators\form\Generator $generator + */ + +echo "<?php\n"; +?> + +use yii\helpers\Html; +use yii\widgets\ActiveForm; + +/** + * @var yii\base\View $this + * @var <?php echo $generator->modelClass; ?> $model + * @var ActiveForm $form + */ +<?php echo "?>"; ?> + +<div class="<?php echo str_replace('/', '-', trim($generator->viewName, '_')); ?>"> + + <?php echo '<?php'; ?> $form = ActiveForm::begin(); ?> + + <?php foreach ($generator->getModelAttributes() as $attribute): ?> + <?php echo '<?php'; ?> echo $form->field($model, '<?php echo $attribute; ?>'); ?> + <?php endforeach; ?> + + <div class="form-group"> + <?php echo '<?php'; ?> echo Html::submitButton('Submit', array('class' => 'btn btn-primary')); ?> + </div> + <?php echo '<?php'; ?> ActiveForm::end(); ?> + +</div><!-- <?php echo str_replace('/', '-', trim($generator->viewName, '-')); ?> --> diff --git a/framework/yii/gii/generators/model/Generator.php b/framework/yii/gii/generators/model/Generator.php new file mode 100644 index 0000000..69edb5f --- /dev/null +++ b/framework/yii/gii/generators/model/Generator.php @@ -0,0 +1,540 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\gii\generators\model; + +use Yii; +use yii\db\ActiveRecord; +use yii\db\Connection; +use yii\db\Schema; +use yii\gii\CodeFile; +use yii\helpers\Inflector; + +/** + * This generator will generate one or multiple ActiveRecord classes for the specified database table. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Generator extends \yii\gii\Generator +{ + public $db = 'db'; + public $ns = 'app\models'; + public $tableName; + public $modelClass; + public $baseClass = 'yii\db\ActiveRecord'; + public $generateRelations = true; + public $generateLabelsFromComments = false; + + + /** + * @inheritdoc + */ + public function getName() + { + return 'Model Generator'; + } + + /** + * @inheritdoc + */ + public function getDescription() + { + return 'This generator generates an ActiveRecord class for the specified database table.'; + } + + /** + * @inheritdoc + */ + public function rules() + { + return array_merge(parent::rules(), array( + array('db, ns, tableName, modelClass, baseClass', 'filter', 'filter' => 'trim'), + array('db, ns, tableName, baseClass', 'required'), + array('db, modelClass', 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'), + array('ns, baseClass', 'match', 'pattern' => '/^[\w\\\\]+$/', 'message' => 'Only word characters and backslashes are allowed.'), + array('tableName', 'match', 'pattern' => '/^(\w+\.)?([\w\*]+)$/', 'message' => 'Only word characters, and optionally an asterisk and/or a dot are allowed.'), + array('db', 'validateDb'), + array('ns', 'validateNamespace'), + array('tableName', 'validateTableName'), + array('modelClass', 'validateModelClass'), + array('baseClass', 'validateClass', 'params' => array('extends' => ActiveRecord::className())), + array('generateRelations, generateLabelsFromComments', 'boolean'), + )); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return array( + 'ns' => 'Namespace', + 'db' => 'Database Connection ID', + 'tableName' => 'Table Name', + 'modelClass' => 'Model Class', + 'baseClass' => 'Base Class', + 'generateRelations' => 'Generate Relations', + 'generateLabelsFromComments' => 'Generate Labels from DB Comments', + ); + } + + /** + * @inheritdoc + */ + public function hints() + { + return array( + 'ns' => 'This is the namespace of the ActiveRecord class to be generated, e.g., <code>app\models</code>', + 'db' => 'This is the ID of the DB application component.', + 'tableName' => 'This is the name of the DB table that the new ActiveRecord class is associated with, e.g. <code>tbl_post</code>. + The table name may consist of the DB schema part if needed, e.g. <code>public.tbl_post</code>. + The table name may contain an asterisk to match multiple table names, e.g. <code>tbl_*</code> + will match tables who name starts with <code>tbl_</code>. In this case, multiple ActiveRecord classes + will be generated, one for each matching table name; and the class names will be generated from + the matching characters. For example, table <code>tbl_post</code> will generate <code>Post</code> + class.', + 'modelClass' => 'This is the name of the ActiveRecord class to be generated. The class name should not contain + the namespace part as it is specified in "Namespace". You do not need to specify the class name + if "Table Name" contains an asterisk at the end, in which case multiple ActiveRecord classes will be generated.', + 'baseClass' => 'This is the base class of the new ActiveRecord class. It should be a fully qualified namespaced class name.', + 'generateRelations' => 'This indicates whether the generator should generate relations based on + foreign key constraints it detects in the database. Note that if your database contains too many tables, + you may want to uncheck this option to accelerate the code generation proc ess.', + 'generateLabelsFromComments' => 'This indicates whether the generator should generate attribute labels + by using the comments of the corresponding DB columns.', + ); + } + + /** + * @inheritdoc + */ + public function requiredTemplates() + { + return array( + 'model.php', + ); + } + + /** + * @inheritdoc + */ + public function stickyAttributes() + { + return array('ns', 'db', 'baseClass', 'generateRelations', 'generateLabelsFromComments'); + } + + /** + * @inheritdoc + */ + public function generate() + { + $files = array(); + $relations = $this->generateRelations(); + $db = $this->getDbConnection(); + foreach ($this->getTableNames() as $tableName) { + $className = $this->generateClassName($tableName); + $tableSchema = $db->getTableSchema($tableName); + $params = array( + 'tableName' => $tableName, + 'className' => $className, + 'tableSchema' => $tableSchema, + 'labels' => $this->generateLabels($tableSchema), + 'rules' => $this->generateRules($tableSchema), + 'relations' => isset($relations[$className]) ? $relations[$className] : array(), + ); + $files[] = new CodeFile( + Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $className . '.php', + $this->render('model.php', $params) + ); + } + + return $files; + } + + /** + * Generates the attribute labels for the specified table. + * @param \yii\db\TableSchema $table the table schema + * @return array the generated attribute labels (name => label) + */ + public function generateLabels($table) + { + $labels = array(); + foreach ($table->columns as $column) { + if ($this->generateLabelsFromComments && !empty($column->comment)) { + $labels[$column->name] = $column->comment; + } elseif (!strcasecmp($column->name, 'id')) { + $labels[$column->name] = 'ID'; + } else { + $label = Inflector::camel2words($column->name); + if (strcasecmp(substr($label, -3), ' id') === 0) { + $label = substr($label, 0, -3) . ' ID'; + } + $labels[$column->name] = $label; + } + } + return $labels; + } + + /** + * Generates validation rules for the specified table. + * @param \yii\db\TableSchema $table the table schema + * @return array the generated validation rules + */ + public function generateRules($table) + { + $types = array(); + $lengths = array(); + foreach ($table->columns as $column) { + if ($column->autoIncrement) { + continue; + } + if (!$column->allowNull && $column->defaultValue === null) { + $types['required'][] = $column->name; + } + switch ($column->type) { + case Schema::TYPE_SMALLINT: + case Schema::TYPE_INTEGER: + case Schema::TYPE_BIGINT: + $types['integer'][] = $column->name; + break; + case Schema::TYPE_BOOLEAN: + $types['boolean'][] = $column->name; + break; + case Schema::TYPE_FLOAT: + case Schema::TYPE_DECIMAL: + case Schema::TYPE_MONEY: + $types['number'][] = $column->name; + break; + case Schema::TYPE_DATE: + case Schema::TYPE_TIME: + case Schema::TYPE_DATETIME: + case Schema::TYPE_TIMESTAMP: + $types['safe'][] = $column->name; + break; + default: // strings + if ($column->size > 0) { + $lengths[$column->size][] = $column->name; + } else { + $types['string'][] = $column->name; + } + } + } + + $rules = array(); + foreach ($types as $type => $columns) { + $rules[] = "array('" . implode(', ', $columns) . "', '$type')"; + } + foreach ($lengths as $length => $columns) { + $rules[] = "array('" . implode(', ', $columns) . "', 'string', 'max' => $length)"; + } + + return $rules; + } + + /** + * @return array the generated relation declarations + */ + protected function generateRelations() + { + if (!$this->generateRelations) { + return array(); + } + + $db = $this->getDbConnection(); + + if (($pos = strpos($this->tableName, '.')) !== false) { + $schemaName = substr($this->tableName, 0, $pos); + } else { + $schemaName = ''; + } + + $relations = array(); + foreach ($db->getSchema()->getTableSchemas($schemaName) as $table) { + $tableName = $table->name; + $className = $this->generateClassName($tableName); + foreach ($table->foreignKeys as $refs) { + $refTable = $refs[0]; + unset($refs[0]); + $fks = array_keys($refs); + $refClassName = $this->generateClassName($refTable); + + // Add relation for this table + $link = $this->generateRelationLink(array_flip($refs)); + $relationName = $this->generateRelationName($relations, $className, $table, $fks[0], false); + $relations[$className][$relationName] = array( + "return \$this->hasOne('$refClassName', $link);", + $refClassName, + false, + ); + + // Add relation for the referenced table + $hasMany = false; + foreach ($fks as $key) { + if (!in_array($key, $table->primaryKey, true)) { + $hasMany = true; + break; + } + } + $link = $this->generateRelationLink($refs); + $relationName = $this->generateRelationName($relations, $refClassName, $refTable, $className, $hasMany); + $relations[$refClassName][$relationName] = array( + "return \$this->" . ($hasMany ? 'hasMany' : 'hasOne') . "('$className', $link);", + $className, + $hasMany, + ); + } + + if (($fks = $this->checkPivotTable($table)) === false) { + continue; + } + $table0 = $fks[$table->primaryKey[0]][0]; + $table1 = $fks[$table->primaryKey[1]][0]; + $className0 = $this->generateClassName($table0); + $className1 = $this->generateClassName($table1); + + $link = $this->generateRelationLink(array($fks[$table->primaryKey[1]][1] => $table->primaryKey[1])); + $viaLink = $this->generateRelationLink(array($table->primaryKey[0] => $fks[$table->primaryKey[0]][1])); + $relationName = $this->generateRelationName($relations, $className0, $db->getTableSchema($table0), $table->primaryKey[1], true); + $relations[$className0][$relationName] = array( + "return \$this->hasMany('$className1', $link)->viaTable('{$table->name}', $viaLink);", + $className0, + true, + ); + + $link = $this->generateRelationLink(array($fks[$table->primaryKey[0]][1] => $table->primaryKey[0])); + $viaLink = $this->generateRelationLink(array($table->primaryKey[1] => $fks[$table->primaryKey[1]][1])); + $relationName = $this->generateRelationName($relations, $className1, $db->getTableSchema($table1), $table->primaryKey[0], true); + $relations[$className1][$relationName] = array( + "return \$this->hasMany('$className0', $link)->viaTable('{$table->name}', $viaLink);", + $className1, + true, + ); + } + return $relations; + } + + /** + * Generates the link parameter to be used in generating the relation declaration. + * @param array $refs reference constraint + * @return string the generated link parameter. + */ + protected function generateRelationLink($refs) + { + $pairs = array(); + foreach ($refs as $a => $b) { + $pairs[] = "'$a' => '$b'"; + } + return 'array(' . implode(', ', $pairs) . ')'; + } + + /** + * Checks if the given table is a pivot table. + * For simplicity, this method only deals with the case where the pivot contains two PK columns, + * each referencing a column in a different table. + * @param \yii\db\TableSchema the table being checked + * @return array|boolean the relevant foreign key constraint information if the table is a pivot table, + * or false if the table is not a pivot table. + */ + protected function checkPivotTable($table) + { + $pk = $table->primaryKey; + if (count($pk) !== 2) { + return false; + } + $fks = array(); + foreach ($table->foreignKeys as $refs) { + if (count($refs) === 2) { + if (isset($refs[$pk[0]])) { + $fks[$pk[0]] = array($refs[0], $refs[$pk[0]]); + } elseif (isset($refs[$pk[1]])) { + $fks[$pk[1]] = array($refs[0], $refs[$pk[1]]); + } + } + } + if (count($fks) === 2 && $fks[$pk[0]][0] !== $fks[$pk[1]][0]) { + return $fks; + } else { + return false; + } + } + + /** + * Generate a relation name for the specified table and a base name. + * @param array $relations the relations being generated currently. + * @param string $className the class name that will contain the relation declarations + * @param \yii\db\TableSchema $table the table schema + * @param string $key a base name that the relation name may be generated from + * @param boolean $multiple whether this is a has-many relation + * @return string the relation name + */ + protected function generateRelationName($relations, $className, $table, $key, $multiple) + { + if (strcasecmp(substr($key, -2), 'id') === 0 && strcasecmp($key, 'id')) { + $key = rtrim(substr($key, 0, -2), '_'); + } + if ($multiple) { + $key = Inflector::pluralize($key); + } + $name = $rawName = Inflector::id2camel($key, '_'); + $i = 0; + while (isset($table->columns[$name])) { + $name = $rawName . ($i++); + } + while (isset($relations[$className][$name])) { + $name = $rawName . ($i++); + } + + return $name; + } + + /** + * Validates the [[db]] attribute. + */ + public function validateDb() + { + if (Yii::$app->hasComponent($this->db) === false) { + $this->addError('db', 'There is no application component named "db".'); + } elseif (!Yii::$app->getComponent($this->db) instanceof Connection) { + $this->addError('db', 'The "db" application component must be a DB connection instance.'); + } + } + + /** + * Validates the [[ns]] attribute. + */ + public function validateNamespace() + { + $this->ns = ltrim($this->ns, '\\'); + $path = Yii::getAlias('@' . str_replace('\\', '/', $this->ns), false); + if ($path === false) { + $this->addError('ns', 'Namespace must be associated with an existing directory.'); + } + } + + /** + * Validates the [[modelClass]] attribute. + */ + public function validateModelClass() + { + if ($this->isReservedKeyword($this->modelClass)) { + $this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.'); + } + if (strpos($this->tableName, '*') === false && $this->modelClass == '') { + $this->addError('modelClass', 'Model Class cannot be blank.'); + } + } + + /** + * Validates the [[tableName]] attribute. + */ + public function validateTableName() + { + if (($pos = strpos($this->tableName, '*')) !== false && strpos($this->tableName, '*', $pos + 1) !== false) { + $this->addError('tableName', 'At most one asterisk is allowed.'); + return; + } + $tables = $this->getTableNames(); + if (empty($tables)) { + $this->addError('tableName', "Table '{$this->tableName}' does not exist."); + } else { + foreach ($tables as $table) { + $class = $this->generateClassName($table); + if ($this->isReservedKeyword($class)) { + $this->addError('tableName', "Table '$table' will generate a class which is a reserved PHP keyword."); + break; + } + } + } + } + + private $_tableNames; + private $_classNames; + + /** + * @return array the table names that match the pattern specified by [[tableName]]. + */ + protected function getTableNames() + { + if ($this->_tableNames !== null) { + return $this->_tableNames; + } + $db = $this->getDbConnection(); + if ($db === null) { + return array(); + } + $tableNames = array(); + if (strpos($this->tableName, '*') !== false) { + if (($pos = strrpos($this->tableName, '.')) !== false) { + $schema = substr($this->tableName, 0, $pos); + $pattern = '/^' . str_replace('*', '\w+', substr($this->tableName, $pos + 1)) . '$/'; + } else { + $schema = ''; + $pattern = '/^' . str_replace('*', '\w+', $this->tableName) . '$/'; + } + + foreach ($db->schema->getTableNames($schema) as $table) { + if (preg_match($pattern, $table)) { + $tableNames[] = $schema === '' ? $table : ($schema . '.' . $table); + } + } + } elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) { + $tableNames[] = $this->tableName; + $this->_classNames[$this->tableName] = $this->modelClass; + } + return $this->_tableNames = $tableNames; + } + + /** + * Generates a class name from the specified table name. + * @param string $tableName the table name (which may contain schema prefix) + * @return string the generated class name + */ + protected function generateClassName($tableName) + { + if (isset($this->_classNames[$tableName])) { + return $this->_classNames[$tableName]; + } + + if (($pos = strrpos($tableName, '.')) !== false) { + $tableName = substr($tableName, $pos + 1); + } + + $db = $this->getDbConnection(); + $patterns = array(); + if (strpos($this->tableName, '*') !== false) { + $pattern = $this->tableName; + if (($pos = strrpos($pattern, '.')) !== false) { + $pattern = substr($pattern, $pos + 1); + } + $patterns[] = '/^' . str_replace('*', '(\w+)', $pattern) . '$/'; + } + if (!empty($db->tablePrefix)) { + $patterns[] = "/^{$db->tablePrefix}(.*?)$/"; + $patterns[] = "/^(.*?){$db->tablePrefix}$/"; + } else { + $patterns[] = "/^tbl_(.*?)$/"; + } + + $className = $tableName; + foreach ($patterns as $pattern) { + if (preg_match($pattern, $tableName, $matches)) { + $className = $matches[1]; + break; + } + } + return $this->_classNames[$tableName] = Inflector::id2camel($className, '_'); + } + + /** + * @return Connection the DB connection as specified by [[db]]. + */ + protected function getDbConnection() + { + return Yii::$app->{$this->db}; + } +} diff --git a/framework/yii/gii/generators/model/form.php b/framework/yii/gii/generators/model/form.php new file mode 100644 index 0000000..9eca27c --- /dev/null +++ b/framework/yii/gii/generators/model/form.php @@ -0,0 +1,14 @@ +<?php +/** + * @var yii\base\View $this + * @var yii\widgets\ActiveForm $form + * @var yii\gii\generators\form\Generator $generator + */ + +echo $form->field($generator, 'tableName'); +echo $form->field($generator, 'modelClass'); +echo $form->field($generator, 'ns'); +echo $form->field($generator, 'baseClass'); +echo $form->field($generator, 'db'); +echo $form->field($generator, 'generateRelations')->checkbox(); +echo $form->field($generator, 'generateLabelsFromComments')->checkbox(); diff --git a/framework/yii/gii/generators/model/templates/model.php b/framework/yii/gii/generators/model/templates/model.php new file mode 100644 index 0000000..b194294 --- /dev/null +++ b/framework/yii/gii/generators/model/templates/model.php @@ -0,0 +1,72 @@ +<?php +/** + * This is the template for generating the model class of a specified table. + * + * @var yii\base\View $this + * @var yii\gii\generators\model\Generator $generator + * @var string $tableName full table name + * @var string $className class name + * @var yii\db\TableSchema $tableSchema + * @var string[] $labels list of attribute labels (name=>label) + * @var string[] $rules list of validation rules + * @var array $relations list of relations (name=>relation declaration) + */ + +echo "<?php\n"; +?> + +namespace <?php echo $generator->ns; ?>; + +/** + * This is the model class for table "<?php echo $tableName; ?>". + * +<?php foreach ($tableSchema->columns as $column): ?> + * @property <?php echo "{$column->phpType} \${$column->name}\n"; ?> +<?php endforeach; ?> +<?php if (!empty($relations)): ?> + * +<?php foreach ($relations as $name => $relation): ?> + * @property <?php echo $relation[1] . ($relation[2] ? '[]' : '') . ' $' . lcfirst($name) . "\n"; ?> +<?php endforeach; ?> +<?php endif; ?> + */ +class <?php echo $className; ?> extends <?php echo '\\' . ltrim($generator->baseClass, '\\') . "\n"; ?> +{ + /** + * @inheritdoc + */ + public static function tableName() + { + return '<?php echo $tableName; ?>'; + } + + /** + * @inheritdoc + */ + public function rules() + { + return array(<?php echo "\n\t\t\t" . implode(",\n\t\t\t", $rules) . "\n\t\t"; ?>); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return array( +<?php foreach ($labels as $name => $label): ?> + <?php echo "'$name' => '" . addslashes($label) . "',\n"; ?> +<?php endforeach; ?> + ); + } +<?php foreach ($relations as $name => $relation): ?> + + /** + * @return \yii\db\ActiveRelation + */ + public function get<?php echo $name; ?>() + { + <?php echo $relation[0] . "\n"; ?> + } +<?php endforeach; ?> +} diff --git a/framework/yii/gii/generators/module/Generator.php b/framework/yii/gii/generators/module/Generator.php new file mode 100644 index 0000000..39c68f0 --- /dev/null +++ b/framework/yii/gii/generators/module/Generator.php @@ -0,0 +1,169 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\gii\generators\module; + +use Yii; +use yii\gii\CodeFile; +use yii\helpers\Html; +use yii\helpers\StringHelper; + +/** + * This generator will generate the skeleton code needed by a module. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Generator extends \yii\gii\Generator +{ + public $moduleClass; + public $moduleID; + + /** + * @inheritdoc + */ + public function getName() + { + return 'Module Generator'; + } + + /** + * @inheritdoc + */ + public function getDescription() + { + return 'This generator helps you to generate the skeleton code needed by a Yii module.'; + } + + /** + * @inheritdoc + */ + public function rules() + { + return array_merge(parent::rules(), array( + array('moduleID, moduleClass', 'filter', 'filter' => 'trim'), + array('moduleID, moduleClass', 'required'), + array('moduleID', 'match', 'pattern' => '/^[\w\\-]+$/', 'message' => 'Only word characters and dashes are allowed.'), + array('moduleClass', 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'), + array('moduleClass', 'validateModuleClass'), + )); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return array( + 'moduleID' => 'Module ID', + 'moduleClass' => 'Module Class', + ); + } + + /** + * @inheritdoc + */ + public function hints() + { + return array( + 'moduleID' => 'This refers to the ID of the module, e.g., <code>admin</code>.', + 'moduleClass' => 'This is the fully qualified class name of the module, e.g., <code>app\modules\admin\Module</code>.', + ); + } + + /** + * @inheritdoc + */ + public function successMessage() + { + if (Yii::$app->hasModule($this->moduleID)) { + $link = Html::a('try it now', Yii::$app->getUrlManager()->createUrl($this->moduleID), array('target' => '_blank')); + return "The module has been generated successfully. You may $link."; + } + + $output = <<<EOD +<p>The module has been generated successfully.</p> +<p>To access the module, you need to add this to your application configuration:</p> +EOD; + $code = <<<EOD +<?php + ...... + 'modules' => array( + '{$this->moduleID}' => array( + 'class' => '{$this->moduleClass}', + ), + ), + ...... +EOD; + + return $output . '<pre>' . highlight_string($code, true) . '</pre>'; + } + + /** + * @inheritdoc + */ + public function requiredTemplates() + { + return array( + 'module.php', + 'controller.php', + 'view.php', + ); + } + + /** + * @inheritdoc + */ + public function generate() + { + $files = array(); + $modulePath = $this->getModulePath(); + $files[] = new CodeFile( + $modulePath . '/' . StringHelper::basename($this->moduleClass) . '.php', + $this->render("module.php") + ); + $files[] = new CodeFile( + $modulePath . '/controllers/DefaultController.php', + $this->render("controller.php") + ); + $files[] = new CodeFile( + $modulePath . '/views/default/index.php', + $this->render("view.php") + ); + + return $files; + } + + /** + * Validates [[moduleClass]] to make sure it is a fully qualified class name. + */ + public function validateModuleClass() + { + if (strpos($this->moduleClass, '\\') === false || Yii::getAlias('@' . str_replace('\\', '/', $this->moduleClass)) === false) { + $this->addError('moduleClass', 'Module class must be properly namespaced.'); + } + if (substr($this->moduleClass, -1, 1) == '\\') { + $this->addError('moduleClass', 'Module class name must not be empty. Please enter a fully qualified class name. e.g. "app\\modules\\admin\\Module".'); + } + } + + /** + * @return boolean the directory that contains the module class + */ + public function getModulePath() + { + return Yii::getAlias('@' . str_replace('\\', '/', substr($this->moduleClass, 0, strrpos($this->moduleClass, '\\')))); + } + + /** + * @return string the controller namespace of the module. + */ + public function getControllerNamespace() + { + return substr($this->moduleClass, 0, strrpos($this->moduleClass, '\\')) . '\controllers'; + } +} diff --git a/framework/yii/gii/generators/module/form.php b/framework/yii/gii/generators/module/form.php new file mode 100644 index 0000000..8a0cc88 --- /dev/null +++ b/framework/yii/gii/generators/module/form.php @@ -0,0 +1,13 @@ +<?php +/** + * @var yii\base\View $this + * @var yii\widgets\ActiveForm $form + * @var yii\gii\generators\module\Generator $generator + */ +?> +<div class="module-form"> +<?php + echo $form->field($generator, 'moduleClass'); + echo $form->field($generator, 'moduleID'); +?> +</div> diff --git a/framework/yii/gii/generators/module/templates/controller.php b/framework/yii/gii/generators/module/templates/controller.php new file mode 100644 index 0000000..4d3da93 --- /dev/null +++ b/framework/yii/gii/generators/module/templates/controller.php @@ -0,0 +1,21 @@ +<?php +/** + * This is the template for generating a controller class within a module. + * + * @var yii\base\View $this + * @var yii\gii\generators\module\Generator $generator + */ +echo "<?php\n"; +?> + +namespace <?php echo $generator->getControllerNamespace(); ?>; + +use yii\web\Controller; + +class DefaultController extends Controller +{ + public function actionIndex() + { + return $this->render('index'); + } +} diff --git a/framework/yii/gii/generators/module/templates/module.php b/framework/yii/gii/generators/module/templates/module.php new file mode 100644 index 0000000..40af635 --- /dev/null +++ b/framework/yii/gii/generators/module/templates/module.php @@ -0,0 +1,29 @@ +<?php +/** + * This is the template for generating a module class file. + * + * @var yii\base\View $this + * @var yii\gii\generators\module\Generator $generator + */ +$className = $generator->moduleClass; +$pos = strrpos($className, '\\'); +$ns = ltrim(substr($className, 0, $pos), '\\'); +$className = substr($className, $pos + 1); + +echo "<?php\n"; +?> + +namespace <?php echo $ns; ?>; + + +class <?php echo $className; ?> extends \yii\base\Module +{ + public $controllerNamespace = '<?php echo $generator->getControllerNamespace(); ?>'; + + public function init() + { + parent::init(); + + // custom initialization code goes here + } +} diff --git a/framework/yii/gii/generators/module/templates/view.php b/framework/yii/gii/generators/module/templates/view.php new file mode 100644 index 0000000..d0e1ce6 --- /dev/null +++ b/framework/yii/gii/generators/module/templates/view.php @@ -0,0 +1,18 @@ +<?php +/** + * @var yii\base\View $this + * @var yii\gii\generators\module\Generator $generator + */ +?> +<div class="<?php echo $generator->moduleID . '-default-index'; ?>"> + <h1><?php echo "<?php"; ?> echo $this->context->action->uniqueId; ?></h1> + <p> + This is the view content for action "<?php echo "<?php"; ?> echo $this->context->action->id; ?>". + The action belongs to the controller "<?php echo "<?php"; ?> echo get_class($this->context); ?>" + in the "<?php echo "<?php"; ?> echo $this->context->module->id; ?>" module. + </p> + <p> + You may customize this page by editing the following file:<br> + <code><?php echo "<?php"; ?> echo __FILE__; ?></code> + </p> +</div> diff --git a/framework/yii/gii/views/default/diff.php b/framework/yii/gii/views/default/diff.php new file mode 100644 index 0000000..bb4e455 --- /dev/null +++ b/framework/yii/gii/views/default/diff.php @@ -0,0 +1,15 @@ +<?php +/** + * @var yii\base\View $this + * @var mixed $diff + */ +?> +<div class="default-diff"> + <?php if ($diff === false): ?> + <div class="alert alert-danger">Diff is not supported for this file type.</div> + <?php elseif (empty($diff)): ?> + <div class="alert alert-success">Identical.</div> + <?php else: ?> + <div class="content"><?php echo $diff; ?></div> + <?php endif; ?> +</div> diff --git a/framework/yii/gii/views/default/index.php b/framework/yii/gii/views/default/index.php new file mode 100644 index 0000000..7dd1fdd --- /dev/null +++ b/framework/yii/gii/views/default/index.php @@ -0,0 +1,33 @@ +<?php +use yii\helpers\Html; + +/** + * @var $this \yii\base\View + * @var $content string + * @var yii\gii\Generator[] $generators + * @var yii\gii\Generator $activeGenerator + */ +$generators = Yii::$app->controller->module->generators; +$activeGenerator = Yii::$app->controller->generator; +$this->title = 'Welcome to Gii'; +?> +<div class="default-index"> + <div class="page-header"> + <h1>Welcome to Gii <small>a magic tool that can write code for you</small></h1> + </div> + + <p class="lead">Start the fun with the following code generators:</p> + + <div class="row"> + <?php foreach ($generators as $id => $generator): ?> + <div class="generator col-lg-4"> + <h3><?php echo Html::encode($generator->getName()); ?></h3> + <p><?php echo $generator->getDescription(); ?></p> + <p><?php echo Html::a('Start »', array('default/view', 'id' => $id), array('class' => 'btn btn-default')); ?></p> + </div> + <?php endforeach; ?> + </div> + + <p><a class="btn btn-success" href="http://www.yiiframework.com/extensions/?tag=gii">Get More Generators</a></p> + +</div> diff --git a/framework/yii/gii/views/default/view.php b/framework/yii/gii/views/default/view.php new file mode 100644 index 0000000..91c6bcb --- /dev/null +++ b/framework/yii/gii/views/default/view.php @@ -0,0 +1,73 @@ +<?php + +use yii\gii\Generator; +use yii\helpers\Html; +use yii\widgets\ActiveForm; +use yii\gii\components\ActiveField; +use yii\gii\CodeFile; + +/** + * @var yii\base\View $this + * @var yii\gii\Generator $generator + * @var string $id + * @var yii\widgets\ActiveForm $form + * @var string $results + * @var boolean $hasError + * @var CodeFile[] $files + * @var array $answers + */ + +$this->title = $generator->getName(); +$templates = array(); +foreach ($generator->templates as $name => $path) { + $templates[$name] = "$name ($path)"; +} +?> +<div class="default-view"> + <h1><?php echo Html::encode($this->title); ?></h1> + + <p><?php echo $generator->getDescription(); ?></p> + + <?php $form = ActiveForm::begin(array( + 'id' => "$id-generator", + 'successCssClass' => '', + 'fieldConfig' => array('class' => ActiveField::className()), + )); ?> + <div class="row"> + <div class="col-lg-8"> + <?php echo $this->renderFile($generator->formView(), array( + 'generator' => $generator, + 'form' => $form, + )); ?> + <?php echo $form->field($generator, 'template')->sticky() + ->label('Code Template') + ->dropDownList($templates)->hint(' + Please select which set of the templates should be used to generated the code. + '); ?> + <div class="form-group"> + <?php echo Html::submitButton('Preview', array('name' => 'preview', 'class' => 'btn btn-primary')); ?> + + <?php if(isset($files)): ?> + <?php echo Html::submitButton('Generate', array('name' => 'generate', 'class' => 'btn btn-success')); ?> + <?php endif; ?> + </div> + </div> + </div> + + <?php + if (isset($results)) { + echo $this->render('view/results', array( + 'generator' => $generator, + 'results' => $results, + 'hasError' => $hasError, + )); + } elseif (isset($files)) { + echo $this->render('view/files', array( + 'generator' => $generator, + 'files' => $files, + 'answers' => $answers, + )); + } + ?> + <?php ActiveForm::end(); ?> +</div> diff --git a/framework/yii/gii/views/default/view/files.php b/framework/yii/gii/views/default/view/files.php new file mode 100644 index 0000000..5ba08e8 --- /dev/null +++ b/framework/yii/gii/views/default/view/files.php @@ -0,0 +1,79 @@ +<?php + +use yii\gii\Generator; +use yii\helpers\Html; +use yii\gii\CodeFile; + +/** + * @var $this \yii\base\View + * @var $generator \yii\gii\Generator + * @var CodeFile[] $files + * @var array $answers + */ +?> +<div class="default-view-files"> + <p>Click on the above <code>Generate</code> button to generate the files selected below:</p> + + <table class="table table-bordered table-striped table-condensed"> + <thead> + <tr> + <th class="file">Code File</th> + <th class="action">Action</th> + <th> + <?php + foreach ($files as $file) { + if ($file->operation !== CodeFile::OP_SKIP) { + echo '<input type="checkbox" id="check-all">'; + break; + } + } + ?> + </th> + </tr> + </thead> + <tbody> + <?php foreach ($files as $file): ?> + <tr class="<?php echo $file->operation; ?>"> + <td class="file"> + <?php echo Html::a(Html::encode($file->getRelativePath()), array('preview', 'file' => $file->id), array('class' => 'preview-code', 'data-title' => $file->getRelativePath())); ?> + <?php if ($file->operation === CodeFile::OP_OVERWRITE): ?> + <?php echo Html::a('diff', array('diff', 'file' => $file->id), array('class' => 'diff-code label label-warning', 'data-title' => $file->getRelativePath())); ?> + <?php endif; ?> + </td> + <td class="action"> + <?php + if ($file->operation === CodeFile::OP_SKIP) { + echo 'unchanged'; + } else { + echo $file->operation; + } + ?> + </td> + <td class="check"> + <?php + if ($file->operation === CodeFile::OP_SKIP) { + echo ' '; + } else { + echo Html::checkBox("answers[{$file->id}]", isset($answers) ? isset($answers[$file->id]) : ($file->operation === CodeFile::OP_NEW)); + } + ?> + </td> + </tr> + <?php endforeach; ?> + </tbody> + </table> + + <div class="modal fade" id="preview-modal" tabindex="-1" role="dialog"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title">Modal title</h4> + </div> + <div class="modal-body"> + <p>Please wait ...</p> + </div> + </div> + </div> + </div> +</div> diff --git a/framework/yii/gii/views/default/view/results.php b/framework/yii/gii/views/default/view/results.php new file mode 100644 index 0000000..caca404 --- /dev/null +++ b/framework/yii/gii/views/default/view/results.php @@ -0,0 +1,22 @@ +<?php + +use yii\gii\Generator; +use yii\gii\CodeFile; + +/** + * @var yii\base\View $this + * @var yii\gii\Generator $generator + * @var string $results + * @var boolean $hasError + */ +?> +<div class="default-view-results"> + <?php + if ($hasError) { + echo '<div class="alert alert-danger">There was something wrong when generating the code. Please check the following messages.</div>'; + } else { + echo '<div class="alert alert-success">' . $generator->successMessage() . '</div>'; + } + ?> + <pre><?php echo nl2br($results); ?></pre> +</div> diff --git a/framework/yii/gii/views/layouts/generator.php b/framework/yii/gii/views/layouts/generator.php new file mode 100644 index 0000000..fcf9fbc --- /dev/null +++ b/framework/yii/gii/views/layouts/generator.php @@ -0,0 +1,31 @@ +<?php +use yii\helpers\Html; + +/** + * @var $this \yii\base\View + * @var $content string + * @var yii\gii\Generator[] $generators + * @var yii\gii\Generator $activeGenerator + */ +$generators = Yii::$app->controller->module->generators; +$activeGenerator = Yii::$app->controller->generator; +?> +<?php $this->beginContent('@yii/gii/views/layouts/main.php'); ?> +<div class="row"> + <div class="col-lg-3"> + <div class="list-group"> + <?php + foreach ($generators as $id => $generator) { + $label = '<i class="glyphicon glyphicon-chevron-right"></i>' . Html::encode($generator->getName()); + echo Html::a($label, array('default/view', 'id' => $id), array( + 'class' => $generator === $activeGenerator ? 'list-group-item active' : 'list-group-item', + )); + } + ?> + </div> + </div> + <div class="col-lg-9"> + <?php echo $content; ?> + </div> +</div> +<?php $this->endContent(); ?> diff --git a/framework/yii/gii/views/layouts/main.php b/framework/yii/gii/views/layouts/main.php new file mode 100644 index 0000000..5a70ac4 --- /dev/null +++ b/framework/yii/gii/views/layouts/main.php @@ -0,0 +1,55 @@ +<?php +use yii\bootstrap\NavBar; +use yii\bootstrap\Nav; +use yii\helpers\Html; + +/** + * @var $this \yii\base\View + * @var $content string + */ +$asset = yii\gii\GiiAsset::register($this); +?> +<?php $this->beginPage(); ?> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"/> + <title><?php echo Html::encode($this->title); ?></title> + <?php $this->head(); ?> +</head> +<body> +<?php $this->beginBody(); ?> +<?php +NavBar::begin(array( + 'brandLabel' => Html::img($asset->baseUrl . '/logo.png'), + 'brandUrl' => array('default/index'), + 'options' => array( + 'class' => 'navbar-inverse navbar-fixed-top', + ), +)); +echo Nav::widget(array( + 'options' => array('class' => 'nav navbar-nav pull-right'), + 'items' => array( + array('label' => 'Home', 'url' => array('default/index')), + array('label' => 'Help', 'url' => 'http://www.yiiframework.com/doc/guide/topics.gii'), + array('label' => 'Application', 'url' => Yii::$app->homeUrl), + ), +)); +NavBar::end(); +?> + +<div class="container"> + <?php echo $content; ?> +</div> + +<footer class="footer"> + <div class="container"> + <p class="pull-left">A Product of <a href="http://www.yiisoft.com/">Yii Software LLC</a></p> + <p class="pull-right"><?php echo Yii::powered(); ?></p> + </div> +</footer> + +<?php $this->endBody(); ?> +</body> +</html> +<?php $this->endPage(); ?> diff --git a/framework/yii/grid/ActionColumn.php b/framework/yii/grid/ActionColumn.php new file mode 100644 index 0000000..aae59ae --- /dev/null +++ b/framework/yii/grid/ActionColumn.php @@ -0,0 +1,100 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\grid; + +use Yii; +use Closure; +use yii\helpers\Html; +use yii\helpers\Inflector; +use yii\helpers\StringHelper; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ActionColumn extends Column +{ + public $template = '{view} {update} {delete}'; + public $buttons = array(); + public $urlCreator; + + public function init() + { + parent::init(); + $this->initDefaultButtons(); + } + + protected function initDefaultButtons() + { + if (!isset($this->buttons['view'])) { + $this->buttons['view'] = function ($model, $column) { + /** @var ActionColumn $column */ + $url = $column->createUrl($model, 'view'); + return Html::a('<span class="glyphicon glyphicon-eye-open"></span>', $url, array( + 'title' => Yii::t('yii', 'View'), + )); + }; + } + if (!isset($this->buttons['update'])) { + $this->buttons['update'] = function ($model, $column) { + /** @var ActionColumn $column */ + $url = $column->createUrl($model, 'update'); + return Html::a('<span class="glyphicon glyphicon-pencil"></span>', $url, array( + 'title' => Yii::t('yii', 'Update'), + )); + }; + } + if (!isset($this->buttons['delete'])) { + $this->buttons['delete'] = function ($model, $column) { + /** @var ActionColumn $column */ + $url = $column->createUrl($model, 'delete'); + return Html::a('<span class="glyphicon glyphicon-trash"></span>', $url, array( + 'title' => Yii::t('yii', 'Delete'), + 'data-confirm' => Yii::t('yii', 'Are you sure to delete this item?'), + 'data-method' => 'post', + )); + }; + } + } + + /** + * @param \yii\db\ActiveRecord $model + * @param string $action + * @return string + */ + public function createUrl($model, $action) + { + if ($this->urlCreator instanceof Closure) { + return call_user_func($this->urlCreator, $model, $action); + } else { + $params = $model->getPrimaryKey(true); + if (count($params) === 1) { + $params = array('id' => reset($params)); + } + return Yii::$app->controller->createUrl($action, $params); + } + } + + /** + * Renders the data cell content. + * @param mixed $model the data model + * @param integer $index the zero-based index of the data model among the models array returned by [[dataProvider]]. + * @return string the rendering result + */ + protected function renderDataCellContent($model, $index) + { + return preg_replace_callback('/\\{(\w+)\\}/', function ($matches) use ($this, $model) { + $name = $matches[1]; + if (isset($this->buttons[$name])) { + return call_user_func($this->buttons[$name], $model, $this); + } else { + return ''; + } + }, $this->template); + } +} diff --git a/framework/yii/grid/CheckboxColumn.php b/framework/yii/grid/CheckboxColumn.php new file mode 100644 index 0000000..e9170f4 --- /dev/null +++ b/framework/yii/grid/CheckboxColumn.php @@ -0,0 +1,84 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\grid; + +use Closure; +use yii\base\InvalidConfigException; +use yii\helpers\Html; + +/** + * CheckboxColumn displays a column of checkboxes in a grid view. + * Users may click on the checkboxes to select rows of the grid. The selected rows may be + * obtained by calling the following JavaScript code: + * + * ~~~ + * var keys = $('#grid').yiiGridView('getSelectedRows'); + * // keys is an array consisting of the keys associated with the selected rows + * ~~~ + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class CheckboxColumn extends Column +{ + public $name = 'selection'; + public $checkboxOptions = array(); + public $multiple = true; + + + public function init() + { + parent::init(); + if (empty($this->name)) { + throw new InvalidConfigException('The "name" property must be set.'); + } + if (substr($this->name, -2) !== '[]') { + $this->name .= '[]'; + } + } + + /** + * Renders the header cell content. + * The default implementation simply renders {@link header}. + * This method may be overridden to customize the rendering of the header cell. + * @return string the rendering result + */ + protected function renderHeaderCellContent() + { + $name = rtrim($this->name, '[]') . '_all'; + $id = $this->grid->options['id']; + $options = json_encode(array( + 'name' => $this->name, + 'multiple' => $this->multiple, + 'checkAll' => $name, + )); + $this->grid->getView()->registerJs("jQuery('#$id').yiiGridView('setSelectionColumn', $options);"); + + if ($this->header !== null || !$this->multiple) { + return parent::renderHeaderCellContent(); + } else { + return Html::checkBox($name, false, array('class' => 'select-on-check-all')); + } + } + + /** + * Renders the data cell content. + * @param mixed $model the data model + * @param integer $index the zero-based index of the data model among the models array returned by [[dataProvider]]. + * @return string the rendering result + */ + protected function renderDataCellContent($model, $index) + { + if ($this->checkboxOptions instanceof Closure) { + $options = call_user_func($this->checkboxOptions, $model, $index, $this); + } else { + $options = $this->checkboxOptions; + } + return Html::checkbox($this->name, !empty($options['checked']), $options); + } +} diff --git a/framework/yii/grid/Column.php b/framework/yii/grid/Column.php new file mode 100644 index 0000000..b49f73e --- /dev/null +++ b/framework/yii/grid/Column.php @@ -0,0 +1,142 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\grid; + +use Closure; +use yii\base\Object; +use yii\helpers\Html; + +/** + * Column is the base class of all [[GridView]] column classes. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Column extends Object +{ + /** + * @var GridView the grid view object that owns this column. + */ + public $grid; + /** + * @var string the header cell content. Note that it will not be HTML-encoded. + */ + public $header; + /** + * @var string the footer cell content. Note that it will not be HTML-encoded. + */ + public $footer; + /** + * @var callable + */ + public $content; + /** + * @var boolean whether this column is visible. Defaults to true. + */ + public $visible = true; + public $options = array(); + public $headerOptions = array(); + /** + * @var array|\Closure + */ + public $contentOptions = array(); + public $footerOptions = array(); + /** + * @var array the HTML attributes for the filter cell tag. + */ + public $filterOptions=array(); + + + /** + * Renders the header cell. + */ + public function renderHeaderCell() + { + return Html::tag('th', $this->renderHeaderCellContent(), $this->headerOptions); + } + + /** + * Renders the footer cell. + */ + public function renderFooterCell() + { + return Html::tag('td', $this->renderFooterCellContent(), $this->footerOptions); + } + + /** + * Renders a data cell. + * @param mixed $model the data model being rendered + * @param integer $index the zero-based index of the data item among the item array returned by [[dataProvider]]. + * @return string the rendering result + */ + public function renderDataCell($model, $index) + { + if ($this->contentOptions instanceof Closure) { + $options = call_user_func($this->contentOptions, $model, $index, $this); + } else { + $options = $this->contentOptions; + } + return Html::tag('td', $this->renderDataCellContent($model, $index), $options); + } + + /** + * Renders the filter cell. + */ + public function renderFilterCell() + { + return Html::tag('td', $this->renderFilterCellContent(), $this->filterOptions); + } + + /** + * Renders the header cell content. + * The default implementation simply renders {@link header}. + * This method may be overridden to customize the rendering of the header cell. + * @return string the rendering result + */ + protected function renderHeaderCellContent() + { + return trim($this->header) !== '' ? $this->header : $this->grid->emptyCell; + } + + /** + * Renders the footer cell content. + * The default implementation simply renders {@link footer}. + * This method may be overridden to customize the rendering of the footer cell. + * @return string the rendering result + */ + protected function renderFooterCellContent() + { + return trim($this->footer) !== '' ? $this->footer : $this->grid->emptyCell; + } + + /** + * Renders the data cell content. + * @param mixed $model the data model + * @param integer $index the zero-based index of the data model among the models array returned by [[dataProvider]]. + * @return string the rendering result + */ + protected function renderDataCellContent($model, $index) + { + if ($this->content !== null) { + return call_user_func($this->content, $model, $index, $this); + } else { + return $this->grid->emptyCell; + } + } + + /** + * Renders the filter cell content. + * The default implementation simply renders a space. + * This method may be overridden to customize the rendering of the filter cell (if any). + * @return string the rendering result + */ + protected function renderFilterCellContent() + { + return $this->grid->emptyCell; + } +} diff --git a/framework/yii/grid/DataColumn.php b/framework/yii/grid/DataColumn.php new file mode 100644 index 0000000..478394b --- /dev/null +++ b/framework/yii/grid/DataColumn.php @@ -0,0 +1,143 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\grid; + +use yii\base\Model; +use yii\data\ActiveDataProvider; +use yii\db\ActiveQuery; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; +use yii\helpers\Inflector; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DataColumn extends Column +{ + /** + * @var string the attribute name associated with this column. When neither [[content]] nor [[value]] + * is specified, the value of the specified attribute will be retrieved from each data model and displayed. + * + * Also, if [[header]] is not specified, the label associated with the attribute will be displayed. + */ + public $attribute; + /** + * @var string label to be displayed in the [[header|header cell]] and also to be used as the sorting + * link label when sorting is enabled for this column. + * If it is not set and the models provided by the GridViews data provider are instances + * of [[yii\db\ActiveRecord]], the label will be determined using [[yii\db\ActiveRecord::getAttributeLabel()]]. + * Otherwise [[yii\helpers\Inflector::camel2words()]] will be used to get a label. + */ + public $label; + /** + * @var \Closure an anonymous function that returns the value to be displayed for every data model. + * If this is not set, `$model[$attribute]` will be used to obtain the value. + */ + public $value; + /** + * @var string in which format should the value of each data model be displayed as (e.g. "text", "html"). + * Supported formats are determined by the [[GridView::formatter|formatter]] used by the [[GridView]]. + * Default format is "text" which will format the value as an HTML-encoded plain text when + * [[\yii\base\Formatter]] or [[\yii\i18n\Formatter]] is used. + */ + public $format = 'text'; + /** + * @var boolean whether to allow sorting by this column. If true and [[attribute]] is found in + * the sort definition of [[GridView::dataProvider]], then the header cell of this column + * will contain a link that may trigger the sorting when being clicked. + */ + public $enableSorting = true; + /** + * @var array the HTML attributes for the link tag in the header cell + * generated by [[Sort::link]] when sorting is enabled for this column. + */ + public $sortLinkOptions = array(); + /** + * @var string|array|boolean the HTML code representing a filter input (e.g. a text field, a dropdown list) + * that is used for this data column. This property is effective only when [[GridView::filterModel]] is set. + * + * - If this property is not set, a text field will be generated as the filter input; + * - If this property is an array, a dropdown list will be generated that uses this property value as + * the list options. + * - If you don't want a filter for this data column, set this value to be false. + */ + public $filter; + /** + * @var array the HTML attributes for the filter input fields. This property is used in combination with + * the [[filter]] property. When [[filter]] is not set or is an array, this property will be used to + * render the HTML attributes for the generated filter input fields. + */ + public $filterInputOptions = array('class' => 'form-control', 'id' => null); + + + protected function renderHeaderCellContent() + { + if ($this->header !== null || $this->label === null && $this->attribute === null) { + return parent::renderHeaderCellContent(); + } + + $provider = $this->grid->dataProvider; + + if ($this->label === null) { + if ($provider instanceof ActiveDataProvider && $provider->query instanceof ActiveQuery) { + /** @var Model $model */ + $model = new $provider->query->modelClass; + $label = $model->getAttributeLabel($this->attribute); + } else { + $models = $provider->getModels(); + if (($model = reset($models)) instanceof Model) { + /** @var Model $model */ + $label = $model->getAttributeLabel($this->attribute); + } else { + $label = Inflector::camel2words($this->attribute); + } + } + } else { + $label = $this->label; + } + + if ($this->attribute !== null && $this->enableSorting && + ($sort = $provider->getSort()) !== false && $sort->hasAttribute($this->attribute)) { + + return $sort->link($this->attribute, array_merge($this->sortLinkOptions, array('label' => Html::encode($label)))); + } else { + return Html::encode($label); + } + } + + protected function renderFilterCellContent() + { + if (is_string($this->filter)) { + return $this->filter; + } elseif ($this->filter !== false && $this->grid->filterModel instanceof Model && + $this->attribute !== null && $this->grid->filterModel->isAttributeActive($this->attribute)) + { + if (is_array($this->filter)) { + $options = array_merge(array('prompt' => ''), $this->filterInputOptions); + return Html::activeDropDownList($this->grid->filterModel, $this->attribute, $this->filter, $options); + } else { + return Html::activeTextInput($this->grid->filterModel, $this->attribute, $this->filterInputOptions); + } + } else { + return parent::renderFilterCellContent(); + } + } + + protected function renderDataCellContent($model, $index) + { + if ($this->value !== null) { + $value = call_user_func($this->value, $model, $index, $this); + } elseif ($this->content === null && $this->attribute !== null) { + $value = ArrayHelper::getValue($model, $this->attribute); + } else { + return parent::renderDataCellContent($model, $index); + } + return $this->grid->formatter->format($value, $this->format); + } +} diff --git a/framework/yii/grid/GridView.php b/framework/yii/grid/GridView.php new file mode 100644 index 0000000..4bf7a39 --- /dev/null +++ b/framework/yii/grid/GridView.php @@ -0,0 +1,429 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\grid; + +use Yii; +use Closure; +use yii\base\Formatter; +use yii\base\InvalidConfigException; +use yii\base\Widget; +use yii\db\ActiveRecord; +use yii\helpers\Html; +use yii\helpers\Json; +use yii\widgets\BaseListView; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class GridView extends BaseListView +{ + const FILTER_POS_HEADER = 'header'; + const FILTER_POS_FOOTER = 'footer'; + const FILTER_POS_BODY = 'body'; + + /** + * @var string the default data column class if the class name is not explicitly specified when configuring a data column. + * Defaults to 'yii\grid\DataColumn'. + */ + public $dataColumnClass; + /** + * @var string the caption of the grid table + * @see captionOptions + */ + public $caption; + /** + * @var array the HTML attributes for the caption element + * @see caption + */ + public $captionOptions = array(); + /** + * @var array the HTML attributes for the grid table element + */ + public $tableOptions = array('class' => 'table table-striped table-bordered'); + /** + * @var array the HTML attributes for the table header row + */ + public $headerRowOptions = array(); + /** + * @var array the HTML attributes for the table footer row + */ + public $footerRowOptions = array(); + /** + * @var array|Closure the HTML attributes for the table body rows. This can be either an array + * specifying the common HTML attributes for all body rows, or an anonymous function that + * returns an array of the HTML attributes. The anonymous function will be called once for every + * data model returned by [[dataProvider]]. It should have the following signature: + * + * ~~~php + * function ($model, $key, $index, $grid) + * ~~~ + * + * - `$model`: the current data model being rendered + * - `$key`: the key value associated with the current data model + * - `$index`: the zero-based index of the data model in the model array returned by [[dataProvider]] + * - `$grid`: the GridView object + */ + public $rowOptions = array(); + /** + * @var Closure an anonymous function that is called once BEFORE rendering each data model. + * It should have the similar signature as [[rowOptions]]. The return result of the function + * will be rendered directly. + */ + public $beforeRow; + /** + * @var Closure an anonymous function that is called once AFTER rendering each data model. + * It should have the similar signature as [[rowOptions]]. The return result of the function + * will be rendered directly. + */ + public $afterRow; + /** + * @var boolean whether to show the header section of the grid table. + */ + public $showHeader = true; + /** + * @var boolean whether to show the footer section of the grid table. + */ + public $showFooter = false; + /** + * @var array|Formatter the formatter used to format model attribute values into displayable texts. + * This can be either an instance of [[Formatter]] or an configuration array for creating the [[Formatter]] + * instance. If this property is not set, the "formatter" application component will be used. + */ + public $formatter; + /** + * @var array grid column configuration. Each array element represents the configuration + * for one particular grid column. For example, + * + * ~~~php + * array( + * array( + * 'class' => SerialColumn::className(), + * ), + * array( + * 'class' => DataColumn::className(), + * 'attribute' => 'name', + * 'format' => 'text', + * 'label' => 'Name', + * ), + * array( + * 'class' => CheckboxColumn::className(), + * ), + * ) + * ~~~ + * + * If a column is of class [[DataColumn]], the "class" element can be omitted. + * + * As a shortcut format, a string may be used to specify the configuration of a data column + * which only contains "attribute", "format", and/or "label" options: `"attribute:format:label"`. + * For example, the above "name" column can also be specified as: `"name:text:Name"`. + * Both "format" and "label" are optional. They will take default values if absent. + */ + public $columns = array(); + public $emptyCell = ' '; + /** + * @var \yii\base\Model the model that keeps the user-entered filter data. When this property is set, + * the grid view will enable column-based filtering. Each data column by default will display a text field + * at the top that users can fill in to filter the data. + * + * Note that in order to show an input field for filtering, a column must have its [[DataColumn::attribute]] + * property set or have [[DataColumn::filter]] set as the HTML code for the input field. + * + * When this property is not set (null) the filtering feature is disabled. + */ + public $filterModel; + /** + * @var string|array the URL for returning the filtering result. [[Html::url()]] will be called to + * normalize the URL. If not set, the current controller action will be used. + * When the user makes change to any filter input, the current filtering inputs will be appended + * as GET parameters to this URL. + */ + public $filterUrl; + public $filterSelector; + /** + * @var string whether the filters should be displayed in the grid view. Valid values include: + * + * - [[FILTER_POS_HEADER]]: the filters will be displayed on top of each column's header cell. + * - [[FILTER_POS_BODY]]: the filters will be displayed right below each column's header cell. + * - [[FILTER_POS_FOOTER]]: the filters will be displayed below each column's footer cell. + */ + public $filterPosition = self::FILTER_POS_BODY; + /** + * @var array the HTML attributes for the filter row element + */ + public $filterRowOptions = array('class' => 'filters'); + + /** + * Initializes the grid view. + * This method will initialize required property values and instantiate {@link columns} objects. + */ + public function init() + { + parent::init(); + if ($this->formatter == null) { + $this->formatter = Yii::$app->getFormatter(); + } elseif (is_array($this->formatter)) { + $this->formatter = Yii::createObject($this->formatter); + } + if (!$this->formatter instanceof Formatter) { + throw new InvalidConfigException('The "formatter" property must be either a Format object or a configuration array.'); + } + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + if (!isset($this->filterRowOptions['id'])) { + $this->filterRowOptions['id'] = $this->options['id'] . '-filters'; + } + + $this->initColumns(); + } + + /** + * Runs the widget. + */ + public function run() + { + $id = $this->options['id']; + $options = Json::encode($this->getClientOptions()); + $view = $this->getView(); + GridViewAsset::register($view); + $view->registerJs("jQuery('#$id').yiiGridView($options);"); + parent::run(); + } + + + /** + * Returns the options for the grid view JS widget. + * @return array the options + */ + protected function getClientOptions() + { + $filterUrl = isset($this->filterUrl) ? $this->filterUrl : array(Yii::$app->controller->action->id); + $id = $this->filterRowOptions['id']; + $filterSelector = "#$id input, #$id select"; + if (isset($this->filterSelector)) { + $filterSelector .= ', ' . $this->filterSelector; + } + + return array( + 'filterUrl' => Html::url($filterUrl), + 'filterSelector' => $filterSelector, + ); + } + + /** + * Renders the data models for the grid view. + */ + public function renderItems() + { + $content = array_filter(array( + $this->renderCaption(), + $this->renderColumnGroup(), + $this->showHeader ? $this->renderTableHeader() : false, + $this->showFooter ? $this->renderTableFooter() : false, + $this->renderTableBody(), + )); + return Html::tag('table', implode("\n", $content), $this->tableOptions); + } + + public function renderCaption() + { + if (!empty($this->caption)) { + return Html::tag('caption', $this->caption, $this->captionOptions); + } else { + return false; + } + } + + public function renderColumnGroup() + { + $requireColumnGroup = false; + foreach ($this->columns as $column) { + /** @var Column $column */ + if (!empty($column->options)) { + $requireColumnGroup = true; + break; + } + } + if ($requireColumnGroup) { + $cols = array(); + foreach ($this->columns as $column) { + $cols[] = Html::tag('col', '', $column->options); + } + return Html::tag('colgroup', implode("\n", $cols)); + } else { + return false; + } + } + + /** + * Renders the table header. + * @return string the rendering result + */ + public function renderTableHeader() + { + $cells = array(); + foreach ($this->columns as $column) { + /** @var Column $column */ + $cells[] = $column->renderHeaderCell(); + } + $content = implode('', $cells); + if ($this->filterPosition == self::FILTER_POS_HEADER) { + $content = $this->renderFilters() . $content; + } elseif ($this->filterPosition == self::FILTER_POS_BODY) { + $content .= $this->renderFilters(); + } + return "<thead>\n" . Html::tag('tr', $content, $this->headerRowOptions) . "\n</thead>"; + } + + /** + * Renders the table footer. + * @return string the rendering result + */ + public function renderTableFooter() + { + $cells = array(); + foreach ($this->columns as $column) { + /** @var Column $column */ + $cells[] = $column->renderFooterCell(); + } + $content = implode('', $cells); + if ($this->filterPosition == self::FILTER_POS_FOOTER) { + $content .= $this->renderFilters(); + } + return "<tfoot>\n" . Html::tag('tr', $content, $this->footerRowOptions) . "\n</tfoot>"; + } + + /** + * Renders the filter. + */ + public function renderFilters() + { + if ($this->filterModel !== null) { + $cells = array(); + foreach ($this->columns as $column) { + /** @var Column $column */ + $cells[] = $column->renderFilterCell(); + } + return Html::tag('tr', implode('', $cells), $this->filterRowOptions); + } else { + return ''; + } + } + + /** + * Renders the table body. + * @return string the rendering result + */ + public function renderTableBody() + { + $models = array_values($this->dataProvider->getModels()); + $keys = $this->dataProvider->getKeys(); + $rows = array(); + foreach ($models as $index => $model) { + $key = $keys[$index]; + if ($this->beforeRow !== null) { + $row = call_user_func($this->beforeRow, $model, $key, $index, $this); + if (!empty($row)) { + $rows[] = $row; + } + } + + $rows[] = $this->renderTableRow($model, $key, $index); + + if ($this->afterRow !== null) { + $row = call_user_func($this->afterRow, $model, $key, $index, $this); + if (!empty($row)) { + $rows[] = $row; + } + } + } + return "<tbody>\n" . implode("\n", $rows) . "\n</tbody>"; + } + + /** + * Renders a table row with the given data model and key. + * @param mixed $model the data model to be rendered + * @param mixed $key the key associated with the data model + * @param integer $index the zero-based index of the data model among the model array returned by [[dataProvider]]. + * @return string the rendering result + */ + public function renderTableRow($model, $key, $index) + { + $cells = array(); + /** @var Column $column */ + foreach ($this->columns as $column) { + $cells[] = $column->renderDataCell($model, $index); + } + if ($this->rowOptions instanceof Closure) { + $options = call_user_func($this->rowOptions, $model, $key, $index, $this); + } else { + $options = $this->rowOptions; + } + $options['data-key'] = $key; + return Html::tag('tr', implode('', $cells), $options); + } + + /** + * Creates column objects and initializes them. + */ + protected function initColumns() + { + if (empty($this->columns)) { + $this->guessColumns(); + } + foreach ($this->columns as $i => $column) { + if (is_string($column)) { + $column = $this->createDataColumn($column); + } else { + $column = Yii::createObject(array_merge(array( + 'class' => $this->dataColumnClass ?: DataColumn::className(), + 'grid' => $this, + ), $column)); + } + if (!$column->visible) { + unset($this->columns[$i]); + continue; + } + $this->columns[$i] = $column; + } + } + + /** + * Creates a [[DataColumn]] object based on a string in the format of "attribute:format:label". + * @param string $text the column specification string + * @return DataColumn the column instance + * @throws InvalidConfigException if the column specification is invalid + */ + protected function createDataColumn($text) + { + if (!preg_match('/^([\w\.]+)(:(\w*))?(:(.*))?$/', $text, $matches)) { + throw new InvalidConfigException('The column must be specified in the format of "attribute", "attribute:format" or "attribute:format:label'); + } + return Yii::createObject(array( + 'class' => $this->dataColumnClass ?: DataColumn::className(), + 'grid' => $this, + 'attribute' => $matches[1], + 'format' => isset($matches[3]) ? $matches[3] : 'text', + 'label' => isset($matches[5]) ? $matches[5] : null, + )); + } + + protected function guessColumns() + { + $models = $this->dataProvider->getModels(); + $model = reset($models); + if (is_array($model) || is_object($model)) { + foreach ($model as $name => $value) { + $this->columns[] = $name; + } + } else { + throw new InvalidConfigException('Unable to generate columns from data.'); + } + } +} diff --git a/framework/yii/grid/GridViewAsset.php b/framework/yii/grid/GridViewAsset.php new file mode 100644 index 0000000..decf674 --- /dev/null +++ b/framework/yii/grid/GridViewAsset.php @@ -0,0 +1,26 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\grid; + +use yii\web\AssetBundle; + +/** + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class GridViewAsset extends AssetBundle +{ + public $sourcePath = '@yii/assets'; + public $js = array( + 'yii.gridView.js', + ); + public $depends = array( + 'yii\web\YiiAsset', + ); +} diff --git a/framework/yii/grid/SerialColumn.php b/framework/yii/grid/SerialColumn.php new file mode 100644 index 0000000..6a875ae --- /dev/null +++ b/framework/yii/grid/SerialColumn.php @@ -0,0 +1,35 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\grid; + +/** + * SerialColumn displays a column of row numbers (1-based). + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class SerialColumn extends Column +{ + public $header = '#'; + + /** + * Renders the data cell content. + * @param mixed $model the data model + * @param integer $index the zero-based index of the data model among the models array returned by [[dataProvider]]. + * @return string the rendering result + */ + protected function renderDataCellContent($model, $index) + { + $pagination = $this->grid->dataProvider->getPagination(); + if ($pagination !== false) { + return $pagination->getOffset() + $index + 1; + } else { + return $index + 1; + } + } +} diff --git a/yii/helpers/ArrayHelper.php b/framework/yii/helpers/ArrayHelper.php similarity index 88% rename from yii/helpers/ArrayHelper.php rename to framework/yii/helpers/ArrayHelper.php index d58341c..9d428f5 100644 --- a/yii/helpers/ArrayHelper.php +++ b/framework/yii/helpers/ArrayHelper.php @@ -8,12 +8,12 @@ namespace yii\helpers; /** - * ArrayHelper provides additional array functionality you can use in your + * ArrayHelper provides additional array functionality that you can use in your * application. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class ArrayHelper extends base\ArrayHelper +class ArrayHelper extends BaseArrayHelper { } diff --git a/yii/helpers/base/ArrayHelper.php b/framework/yii/helpers/BaseArrayHelper.php similarity index 71% rename from yii/helpers/base/ArrayHelper.php rename to framework/yii/helpers/BaseArrayHelper.php index e482883..0ed584f 100644 --- a/yii/helpers/base/ArrayHelper.php +++ b/framework/yii/helpers/BaseArrayHelper.php @@ -5,21 +5,91 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\helpers\base; +namespace yii\helpers; use Yii; +use yii\base\Arrayable; use yii\base\InvalidParamException; /** - * ArrayHelper provides additional array functionality you can use in your - * application. + * BaseArrayHelper provides concrete implementation for [[ArrayHelper]]. + * + * Do not use BaseArrayHelper. Use [[ArrayHelper]] instead. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class ArrayHelper +class BaseArrayHelper { /** + * Converts an object or an array of objects into an array. + * @param object|array $object the object to be converted into an array + * @param array $properties a mapping from object class names to the properties that need to put into the resulting arrays. + * The properties specified for each class is an array of the following format: + * + * ~~~ + * array( + * 'app\models\Post' => array( + * 'id', + * 'title', + * // the key name in array result => property name + * 'createTime' => 'create_time', + * // the key name in array result => anonymous function + * 'length' => function ($post) { + * return strlen($post->content); + * }, + * ), + * ) + * ~~~ + * + * The result of `ArrayHelper::toArray($post, $properties)` could be like the following: + * + * ~~~ + * array( + * 'id' => 123, + * 'title' => 'test', + * 'createTime' => '2013-01-01 12:00AM', + * 'length' => 301, + * ) + * ~~~ + * + * @param boolean $recursive whether to recursively converts properties which are objects into arrays. + * @return array the array representation of the object + */ + public static function toArray($object, $properties = array(), $recursive = true) + { + if (!empty($properties) && is_object($object)) { + $className = get_class($object); + if (!empty($properties[$className])) { + $result = array(); + foreach ($properties[$className] as $key => $name) { + if (is_int($key)) { + $result[$name] = $object->$name; + } else { + $result[$key] = static::getValue($object, $name); + } + } + return $result; + } + } + if ($object instanceof Arrayable) { + $object = $object->toArray(); + if (!$recursive) { + return $object; + } + } + $result = array(); + foreach ($object as $key => $value) { + if ($recursive && (is_array($value) || is_object($value))) { + $result[$key] = static::toArray($value, true); + } else { + $result[$key] = $value; + } + } + return $result; + } + + /** * 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). @@ -73,7 +143,7 @@ class ArrayHelper * 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 + * @return mixed the value of the element if found, default value otherwise */ public static function getValue($array, $key, $default = null) { @@ -93,11 +163,11 @@ class ArrayHelper * Usage examples, * * ~~~ - * // $array = array('type'=>'A', 'options'=>array(1,2)); + * // $array = array('type' => 'A', 'options' => array(1, 2)); * // working with array * $type = \yii\helpers\ArrayHelper::remove($array, 'type'); * // $array content - * // $array = array('options'=>array(1,2)); + * // $array = array('options' => array(1, 2)); * ~~~ * * @param array $array the array to extract value from @@ -139,7 +209,7 @@ class ArrayHelper * // ) * * // using anonymous function - * $result = ArrayHelper::index($array, function(element) { + * $result = ArrayHelper::index($array, function ($element) { * return $element['id']; * }); * ~~~ @@ -173,7 +243,7 @@ class ArrayHelper * // the result is: array( '123', '345') * * // using anonymous function - * $result = ArrayHelper::getColumn($array, function(element) { + * $result = ArrayHelper::getColumn($array, function ($element) { * return $element['id']; * }); * ~~~ @@ -263,8 +333,8 @@ class ArrayHelper * 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 boolean|array $descending whether to sort in descending or ascending order. When + * sorting by multiple keys with different descending orders, use an array of descending flags. * @param integer|array $sortFlag the PHP sort flag. Valid values include * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING` and `SORT_LOCALE_STRING`. * Please refer to [PHP manual](http://php.net/manual/en/function.sort.php) @@ -272,20 +342,20 @@ class ArrayHelper * @param boolean|array $caseSensitive whether to sort string in case-sensitive manner. This parameter * is used only when `$sortFlag` is `SORT_STRING`. * When sorting by multiple keys with different case sensitivities, use an array of boolean values. - * @throws InvalidParamException if the $ascending or $sortFlag parameters do not have + * @throws InvalidParamException if the $descending 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, $caseSensitive = true) + public static function multisort(&$array, $key, $descending = false, $sortFlag = SORT_REGULAR, $caseSensitive = true) { $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($descending)) { + $descending = array_fill(0, $n, $descending); + } elseif (count($descending) !== $n) { + throw new InvalidParamException('The length of $descending parameter must be the same as that of $keys.'); } if (is_scalar($sortFlag)) { $sortFlag = array_fill(0, $n, $sortFlag); @@ -315,7 +385,7 @@ class ArrayHelper } else { $args[] = static::getColumn($array, $key); } - $args[] = $ascending[$i] ? SORT_ASC : SORT_DESC; + $args[] = $descending[$i] ? SORT_DESC : SORT_ASC; $args[] = $flag; } $args[] = &$array; diff --git a/yii/helpers/base/Console.php b/framework/yii/helpers/BaseConsole.php similarity index 84% rename from yii/helpers/base/Console.php rename to framework/yii/helpers/BaseConsole.php index b611919..3f11031 100644 --- a/yii/helpers/base/Console.php +++ b/framework/yii/helpers/BaseConsole.php @@ -5,27 +5,17 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\helpers\base; +namespace yii\helpers; /** - * TODO adjust phpdoc - * TODO test this on all kinds of terminals, especially windows (check out lib ncurses) - * - * Console View is the base class for console view components - * - * A console view provides functionality to create rich console application by allowing to format output - * by adding color and font style to it. - * - * The following constants are available for formatting: - * - * TODO document constants - * + * BaseConsole provides concrete implementation for [[Console]]. * + * Do not use BaseConsole. Use [[Console]] instead. * * @author Carsten Brandt <mail@cebe.cc> * @since 2.0 */ -class Console +class BaseConsole { const FG_BLACK = 30; const FG_RED = 31; @@ -45,6 +35,7 @@ class Console const BG_CYAN = 46; const BG_GREY = 47; + const RESET = 0; const NORMAL = 0; const BOLD = 1; const ITALIC = 3; @@ -240,59 +231,87 @@ class Console } /** - * Sets the ANSI format for any text that is printed afterwards. + * Returns the ANSI format code. * - * You can pass any of the FG_*, BG_* and TEXT_* constants and also [[xterm256ColorFg]] and [[xterm256ColorBg]]. - * TODO: documentation + * @param array $format An array containing formatting values. + * You can pass any of the FG_*, BG_* and TEXT_* constants + * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format. + * @return string The ANSI format code according to the given formatting constants. */ - public static function ansiFormatBegin() + public static function ansiFormatCode($format) { - echo "\033[" . implode(';', func_get_args()) . 'm'; + return "\033[" . implode(';', $format) . 'm'; } /** - * Resets any ANSI format set by previous method [[ansiFormatBegin()]] - * Any output after this is will have default text style. + * Echoes an ANSI format code that affects the formatting of any text that is printed afterwards. + * + * @param array $format An array containing formatting values. + * You can pass any of the FG_*, BG_* and TEXT_* constants + * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format. + * @see ansiFormatCode() + * @see ansiFormatEnd() */ - public static function ansiFormatReset() + public static function beginAnsiFormat($format) { - echo "\033[0m"; + echo "\033[" . implode(';', $format) . 'm'; } /** - * Returns the ANSI format code. + * Resets any ANSI format set by previous method [[ansiFormatBegin()]] + * Any output after this will have default text format. + * This is equal to calling * - * You can pass any of the FG_*, BG_* and TEXT_* constants and also [[xterm256ColorFg]] and [[xterm256ColorBg]]. - * TODO: documentation + * ```php + * echo Console::ansiFormatCode(array(Console::RESET)) + * ``` */ - public static function ansiFormatCode($format) + public static function endAnsiFormat() { - return "\033[" . implode(';', $format) . 'm'; + echo "\033[0m"; } /** * Will return a string formatted with the given ANSI style * * @param string $string the string to be formatted - * @param array $format array containing formatting values. - * You can pass any of the FG_*, BG_* and TEXT_* constants and also [[xterm256ColorFg]] and [[xterm256ColorBg]]. + * @param array $format An array containing formatting values. + * You can pass any of the FG_*, BG_* and TEXT_* constants + * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format. * @return string */ - public static function ansiFormat($string, $format=array()) + public static function ansiFormat($string, $format = array()) { $code = implode(';', $format); 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! + /** + * Returns the ansi format code for xterm foreground color. + * You can pass the return value of this to one of the formatting methods: + * [[ansiFormat]], [[ansiFormatCode]], [[beginAnsiFormat]] + * + * @param integer $colorCode xterm color code + * @return string + * @see http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors + */ + public static function xtermFgColor($colorCode) { - return '38;5;' . $i; + return '38;5;' . $colorCode; } - public static function xterm256ColorBg($i) // TODO naming! + /** + * Returns the ansi format code for xterm background color. + * You can pass the return value of this to one of the formatting methods: + * [[ansiFormat]], [[ansiFormatCode]], [[beginAnsiFormat]] + * + * @param integer $colorCode xterm color code + * @return string + * @see http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors + */ + public static function xtermBgColor($colorCode) { - return '48;5;' . $i; + return '48;5;' . $colorCode; } /** @@ -303,10 +322,15 @@ class Console */ public static function stripAnsiFormat($string) { - return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color + return preg_replace('/\033\[[\d;?]*\w/', '', $string); } - // TODO refactor and review + /** + * Converts an ANSI formatted string to HTML + * @param $string + * @return mixed + */ + // TODO rework/refactor according to https://github.com/yiisoft/yii2/issues/746 public static function ansiToHtml($string) { $tags = 0; @@ -412,16 +436,17 @@ class Console ); } + // TODO rework/refactor according to https://github.com/yiisoft/yii2/issues/746 public function markdownToAnsi() { // TODO implement } /** - * TODO syntax copied from https://github.com/pear/Console_Color2/blob/master/Console/Color2.php + * Converts a string to ansi formatted by replacing patterns like %y (for yellow) with ansi control codes * - * Converts colorcodes in the format %y (for yellow) into ansi-control - * codes. The conversion table is: ('bold' meaning 'light' on some + * Uses almost the same syntax as https://github.com/pear/Console_Color2/blob/master/Console/Color2.php + * The conversion table is: ('bold' meaning 'light' on some * terminals). It's almost the same conversion table irssi uses. * <pre> * text text background @@ -450,9 +475,9 @@ class Console * * @param string $string String to convert * @param bool $colored Should the string be colored? - * * @return string */ + // TODO rework/refactor according to https://github.com/yiisoft/yii2/issues/746 public static function renderColoredString($string, $colored = true) { static $conversions = array( @@ -508,22 +533,24 @@ class Console } /** - * Escapes % so they don't get interpreted as color codes - * - * @param string $string String to escape - * - * @access public - * @return string - */ + * Escapes % so they don't get interpreted as color codes when + * the string is parsed by [[renderColoredString]] + * + * @param string $string String to escape + * + * @access public + * @return string + */ + // TODO rework/refactor according to https://github.com/yiisoft/yii2/issues/746 public static function escape($string) { return str_replace('%', '%%', $string); } /** - * Returns true if the stream supports colorization. ANSI colors is disabled if not supported by the stream. + * Returns true if the stream supports colorization. ANSI colors are disabled if not supported by the stream. * - * - windows without asicon + * - windows without ansicon * - not tty consoles * * @param mixed $stream @@ -532,7 +559,7 @@ class Console public static function streamSupportsAnsiColors($stream) { return DIRECTORY_SEPARATOR == '\\' - ? null !== getenv('ANSICON') + ? getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON' : function_exists('posix_isatty') && @posix_isatty($stream); } @@ -542,18 +569,49 @@ class Console */ public static function isRunningOnWindows() { - return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; + return DIRECTORY_SEPARATOR == '\\'; } /** - * Usage: list($w, $h) = ConsoleHelper::getScreenSize(); + * Usage: list($width, $height) = ConsoleHelper::getScreenSize(); * - * @return array + * @param bool $refresh whether to force checking and not re-use cached size value. + * This is useful to detect changing window size while the application is running but may + * not get up to date values on every terminal. + * @return array|boolean An array of ($width, $height) or false when it was not able to determine size. */ - public static function getScreenSize() + public static function getScreenSize($refresh = false) { - // TODO implement - return array(150, 50); + static $size; + if ($size !== null && !$refresh) { + return $size; + } + + if (static::isRunningOnWindows()) { + $output = array(); + exec('mode con', $output); + if (isset($output) && strpos($output[1], 'CON') !== false) { + return $size = array((int)preg_replace('~[^0-9]~', '', $output[3]), (int)preg_replace('~[^0-9]~', '', $output[4])); + } + } else { + // try stty if available + $stty = array(); + if (exec('stty -a 2>&1', $stty) && preg_match('/rows\s+(\d+);\s*columns\s+(\d+);/mi', implode(' ', $stty), $matches)) { + return $size = array($matches[2], $matches[1]); + } + + // fallback to tput, which may not be updated on terminal resize + if (($width = (int) exec('tput cols 2>&1')) > 0 && ($height = (int) exec('tput lines 2>&1')) > 0) { + return $size = array($width, $height); + } + + // fallback to ENV variables, which may not be updated on terminal resize + if (($width = (int) getenv('COLUMNS')) > 0 && ($height = (int) getenv('LINES')) > 0) { + return $size = array($width, $height); + } + } + + return $size = false; } /** @@ -607,27 +665,23 @@ class Console /** * Prints text to STDOUT appended with a carriage return (PHP_EOL). * - * @param string $text - * @param bool $raw - * - * @return mixed Number of bytes printed or bool false on error + * @param string $string the text to print + * @return integer|boolean number of bytes printed or false on error. */ - public static function output($text = null) + public static function output($string = null) { - return static::stdout($text . PHP_EOL); + return static::stdout($string . PHP_EOL); } /** * Prints text to STDERR appended with a carriage return (PHP_EOL). * - * @param string $text - * @param bool $raw - * - * @return mixed Number of bytes printed or false on error + * @param string $string the text to print + * @return integer|boolean number of bytes printed or false on error. */ - public static function error($text = null) + public static function error($string = null) { - return static::stderr($text . PHP_EOL); + return static::stderr($string . PHP_EOL); } /** @@ -646,14 +700,14 @@ class Console public static function prompt($text, $options = array()) { $options = ArrayHelper::merge( - $options, array( 'required' => false, 'default' => null, 'pattern' => null, 'validator' => null, 'error' => 'Invalid input.', - ) + ), + $options ); $error = null; diff --git a/yii/helpers/base/FileHelper.php b/framework/yii/helpers/BaseFileHelper.php similarity index 60% rename from yii/helpers/base/FileHelper.php rename to framework/yii/helpers/BaseFileHelper.php index 954c86e..7540168 100644 --- a/yii/helpers/base/FileHelper.php +++ b/framework/yii/helpers/BaseFileHelper.php @@ -7,18 +7,20 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\helpers\base; +namespace yii\helpers; use Yii; /** - * Filesystem helper + * BaseFileHelper provides concrete implementation for [[FileHelper]]. + * + * Do not use BaseFileHelper. Use [[FileHelper]] instead. * * @author Qiang Xue <qiang.xue@gmail.com> * @author Alex Makarov <sam@rmcreative.ru> * @since 2.0 */ -class FileHelper +class BaseFileHelper { /** * Normalizes a file/directory path. @@ -95,7 +97,7 @@ class FileHelper } } - return $checkExtension ? self::getMimeTypeByExtension($file) : null; + return $checkExtension ? static::getMimeTypeByExtension($file) : null; } /** @@ -131,19 +133,40 @@ class FileHelper * @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. + * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775. * - 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 each directory or file. + * The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered. + * The callback can return one of the following values: + * + * * true: the directory or file will be copied (the "only" and "except" options will be ignored) + * * false: the directory or file will NOT be copied (the "only" and "except" options will be ignored) + * * null: the "only" and "except" options will determine whether the directory or file should be copied + * + * - only: array, list of patterns that the file paths should match if they want to be copied. + * A path matches a pattern if it contains the pattern string at its end. + * For example, '.php' matches all file paths ending with '.php'. + * Note, the '/' characters in a pattern matches both '/' and '\' in the paths. + * If a file path matches a pattern in both "only" and "except", it will NOT be copied. + * - except: array, list of patterns that the files or directories should match if they want to be excluded from being copied. + * A path matches a pattern if it contains the pattern string at its end. + * Patterns ending with '/' apply to directory paths only, and patterns not ending with '/' + * apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b'; + * and '.svn/' matches directory paths ending with '.svn'. Note, the '/' characters in a pattern matches + * both '/' and '\' in the paths. + * - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true. * - 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`. + * file to be copied from, while `$to` is the copy target. + * - afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied. + * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or + * file copied from, while `$to` is the copy target. */ public static function copyDirectory($src, $dst, $options = array()) { if (!is_dir($dst)) { - mkdir($dst, isset($options['dirMode']) ? $options['dirMode'] : 0777, true); + static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true); } $handle = opendir($src); @@ -153,7 +176,10 @@ class FileHelper } $from = $src . DIRECTORY_SEPARATOR . $file; $to = $dst . DIRECTORY_SEPARATOR . $file; - if (!isset($options['beforeCopy']) || call_user_func($options['beforeCopy'], $from, $to)) { + if (static::filterPath($from, $options)) { + if (isset($options['beforeCopy']) && !call_user_func($options['beforeCopy'], $from, $to)) { + continue; + } if (is_file($from)) { copy($from, $to); if (isset($options['fileMode'])) { @@ -169,4 +195,142 @@ class FileHelper } closedir($handle); } + + /** + * Removes a directory (and all its content) recursively. + * @param string $dir the directory to be deleted recursively. + */ + public static function removeDirectory($dir) + { + if (!is_dir($dir) || !($handle = opendir($dir))) { + return; + } + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $path = $dir . DIRECTORY_SEPARATOR . $file; + if (is_file($path)) { + unlink($path); + } else { + static::removeDirectory($path); + } + } + closedir($handle); + rmdir($dir); + } + + /** + * Returns the files found under the specified directory and subdirectories. + * @param string $dir the directory under which the files will be looked for. + * @param array $options options for file searching. Valid options are: + * + * - filter: callback, a PHP callback that is called for each directory or file. + * The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered. + * The callback can return one of the following values: + * + * * true: the directory or file will be returned (the "only" and "except" options will be ignored) + * * false: the directory or file will NOT be returned (the "only" and "except" options will be ignored) + * * null: the "only" and "except" options will determine whether the directory or file should be returned + * + * - only: array, list of patterns that the file paths should match if they want to be returned. + * A path matches a pattern if it contains the pattern string at its end. + * For example, '.php' matches all file paths ending with '.php'. + * Note, the '/' characters in a pattern matches both '/' and '\' in the paths. + * If a file path matches a pattern in both "only" and "except", it will NOT be returned. + * - except: array, list of patterns that the file paths or directory paths should match if they want to be excluded from the result. + * A path matches a pattern if it contains the pattern string at its end. + * Patterns ending with '/' apply to directory paths only, and patterns not ending with '/' + * apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b'; + * and '.svn/' matches directory paths ending with '.svn'. Note, the '/' characters in a pattern matches + * both '/' and '\' in the paths. + * - recursive: boolean, whether the files under the subdirectories should also be looked for. Defaults to true. + * @return array files found under the directory. The file list is sorted. + */ + public static function findFiles($dir, $options = array()) + { + $list = array(); + $handle = opendir($dir); + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $path = $dir . DIRECTORY_SEPARATOR . $file; + if (static::filterPath($path, $options)) { + if (is_file($path)) { + $list[] = $path; + } elseif (!isset($options['recursive']) || $options['recursive']) { + $list = array_merge($list, static::findFiles($path, $options)); + } + } + } + closedir($handle); + return $list; + } + + /** + * Checks if the given file path satisfies the filtering options. + * @param string $path the path of the file or directory to be checked + * @param array $options the filtering options. See [[findFiles()]] for explanations of + * the supported options. + * @return boolean whether the file or directory satisfies the filtering options. + */ + public static function filterPath($path, $options) + { + if (isset($options['filter'])) { + $result = call_user_func($options['filter'], $path); + if (is_bool($result)) { + return $result; + } + } + $path = str_replace('\\', '/', $path); + if ($isDir = is_dir($path)) { + $path .= '/'; + } + $n = StringHelper::strlen($path); + + if (!empty($options['except'])) { + foreach ($options['except'] as $name) { + if (StringHelper::substr($path, -StringHelper::strlen($name), $n) === $name) { + return false; + } + } + } + + if (!$isDir && !empty($options['only'])) { + foreach ($options['only'] as $name) { + if (StringHelper::substr($path, -StringHelper::strlen($name), $n) === $name) { + return true; + } + } + return false; + } + return true; + } + + /** + * Creates a new directory. + * + * This method is similar to the PHP `mkdir()` function except that + * it uses `chmod()` to set the permission of the created directory + * in order to avoid the impact of the `umask` setting. + * + * @param string $path path of the directory to be created. + * @param integer $mode the permission to be set for the created directory. + * @param boolean $recursive whether to create parent directories if they do not exist. + * @return boolean whether the directory is created successfully + */ + public static function createDirectory($path, $mode = 0775, $recursive = true) + { + if (is_dir($path)) { + return true; + } + $parentDir = dirname($path); + if ($recursive && !is_dir($parentDir)) { + static::createDirectory($parentDir, $mode, true); + } + $result = mkdir($path, $mode); + chmod($path, $mode); + return $result; + } } diff --git a/yii/helpers/base/Html.php b/framework/yii/helpers/BaseHtml.php similarity index 78% rename from yii/helpers/base/Html.php rename to framework/yii/helpers/BaseHtml.php index f601772..ff12cd5 100644 --- a/yii/helpers/base/Html.php +++ b/framework/yii/helpers/BaseHtml.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\helpers\base; +namespace yii\helpers; use Yii; use yii\base\InvalidParamException; @@ -13,21 +13,17 @@ use yii\web\Request; use yii\base\Model; /** - * Html provides a set of static methods for generating commonly used HTML tags. + * BaseHtml provides concrete implementation for [[Html]]. + * + * Do not use BaseHtml. Use [[Html]] instead. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Html +class BaseHtml { /** - * @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( @@ -49,48 +45,8 @@ class Html '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()]]. + * that are rendered by [[renderTagAttributes()]]. */ public static $attributeOrder = array( 'type', @@ -165,12 +121,8 @@ class Html */ public static function tag($name, $content = '', $options = array()) { - $html = '<' . $name . static::renderTagAttributes($options); - if (isset(static::$voidElements[strtolower($name)])) { - return $html . (static::$closeVoidElements ? ' />' : '>'); - } else { - return $html . ">$content</$name>"; - } + $html = "<$name" . static::renderTagAttributes($options) . '>'; + return isset(static::$voidElements[strtolower($name)]) ? $html : "$html$content</$name>"; } /** @@ -185,7 +137,7 @@ class Html */ public static function beginTag($name, $options = array()) { - return '<' . $name . static::renderTagAttributes($options) . '>'; + return "<$name" . static::renderTagAttributes($options) . '>'; } /** @@ -201,16 +153,6 @@ class Html } /** - * Encloses the given content within a CDATA tag. - * @param string $content the content to be enclosed within the CDATA tag - * @return string the CDATA tag with the enclosed content. - */ - public static function cdata($content) - { - return '<![CDATA[' . $content . ']]>'; - } - - /** * Generates a style tag. * @param string $content the style content * @param array $options the tag options in terms of name-value pairs. These will be rendered as @@ -221,10 +163,7 @@ class Html */ public static function style($content, $options = array()) { - if (!isset($options['type'])) { - $options['type'] = 'text/css'; - } - return static::tag('style', "/*<![CDATA[*/\n{$content}\n/*]]>*/", $options); + return static::tag('style', $content, $options); } /** @@ -238,10 +177,7 @@ class Html */ public static function script($content, $options = array()) { - if (!isset($options['type'])) { - $options['type'] = 'text/javascript'; - } - return static::tag('script', "/*<![CDATA[*/\n{$content}\n/*]]>*/", $options); + return static::tag('script', $content, $options); } /** @@ -256,7 +192,6 @@ class Html public static function cssFile($url, $options = array()) { $options['rel'] = 'stylesheet'; - $options['type'] = 'text/css'; $options['href'] = static::url($url); return static::tag('link', '', $options); } @@ -272,7 +207,6 @@ class Html */ public static function jsFile($url, $options = array()) { - $options['type'] = 'text/javascript'; $options['src'] = static::url($url); return static::tag('script', '', $options); } @@ -303,8 +237,8 @@ class Html $hiddenInputs[] = static::hiddenInput($request->restVar, $method); $method = 'post'; } - if ($request->enableCsrfValidation) { - $hiddenInputs[] = static::hiddenInput($request->csrfTokenName, $request->getCsrfToken()); + if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) { + $hiddenInputs[] = static::hiddenInput($request->csrfVar, $request->getCsrfToken()); } } @@ -313,7 +247,10 @@ class Html // we use hidden fields to add them back foreach (explode('&', substr($action, $pos + 1)) as $pair) { if (($pos1 = strpos($pair, '=')) !== false) { - $hiddenInputs[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1))); + $hiddenInputs[] = static::hiddenInput( + urldecode(substr($pair, 0, $pos1)), + urldecode(substr($pair, $pos1 + 1)) + ); } else { $hiddenInputs[] = static::hiddenInput(urldecode($pair), ''); } @@ -344,7 +281,7 @@ class Html /** * Generates a hyperlink tag. * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * such as an image tag. If this 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 @@ -366,7 +303,7 @@ class Html /** * 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()]] + * such as an image tag. If this 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. @@ -395,7 +332,7 @@ class Html if (!isset($options['alt'])) { $options['alt'] = ''; } - return static::tag('img', null, $options); + return static::tag('img', '', $options); } /** @@ -421,21 +358,13 @@ class Html * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, * you should consider [[encode()]] it to prevent XSS attacks. - * @param 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. - * 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($content = 'Button', $name = null, $value = null, $options = array()) + public static function button($content = 'Button', $options = array()) { - $options['name'] = $name; - $options['value'] = $value; - if (!isset($options['type'])) { - $options['type'] = 'button'; - } return static::tag('button', $content, $options); } @@ -444,17 +373,15 @@ class Html * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, * you should consider [[encode()]] it to prevent XSS attacks. - * @param 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 submit button tag */ - public static function submitButton($content = 'Submit', $name = null, $value = null, $options = array()) + public static function submitButton($content = 'Submit', $options = array()) { $options['type'] = 'submit'; - return static::button($content, $name, $value, $options); + return static::button($content, $options); } /** @@ -462,17 +389,15 @@ class Html * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, * you should consider [[encode()]] it to prevent XSS attacks. - * @param 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 reset button tag */ - public static function resetButton($content = 'Reset', $name = null, $value = null, $options = array()) + public static function resetButton($content = 'Reset', $options = array()) { $options['type'] = 'reset'; - return static::button($content, $name, $value, $options); + return static::button($content, $options); } /** @@ -489,49 +414,52 @@ class Html { $options['type'] = $type; $options['name'] = $name; - $options['value'] = $value; - return static::tag('input', null, $options); + $options['value'] = $value === null ? null : (string)$value; + return static::tag('input', '', $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 string $label 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()) + public static function buttonInput($label = 'Button', $options = array()) { - return static::input('button', $name, $value, $options); + $options['type'] = 'button'; + $options['value'] = $label; + return static::tag('input', '', $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 string $label 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()) + public static function submitInput($label = 'Submit', $options = array()) { - return static::input('submit', $name, $value, $options); + $options['type'] = 'submit'; + $options['value'] = $label; + return static::tag('input', '', $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 string $label 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()) + public static function resetInput($label = 'Reset', $options = array()) { - return static::input('reset', $name, $value, $options); + $options['type'] = 'reset'; + $options['value'] = $label; + return static::tag('input', '', $options); } /** @@ -617,6 +545,10 @@ class Html * - 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. + * - label: string, a label displayed next to the radio button. 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 [[encode()]] it to prevent XSS attacks. + * When this option is specified, the radio button will be enclosed by a label tag. + * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. * * 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. @@ -625,7 +557,7 @@ class Html */ public static function radio($name, $checked = false, $options = array()) { - $options['checked'] = $checked; + $options['checked'] = (boolean)$checked; $value = array_key_exists('value', $options) ? $options['value'] : '1'; if (isset($options['uncheck'])) { // add a hidden field so that if the radio button is not selected, it still submits a value @@ -634,7 +566,15 @@ class Html } else { $hidden = ''; } - return $hidden . static::input('radio', $name, $value, $options); + if (isset($options['label'])) { + $label = $options['label']; + $labelOptions = isset($options['labelOptions']) ? $options['labelOptions'] : array(); + unset($options['label'], $options['labelOptions']); + $content = static::label(static::input('radio', $name, $value, $options) . ' ' . $label, null, $labelOptions); + return $hidden . static::tag('div', $content, array('class' => 'radio')); + } else { + return $hidden . static::input('radio', $name, $value, $options); + } } /** @@ -646,6 +586,10 @@ class Html * - 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. + * - label: string, a label displayed next to the checkbox. 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 [[encode()]] it to prevent XSS attacks. + * When this option is specified, the checkbox will be enclosed by a label tag. + * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. * * 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. @@ -654,7 +598,7 @@ class Html */ public static function checkbox($name, $checked = false, $options = array()) { - $options['checked'] = $checked; + $options['checked'] = (boolean)$checked; $value = array_key_exists('value', $options) ? $options['value'] : '1'; if (isset($options['uncheck'])) { // add a hidden field so that if the checkbox is not selected, it still submits a value @@ -663,7 +607,15 @@ class Html } else { $hidden = ''; } - return $hidden . static::input('checkbox', $name, $value, $options); + if (isset($options['label'])) { + $label = $options['label']; + $labelOptions = isset($options['labelOptions']) ? $options['labelOptions'] : array(); + unset($options['label'], $options['labelOptions']); + $content = static::label(static::input('checkbox', $name, $value, $options) . ' ' . $label, null, $labelOptions); + return $hidden . static::tag('div', $content, array('class' => 'checkbox')); + } else { + return $hidden . static::input('checkbox', $name, $value, $options); + } } /** @@ -773,11 +725,14 @@ class Html * @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: + * @param array $options options (name => config) for the checkbox list container tag. + * The following options are specially handled: * + * - tag: string, the tag name of the container element. * - 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. + * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. + * This option is ignored if `item` option is set. * - 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: @@ -798,6 +753,7 @@ class Html } $formatter = isset($options['item']) ? $options['item'] : null; + $encode = !isset($options['encode']) || $options['encode']; $lines = array(); $index = 0; foreach ($items as $value => $label) { @@ -807,7 +763,10 @@ class Html if ($formatter !== null) { $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { - $lines[] = static::label(static::checkbox($name, $checked, array('value' => $value)) . ' ' . $label); + $lines[] = static::checkbox($name, $checked, array( + 'value' => $value, + 'label' => $encode ? static::encode($label) : $label, + )); } $index++; } @@ -821,7 +780,10 @@ class Html } $separator = isset($options['separator']) ? $options['separator'] : "\n"; - return $hidden . implode($separator, $lines); + $tag = isset($options['tag']) ? $options['tag'] : 'div'; + unset($options['tag'], $options['unselect'], $options['encode'], $options['separator'], $options['item']); + + return $hidden . static::tag($tag, implode($separator, $lines), $options); } /** @@ -831,11 +793,12 @@ class Html * @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. + * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. + * This option is ignored if `item` option is set. * - 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: @@ -851,6 +814,7 @@ class Html */ public static function radioList($name, $selection = null, $items = array(), $options = array()) { + $encode = !isset($options['encode']) || $options['encode']; $formatter = isset($options['item']) ? $options['item'] : null; $lines = array(); $index = 0; @@ -861,7 +825,10 @@ class Html if ($formatter !== null) { $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { - $lines[] = static::label(static::radio($name, $checked, array('value' => $value)) . ' ' . $label); + $lines[] = static::radio($name, $checked, array( + 'value' => $value, + 'label' => $encode ? static::encode($label) : $label, + )); } $index++; } @@ -874,7 +841,79 @@ class Html $hidden = ''; } - return $hidden . implode($separator, $lines); + $tag = isset($options['tag']) ? $options['tag'] : 'div'; + unset($options['tag'], $options['unselect'], $options['encode'], $options['separator'], $options['item']); + + return $hidden . static::tag($tag, implode($separator, $lines), $options); + } + + /** + * Generates an unordered list. + * @param array|\Traversable $items the items for generating the list. Each item generates a single list item. + * Note that items will be automatically HTML encoded if `$options['encode']` is not set or true. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - encode: boolean, whether to HTML-encode the items. Defaults to true. + * This option is ignored if the `item` option is specified. + * - itemOptions: array, the HTML attributes for the `li` tags. This option is ignored if the `item` option is specified. + * - item: callable, a callback that is used to generate each individual list item. + * The signature of this callback must be: + * + * ~~~ + * function ($item, $index) + * ~~~ + * + * where $index is the array key corresponding to `$item` in `$items`. The callback should return + * the whole list item tag. + * + * @return string the generated unordered list. An empty string is returned if `$items` is empty. + */ + public static function ul($items, $options = array()) + { + if (empty($items)) { + return ''; + } + $tag = isset($options['tag']) ? $options['tag'] : 'ul'; + $encode = !isset($options['encode']) || $options['encode']; + $formatter = isset($options['item']) ? $options['item'] : null; + $itemOptions = isset($options['itemOptions']) ? $options['itemOptions'] : array(); + unset($options['tag'], $options['encode'], $options['item'], $options['itemOptions']); + $results = array(); + foreach ($items as $index => $item) { + if ($formatter !== null) { + $results[] = call_user_func($formatter, $item, $index); + } else { + $results[] = static::tag('li', $encode ? static::encode($item) : $item, $itemOptions); + } + } + return static::tag($tag, "\n" . implode("\n", $results) . "\n", $options); + } + + /** + * Generates an ordered list. + * @param array|\Traversable $items the items for generating the list. Each item generates a single list item. + * Note that items will be automatically HTML encoded if `$options['encode']` is not set or true. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - encode: boolean, whether to HTML-encode the items. Defaults to true. + * This option is ignored if the `item` option is specified. + * - itemOptions: array, the HTML attributes for the `li` tags. This option is ignored if the `item` option is specified. + * - item: callable, a callback that is used to generate each individual list item. + * The signature of this callback must be: + * + * ~~~ + * function ($item, $index) + * ~~~ + * + * where $index is the array key corresponding to `$item` in `$items`. The callback should return + * the whole list item tag. + * + * @return string the generated ordered list. An empty string is returned if `$items` is empty. + */ + public static function ol($items, $options = array()) + { + $options['tag'] = 'ol'; + return static::ul($items, $options); } /** @@ -904,6 +943,30 @@ class Html } /** + * Generates a tag that contains the first validation error of the specified model attribute. + * Note that even if there is no validation error, this method will still return an empty error tag. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $options the tag options in terms of name-value pairs. The values will be HTML-encoded + * using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * The following options are specially handled: + * + * - tag: this specifies the tag name. If not set, "div" will be used. + * + * @return string the generated label tag + */ + public static function error($model, $attribute, $options = array()) + { + $attribute = static::getAttributeName($attribute); + $error = $model->getFirstError($attribute); + $tag = isset($options['tag']) ? $options['tag'] : 'div'; + unset($options['tag']); + return Html::tag($tag, Html::encode($error), $options); + } + + /** * Generates an input tag for the given model attribute. * This method will generate the "name" and "value" tag attributes automatically for the model attribute * unless they are explicitly specified in `$options`. @@ -986,7 +1049,10 @@ class Html */ public static function activeFileInput($model, $attribute, $options = array()) { - return static::activeInput('file', $model, $attribute, $options); + // add a hidden field so that if a model only has a file field, we can + // still use isset($_POST[$modelClass]) to detect if the input is submitted + return static::activeHiddenInput($model, $attribute, array('id' => null, 'value' => '')) + . static::activeInput('file', $model, $attribute, $options); } /** @@ -1011,7 +1077,6 @@ class Html /** * Generates a radio button tag for the given model attribute. - * This method will generate the "name" tag attribute automatically unless it is explicitly specified in `$options`. * This method will generate the "checked" tag attribute according to the model attribute value. * @param Model $model the model object * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format @@ -1022,6 +1087,10 @@ class Html * it will take the default value '0'. This method will render a hidden input 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. + * - label: string, a label displayed next to the radio button. 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 [[encode()]] it to prevent XSS attacks. + * When this option is specified, the radio button will be enclosed by a label tag. + * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. * * 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. @@ -1043,7 +1112,6 @@ class Html /** * Generates a checkbox tag for the given model attribute. - * This method will generate the "name" tag attribute automatically unless it is explicitly specified in `$options`. * This method will generate the "checked" tag attribute according to the model attribute value. * @param Model $model the model object * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format @@ -1054,6 +1122,10 @@ class Html * it will take the default value '0'. This method will render a hidden input 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. + * - label: string, a label displayed next to the checkbox. 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 [[encode()]] it to prevent XSS attacks. + * When this option is specified, the checkbox will be enclosed by a label tag. + * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. * * 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. @@ -1204,6 +1276,9 @@ class Html if (!array_key_exists('unselect', $options)) { $options['unselect'] = '0'; } + if (!array_key_exists('id', $options)) { + $options['id'] = static::getInputId($model, $attribute); + } return static::checkboxList($name, $checked, $items, $options); } @@ -1241,6 +1316,9 @@ class Html if (!array_key_exists('unselect', $options)) { $options['unselect'] = '0'; } + if (!array_key_exists('id', $options)) { + $options['id'] = static::getInputId($model, $attribute); + } return static::radioList($name, $checked, $items, $options); } @@ -1283,9 +1361,9 @@ class Html $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); } else { $attrs = isset($options[$key]) ? $options[$key] : array(); - $attrs['value'] = $key; + $attrs['value'] = (string)$key; $attrs['selected'] = $selection !== null && - (!is_array($selection) && !strcmp($key, $selection) + (!is_array($selection) && !strcmp($key, $selection) || is_array($selection) && in_array($key, $selection)); $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); } @@ -1296,12 +1374,12 @@ class Html /** * Renders the HTML tag attributes. - * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially - * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. + * Attributes whose values are of boolean type will be treated as + * [boolean attributes](http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes). + * And attributes whose values are null will not be rendered. * @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 + * into a string with a leading white space (so 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) @@ -1318,9 +1396,9 @@ class Html $html = ''; foreach ($attributes as $name => $value) { - if (isset(static::$booleanAttributes[strtolower($name)])) { - if ($value || strcasecmp($name, $value) === 0) { - $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; + if (is_bool($value)) { + if ($value) { + $html .= " $name"; } } elseif ($value !== null) { $html .= " $name=\"" . static::encode($value) . '"'; @@ -1336,11 +1414,12 @@ class Html * * - is an empty string: the currently requested URL will be returned; * - is a non-empty string: it will first be processed by [[Yii::getAlias()]]. If the result - * is an absolute URL, it will be returned with any change further; Otherwise, the result + * is an absolute URL, it will be returned without any change further; Otherwise, the result * will be prefixed with [[\yii\web\Request::baseUrl]] 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')`. + * In case there is no controller, [[\yii\web\UrlManager::createUrl()]] will be used. * * @param array|string $url the parameter to be used to generate a valid URL * @return string the normalized URL @@ -1352,7 +1431,7 @@ class Html if (isset($url[0])) { $route = $url[0]; $params = array_splice($url, 1); - if (Yii::$app->controller !== null) { + if (Yii::$app->controller instanceof \yii\web\Controller) { return Yii::$app->controller->createUrl($route, $params); } else { return Yii::$app->getUrlManager()->createUrl($route, $params); @@ -1364,7 +1443,7 @@ class Html return Yii::$app->getRequest()->getUrl(); } else { $url = Yii::getAlias($url); - if ($url[0] === '/' || strpos($url, '://')) { + if ($url !== '' && ($url[0] === '/' || $url[0] === '#' || strpos($url, '://'))) { return $url; } else { return Yii::$app->getRequest()->getBaseUrl() . '/' . $url; @@ -1373,6 +1452,44 @@ class Html } /** + * Adds a CSS class to the specified options. + * If the CSS class is already in the options, it will not be added again. + * @param array $options the options to be modified. + * @param string $class the CSS class to be added + */ + public static function addCssClass(&$options, $class) + { + if (isset($options['class'])) { + $classes = ' ' . $options['class'] . ' '; + if (($pos = strpos($classes, ' ' . $class . ' ')) === false) { + $options['class'] .= ' ' . $class; + } + } else { + $options['class'] = $class; + } + } + + /** + * Removes a CSS class from the specified options. + * @param array $options the options to be modified. + * @param string $class the CSS class to be removed + */ + public static function removeCssClass(&$options, $class) + { + if (isset($options['class'])) { + $classes = array_unique(preg_split('/\s+/', $options['class'] . ' ' . $class, -1, PREG_SPLIT_NO_EMPTY)); + if (($index = array_search($class, $classes)) !== false) { + unset($classes[$index]); + } + if (empty($classes)) { + unset($options['class']); + } else { + $options['class'] = implode(' ', $classes); + } + } + } + + /** * Returns the real attribute name from the given attribute expression. * * An attribute expression is an attribute name prefixed and/or suffixed with array indexes. @@ -1468,7 +1585,7 @@ class Html * Generates an appropriate input ID for the specified attribute name or expression. * * This method converts the result [[getInputName()]] into a valid input ID. - * For example, [[getInputName()]] returns `Post[content]`, this method will return `Post-method`. + * For example, if [[getInputName()]] returns `Post[content]`, this method will return `post-content`. * @param Model $model the model object * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for explanation of attribute expression. * @return string the generated input ID @@ -1479,5 +1596,4 @@ class Html $name = strtolower(static::getInputName($model, $attribute)); return str_replace(array('[]', '][', '[', ']', ' '), array('', '-', '-', '', '-'), $name); } - } diff --git a/yii/helpers/base/Purifier.php b/framework/yii/helpers/BaseHtmlPurifier.php similarity index 55% rename from yii/helpers/base/Purifier.php rename to framework/yii/helpers/BaseHtmlPurifier.php index 2c5d334..17d2122 100644 --- a/yii/helpers/base/Purifier.php +++ b/framework/yii/helpers/BaseHtmlPurifier.php @@ -4,35 +4,30 @@ * @link http://www.yiiframework.com/ * @license http://www.yiiframework.com/license/ */ -namespace yii\helpers\base; +namespace yii\helpers; /** - * Purifier provides an ability to clean up HTML from any harmful code. + * BaseHtmlPurifier provides concrete implementation for [[HtmlPurifier]]. * - * Basic usage is the following: - * - * ```php - * $my_html = Purifier::process($my_text); - * ``` - * - * If you want to configure it: - * - * ```php - * $my_html = Purifier::process($my_text, array( - * 'Attr.EnableID' => true, - * )); - * ``` - * - * For more details please refer to HTMLPurifier documentation](http://htmlpurifier.org/). + * Do not use BaseHtmlPurifier. Use [[HtmlPurifier]] instead. * * @author Alexander Makarov <sam@rmcreative.ru> * @since 2.0 */ -class Purifier +class BaseHtmlPurifier { + /** + * Passes markup through HTMLPurifier making it safe to output to end user + * + * @param string $content + * @param array|null $config + * @return string + */ public static function process($content, $config = null) { - $purifier=\HTMLPurifier::instance($config); + $configInstance = \HTMLPurifier_Config::create($config); + $configInstance->autoFinalize = false; + $purifier=\HTMLPurifier::instance($configInstance); $purifier->config->set('Cache.SerializerPath', \Yii::$app->getRuntimePath()); return $purifier->purify($content); } diff --git a/framework/yii/helpers/BaseInflector.php b/framework/yii/helpers/BaseInflector.php new file mode 100644 index 0000000..affd3dd --- /dev/null +++ b/framework/yii/helpers/BaseInflector.php @@ -0,0 +1,480 @@ +<?php +/** + * @copyright Copyright (c) 2008 Yii Software LLC + * @link http://www.yiiframework.com/ + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\helpers; + +use Yii; + +/** + * BaseInflector provides concrete implementation for [[Inflector]]. + * + * Do not use BaseInflector. Use [[Inflector]] instead. + * + * @author Antonio Ramirez <amigo.cobos@gmail.com> + * @since 2.0 + */ +class BaseInflector +{ + /** + * @var array the rules for converting a word into its plural form. + * The keys are the regular expressions and the values are the corresponding replacements. + */ + public static $plurals = array( + '/([nrlm]ese|deer|fish|sheep|measles|ois|pox|media)$/i' => '\1', + '/^(sea[- ]bass)$/i' => '\1', + '/(m)ove$/i' => '\1oves', + '/(f)oot$/i' => '\1eet', + '/(h)uman$/i' => '\1umans', + '/(s)tatus$/i' => '\1tatuses', + '/(s)taff$/i' => '\1taff', + '/(t)ooth$/i' => '\1eeth', + '/(quiz)$/i' => '\1zes', + '/^(ox)$/i' => '\1\2en', + '/([m|l])ouse$/i' => '\1ice', + '/(matr|vert|ind)(ix|ex)$/i' => '\1ices', + '/(x|ch|ss|sh)$/i' => '\1es', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(hive)$/i' => '\1s', + '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', + '/sis$/i' => 'ses', + '/([ti])um$/i' => '\1a', + '/(p)erson$/i' => '\1eople', + '/(m)an$/i' => '\1en', + '/(c)hild$/i' => '\1hildren', + '/(buffal|tomat|potat|ech|her|vet)o$/i' => '\1oes', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$/i' => '\1i', + '/us$/i' => 'uses', + '/(alias)$/i' => '\1es', + '/(ax|cris|test)is$/i' => '\1es', + '/s$/' => 's', + '/^$/' => '', + '/$/' => 's', + ); + /** + * @var array the rules for converting a word into its singular form. + * The keys are the regular expressions and the values are the corresponding replacements. + */ + public static $singulars = array( + '/([nrlm]ese|deer|fish|sheep|measles|ois|pox|media|ss)$/i' => '\1', + '/^(sea[- ]bass)$/i' => '\1', + '/(s)tatuses$/i' => '\1tatus', + '/(f)eet$/i' => '\1oot', + '/(t)eeth$/i' => '\1ooth', + '/^(.*)(menu)s$/i' => '\1\2', + '/(quiz)zes$/i' => '\\1', + '/(matr)ices$/i' => '\1ix', + '/(vert|ind)ices$/i' => '\1ex', + '/^(ox)en/i' => '\1', + '/(alias)(es)*$/i' => '\1', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us', + '/([ftw]ax)es/i' => '\1', + '/(cris|ax|test)es$/i' => '\1is', + '/(shoe|slave)s$/i' => '\1', + '/(o)es$/i' => '\1', + '/ouses$/' => 'ouse', + '/([^a])uses$/' => '\1us', + '/([m|l])ice$/i' => '\1ouse', + '/(x|ch|ss|sh)es$/i' => '\1', + '/(m)ovies$/i' => '\1\2ovie', + '/(s)eries$/i' => '\1\2eries', + '/([^aeiouy]|qu)ies$/i' => '\1y', + '/([lr])ves$/i' => '\1f', + '/(tive)s$/i' => '\1', + '/(hive)s$/i' => '\1', + '/(drive)s$/i' => '\1', + '/([^fo])ves$/i' => '\1fe', + '/(^analy)ses$/i' => '\1sis', + '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', + '/([ti])a$/i' => '\1um', + '/(p)eople$/i' => '\1\2erson', + '/(m)en$/i' => '\1an', + '/(c)hildren$/i' => '\1\2hild', + '/(n)ews$/i' => '\1\2ews', + '/eaus$/' => 'eau', + '/^(.*us)$/' => '\\1', + '/s$/i' => '', + ); + /** + * @var array the special rules for converting a word between its plural form and singular form. + * The keys are the special words in singular form, and the values are the corresponding plural form. + */ + public static $specials = array( + 'atlas' => 'atlases', + 'beef' => 'beefs', + 'brother' => 'brothers', + 'cafe' => 'cafes', + 'child' => 'children', + 'cookie' => 'cookies', + 'corpus' => 'corpuses', + 'cow' => 'cows', + 'curve' => 'curves', + 'foe' => 'foes', + 'ganglion' => 'ganglions', + 'genie' => 'genies', + 'genus' => 'genera', + 'graffito' => 'graffiti', + 'hoof' => 'hoofs', + 'loaf' => 'loaves', + 'man' => 'men', + 'money' => 'monies', + 'mongoose' => 'mongooses', + 'move' => 'moves', + 'mythos' => 'mythoi', + 'niche' => 'niches', + 'numen' => 'numina', + 'occiput' => 'occiputs', + 'octopus' => 'octopuses', + 'opus' => 'opuses', + 'ox' => 'oxen', + 'penis' => 'penises', + 'sex' => 'sexes', + 'soliloquy' => 'soliloquies', + 'testis' => 'testes', + 'trilby' => 'trilbys', + 'turf' => 'turfs', + 'wave' => 'waves', + 'Amoyese' => 'Amoyese', + 'bison' => 'bison', + 'Borghese' => 'Borghese', + 'bream' => 'bream', + 'breeches' => 'breeches', + 'britches' => 'britches', + 'buffalo' => 'buffalo', + 'cantus' => 'cantus', + 'carp' => 'carp', + 'chassis' => 'chassis', + 'clippers' => 'clippers', + 'cod' => 'cod', + 'coitus' => 'coitus', + 'Congoese' => 'Congoese', + 'contretemps' => 'contretemps', + 'corps' => 'corps', + 'debris' => 'debris', + 'diabetes' => 'diabetes', + 'djinn' => 'djinn', + 'eland' => 'eland', + 'elk' => 'elk', + 'equipment' => 'equipment', + 'Faroese' => 'Faroese', + 'flounder' => 'flounder', + 'Foochowese' => 'Foochowese', + 'gallows' => 'gallows', + 'Genevese' => 'Genevese', + 'Genoese' => 'Genoese', + 'Gilbertese' => 'Gilbertese', + 'graffiti' => 'graffiti', + 'headquarters' => 'headquarters', + 'herpes' => 'herpes', + 'hijinks' => 'hijinks', + 'Hottentotese' => 'Hottentotese', + 'information' => 'information', + 'innings' => 'innings', + 'jackanapes' => 'jackanapes', + 'Kiplingese' => 'Kiplingese', + 'Kongoese' => 'Kongoese', + 'Lucchese' => 'Lucchese', + 'mackerel' => 'mackerel', + 'Maltese' => 'Maltese', + 'mews' => 'mews', + 'moose' => 'moose', + 'mumps' => 'mumps', + 'Nankingese' => 'Nankingese', + 'news' => 'news', + 'nexus' => 'nexus', + 'Niasese' => 'Niasese', + 'Pekingese' => 'Pekingese', + 'Piedmontese' => 'Piedmontese', + 'pincers' => 'pincers', + 'Pistoiese' => 'Pistoiese', + 'pliers' => 'pliers', + 'Portuguese' => 'Portuguese', + 'proceedings' => 'proceedings', + 'rabies' => 'rabies', + 'rice' => 'rice', + 'rhinoceros' => 'rhinoceros', + 'salmon' => 'salmon', + 'Sarawakese' => 'Sarawakese', + 'scissors' => 'scissors', + 'series' => 'series', + 'Shavese' => 'Shavese', + 'shears' => 'shears', + 'siemens' => 'siemens', + 'species' => 'species', + 'swine' => 'swine', + 'testes' => 'testes', + 'trousers' => 'trousers', + 'trout' => 'trout', + 'tuna' => 'tuna', + 'Vermontese' => 'Vermontese', + 'Wenchowese' => 'Wenchowese', + 'whiting' => 'whiting', + 'wildebeest' => 'wildebeest', + 'Yengeese' => 'Yengeese', + ); + /** + * @var array map of special chars and its translation. This is used by [[slug()]]. + */ + public static $transliteration = array( + '/ä|æ|ǽ/' => 'ae', + '/ö|œ/' => 'oe', + '/ü/' => 'ue', + '/Ä/' => 'Ae', + '/Ü/' => 'Ue', + '/Ö/' => 'Oe', + '/À|Á|Â|Ã|Å|Ǻ|Ā|Ă|Ą|Ǎ/' => 'A', + '/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª/' => 'a', + '/Ç|Ć|Ĉ|Ċ|Č/' => 'C', + '/ç|ć|ĉ|ċ|č/' => 'c', + '/Ð|Ď|Đ/' => 'D', + '/ð|ď|đ/' => 'd', + '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě/' => 'E', + '/è|é|ê|ë|ē|ĕ|ė|ę|ě/' => 'e', + '/Ĝ|Ğ|Ġ|Ģ/' => 'G', + '/ĝ|ğ|ġ|ģ/' => 'g', + '/Ĥ|Ħ/' => 'H', + '/ĥ|ħ/' => 'h', + '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ/' => 'I', + '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı/' => 'i', + '/Ĵ/' => 'J', + '/ĵ/' => 'j', + '/Ķ/' => 'K', + '/ķ/' => 'k', + '/Ĺ|Ļ|Ľ|Ŀ|Ł/' => 'L', + '/ĺ|ļ|ľ|ŀ|ł/' => 'l', + '/Ñ|Ń|Ņ|Ň/' => 'N', + '/ñ|ń|ņ|ň|ʼn/' => 'n', + '/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ/' => 'O', + '/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º/' => 'o', + '/Ŕ|Ŗ|Ř/' => 'R', + '/ŕ|ŗ|ř/' => 'r', + '/Ś|Ŝ|Ş|Ș|Š/' => 'S', + '/ś|ŝ|ş|ș|š|ſ/' => 's', + '/Ţ|Ț|Ť|Ŧ/' => 'T', + '/ţ|ț|ť|ŧ/' => 't', + '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ/' => 'U', + '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ/' => 'u', + '/Ý|Ÿ|Ŷ/' => 'Y', + '/ý|ÿ|ŷ/' => 'y', + '/Ŵ/' => 'W', + '/ŵ/' => 'w', + '/Ź|Ż|Ž/' => 'Z', + '/ź|ż|ž/' => 'z', + '/Æ|Ǽ/' => 'AE', + '/ß/' => 'ss', + '/IJ/' => 'IJ', + '/ij/' => 'ij', + '/Œ/' => 'OE', + '/ƒ/' => 'f' + ); + + /** + * 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 $word the word to be pluralized + * @return string the pluralized word + */ + public static function pluralize($word) + { + if (isset(self::$specials[$word])) { + return self::$specials[$word]; + } + foreach (static::$plurals as $rule => $replacement) { + if (preg_match($rule, $word)) { + return preg_replace($rule, $replacement, $word); + } + } + return $word; + } + + /** + * Returns the singular of the $word + * @param string $word the english word to singularize + * @return string Singular noun. + */ + public static function singularize($word) + { + $result = array_search($word, self::$specials, true); + if ($result !== false) { + return $result; + } + foreach (static::$singulars as $rule => $replacement) { + if (preg_match($rule, $word)) { + return preg_replace($rule, $replacement, $word); + } + } + return $word; + } + + /** + * Converts an underscored or CamelCase word into a English + * sentence. + * @param string $words + * @param bool $ucAll whether to set all words to uppercase + * @return string + */ + public static function titleize($words, $ucAll = false) + { + $words = static::humanize(static::underscore($words), $ucAll); + return $ucAll ? ucwords($words) : ucfirst($words); + } + + /** + * Returns given word as CamelCased + * Converts a word like "send_email" to "SendEmail". It + * will remove non alphanumeric character from the word, so + * "who's online" will be converted to "WhoSOnline" + * @see variablize + * @param string $word the word to CamelCase + * @return string + */ + public static function camelize($word) + { + return str_replace(' ', '', ucwords(preg_replace('/[^A-Z^a-z^0-9]+/', ' ', $word))); + } + + /** + * 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('/(?<![A-Z])[A-Z]/', ' \0', $name)))); + return $ucwords ? ucwords($label) : $label; + } + + /** + * Converts a CamelCase name into an ID in lowercase. + * Words in the ID may be concatenated using the specified character (defaults to '-'). + * For example, 'PostTag' will be converted to 'post-tag'. + * @param string $name the string to be converted + * @param string $separator the character used to concatenate the words in the ID + * @return string the resulting ID + */ + public static function camel2id($name, $separator = '-') + { + if ($separator === '_') { + return trim(strtolower(preg_replace('/(?<![A-Z])[A-Z]/', '_\0', $name)), '_'); + } else { + return trim(strtolower(str_replace('_', $separator, preg_replace('/(?<![A-Z])[A-Z]/', $separator . '\0', $name))), $separator); + } + } + + /** + * Converts an ID into a CamelCase name. + * Words in the ID separated by `$separator` (defaults to '-') will be concatenated into a CamelCase name. + * For example, 'post-tag' is converted to 'PostTag'. + * @param string $id the ID to be converted + * @param string $separator the character used to separate the words in the ID + * @return string the resulting CamelCase name + */ + public static function id2camel($id, $separator = '-') + { + return str_replace(' ', '', ucwords(implode(' ', explode($separator, $id)))); + } + + /** + * Converts any "CamelCased" into an "underscored_word". + * @param string $words the word(s) to underscore + * @return string + */ + public static function underscore($words) + { + return strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $words)); + } + + /** + * Returns a human-readable string from $word + * @param string $word the string to humanize + * @param bool $ucAll whether to set all words to uppercase or not + * @return string + */ + public static function humanize($word, $ucAll = false) + { + $word = str_replace('_', ' ', preg_replace('/_id$/', '', $word)); + return $ucAll ? ucwords($word) : ucfirst($word); + } + + /** + * Same as camelize but first char is in lowercase. + * Converts a word like "send_email" to "sendEmail". It + * will remove non alphanumeric character from the word, so + * "who's online" will be converted to "whoSOnline" + * @param string $word to lowerCamelCase + * @return string + */ + public static function variablize($word) + { + $word = static::camelize($word); + return strtolower($word[0]) . substr($word, 1); + } + + /** + * Converts a class name to its table name (pluralized) + * naming conventions. For example, converts "Person" to "people" + * @param string $className the class name for getting related table_name + * @return string + */ + public static function tableize($className) + { + return static::pluralize(static::underscore($className)); + } + + /** + * Returns a string with all spaces converted to given replacement and + * non word characters removed. Maps special characters to ASCII using + * `Inflector::$transliteration` + * @param string $string An arbitrary string to convert + * @param string $replacement The replacement to use for spaces + * @return string The converted string. + */ + public static function slug($string, $replacement = '-') + { + $map = static::$transliteration + array( + '/[^\w\s]/' => ' ', + '/\\s+/' => $replacement, + '/(?<=[a-z])([A-Z])/' => $replacement . '\\1', + str_replace(':rep', preg_quote($replacement, '/'), '/^[:rep]+|[:rep]+$/') => '' + ); + return preg_replace(array_keys($map), array_values($map), $string); + } + + /** + * Converts a table name to its class name. For example, converts "people" to "Person" + * @param string $tableName + * @return string + */ + public static function classify($tableName) + { + return static::camelize(static::singularize($tableName)); + } + + /** + * Converts number to its ordinal English form. For example, converts 13 to 13th, 2 to 2nd ... + * @param int $number the number to get its ordinal value + * @return string + */ + public static function ordinalize($number) + { + if (in_array(($number % 100), range(11, 13))) { + return $number . 'th'; + } + switch ($number % 10) { + case 1: return $number . 'st'; + case 2: return $number . 'nd'; + case 3: return $number . 'rd'; + default: return $number . 'th'; + } + } +} diff --git a/yii/helpers/base/Json.php b/framework/yii/helpers/BaseJson.php similarity index 82% rename from yii/helpers/base/Json.php rename to framework/yii/helpers/BaseJson.php index 8de55f9..80224d5 100644 --- a/yii/helpers/base/Json.php +++ b/framework/yii/helpers/BaseJson.php @@ -5,19 +5,21 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\helpers\base; +namespace yii\helpers; use yii\base\InvalidParamException; +use yii\base\Arrayable; use yii\web\JsExpression; /** - * Json is a helper class providing JSON data encoding and decoding. - * It enhances the PHP built-in functions `json_encode()` and `json_decode()` - * by supporting encoding JavaScript expressions and throwing exceptions when decoding fails. + * BaseJson provides concrete implementation for [[Json]]. + * + * Do not use BaseJson. Use [[Json]] instead. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Json +class BaseJson { /** * Encodes the given value into a JSON string. @@ -32,7 +34,7 @@ class Json public static function encode($value, $options = 0) { $expressions = array(); - $value = static::processData($value, $expressions); + $value = static::processData($value, $expressions, uniqid()); $json = json_encode($value, $options); return empty($expressions) ? $json : strtr($json, $expressions); } @@ -74,34 +76,31 @@ class Json * Pre-processes the data before sending it to `json_encode()`. * @param mixed $data the data to be processed * @param array $expressions collection of JavaScript expressions + * @param string $expPrefix a prefix internally used to handle JS expressions * @return mixed the processed data */ - protected static function processData($data, &$expressions) + protected static function processData($data, &$expressions, $expPrefix) { - if (is_array($data)) { - foreach ($data as $key => $value) { - if (is_array($value) || is_object($value)) { - $data[$key] = static::processData($value, $expressions); - } - } - return $data; - } elseif (is_object($data)) { + if (is_object($data)) { if ($data instanceof JsExpression) { - $token = '!{[' . count($expressions) . ']}!'; + $token = "!{[$expPrefix=" . count($expressions) . ']}!'; $expressions['"' . $token . '"'] = $data->expression; return $token; } - $result = array(); + $data = $data instanceof Arrayable ? $data->toArray() : get_object_vars($data); + if ($data === array() && !$data instanceof Arrayable) { + return new \stdClass(); + } + } + + if (is_array($data)) { foreach ($data as $key => $value) { if (is_array($value) || is_object($value)) { - $result[$key] = static::processData($value, $expressions); - } else { - $result[$key] = $value; + $data[$key] = static::processData($value, $expressions, $expPrefix); } } - return $result; - } else { - return $data; } + + return $data; } } diff --git a/yii/helpers/base/Markdown.php b/framework/yii/helpers/BaseMarkdown.php similarity index 66% rename from yii/helpers/base/Markdown.php rename to framework/yii/helpers/BaseMarkdown.php index 1efdccb..40a1dc4 100644 --- a/yii/helpers/base/Markdown.php +++ b/framework/yii/helpers/BaseMarkdown.php @@ -5,41 +5,35 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\helpers\base; +namespace yii\helpers; use Michelf\MarkdownExtra; /** - * Markdown provides an ability to transform markdown into HTML. + * BaseMarkdown provides concrete implementation for [[Markdown]]. * - * Basic usage is the following: + * Do not use BaseMarkdown. Use [[Markdown]] instead. * - * ```php - * $myHtml = Markdown::process($myText); - * ``` - * - * If you want to configure the parser: - * - * ```php - * $myHtml = Markdown::process($myText, array( - * 'fn_id_prefix' => 'footnote_', - * )); - * ``` - * - * For more details please refer to [PHP Markdown library documentation](http://michelf.ca/projects/php-markdown/). * @author Alexander Makarov <sam@rmcreative.ru> * @since 2.0 */ -class Markdown +class BaseMarkdown { /** * @var MarkdownExtra */ protected static $markdown; + /** + * Converts markdown into HTML + * + * @param string $content + * @param array $config + * @return string + */ public static function process($content, $config = array()) { - if (static::$markdown===null) { + if (static::$markdown === null) { static::$markdown = new MarkdownExtra(); } foreach ($config as $name => $value) { diff --git a/yii/helpers/base/SecurityHelper.php b/framework/yii/helpers/BaseSecurity.php similarity index 60% rename from yii/helpers/base/SecurityHelper.php rename to framework/yii/helpers/BaseSecurity.php index 3f69fee..a1b0ec4 100644 --- a/yii/helpers/base/SecurityHelper.php +++ b/framework/yii/helpers/BaseSecurity.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\helpers\base; +namespace yii\helpers; use Yii; use yii\base\Exception; @@ -13,39 +13,51 @@ use yii\base\InvalidConfigException; use yii\base\InvalidParamException; /** - * SecurityHelper provides a set of methods to handle common security-related tasks. + * BaseSecurity provides concrete implementation for [[Security]]. * - * In particular, SecurityHelper supports the following features: - * - * - Encryption/decryption: [[encrypt()]] and [[decrypt()]] - * - Data tampering prevention: [[hashData()]] and [[validateData()]] - * - Password validation: [[generatePasswordHash()]] and [[validatePassword()]] - * - * Additionally, SecurityHelper provides [[getSecretKey()]] to support generating - * named secret keys. These secret keys, once generated, will be stored in a file - * and made available in future requests. + * Do not use BaseSecurity. Use [[Security]] instead. * * @author Qiang Xue <qiang.xue@gmail.com> * @author Tom Worster <fsb@thefsb.org> * @since 2.0 */ -class SecurityHelper +class BaseSecurity { /** + * Uses AES, block size is 128-bit (16 bytes). + */ + const CRYPT_BLOCK_SIZE = 16; + + /** + * Uses AES-192, key size is 192-bit (24 bytes). + */ + const CRYPT_KEY_SIZE = 24; + + /** + * Uses SHA-256. + */ + const DERIVATION_HASH = 'sha256'; + + /** + * Uses 1000 iterations. + */ + const DERIVATION_ITERATIONS = 1000; + + /** * Encrypts data. * @param string $data data to be encrypted. - * @param string $key the encryption secret key + * @param string $password the encryption password * @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) + public static function encrypt($data, $password) { $module = static::openCryptModule(); - // 192-bit (24 bytes) key size - $key = StringHelper::substr($key, 0, 24); + $data = static::addPadding($data); srand(); $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); + $key = static::deriveKey($password, $iv); mcrypt_generic_init($module, $key, $iv); $encrypted = $iv . mcrypt_generic($module, $data); mcrypt_generic_deinit($module); @@ -56,23 +68,69 @@ class SecurityHelper /** * Decrypts data * @param string $data data to be decrypted. - * @param string $key the decryption secret key + * @param string $password the decryption password * @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) + public static function decrypt($data, $password) { $module = static::openCryptModule(); - // 192-bit (24 bytes) key size - $key = StringHelper::substr($key, 0, 24); $ivSize = mcrypt_enc_get_iv_size($module); $iv = StringHelper::substr($data, 0, $ivSize); + $key = static::deriveKey($password, $iv); 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"); + return static::stripPadding($decrypted); + } + + /** + * Adds a padding to the given data (PKCS #7). + * @param string $data the data to pad + * @return string the padded data + */ + protected static function addPadding($data) + { + $pad = self::CRYPT_BLOCK_SIZE - (StringHelper::strlen($data) % self::CRYPT_BLOCK_SIZE); + return $data . str_repeat(chr($pad), $pad); + } + + /** + * Strips the padding from the given data. + * @param string $data the data to trim + * @return string the trimmed data + */ + protected static function stripPadding($data) + { + $end = StringHelper::substr($data, -1, NULL); + $last = ord($end); + $n = StringHelper::strlen($data) - $last; + if (StringHelper::substr($data, $n, NULL) == str_repeat($end, $last)) { + return StringHelper::substr($data, 0, $n); + } + return false; + } + + /** + * Derives a key from the given password (PBKDF2). + * @param string $password the source password + * @param string $salt the random salt + * @return string the derived key + */ + protected static function deriveKey($password, $salt) + { + if (function_exists('hash_pbkdf2')) { + return hash_pbkdf2(self::DERIVATION_HASH, $password, $salt, self::DERIVATION_ITERATIONS, self::CRYPT_KEY_SIZE, true); + } + $hmac = hash_hmac(self::DERIVATION_HASH, $salt . pack('N', 1), $password, true); + $xorsum = $hmac; + for ($i = 1; $i < self::DERIVATION_ITERATIONS; $i++) { + $hmac = hash_hmac(self::DERIVATION_HASH, $hmac, $password, true); + $xorsum ^= $hmac; + } + return substr($xorsum, 0, self::CRYPT_KEY_SIZE); } /** @@ -128,18 +186,36 @@ class SecurityHelper static $keys; $keyFile = Yii::$app->getRuntimePath() . '/keys.php'; if ($keys === null) { - $keys = is_file($keyFile) ? require($keyFile) : array(); + $keys = array(); + if (is_file($keyFile)) { + $keys = require($keyFile); + } } if (!isset($keys[$name])) { - // generate a 32-char random key - $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - $keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length); + $keys[$name] = static::generateRandomKey($length); file_put_contents($keyFile, "<?php\nreturn " . var_export($keys, true) . ";\n"); } return $keys[$name]; } /** + * Generates a random key. The key may contain uppercase and lowercase latin letters, digits, underscore, dash and dot. + * @param integer $length the length of the key that should be generated + * @return string the generated random key + */ + public static function generateRandomKey($length = 32) + { + if (function_exists('openssl_random_pseudo_bytes')) { + $key = strtr(base64_encode(openssl_random_pseudo_bytes($length, $strong)), '+/=', '_-.'); + if ($strong) { + return substr($key, 0, $length); + } + } + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.'; + return substr(str_shuffle(str_repeat($chars, 5)), 0, $length); + } + + /** * Opens the mcrypt module. * @return resource the mcrypt module handle. * @throws InvalidConfigException if mcrypt extension is not installed @@ -167,11 +243,11 @@ class SecurityHelper * * ~~~ * // generates the hash (usually done during user registration or when the password is changed) - * $hash = SecurityHelper::generatePasswordHash($password); + * $hash = Security::generatePasswordHash($password); * // ...save $hash in database... * * // during login, validate if the password entered is correct using $hash fetched from database - * if (SecurityHelper::verifyPassword($password, $hash) { + * if (Security::validatePassword($password, $hash) { * // password is good * } else { * // password is bad @@ -252,7 +328,7 @@ class SecurityHelper protected static function generateSalt($cost = 13) { $cost = (int)$cost; - if ($cost < 4 || $cost > 30) { + if ($cost < 4 || $cost > 31) { throw new InvalidParamException('Cost must be between 4 and 31.'); } diff --git a/framework/yii/helpers/BaseStringHelper.php b/framework/yii/helpers/BaseStringHelper.php new file mode 100644 index 0000000..e1622b9 --- /dev/null +++ b/framework/yii/helpers/BaseStringHelper.php @@ -0,0 +1,138 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\helpers; + +use yii\base\InvalidParamException; + +/** + * BaseStringHelper provides concrete implementation for [[StringHelper]]. + * + * Do not use BaseStringHelper. Use [[StringHelper]] instead. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Alex Makarov <sam@rmcreative.ru> + * @since 2.0 + */ +class BaseStringHelper +{ + /** + * Returns the number of bytes in the given string. + * This method ensures the string is treated as a byte array by using `mb_strlen()`. + * @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 mb_strlen($string, '8bit'); + } + + /** + * Returns the portion of string specified by the start and length parameters. + * This method ensures the string is treated as a byte array by using `mb_substr()`. + * @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 mb_substr($string, $start, $length, '8bit'); + } + + /** + * Returns the trailing name component of a path. + * This method is similar to the php function `basename()` except that it will + * treat both \ and / as directory separators, independent of the operating system. + * This method was mainly created to work on php namespaces. When working with real + * file paths, php's `basename()` should work fine for you. + * Note: this method is not aware of the actual filesystem, or path components such as "..". + * @param string $path A path string. + * @param string $suffix If the name component ends in suffix this will also be cut off. + * @return string the trailing name component of the given path. + * @see http://www.php.net/manual/en/function.basename.php + */ + public static function basename($path, $suffix = '') + { + if (($len = mb_strlen($suffix)) > 0 && mb_substr($path, -$len) == $suffix) { + $path = mb_substr($path, 0, -$len); + } + $path = rtrim(str_replace('\\', '/', $path), '/\\'); + if (($pos = mb_strrpos($path, '/')) !== false) { + return mb_substr($path, $pos + 1); + } + return $path; + } + + /** + * Returns parent directory's path. + * This method is similar to `dirname()` except that it will treat + * both \ and / as directory separators, independent of the operating system. + * @param string $path A path string. + * @return string the parent directory's path. + * @see http://www.php.net/manual/en/function.basename.php + */ + public static function dirname($path) + { + $pos = mb_strrpos(str_replace('\\', '/', $path), '/'); + if ($pos !== false) { + return mb_substr($path, 0, $pos); + } else { + return $path; + } + } + + /** + * Compares two strings or string arrays, and return their differences. + * This is a wrapper of the [phpspec/php-diff](https://packagist.org/packages/phpspec/php-diff) package. + * @param string|array $lines1 the first string or string array to be compared. If it is a string, + * it will be converted into a string array by breaking at newlines. + * @param string|array $lines2 the second string or string array to be compared. If it is a string, + * it will be converted into a string array by breaking at newlines. + * @param string $format the output format. It must be 'inline', 'unified', 'context', 'side-by-side', or 'array'. + * @return string|array the comparison result. An array is returned if `$format` is 'array'. For all other + * formats, a string is returned. + * @throws InvalidParamException if the format is invalid. + */ + public static function diff($lines1, $lines2, $format = 'inline') + { + if (!is_array($lines1)) { + $lines1 = explode("\n", $lines1); + } + if (!is_array($lines2)) { + $lines2 = explode("\n", $lines2); + } + foreach ($lines1 as $i => $line) { + $lines1[$i] = rtrim($line, "\r\n"); + } + foreach ($lines2 as $i => $line) { + $lines2[$i] = rtrim($line, "\r\n"); + } + switch ($format) { + case 'inline': + $renderer = new \Diff_Renderer_Html_Inline(); + break; + case 'array': + $renderer = new \Diff_Renderer_Html_Array(); + break; + case 'side-by-side': + $renderer = new \Diff_Renderer_Html_SideBySide(); + break; + case 'context': + $renderer = new \Diff_Renderer_Text_Context(); + break; + case 'unified': + $renderer = new \Diff_Renderer_Text_Unified(); + break; + default: + throw new InvalidParamException("Output format must be 'inline', 'side-by-side', 'array', 'context' or 'unified'."); + } + $diff = new \Diff($lines1, $lines2); + return $diff->render($renderer); + } +} diff --git a/yii/helpers/base/VarDumper.php b/framework/yii/helpers/BaseVarDumper.php similarity index 92% rename from yii/helpers/base/VarDumper.php rename to framework/yii/helpers/BaseVarDumper.php index 730aafe..f109125 100644 --- a/yii/helpers/base/VarDumper.php +++ b/framework/yii/helpers/BaseVarDumper.php @@ -6,24 +6,17 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\helpers\base; +namespace yii\helpers; /** - * VarDumper is intended to replace the buggy PHP function var_dump and print_r. - * It can correctly identify the recursively referenced objects in a complex - * object structure. It also has a recursive depth control to avoid indefinite - * recursive display of some peculiar variables. + * BaseVarDumper provides concrete implementation for [[VarDumper]]. * - * VarDumper can be used as follows, - * - * ~~~ - * VarDumper::dump($var); - * ~~~ + * Do not use BaseVarDumper. Use [[VarDumper]] instead. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class VarDumper +class BaseVarDumper { private static $_objects; private static $_output; diff --git a/yii/helpers/Console.php b/framework/yii/helpers/Console.php similarity index 71% rename from yii/helpers/Console.php rename to framework/yii/helpers/Console.php index 0055107..a34dc96 100644 --- a/yii/helpers/Console.php +++ b/framework/yii/helpers/Console.php @@ -8,15 +8,12 @@ namespace yii\helpers; /** - * TODO adjust phpdoc - * Console View is the base class for console view components - * - * A console view provides functionality to create rich console application by allowing to format output - * by adding color and font style to it. + * Console helper provides useful methods for command line related tasks such as getting input or formatting and coloring + * output. * * @author Carsten Brandt <mail@cebe.cc> * @since 2.0 */ -class Console extends base\Console +class Console extends BaseConsole { } diff --git a/yii/helpers/FileHelper.php b/framework/yii/helpers/FileHelper.php similarity index 94% rename from yii/helpers/FileHelper.php rename to framework/yii/helpers/FileHelper.php index 04ce4e1..63954a4 100644 --- a/yii/helpers/FileHelper.php +++ b/framework/yii/helpers/FileHelper.php @@ -16,6 +16,6 @@ namespace yii\helpers; * @author Alex Makarov <sam@rmcreative.ru> * @since 2.0 */ -class FileHelper extends base\FileHelper +class FileHelper extends BaseFileHelper { } diff --git a/yii/helpers/Html.php b/framework/yii/helpers/Html.php similarity index 93% rename from yii/helpers/Html.php rename to framework/yii/helpers/Html.php index b3a0743..f4fbbba 100644 --- a/yii/helpers/Html.php +++ b/framework/yii/helpers/Html.php @@ -13,6 +13,6 @@ namespace yii\helpers; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Html extends base\Html +class Html extends BaseHtml { } diff --git a/yii/helpers/Purifier.php b/framework/yii/helpers/HtmlPurifier.php similarity index 82% rename from yii/helpers/Purifier.php rename to framework/yii/helpers/HtmlPurifier.php index b659531..e1511e4 100644 --- a/yii/helpers/Purifier.php +++ b/framework/yii/helpers/HtmlPurifier.php @@ -8,27 +8,30 @@ namespace yii\helpers; /** - * Purifier provides an ability to clean up HTML from any harmful code. + * HtmlPurifier provides an ability to clean up HTML from any harmful code. * * Basic usage is the following: * * ```php - * $my_html = Purifier::process($my_text); + * echo HtmlPurifier::process($html); * ``` * * If you want to configure it: * * ```php - * $my_html = Purifier::process($my_text, array( + * echo HtmlPurifier::process($html, array( * 'Attr.EnableID' => true, * )); * ``` * * For more details please refer to HTMLPurifier documentation](http://htmlpurifier.org/). * + * Note that you should add `ezyang/htmlpurifier` to your composer.json `require` section and run `composer install` + * before using it. + * * @author Alexander Makarov <sam@rmcreative.ru> * @since 2.0 */ -class Purifier extends base\Purifier +class HtmlPurifier extends BaseHtmlPurifier { } diff --git a/yii/helpers/Json.php b/framework/yii/helpers/Inflector.php similarity index 81% rename from yii/helpers/Json.php rename to framework/yii/helpers/Inflector.php index 5e77c3f..ab4713e 100644 --- a/yii/helpers/Json.php +++ b/framework/yii/helpers/Inflector.php @@ -8,11 +8,11 @@ namespace yii\helpers; /** + * Inflector pluralizes and singularizes English nouns. It also contains some other useful methods. * - * @author Qiang Xue <qiang.xue@gmail.com> + * @author Antonio Ramirez <amigo.cobos@gmail.com> * @since 2.0 */ -class Json extends base\Json +class Inflector extends BaseInflector { - } diff --git a/yii/debug/controllers/DefaultController.php b/framework/yii/helpers/Json.php similarity index 64% rename from yii/debug/controllers/DefaultController.php rename to framework/yii/helpers/Json.php index ca90920..8ca436a 100644 --- a/yii/debug/controllers/DefaultController.php +++ b/framework/yii/helpers/Json.php @@ -5,18 +5,15 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\debug\controllers; - -use yii\web\Controller; +namespace yii\helpers; /** + * Json is a helper class providing JSON data encoding and decoding. + * It enhances the PHP built-in functions `json_encode()` and `json_decode()` + * by supporting encoding JavaScript expressions and throwing exceptions when decoding fails. * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class DefaultController extends Controller +class Json extends BaseJson { - public function actionIndex($tag) - { - echo $tag; - } -} \ No newline at end of file +} diff --git a/yii/helpers/Markdown.php b/framework/yii/helpers/Markdown.php similarity index 92% rename from yii/helpers/Markdown.php rename to framework/yii/helpers/Markdown.php index 3f6c98e..3dcc750 100644 --- a/yii/helpers/Markdown.php +++ b/framework/yii/helpers/Markdown.php @@ -24,10 +24,12 @@ namespace yii\helpers; * )); * ``` * + * Note that in order to use this helper you need to install "michelf/php-markdown" Composer package. + * * For more details please refer to [PHP Markdown library documentation](http://michelf.ca/projects/php-markdown/). * @author Alexander Makarov <sam@rmcreative.ru> * @since 2.0 */ -class Markdown extends base\Markdown +class Markdown extends BaseMarkdown { } diff --git a/yii/helpers/SecurityHelper.php b/framework/yii/helpers/Security.php similarity index 75% rename from yii/helpers/SecurityHelper.php rename to framework/yii/helpers/Security.php index d16e7e6..0e3ee38 100644 --- a/yii/helpers/SecurityHelper.php +++ b/framework/yii/helpers/Security.php @@ -8,15 +8,15 @@ namespace yii\helpers; /** - * SecurityHelper provides a set of methods to handle common security-related tasks. + * Security provides a set of methods to handle common security-related tasks. * - * In particular, SecurityHelper supports the following features: + * In particular, Security supports the following features: * * - Encryption/decryption: [[encrypt()]] and [[decrypt()]] * - Data tampering prevention: [[hashData()]] and [[validateData()]] * - Password validation: [[generatePasswordHash()]] and [[validatePassword()]] * - * Additionally, SecurityHelper provides [[getSecretKey()]] to support generating + * Additionally, Security provides [[getSecretKey()]] to support generating * named secret keys. These secret keys, once generated, will be stored in a file * and made available in future requests. * @@ -24,6 +24,6 @@ namespace yii\helpers; * @author Tom Worster <fsb@thefsb.org> * @since 2.0 */ -class SecurityHelper extends base\SecurityHelper +class Security extends BaseSecurity { } diff --git a/yii/helpers/StringHelper.php b/framework/yii/helpers/StringHelper.php similarity index 94% rename from yii/helpers/StringHelper.php rename to framework/yii/helpers/StringHelper.php index 22b881a..5ecd390 100644 --- a/yii/helpers/StringHelper.php +++ b/framework/yii/helpers/StringHelper.php @@ -14,6 +14,6 @@ namespace yii\helpers; * @author Alex Makarov <sam@rmcreative.ru> * @since 2.0 */ -class StringHelper extends base\StringHelper +class StringHelper extends BaseStringHelper { } diff --git a/yii/helpers/VarDumper.php b/framework/yii/helpers/VarDumper.php similarity index 96% rename from yii/helpers/VarDumper.php rename to framework/yii/helpers/VarDumper.php index 59a1718..1ac5aa7 100644 --- a/yii/helpers/VarDumper.php +++ b/framework/yii/helpers/VarDumper.php @@ -23,6 +23,6 @@ namespace yii\helpers; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class VarDumper extends base\VarDumper +class VarDumper extends BaseVarDumper { } diff --git a/yii/helpers/base/mimeTypes.php b/framework/yii/helpers/mimeTypes.php similarity index 100% rename from yii/helpers/base/mimeTypes.php rename to framework/yii/helpers/mimeTypes.php diff --git a/framework/yii/i18n/DbMessageSource.php b/framework/yii/i18n/DbMessageSource.php new file mode 100644 index 0000000..14cf0b9 --- /dev/null +++ b/framework/yii/i18n/DbMessageSource.php @@ -0,0 +1,158 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\i18n; + +use Yii; +use yii\base\InvalidConfigException; +use yii\helpers\ArrayHelper; +use yii\caching\Cache; +use yii\db\Connection; +use yii\db\Query; + +/** + * DbMessageSource extends [[MessageSource]] and represents a message source that stores translated + * messages in database. + * + * The database must contain the following two tables: + * + * ~~~ + * CREATE TABLE tbl_source_message ( + * id INTEGER PRIMARY KEY, + * category VARCHAR(32), + * message TEXT + * ); + * + * CREATE TABLE tbl_message ( + * id INTEGER, + * language VARCHAR(16), + * translation TEXT, + * PRIMARY KEY (id, language), + * CONSTRAINT fk_message_source_message FOREIGN KEY (id) + * REFERENCES tbl_source_message (id) ON DELETE CASCADE ON UPDATE RESTRICT + * ); + * ~~~ + * + * The `tbl_source_message` table stores the messages to be translated, and the `tbl_message` table stores + * the translated messages. The name of these two tables can be customized by setting [[sourceMessageTable]] + * and [[messageTable]], respectively. + * + * @author resurtm <resurtm@gmail.com> + * @since 2.0 + */ +class DbMessageSource extends MessageSource +{ + /** + * Prefix which would be used when generating cache key. + */ + const CACHE_KEY_PREFIX = 'DbMessageSource'; + + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbMessageSource object is created, if you want to change this property, you should only assign + * it with a DB connection object. + */ + public $db = 'db'; + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * The messages data will be cached using this cache object. Note, this property has meaning only + * in case [[cachingDuration]] set to non-zero value. + * After the DbMessageSource object is created, if you want to change this property, you should only assign + * it with a cache object. + */ + public $cache = 'cache'; + /** + * @var string the name of the source message table. + */ + public $sourceMessageTable = 'tbl_source_message'; + /** + * @var string the name of the translated message table. + */ + public $messageTable = 'tbl_message'; + /** + * @var integer the time in seconds that the messages can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + * @see enableCaching + */ + public $cachingDuration = 0; + /** + * @var boolean whether to enable caching translated messages + */ + public $enableCaching = false; + + /** + * Initializes the DbMessageSource component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * Configured [[cache]] component would also be initialized. + * @throws InvalidConfigException if [[db]] is invalid or [[cache]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbMessageSource::db must be either a DB connection instance or the application component ID of a DB connection."); + } + if ($this->enableCaching) { + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + if (!$this->cache instanceof Cache) { + throw new InvalidConfigException("DbMessageSource::cache must be either a cache object or the application component ID of the cache object."); + } + } + } + + /** + * Loads the message translation for the specified language and category. + * Child classes should override this method to return the message translations of + * the specified language and category. + * @param string $category the message category + * @param string $language the target language + * @return array the loaded messages. The keys are original messages, and the values + * are translated messages. + */ + protected function loadMessages($category, $language) + { + if ($this->enableCaching) { + $key = array( + __CLASS__, + $category, + $language, + ); + $messages = $this->cache->get($key); + if ($messages === false) { + $messages = $this->loadMessagesFromDb($category, $language); + $this->cache->set($key, $messages, $this->cachingDuration); + } + return $messages; + } else { + return $this->loadMessagesFromDb($category, $language); + } + } + + /** + * Loads the messages from database. + * You may override this method to customize the message storage in the database. + * @param string $category the message category. + * @param string $language the target language. + * @return array the messages loaded from database. + */ + protected function loadMessagesFromDb($category, $language) + { + $query = new Query(); + $messages = $query->select(array('t1.message message', 't2.translation translation')) + ->from(array($this->sourceMessageTable . ' t1', $this->messageTable . ' t2')) + ->where('t1.id = t2.id AND t1.category = :category AND t2.language = :language') + ->params(array(':category' => $category, ':language' => $language)) + ->createCommand($this->db) + ->queryAll(); + return ArrayHelper::map($messages, 'message', 'translation'); + } +} diff --git a/framework/yii/i18n/Formatter.php b/framework/yii/i18n/Formatter.php new file mode 100644 index 0000000..979a46f --- /dev/null +++ b/framework/yii/i18n/Formatter.php @@ -0,0 +1,291 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\i18n; + +use Yii; +use IntlDateFormatter; +use NumberFormatter; +use DateTime; +use yii\base\InvalidConfigException; + +/** + * Formatter is the localized version of [[\yii\base\Formatter]]. + * + * Formatter requires the PHP "intl" extension to be installed. Formatter supports localized + * formatting of date, time and numbers, based on the current [[locale]]. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Formatter extends \yii\base\Formatter +{ + /** + * @var string the locale ID that is used to localize the date and number formatting. + * If not set, [[\yii\base\Application::language]] will be used. + */ + public $locale; + /** + * @var string the default format string to be used to format a date. + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + */ + public $dateFormat = 'short'; + /** + * @var string the default format string to be used to format a time. + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + */ + public $timeFormat = 'short'; + /** + * @var string the default format string to be used to format a date and time. + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + */ + public $datetimeFormat = 'short'; + /** + * @var array the options to be set for the NumberFormatter objects. Please refer to + * [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute) + * for the possible options. This property is used by [[createNumberFormatter]] when + * creating a new number formatter to format decimals, currencies, etc. + */ + public $numberFormatOptions = array(); + /** + * @var string the character displayed as the decimal point when formatting a number. + * If not set, the decimal separator corresponding to [[locale]] will be used. + */ + public $decimalSeparator; + /** + * @var string the character displayed as the thousands separator character when formatting a number. + * If not set, the thousand separator corresponding to [[locale]] will be used. + */ + public $thousandSeparator; + + + /** + * Initializes the component. + * This method will check if the "intl" PHP extension is installed and set the + * default value of [[locale]]. + * @throws InvalidConfigException if the "intl" PHP extension is not installed. + */ + public function init() + { + if (!extension_loaded('intl')) { + throw new InvalidConfigException('The "intl" PHP extension is not install. It is required to format data values in localized formats.'); + } + if ($this->locale === null) { + $this->locale = Yii::$app->language; + } + if ($this->decimalSeparator === null || $this->thousandSeparator === null) { + $formatter = new NumberFormatter($this->locale, NumberFormatter::DECIMAL); + if ($this->decimalSeparator === null) { + $this->decimalSeparator = $formatter->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); + } + if ($this->thousandSeparator === null) { + $this->thousandSeparator = $formatter->getSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL); + } + } + + parent::init(); + } + + private $_dateFormats = array( + 'short' => IntlDateFormatter::SHORT, + 'medium' => IntlDateFormatter::MEDIUM, + 'long' => IntlDateFormatter::LONG, + 'full' => IntlDateFormatter::FULL, + ); + + /** + * Formats the value as a date. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[dateFormat]] will be used. + * + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + * + * @return string the formatted result + * @see dateFormat + */ + public function asDate($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + $value = $this->normalizeDatetimeValue($value); + if ($format === null) { + $format = $this->dateFormat; + } + if (isset($this->_dateFormats[$format])) { + $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE); + } else { + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE); + $formatter->setPattern($format); + } + return $formatter->format($value); + } + + /** + * Formats the value as a time. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[dateFormat]] will be used. + * + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + * + * @return string the formatted result + * @see timeFormat + */ + public function asTime($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + $value = $this->normalizeDatetimeValue($value); + if ($format === null) { + $format = $this->timeFormat; + } + if (isset($this->_dateFormats[$format])) { + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format]); + } else { + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE); + $formatter->setPattern($format); + } + return $formatter->format($value); + } + + /** + * Formats the value as a datetime. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[dateFormat]] will be used. + * + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + * + * @return string the formatted result + * @see datetimeFormat + */ + public function asDatetime($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + $value = $this->normalizeDatetimeValue($value); + if ($format === null) { + $format = $this->datetimeFormat; + } + if (isset($this->_dateFormats[$format])) { + $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format]); + } else { + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE); + $formatter->setPattern($format); + } + return $formatter->format($value); + } + + /** + * Formats the value as a decimal number. + * @param mixed $value the value to be formatted + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * for details on how to specify a format. + * @return string the formatted result. + */ + public function asDecimal($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + return $this->createNumberFormatter(NumberFormatter::DECIMAL, $format)->format($value); + } + + /** + * Formats the value as a currency number. + * @param mixed $value the value to be formatted + * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use. + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * for details on how to specify a format. + * @return string the formatted result. + */ + public function asCurrency($value, $currency = 'USD', $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + return $this->createNumberFormatter(NumberFormatter::CURRENCY, $format)->formatCurrency($value, $currency); + } + + /** + * Formats the value as a percent number. + * @param mixed $value the value to be formatted + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * for details on how to specify a format. + * @return string the formatted result. + */ + public function asPercent($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + return $this->createNumberFormatter(NumberFormatter::PERCENT, $format)->format($value); + } + + /** + * Formats the value as a scientific number. + * @param mixed $value the value to be formatted + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * for details on how to specify a format. + * @return string the formatted result. + */ + public function asScientific($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + return $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $format)->format($value); + } + + /** + * Creates a number formatter based on the given type and format. + * @param integer $type the type of the number formatter + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * @return NumberFormatter the created formatter instance + */ + protected function createNumberFormatter($type, $format) + { + $formatter = new NumberFormatter($this->locale, $type); + if ($format !== null) { + $formatter->setPattern($format); + } + if (!empty($this->numberFormatOptions)) { + foreach ($this->numberFormatOptions as $name => $attribute) { + $formatter->setAttribute($name, $attribute); + } + } + return $formatter; + } +} diff --git a/yii/i18n/GettextFile.php b/framework/yii/i18n/GettextFile.php similarity index 100% rename from yii/i18n/GettextFile.php rename to framework/yii/i18n/GettextFile.php diff --git a/yii/i18n/GettextMessageSource.php b/framework/yii/i18n/GettextMessageSource.php similarity index 79% rename from yii/i18n/GettextMessageSource.php rename to framework/yii/i18n/GettextMessageSource.php index 0eb7cb3..745eb38 100644 --- a/yii/i18n/GettextMessageSource.php +++ b/framework/yii/i18n/GettextMessageSource.php @@ -9,6 +9,23 @@ namespace yii\i18n; use Yii; +/** + * GettextMessageSource represents a message source that is based on GNU Gettext. + * + * Each GettextMessageSource instance represents the message tranlations + * for a single domain. And each message category represents a message context + * in Gettext. Translated messages are stored as either a MO or PO file, + * depending on the [[useMoFile]] property value. + * + * All translations are saved under the [[basePath]] directory. + * + * Translations in one language are kept as MO or PO files under an individual + * subdirectory whose name is the language ID. The file name is specified via + * [[catalog]] property, which defaults to 'messages'. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ class GettextMessageSource extends MessageSource { const MO_FILE_EXT = '.mo'; @@ -31,6 +48,15 @@ class GettextMessageSource extends MessageSource */ public $useBigEndian = false; + /** + * Loads the message translation for the specified language and category. + * Child classes should override this method to return the message translations of + * the specified language and category. + * @param string $category the message category + * @param string $language the target language + * @return array the loaded messages. The keys are original messages, and the values + * are translated messages. + */ protected function loadMessages($category, $language) { $messageFile = Yii::getAlias($this->basePath) . '/' . $language . '/' . $this->catalog; diff --git a/yii/i18n/GettextMoFile.php b/framework/yii/i18n/GettextMoFile.php similarity index 99% rename from yii/i18n/GettextMoFile.php rename to framework/yii/i18n/GettextMoFile.php index bacba52..c12aebe 100644 --- a/yii/i18n/GettextMoFile.php +++ b/framework/yii/i18n/GettextMoFile.php @@ -109,7 +109,7 @@ class GettextMoFile extends GettextFile if (($context && $separatorPosition !== false && substr($id, 0, $separatorPosition) === $context) || (!$context && $separatorPosition === false)) { if ($separatorPosition !== false) { - $id = substr($id,$separatorPosition+1); + $id = substr($id, $separatorPosition+1); } $message = $this->readString($fileHandle, $targetLengths[$i], $targetOffsets[$i]); diff --git a/yii/i18n/GettextPoFile.php b/framework/yii/i18n/GettextPoFile.php similarity index 100% rename from yii/i18n/GettextPoFile.php rename to framework/yii/i18n/GettextPoFile.php diff --git a/yii/i18n/I18N.php b/framework/yii/i18n/I18N.php similarity index 65% rename from yii/i18n/I18N.php rename to framework/yii/i18n/I18N.php index 7fae5b0..625ba5c 100644 --- a/yii/i18n/I18N.php +++ b/framework/yii/i18n/I18N.php @@ -10,7 +10,7 @@ namespace yii\i18n; use Yii; use yii\base\Component; use yii\base\InvalidConfigException; -use yii\base\InvalidParamException; +use yii\log\Logger; /** * I18N provides features related with internationalization (I18N) and localization (L10N). @@ -37,17 +37,6 @@ class I18N extends Component * 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. @@ -73,43 +62,43 @@ 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 $category the message category. * @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. + * @param string $language the language code (e.g. `en_US`, `en`). * @return string the translated message. */ - public function translate($message, $params = array(), $language = null) + public function translate($category, $message, $params, $language) { - if ($language === null) { - $language = Yii::$app->language; - } - - // allow chars for category: word chars, ".", "-", "/","\" - if (strpos($message, '|') !== false && preg_match('/^([\w\-\\/\.\\\\]+)\|(.*)/', $message, $matches)) { - $category = $matches[1]; - $message = $matches[2]; - } else { - $category = 'app'; - } - $message = $this->getMessageSource($category)->translate($category, $message, $language); - if (!is_array($params)) { - $params = array($params); + $params = (array)$params; + if ($params === array()) { + return $message; } - if (isset($params[0])) { - $message = $this->applyPluralRules($message, $params[0], $language); - if (!isset($params['{n}'])) { - $params['{n}'] = $params[0]; + if (class_exists('MessageFormatter', false) && preg_match('~{\s*[\d\w]+\s*,~u', $message)) { + $formatter = new MessageFormatter($language, $message); + if ($formatter === null) { + Yii::warning("$language message from category $category is invalid. Message is: $message."); + return $message; + } + $result = $formatter->format($params); + if ($result === false) { + $errorMessage = $formatter->getErrorMessage(); + Yii::warning("$language message from category $category failed with error: $errorMessage. Message is: $message."); + return $message; + } else { + return $result; } - unset($params[0]); } - return empty($params) ? $message : strtr($message, $params); + $p = array(); + foreach($params as $name => $value) { + $p['{' . $name . '}'] = $value; + } + return strtr($message, $p); } /** @@ -125,7 +114,7 @@ class I18N extends Component } else { // try wildcard matching foreach ($this->translations as $pattern => $config) { - if (substr($pattern, -1) === '*' && strpos($category, rtrim($pattern, '*')) === 0) { + if ($pattern === '*' || substr($pattern, -1) === '*' && strpos($category, rtrim($pattern, '*')) === 0) { $source = $config; break; } @@ -137,62 +126,4 @@ class I18N extends Component throw new InvalidConfigException("Unable to locate message source for category '$category'."); } } - - /** - * 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->getPluralRules($language); - foreach ($rules as $i => $rule) { - if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { - return $chunks[$i]; - } - } - $n = count($rules); - return isset($chunks[$n]) ? $chunks[$n] : $chunks[0]; - } - - 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 - * @param mixed $n the number value - * @return boolean the expression result - */ - protected function evaluate($expression, $n) - { - return eval("return $expression;"); - } } diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php new file mode 100644 index 0000000..2325573 --- /dev/null +++ b/framework/yii/i18n/MessageFormatter.php @@ -0,0 +1,117 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\i18n; + +/** + * MessageFormatter is an enhanced version of PHP intl class that no matter which PHP and ICU versions are used: + * + * - Accepts named arguments and mixed numeric and named arguments. + * - Issues no error when an insufficient number of arguments have been provided. Instead, the placeholders will not be + * substituted. + * - Fixes PHP 5.5 weird placeholder replacement in case no arguments are provided at all (https://bugs.php.net/bug.php?id=65920). + * + * @author Alexander Makarov <sam@rmcreative.ru> + * @author Carsten Brandt <mail@cebe.cc> + * @since 2.0 + */ +class MessageFormatter extends \MessageFormatter +{ + /** + * Format the message. + * + * @link http://php.net/manual/en/messageformatter.format.php + * @param array $args Arguments to insert into the format string. + * @return string|boolean The formatted string, or false if an error occurred. + */ + public function format($args) + { + if ($args === array()) { + return $this->getPattern(); + } + + if (version_compare(PHP_VERSION, '5.5.0', '<')) { + $pattern = self::replaceNamedArguments($this->getPattern(), $args); + $this->setPattern($pattern); + $args = array_values($args); + } + return parent::format($args); + } + + /** + * Quick format message. + * + * @link http://php.net/manual/en/messageformatter.formatmessage.php + * @param string $locale The locale to use for formatting locale-dependent parts. + * @param string $pattern The pattern string to insert things into. + * @param array $args The array of values to insert into the format string. + * @return string|boolean The formatted pattern string or false if an error occurred. + */ + public static function formatMessage($locale, $pattern, $args) + { + if ($args === array()) { + return $pattern; + } + + if (version_compare(PHP_VERSION, '5.5.0', '<')) { + $pattern = self::replaceNamedArguments($pattern, $args); + $args = array_values($args); + } + return parent::formatMessage($locale, $pattern, $args); + } + + /** + * Replace named placeholders with numeric placeholders and quote unused. + * + * @param string $pattern The pattern string to replace things into. + * @param array $args The array of values to insert into the format string. + * @return string The pattern string with placeholders replaced. + */ + private static function replaceNamedArguments($pattern, $args) + { + $map = array_flip(array_keys($args)); + + // parsing pattern based on ICU grammar: + // http://icu-project.org/apiref/icu4c/classMessageFormat.html#details + $parts = explode('{', $pattern); + $c = count($parts); + $pattern = $parts[0]; + $d = 0; + $stack = array(); + for($i = 1; $i < $c; $i++) { + if (preg_match('~^(\s*)([\d\w]+)(\s*)([},])(\s*)(.*)$~us', $parts[$i], $matches)) { + // if we are not inside a plural or select this is a message + if (!isset($stack[$d]) || $stack[$d] != 'plural' && $stack[$d] != 'select') { + $d++; + // replace normal arg if it is available + if (isset($map[$matches[2]])) { + $q = ''; + $pattern .= '{' . $matches[1] . $map[$matches[2]] . $matches[3]; + } else { + // quote unused args + $q = ($matches[4] == '}') ? "'" : ""; + $pattern .= "$q{" . $matches[1] . $matches[2] . $matches[3]; + } + $pattern .= ($term = $matches[4] . $q . $matches[5] . $matches[6]); + // store type of current level + $stack[$d] = ($matches[4] == ',') ? substr($matches[6], 0, 6) : 'none'; + // if it's plural or select, the next bracket is NOT begin of a message then! + if ($stack[$d] == 'plural' || $stack[$d] == 'select') { + $i++; + $d -= substr_count($term, '}'); + } else { + $d -= substr_count($term, '}'); + continue; + } + } + } + $pattern .= '{' . $parts[$i]; + $d += 1 - substr_count($parts[$i], '}'); + } + return $pattern; + } +} diff --git a/yii/i18n/MessageSource.php b/framework/yii/i18n/MessageSource.php similarity index 100% rename from yii/i18n/MessageSource.php rename to framework/yii/i18n/MessageSource.php index cf23338..90adbfb 100644 --- a/yii/i18n/MessageSource.php +++ b/framework/yii/i18n/MessageSource.php @@ -118,4 +118,3 @@ class MessageSource extends Component } } } - diff --git a/yii/i18n/MissingTranslationEvent.php b/framework/yii/i18n/MissingTranslationEvent.php similarity index 100% rename from yii/i18n/MissingTranslationEvent.php rename to framework/yii/i18n/MissingTranslationEvent.php diff --git a/yii/i18n/PhpMessageSource.php b/framework/yii/i18n/PhpMessageSource.php similarity index 95% rename from yii/i18n/PhpMessageSource.php rename to framework/yii/i18n/PhpMessageSource.php index f62939f..1cd2103 100644 --- a/yii/i18n/PhpMessageSource.php +++ b/framework/yii/i18n/PhpMessageSource.php @@ -26,6 +26,8 @@ use Yii; * ); * ~~~ * + * You may use [[fileMap]] to customize the association between category names and the file names. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -60,10 +62,8 @@ class PhpMessageSource extends MessageSource $messageFile = Yii::getAlias($this->basePath) . "/$language/"; if (isset($this->fileMap[$category])) { $messageFile .= $this->fileMap[$category]; - } elseif (($pos = strrpos($category, '\\')) !== false) { - $messageFile .= (substr($category, $pos) . '.php'); } else { - $messageFile .= "$category.php"; + $messageFile .= str_replace('\\', '/', $category) . '.php'; } if (is_file($messageFile)) { $messages = include($messageFile); diff --git a/yii/logging/DbTarget.php b/framework/yii/log/DbTarget.php similarity index 95% rename from yii/logging/DbTarget.php rename to framework/yii/log/DbTarget.php index ce9d843..a828a72 100644 --- a/yii/logging/DbTarget.php +++ b/framework/yii/log/DbTarget.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\logging; +namespace yii\log; use Yii; use yii\db\Connection; @@ -72,16 +72,14 @@ class DbTarget extends Target /** * Stores log messages to DB. - * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure - * of each message. */ - public function export($messages) + public function export() { $tableName = $this->db->quoteTableName($this->logTable); $sql = "INSERT INTO $tableName ([[level]], [[category]], [[log_time]], [[message]]) VALUES (:level, :category, :log_time, :message)"; $command = $this->db->createCommand($sql); - foreach ($messages as $message) { + foreach ($this->messages as $message) { $command->bindValues(array( ':level' => $message[1], ':category' => $message[2], diff --git a/yii/logging/EmailTarget.php b/framework/yii/log/EmailTarget.php similarity index 90% rename from yii/logging/EmailTarget.php rename to framework/yii/log/EmailTarget.php index bb02e34..8ae1a88 100644 --- a/yii/logging/EmailTarget.php +++ b/framework/yii/log/EmailTarget.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\logging; +namespace yii\log; /** * EmailTarget sends selected log messages to the specified email addresses. @@ -38,17 +38,15 @@ class EmailTarget 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. */ - public function export($messages) + public function export() { $body = ''; - foreach ($messages as $message) { + foreach ($this->messages as $message) { $body .= $this->formatMessage($message); } $body = wordwrap($body, 70); - $subject = $this->subject === null ? \Yii::t('yii|Application Log') : $this->subject; + $subject = $this->subject === null ? \Yii::t('yii', 'Application Log') : $this->subject; foreach ($this->emails as $email) { $this->sendEmail($subject, $body, $email, $this->sentFrom, $this->headers); } diff --git a/yii/logging/FileTarget.php b/framework/yii/log/FileTarget.php similarity index 85% rename from yii/logging/FileTarget.php rename to framework/yii/log/FileTarget.php index 69799cd..5aa4c12 100644 --- a/yii/logging/FileTarget.php +++ b/framework/yii/log/FileTarget.php @@ -5,10 +5,11 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\logging; +namespace yii\log; use Yii; use yii\base\InvalidConfigException; +use yii\helpers\FileHelper; /** * FileTarget records log messages in a file. @@ -37,6 +38,19 @@ class FileTarget extends Target * @var integer number of log files used for rotation. Defaults to 5. */ public $maxLogFiles = 5; + /** + * @var integer the permission to be set for newly created log files. + * This value will be used by PHP chmod() function. No umask will be applied. + * If not set, the permission will be determined by the current environment. + */ + public $fileMode; + /** + * @var integer the permission to be set for newly created directories. + * This value will be used by PHP chmod() function. No umask will be applied. + * Defaults to 0775, meaning the directory is read-writable by owner and group, + * but read-only for other users. + */ + public $dirMode = 0775; /** @@ -53,7 +67,7 @@ class FileTarget extends Target } $logPath = dirname($this->logFile); if (!is_dir($logPath)) { - @mkdir($logPath, 0777, true); + FileHelper::createDirectory($logPath, $this->dirMode, true); } if ($this->maxLogFiles < 1) { $this->maxLogFiles = 1; @@ -64,15 +78,13 @@ 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. + * Writes log messages to a file. * @throws InvalidConfigException if unable to open the log file for writing */ - public function export($messages) + public function export() { $text = ''; - foreach ($messages as $message) { + foreach ($this->messages as $message) { $text .= $this->formatMessage($message); } if (($fp = @fopen($this->logFile, 'a')) === false) { @@ -81,14 +93,17 @@ class FileTarget extends Target @flock($fp, LOCK_EX); if (@filesize($this->logFile) > $this->maxFileSize * 1024) { $this->rotateFiles(); - @flock($fp,LOCK_UN); + @flock($fp, LOCK_UN); @fclose($fp); @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX); } else { @fwrite($fp, $text); - @flock($fp,LOCK_UN); + @flock($fp, LOCK_UN); @fclose($fp); } + if ($this->fileMode !== null) { + @chmod($this->logFile, $this->fileMode); + } } /** diff --git a/yii/logging/Logger.php b/framework/yii/log/Logger.php similarity index 50% rename from yii/logging/Logger.php rename to framework/yii/log/Logger.php index b557c5e..2046ecc 100644 --- a/yii/logging/Logger.php +++ b/framework/yii/log/Logger.php @@ -5,17 +5,69 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\logging; +namespace yii\log; -use \yii\base\Component; -use \yii\base\InvalidConfigException; +use Yii; +use yii\base\Component; +use yii\base\InvalidConfigException; /** - * Logger records logged messages in memory. + * Logger records logged messages in memory and sends them to different targets as needed. + * + * Logger is registered as a core application component and can be accessed using `Yii::$app->log`. + * You can call the method [[log()]] to record a single log message. For convenience, a set of shortcut + * methods are provided for logging messages of various severity levels via the [[Yii]] class: + * + * - [[Yii::trace()]] + * - [[Yii::error()]] + * - [[Yii::warning()]] + * - [[Yii::info()]] + * - [[Yii::beginProfile()]] + * - [[Yii::endProfile()]] + * + * When enough messages are accumulated in the logger, or when the current request finishes, + * the logged messages will be sent to different [[targets]], such as log files, emails. + * + * You may configure the targets in application configuration, like the following: + * + * ~~~ + * array( + * 'components' => array( + * 'log' => array( + * 'targets' => array( + * 'file' => array( + * 'class' => 'yii\log\FileTarget', + * 'levels' => array('trace', 'info'), + * 'categories' => array('yii\*'), + * ), + * 'email' => array( + * 'class' => 'yii\log\EmailTarget', + * 'levels' => array('error', 'warning'), + * 'emails' => array('admin@example.com'), + * ), + * ), + * ), + * ), + * ) + * ~~~ + * + * Each log target can have a name and can be referenced via the [[targets]] property + * as follows: + * + * ~~~ + * Yii::$app->log->targets['file']->enabled = false; + * ~~~ * * When the application ends or [[flushInterval]] is reached, Logger will call [[flush()]] * to send logged messages to different log targets, such as file, email, Web. * + * @property array $dbProfiling The first element indicates the number of SQL statements executed, and the + * second element the total time spent in SQL execution. This property is read-only. + * @property float $elapsedTime The total elapsed time in seconds for current request. This property is + * read-only. + * @property array $profiling The profiling results. Each array element has the following structure: + * `array($token, $category, $time)`. This property is read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -57,15 +109,7 @@ class Logger extends Component /** - * @var integer how many messages should be logged before they are flushed from memory and sent to targets. - * Defaults to 1000, meaning the [[flush]] method will be invoked once every 1000 messages logged. - * Set this property to be 0 if you don't want to flush messages until the application terminates. - * This property mainly affects how much memory will be taken by the logged messages. - * A smaller value means less memory, but will increase the execution time due to the overhead of [[flush()]]. - */ - public $flushInterval = 1000; - /** - * @var array logged messages. This property is mainly managed by [[log()]] and [[flush()]]. + * @var array logged messages. This property is managed by [[log()]] and [[flush()]]. * Each log message is of the following structure: * * ~~~ @@ -74,19 +118,37 @@ class Logger extends Component * [1] => level (integer) * [2] => category (string) * [3] => timestamp (float, obtained by microtime(true)) + * [4] => traces (array, debug backtrace, contains the application code call stacks) * ) * ~~~ */ public $messages = array(); /** - * @var Router the log target router registered with this logger. + * @var array debug data. This property stores various types of debug data reported at + * different instrument places. + */ + public $data = array(); + /** + * @var array|Target[] the log targets. Each array element represents a single [[Target|log target]] instance + * or the configuration for creating the log target instance. + */ + public $targets = array(); + /** + * @var integer how many messages should be logged before they are flushed from memory and sent to targets. + * Defaults to 1000, meaning the [[flush]] method will be invoked once every 1000 messages logged. + * Set this property to be 0 if you don't want to flush messages until the application terminates. + * This property mainly affects how much memory will be taken by the logged messages. + * A smaller value means less memory, but will increase the execution time due to the overhead of [[flush()]]. */ - public $router; + public $flushInterval = 1000; /** - * @var string a tag that uniquely identifies the current request. This can be used - * to differentiate the log messages for different requests. + * @var integer how much call stack information (file name and line number) should be logged for each message. + * If it is greater than 0, at most that number of call stacks will be logged. Note that only application + * call stacks are counted. + * + * If not set, it will default to 3 when `YII_ENV` is set as "dev", and 0 otherwise. */ - public $tag; + public $traceLevel; /** * Initializes the logger by registering [[flush()]] as a shutdown function. @@ -94,14 +156,21 @@ class Logger extends Component public function init() { parent::init(); - $this->tag = date('Ymd-His', microtime(true)); + if ($this->traceLevel === null) { + $this->traceLevel = YII_ENV_DEV ? 3 : 0; + } + foreach ($this->targets as $name => $target) { + if (!$target instanceof Target) { + $this->targets[$name] = Yii::createObject($target); + } + } register_shutdown_function(array($this, 'flush'), true); } /** * Logs a message with the given type and category. - * If `YII_DEBUG` is true and `YII_TRACE_LEVEL` is greater than 0, then additional - * call stack information about application code will be appended to the message. + * If [[traceLevel]] is greater than 0, additional call stack information about + * the application code will be logged as well. * @param string $message the message to be logged. * @param integer $level the level of the message. This must be one of the following: * `Logger::LEVEL_ERROR`, `Logger::LEVEL_WARNING`, `Logger::LEVEL_INFO`, `Logger::LEVEL_TRACE`, @@ -111,19 +180,22 @@ class Logger extends Component public function log($message, $level, $category = 'application') { $time = microtime(true); - if (YII_DEBUG && YII_TRACE_LEVEL > 0) { - $traces = debug_backtrace(); + $traces = array(); + if ($this->traceLevel > 0) { $count = 0; - foreach ($traces as $trace) { + $ts = debug_backtrace(); + array_pop($ts); // remove the last trace since it would be the entry script, not very useful + foreach ($ts as $trace) { if (isset($trace['file'], $trace['line']) && strpos($trace['file'], YII_PATH) !== 0) { - $message .= "\nin {$trace['file']} ({$trace['line']})"; - if (++$count >= YII_TRACE_LEVEL) { + unset($trace['object'], $trace['args']); + $traces[] = $trace; + if (++$count >= $this->traceLevel) { break; } } } } - $this->messages[] = array($message, $level, $category, $time); + $this->messages[] = array($message, $level, $category, $time, $traces); if ($this->flushInterval > 0 && count($this->messages) >= $this->flushInterval) { $this->flush(); } @@ -131,13 +203,15 @@ class Logger extends Component /** * Flushes log messages from memory to targets. - * This method will trigger an [[EVENT_FLUSH]] or [[EVENT_FINAL_FLUSH]] event depending on the $final value. * @param boolean $final whether this is a final call during a request. */ public function flush($final = false) { - if ($this->router) { - $this->router->dispatch($this->messages, $final); + /** @var Target $target */ + foreach ($this->targets as $target) { + if ($target->enabled) { + $target->collect($this->messages, $final); + } } $this->messages = array(); } @@ -146,7 +220,7 @@ class Logger extends Component * Returns the total elapsed time since the start of the current request. * This method calculates the difference between now and the timestamp * defined by constant `YII_BEGIN_TIME` which is evaluated at the beginning - * of [[YiiBase]] class file. + * of [[BaseYii]] class file. * @return float the total elapsed time in seconds for current request. */ public function getElapsedTime() @@ -205,6 +279,24 @@ class Logger extends Component return array_values($timings); } + /** + * Returns the statistical results of DB queries. + * The results returned include the number of SQL statements executed and + * the total time spent. + * @return array the first element indicates the number of SQL statements executed, + * and the second element the total time spent in SQL execution. + */ + public function getDbProfiling() + { + $timings = $this->getProfiling(array('yii\db\Command::query', 'yii\db\Command::execute')); + $count = count($timings); + $time = 0; + foreach ($timings as $timing) { + $time += $timing[1]; + } + return array($count, $time); + } + private function calculateTimings() { $timings = array(); @@ -231,4 +323,22 @@ class Logger extends Component return $timings; } + + /** + * Returns the text display of the specified level. + * @param integer $level the message level, e.g. [[LEVEL_ERROR]], [[LEVEL_WARNING]]. + * @return string the text display of the level + */ + public static function getLevelName($level) + { + static $levels = array( + self::LEVEL_ERROR => 'error', + self::LEVEL_WARNING => 'warning', + self::LEVEL_INFO => 'info', + self::LEVEL_TRACE => 'trace', + self::LEVEL_PROFILE_BEGIN => 'profile begin', + self::LEVEL_PROFILE_END => 'profile end', + ); + return isset($levels[$level]) ? $levels[$level] : 'unknown'; + } } diff --git a/yii/logging/Target.php b/framework/yii/log/Target.php similarity index 79% rename from yii/logging/Target.php rename to framework/yii/log/Target.php index 311334d..0cb72ef 100644 --- a/yii/logging/Target.php +++ b/framework/yii/log/Target.php @@ -5,8 +5,10 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\logging; +namespace yii\log; +use Yii; +use yii\base\Component; use yii\base\InvalidConfigException; /** @@ -20,12 +22,14 @@ use yii\base\InvalidConfigException; * satisfying both filter conditions will be handled. Additionally, you * may specify [[except]] to exclude messages of certain categories. * - * @property integer $levels the message levels that this target is interested in. + * @property integer $levels The message levels that this target is interested in. This is a bitmap of level + * values. Defaults to 0, meaning all available levels. Note that the type of this property differs in getter + * and setter. See [[getLevels()]] and [[setLevels()]] for details. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -abstract class Target extends \yii\base\Component +abstract class Target extends Component { /** * @var boolean whether to enable this log target. Defaults to true. @@ -66,18 +70,17 @@ abstract class Target extends \yii\base\Component public $exportInterval = 1000; /** * @var array the messages that are retrieved from the logger so far by this log target. + * Please refer to [[Logger::messages]] for the details about the message structure. */ - private $_messages = array(); + public $messages = array(); private $_levels = 0; /** - * Exports log messages to a specific destination. + * Exports log [[messages]] to a specific destination. * Child classes must implement this method. - * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure - * of each message. */ - abstract public function export($messages); + abstract public function export(); /** * Processes the given log messages. @@ -89,14 +92,14 @@ abstract class Target extends \yii\base\Component */ public function collect($messages, $final) { - $this->_messages = array_merge($this->_messages, $this->filterMessages($messages)); - $count = count($this->_messages); + $this->messages = array_merge($this->messages, $this->filterMessages($messages, $this->getLevels(), $this->categories, $this->except)); + $count = count($this->messages); if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) { if (($context = $this->getContextMessage()) !== '') { - $this->_messages[] = array($context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME); + $this->messages[] = array($context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME); } - $this->export($this->_messages); - $this->_messages = array(); + $this->export(); + $this->messages = array(); } } @@ -108,8 +111,9 @@ abstract class Target extends \yii\base\Component protected function getContextMessage() { $context = array(); - if ($this->logUser && ($user = \Yii::$app->getComponent('user', false)) !== null) { - $context[] = 'User: ' . $user->getName() . ' (ID: ' . $user->getId() . ')'; + if ($this->logUser && ($user = Yii::$app->getComponent('user', false)) !== null) { + /** @var $user \yii\web\User */ + $context[] = 'User: ' . $user->getId(); } foreach ($this->logVars as $name) { @@ -176,22 +180,22 @@ abstract class Target extends \yii\base\Component /** * Filters the given messages according to their categories and levels. * @param array $messages messages to be filtered + * @param integer $levels the message levels to filter by. This is a bitmap of + * level values. Value 0 means allowing all levels. + * @param array $categories the message categories to filter by. If empty, it means all categories are allowed. + * @param array $except the message categories to exclude. If empty, it means all categories are allowed. * @return array the filtered messages. - * @see filterByCategory - * @see filterByLevel */ - protected function filterMessages($messages) + public static function filterMessages($messages, $levels = 0, $categories = array(), $except = array()) { - $levels = $this->getLevels(); - foreach ($messages as $i => $message) { if ($levels && !($levels & $message[1])) { unset($messages[$i]); continue; } - $matched = empty($this->categories); - foreach ($this->categories as $category) { + $matched = empty($categories); + foreach ($categories as $category) { if ($message[2] === $category || substr($category, -1) === '*' && strpos($message[2], rtrim($category, '*')) === 0) { $matched = true; break; @@ -199,13 +203,11 @@ abstract class Target extends \yii\base\Component } if ($matched) { - foreach ($this->except as $category) { + foreach ($except as $category) { $prefix = rtrim($category, '*'); - foreach ($messages as $i => $message) { - if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { - $matched = false; - break; - } + if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { + $matched = false; + break; } } } @@ -225,16 +227,8 @@ abstract class Target extends \yii\base\Component */ public function formatMessage($message) { - static $levels = array( - Logger::LEVEL_ERROR => 'error', - Logger::LEVEL_WARNING => 'warning', - Logger::LEVEL_INFO => 'info', - Logger::LEVEL_TRACE => 'trace', - Logger::LEVEL_PROFILE_BEGIN => 'profile begin', - Logger::LEVEL_PROFILE_END => 'profile end', - ); list($text, $level, $category, $timestamp) = $message; - $level = isset($levels[$level]) ? $levels[$level] : 'unknown'; + $level = Logger::getLevelName($level); if (!is_string($text)) { $text = var_export($text, true); } diff --git a/framework/yii/messages/config.php b/framework/yii/messages/config.php new file mode 100644 index 0000000..e1c79ff --- /dev/null +++ b/framework/yii/messages/config.php @@ -0,0 +1,45 @@ +<?php + +return array( + // string, required, root directory of all source files + 'sourcePath' => __DIR__ . '/..', + // string, required, root directory containing message translations. + 'messagePath' => __DIR__, + // array, required, list of language codes that the extracted messages + // should be translated to. For example, array('zh_cn', 'de'). + 'languages' => array('de'), + // string, the name of the function for translating messages. + // Defaults to 'Yii::t'. This is used as a mark to find the messages to be + // translated. You may use a string for single function name or an array for + // multiple function names. + 'translator' => 'Yii::t', + // boolean, whether to sort messages by keys when merging new messages + // with the existing ones. Defaults to false, which means the new (untranslated) + // messages will be separated from the old (translated) ones. + 'sort' => false, + // boolean, whether the message file should be overwritten with the merged messages + 'overwrite' => true, + // boolean, whether to remove messages that no longer appear in the source code. + // Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks. + 'removeUnused' => false, + // array, list of patterns that specify which files/directories should be processed. + // If empty or not set, all files/directories will be processed. + // A path matches a pattern if it contains the pattern string at its end. For example, + // '/a/b' will match all files and directories ending with '/a/b'; + // and the '.svn' will match all files and directories whose name ends with '.svn'. + // Note, the '/' characters in a pattern matches both '/' and '\'. + // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed. + 'only' => array('.php'), + // array, list of patterns that specify which files/directories should NOT be processed. + // If empty or not set, all files/directories will be processed. + // Please refer to "only" for details about the patterns. + 'except' => array( + '.svn', + '.git', + '.gitignore', + '.gitkeep', + '.hgignore', + '.hgkeep', + '/messages', + ), +); diff --git a/framework/yii/rbac/Assignment.php b/framework/yii/rbac/Assignment.php new file mode 100644 index 0000000..065aaa5 --- /dev/null +++ b/framework/yii/rbac/Assignment.php @@ -0,0 +1,55 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rbac; + +use Yii; +use yii\base\Object; + +/** + * Assignment represents an assignment of a role to a user. + * It includes additional assignment information such as [[bizRule]] and [[data]]. + * Do not create a Assignment instance using the 'new' operator. + * Instead, call [[Manager::assign()]]. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Alexander Kochetov <creocoder@gmail.com> + * @since 2.0 + */ +class Assignment extends Object +{ + /** + * @var Manager the auth manager of this item + */ + public $manager; + /** + * @var string the business rule associated with this assignment + */ + public $bizRule; + /** + * @var mixed additional data for this assignment + */ + public $data; + /** + * @var mixed user ID (see [[User::id]]). Do not modify this property after it is populated. + * To modify the user ID of an assignment, you must remove the assignment and create a new one. + */ + public $userId; + /** + * @return string the authorization item name. Do not modify this property after it is populated. + * To modify the item name of an assignment, you must remove the assignment and create a new one. + */ + public $itemName; + + /** + * Saves the changes to an authorization assignment. + */ + public function save() + { + $this->manager->saveAssignment($this); + } +} diff --git a/yii/rbac/DbManager.php b/framework/yii/rbac/DbManager.php similarity index 83% rename from yii/rbac/DbManager.php rename to framework/yii/rbac/DbManager.php index f3658b2..46c375f 100644 --- a/yii/rbac/DbManager.php +++ b/framework/yii/rbac/DbManager.php @@ -24,7 +24,7 @@ use yii\base\InvalidParamException; * the three tables used to store the authorization data by setting [[itemTable]], * [[itemChildTable]] and [[assignmentTable]]. * - * @property array $authItems The authorization items of the specific type. + * @property Item[] $items The authorization items of the specific type. This property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @author Alexander Kochetov <creocoder@gmail.com> @@ -106,13 +106,13 @@ class DbManager extends Manager if (!isset($params['userId'])) { $params['userId'] = $userId; } - if ($this->executeBizRule($item->getBizRule(), $params, $item->getData())) { + if ($this->executeBizRule($item->bizRule, $params, $item->data)) { if (in_array($itemName, $this->defaultRoles)) { return true; } if (isset($assignments[$itemName])) { $assignment = $assignments[$itemName]; - if ($this->executeBizRule($assignment->getBizRule(), $params, $assignment->getData())) { + if ($this->executeBizRule($assignment->bizRule, $params, $assignment->data)) { return true; } } @@ -146,10 +146,7 @@ class DbManager extends Manager } $query = new Query; $rows = $query->from($this->itemTable) - ->where(array('or', 'name=:name1', 'name=:name2'), array( - ':name1' => $itemName, - ':name2' => $childName - )) + ->where(array('or', 'name=:name1', 'name=:name2'), array(':name1' => $itemName, ':name2' => $childName)) ->createCommand($this->db) ->queryAll(); if (count($rows) == 2) { @@ -165,10 +162,8 @@ class DbManager extends Manager throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); } $this->db->createCommand() - ->insert($this->itemChildTable, array( - 'parent' => $itemName, - 'child' => $childName, - )); + ->insert($this->itemChildTable, array('parent' => $itemName, 'child' => $childName)) + ->execute(); return true; } else { throw new Exception("Either '$itemName' or '$childName' does not exist."); @@ -185,10 +180,8 @@ class DbManager extends Manager public function removeItemChild($itemName, $childName) { return $this->db->createCommand() - ->delete($this->itemChildTable, array( - 'parent' => $itemName, - 'child' => $childName - )) > 0; + ->delete($this->itemChildTable, array('parent' => $itemName, 'child' => $childName)) + ->execute() > 0; } /** @@ -202,10 +195,7 @@ class DbManager extends Manager $query = new Query; return $query->select(array('parent')) ->from($this->itemChildTable) - ->where(array( - 'parent' => $itemName, - 'child' => $childName - )) + ->where(array('parent' => $itemName, 'child' => $childName)) ->createCommand($this->db) ->queryScalar() !== false; } @@ -219,11 +209,8 @@ class DbManager extends Manager public function getItemChildren($names) { $query = new Query; - $rows = $query->select(array('name', 'type', 'description', 'bizrule', 'data')) - ->from(array( - $this->itemTable, - $this->itemChildTable - )) + $rows = $query->select(array('name', 'type', 'description', 'biz_rule', 'data')) + ->from(array($this->itemTable, $this->itemChildTable)) ->where(array('parent' => $names, 'name' => new Expression('child'))) ->createCommand($this->db) ->queryAll(); @@ -232,7 +219,14 @@ class DbManager extends Manager if (($data = @unserialize($row['data'])) === false) { $data = null; } - $children[$row['name']] = new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + $children[$row['name']] = new Item(array( + 'manager' => $this, + 'name' => $row['name'], + 'type' => $row['type'], + 'description' => $row['description'], + 'bizRule' => $row['biz_rule'], + 'data' => $data, + )); } return $children; } @@ -256,10 +250,17 @@ class DbManager extends Manager ->insert($this->assignmentTable, array( 'user_id' => $userId, 'item_name' => $itemName, - 'bizrule' => $bizRule, - 'data' => serialize($data) - )); - return new Assignment($this, $userId, $itemName, $bizRule, $data); + 'biz_rule' => $bizRule, + 'data' => serialize($data), + )) + ->execute(); + return new Assignment(array( + 'manager' => $this, + 'userId' => $userId, + 'itemName' => $itemName, + 'bizRule' => $bizRule, + 'data' => $data, + )); } /** @@ -271,10 +272,8 @@ class DbManager extends Manager public function revoke($userId, $itemName) { return $this->db->createCommand() - ->delete($this->assignmentTable, array( - 'user_id' => $userId, - 'item_name' => $itemName - )) > 0; + ->delete($this->assignmentTable, array('user_id' => $userId, 'item_name' => $itemName)) + ->execute() > 0; } /** @@ -283,15 +282,12 @@ class DbManager extends Manager * @param string $itemName the item name * @return boolean whether the item has been assigned to the user. */ - public function isAssigned($itemName, $userId) + public function isAssigned($userId, $itemName) { $query = new Query; return $query->select(array('item_name')) ->from($this->assignmentTable) - ->where(array( - 'user_id' => $userId, - 'item_name' => $itemName - )) + ->where(array('user_id' => $userId, 'item_name' => $itemName)) ->createCommand($this->db) ->queryScalar() !== false; } @@ -307,17 +303,20 @@ class DbManager extends Manager { $query = new Query; $row = $query->from($this->assignmentTable) - ->where(array( - 'user_id' => $userId, - 'item_name' => $itemName - )) + ->where(array('user_id' => $userId, 'item_name' => $itemName)) ->createCommand($this->db) - ->queryRow(); + ->queryOne(); if ($row !== false) { if (($data = @unserialize($row['data'])) === false) { $data = null; } - return new Assignment($this, $row['user_id'], $row['item_name'], $row['bizrule'], $data); + return new Assignment(array( + 'manager' => $this, + 'userId' => $row['user_id'], + 'itemName' => $row['item_name'], + 'bizRule' => $row['biz_rule'], + 'data' => $data, + )); } else { return null; } @@ -341,7 +340,13 @@ class DbManager extends Manager if (($data = @unserialize($row['data'])) === false) { $data = null; } - $assignments[$row['item_name']] = new Assignment($this, $row['user_id'], $row['item_name'], $row['bizrule'], $data); + $assignments[$row['item_name']] = new Assignment(array( + 'manager' => $this, + 'userId' => $row['user_id'], + 'itemName' => $row['item_name'], + 'bizRule' => $row['biz_rule'], + 'data' => $data, + )); } return $assignments; } @@ -354,12 +359,13 @@ class DbManager extends Manager { $this->db->createCommand() ->update($this->assignmentTable, array( - 'bizrule' => $assignment->getBizRule(), - 'data' => serialize($assignment->getData()), + 'biz_rule' => $assignment->bizRule, + 'data' => serialize($assignment->data), ), array( - 'user_id' => $assignment->getUserId(), - 'item_name' => $assignment->getItemName() - )); + 'user_id' => $assignment->userId, + 'item_name' => $assignment->itemName, + )) + ->execute(); } /** @@ -381,24 +387,14 @@ class DbManager extends Manager ->where(array('type' => $type)) ->createCommand($this->db); } elseif ($type === null) { - $command = $query->select(array('name', 'type', 'description', 't1.bizrule', 't1.data')) - ->from(array( - $this->itemTable . ' t1', - $this->assignmentTable . ' t2' - )) + $command = $query->select(array('name', 'type', 'description', 't1.biz_rule', 't1.data')) + ->from(array($this->itemTable . ' t1', $this->assignmentTable . ' t2')) ->where(array('user_id' => $userId, 'name' => new Expression('item_name'))) ->createCommand($this->db); } else { - $command = $query->select('name', 'type', 'description', 't1.bizrule', 't1.data') - ->from(array( - $this->itemTable . ' t1', - $this->assignmentTable . ' t2' - )) - ->where(array( - 'user_id' => $userId, - 'type' => $type, - 'name' => new Expression('item_name'), - )) + $command = $query->select('name', 'type', 'description', 't1.biz_rule', 't1.data') + ->from(array($this->itemTable . ' t1', $this->assignmentTable . ' t2')) + ->where(array('user_id' => $userId, 'type' => $type, 'name' => new Expression('item_name'))) ->createCommand($this->db); } $items = array(); @@ -406,7 +402,14 @@ class DbManager extends Manager if (($data = @unserialize($row['data'])) === false) { $data = null; } - $items[$row['name']] = new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + $items[$row['name']] = new Item(array( + 'manager' => $this, + 'name' => $row['name'], + 'type' => $row['type'], + 'description' => $row['description'], + 'bizRule' => $row['biz_rule'], + 'data' => $data, + )); } return $items; } @@ -433,10 +436,18 @@ class DbManager extends Manager 'name' => $name, 'type' => $type, 'description' => $description, - 'bizrule' => $bizRule, - 'data' => serialize($data) - )); - return new Item($this, $name, $type, $description, $bizRule, $data); + 'biz_rule' => $bizRule, + 'data' => serialize($data), + )) + ->execute(); + return new Item(array( + 'manager' => $this, + 'name' => $name, + 'type' => $type, + 'description' => $description, + 'bizRule' => $bizRule, + 'data' => $data, + )); } /** @@ -448,13 +459,15 @@ class DbManager extends Manager { if ($this->usingSqlite()) { $this->db->createCommand() - ->delete($this->itemChildTable, array('or', 'parent=:name1', 'child=:name2'), array( - ':name1' => $name, - ':name2' => $name - )); - $this->db->createCommand()->delete($this->assignmentTable, array('item_name' => $name)); + ->delete($this->itemChildTable, array('or', 'parent=:name', 'child=:name'), array(':name' => $name)) + ->execute(); + $this->db->createCommand() + ->delete($this->assignmentTable, array('item_name' => $name)) + ->execute(); } - return $this->db->createCommand()->delete($this->itemTable, array('name' => $name)) > 0; + return $this->db->createCommand() + ->delete($this->itemTable, array('name' => $name)) + ->execute() > 0; } /** @@ -468,15 +481,23 @@ class DbManager extends Manager $row = $query->from($this->itemTable) ->where(array('name' => $name)) ->createCommand($this->db) - ->queryRow(); + ->queryOne(); if ($row !== false) { if (($data = @unserialize($row['data'])) === false) { $data = null; } - return new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); - } else + return new Item(array( + 'manager' => $this, + 'name' => $row['name'], + 'type' => $row['type'], + 'description' => $row['description'], + 'bizRule' => $row['biz_rule'], + 'data' => $data, + )); + } else { return null; + } } /** @@ -488,35 +509,27 @@ class DbManager extends Manager { if ($this->usingSqlite() && $oldName !== null && $item->getName() !== $oldName) { $this->db->createCommand() - ->update($this->itemChildTable, array( - 'parent' => $item->getName(), - ), array( - 'parent' => $oldName, - )); + ->update($this->itemChildTable, array('parent' => $item->getName()), array('parent' => $oldName)) + ->execute(); $this->db->createCommand() - ->update($this->itemChildTable, array( - 'child' => $item->getName(), - ), array( - 'child' => $oldName, - )); + ->update($this->itemChildTable, array('child' => $item->getName()), array('child' => $oldName)) + ->execute(); $this->db->createCommand() - ->update($this->assignmentTable, array( - 'item_name' => $item->getName(), - ), array( - 'item_name' => $oldName, - )); + ->update($this->assignmentTable, array('item_name' => $item->getName()), array('item_name' => $oldName)) + ->execute(); } $this->db->createCommand() ->update($this->itemTable, array( 'name' => $item->getName(), - 'type' => $item->getType(), - 'description' => $item->getDescription(), - 'bizrule' => $item->getBizRule(), - 'data' => serialize($item->getData()), + 'type' => $item->type, + 'description' => $item->description, + 'biz_rule' => $item->bizRule, + 'data' => serialize($item->data), ), array( 'name' => $oldName === null ? $item->getName() : $oldName, - )); + )) + ->execute(); } /** @@ -532,8 +545,8 @@ class DbManager extends Manager public function clearAll() { $this->clearAssignments(); - $this->db->createCommand()->delete($this->itemChildTable); - $this->db->createCommand()->delete($this->itemTable); + $this->db->createCommand()->delete($this->itemChildTable)->execute(); + $this->db->createCommand()->delete($this->itemTable)->execute(); } /** @@ -541,7 +554,7 @@ class DbManager extends Manager */ public function clearAssignments() { - $this->db->createCommand()->delete($this->assignmentTable); + $this->db->createCommand()->delete($this->assignmentTable)->execute(); } /** diff --git a/yii/rbac/Item.php b/framework/yii/rbac/Item.php similarity index 59% rename from yii/rbac/Item.php rename to framework/yii/rbac/Item.php index ef56a53..f7930c1 100644 --- a/yii/rbac/Item.php +++ b/framework/yii/rbac/Item.php @@ -18,13 +18,8 @@ use yii\base\Object; * A user may be assigned one or several authorization items (called [[Assignment]] assignments). * He can perform an operation only when it is among his assigned items. * - * @property Manager $authManager The authorization manager. - * @property integer $type The authorization item type. This could be 0 (operation), 1 (task) or 2 (role). + * @property Item[] $children All child items of this item. This property is read-only. * @property string $name The item name. - * @property string $description The item description. - * @property string $bizRule The business rule associated with this item. - * @property mixed $data The additional data associated with this item. - * @property array $children All child items of this item. * * @author Qiang Xue <qiang.xue@gmail.com> * @author Alexander Kochetov <creocoder@gmail.com> @@ -36,31 +31,30 @@ class Item extends Object const TYPE_TASK = 1; const TYPE_ROLE = 2; - private $_auth; - private $_type; - private $_name; - private $_description; - private $_bizRule; - private $_data; - /** - * Constructor. - * @param Manager $auth authorization manager - * @param string $name authorization item name - * @param integer $type authorization item type. This can be 0 (operation), 1 (task) or 2 (role). - * @param string $description the description - * @param string $bizRule the business rule associated with this item - * @param mixed $data additional data for this item + * @var Manager the auth manager of this item */ - public function __construct($auth, $name, $type, $description = '', $bizRule = null, $data = null) - { - $this->_type = (int)$type; - $this->_auth = $auth; - $this->_name = $name; - $this->_description = $description; - $this->_bizRule = $bizRule; - $this->_data = $data; - } + public $manager; + /** + * @var string the item description + */ + public $description; + /** + * @var string the business rule associated with this item + */ + public $bizRule; + /** + * @var mixed the additional data associated with this item + */ + public $data; + /** + * @var integer the authorization item type. This could be 0 (operation), 1 (task) or 2 (role). + */ + public $type; + + private $_name; + private $_oldName; + /** * Checks to see if the specified item is within the hierarchy starting from this item. @@ -73,11 +67,11 @@ class Item extends Object public function checkAccess($itemName, $params = array()) { Yii::trace('Checking permission: ' . $this->_name, __METHOD__); - if ($this->_auth->executeBizRule($this->_bizRule, $params, $this->_data)) { + if ($this->manager->executeBizRule($this->bizRule, $params, $this->data)) { if ($this->_name == $itemName) { return true; } - foreach ($this->_auth->getItemChildren($this->_name) as $item) { + foreach ($this->manager->getItemChildren($this->_name) as $item) { if ($item->checkAccess($itemName, $params)) { return true; } @@ -87,22 +81,6 @@ class Item extends Object } /** - * @return Manager the authorization manager - */ - public function getManager() - { - return $this->_auth; - } - - /** - * @return integer the authorization item type. This could be 0 (operation), 1 (task) or 2 (role). - */ - public function getType() - { - return $this->_type; - } - - /** * @return string the item name */ public function getName() @@ -116,66 +94,8 @@ class Item extends Object public function setName($value) { if ($this->_name !== $value) { - $oldName = $this->_name; + $this->_oldName = $this->_name; $this->_name = $value; - $this->_auth->saveItem($this, $oldName); - } - } - - /** - * @return string the item description - */ - public function getDescription() - { - return $this->_description; - } - - /** - * @param string $value the item description - */ - public function setDescription($value) - { - if ($this->_description !== $value) { - $this->_description = $value; - $this->_auth->saveItem($this); - } - } - - /** - * @return string the business rule associated with this item - */ - public function getBizRule() - { - return $this->_bizRule; - } - - /** - * @param string $value the business rule associated with this item - */ - public function setBizRule($value) - { - if ($this->_bizRule !== $value) { - $this->_bizRule = $value; - $this->_auth->saveItem($this); - } - } - - /** - * @return mixed the additional data associated with this item - */ - public function getData() - { - return $this->_data; - } - - /** - * @param mixed $value the additional data associated with this item - */ - public function setData($value) - { - if ($this->_data !== $value) { - $this->_data = $value; - $this->_auth->saveItem($this); } } @@ -188,7 +108,7 @@ class Item extends Object */ public function addChild($name) { - return $this->_auth->addItemChild($this->_name, $name); + return $this->manager->addItemChild($this->_name, $name); } /** @@ -200,7 +120,7 @@ class Item extends Object */ public function removeChild($name) { - return $this->_auth->removeItemChild($this->_name, $name); + return $this->manager->removeItemChild($this->_name, $name); } /** @@ -211,7 +131,7 @@ class Item extends Object */ public function hasChild($name) { - return $this->_auth->hasItemChild($this->_name, $name); + return $this->manager->hasItemChild($this->_name, $name); } /** @@ -221,7 +141,7 @@ class Item extends Object */ public function getChildren() { - return $this->_auth->getItemChildren($this->_name); + return $this->manager->getItemChildren($this->_name); } /** @@ -236,7 +156,7 @@ class Item extends Object */ public function assign($userId, $bizRule = null, $data = null) { - return $this->_auth->assign($userId, $this->_name, $bizRule, $data); + return $this->manager->assign($userId, $this->_name, $bizRule, $data); } /** @@ -247,7 +167,7 @@ class Item extends Object */ public function revoke($userId) { - return $this->_auth->revoke($userId, $this->_name); + return $this->manager->revoke($userId, $this->_name); } /** @@ -258,7 +178,7 @@ class Item extends Object */ public function isAssigned($userId) { - return $this->_auth->isAssigned($userId, $this->_name); + return $this->manager->isAssigned($userId, $this->_name); } /** @@ -270,6 +190,15 @@ class Item extends Object */ public function getAssignment($userId) { - return $this->_auth->getAssignment($userId, $this->_name); + return $this->manager->getAssignment($userId, $this->_name); + } + + /** + * Saves an authorization item to persistent storage. + */ + public function save() + { + $this->manager->saveItem($this, $this->_oldName); + unset($this->_oldName); } } diff --git a/yii/rbac/Manager.php b/framework/yii/rbac/Manager.php similarity index 91% rename from yii/rbac/Manager.php rename to framework/yii/rbac/Manager.php index 5c3c4f6..3d3aafc 100644 --- a/yii/rbac/Manager.php +++ b/framework/yii/rbac/Manager.php @@ -33,9 +33,9 @@ use yii\base\InvalidParamException; * at appropriate places in the application code to check if the current user * has the needed permission for an operation. * - * @property array $roles Roles (name=>Item). - * @property array $tasks Tasks (name=>Item). - * @property array $operations Operations (name=>Item). + * @property Item[] $operations Operations (name => AuthItem). This property is read-only. + * @property Item[] $roles Roles (name => AuthItem). This property is read-only. + * @property Item[] $tasks Tasks (name => AuthItem). This property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @author Alexander Kochetov <creocoder@gmail.com> @@ -107,7 +107,7 @@ abstract class Manager extends Component * This is a shortcut method to [[Manager::getItems()]]. * @param mixed $userId the user ID. If not null, only the roles directly assigned to the user * will be returned. Otherwise, all roles will be returned. - * @return Item[] roles (name=>AuthItem) + * @return Item[] roles (name => AuthItem) */ public function getRoles($userId = null) { @@ -119,7 +119,7 @@ abstract class Manager extends Component * This is a shortcut method to [[Manager::getItems()]]. * @param mixed $userId the user ID. If not null, only the tasks directly assigned to the user * will be returned. Otherwise, all tasks will be returned. - * @return Item[] tasks (name=>AuthItem) + * @return Item[] tasks (name => AuthItem) */ public function getTasks($userId = null) { @@ -131,7 +131,7 @@ abstract class Manager extends Component * This is a shortcut method to [[Manager::getItems()]]. * @param mixed $userId the user ID. If not null, only the operations directly assigned to the user * will be returned. Otherwise, all operations will be returned. - * @return Item[] operations (name=>AuthItem) + * @return Item[] operations (name => AuthItem) */ public function getOperations($userId = null) { @@ -161,7 +161,7 @@ abstract class Manager extends Component { static $types = array('operation', 'task', 'role'); if ($parentType < $childType) { - throw new InvalidParamException("Cannot add an item of type '$types[$childType]' to an item of type '$types[$parentType]'."); + throw new InvalidParamException("Cannot add an item of type '{$types[$childType]}' to an item of type '{$types[$parentType]}'."); } } diff --git a/yii/rbac/PhpManager.php b/framework/yii/rbac/PhpManager.php similarity index 90% rename from yii/rbac/PhpManager.php rename to framework/yii/rbac/PhpManager.php index 8a4dbec..203b17e 100644 --- a/yii/rbac/PhpManager.php +++ b/framework/yii/rbac/PhpManager.php @@ -23,7 +23,7 @@ use yii\base\InvalidParamException; * (for example, the authorization data for a personal blog system). * Use [[DbManager]] for more complex authorization data. * - * @property array $authItems The authorization items of the specific type. + * @property Item[] $items The authorization items of the specific type. This property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @author Alexander Kochetov <creocoder@gmail.com> @@ -80,14 +80,14 @@ class PhpManager extends Manager if (!isset($params['userId'])) { $params['userId'] = $userId; } - if ($this->executeBizRule($item->getBizRule(), $params, $item->getData())) { + if ($this->executeBizRule($item->bizRule, $params, $item->data)) { if (in_array($itemName, $this->defaultRoles)) { return true; } if (isset($this->_assignments[$userId][$itemName])) { /** @var $assignment Assignment */ $assignment = $this->_assignments[$userId][$itemName]; - if ($this->executeBizRule($assignment->getBizRule(), $params, $assignment->getData())) { + if ($this->executeBizRule($assignment->bizRule, $params, $assignment->data)) { return true; } } @@ -117,7 +117,7 @@ class PhpManager extends Manager $child = $this->_items[$childName]; /** @var $item Item */ $item = $this->_items[$itemName]; - $this->checkItemChildType($item->getType(), $child->getType()); + $this->checkItemChildType($item->type, $child->type); if ($this->detectLoop($itemName, $childName)) { throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); } @@ -194,7 +194,13 @@ class PhpManager extends Manager } elseif (isset($this->_assignments[$userId][$itemName])) { throw new InvalidParamException("Authorization item '$itemName' has already been assigned to user '$userId'."); } else { - return $this->_assignments[$userId][$itemName] = new Assignment($this, $userId, $itemName, $bizRule, $data); + return $this->_assignments[$userId][$itemName] = new Assignment(array( + 'manager' => $this, + 'userId' => $userId, + 'itemName' => $itemName, + 'bizRule' => $bizRule, + 'data' => $data, + )); } } @@ -265,15 +271,15 @@ class PhpManager extends Manager if ($userId === null) { foreach ($this->_items as $name => $item) { /** @var $item Item */ - if ($item->getType() == $type) { + if ($item->type == $type) { $items[$name] = $item; } } } elseif (isset($this->_assignments[$userId])) { foreach ($this->_assignments[$userId] as $assignment) { /** @var $assignment Assignment */ - $name = $assignment->getItemName(); - if (isset($this->_items[$name]) && ($type === null || $this->_items[$name]->getType() == $type)) { + $name = $assignment->itemName; + if (isset($this->_items[$name]) && ($type === null || $this->_items[$name]->type == $type)) { $items[$name] = $this->_items[$name]; } } @@ -301,7 +307,14 @@ class PhpManager extends Manager if (isset($this->_items[$name])) { throw new Exception('Unable to add an item whose name is the same as an existing item.'); } - return $this->_items[$name] = new Item($this, $name, $type, $description, $bizRule, $data); + return $this->_items[$name] = new Item(array( + 'manager' => $this, + 'name' => $name, + 'type' => $type, + 'description' => $description, + 'bizRule' => $bizRule, + 'data' => $data, + )); } /** @@ -343,8 +356,7 @@ class PhpManager extends Manager */ public function saveItem($item, $oldName = null) { - if ($oldName !== null && ($newName = $item->getName()) !== $oldName) // name changed - { + if ($oldName !== null && ($newName = $item->getName()) !== $oldName) { // name changed if (isset($this->_items[$newName])) { throw new InvalidParamException("Unable to change the item name. The name '$newName' is already used by another item."); } @@ -390,10 +402,10 @@ class PhpManager extends Manager foreach ($this->_items as $name => $item) { /** @var $item Item */ $items[$name] = array( - 'type' => $item->getType(), - 'description' => $item->getDescription(), - 'bizRule' => $item->getBizRule(), - 'data' => $item->getData(), + 'type' => $item->type, + 'description' => $item->description, + 'bizRule' => $item->bizRule, + 'data' => $item->data, ); if (isset($this->_children[$name])) { foreach ($this->_children[$name] as $child) { @@ -408,8 +420,8 @@ class PhpManager extends Manager /** @var $assignment Assignment */ if (isset($items[$name])) { $items[$name]['assignments'][$userId] = array( - 'bizRule' => $assignment->getBizRule(), - 'data' => $assignment->getData(), + 'bizRule' => $assignment->bizRule, + 'data' => $assignment->data, ); } } @@ -428,7 +440,14 @@ class PhpManager extends Manager $items = $this->loadFromFile($this->authFile); foreach ($items as $name => $item) { - $this->_items[$name] = new Item($this, $name, $item['type'], $item['description'], $item['bizRule'], $item['data']); + $this->_items[$name] = new Item(array( + 'manager' => $this, + 'name' => $name, + 'type' => $item['type'], + 'description' => $item['description'], + 'bizRule' => $item['bizRule'], + 'data' => $item['data'], + )); } foreach ($items as $name => $item) { @@ -441,7 +460,13 @@ class PhpManager extends Manager } if (isset($item['assignments'])) { foreach ($item['assignments'] as $userId => $assignment) { - $this->_assignments[$userId][$name] = new Assignment($this, $name, $userId, $assignment['bizRule'], $assignment['data']); + $this->_assignments[$userId][$name] = new Assignment(array( + 'manager' => $this, + 'userId' => $userId, + 'itemName' => $name, + 'bizRule' => $assignment['bizRule'], + 'data' => $assignment['data'], + )); } } } diff --git a/yii/rbac/schema-mssql.sql b/framework/yii/rbac/schema-mssql.sql similarity index 91% rename from yii/rbac/schema-mssql.sql rename to framework/yii/rbac/schema-mssql.sql index a1170b1..923417a 100644 --- a/yii/rbac/schema-mssql.sql +++ b/framework/yii/rbac/schema-mssql.sql @@ -18,9 +18,10 @@ create table [tbl_auth_item] [name] varchar(64) not null, [type] integer not null, [description] text, - [bizrule] text, + [biz_rule] text, [data] text, - primary key ([name]) + primary key ([name]), + key [type] ([type]) ); create table [tbl_auth_item_child] @@ -36,7 +37,7 @@ create table [tbl_auth_assignment] ( [item_name] varchar(64) not null, [user_id] varchar(64) not null, - [bizrule] text, + [biz_rule] text, [data] text, primary key ([item_name],[user_id]), foreign key ([item_name]) references [tbl_auth_item] ([name]) on delete cascade on update cascade diff --git a/yii/rbac/schema-mysql.sql b/framework/yii/rbac/schema-mysql.sql similarity index 91% rename from yii/rbac/schema-mysql.sql rename to framework/yii/rbac/schema-mysql.sql index aa8015b..1530409 100644 --- a/yii/rbac/schema-mysql.sql +++ b/framework/yii/rbac/schema-mysql.sql @@ -18,9 +18,10 @@ create table `tbl_auth_item` `name` varchar(64) not null, `type` integer not null, `description` text, - `bizrule` text, + `biz_rule` text, `data` text, - primary key (`name`) + primary key (`name`), + key `type` (`type`) ) engine InnoDB; create table `tbl_auth_item_child` @@ -36,7 +37,7 @@ create table `tbl_auth_assignment` ( `item_name` varchar(64) not null, `user_id` varchar(64) not null, - `bizrule` text, + `biz_rule` text, `data` text, primary key (`item_name`,`user_id`), foreign key (`item_name`) references `tbl_auth_item` (`name`) on delete cascade on update cascade diff --git a/yii/rbac/schema-oci.sql b/framework/yii/rbac/schema-oci.sql similarity index 91% rename from yii/rbac/schema-oci.sql rename to framework/yii/rbac/schema-oci.sql index 16b7964..78dbb3d 100644 --- a/yii/rbac/schema-oci.sql +++ b/framework/yii/rbac/schema-oci.sql @@ -18,9 +18,10 @@ create table "tbl_auth_item" "name" varchar(64) not null, "type" integer not null, "description" text, - "bizrule" text, + "biz_rule" text, "data" text, - primary key ("name") + primary key ("name"), + key "type" ("type") ); create table "tbl_auth_item_child" @@ -36,7 +37,7 @@ create table "tbl_auth_assignment" ( "item_name" varchar(64) not null, "user_id" varchar(64) not null, - "bizrule" text, + "biz_rule" text, "data" text, primary key ("item_name","user_id"), foreign key ("item_name") references "tbl_auth_item" ("name") on delete cascade on update cascade diff --git a/yii/rbac/schema-pgsql.sql b/framework/yii/rbac/schema-pgsql.sql similarity index 91% rename from yii/rbac/schema-pgsql.sql rename to framework/yii/rbac/schema-pgsql.sql index 16b7964..78dbb3d 100644 --- a/yii/rbac/schema-pgsql.sql +++ b/framework/yii/rbac/schema-pgsql.sql @@ -18,9 +18,10 @@ create table "tbl_auth_item" "name" varchar(64) not null, "type" integer not null, "description" text, - "bizrule" text, + "biz_rule" text, "data" text, - primary key ("name") + primary key ("name"), + key "type" ("type") ); create table "tbl_auth_item_child" @@ -36,7 +37,7 @@ create table "tbl_auth_assignment" ( "item_name" varchar(64) not null, "user_id" varchar(64) not null, - "bizrule" text, + "biz_rule" text, "data" text, primary key ("item_name","user_id"), foreign key ("item_name") references "tbl_auth_item" ("name") on delete cascade on update cascade diff --git a/yii/rbac/schema-sqlite.sql b/framework/yii/rbac/schema-sqlite.sql similarity index 91% rename from yii/rbac/schema-sqlite.sql rename to framework/yii/rbac/schema-sqlite.sql index 668196e..2b79bbf 100644 --- a/yii/rbac/schema-sqlite.sql +++ b/framework/yii/rbac/schema-sqlite.sql @@ -18,9 +18,10 @@ create table 'tbl_auth_item' "name" varchar(64) not null, "type" integer not null, "description" text, - "bizrule" text, + "biz_rule" text, "data" text, - primary key ("name") + primary key ("name"), + key "type" ("type") ); create table 'tbl_auth_item_child' @@ -36,7 +37,7 @@ create table 'tbl_auth_assignment' ( "item_name" varchar(64) not null, "user_id" varchar(64) not null, - "bizrule" text, + "biz_rule" text, "data" text, primary key ("item_name","user_id"), foreign key ("item_name") references 'tbl_auth_item' ("name") on delete cascade on update cascade diff --git a/framework/yii/redis/Connection.php b/framework/yii/redis/Connection.php new file mode 100644 index 0000000..848b408 --- /dev/null +++ b/framework/yii/redis/Connection.php @@ -0,0 +1,428 @@ +<?php +/** + * Connection class file + * + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\redis; + +use yii\base\Component; +use yii\base\InvalidConfigException; +use yii\db\Exception; +use yii\helpers\Inflector; + +/** + * + * @method mixed set($key, $value) Set the string value of a key + * @method mixed get($key) Set the string value of a key + * TODO document methods + * + * @property string $driverName Name of the DB driver. This property is read-only. + * @property boolean $isActive Whether the DB connection is established. This property is read-only. + * @property Transaction $transaction The currently active transaction. Null if no active transaction. This + * property is read-only. + * + * @author Carsten Brandt <mail@cebe.cc> + * @since 2.0 + */ +class Connection extends Component +{ + /** + * @event Event an event that is triggered after a DB connection is established + */ + const EVENT_AFTER_OPEN = 'afterOpen'; + + /** + * @var string the Data Source Name, or DSN, contains the information required to connect to the database. + * DSN format: redis://server:port[/db] + * Where db is a zero based integer which refers to the DB to use. + * If no DB is given, ID 0 is used. + * + * Example: redis://localhost:6379/2 + */ + public $dsn; + /** + * @var string the password for establishing DB connection. Defaults to null meaning no AUTH command is send. + * See http://redis.io/commands/auth + */ + public $password; + /** + * @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout") + */ + public $connectionTimeout = null; + /** + * @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used. + */ + public $dataTimeout = null; + + /** + * @var array List of available redis commands http://redis.io/commands + */ + public $redisCommands = array( + 'BRPOP', // key [key ...] timeout Remove and get the last element in a list, or block until one is available + 'BRPOPLPUSH', // source destination timeout Pop a value from a list, push it to another list and return it; or block until one is available + 'CLIENT KILL', // ip:port Kill the connection of a client + 'CLIENT LIST', // Get the list of client connections + 'CLIENT GETNAME', // Get the current connection name + 'CLIENT SETNAME', // connection-name Set the current connection name + 'CONFIG GET', // parameter Get the value of a configuration parameter + 'CONFIG SET', // parameter value Set a configuration parameter to the given value + 'CONFIG RESETSTAT', // Reset the stats returned by INFO + 'DBSIZE', // Return the number of keys in the selected database + 'DEBUG OBJECT', // key Get debugging information about a key + 'DEBUG SEGFAULT', // Make the server crash + 'DECR', // key Decrement the integer value of a key by one + 'DECRBY', // key decrement Decrement the integer value of a key by the given number + 'DEL', // key [key ...] Delete a key + 'DISCARD', // Discard all commands issued after MULTI + 'DUMP', // key Return a serialized version of the value stored at the specified key. + 'ECHO', // message Echo the given string + 'EVAL', // script numkeys key [key ...] arg [arg ...] Execute a Lua script server side + 'EVALSHA', // sha1 numkeys key [key ...] arg [arg ...] Execute a Lua script server side + 'EXEC', // Execute all commands issued after MULTI + 'EXISTS', // key Determine if a key exists + 'EXPIRE', // key seconds Set a key's time to live in seconds + 'EXPIREAT', // key timestamp Set the expiration for a key as a UNIX timestamp + 'FLUSHALL', // Remove all keys from all databases + 'FLUSHDB', // Remove all keys from the current database + 'GET', // key Get the value of a key + 'GETBIT', // key offset Returns the bit value at offset in the string value stored at key + 'GETRANGE', // key start end Get a substring of the string stored at a key + 'GETSET', // key value Set the string value of a key and return its old value + 'HDEL', // key field [field ...] Delete one or more hash fields + 'HEXISTS', // key field Determine if a hash field exists + 'HGET', // key field Get the value of a hash field + 'HGETALL', // key Get all the fields and values in a hash + 'HINCRBY', // key field increment Increment the integer value of a hash field by the given number + 'HINCRBYFLOAT', // key field increment Increment the float value of a hash field by the given amount + 'HKEYS', // key Get all the fields in a hash + 'HLEN', // key Get the number of fields in a hash + 'HMGET', // key field [field ...] Get the values of all the given hash fields + 'HMSET', // key field value [field value ...] Set multiple hash fields to multiple values + 'HSET', // key field value Set the string value of a hash field + 'HSETNX', // key field value Set the value of a hash field, only if the field does not exist + 'HVALS', // key Get all the values in a hash + 'INCR', // key Increment the integer value of a key by one + 'INCRBY', // key increment Increment the integer value of a key by the given amount + 'INCRBYFLOAT', // key increment Increment the float value of a key by the given amount + 'INFO', // [section] Get information and statistics about the server + 'KEYS', // pattern Find all keys matching the given pattern + 'LASTSAVE', // Get the UNIX time stamp of the last successful save to disk + 'LINDEX', // key index Get an element from a list by its index + 'LINSERT', // key BEFORE|AFTER pivot value Insert an element before or after another element in a list + 'LLEN', // key Get the length of a list + 'LPOP', // key Remove and get the first element in a list + 'LPUSH', // key value [value ...] Prepend one or multiple values to a list + 'LPUSHX', // key value Prepend a value to a list, only if the list exists + 'LRANGE', // key start stop Get a range of elements from a list + 'LREM', // key count value Remove elements from a list + 'LSET', // key index value Set the value of an element in a list by its index + 'LTRIM', // key start stop Trim a list to the specified range + 'MGET', // key [key ...] Get the values of all the given keys + 'MIGRATE', // host port key destination-db timeout Atomically transfer a key from a Redis instance to another one. + 'MONITOR', // Listen for all requests received by the server in real time + 'MOVE', // key db Move a key to another database + 'MSET', // key value [key value ...] Set multiple keys to multiple values + 'MSETNX', // key value [key value ...] Set multiple keys to multiple values, only if none of the keys exist + 'MULTI', // Mark the start of a transaction block + 'OBJECT', // subcommand [arguments [arguments ...]] Inspect the internals of Redis objects + 'PERSIST', // key Remove the expiration from a key + 'PEXPIRE', // key milliseconds Set a key's time to live in milliseconds + 'PEXPIREAT', // key milliseconds-timestamp Set the expiration for a key as a UNIX timestamp specified in milliseconds + 'PING', // Ping the server + 'PSETEX', // key milliseconds value Set the value and expiration in milliseconds of a key + 'PSUBSCRIBE', // pattern [pattern ...] Listen for messages published to channels matching the given patterns + 'PTTL', // key Get the time to live for a key in milliseconds + 'PUBLISH', // channel message Post a message to a channel + 'PUNSUBSCRIBE', // [pattern [pattern ...]] Stop listening for messages posted to channels matching the given patterns + 'QUIT', // Close the connection + 'RANDOMKEY', // Return a random key from the keyspace + 'RENAME', // key newkey Rename a key + 'RENAMENX', // key newkey Rename a key, only if the new key does not exist + 'RESTORE', // key ttl serialized-value Create a key using the provided serialized value, previously obtained using DUMP. + 'RPOP', // key Remove and get the last element in a list + 'RPOPLPUSH', // source destination Remove the last element in a list, append it to another list and return it + 'RPUSH', // key value [value ...] Append one or multiple values to a list + 'RPUSHX', // key value Append a value to a list, only if the list exists + 'SADD', // key member [member ...] Add one or more members to a set + 'SAVE', // Synchronously save the dataset to disk + 'SCARD', // key Get the number of members in a set + 'SCRIPT EXISTS', // script [script ...] Check existence of scripts in the script cache. + 'SCRIPT FLUSH', // Remove all the scripts from the script cache. + 'SCRIPT KILL', // Kill the script currently in execution. + 'SCRIPT LOAD', // script Load the specified Lua script into the script cache. + 'SDIFF', // key [key ...] Subtract multiple sets + 'SDIFFSTORE', // destination key [key ...] Subtract multiple sets and store the resulting set in a key + 'SELECT', // index Change the selected database for the current connection + 'SET', // key value Set the string value of a key + 'SETBIT', // key offset value Sets or clears the bit at offset in the string value stored at key + 'SETEX', // key seconds value Set the value and expiration of a key + 'SETNX', // key value Set the value of a key, only if the key does not exist + 'SETRANGE', // key offset value Overwrite part of a string at key starting at the specified offset + 'SHUTDOWN', // [NOSAVE] [SAVE] Synchronously save the dataset to disk and then shut down the server + 'SINTER', // key [key ...] Intersect multiple sets + 'SINTERSTORE', // destination key [key ...] Intersect multiple sets and store the resulting set in a key + 'SISMEMBER', // key member Determine if a given value is a member of a set + 'SLAVEOF', // host port Make the server a slave of another instance, or promote it as master + 'SLOWLOG', // subcommand [argument] Manages the Redis slow queries log + 'SMEMBERS', // key Get all the members in a set + 'SMOVE', // source destination member Move a member from one set to another + 'SORT', // key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination] Sort the elements in a list, set or sorted set + 'SPOP', // key Remove and return a random member from a set + 'SRANDMEMBER', // key [count] Get one or multiple random members from a set + 'SREM', // key member [member ...] Remove one or more members from a set + 'STRLEN', // key Get the length of the value stored in a key + 'SUBSCRIBE', // channel [channel ...] Listen for messages published to the given channels + 'SUNION', // key [key ...] Add multiple sets + 'SUNIONSTORE', // destination key [key ...] Add multiple sets and store the resulting set in a key + 'SYNC', // Internal command used for replication + 'TIME', // Return the current server time + 'TTL', // key Get the time to live for a key + 'TYPE', // key Determine the type stored at key + 'UNSUBSCRIBE', // [channel [channel ...]] Stop listening for messages posted to the given channels + 'UNWATCH', // Forget about all watched keys + 'WATCH', // key [key ...] Watch the given keys to determine execution of the MULTI/EXEC block + 'ZADD', // key score member [score member ...] Add one or more members to a sorted set, or update its score if it already exists + 'ZCARD', // key Get the number of members in a sorted set + 'ZCOUNT', // key min max Count the members in a sorted set with scores within the given values + 'ZINCRBY', // key increment member Increment the score of a member in a sorted set + 'ZINTERSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Intersect multiple sorted sets and store the resulting sorted set in a new key + 'ZRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index + 'ZRANGEBYSCORE', // key min max [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score + 'ZRANK', // key member Determine the index of a member in a sorted set + 'ZREM', // key member [member ...] Remove one or more members from a sorted set + 'ZREMRANGEBYRANK', // key start stop Remove all members in a sorted set within the given indexes + 'ZREMRANGEBYSCORE', // key min max Remove all members in a sorted set within the given scores + 'ZREVRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index, with scores ordered from high to low + 'ZREVRANGEBYSCORE', // key max min [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score, with scores ordered from high to low + 'ZREVRANK', // key member Determine the index of a member in a sorted set, with scores ordered from high to low + 'ZSCORE', // key member Get the score associated with the given member in a sorted set + 'ZUNIONSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Add multiple sorted sets and store the resulting sorted set in a new key + ); + /** + * @var Transaction the currently active transaction + */ + private $_transaction; + /** + * @var resource redis socket connection + */ + private $_socket; + + /** + * Closes the connection when this component is being serialized. + * @return array + */ + public function __sleep() + { + $this->close(); + return array_keys(get_object_vars($this)); + } + + /** + * Returns a value indicating whether the DB connection is established. + * @return boolean whether the DB connection is established + */ + public function getIsActive() + { + return $this->_socket !== null; + } + + /** + * Establishes a DB connection. + * It does nothing if a DB connection has already been established. + * @throws Exception if connection fails + */ + public function open() + { + if ($this->_socket === null) { + if (empty($this->dsn)) { + throw new InvalidConfigException('Connection.dsn cannot be empty.'); + } + $dsn = explode('/', $this->dsn); + $host = $dsn[2]; + if (strpos($host, ':')===false) { + $host .= ':6379'; + } + $db = isset($dsn[3]) ? $dsn[3] : 0; + + \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); + $this->_socket = @stream_socket_client( + $host, + $errorNumber, + $errorDescription, + $this->connectionTimeout ? $this->connectionTimeout : ini_get("default_socket_timeout") + ); + if ($this->_socket) { + if ($this->dataTimeout !== null) { + stream_set_timeout($this->_socket, $timeout=(int)$this->dataTimeout, (int) (($this->dataTimeout - $timeout) * 1000000)); + } + if ($this->password !== null) { + $this->executeCommand('AUTH', array($this->password)); + } + $this->executeCommand('SELECT', array($db)); + $this->initConnection(); + } else { + \Yii::error("Failed to open DB connection ({$this->dsn}): " . $errorNumber . ' - ' . $errorDescription, __CLASS__); + $message = YII_DEBUG ? 'Failed to open DB connection: ' . $errorNumber . ' - ' . $errorDescription : 'Failed to open DB connection.'; + throw new Exception($message, $errorDescription, (int)$errorNumber); + } + } + } + + /** + * Closes the currently active DB connection. + * It does nothing if the connection is already closed. + */ + public function close() + { + if ($this->_socket !== null) { + \Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__); + $this->executeCommand('QUIT'); + stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR); + $this->_socket = null; + $this->_transaction = null; + } + } + + /** + * Initializes the DB connection. + * This method is invoked right after the DB connection is established. + * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. + */ + protected function initConnection() + { + $this->trigger(self::EVENT_AFTER_OPEN); + } + + /** + * Returns the currently active transaction. + * @return Transaction the currently active transaction. Null if no active transaction. + */ + public function getTransaction() + { + return $this->_transaction && $this->_transaction->isActive ? $this->_transaction : null; + } + + /** + * Starts a transaction. + * @return Transaction the transaction initiated + */ + public function beginTransaction() + { + $this->open(); + $this->_transaction = new Transaction(array( + 'db' => $this, + )); + $this->_transaction->begin(); + return $this->_transaction; + } + + /** + * Returns the name of the DB driver for the current [[dsn]]. + * @return string name of the DB driver + */ + public function getDriverName() + { + if (($pos = strpos($this->dsn, ':')) !== false) { + return strtolower(substr($this->dsn, 0, $pos)); + } else { + return 'redis'; + } + } + + /** + * + * @param string $name + * @param array $params + * @return mixed + */ + public function __call($name, $params) + { + $redisCommand = strtoupper(Inflector::camel2words($name, false)); + if (in_array($redisCommand, $this->redisCommands)) { + return $this->executeCommand($name, $params); + } else { + return parent::__call($name, $params); + } + } + + /** + * Executes a redis command. + * For a list of available commands and their parameters see http://redis.io/commands. + * + * @param string $name the name of the command + * @param array $params list of parameters for the command + * @return array|bool|null|string Dependend on the executed command this method + * will return different data types: + * + * - `true` for commands that return "status reply". + * - `string` for commands that return "integer reply" + * as the value is in the range of a signed 64 bit integer. + * - `string` or `null` for commands that return "bulk reply". + * - `array` for commands that return "Multi-bulk replies". + * + * See [redis protocol description](http://redis.io/topics/protocol) + * for details on the mentioned reply types. + * @trows Exception for commands that return [error reply](http://redis.io/topics/protocol#error-reply). + */ + public function executeCommand($name, $params=array()) + { + $this->open(); + + array_unshift($params, $name); + $command = '*' . count($params) . "\r\n"; + foreach($params as $arg) { + $command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n"; + } + + \Yii::trace("Executing Redis Command: {$name}", __CLASS__); + fwrite($this->_socket, $command); + + return $this->parseResponse(implode(' ', $params)); + } + + private function parseResponse($command) + { + if(($line = fgets($this->_socket)) === false) { + throw new Exception("Failed to read from socket.\nRedis command was: " . $command); + } + $type = $line[0]; + $line = mb_substr($line, 1, -2, '8bit'); + switch($type) + { + case '+': // Status reply + return true; + case '-': // Error reply + throw new Exception("Redis error: " . $line . "\nRedis command was: " . $command); + case ':': // Integer reply + // no cast to int as it is in the range of a signed 64 bit integer + return $line; + case '$': // Bulk replies + if ($line == '-1') { + return null; + } + $length = $line + 2; + $data = ''; + while ($length > 0) { + if(($block = fread($this->_socket, $line + 2)) === false) { + throw new Exception("Failed to read from socket.\nRedis command was: " . $command); + } + $data .= $block; + $length -= mb_strlen($block, '8bit'); + } + return mb_substr($data, 0, -2, '8bit'); + case '*': // Multi-bulk replies + $count = (int) $line; + $data = array(); + for($i = 0; $i < $count; $i++) { + $data[] = $this->parseResponse($command); + } + return $data; + default: + throw new Exception('Received illegal data from redis: ' . $line . "\nRedis command was: " . $command); + } + } +} diff --git a/framework/yii/redis/Transaction.php b/framework/yii/redis/Transaction.php new file mode 100644 index 0000000..94cff7a --- /dev/null +++ b/framework/yii/redis/Transaction.php @@ -0,0 +1,93 @@ +<?php +/** + * Transaction class file. + * + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\redis; + +use yii\base\InvalidConfigException; +use yii\db\Exception; + +/** + * Transaction represents a DB transaction. + * + * @property boolean $isActive Whether this transaction is active. Only an active transaction can [[commit()]] + * or [[rollBack()]]. This property is read-only. + * + * @author Carsten Brandt <mail@cebe.cc> + * @since 2.0 + */ +class Transaction extends \yii\base\Object +{ + /** + * @var Connection the database connection that this transaction is associated with. + */ + public $db; + /** + * @var boolean whether this transaction is active. Only an active transaction + * can [[commit()]] or [[rollBack()]]. This property is set true when the transaction is started. + */ + private $_active = false; + + /** + * Returns a value indicating whether this transaction is active. + * @return boolean whether this transaction is active. Only an active transaction + * can [[commit()]] or [[rollBack()]]. + */ + public function getIsActive() + { + return $this->_active; + } + + /** + * Begins a transaction. + * @throws InvalidConfigException if [[connection]] is null + */ + public function begin() + { + if (!$this->_active) { + if ($this->db === null) { + throw new InvalidConfigException('Transaction::db must be set.'); + } + \Yii::trace('Starting transaction', __CLASS__); + $this->db->open(); + $this->db->createCommand('MULTI')->execute(); + $this->_active = true; + } + } + + /** + * Commits a transaction. + * @throws Exception if the transaction or the DB connection is not active. + */ + public function commit() + { + if ($this->_active && $this->db && $this->db->isActive) { + \Yii::trace('Committing transaction', __CLASS__); + $this->db->createCommand('EXEC')->execute(); + // TODO handle result of EXEC + $this->_active = false; + } else { + throw new Exception('Failed to commit transaction: transaction was inactive.'); + } + } + + /** + * Rolls back a transaction. + * @throws Exception if the transaction or the DB connection is not active. + */ + public function rollback() + { + if ($this->_active && $this->db && $this->db->isActive) { + \Yii::trace('Rolling back transaction', __CLASS__); + $this->db->pdo->commit(); + $this->_active = false; + } else { + throw new Exception('Failed to roll back transaction: transaction was inactive.'); + } + } +} diff --git a/framework/yii/requirements/YiiRequirementChecker.php b/framework/yii/requirements/YiiRequirementChecker.php new file mode 100644 index 0000000..c8ad45d --- /dev/null +++ b/framework/yii/requirements/YiiRequirementChecker.php @@ -0,0 +1,395 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +if (version_compare(PHP_VERSION, '4.3', '<')) { + echo 'At least PHP 4.3 is required to run this script!'; + exit(1); +} + +/** + * YiiRequirementChecker allows checking, if current system meets the requirements for running the Yii application. + * This class allows rendering of the check report for the web and console application interface. + * + * Example: + * + * ~~~php + * require_once('path/to/YiiRequirementChecker.php'); + * $requirementsChecker = new YiiRequirementChecker(); + * $requirements = array( + * array( + * 'name' => 'PHP Some Extension', + * 'mandatory' => true, + * 'condition' => extension_loaded('some_extension'), + * 'by' => 'Some application feature', + * 'memo' => 'PHP extension "some_extension" required', + * ), + * ); + * $requirementsChecker->checkYii()->check($requirements)->render(); + * ~~~ + * + * If you wish to render the report with your own representation, use [[getResult()]] instead of [[render()]] + * + * Requirement condition could be in format "eval:PHP expression". + * In this case specified PHP expression will be evaluated in the context of this class instance. + * For example: + * + * ~~~ + * $requirements = array( + * array( + * 'name' => 'Upload max file size', + * 'condition' => 'eval:$this->checkUploadMaxFileSize("5M")', + * ), + * ); + * ~~~ + * + * Note: this class definition does not match ordinary Yii style, because it should match PHP 4.3 + * and should not use features from newer PHP versions! + * + * @property array|null $result the check results, this property is for internal usage only. + * + * @author Paul Klimov <klimov.paul@gmail.com> + * @since 2.0 + */ +class YiiRequirementChecker +{ + /** + * Check the given requirements, collecting results into internal field. + * This method can be invoked several times checking different requirement sets. + * Use [[getResult()]] or [[render()]] to get the results. + * @param array|string $requirements requirements to be checked. + * If an array, it is treated as the set of requirements; + * If a string, it is treated as the path of the file, which contains the requirements; + * @return static self instance. + */ + function check($requirements) + { + if (is_string($requirements)) { + $requirements = require($requirements); + } + if (!is_array($requirements)) { + $this->usageError('Requirements must be an array, "' . gettype($requirements) . '" has been given!'); + } + if (!isset($this->result) || !is_array($this->result)) { + $this->result = array( + 'summary' => array( + 'total' => 0, + 'errors' => 0, + 'warnings' => 0, + ), + 'requirements' => array(), + ); + } + foreach ($requirements as $key => $rawRequirement) { + $requirement = $this->normalizeRequirement($rawRequirement, $key); + $this->result['summary']['total']++; + if (!$requirement['condition']) { + if ($requirement['mandatory']) { + $requirement['error'] = true; + $requirement['warning'] = true; + $this->result['summary']['errors']++; + } else { + $requirement['error'] = false; + $requirement['warning'] = true; + $this->result['summary']['warnings']++; + } + } else { + $requirement['error'] = false; + $requirement['warning'] = false; + } + $this->result['requirements'][] = $requirement; + } + return $this; + } + + /** + * Performs the check for the Yii core requirements. + * @return YiiRequirementChecker self instance. + */ + function checkYii() + { + return $this->check(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'requirements.php'); + } + + /** + * Return the check results. + * @return array|null check results in format: + * <code> + * array( + * 'summary' => array( + * 'total' => total number of checks, + * 'errors' => number of errors, + * 'warnings' => number of warnings, + * ), + * 'requirements' => array( + * array( + * ... + * 'error' => is there an error, + * 'warning' => is there a warning, + * ), + * ... + * ), + * ) + * </code> + */ + function getResult() + { + if (isset($this->result)) { + return $this->result; + } else { + return null; + } + } + + /** + * Renders the requirements check result. + * The output will vary depending is a script running from web or from console. + */ + function render() + { + if (!isset($this->result)) { + $this->usageError('Nothing to render!'); + } + $baseViewFilePath = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'views'; + if (!empty($_SERVER['argv'])) { + $viewFileName = $baseViewFilePath . DIRECTORY_SEPARATOR . 'console' . DIRECTORY_SEPARATOR . 'index.php'; + } else { + $viewFileName = $baseViewFilePath . DIRECTORY_SEPARATOR . 'web' . DIRECTORY_SEPARATOR . 'index.php'; + } + $this->renderViewFile($viewFileName, $this->result); + } + + /** + * Checks if the given PHP extension is available and its version matches the given one. + * @param string $extensionName PHP extension name. + * @param string $version required PHP extension version. + * @param string $compare comparison operator, by default '>=' + * @return boolean if PHP extension version matches. + */ + function checkPhpExtensionVersion($extensionName, $version, $compare = '>=') + { + if (!extension_loaded($extensionName)) { + return false; + } + $extensionVersion = phpversion($extensionName); + if (empty($extensionVersion)) { + return false; + } + return version_compare($extensionVersion, $version, $compare); + } + + /** + * Checks if PHP configuration option (from php.ini) is on. + * @param string $name configuration option name. + * @return boolean option is on. + */ + function checkPhpIniOn($name) + { + $value = ini_get($name); + if (empty($value)) { + return false; + } + return ((integer)$value == 1 || strtolower($value) == 'on'); + } + + /** + * Checks if PHP configuration option (from php.ini) is off. + * @param string $name configuration option name. + * @return boolean option is off. + */ + function checkPhpIniOff($name) + { + $value = ini_get($name); + if (empty($value)) { + return true; + } + return (strtolower($value) == 'off'); + } + + /** + * Compare byte sizes of values given in the verbose representation, + * like '5M', '15K' etc. + * @param string $a first value. + * @param string $b second value. + * @param string $compare comparison operator, by default '>='. + * @return boolean comparison result. + */ + function compareByteSize($a, $b, $compare = '>=') + { + $compareExpression = '(' . $this->getByteSize($a) . $compare . $this->getByteSize($b) . ')'; + return $this->evaluateExpression($compareExpression); + } + + /** + * Gets the size in bytes from verbose size representation. + * For example: '5K' => 5*1024 + * @param string $verboseSize verbose size representation. + * @return integer actual size in bytes. + */ + function getByteSize($verboseSize) + { + if (empty($verboseSize)) { + return 0; + } + if (is_numeric($verboseSize)) { + return (integer)$verboseSize; + } + $sizeUnit = trim($verboseSize, '0123456789'); + $size = str_replace($sizeUnit, '', $verboseSize); + $size = trim($size); + if (!is_numeric($size)) { + return 0; + } + switch (strtolower($sizeUnit)) { + case 'kb': + case 'k': { + return $size * 1024; + } + case 'mb': + case 'm': { + return $size * 1024 * 1024; + } + case 'gb': + case 'g': { + return $size * 1024 * 1024 * 1024; + } + default: { + return 0; + } + } + } + + /** + * Checks if upload max file size matches the given range. + * @param string|null $min verbose file size minimum required value, pass null to skip minimum check. + * @param string|null $max verbose file size maximum required value, pass null to skip maximum check. + * @return boolean success. + */ + function checkUploadMaxFileSize($min = null, $max = null) + { + $postMaxSize = ini_get('post_max_size'); + $uploadMaxFileSize = ini_get('upload_max_filesize'); + if ($min !== null) { + $minCheckResult = $this->compareByteSize($postMaxSize, $min, '>=') && $this->compareByteSize($uploadMaxFileSize, $min, '>='); + } else { + $minCheckResult = true; + } + if ($max !== null) { + var_dump($postMaxSize, $uploadMaxFileSize, $max); + $maxCheckResult = $this->compareByteSize($postMaxSize, $max, '<=') && $this->compareByteSize($uploadMaxFileSize, $max, '<='); + } else { + $maxCheckResult = true; + } + return ($minCheckResult && $maxCheckResult); + } + + /** + * Renders a view file. + * This method includes the view file as a PHP script + * and captures the display result if required. + * @param string $_viewFile_ view file + * @param array $_data_ data to be extracted and made available to the view file + * @param boolean $_return_ whether the rendering result should be returned as a string + * @return string the rendering result. Null if the rendering result is not required. + */ + function renderViewFile($_viewFile_, $_data_ = null, $_return_ = false) + { + // we use special variable names here to avoid conflict when extracting data + if (is_array($_data_)) { + extract($_data_, EXTR_PREFIX_SAME, 'data'); + } else { + $data = $_data_; + } + if ($_return_) { + ob_start(); + ob_implicit_flush(false); + require($_viewFile_); + return ob_get_clean(); + } else { + require($_viewFile_); + } + } + + /** + * Normalizes requirement ensuring it has correct format. + * @param array $requirement raw requirement. + * @param int $requirementKey requirement key in the list. + * @return array normalized requirement. + */ + function normalizeRequirement($requirement, $requirementKey = 0) + { + if (!is_array($requirement)) { + $this->usageError('Requirement must be an array!'); + } + if (!array_key_exists('condition', $requirement)) { + $this->usageError("Requirement '{$requirementKey}' has no condition!"); + } else { + $evalPrefix = 'eval:'; + if (is_string($requirement['condition']) && strpos($requirement['condition'], $evalPrefix) === 0) { + $expression = substr($requirement['condition'], strlen($evalPrefix)); + $requirement['condition'] = $this->evaluateExpression($expression); + } + } + if (!array_key_exists('name', $requirement)) { + $requirement['name'] = is_numeric($requirementKey) ? 'Requirement #' . $requirementKey : $requirementKey; + } + if (!array_key_exists('mandatory', $requirement)) { + if (array_key_exists('required', $requirement)) { + $requirement['mandatory'] = $requirement['required']; + } else { + $requirement['mandatory'] = false; + } + } + if (!array_key_exists('by', $requirement)) { + $requirement['by'] = 'Unknown'; + } + if (!array_key_exists('memo', $requirement)) { + $requirement['memo'] = ''; + } + return $requirement; + } + + /** + * Displays a usage error. + * This method will then terminate the execution of the current application. + * @param string $message the error message + */ + function usageError($message) + { + echo "Error: $message\n\n"; + exit(1); + } + + /** + * Evaluates a PHP expression under the context of this class. + * @param string $expression a PHP expression to be evaluated. + * @return mixed the expression result. + */ + function evaluateExpression($expression) + { + return eval('return ' . $expression . ';'); + } + + /** + * Returns the server information. + * @return string server information. + */ + function getServerInfo() + { + $info = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : ''; + return $info; + } + + /** + * Returns the now date if possible in string representation. + * @return string now date. + */ + function getNowDate() + { + $nowDate = @strftime('%Y-%m-%d %H:%M', time()); + return $nowDate; + } +} diff --git a/framework/yii/requirements/requirements.php b/framework/yii/requirements/requirements.php new file mode 100644 index 0000000..571aa57 --- /dev/null +++ b/framework/yii/requirements/requirements.php @@ -0,0 +1,50 @@ +<?php +/** + * These are the Yii core requirements for the [[YiiRequirementChecker]] instance. + * These requirements are mandatory for any Yii application. + * + * @var $this YiiRequirementChecker + */ +return array( + array( + 'name' => 'PHP version', + 'mandatory' => true, + 'condition' => version_compare(PHP_VERSION, '5.3.7', '>='), + 'by' => '<a href="http://www.yiiframework.com">Yii Framework</a>', + 'memo' => 'PHP 5.3.7 or higher is required.', + ), + array( + 'name' => 'Reflection extension', + 'mandatory' => true, + 'condition' => class_exists('Reflection', false), + 'by' => '<a href="http://www.yiiframework.com">Yii Framework</a>', + ), + array( + 'name' => 'PCRE extension', + 'mandatory' => true, + 'condition' => extension_loaded('pcre'), + 'by' => '<a href="http://www.yiiframework.com">Yii Framework</a>', + ), + array( + 'name' => 'SPL extension', + 'mandatory' => true, + 'condition' => extension_loaded('SPL'), + 'by' => '<a href="http://www.yiiframework.com">Yii Framework</a>', + ), + array( + 'name' => 'MBString extension', + 'mandatory' => true, + 'condition' => extension_loaded('mbstring'), + 'by' => '<a href="http://www.php.net/manual/en/book.mbstring.php">Multibyte string</a> processing', + 'memo' => 'Required for multibyte encoding string processing.' + ), + array( + 'name' => 'Intl extension', + 'mandatory' => false, + 'condition' => $this->checkPhpExtensionVersion('intl', '1.0.2', '>='), + 'by' => '<a href="http://www.php.net/manual/en/book.intl.php">Internationalization</a> support', + 'memo' => 'PHP Intl extension 1.0.2 or higher is required when you want to use advanced parameters formatting + in <code>\Yii::t()</code>, <abbr title="Internationalized domain names">IDN</abbr>-feature of + <code>EmailValidator</code> or <code>UrlValidator</code> or the <code>yii\i18n\Formatter</code> class.' + ), +); diff --git a/framework/yii/requirements/views/console/index.php b/framework/yii/requirements/views/console/index.php new file mode 100644 index 0000000..6935107 --- /dev/null +++ b/framework/yii/requirements/views/console/index.php @@ -0,0 +1,36 @@ +<?php +/* @var $this YiiRequirementChecker */ +/* @var $summary array */ +/* @var $requirements array[] */ + +echo "\nYii Application Requirement Checker\n\n"; + +echo "This script checks if your server configuration meets the requirements\n"; +echo "for running Yii application.\n"; +echo "It checks if the server is running the right version of PHP,\n"; +echo "if appropriate PHP extensions have been loaded, and if php.ini file settings are correct.\n"; + +$header = 'Check conclusion:'; +echo "\n{$header}\n"; +echo str_pad('', strlen($header), '-')."\n\n"; + +foreach ($requirements as $key => $requirement) { + if ($requirement['condition']) { + echo $requirement['name'].": OK\n"; + echo "\n"; + } else { + echo $requirement['name'].': '.($requirement['mandatory'] ? 'FAILED!!!' : 'WARNING!!!')."\n"; + echo 'Required by: '.strip_tags($requirement['by'])."\n"; + $memo = strip_tags($requirement['memo']); + if (!empty($memo)) { + echo 'Memo: '.strip_tags($requirement['memo'])."\n"; + } + echo "\n"; + } +} + +$summaryString = 'Errors: '.$summary['errors'].' Warnings: '.$summary['warnings'].' Total checks: '.$summary['total']; +echo str_pad('', strlen($summaryString), '-')."\n"; +echo $summaryString; + +echo "\n\n"; diff --git a/framework/yii/requirements/views/web/css.php b/framework/yii/requirements/views/web/css.php new file mode 100644 index 0000000..2946a4d --- /dev/null +++ b/framework/yii/requirements/views/web/css.php @@ -0,0 +1,6807 @@ +<style type="text/css"> +/*! + * Bootstrap v3.0.0 + * + * Copyright 2013 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world by @mdo and @fat. + */ + +/*! normalize.css v2.1.0 | MIT License | git.io/normalize */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; +} + +audio, +canvas, +video { + display: inline-block; +} + +audio:not([controls]) { + display: none; + height: 0; +} + +[hidden] { + display: none; +} + +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + margin: 0; +} + +a:focus { + outline: thin dotted; +} + +a:active, +a:hover { + outline: 0; +} + +h1 { + margin: 0.67em 0; + font-size: 2em; +} + +abbr[title] { + border-bottom: 1px dotted; +} + +b, +strong { + font-weight: bold; +} + +dfn { + font-style: italic; +} + +hr { + height: 0; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +mark { + color: #000; + background: #ff0; +} + +code, +kbd, +pre, +samp { + font-family: monospace, serif; + font-size: 1em; +} + +pre { + white-space: pre-wrap; +} + +q { + quotes: "\201C" "\201D" "\2018" "\2019"; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + border: 0; +} + +svg:not(:root) { + overflow: hidden; +} + +figure { + margin: 0; +} + +fieldset { + padding: 0.35em 0.625em 0.75em; + margin: 0 2px; + border: 1px solid #c0c0c0; +} + +legend { + padding: 0; + border: 0; +} + +button, +input, +select, +textarea { + margin: 0; + font-family: inherit; + font-size: 100%; +} + +button, +input { + line-height: normal; +} + +button, +select { + text-transform: none; +} + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +button[disabled], +html input[disabled] { + cursor: default; +} + +input[type="checkbox"], +input[type="radio"] { + padding: 0; + box-sizing: border-box; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +@media print { + * { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + @page { + margin: 2cm .5cm; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + .navbar { + display: none; + } + .table td, + .table th { + background-color: #fff !important; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} + +*, +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +html { + font-size: 62.5%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.428571429; + color: #333333; + background-color: #ffffff; +} + +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input, +select[multiple], +textarea { + background-image: none; +} + +a { + color: #428bca; + text-decoration: none; +} + +a:hover, +a:focus { + color: #2a6496; + text-decoration: underline; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +img { + vertical-align: middle; +} + +.img-responsive { + display: block; + height: auto; + max-width: 100%; +} + +.img-rounded { + border-radius: 6px; +} + +.img-thumbnail { + display: inline-block; + height: auto; + max-width: 100%; + padding: 4px; + line-height: 1.428571429; + background-color: #ffffff; + border: 1px solid #dddddd; + border-radius: 4px; + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +.img-circle { + border-radius: 50%; +} + +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eeeeee; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + border: 0; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 16.099999999999998px; + font-weight: 200; + line-height: 1.4; +} + +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} + +small { + font-size: 85%; +} + +cite { + font-style: normal; +} + +.text-muted { + color: #999999; +} + +.text-primary { + color: #428bca; +} + +.text-warning { + color: #c09853; +} + +.text-danger { + color: #b94a48; +} + +.text-success { + color: #468847; +} + +.text-info { + color: #3a87ad; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 500; + line-height: 1.1; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small { + font-weight: normal; + line-height: 1; + color: #999999; +} + +h1, +h2, +h3 { + margin-top: 20px; + margin-bottom: 10px; +} + +h4, +h5, +h6 { + margin-top: 10px; + margin-bottom: 10px; +} + +h1, +.h1 { + font-size: 36px; +} + +h2, +.h2 { + font-size: 30px; +} + +h3, +.h3 { + font-size: 24px; +} + +h4, +.h4 { + font-size: 18px; +} + +h5, +.h5 { + font-size: 14px; +} + +h6, +.h6 { + font-size: 12px; +} + +h1 small, +.h1 small { + font-size: 24px; +} + +h2 small, +.h2 small { + font-size: 18px; +} + +h3 small, +.h3 small, +h4 small, +.h4 small { + font-size: 14px; +} + +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} + +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} + +dl { + margin-bottom: 20px; +} + +dt, +dd { + line-height: 1.428571429; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 0; +} + +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } + .dl-horizontal dd:before, + .dl-horizontal dd:after { + display: table; + content: " "; + } + .dl-horizontal dd:after { + clear: both; + } + .dl-horizontal dd:before, + .dl-horizontal dd:after { + display: table; + content: " "; + } + .dl-horizontal dd:after { + clear: both; + } +} + +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #999999; +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + font-size: 17.5px; + font-weight: 300; + line-height: 1.25; +} + +blockquote p:last-child { + margin-bottom: 0; +} + +blockquote small { + display: block; + line-height: 1.428571429; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +blockquote.pull-right small:before { + content: ''; +} + +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 20px; + font-style: normal; + line-height: 1.428571429; +} + +code, +pre { + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; +} + +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + white-space: nowrap; + background-color: #f9f2f4; + border-radius: 4px; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.428571429; + color: #333333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 20px; +} + +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +.container:before, +.container:after { + display: table; + content: " "; +} + +.container:after { + clear: both; +} + +.container:before, +.container:after { + display: table; + content: " "; +} + +.container:after { + clear: both; +} + +.row { + margin-right: -15px; + margin-left: -15px; +} + +.row:before, +.row:after { + display: table; + content: " "; +} + +.row:after { + clear: both; +} + +.row:before, +.row:after { + display: table; + content: " "; +} + +.row:after { + clear: both; +} + +.col-xs-1, +.col-xs-2, +.col-xs-3, +.col-xs-4, +.col-xs-5, +.col-xs-6, +.col-xs-7, +.col-xs-8, +.col-xs-9, +.col-xs-10, +.col-xs-11, +.col-xs-12, +.col-sm-1, +.col-sm-2, +.col-sm-3, +.col-sm-4, +.col-sm-5, +.col-sm-6, +.col-sm-7, +.col-sm-8, +.col-sm-9, +.col-sm-10, +.col-sm-11, +.col-sm-12, +.col-md-1, +.col-md-2, +.col-md-3, +.col-md-4, +.col-md-5, +.col-md-6, +.col-md-7, +.col-md-8, +.col-md-9, +.col-md-10, +.col-md-11, +.col-md-12, +.col-lg-1, +.col-lg-2, +.col-lg-3, +.col-lg-4, +.col-lg-5, +.col-lg-6, +.col-lg-7, +.col-lg-8, +.col-lg-9, +.col-lg-10, +.col-lg-11, +.col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} + +.col-xs-1, +.col-xs-2, +.col-xs-3, +.col-xs-4, +.col-xs-5, +.col-xs-6, +.col-xs-7, +.col-xs-8, +.col-xs-9, +.col-xs-10, +.col-xs-11 { + float: left; +} + +.col-xs-1 { + width: 8.333333333333332%; +} + +.col-xs-2 { + width: 16.666666666666664%; +} + +.col-xs-3 { + width: 25%; +} + +.col-xs-4 { + width: 33.33333333333333%; +} + +.col-xs-5 { + width: 41.66666666666667%; +} + +.col-xs-6 { + width: 50%; +} + +.col-xs-7 { + width: 58.333333333333336%; +} + +.col-xs-8 { + width: 66.66666666666666%; +} + +.col-xs-9 { + width: 75%; +} + +.col-xs-10 { + width: 83.33333333333334%; +} + +.col-xs-11 { + width: 91.66666666666666%; +} + +.col-xs-12 { + width: 100%; +} + +@media (min-width: 768px) { + .container { + max-width: 750px; + } + .col-sm-1, + .col-sm-2, + .col-sm-3, + .col-sm-4, + .col-sm-5, + .col-sm-6, + .col-sm-7, + .col-sm-8, + .col-sm-9, + .col-sm-10, + .col-sm-11 { + float: left; + } + .col-sm-1 { + width: 8.333333333333332%; + } + .col-sm-2 { + width: 16.666666666666664%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-4 { + width: 33.33333333333333%; + } + .col-sm-5 { + width: 41.66666666666667%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-7 { + width: 58.333333333333336%; + } + .col-sm-8 { + width: 66.66666666666666%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-10 { + width: 83.33333333333334%; + } + .col-sm-11 { + width: 91.66666666666666%; + } + .col-sm-12 { + width: 100%; + } + .col-sm-push-1 { + left: 8.333333333333332%; + } + .col-sm-push-2 { + left: 16.666666666666664%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-4 { + left: 33.33333333333333%; + } + .col-sm-push-5 { + left: 41.66666666666667%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-7 { + left: 58.333333333333336%; + } + .col-sm-push-8 { + left: 66.66666666666666%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-10 { + left: 83.33333333333334%; + } + .col-sm-push-11 { + left: 91.66666666666666%; + } + .col-sm-pull-1 { + right: 8.333333333333332%; + } + .col-sm-pull-2 { + right: 16.666666666666664%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-4 { + right: 33.33333333333333%; + } + .col-sm-pull-5 { + right: 41.66666666666667%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-7 { + right: 58.333333333333336%; + } + .col-sm-pull-8 { + right: 66.66666666666666%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-10 { + right: 83.33333333333334%; + } + .col-sm-pull-11 { + right: 91.66666666666666%; + } + .col-sm-offset-1 { + margin-left: 8.333333333333332%; + } + .col-sm-offset-2 { + margin-left: 16.666666666666664%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-4 { + margin-left: 33.33333333333333%; + } + .col-sm-offset-5 { + margin-left: 41.66666666666667%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-7 { + margin-left: 58.333333333333336%; + } + .col-sm-offset-8 { + margin-left: 66.66666666666666%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-10 { + margin-left: 83.33333333333334%; + } + .col-sm-offset-11 { + margin-left: 91.66666666666666%; + } +} + +@media (min-width: 992px) { + .container { + max-width: 970px; + } + .col-md-1, + .col-md-2, + .col-md-3, + .col-md-4, + .col-md-5, + .col-md-6, + .col-md-7, + .col-md-8, + .col-md-9, + .col-md-10, + .col-md-11 { + float: left; + } + .col-md-1 { + width: 8.333333333333332%; + } + .col-md-2 { + width: 16.666666666666664%; + } + .col-md-3 { + width: 25%; + } + .col-md-4 { + width: 33.33333333333333%; + } + .col-md-5 { + width: 41.66666666666667%; + } + .col-md-6 { + width: 50%; + } + .col-md-7 { + width: 58.333333333333336%; + } + .col-md-8 { + width: 66.66666666666666%; + } + .col-md-9 { + width: 75%; + } + .col-md-10 { + width: 83.33333333333334%; + } + .col-md-11 { + width: 91.66666666666666%; + } + .col-md-12 { + width: 100%; + } + .col-md-push-0 { + left: auto; + } + .col-md-push-1 { + left: 8.333333333333332%; + } + .col-md-push-2 { + left: 16.666666666666664%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-4 { + left: 33.33333333333333%; + } + .col-md-push-5 { + left: 41.66666666666667%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-7 { + left: 58.333333333333336%; + } + .col-md-push-8 { + left: 66.66666666666666%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-10 { + left: 83.33333333333334%; + } + .col-md-push-11 { + left: 91.66666666666666%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-pull-1 { + right: 8.333333333333332%; + } + .col-md-pull-2 { + right: 16.666666666666664%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-4 { + right: 33.33333333333333%; + } + .col-md-pull-5 { + right: 41.66666666666667%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-7 { + right: 58.333333333333336%; + } + .col-md-pull-8 { + right: 66.66666666666666%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-10 { + right: 83.33333333333334%; + } + .col-md-pull-11 { + right: 91.66666666666666%; + } + .col-md-offset-0 { + margin-left: 0; + } + .col-md-offset-1 { + margin-left: 8.333333333333332%; + } + .col-md-offset-2 { + margin-left: 16.666666666666664%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-4 { + margin-left: 33.33333333333333%; + } + .col-md-offset-5 { + margin-left: 41.66666666666667%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-7 { + margin-left: 58.333333333333336%; + } + .col-md-offset-8 { + margin-left: 66.66666666666666%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-10 { + margin-left: 83.33333333333334%; + } + .col-md-offset-11 { + margin-left: 91.66666666666666%; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1170px; + } + .col-lg-1, + .col-lg-2, + .col-lg-3, + .col-lg-4, + .col-lg-5, + .col-lg-6, + .col-lg-7, + .col-lg-8, + .col-lg-9, + .col-lg-10, + .col-lg-11 { + float: left; + } + .col-lg-1 { + width: 8.333333333333332%; + } + .col-lg-2 { + width: 16.666666666666664%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-4 { + width: 33.33333333333333%; + } + .col-lg-5 { + width: 41.66666666666667%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-7 { + width: 58.333333333333336%; + } + .col-lg-8 { + width: 66.66666666666666%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-10 { + width: 83.33333333333334%; + } + .col-lg-11 { + width: 91.66666666666666%; + } + .col-lg-12 { + width: 100%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-push-1 { + left: 8.333333333333332%; + } + .col-lg-push-2 { + left: 16.666666666666664%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-4 { + left: 33.33333333333333%; + } + .col-lg-push-5 { + left: 41.66666666666667%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-7 { + left: 58.333333333333336%; + } + .col-lg-push-8 { + left: 66.66666666666666%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-10 { + left: 83.33333333333334%; + } + .col-lg-push-11 { + left: 91.66666666666666%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-pull-1 { + right: 8.333333333333332%; + } + .col-lg-pull-2 { + right: 16.666666666666664%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-4 { + right: 33.33333333333333%; + } + .col-lg-pull-5 { + right: 41.66666666666667%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-7 { + right: 58.333333333333336%; + } + .col-lg-pull-8 { + right: 66.66666666666666%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-10 { + right: 83.33333333333334%; + } + .col-lg-pull-11 { + right: 91.66666666666666%; + } + .col-lg-offset-0 { + margin-left: 0; + } + .col-lg-offset-1 { + margin-left: 8.333333333333332%; + } + .col-lg-offset-2 { + margin-left: 16.666666666666664%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-4 { + margin-left: 33.33333333333333%; + } + .col-lg-offset-5 { + margin-left: 41.66666666666667%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-7 { + margin-left: 58.333333333333336%; + } + .col-lg-offset-8 { + margin-left: 66.66666666666666%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-10 { + margin-left: 83.33333333333334%; + } + .col-lg-offset-11 { + margin-left: 91.66666666666666%; + } +} + +table { + max-width: 100%; + background-color: transparent; +} + +th { + text-align: left; +} + +.table { + width: 100%; + margin-bottom: 20px; +} + +.table thead > tr > th, +.table tbody > tr > th, +.table tfoot > tr > th, +.table thead > tr > td, +.table tbody > tr > td, +.table tfoot > tr > td { + padding: 8px; + line-height: 1.428571429; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #dddddd; +} + +.table caption + thead tr:first-child th, +.table colgroup + thead tr:first-child th, +.table thead:first-child tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table .table { + background-color: #ffffff; +} + +.table-condensed thead > tr > th, +.table-condensed tbody > tr > th, +.table-condensed tfoot > tr > th, +.table-condensed thead > tr > td, +.table-condensed tbody > tr > td, +.table-condensed tfoot > tr > td { + padding: 5px; +} + +.table-bordered { + border: 1px solid #dddddd; +} + +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #dddddd; +} + +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} + +.table-striped > tbody > tr:nth-child(odd) > td, +.table-striped > tbody > tr:nth-child(odd) > th { + background-color: #f9f9f9; +} + +.table-hover > tbody > tr:hover > td, +.table-hover > tbody > tr:hover > th { + background-color: #f5f5f5; +} + +table col[class*="col-"] { + display: table-column; + float: none; +} + +table td[class*="col-"], +table th[class*="col-"] { + display: table-cell; + float: none; +} + +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} + +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td { + background-color: #d0e9c6; + border-color: #c9e2b3; +} + +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; + border-color: #eed3d7; +} + +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td { + background-color: #ebcccc; + border-color: #e6c1c7; +} + +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; + border-color: #fbeed5; +} + +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td { + background-color: #faf2cc; + border-color: #f8e5be; +} + +@media (max-width: 768px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-x: scroll; + overflow-y: hidden; + border: 1px solid #dddddd; + } + .table-responsive > .table { + margin-bottom: 0; + background-color: #fff; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > thead > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > thead > tr:last-child > td, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +label { + display: inline-block; + margin-bottom: 5px; + font-weight: bold; +} + +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + /* IE8-9 */ + + line-height: normal; +} + +input[type="file"] { + display: block; +} + +select[multiple], +select[size] { + height: auto; +} + +select optgroup { + font-family: inherit; + font-size: inherit; + font-style: inherit; +} + +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + height: auto; +} + +.form-control:-moz-placeholder { + color: #999999; +} + +.form-control::-moz-placeholder { + color: #999999; +} + +.form-control:-ms-input-placeholder { + color: #999999; +} + +.form-control::-webkit-input-placeholder { + color: #999999; +} + +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.428571429; + color: #555555; + vertical-align: middle; + background-color: #ffffff; + border: 1px solid #cccccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; +} + +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); +} + +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + cursor: not-allowed; + background-color: #eeeeee; +} + +textarea.form-control { + height: auto; +} + +.form-group { + margin-bottom: 15px; +} + +.radio, +.checkbox { + display: block; + min-height: 20px; + padding-left: 20px; + margin-top: 10px; + margin-bottom: 10px; + vertical-align: middle; +} + +.radio label, +.checkbox label { + display: inline; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; +} + +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + float: left; + margin-left: -20px; +} + +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} + +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + vertical-align: middle; + cursor: pointer; +} + +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +.radio[disabled], +.radio-inline[disabled], +.checkbox[disabled], +.checkbox-inline[disabled], +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"], +fieldset[disabled] .radio, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} + +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +select.input-sm { + height: 30px; + line-height: 30px; +} + +textarea.input-sm { + height: auto; +} + +.input-lg { + height: 45px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} + +select.input-lg { + height: 45px; + line-height: 45px; +} + +textarea.input-lg { + height: auto; +} + +.has-warning .help-block, +.has-warning .control-label { + color: #c09853; +} + +.has-warning .form-control { + border-color: #c09853; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.has-warning .form-control:focus { + border-color: #a47e3c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; +} + +.has-warning .input-group-addon { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.has-error .help-block, +.has-error .control-label { + color: #b94a48; +} + +.has-error .form-control { + border-color: #b94a48; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.has-error .form-control:focus { + border-color: #953b39; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; +} + +.has-error .input-group-addon { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.has-success .help-block, +.has-success .control-label { + color: #468847; +} + +.has-success .form-control { + border-color: #468847; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.has-success .form-control:focus { + border-color: #356635; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; +} + +.has-success .input-group-addon { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +.form-control-static { + padding-top: 7px; + margin-bottom: 0; +} + +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} + +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + float: none; + margin-left: 0; + } +} + +.form-horizontal .control-label, +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; +} + +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; +} + +.form-horizontal .form-group:before, +.form-horizontal .form-group:after { + display: table; + content: " "; +} + +.form-horizontal .form-group:after { + clear: both; +} + +.form-horizontal .form-group:before, +.form-horizontal .form-group:after { + display: table; + content: " "; +} + +.form-horizontal .form-group:after { + clear: both; +} + +@media (min-width: 768px) { + .form-horizontal .control-label { + text-align: right; + } +} + +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.428571429; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + border: 1px solid transparent; + border-radius: 4px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn:hover, +.btn:focus { + color: #333333; + text-decoration: none; +} + +.btn:active, +.btn.active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + pointer-events: none; + cursor: not-allowed; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; +} + +.btn-default { + color: #333333; + background-color: #ffffff; + border-color: #cccccc; +} + +.btn-default:hover, +.btn-default:focus, +.btn-default:active, +.btn-default.active, +.open .dropdown-toggle.btn-default { + color: #333333; + background-color: #ebebeb; + border-color: #adadad; +} + +.btn-default:active, +.btn-default.active, +.open .dropdown-toggle.btn-default { + background-image: none; +} + +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #ffffff; + border-color: #cccccc; +} + +.btn-primary { + color: #ffffff; + background-color: #428bca; + border-color: #357ebd; +} + +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active, +.btn-primary.active, +.open .dropdown-toggle.btn-primary { + color: #ffffff; + background-color: #3276b1; + border-color: #285e8e; +} + +.btn-primary:active, +.btn-primary.active, +.open .dropdown-toggle.btn-primary { + background-image: none; +} + +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #428bca; + border-color: #357ebd; +} + +.btn-warning { + color: #ffffff; + background-color: #f0ad4e; + border-color: #eea236; +} + +.btn-warning:hover, +.btn-warning:focus, +.btn-warning:active, +.btn-warning.active, +.open .dropdown-toggle.btn-warning { + color: #ffffff; + background-color: #ed9c28; + border-color: #d58512; +} + +.btn-warning:active, +.btn-warning.active, +.open .dropdown-toggle.btn-warning { + background-image: none; +} + +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #f0ad4e; + border-color: #eea236; +} + +.btn-danger { + color: #ffffff; + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-danger:hover, +.btn-danger:focus, +.btn-danger:active, +.btn-danger.active, +.open .dropdown-toggle.btn-danger { + color: #ffffff; + background-color: #d2322d; + border-color: #ac2925; +} + +.btn-danger:active, +.btn-danger.active, +.open .dropdown-toggle.btn-danger { + background-image: none; +} + +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-success { + color: #ffffff; + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-success:hover, +.btn-success:focus, +.btn-success:active, +.btn-success.active, +.open .dropdown-toggle.btn-success { + color: #ffffff; + background-color: #47a447; + border-color: #398439; +} + +.btn-success:active, +.btn-success.active, +.open .dropdown-toggle.btn-success { + background-image: none; +} + +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-info { + color: #ffffff; + background-color: #5bc0de; + border-color: #46b8da; +} + +.btn-info:hover, +.btn-info:focus, +.btn-info:active, +.btn-info.active, +.open .dropdown-toggle.btn-info { + color: #ffffff; + background-color: #39b3d7; + border-color: #269abc; +} + +.btn-info:active, +.btn-info.active, +.open .dropdown-toggle.btn-info { + background-image: none; +} + +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #5bc0de; + border-color: #46b8da; +} + +.btn-link { + font-weight: normal; + color: #428bca; + cursor: pointer; + border-radius: 0; +} + +.btn-link, +.btn-link:active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} + +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} + +.btn-link:hover, +.btn-link:focus { + color: #2a6496; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #999999; + text-decoration: none; +} + +.btn-lg { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} + +.btn-sm, +.btn-xs { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-xs { + padding: 1px 5px; +} + +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 0; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + display: none; +} + +.collapse.in { + display: block; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +@font-face { + font-family: 'Glyphicons Halflings'; + src: url('../fonts/glyphicons-halflings-regular.eot'); + src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg'); +} + +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + -webkit-font-smoothing: antialiased; + font-style: normal; + font-weight: normal; + line-height: 1; +} + +.glyphicon-asterisk:before { + content: "\2a"; +} + +.glyphicon-plus:before { + content: "\2b"; +} + +.glyphicon-euro:before { + content: "\20ac"; +} + +.glyphicon-minus:before { + content: "\2212"; +} + +.glyphicon-cloud:before { + content: "\2601"; +} + +.glyphicon-envelope:before { + content: "\2709"; +} + +.glyphicon-pencil:before { + content: "\270f"; +} + +.glyphicon-glass:before { + content: "\e001"; +} + +.glyphicon-music:before { + content: "\e002"; +} + +.glyphicon-search:before { + content: "\e003"; +} + +.glyphicon-heart:before { + content: "\e005"; +} + +.glyphicon-star:before { + content: "\e006"; +} + +.glyphicon-star-empty:before { + content: "\e007"; +} + +.glyphicon-user:before { + content: "\e008"; +} + +.glyphicon-film:before { + content: "\e009"; +} + +.glyphicon-th-large:before { + content: "\e010"; +} + +.glyphicon-th:before { + content: "\e011"; +} + +.glyphicon-th-list:before { + content: "\e012"; +} + +.glyphicon-ok:before { + content: "\e013"; +} + +.glyphicon-remove:before { + content: "\e014"; +} + +.glyphicon-zoom-in:before { + content: "\e015"; +} + +.glyphicon-zoom-out:before { + content: "\e016"; +} + +.glyphicon-off:before { + content: "\e017"; +} + +.glyphicon-signal:before { + content: "\e018"; +} + +.glyphicon-cog:before { + content: "\e019"; +} + +.glyphicon-trash:before { + content: "\e020"; +} + +.glyphicon-home:before { + content: "\e021"; +} + +.glyphicon-file:before { + content: "\e022"; +} + +.glyphicon-time:before { + content: "\e023"; +} + +.glyphicon-road:before { + content: "\e024"; +} + +.glyphicon-download-alt:before { + content: "\e025"; +} + +.glyphicon-download:before { + content: "\e026"; +} + +.glyphicon-upload:before { + content: "\e027"; +} + +.glyphicon-inbox:before { + content: "\e028"; +} + +.glyphicon-play-circle:before { + content: "\e029"; +} + +.glyphicon-repeat:before { + content: "\e030"; +} + +.glyphicon-refresh:before { + content: "\e031"; +} + +.glyphicon-list-alt:before { + content: "\e032"; +} + +.glyphicon-flag:before { + content: "\e034"; +} + +.glyphicon-headphones:before { + content: "\e035"; +} + +.glyphicon-volume-off:before { + content: "\e036"; +} + +.glyphicon-volume-down:before { + content: "\e037"; +} + +.glyphicon-volume-up:before { + content: "\e038"; +} + +.glyphicon-qrcode:before { + content: "\e039"; +} + +.glyphicon-barcode:before { + content: "\e040"; +} + +.glyphicon-tag:before { + content: "\e041"; +} + +.glyphicon-tags:before { + content: "\e042"; +} + +.glyphicon-book:before { + content: "\e043"; +} + +.glyphicon-print:before { + content: "\e045"; +} + +.glyphicon-font:before { + content: "\e047"; +} + +.glyphicon-bold:before { + content: "\e048"; +} + +.glyphicon-italic:before { + content: "\e049"; +} + +.glyphicon-text-height:before { + content: "\e050"; +} + +.glyphicon-text-width:before { + content: "\e051"; +} + +.glyphicon-align-left:before { + content: "\e052"; +} + +.glyphicon-align-center:before { + content: "\e053"; +} + +.glyphicon-align-right:before { + content: "\e054"; +} + +.glyphicon-align-justify:before { + content: "\e055"; +} + +.glyphicon-list:before { + content: "\e056"; +} + +.glyphicon-indent-left:before { + content: "\e057"; +} + +.glyphicon-indent-right:before { + content: "\e058"; +} + +.glyphicon-facetime-video:before { + content: "\e059"; +} + +.glyphicon-picture:before { + content: "\e060"; +} + +.glyphicon-map-marker:before { + content: "\e062"; +} + +.glyphicon-adjust:before { + content: "\e063"; +} + +.glyphicon-tint:before { + content: "\e064"; +} + +.glyphicon-edit:before { + content: "\e065"; +} + +.glyphicon-share:before { + content: "\e066"; +} + +.glyphicon-check:before { + content: "\e067"; +} + +.glyphicon-move:before { + content: "\e068"; +} + +.glyphicon-step-backward:before { + content: "\e069"; +} + +.glyphicon-fast-backward:before { + content: "\e070"; +} + +.glyphicon-backward:before { + content: "\e071"; +} + +.glyphicon-play:before { + content: "\e072"; +} + +.glyphicon-pause:before { + content: "\e073"; +} + +.glyphicon-stop:before { + content: "\e074"; +} + +.glyphicon-forward:before { + content: "\e075"; +} + +.glyphicon-fast-forward:before { + content: "\e076"; +} + +.glyphicon-step-forward:before { + content: "\e077"; +} + +.glyphicon-eject:before { + content: "\e078"; +} + +.glyphicon-chevron-left:before { + content: "\e079"; +} + +.glyphicon-chevron-right:before { + content: "\e080"; +} + +.glyphicon-plus-sign:before { + content: "\e081"; +} + +.glyphicon-minus-sign:before { + content: "\e082"; +} + +.glyphicon-remove-sign:before { + content: "\e083"; +} + +.glyphicon-ok-sign:before { + content: "\e084"; +} + +.glyphicon-question-sign:before { + content: "\e085"; +} + +.glyphicon-info-sign:before { + content: "\e086"; +} + +.glyphicon-screenshot:before { + content: "\e087"; +} + +.glyphicon-remove-circle:before { + content: "\e088"; +} + +.glyphicon-ok-circle:before { + content: "\e089"; +} + +.glyphicon-ban-circle:before { + content: "\e090"; +} + +.glyphicon-arrow-left:before { + content: "\e091"; +} + +.glyphicon-arrow-right:before { + content: "\e092"; +} + +.glyphicon-arrow-up:before { + content: "\e093"; +} + +.glyphicon-arrow-down:before { + content: "\e094"; +} + +.glyphicon-share-alt:before { + content: "\e095"; +} + +.glyphicon-resize-full:before { + content: "\e096"; +} + +.glyphicon-resize-small:before { + content: "\e097"; +} + +.glyphicon-exclamation-sign:before { + content: "\e101"; +} + +.glyphicon-gift:before { + content: "\e102"; +} + +.glyphicon-leaf:before { + content: "\e103"; +} + +.glyphicon-eye-open:before { + content: "\e105"; +} + +.glyphicon-eye-close:before { + content: "\e106"; +} + +.glyphicon-warning-sign:before { + content: "\e107"; +} + +.glyphicon-plane:before { + content: "\e108"; +} + +.glyphicon-random:before { + content: "\e110"; +} + +.glyphicon-comment:before { + content: "\e111"; +} + +.glyphicon-magnet:before { + content: "\e112"; +} + +.glyphicon-chevron-up:before { + content: "\e113"; +} + +.glyphicon-chevron-down:before { + content: "\e114"; +} + +.glyphicon-retweet:before { + content: "\e115"; +} + +.glyphicon-shopping-cart:before { + content: "\e116"; +} + +.glyphicon-folder-close:before { + content: "\e117"; +} + +.glyphicon-folder-open:before { + content: "\e118"; +} + +.glyphicon-resize-vertical:before { + content: "\e119"; +} + +.glyphicon-resize-horizontal:before { + content: "\e120"; +} + +.glyphicon-hdd:before { + content: "\e121"; +} + +.glyphicon-bullhorn:before { + content: "\e122"; +} + +.glyphicon-certificate:before { + content: "\e124"; +} + +.glyphicon-thumbs-up:before { + content: "\e125"; +} + +.glyphicon-thumbs-down:before { + content: "\e126"; +} + +.glyphicon-hand-right:before { + content: "\e127"; +} + +.glyphicon-hand-left:before { + content: "\e128"; +} + +.glyphicon-hand-up:before { + content: "\e129"; +} + +.glyphicon-hand-down:before { + content: "\e130"; +} + +.glyphicon-circle-arrow-right:before { + content: "\e131"; +} + +.glyphicon-circle-arrow-left:before { + content: "\e132"; +} + +.glyphicon-circle-arrow-up:before { + content: "\e133"; +} + +.glyphicon-circle-arrow-down:before { + content: "\e134"; +} + +.glyphicon-globe:before { + content: "\e135"; +} + +.glyphicon-tasks:before { + content: "\e137"; +} + +.glyphicon-filter:before { + content: "\e138"; +} + +.glyphicon-fullscreen:before { + content: "\e140"; +} + +.glyphicon-dashboard:before { + content: "\e141"; +} + +.glyphicon-heart-empty:before { + content: "\e143"; +} + +.glyphicon-link:before { + content: "\e144"; +} + +.glyphicon-phone:before { + content: "\e145"; +} + +.glyphicon-usd:before { + content: "\e148"; +} + +.glyphicon-gbp:before { + content: "\e149"; +} + +.glyphicon-sort:before { + content: "\e150"; +} + +.glyphicon-sort-by-alphabet:before { + content: "\e151"; +} + +.glyphicon-sort-by-alphabet-alt:before { + content: "\e152"; +} + +.glyphicon-sort-by-order:before { + content: "\e153"; +} + +.glyphicon-sort-by-order-alt:before { + content: "\e154"; +} + +.glyphicon-sort-by-attributes:before { + content: "\e155"; +} + +.glyphicon-sort-by-attributes-alt:before { + content: "\e156"; +} + +.glyphicon-unchecked:before { + content: "\e157"; +} + +.glyphicon-expand:before { + content: "\e158"; +} + +.glyphicon-collapse-down:before { + content: "\e159"; +} + +.glyphicon-collapse-up:before { + content: "\e160"; +} + +.glyphicon-log-in:before { + content: "\e161"; +} + +.glyphicon-flash:before { + content: "\e162"; +} + +.glyphicon-log-out:before { + content: "\e163"; +} + +.glyphicon-new-window:before { + content: "\e164"; +} + +.glyphicon-record:before { + content: "\e165"; +} + +.glyphicon-save:before { + content: "\e166"; +} + +.glyphicon-open:before { + content: "\e167"; +} + +.glyphicon-saved:before { + content: "\e168"; +} + +.glyphicon-import:before { + content: "\e169"; +} + +.glyphicon-export:before { + content: "\e170"; +} + +.glyphicon-send:before { + content: "\e171"; +} + +.glyphicon-floppy-disk:before { + content: "\e172"; +} + +.glyphicon-floppy-saved:before { + content: "\e173"; +} + +.glyphicon-floppy-remove:before { + content: "\e174"; +} + +.glyphicon-floppy-save:before { + content: "\e175"; +} + +.glyphicon-floppy-open:before { + content: "\e176"; +} + +.glyphicon-credit-card:before { + content: "\e177"; +} + +.glyphicon-transfer:before { + content: "\e178"; +} + +.glyphicon-cutlery:before { + content: "\e179"; +} + +.glyphicon-header:before { + content: "\e180"; +} + +.glyphicon-compressed:before { + content: "\e181"; +} + +.glyphicon-earphone:before { + content: "\e182"; +} + +.glyphicon-phone-alt:before { + content: "\e183"; +} + +.glyphicon-tower:before { + content: "\e184"; +} + +.glyphicon-stats:before { + content: "\e185"; +} + +.glyphicon-sd-video:before { + content: "\e186"; +} + +.glyphicon-hd-video:before { + content: "\e187"; +} + +.glyphicon-subtitles:before { + content: "\e188"; +} + +.glyphicon-sound-stereo:before { + content: "\e189"; +} + +.glyphicon-sound-dolby:before { + content: "\e190"; +} + +.glyphicon-sound-5-1:before { + content: "\e191"; +} + +.glyphicon-sound-6-1:before { + content: "\e192"; +} + +.glyphicon-sound-7-1:before { + content: "\e193"; +} + +.glyphicon-copyright-mark:before { + content: "\e194"; +} + +.glyphicon-registration-mark:before { + content: "\e195"; +} + +.glyphicon-cloud-download:before { + content: "\e197"; +} + +.glyphicon-cloud-upload:before { + content: "\e198"; +} + +.glyphicon-tree-conifer:before { + content: "\e199"; +} + +.glyphicon-tree-deciduous:before { + content: "\e200"; +} + +.glyphicon-briefcase:before { + content: "\1f4bc"; +} + +.glyphicon-calendar:before { + content: "\1f4c5"; +} + +.glyphicon-pushpin:before { + content: "\1f4cc"; +} + +.glyphicon-paperclip:before { + content: "\1f4ce"; +} + +.glyphicon-camera:before { + content: "\1f4f7"; +} + +.glyphicon-lock:before { + content: "\1f512"; +} + +.glyphicon-bell:before { + content: "\1f514"; +} + +.glyphicon-bookmark:before { + content: "\1f516"; +} + +.glyphicon-fire:before { + content: "\1f525"; +} + +.glyphicon-wrench:before { + content: "\1f527"; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-bottom: 0 dotted; + border-left: 4px solid transparent; + content: ""; +} + +.dropdown { + position: relative; +} + +.dropdown-toggle:focus { + outline: 0; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + list-style: none; + background-color: #ffffff; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} + +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.428571429; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: #ffffff; + text-decoration: none; + background-color: #428bca; +} + +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #ffffff; + text-decoration: none; + background-color: #428bca; + outline: 0; +} + +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #999999; +} + +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.open > .dropdown-menu { + display: block; +} + +.open > a { + outline: 0; +} + +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.428571429; + color: #999999; +} + +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0 dotted; + border-bottom: 4px solid #000000; + content: ""; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } +} + +.btn-default .caret { + border-top-color: #333333; +} + +.btn-primary .caret, +.btn-success .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret { + border-top-color: #fff; +} + +.dropup .btn-default .caret { + border-bottom-color: #333333; +} + +.dropup .btn-primary .caret, +.dropup .btn-success .caret, +.dropup .btn-warning .caret, +.dropup .btn-danger .caret, +.dropup .btn-info .caret { + border-bottom-color: #fff; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} + +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus { + outline: none; +} + +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} + +.btn-toolbar:before, +.btn-toolbar:after { + display: table; + content: " "; +} + +.btn-toolbar:after { + clear: both; +} + +.btn-toolbar:before, +.btn-toolbar:after { + display: table; + content: " "; +} + +.btn-toolbar:after { + clear: both; +} + +.btn-toolbar .btn-group { + float: left; +} + +.btn-toolbar > .btn + .btn, +.btn-toolbar > .btn-group + .btn, +.btn-toolbar > .btn + .btn-group, +.btn-toolbar > .btn-group + .btn-group { + margin-left: 5px; +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +.btn-group > .btn:first-child { + margin-left: 0; +} + +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.btn-group > .btn-group { + float: left; +} + +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.btn-group > .btn-group:first-child > .btn:last-child, +.btn-group > .btn-group:first-child > .dropdown-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn-group:last-child > .btn:first-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group-xs > .btn { + padding: 5px 10px; + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} + +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} + +.btn-group > .btn-lg + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +.btn .caret { + margin-left: 0; +} + +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} + +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} + +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group { + display: block; + float: none; + width: 100%; + max-width: 100%; +} + +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after { + display: table; + content: " "; +} + +.btn-group-vertical > .btn-group:after { + clear: both; +} + +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after { + display: table; + content: " "; +} + +.btn-group-vertical > .btn-group:after { + clear: both; +} + +.btn-group-vertical > .btn-group > .btn { + float: none; +} + +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-right-radius: 0; + border-bottom-left-radius: 4px; + border-top-left-radius: 0; +} + +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.btn-group-vertical > .btn-group:first-child > .btn:last-child, +.btn-group-vertical > .btn-group:first-child > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn-group:last-child > .btn:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.btn-group-justified { + display: table; + width: 100%; + border-collapse: separate; + table-layout: fixed; +} + +.btn-group-justified .btn { + display: table-cell; + float: none; + width: 1%; +} + +[data-toggle="buttons"] > .btn > input[type="radio"], +[data-toggle="buttons"] > .btn > input[type="checkbox"] { + display: none; +} + +.input-group { + position: relative; + display: table; + border-collapse: separate; +} + +.input-group.col { + float: none; + padding-right: 0; + padding-left: 0; +} + +.input-group .form-control { + width: 100%; + margin-bottom: 0; +} + +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 45px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} + +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 45px; + line-height: 45px; +} + +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn { + height: auto; +} + +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} + +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn { + height: auto; +} + +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} + +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} + +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + text-align: center; + background-color: #eeeeee; + border: 1px solid #cccccc; + border-radius: 4px; +} + +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} + +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} + +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} + +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group-addon:first-child { + border-right: 0; +} + +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.input-group-addon:last-child { + border-left: 0; +} + +.input-group-btn { + position: relative; + white-space: nowrap; +} + +.input-group-btn > .btn { + position: relative; +} + +.input-group-btn > .btn + .btn { + margin-left: -4px; +} + +.input-group-btn > .btn:hover, +.input-group-btn > .btn:active { + z-index: 2; +} + +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav:before, +.nav:after { + display: table; + content: " "; +} + +.nav:after { + clear: both; +} + +.nav:before, +.nav:after { + display: table; + content: " "; +} + +.nav:after { + clear: both; +} + +.nav > li { + position: relative; + display: block; +} + +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} + +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > li.disabled > a { + color: #999999; +} + +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #999999; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} + +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #eeeeee; + border-color: #428bca; +} + +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} + +.nav > li > a > img { + max-width: none; +} + +.nav-tabs { + border-bottom: 1px solid #dddddd; +} + +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} + +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.428571429; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555555; + cursor: default; + background-color: #ffffff; + border: 1px solid #dddddd; + border-bottom-color: transparent; +} + +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} + +.nav-tabs.nav-justified > li { + float: none; +} + +.nav-tabs.nav-justified > li > a { + text-align: center; +} + +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } +} + +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-bottom: 1px solid #dddddd; +} + +.nav-tabs.nav-justified > .active > a { + border-bottom-color: #ffffff; +} + +.nav-pills > li { + float: left; +} + +.nav-pills > li > a { + border-radius: 5px; +} + +.nav-pills > li + li { + margin-left: 2px; +} + +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #ffffff; + background-color: #428bca; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} + +.nav-justified { + width: 100%; +} + +.nav-justified > li { + float: none; +} + +.nav-justified > li > a { + text-align: center; +} + +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } +} + +.nav-tabs-justified { + border-bottom: 0; +} + +.nav-tabs-justified > li > a { + margin-right: 0; + border-bottom: 1px solid #dddddd; +} + +.nav-tabs-justified > .active > a { + border-bottom-color: #ffffff; +} + +.tabbable:before, +.tabbable:after { + display: table; + content: " "; +} + +.tabbable:after { + clear: both; +} + +.tabbable:before, +.tabbable:after { + display: table; + content: " "; +} + +.tabbable:after { + clear: both; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.nav .caret { + border-top-color: #428bca; + border-bottom-color: #428bca; +} + +.nav a:hover .caret { + border-top-color: #2a6496; + border-bottom-color: #2a6496; +} + +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.navbar { + position: relative; + z-index: 1000; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; +} + +.navbar:before, +.navbar:after { + display: table; + content: " "; +} + +.navbar:after { + clear: both; +} + +.navbar:before, +.navbar:after { + display: table; + content: " "; +} + +.navbar:after { + clear: both; +} + +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} + +.navbar-header:before, +.navbar-header:after { + display: table; + content: " "; +} + +.navbar-header:after { + clear: both; +} + +.navbar-header:before, +.navbar-header:after { + display: table; + content: " "; +} + +.navbar-header:after { + clear: both; +} + +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} + +.navbar-collapse { + max-height: 340px; + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + -webkit-overflow-scrolling: touch; +} + +.navbar-collapse:before, +.navbar-collapse:after { + display: table; + content: " "; +} + +.navbar-collapse:after { + clear: both; +} + +.navbar-collapse:before, +.navbar-collapse:after { + display: table; + content: " "; +} + +.navbar-collapse:after { + clear: both; +} + +.navbar-collapse.in { + overflow-y: auto; +} + +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-collapse .navbar-nav.navbar-left:first-child { + margin-left: -15px; + } + .navbar-collapse .navbar-nav.navbar-right:last-child { + margin-right: -15px; + } + .navbar-collapse .navbar-text:last-child { + margin-right: 0; + } +} + +.container > .navbar-header, +.container > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} + +@media (min-width: 768px) { + .container > .navbar-header, + .container > .navbar-collapse { + margin-right: 0; + margin-left: 0; + } +} + +.navbar-static-top { + border-width: 0 0 1px; +} + +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + border-width: 0 0 1px; +} + +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} + +.navbar-fixed-top { + top: 0; + z-index: 1030; +} + +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; +} + +.navbar-brand { + float: left; + padding: 15px 15px; + font-size: 18px; + line-height: 20px; +} + +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} + +@media (min-width: 768px) { + .navbar > .container .navbar-brand { + margin-left: -15px; + } +} + +.navbar-toggle { + position: relative; + float: right; + padding: 9px 10px; + margin-top: 8px; + margin-right: 15px; + margin-bottom: 8px; + background-color: transparent; + border: 1px solid transparent; + border-radius: 4px; +} + +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} + +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} + +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} + +.navbar-nav { + margin: 7.5px -15px; +} + +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} + +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} + +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + } +} + +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + } +} + +.navbar-form { + padding: 10px 15px; + margin-top: 8px; + margin-right: -15px; + margin-bottom: 8px; + margin-left: -15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); +} + +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + } + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + float: none; + margin-left: 0; + } +} + +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } +} + +@media (min-width: 768px) { + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} + +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.navbar-nav.pull-right > li > .dropdown-menu, +.navbar-nav > li > .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; +} + +.navbar-text { + float: left; + margin-top: 15px; + margin-bottom: 15px; +} + +@media (min-width: 768px) { + .navbar-text { + margin-right: 15px; + margin-left: 15px; + } +} + +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} + +.navbar-default .navbar-brand { + color: #777777; +} + +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #5e5e5e; + background-color: transparent; +} + +.navbar-default .navbar-text { + color: #777777; +} + +.navbar-default .navbar-nav > li > a { + color: #777777; +} + +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #333333; + background-color: transparent; +} + +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555555; + background-color: #e7e7e7; +} + +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #cccccc; + background-color: transparent; +} + +.navbar-default .navbar-toggle { + border-color: #dddddd; +} + +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #dddddd; +} + +.navbar-default .navbar-toggle .icon-bar { + background-color: #cccccc; +} + +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e6e6e6; +} + +.navbar-default .navbar-nav > .dropdown > a:hover .caret, +.navbar-default .navbar-nav > .dropdown > a:focus .caret { + border-top-color: #333333; + border-bottom-color: #333333; +} + +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + color: #555555; + background-color: #e7e7e7; +} + +.navbar-default .navbar-nav > .open > a .caret, +.navbar-default .navbar-nav > .open > a:hover .caret, +.navbar-default .navbar-nav > .open > a:focus .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar-default .navbar-nav > .dropdown > a .caret { + border-top-color: #777777; + border-bottom-color: #777777; +} + +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #777777; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #333333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555555; + background-color: #e7e7e7; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #cccccc; + background-color: transparent; + } +} + +.navbar-default .navbar-link { + color: #777777; +} + +.navbar-default .navbar-link:hover { + color: #333333; +} + +.navbar-inverse { + background-color: #222222; + border-color: #080808; +} + +.navbar-inverse .navbar-brand { + color: #999999; +} + +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .navbar-text { + color: #999999; +} + +.navbar-inverse .navbar-nav > li > a { + color: #999999; +} + +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #ffffff; + background-color: #080808; +} + +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444444; + background-color: transparent; +} + +.navbar-inverse .navbar-toggle { + border-color: #333333; +} + +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #333333; +} + +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #ffffff; +} + +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} + +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + color: #ffffff; + background-color: #080808; +} + +.navbar-inverse .navbar-nav > .dropdown > a:hover .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .navbar-nav > .dropdown > a .caret { + border-top-color: #999999; + border-bottom-color: #999999; +} + +.navbar-inverse .navbar-nav > .open > a .caret, +.navbar-inverse .navbar-nav > .open > a:hover .caret, +.navbar-inverse .navbar-nav > .open > a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #999999; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #ffffff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #ffffff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444444; + background-color: transparent; + } +} + +.navbar-inverse .navbar-link { + color: #999999; +} + +.navbar-inverse .navbar-link:hover { + color: #ffffff; +} + +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} + +.breadcrumb > li { + display: inline-block; +} + +.breadcrumb > li + li:before { + padding: 0 5px; + color: #cccccc; + content: "/\00a0"; +} + +.breadcrumb > .active { + color: #999999; +} + +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} + +.pagination > li { + display: inline; +} + +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.428571429; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; +} + +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-bottom-left-radius: 4px; + border-top-left-radius: 4px; +} + +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + background-color: #eeeeee; +} + +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 2; + color: #ffffff; + cursor: default; + background-color: #428bca; + border-color: #428bca; +} + +.pagination > .disabled > span, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #999999; + cursor: not-allowed; + background-color: #ffffff; + border-color: #dddddd; +} + +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; +} + +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-bottom-left-radius: 6px; + border-top-left-radius: 6px; +} + +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; +} + +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; +} + +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} + +.pager:before, +.pager:after { + display: table; + content: " "; +} + +.pager:after { + clear: both; +} + +.pager:before, +.pager:after { + display: table; + content: " "; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #ffffff; + border: 1px solid #dddddd; + border-radius: 15px; +} + +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.pager .next > a, +.pager .next > span { + float: right; +} + +.pager .previous > a, +.pager .previous > span { + float: left; +} + +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #999999; + cursor: not-allowed; + background-color: #ffffff; +} + +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #ffffff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + +.label[href]:hover, +.label[href]:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label:empty { + display: none; +} + +.label-default { + background-color: #999999; +} + +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #808080; +} + +.label-primary { + background-color: #428bca; +} + +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #3071a9; +} + +.label-success { + background-color: #5cb85c; +} + +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} + +.label-info { + background-color: #5bc0de; +} + +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} + +.label-warning { + background-color: #f0ad4e; +} + +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} + +.label-danger { + background-color: #d9534f; +} + +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} + +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #ffffff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; + border-radius: 10px; +} + +.badge:empty { + display: none; +} + +a.badge:hover, +a.badge:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.btn .badge { + position: relative; + top: -1px; +} + +a.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #428bca; + background-color: #ffffff; +} + +.nav-pills > li > a > .badge { + margin-left: 3px; +} + +.jumbotron { + padding: 30px; + margin-bottom: 30px; + font-size: 21px; + font-weight: 200; + line-height: 2.1428571435; + color: inherit; + background-color: #eeeeee; +} + +.jumbotron h1 { + line-height: 1; + color: inherit; +} + +.jumbotron p { + line-height: 1.4; +} + +.container .jumbotron { + border-radius: 6px; +} + +@media screen and (min-width: 768px) { + .jumbotron { + padding-top: 48px; + padding-bottom: 48px; + } + .container .jumbotron { + padding-right: 60px; + padding-left: 60px; + } + .jumbotron h1 { + font-size: 63px; + } +} + +.thumbnail { + display: inline-block; + display: block; + height: auto; + max-width: 100%; + padding: 4px; + line-height: 1.428571429; + background-color: #ffffff; + border: 1px solid #dddddd; + border-radius: 4px; + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +.thumbnail > img { + display: block; + height: auto; + max-width: 100%; +} + +a.thumbnail:hover, +a.thumbnail:focus { + border-color: #428bca; +} + +.thumbnail > img { + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #333333; +} + +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert h4 { + margin-top: 0; + color: inherit; +} + +.alert .alert-link { + font-weight: bold; +} + +.alert > p, +.alert > ul { + margin-bottom: 0; +} + +.alert > p + p { + margin-top: 5px; +} + +.alert-dismissable { + padding-right: 35px; +} + +.alert-dismissable .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} + +.alert-success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-success hr { + border-top-color: #c9e2b3; +} + +.alert-success .alert-link { + color: #356635; +} + +.alert-info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-info hr { + border-top-color: #a6e1ec; +} + +.alert-info .alert-link { + color: #2d6987; +} + +.alert-warning { + color: #c09853; + background-color: #fcf8e3; + border-color: #fbeed5; +} + +.alert-warning hr { + border-top-color: #f8e5be; +} + +.alert-warning .alert-link { + color: #a47e3c; +} + +.alert-danger { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-danger hr { + border-top-color: #e6c1c7; +} + +.alert-danger .alert-link { + color: #953b39; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + color: #ffffff; + text-align: center; + background-color: #428bca; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress-striped .progress-bar { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 40px 40px; +} + +.progress.active .progress-bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-bar-success { + background-color: #5cb85c; +} + +.progress-striped .progress-bar-success { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-bar-info { + background-color: #5bc0de; +} + +.progress-striped .progress-bar-info { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-bar-warning { + background-color: #f0ad4e; +} + +.progress-striped .progress-bar-warning { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-bar-danger { + background-color: #d9534f; +} + +.progress-striped .progress-bar-danger { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.media, +.media-body { + overflow: hidden; + zoom: 1; +} + +.media, +.media .media { + margin-top: 15px; +} + +.media:first-child { + margin-top: 0; +} + +.media-object { + display: block; +} + +.media-heading { + margin: 0 0 5px; +} + +.media > .pull-left { + margin-right: 10px; +} + +.media > .pull-right { + margin-left: 10px; +} + +.media-list { + padding-left: 0; + list-style: none; +} + +.list-group { + padding-left: 0; + margin-bottom: 20px; +} + +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #ffffff; + border: 1px solid #dddddd; +} + +.list-group-item:first-child { + border-top-right-radius: 4px; + border-top-left-radius: 4px; +} + +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} + +.list-group-item > .badge { + float: right; +} + +.list-group-item > .badge + .badge { + margin-right: 5px; +} + +a.list-group-item { + color: #555555; +} + +a.list-group-item .list-group-item-heading { + color: #333333; +} + +a.list-group-item:hover, +a.list-group-item:focus { + text-decoration: none; + background-color: #f5f5f5; +} + +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: #ffffff; + background-color: #428bca; + border-color: #428bca; +} + +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading { + color: inherit; +} + +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #e1edf7; +} + +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} + +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} + +.panel { + margin-bottom: 20px; + background-color: #ffffff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.panel-body { + padding: 15px; +} + +.panel-body:before, +.panel-body:after { + display: table; + content: " "; +} + +.panel-body:after { + clear: both; +} + +.panel-body:before, +.panel-body:after { + display: table; + content: " "; +} + +.panel-body:after { + clear: both; +} + +.panel > .list-group { + margin-bottom: 0; +} + +.panel > .list-group .list-group-item { + border-width: 1px 0; +} + +.panel > .list-group .list-group-item:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.panel > .list-group .list-group-item:last-child { + border-bottom: 0; +} + +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} + +.panel > .table { + margin-bottom: 0; +} + +.panel > .panel-body + .table { + border-top: 1px solid #dddddd; +} + +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} + +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; +} + +.panel-title > a { + color: inherit; +} + +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #dddddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} + +.panel-group .panel { + margin-bottom: 0; + overflow: hidden; + border-radius: 4px; +} + +.panel-group .panel + .panel { + margin-top: 5px; +} + +.panel-group .panel-heading { + border-bottom: 0; +} + +.panel-group .panel-heading + .panel-collapse .panel-body { + border-top: 1px solid #dddddd; +} + +.panel-group .panel-footer { + border-top: 0; +} + +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #dddddd; +} + +.panel-default { + border-color: #dddddd; +} + +.panel-default > .panel-heading { + color: #333333; + background-color: #f5f5f5; + border-color: #dddddd; +} + +.panel-default > .panel-heading + .panel-collapse .panel-body { + border-top-color: #dddddd; +} + +.panel-default > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #dddddd; +} + +.panel-primary { + border-color: #428bca; +} + +.panel-primary > .panel-heading { + color: #ffffff; + background-color: #428bca; + border-color: #428bca; +} + +.panel-primary > .panel-heading + .panel-collapse .panel-body { + border-top-color: #428bca; +} + +.panel-primary > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #428bca; +} + +.panel-success { + border-color: #d6e9c6; +} + +.panel-success > .panel-heading { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.panel-success > .panel-heading + .panel-collapse .panel-body { + border-top-color: #d6e9c6; +} + +.panel-success > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #d6e9c6; +} + +.panel-warning { + border-color: #fbeed5; +} + +.panel-warning > .panel-heading { + color: #c09853; + background-color: #fcf8e3; + border-color: #fbeed5; +} + +.panel-warning > .panel-heading + .panel-collapse .panel-body { + border-top-color: #fbeed5; +} + +.panel-warning > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #fbeed5; +} + +.panel-danger { + border-color: #eed3d7; +} + +.panel-danger > .panel-heading { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.panel-danger > .panel-heading + .panel-collapse .panel-body { + border-top-color: #eed3d7; +} + +.panel-danger > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #eed3d7; +} + +.panel-info { + border-color: #bce8f1; +} + +.panel-info > .panel-heading { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.panel-info > .panel-heading + .panel-collapse .panel-body { + border-top-color: #bce8f1; +} + +.panel-info > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #bce8f1; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-lg { + padding: 24px; + border-radius: 6px; +} + +.well-sm { + padding: 9px; + border-radius: 3px; +} + +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover, +.close:focus { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.5; + filter: alpha(opacity=50); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.modal-open { + overflow: hidden; +} + +body.modal-open, +.modal-open .navbar-fixed-top, +.modal-open .navbar-fixed-bottom { + margin-right: 15px; +} + +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + display: none; + overflow: auto; + overflow-y: scroll; +} + +.modal.fade .modal-dialog { + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + transform: translate(0, -25%); + -webkit-transition: -webkit-transform 0.3s ease-out; + -moz-transition: -moz-transform 0.3s ease-out; + -o-transition: -o-transform 0.3s ease-out; + transition: transform 0.3s ease-out; +} + +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); +} + +.modal-dialog { + z-index: 1050; + width: auto; + padding: 10px; + margin-right: auto; + margin-left: auto; +} + +.modal-content { + position: relative; + background-color: #ffffff; + border: 1px solid #999999; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + outline: none; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + background-clip: padding-box; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; + filter: alpha(opacity=0); +} + +.modal-backdrop.in { + opacity: 0.5; + filter: alpha(opacity=50); +} + +.modal-header { + min-height: 16.428571429px; + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} + +.modal-header .close { + margin-top: -2px; +} + +.modal-title { + margin: 0; + line-height: 1.428571429; +} + +.modal-body { + position: relative; + padding: 20px; +} + +.modal-footer { + padding: 19px 20px 20px; + margin-top: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} + +@media screen and (min-width: 768px) { + .modal-dialog { + right: auto; + left: 50%; + width: 600px; + padding-top: 30px; + padding-bottom: 30px; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + } +} + +.tooltip { + position: absolute; + z-index: 1030; + display: block; + font-size: 12px; + line-height: 1.4; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.9; + filter: alpha(opacity=90); +} + +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} + +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} + +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} + +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.top-left .tooltip-arrow { + bottom: 0; + left: 5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.top-right .tooltip-arrow { + right: 5px; + bottom: 0; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-right-color: #000000; + border-width: 5px 5px 5px 0; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-left-color: #000000; + border-width: 5px 0 5px 5px; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.tooltip.bottom-left .tooltip-arrow { + top: 0; + left: 5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.tooltip.bottom-right .tooltip-arrow { + top: 0; + right: 5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 276px; + padding: 1px; + text-align: left; + white-space: normal; + background-color: #ffffff; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + background-clip: padding-box; +} + +.popover.top { + margin-top: -10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-left: -10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +.popover .arrow, +.popover .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover .arrow { + border-width: 11px; +} + +.popover .arrow:after { + border-width: 10px; + content: ""; +} + +.popover.top .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999999; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} + +.popover.top .arrow:after { + bottom: 1px; + margin-left: -10px; + border-top-color: #ffffff; + border-bottom-width: 0; + content: " "; +} + +.popover.right .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999999; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} + +.popover.right .arrow:after { + bottom: -10px; + left: 1px; + border-right-color: #ffffff; + border-left-width: 0; + content: " "; +} + +.popover.bottom .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-bottom-color: #999999; + border-bottom-color: rgba(0, 0, 0, 0.25); + border-top-width: 0; +} + +.popover.bottom .arrow:after { + top: 1px; + margin-left: -10px; + border-bottom-color: #ffffff; + border-top-width: 0; + content: " "; +} + +.popover.left .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-left-color: #999999; + border-left-color: rgba(0, 0, 0, 0.25); + border-right-width: 0; +} + +.popover.left .arrow:after { + right: 1px; + bottom: -10px; + border-left-color: #ffffff; + border-right-width: 0; + content: " "; +} + +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + height: auto; + max-width: 100%; + line-height: 1; +} + +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} + +.carousel-inner > .active { + left: 0; +} + +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel-inner > .next { + left: 100%; +} + +.carousel-inner > .prev { + left: -100%; +} + +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} + +.carousel-inner > .active.left { + left: -100%; +} + +.carousel-inner > .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #ffffff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.left { + background-image: -webkit-gradient(linear, 0 top, 100% top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.0001))); + background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.5) 0), color-stop(rgba(0, 0, 0, 0.0001) 100%)); + background-image: -moz-linear-gradient(left, rgba(0, 0, 0, 0.5) 0, rgba(0, 0, 0, 0.0001) 100%); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0, rgba(0, 0, 0, 0.0001) 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); +} + +.carousel-control.right { + right: 0; + left: auto; + background-image: -webkit-gradient(linear, 0 top, 100% top, from(rgba(0, 0, 0, 0.0001)), to(rgba(0, 0, 0, 0.5))); + background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.0001) 0), color-stop(rgba(0, 0, 0, 0.5) 100%)); + background-image: -moz-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0, rgba(0, 0, 0, 0.5) 100%); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0, rgba(0, 0, 0, 0.5) 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); +} + +.carousel-control:hover, +.carousel-control:focus { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + left: 50%; + z-index: 5; + display: inline-block; +} + +.carousel-control .icon-prev, +.carousel-control .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + margin-left: -10px; + font-family: serif; +} + +.carousel-control .icon-prev:before { + content: '\2039'; +} + +.carousel-control .icon-next:before { + content: '\203a'; +} + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} + +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + border: 1px solid #ffffff; + border-radius: 10px; +} + +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #ffffff; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #ffffff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); +} + +.carousel-caption .btn { + text-shadow: none; +} + +@media screen and (min-width: 768px) { + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + margin-left: -15px; + font-size: 30px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} + +.clearfix:before, +.clearfix:after { + display: table; + content: " "; +} + +.clearfix:after { + clear: both; +} + +.pull-right { + float: right !important; +} + +.pull-left { + float: left !important; +} + +.hide { + display: none !important; +} + +.show { + display: block !important; +} + +.invisible { + visibility: hidden; +} + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.affix { + position: fixed; +} + +@-ms-viewport { + width: device-width; +} + +@media screen and (max-width: 400px) { + @-ms-viewport { + width: 320px; + } +} + +.hidden { + display: none !important; + visibility: hidden !important; +} + +.visible-xs { + display: none !important; +} + +tr.visible-xs { + display: none !important; +} + +th.visible-xs, +td.visible-xs { + display: none !important; +} + +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + tr.visible-xs { + display: table-row !important; + } + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-xs.visible-sm { + display: block !important; + } + tr.visible-xs.visible-sm { + display: table-row !important; + } + th.visible-xs.visible-sm, + td.visible-xs.visible-sm { + display: table-cell !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-xs.visible-md { + display: block !important; + } + tr.visible-xs.visible-md { + display: table-row !important; + } + th.visible-xs.visible-md, + td.visible-xs.visible-md { + display: table-cell !important; + } +} + +@media (min-width: 1200px) { + .visible-xs.visible-lg { + display: block !important; + } + tr.visible-xs.visible-lg { + display: table-row !important; + } + th.visible-xs.visible-lg, + td.visible-xs.visible-lg { + display: table-cell !important; + } +} + +.visible-sm { + display: none !important; +} + +tr.visible-sm { + display: none !important; +} + +th.visible-sm, +td.visible-sm { + display: none !important; +} + +@media (max-width: 767px) { + .visible-sm.visible-xs { + display: block !important; + } + tr.visible-sm.visible-xs { + display: table-row !important; + } + th.visible-sm.visible-xs, + td.visible-sm.visible-xs { + display: table-cell !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + tr.visible-sm { + display: table-row !important; + } + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-sm.visible-md { + display: block !important; + } + tr.visible-sm.visible-md { + display: table-row !important; + } + th.visible-sm.visible-md, + td.visible-sm.visible-md { + display: table-cell !important; + } +} + +@media (min-width: 1200px) { + .visible-sm.visible-lg { + display: block !important; + } + tr.visible-sm.visible-lg { + display: table-row !important; + } + th.visible-sm.visible-lg, + td.visible-sm.visible-lg { + display: table-cell !important; + } +} + +.visible-md { + display: none !important; +} + +tr.visible-md { + display: none !important; +} + +th.visible-md, +td.visible-md { + display: none !important; +} + +@media (max-width: 767px) { + .visible-md.visible-xs { + display: block !important; + } + tr.visible-md.visible-xs { + display: table-row !important; + } + th.visible-md.visible-xs, + td.visible-md.visible-xs { + display: table-cell !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-md.visible-sm { + display: block !important; + } + tr.visible-md.visible-sm { + display: table-row !important; + } + th.visible-md.visible-sm, + td.visible-md.visible-sm { + display: table-cell !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + tr.visible-md { + display: table-row !important; + } + th.visible-md, + td.visible-md { + display: table-cell !important; + } +} + +@media (min-width: 1200px) { + .visible-md.visible-lg { + display: block !important; + } + tr.visible-md.visible-lg { + display: table-row !important; + } + th.visible-md.visible-lg, + td.visible-md.visible-lg { + display: table-cell !important; + } +} + +.visible-lg { + display: none !important; +} + +tr.visible-lg { + display: none !important; +} + +th.visible-lg, +td.visible-lg { + display: none !important; +} + +@media (max-width: 767px) { + .visible-lg.visible-xs { + display: block !important; + } + tr.visible-lg.visible-xs { + display: table-row !important; + } + th.visible-lg.visible-xs, + td.visible-lg.visible-xs { + display: table-cell !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-lg.visible-sm { + display: block !important; + } + tr.visible-lg.visible-sm { + display: table-row !important; + } + th.visible-lg.visible-sm, + td.visible-lg.visible-sm { + display: table-cell !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-lg.visible-md { + display: block !important; + } + tr.visible-lg.visible-md { + display: table-row !important; + } + th.visible-lg.visible-md, + td.visible-lg.visible-md { + display: table-cell !important; + } +} + +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + tr.visible-lg { + display: table-row !important; + } + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } +} + +.hidden-xs { + display: block !important; +} + +tr.hidden-xs { + display: table-row !important; +} + +th.hidden-xs, +td.hidden-xs { + display: table-cell !important; +} + +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } + tr.hidden-xs { + display: none !important; + } + th.hidden-xs, + td.hidden-xs { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .hidden-xs.hidden-sm { + display: none !important; + } + tr.hidden-xs.hidden-sm { + display: none !important; + } + th.hidden-xs.hidden-sm, + td.hidden-xs.hidden-sm { + display: none !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-xs.hidden-md { + display: none !important; + } + tr.hidden-xs.hidden-md { + display: none !important; + } + th.hidden-xs.hidden-md, + td.hidden-xs.hidden-md { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-xs.hidden-lg { + display: none !important; + } + tr.hidden-xs.hidden-lg { + display: none !important; + } + th.hidden-xs.hidden-lg, + td.hidden-xs.hidden-lg { + display: none !important; + } +} + +.hidden-sm { + display: block !important; +} + +tr.hidden-sm { + display: table-row !important; +} + +th.hidden-sm, +td.hidden-sm { + display: table-cell !important; +} + +@media (max-width: 767px) { + .hidden-sm.hidden-xs { + display: none !important; + } + tr.hidden-sm.hidden-xs { + display: none !important; + } + th.hidden-sm.hidden-xs, + td.hidden-sm.hidden-xs { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } + tr.hidden-sm { + display: none !important; + } + th.hidden-sm, + td.hidden-sm { + display: none !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-sm.hidden-md { + display: none !important; + } + tr.hidden-sm.hidden-md { + display: none !important; + } + th.hidden-sm.hidden-md, + td.hidden-sm.hidden-md { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-sm.hidden-lg { + display: none !important; + } + tr.hidden-sm.hidden-lg { + display: none !important; + } + th.hidden-sm.hidden-lg, + td.hidden-sm.hidden-lg { + display: none !important; + } +} + +.hidden-md { + display: block !important; +} + +tr.hidden-md { + display: table-row !important; +} + +th.hidden-md, +td.hidden-md { + display: table-cell !important; +} + +@media (max-width: 767px) { + .hidden-md.hidden-xs { + display: none !important; + } + tr.hidden-md.hidden-xs { + display: none !important; + } + th.hidden-md.hidden-xs, + td.hidden-md.hidden-xs { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .hidden-md.hidden-sm { + display: none !important; + } + tr.hidden-md.hidden-sm { + display: none !important; + } + th.hidden-md.hidden-sm, + td.hidden-md.hidden-sm { + display: none !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } + tr.hidden-md { + display: none !important; + } + th.hidden-md, + td.hidden-md { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-md.hidden-lg { + display: none !important; + } + tr.hidden-md.hidden-lg { + display: none !important; + } + th.hidden-md.hidden-lg, + td.hidden-md.hidden-lg { + display: none !important; + } +} + +.hidden-lg { + display: block !important; +} + +tr.hidden-lg { + display: table-row !important; +} + +th.hidden-lg, +td.hidden-lg { + display: table-cell !important; +} + +@media (max-width: 767px) { + .hidden-lg.hidden-xs { + display: none !important; + } + tr.hidden-lg.hidden-xs { + display: none !important; + } + th.hidden-lg.hidden-xs, + td.hidden-lg.hidden-xs { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .hidden-lg.hidden-sm { + display: none !important; + } + tr.hidden-lg.hidden-sm { + display: none !important; + } + th.hidden-lg.hidden-sm, + td.hidden-lg.hidden-sm { + display: none !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-lg.hidden-md { + display: none !important; + } + tr.hidden-lg.hidden-md { + display: none !important; + } + th.hidden-lg.hidden-md, + td.hidden-lg.hidden-md { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } + tr.hidden-lg { + display: none !important; + } + th.hidden-lg, + td.hidden-lg { + display: none !important; + } +} + +.visible-print { + display: none !important; +} + +tr.visible-print { + display: none !important; +} + +th.visible-print, +td.visible-print { + display: none !important; +} + +@media print { + .visible-print { + display: block !important; + } + tr.visible-print { + display: table-row !important; + } + th.visible-print, + td.visible-print { + display: table-cell !important; + } + .hidden-print { + display: none !important; + } + tr.hidden-print { + display: none !important; + } + th.hidden-print, + td.hidden-print { + display: none !important; + } +} +</style> diff --git a/framework/yii/requirements/views/web/index.php b/framework/yii/requirements/views/web/index.php new file mode 100644 index 0000000..1887f8b --- /dev/null +++ b/framework/yii/requirements/views/web/index.php @@ -0,0 +1,82 @@ +<?php +/* @var $this YiiRequirementChecker */ +/* @var $summary array */ +/* @var $requirements array[] */ +?> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"/> + <title>Yii Application Requirement Checker</title> + <?php $this->renderViewFile(dirname(__FILE__) . '/css.php'); ?> +</head> +<body> +<div class="container"> + <div class="header"> + <h1>Yii Application Requirement Checker</h1> + </div> + <hr> + + <div class="content"> + <h3>Description</h3> + <p> + This script checks if your server configuration meets the requirements + for running Yii application. + It checks if the server is running the right version of PHP, + if appropriate PHP extensions have been loaded, and if php.ini file settings are correct. + </p> + <p> + There are two kinds of requirements being checked. Mandatory requirements are those that have to be met + to allow Yii to work as expected. There are also some optional requirements beeing checked which will + show you a warning when they do not meet. You can use Yii framework without them but some specific + functionality may be not available in this case. + </p> + + <h3>Conclusion</h3> + <?php if ($summary['errors'] > 0): ?> + <div class="alert alert-error"> + <strong>Unfortunately your server configuration does not satisfy the requirements by this application.<br>Please refer to the table below for detailed explanation.</strong> + </div> + <?php elseif ($summary['warnings'] > 0): ?> + <div class="alert alert-info"> + <strong>Your server configuration satisfies the minimum requirements by this application.<br>Please pay attention to the warnings listed below and check if your application will use the corresponding features.</strong> + </div> + <?php else: ?> + <div class="alert alert-success"> + <strong>Congratulations! Your server configuration satisfies all requirements.</strong> + </div> + <?php endif; ?> + + <h3>Details</h3> + + <table class="table table-bordered"> + <tr><th>Name</th><th>Result</th><th>Required By</th><th>Memo</th></tr> + <?php foreach($requirements as $requirement): ?> + <tr class="<?php echo $requirement['condition'] ? 'success' : ($requirement['mandatory'] ? 'error' : 'warning'); ?>"> + <td> + <?php echo $requirement['name']; ?> + </td> + <td> + <span class="result"><?php echo $requirement['condition'] ? 'Passed' : ($requirement['mandatory'] ? 'Failed' : 'Warning'); ?></span> + </td> + <td> + <?php echo $requirement['by']; ?> + </td> + <td> + <?php echo $requirement['memo']; ?> + </td> + </tr> + <?php endforeach; ?> + </table> + + </div> + + <hr> + + <div class="footer"> + <p>Server: <?php echo $this->getServerInfo() . ' ' . $this->getNowDate(); ?></p> + <p>Powered by <a href="http://www.yiiframework.com/" rel="external">Yii Framework</a></p> + </div> +</div> +</body> +</html> diff --git a/yii/test/TestCase.php b/framework/yii/test/TestCase.php similarity index 86% rename from yii/test/TestCase.php rename to framework/yii/test/TestCase.php index f190e5a..2e9b480 100644 --- a/yii/test/TestCase.php +++ b/framework/yii/test/TestCase.php @@ -10,9 +10,9 @@ namespace yii\test; require_once('PHPUnit/Runner/Version.php'); -spl_autoload_unregister(array('Yii','autoload')); +spl_autoload_unregister(array('Yii', 'autoload')); require_once('PHPUnit/Autoload.php'); -spl_autoload_register(array('Yii','autoload')); // put yii's autoloader at the end +spl_autoload_register(array('Yii', 'autoload')); // put yii's autoloader at the end /** * TestCase is the base class for all test case classes. diff --git a/yii/test/WebTestCase.php b/framework/yii/test/WebTestCase.php similarity index 100% rename from yii/test/WebTestCase.php rename to framework/yii/test/WebTestCase.php diff --git a/yii/validators/BooleanValidator.php b/framework/yii/validators/BooleanValidator.php similarity index 94% rename from yii/validators/BooleanValidator.php rename to framework/yii/validators/BooleanValidator.php index 1420739..7eb1427 100644 --- a/yii/validators/BooleanValidator.php +++ b/framework/yii/validators/BooleanValidator.php @@ -43,7 +43,7 @@ class BooleanValidator extends Validator { parent::init(); if ($this->message === null) { - $this->message = Yii::t('yii|{attribute} must be either "{true}" or "{false}".'); + $this->message = Yii::t('yii', '{attribute} must be either "{true}" or "{false}".'); } } @@ -79,16 +79,17 @@ class BooleanValidator extends Validator * 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. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $options = array( 'trueValue' => $this->trueValue, 'falseValue' => $this->falseValue, 'message' => Html::encode(strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, '{true}' => $this->trueValue, '{false}' => $this->falseValue, ))), @@ -100,6 +101,7 @@ class BooleanValidator extends Validator $options['strict'] = 1; } + ValidationAsset::register($view); return 'yii.validation.boolean(value, messages, ' . json_encode($options) . ');'; } } diff --git a/yii/validators/CompareValidator.php b/framework/yii/validators/CompareValidator.php similarity index 84% rename from yii/validators/CompareValidator.php rename to framework/yii/validators/CompareValidator.php index 68504e5..35e044a 100644 --- a/yii/validators/CompareValidator.php +++ b/framework/yii/validators/CompareValidator.php @@ -58,7 +58,7 @@ class CompareValidator extends Validator * - `<`: validates to see if the value being validated is less than the value being compared with. * - `<=`: validates to see if the value being validated is less than or equal to the value being compared with. */ - public $operator = '='; + public $operator = '=='; /** * @var string the user-defined error message. It may contain the following placeholders which * will be replaced accordingly by the validator: @@ -79,28 +79,28 @@ class CompareValidator extends Validator if ($this->message === null) { switch ($this->operator) { case '==': - $this->message = Yii::t('yii|{attribute} must be repeated exactly.'); + $this->message = Yii::t('yii', '{attribute} must be repeated exactly.'); break; case '===': - $this->message = Yii::t('yii|{attribute} must be repeated exactly.'); + $this->message = Yii::t('yii', '{attribute} must be repeated exactly.'); break; case '!=': - $this->message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); + $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}".'); + $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}".'); + $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}".'); + $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}".'); + $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}".'); + $this->message = Yii::t('yii', '{attribute} must be less than or equal to "{compareValue}".'); break; default: throw new InvalidConfigException("Unknown operator: {$this->operator}"); @@ -119,7 +119,7 @@ class CompareValidator extends Validator { $value = $object->$attribute; if (is_array($value)) { - $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); + $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); return; } if ($this->compareValue !== null) { @@ -170,6 +170,7 @@ class CompareValidator extends Validator case '>=': return $value >= $this->compareValue; case '<': return $value < $this->compareValue; case '<=': return $value <= $this->compareValue; + default: return false; } } @@ -178,9 +179,11 @@ class CompareValidator extends Validator * @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 + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @throws InvalidConfigException if CompareValidator::operator is invalid */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $options = array('operator' => $this->operator); @@ -199,10 +202,11 @@ class CompareValidator extends Validator $options['message'] = Html::encode(strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, + '{compareAttribute}' => $compareValue, '{compareValue}' => $compareValue, ))); + ValidationAsset::register($view); return 'yii.validation.compare(value, messages, ' . json_encode($options) . ');'; } } diff --git a/yii/validators/DateValidator.php b/framework/yii/validators/DateValidator.php similarity index 97% rename from yii/validators/DateValidator.php rename to framework/yii/validators/DateValidator.php index 7c3b181..2f9e18b 100644 --- a/yii/validators/DateValidator.php +++ b/framework/yii/validators/DateValidator.php @@ -38,7 +38,7 @@ class DateValidator extends Validator { parent::init(); if ($this->message === null) { - $this->message = Yii::t('yii|The format of {attribute} is invalid.'); + $this->message = Yii::t('yii', 'The format of {attribute} is invalid.'); } } @@ -58,7 +58,7 @@ class DateValidator extends Validator $date = DateTime::createFromFormat($this->format, $value); if ($date === false) { $this->addError($object, $attribute, $this->message); - } elseif ($this->timestampAttribute !== false) { + } elseif ($this->timestampAttribute !== null) { $object->{$this->timestampAttribute} = $date->getTimestamp(); } } @@ -73,4 +73,3 @@ class DateValidator extends Validator return DateTime::createFromFormat($this->format, $value) !== false; } } - diff --git a/yii/validators/DefaultValueValidator.php b/framework/yii/validators/DefaultValueValidator.php similarity index 100% rename from yii/validators/DefaultValueValidator.php rename to framework/yii/validators/DefaultValueValidator.php index 185dbd4..20df5fd 100644 --- a/yii/validators/DefaultValueValidator.php +++ b/framework/yii/validators/DefaultValueValidator.php @@ -40,4 +40,3 @@ class DefaultValueValidator extends Validator } } } - diff --git a/yii/validators/EmailValidator.php b/framework/yii/validators/EmailValidator.php similarity index 76% rename from yii/validators/EmailValidator.php rename to framework/yii/validators/EmailValidator.php index 949b3f9..7081cfb 100644 --- a/yii/validators/EmailValidator.php +++ b/framework/yii/validators/EmailValidator.php @@ -8,6 +8,7 @@ namespace yii\validators; use Yii; +use yii\base\InvalidConfigException; use yii\helpers\Html; use yii\web\JsExpression; use yii\helpers\Json; @@ -47,6 +48,14 @@ class EmailValidator extends Validator * Defaults to false. */ public $checkPort = false; + /** + * @var boolean whether validation process should take into account IDN (internationalized domain + * names). Defaults to false meaning that validation of emails containing IDN will always fail. + * Note that in order to use IDN validation you have to install and enable `intl` PHP extension, + * otherwise an exception would be thrown. + */ + public $enableIDN = false; + /** * Initializes the validator. @@ -54,8 +63,11 @@ class EmailValidator extends Validator public function init() { parent::init(); + if ($this->enableIDN && !function_exists('idn_to_ascii')) { + throw new InvalidConfigException('In order to use IDN validation intl extension must be installed and enabled.'); + } if ($this->message === null) { - $this->message = Yii::t('yii|{attribute} is not a valid email address.'); + $this->message = Yii::t('yii', '{attribute} is not a valid email address.'); } } @@ -81,10 +93,18 @@ class EmailValidator extends Validator public function validateValue($value) { // make sure string length is limited to avoid DOS attacks - $valid = is_string($value) && strlen($value) <= 254 - && (preg_match($this->pattern, $value) || $this->allowName && preg_match($this->fullPattern, $value)); + if (!is_string($value) || strlen($value) >= 255) { + return false; + } + if (($atPosition = strpos($value, '@')) === false) { + return false; + } + $domain = rtrim(substr($value, $atPosition + 1), '>'); + if ($this->enableIDN) { + $value = idn_to_ascii(ltrim(substr($value, 0, $atPosition), '<')) . '@' . idn_to_ascii($domain); + } + $valid = preg_match($this->pattern, $value) || $this->allowName && preg_match($this->fullPattern, $value); if ($valid) { - $domain = rtrim(substr($value, strpos($value, '@') + 1), '>'); if ($this->checkMX && function_exists('checkdnsrr')) { $valid = checkdnsrr($domain, 'MX'); } @@ -99,9 +119,11 @@ class EmailValidator extends Validator * 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. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $options = array( 'pattern' => new JsExpression($this->pattern), @@ -109,13 +131,17 @@ class EmailValidator extends Validator 'allowName' => $this->allowName, 'message' => Html::encode(strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, ))), + 'enableIDN' => (boolean)$this->enableIDN, ); if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; } + ValidationAsset::register($view); + if ($this->enableIDN) { + PunycodeAsset::register($view); + } return 'yii.validation.email(value, messages, ' . Json::encode($options) . ');'; } } diff --git a/yii/validators/ExistValidator.php b/framework/yii/validators/ExistValidator.php similarity index 97% rename from yii/validators/ExistValidator.php rename to framework/yii/validators/ExistValidator.php index 7aa434c..8cbce5f 100644 --- a/yii/validators/ExistValidator.php +++ b/framework/yii/validators/ExistValidator.php @@ -45,7 +45,7 @@ class ExistValidator extends Validator { parent::init(); if ($this->message === null) { - $this->message = Yii::t('yii|{attribute} is invalid.'); + $this->message = Yii::t('yii', '{attribute} is invalid.'); } } @@ -66,7 +66,7 @@ class ExistValidator 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) : $this->className; $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $query = $className::find(); $query->where(array($attributeName => $value)); @@ -99,4 +99,3 @@ class ExistValidator extends Validator return $query->exists(); } } - diff --git a/yii/validators/FileValidator.php b/framework/yii/validators/FileValidator.php similarity index 84% rename from yii/validators/FileValidator.php rename to framework/yii/validators/FileValidator.php index cc36d07..e2880af 100644 --- a/yii/validators/FileValidator.php +++ b/framework/yii/validators/FileValidator.php @@ -13,13 +13,15 @@ use yii\web\UploadedFile; /** * FileValidator verifies if an attribute is receiving a valid uploaded file. * + * @property integer $sizeLimit The size limit for uploaded files. This property is read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ class FileValidator extends Validator { /** - * @var mixed a list of file name extensions that are allowed to be uploaded. + * @var array|string 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"). * Extension names are case-insensitive. Defaults to null, meaning all file name @@ -97,22 +99,22 @@ class FileValidator extends Validator { parent::init(); if ($this->message === null) { - $this->message = Yii::t('yii|File upload failed.'); + $this->message = Yii::t('yii', 'File upload failed.'); } if ($this->uploadRequired === null) { - $this->uploadRequired = Yii::t('yii|Please upload a file.'); + $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.'); + $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}.'); + $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.'); + $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.'); + $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); @@ -133,7 +135,7 @@ class FileValidator extends Validator return; } foreach ($files as $i => $file) { - if (!$file instanceof UploadedFile || $file->getError() == UPLOAD_ERR_NO_FILE) { + if (!$file instanceof UploadedFile || $file->error == UPLOAD_ERR_NO_FILE) { unset($files[$i]); } } @@ -150,7 +152,7 @@ class FileValidator extends Validator } } else { $file = $object->$attribute; - if ($file instanceof UploadedFile && $file->getError() != UPLOAD_ERR_NO_FILE) { + if ($file instanceof UploadedFile && $file->error != UPLOAD_ERR_NO_FILE) { $this->validateFile($object, $attribute, $file); } else { $this->addError($object, $attribute, $this->uploadRequired); @@ -166,37 +168,37 @@ class FileValidator extends Validator */ protected function validateFile($object, $attribute, $file) { - switch ($file->getError()) { + switch ($file->error) { 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->maxSize !== null && $file->size > $this->maxSize) { + $this->addError($object, $attribute, $this->tooBig, array('{file}' => $file->name, '{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 ($this->minSize !== null && $file->size < $this->minSize) { + $this->addError($object, $attribute, $this->tooSmall, array('{file}' => $file->name, '{limit}' => $this->minSize)); } - 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))); + if (!empty($this->types) && !in_array(strtolower(pathinfo($file->name, PATHINFO_EXTENSION)), $this->types, true)) { + $this->addError($object, $attribute, $this->wrongType, array('{file}' => $file->name, '{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())); + $this->addError($object, $attribute, $this->tooBig, array('{file}' => $file->name, '{limit}' => $this->getSizeLimit())); break; case UPLOAD_ERR_PARTIAL: $this->addError($object, $attribute, $this->message); - Yii::warning('File was only partially uploaded: ' . $file->getName(), __METHOD__); + Yii::warning('File was only partially uploaded: ' . $file->name, __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__); + Yii::warning('Missing the temporary folder to store the uploaded file: ' . $file->name, __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__); + Yii::warning('Failed to write the uploaded file to disk: ' . $file->name, __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__); + Yii::warning('File upload was stopped by some PHP extension: ' . $file->name, __METHOD__); break; default: break; diff --git a/yii/validators/FilterValidator.php b/framework/yii/validators/FilterValidator.php similarity index 100% rename from yii/validators/FilterValidator.php rename to framework/yii/validators/FilterValidator.php index 72a9a9d..560feb1 100644 --- a/yii/validators/FilterValidator.php +++ b/framework/yii/validators/FilterValidator.php @@ -6,6 +6,7 @@ */ namespace yii\validators; + use yii\base\InvalidConfigException; /** diff --git a/yii/validators/InlineValidator.php b/framework/yii/validators/InlineValidator.php similarity index 97% rename from yii/validators/InlineValidator.php rename to framework/yii/validators/InlineValidator.php index 8af5bbc..dd951aa 100644 --- a/yii/validators/InlineValidator.php +++ b/framework/yii/validators/InlineValidator.php @@ -79,12 +79,14 @@ class InlineValidator extends Validator * * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. Null if the validator does not support * client-side validation. * @see enableClientValidation * @see \yii\web\ActiveForm::enableClientValidation */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { if ($this->clientValidate !== null) { $method = $this->clientValidate; diff --git a/yii/validators/NumberValidator.php b/framework/yii/validators/NumberValidator.php similarity index 88% rename from yii/validators/NumberValidator.php rename to framework/yii/validators/NumberValidator.php index 10f0e52..960828d 100644 --- a/yii/validators/NumberValidator.php +++ b/framework/yii/validators/NumberValidator.php @@ -62,14 +62,14 @@ class NumberValidator extends Validator { 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.'); + $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}.'); + $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}.'); + $this->tooBig = Yii::t('yii', '{attribute} must be no greater than {max}.'); } } @@ -83,7 +83,7 @@ class NumberValidator extends Validator { $value = $object->$attribute; if (is_array($value)) { - $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); + $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); return; } $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern; @@ -114,18 +114,18 @@ class NumberValidator extends Validator * 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. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $label = $object->getAttributeLabel($attribute); - $value = $object->$attribute; $options = array( 'pattern' => new JsExpression($this->integerOnly ? $this->integerPattern : $this->numberPattern), 'message' => Html::encode(strtr($this->message, array( '{attribute}' => $label, - '{value}' => $value, ))), ); @@ -133,7 +133,6 @@ class NumberValidator extends Validator $options['min'] = $this->min; $options['tooSmall'] = Html::encode(strtr($this->tooSmall, array( '{attribute}' => $label, - '{value}' => $value, '{min}' => $this->min, ))); } @@ -141,7 +140,6 @@ class NumberValidator extends Validator $options['max'] = $this->max; $options['tooBig'] = Html::encode(strtr($this->tooBig, array( '{attribute}' => $label, - '{value}' => $value, '{max}' => $this->max, ))); } @@ -149,6 +147,7 @@ class NumberValidator extends Validator $options['skipOnEmpty'] = 1; } + ValidationAsset::register($view); return 'yii.validation.number(value, messages, ' . Json::encode($options) . ');'; } } diff --git a/framework/yii/validators/PunycodeAsset.php b/framework/yii/validators/PunycodeAsset.php new file mode 100644 index 0000000..08d3fb4 --- /dev/null +++ b/framework/yii/validators/PunycodeAsset.php @@ -0,0 +1,21 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\validators; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class PunycodeAsset extends AssetBundle +{ + public $sourcePath = '@yii/assets'; + public $js = array( + 'punycode/punycode.js', + ); +} diff --git a/yii/validators/RangeValidator.php b/framework/yii/validators/RangeValidator.php similarity index 95% rename from yii/validators/RangeValidator.php rename to framework/yii/validators/RangeValidator.php index 2a9e15f..fedc027 100644 --- a/yii/validators/RangeValidator.php +++ b/framework/yii/validators/RangeValidator.php @@ -35,7 +35,7 @@ class RangeValidator extends Validator * @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; + public $not = false; /** * Initializes the validator. @@ -48,7 +48,7 @@ class RangeValidator extends Validator throw new InvalidConfigException('The "range" property must be set.'); } if ($this->message === null) { - $this->message = Yii::t('yii|{attribute} is invalid.'); + $this->message = Yii::t('yii', '{attribute} is invalid.'); } } @@ -81,9 +81,11 @@ class RangeValidator extends Validator * 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. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $range = array(); foreach ($this->range as $value) { @@ -94,13 +96,13 @@ class RangeValidator extends Validator 'not' => $this->not, 'message' => Html::encode(strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, ))), ); if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; } + ValidationAsset::register($view); return 'yii.validation.range(value, messages, ' . json_encode($options) . ');'; } } diff --git a/yii/validators/RegularExpressionValidator.php b/framework/yii/validators/RegularExpressionValidator.php similarity index 94% rename from yii/validators/RegularExpressionValidator.php rename to framework/yii/validators/RegularExpressionValidator.php index 23419b9..f5c5d02 100644 --- a/yii/validators/RegularExpressionValidator.php +++ b/framework/yii/validators/RegularExpressionValidator.php @@ -30,9 +30,8 @@ class RegularExpressionValidator extends Validator /** * @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; + public $not = false; /** * Initializes the validator. @@ -45,7 +44,7 @@ class RegularExpressionValidator extends Validator throw new InvalidConfigException('The "pattern" property must be set.'); } if ($this->message === null) { - $this->message = Yii::t('yii|{attribute} is invalid.'); + $this->message = Yii::t('yii', '{attribute} is invalid.'); } } @@ -79,10 +78,11 @@ class RegularExpressionValidator extends Validator * 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. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. - * @throws InvalidConfigException if the "pattern" is not a valid regular expression */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $pattern = $this->pattern; $pattern = preg_replace('/\\\\x\{?([0-9a-fA-F]+)\}?/', '\u$1', $pattern); @@ -103,13 +103,13 @@ class RegularExpressionValidator extends Validator 'not' => $this->not, 'message' => Html::encode(strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, ))), ); if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; } + ValidationAsset::register($view); return 'yii.validation.regularExpression(value, messages, ' . Json::encode($options) . ');'; } } diff --git a/yii/validators/RequiredValidator.php b/framework/yii/validators/RequiredValidator.php similarity index 93% rename from yii/validators/RequiredValidator.php rename to framework/yii/validators/RequiredValidator.php index 4c14a8d..68968be 100644 --- a/yii/validators/RequiredValidator.php +++ b/framework/yii/validators/RequiredValidator.php @@ -57,8 +57,8 @@ class RequiredValidator extends Validator { parent::init(); if ($this->message === null) { - $this->message = $this->requiredValue === null ? Yii::t('yii|{attribute} cannot be blank.') - : Yii::t('yii|{attribute} must be "{requiredValue}".'); + $this->message = $this->requiredValue === null ? Yii::t('yii', '{attribute} cannot be blank.') + : Yii::t('yii', '{attribute} must be "{requiredValue}".'); } } @@ -102,9 +102,11 @@ class RequiredValidator extends Validator * 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. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $options = array(); if ($this->requiredValue !== null) { @@ -121,9 +123,9 @@ class RequiredValidator extends Validator $options['message'] = Html::encode(strtr($options['message'], array( '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, ))); + ValidationAsset::register($view); return 'yii.validation.required(value, messages, ' . json_encode($options) . ');'; } } diff --git a/yii/base/Exception.php b/framework/yii/validators/SafeValidator.php similarity index 73% rename from yii/base/Exception.php rename to framework/yii/validators/SafeValidator.php index 7d26bd0..25da899 100644 --- a/yii/base/Exception.php +++ b/framework/yii/validators/SafeValidator.php @@ -5,21 +5,20 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\base; +namespace yii\validators; /** - * Exception represents a generic exception for all purposes. + * SafeValidator serves as a dummy validator whose main purpose is to mark the attributes to be safe for massive assignment. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Exception extends \Exception +class SafeValidator extends Validator { /** - * @return string the user-friendly name of this exception + * @inheritdoc */ - public function getName() + public function validateAttribute($object, $attribute) { - return \Yii::t('yii|Exception'); } } diff --git a/yii/validators/StringValidator.php b/framework/yii/validators/StringValidator.php similarity index 71% rename from yii/validators/StringValidator.php rename to framework/yii/validators/StringValidator.php index 5d0fa1a..166d2d6 100644 --- a/yii/validators/StringValidator.php +++ b/framework/yii/validators/StringValidator.php @@ -21,17 +21,24 @@ use yii\helpers\Html; class StringValidator extends Validator { /** - * @var integer maximum length. Defaults to null, meaning no maximum limit. + * @var integer|array specifies the length limit of the value to be validated. + * This can be specified in one of the following forms: + * + * - an integer: the exact length that the value should be of; + * - an array of one element: the minimum length that the value should be of. For example, `array(8)`. + * This will overwrite [[min]]. + * - an array of two elements: the minimum and maximum lengths that the value should be of. + * For example, `array(8, 128)`. This will overwrite both [[min]] and [[max]]. */ - public $max; + public $length; /** - * @var integer minimum length. Defaults to null, meaning no minimum limit. + * @var integer maximum length. If not set, it means no maximum length limit. */ - public $min; + public $max; /** - * @var integer exact length. Defaults to null, meaning no exact length limit. + * @var integer minimum length. If not set, it means no minimum length limit. */ - public $is; + public $min; /** * @var string user-defined error message used when the value is not a string */ @@ -45,7 +52,7 @@ class StringValidator extends Validator */ public $tooLong; /** - * @var string user-defined error message used when the length of the value is not equal to [[is]]. + * @var string user-defined error message used when the length of the value is not equal to [[length]]. */ public $notEqual; /** @@ -61,20 +68,29 @@ class StringValidator extends Validator public function init() { parent::init(); + if (is_array($this->length)) { + if (isset($this->length[0])) { + $this->min = $this->length[0]; + } + if (isset($this->length[1])) { + $this->max = $this->length[1]; + } + $this->length = null; + } if ($this->encoding === null) { $this->encoding = Yii::$app->charset; } if ($this->message === null) { - $this->message = Yii::t('yii|{attribute} must be a string.'); + $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.'); + $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.'); + $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.'); + if ($this->length !== null && $this->notEqual === null) { + $this->notEqual = Yii::t('yii', '{attribute} should contain {length} characters.'); } } @@ -101,8 +117,8 @@ class StringValidator extends Validator if ($this->max !== null && $length > $this->max) { $this->addError($object, $attribute, $this->tooLong, array('{max}' => $this->max)); } - if ($this->is !== null && $length !== $this->is) { - $this->addError($object, $attribute, $this->notEqual, array('{length}' => $this->is)); + if ($this->length !== null && $length !== $this->length) { + $this->addError($object, $attribute, $this->notEqual, array('{length}' => $this->length)); } } @@ -119,24 +135,24 @@ class StringValidator extends Validator $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); + && ($this->length === null || $length === $this->length); } /** * 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. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $label = $object->getAttributeLabel($attribute); - $value = $object->$attribute; $options = array( 'message' => Html::encode(strtr($this->message, array( '{attribute}' => $label, - '{value}' => $value, ))), ); @@ -144,7 +160,6 @@ class StringValidator extends Validator $options['min'] = $this->min; $options['tooShort'] = Html::encode(strtr($this->tooShort, array( '{attribute}' => $label, - '{value}' => $value, '{min}' => $this->min, ))); } @@ -152,15 +167,13 @@ class StringValidator extends Validator $options['max'] = $this->max; $options['tooLong'] = Html::encode(strtr($this->tooLong, array( '{attribute}' => $label, - '{value}' => $value, '{max}' => $this->max, ))); } - if ($this->is !== null) { - $options['is'] = $this->is; + if ($this->length !== null) { + $options['is'] = $this->length; $options['notEqual'] = Html::encode(strtr($this->notEqual, array( '{attribute}' => $label, - '{value}' => $value, '{length}' => $this->is, ))); } @@ -168,7 +181,7 @@ class StringValidator extends Validator $options['skipOnEmpty'] = 1; } + ValidationAsset::register($view); return 'yii.validation.string(value, messages, ' . json_encode($options) . ');'; } } - diff --git a/yii/validators/UniqueValidator.php b/framework/yii/validators/UniqueValidator.php similarity index 93% rename from yii/validators/UniqueValidator.php rename to framework/yii/validators/UniqueValidator.php index 7072ff4..c3876e8 100644 --- a/yii/validators/UniqueValidator.php +++ b/framework/yii/validators/UniqueValidator.php @@ -9,6 +9,7 @@ namespace yii\validators; use Yii; use yii\base\InvalidConfigException; +use yii\db\ActiveRecord; /** * CUniqueValidator validates that the attribute value is unique in the corresponding database table. @@ -39,7 +40,7 @@ class UniqueValidator extends Validator { parent::init(); if ($this->message === null) { - $this->message = Yii::t('yii|{attribute} "{value}" has already been taken.'); + $this->message = Yii::t('yii', '{attribute} "{value}" has already been taken.'); } } @@ -55,12 +56,12 @@ class UniqueValidator extends Validator $value = $object->$attribute; if (is_array($value)) { - $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); + $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); + $className = $this->className === null ? get_class($object) : $this->className; $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $table = $className::getTableSchema(); @@ -71,7 +72,7 @@ class UniqueValidator extends Validator $query = $className::find(); $query->where(array($column->name => $value)); - if ($object->getIsNewRecord()) { + if (!$object instanceof ActiveRecord || $object->getIsNewRecord()) { // if current $object isn't in the database yet then it's OK just to call exists() $exists = $query->exists(); } else { diff --git a/yii/validators/UrlValidator.php b/framework/yii/validators/UrlValidator.php similarity index 83% rename from yii/validators/UrlValidator.php rename to framework/yii/validators/UrlValidator.php index c418353..9230b36 100644 --- a/yii/validators/UrlValidator.php +++ b/framework/yii/validators/UrlValidator.php @@ -8,6 +8,7 @@ namespace yii\validators; use Yii; +use yii\base\InvalidConfigException; use yii\helpers\Html; use yii\web\JsExpression; use yii\helpers\Json; @@ -37,6 +38,13 @@ class UrlValidator extends Validator * contain the scheme part. **/ public $defaultScheme; + /** + * @var boolean whether validation process should take into account IDN (internationalized + * domain names). Defaults to false meaning that validation of URLs containing IDN will always + * fail. Note that in order to use IDN validation you have to install and enable `intl` PHP + * extension, otherwise an exception would be thrown. + */ + public $enableIDN = false; /** @@ -45,8 +53,11 @@ class UrlValidator extends Validator public function init() { parent::init(); + if ($this->enableIDN && !function_exists('idn_to_ascii')) { + throw new InvalidConfigException('In order to use IDN validation intl extension must be installed and enabled.'); + } if ($this->message === null) { - $this->message = Yii::t('yii|{attribute} is not a valid URL.'); + $this->message = Yii::t('yii', '{attribute} is not a valid URL.'); } } @@ -87,6 +98,12 @@ class UrlValidator extends Validator $pattern = $this->pattern; } + if ($this->enableIDN) { + $value = preg_replace_callback('/:\/\/([^\/]+)/', function ($matches) { + return '://' . idn_to_ascii($matches[1]); + }, $value); + } + if (preg_match($pattern, $value)) { return true; } @@ -98,10 +115,12 @@ class UrlValidator extends Validator * 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. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. * @see \yii\Web\ActiveForm::enableClientValidation */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { if (strpos($this->pattern, '{schemes}') !== false) { $pattern = str_replace('{schemes}', '(' . implode('|', $this->validSchemes) . ')', $this->pattern); @@ -113,8 +132,8 @@ class UrlValidator extends Validator 'pattern' => new JsExpression($pattern), 'message' => Html::encode(strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, ))), + 'enableIDN' => (boolean)$this->enableIDN, ); if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; @@ -123,7 +142,10 @@ class UrlValidator extends Validator $options['defaultScheme'] = $this->defaultScheme; } + ValidationAsset::register($view); + if ($this->enableIDN) { + PunycodeAsset::register($view); + } return 'yii.validation.url(value, messages, ' . Json::encode($options) . ');'; } } - diff --git a/framework/yii/validators/ValidationAsset.php b/framework/yii/validators/ValidationAsset.php new file mode 100644 index 0000000..625e580 --- /dev/null +++ b/framework/yii/validators/ValidationAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\validators; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ValidationAsset extends AssetBundle +{ + public $sourcePath = '@yii/assets'; + public $js = array( + 'yii.validation.js', + ); + public $depends = array( + 'yii\web\YiiAsset', + ); +} diff --git a/yii/validators/Validator.php b/framework/yii/validators/Validator.php similarity index 94% rename from yii/validators/Validator.php rename to framework/yii/validators/Validator.php index 677191b..ec9afc4 100644 --- a/yii/validators/Validator.php +++ b/framework/yii/validators/Validator.php @@ -35,6 +35,7 @@ use yii\base\NotSupportedException; * - `integer`: [[NumberValidator]] * - `match`: [[RegularExpressionValidator]] * - `required`: [[RequiredValidator]] + * - `safe`: [[SafeValidator]] * - `string`: [[StringValidator]] * - `unique`: [[UniqueValidator]] * - `url`: [[UrlValidator]] @@ -49,7 +50,7 @@ abstract class Validator extends Component */ public static $builtInValidators = array( 'boolean' => 'yii\validators\BooleanValidator', - 'captcha' => 'yii\validators\CaptchaValidator', + 'captcha' => 'yii\captcha\CaptchaValidator', 'compare' => 'yii\validators\CompareValidator', 'date' => 'yii\validators\DateValidator', 'default' => 'yii\validators\DefaultValueValidator', @@ -66,6 +67,7 @@ abstract class Validator extends Component 'match' => 'yii\validators\RegularExpressionValidator', 'number' => 'yii\validators\NumberValidator', 'required' => 'yii\validators\RequiredValidator', + 'safe' => 'yii\validators\SafeValidator', 'string' => 'yii\validators\StringValidator', 'unique' => 'yii\validators\UniqueValidator', 'url' => 'yii\validators\UrlValidator', @@ -74,7 +76,7 @@ abstract class Validator extends Component /** * @var array list of attributes to be validated. */ - public $attributes; + public $attributes = array(); /** * @var string the user-defined error message. It may contain the following placeholders which * will be replaced accordingly by the validator: @@ -147,8 +149,8 @@ abstract class Validator extends Component $params['class'] = __NAMESPACE__ . '\InlineValidator'; $params['method'] = $type; } else { - if (isset(self::$builtInValidators[$type])) { - $type = self::$builtInValidators[$type]; + if (isset(static::$builtInValidators[$type])) { + $type = static::$builtInValidators[$type]; } if (is_array($type)) { foreach ($type as $name => $value) { @@ -179,7 +181,7 @@ abstract class Validator extends Component } foreach ($attributes as $attribute) { $skip = $this->skipOnError && $object->hasErrors($attribute) - || $this->skipOnEmpty && $this->isEmpty($object->$attribute); + || $this->skipOnEmpty && $this->isEmpty($object->$attribute); if (!$skip) { $this->validateAttribute($object, $attribute); } @@ -211,11 +213,13 @@ abstract class Validator extends Component * * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. Null if the validator does not support * client-side validation. * @see \yii\web\ActiveForm::enableClientValidation */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { return null; } diff --git a/framework/yii/views/errorHandler/callStackItem.php b/framework/yii/views/errorHandler/callStackItem.php new file mode 100644 index 0000000..20ad398 --- /dev/null +++ b/framework/yii/views/errorHandler/callStackItem.php @@ -0,0 +1,45 @@ +<?php +/** + * @var string|null $file + * @var integer|null $line + * @var string|null $class + * @var string|null $method + * @var integer $index + * @var string[] $lines + * @var integer $begin + * @var integer $end + * @var \yii\base\ErrorHandler $handler + */ +?> +<li class="<?php if (!$handler->isCoreFile($file) || $index === 1) echo 'application'; ?> call-stack-item" + data-line="<?php echo (int)($line - $begin); ?>"> + <div class="element-wrap"> + <div class="element"> + <span class="item-number"><?php echo (int)$index; ?>.</span> + <span class="text"><?php if ($file !== null) echo 'in ' . $handler->htmlEncode($file); ?></span> + <?php if ($method !== null): ?> + <span class="call"> + <?php if ($file !== null) echo '–' ?> + <?php if ($class !== null) echo $handler->addTypeLinks($class) . '::'; ?><?php echo $handler->addTypeLinks($method . '()'); ?> + </span> + <?php endif; ?> + <span class="at"><?php if ($line !== null) echo 'at line'; ?></span> + <span class="line"><?php if ($line !== null) echo (int)$line + 1; ?></span> + </div> + </div> + <?php if (!empty($lines)): ?> + <div class="code-wrap"> + <div class="error-line"></div> + <?php for ($i = $begin; $i <= $end; ++$i): ?><div class="hover-line"></div><?php endfor; ?> + <div class="code"> + <?php for ($i = $begin; $i <= $end; ++$i): ?><span class="lines-item"><?php echo (int)($i + 1); ?></span><?php endfor; ?> + <pre><?php + // fill empty lines with a whitespace to avoid rendering problems in opera + for ($i = $begin; $i <= $end; ++$i) { + echo (trim($lines[$i]) == '') ? " \n" : $handler->htmlEncode($lines[$i]); + } + ?></pre> + </div> + </div> + <?php endif; ?> +</li> diff --git a/yii/views/error.php b/framework/yii/views/errorHandler/error.php similarity index 74% rename from yii/views/error.php rename to framework/yii/views/errorHandler/error.php index 009050a..8f2e509 100644 --- a/yii/views/error.php +++ b/framework/yii/views/errorHandler/error.php @@ -1,58 +1,76 @@ <?php /** * @var \Exception $exception - * @var \yii\base\ErrorHandler $context + * @var \yii\base\ErrorHandler $handler */ -$context = $this->context; -$title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $exception->getName() : get_class($exception)); +if ($exception instanceof \yii\web\HttpException) { + $code = $exception->statusCode; +} else { + $code = $exception->getCode(); +} +if ($exception instanceof \yii\base\Exception) { + $name = $exception->getName(); +} else { + $name = 'Error'; +} +if ($code) { + $name .= " (#$code)"; +} + +if ($exception instanceof \yii\base\UserException) { + $message = $exception->getMessage(); +} else { + $message = 'An internal server error occurred.'; +} ?> +<?php if (method_exists($this, 'beginPage')) $this->beginPage(); ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> - <title><?php echo $title?></title> + <title><?php echo $handler->htmlEncode($name); ?></title> <style> - body { - font: normal 9pt "Verdana"; - color: #000; - background: #fff; - } + body { + font: normal 9pt "Verdana"; + color: #000; + background: #fff; + } - h1 { - font: normal 18pt "Verdana"; - color: #f00; - margin-bottom: .5em; - } + h1 { + font: normal 18pt "Verdana"; + color: #f00; + margin-bottom: .5em; + } - h2 { - font: normal 14pt "Verdana"; - color: #800000; - margin-bottom: .5em; - } + h2 { + font: normal 14pt "Verdana"; + color: #800000; + margin-bottom: .5em; + } - h3 { - font: bold 11pt "Verdana"; - } + h3 { + font: bold 11pt "Verdana"; + } - p { - font: normal 9pt "Verdana"; - color: #000; - } + p { + font: normal 9pt "Verdana"; + color: #000; + } - .version { - color: gray; - font-size: 8pt; - border-top: 1px solid #aaa; - padding-top: 1em; - margin-bottom: 1em; - } + .version { + color: gray; + font-size: 8pt; + border-top: 1px solid #aaa; + padding-top: 1em; + margin-bottom: 1em; + } </style> </head> <body> - <h1><?php echo $title?></h1> - <h2><?php echo nl2br($context->htmlEncode($exception->getMessage()))?></h2> + <h1><?php echo $handler->htmlEncode($name); ?></h1> + <h2><?php echo nl2br($handler->htmlEncode($message)); ?></h2> <p> The above error occurred while the Web server was processing your request. </p> @@ -60,8 +78,9 @@ $title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $excep Please contact us if you think this is a server error. Thank you. </p> <div class="version"> - <?php echo date('Y-m-d H:i:s', time())?> - <?php echo YII_DEBUG ? $context->versionInfo : ''?> + <?php echo date('Y-m-d H:i:s', time()); ?> </div> + <?php if (method_exists($this, 'endBody')) $this->endBody(); // to allow injecting code into body (mostly by Yii Debug Toolbar) ?> </body> </html> +<?php if (method_exists($this, 'endPage')) $this->endPage(); ?> diff --git a/framework/yii/views/errorHandler/exception.php b/framework/yii/views/errorHandler/exception.php new file mode 100644 index 0000000..f0d3d6e --- /dev/null +++ b/framework/yii/views/errorHandler/exception.php @@ -0,0 +1,480 @@ +<?php +/** + * @var \Exception $exception + * @var \yii\base\ErrorHandler $handler + */ +?> +<?php if (method_exists($this, 'beginPage')) $this->beginPage(); ?> +<!doctype html> +<html lang="en-us"> + +<head> + <meta charset="utf-8"/> + + <title><?php + if ($exception instanceof \yii\web\HttpException) { + echo (int) $exception->statusCode . ' ' . $handler->htmlEncode($exception->getName()); + } elseif ($exception instanceof \yii\base\Exception) { + echo $handler->htmlEncode($exception->getName() . ' – ' . get_class($exception)); + } else { + echo $handler->htmlEncode(get_class($exception)); + } + ?></title> + + <style type="text/css"> +/* reset */ +html,body,div,span,h1,h2,h3,h4,h5,h6,p,pre,a,code,em,img,strong,b,i,ul,li{ + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +body{ + line-height: 1; +} +ul{ + list-style: none; +} + +/* base */ +a{ + text-decoration: none; +} +a:hover{ + text-decoration: underline; +} +h1,h2,h3,p,img,ul li{ + font-family: Arial,sans-serif; + color: #505050; +} +html,body{ + overflow-x: hidden; +} + +/* header */ +.header{ + min-width: 860px; /* 960px - 50px * 2 */ + margin: 0 auto; + background: #f3f3f3; + padding: 40px 50px 30px 50px; + border-bottom: #ccc 1px solid; +} +.header h1{ + font-size: 30px; + color: #e57373; + margin-bottom: 30px; +} +.header h1 span, .header h1 span a{ + color: #e51717; +} +.header h1 a{ + color: #e57373; +} +.header h1 a:hover{ + color: #e51717; +} +.header img{ + float: right; + margin-top: -15px; +} +.header h2{ + font-size: 20px; +} + +/* previous exceptions */ +.header .previous{ + margin: 20px 0; + padding-left: 30px; +} +.header .previous div{ + margin: 20px 0; +} +.header .previous .arrow{ + -moz-transform: scale(-1, 1); + -webkit-transform: scale(-1, 1); + -o-transform: scale(-1, 1); + transform: scale(-1, 1); + filter: progid:DXImageTransform.Microsoft.BasicImage(mirror=1); + font-size: 26px; + position: absolute; + margin-top: -5px; + margin-left: -25px; + color: #e51717; +} +.header .previous h2{ + font-size: 20px; + color: #e57373; + margin-bottom: 10px; +} +.header .previous h2 span{ + color: #e51717; +} +.header .previous h2 a{ + color: #e57373; +} +.header .previous h2 a:hover{ + color: #e51717; +} +.header .previous h3{ + font-size: 14px; + margin: 10px 0; +} +.header .previous p{ + font-size: 14px; + color: #aaa; +} + +/* call stack */ +.call-stack{ + margin-top: 30px; + margin-bottom: 40px; +} +.call-stack ul li{ + margin: 1px 0; +} +.call-stack ul li .element-wrap{ + cursor: pointer; + padding: 15px 0; +} +.call-stack ul li.application .element-wrap{ + background-color: #fafafa; +} +.call-stack ul li .element-wrap:hover{ + background-color: #edf9ff; +} +.call-stack ul li .element{ + min-width: 860px; /* 960px - 50px * 2 */ + margin: 0 auto; + padding: 0 50px; + position: relative; +} +.call-stack ul li a{ + color: #505050; +} +.call-stack ul li a:hover{ + color: #000; +} +.call-stack ul li .item-number{ + width: 45px; + display: inline-block; +} +.call-stack ul li .text{ + color: #aaa; +} +.call-stack ul li.application .text{ + color: #505050; +} +.call-stack ul li .at{ + position: absolute; + right: 110px; /* 50px + 60px */ + color: #aaa; +} +.call-stack ul li.application .at{ + color: #505050; +} +.call-stack ul li .line{ + position: absolute; + right: 50px; + width: 60px; + text-align: right; +} +.call-stack ul li .code-wrap{ + display: none; + position: relative; +} +.call-stack ul li.application .code-wrap{ + display: block; +} +.call-stack ul li .error-line, +.call-stack ul li .hover-line{ + background-color: #ffebeb; + position: absolute; + width: 100%; + z-index: 100; + margin-top: -61px; +} +.call-stack ul li .hover-line{ + background: none; +} +.call-stack ul li .hover-line.hover, +.call-stack ul li .hover-line:hover{ + background: #edf9ff !important; +} +.call-stack ul li .code{ + min-width: 860px; /* 960px - 50px * 2 */ + margin: 15px auto; + padding: 0 50px; + position: relative; +} +.call-stack ul li .code .lines-item{ + position: absolute; + z-index: 200; + display: block; + width: 25px; + text-align: right; + color: #aaa; + line-height: 20px; + font-size: 12px; + margin-top: -63px; + font-family: Consolas, Courier New, monospace; +} +.call-stack ul li .code pre{ + position: relative; + z-index: 200; + left: 50px; + line-height: 20px; + font-size: 12px; + font-family: Consolas, Courier New, monospace; + display: inline; +} +@-moz-document url-prefix() { + .call-stack ul li .code pre{ + line-height: 20px; + } +} + +/* request */ +.request{ + background-color: #fafafa; + padding-top: 40px; + padding-bottom: 40px; + margin-top: 40px; + margin-bottom: 1px; +} +.request .code{ + min-width: 860px; /* 960px - 50px * 2 */ + margin: 0 auto; + padding: 15px 50px; +} +.request .code pre{ + font-size: 14px; + line-height: 18px; + font-family: Consolas, Courier New, monospace; + display: inline; + word-wrap: break-word; +} + +/* footer */ +.footer{ + position: relative; + height: 222px; + min-width: 860px; /* 960px - 50px * 2 */ + padding: 0 50px; + margin: 1px auto 0 auto; +} +.footer p{ + font-size: 16px; + padding-bottom: 10px; +} +.footer p a{ + color: #505050; +} +.footer p a:hover{ + color: #000; +} +.footer .timestamp{ + font-size: 14px; + padding-top: 67px; + margin-bottom: 28px; +} +.footer img{ + position: absolute; + right: -50px; +} + +/* highlight.js */ +pre .subst,pre .title{ + font-weight: normal; + color: #505050; +} +pre .comment,pre .template_comment,pre .javadoc,pre .diff .header{ + color: #808080; + font-style: italic; +} +pre .annotation,pre .decorator,pre .preprocessor,pre .doctype,pre .pi,pre .chunk,pre .shebang,pre .apache .cbracket, +pre .prompt,pre .http .title{ + color: #808000; +} +pre .tag,pre .pi{ + background: #efefef; +} +pre .tag .title,pre .id,pre .attr_selector,pre .pseudo,pre .literal,pre .keyword,pre .hexcolor,pre .css .function, +pre .ini .title,pre .css .class,pre .list .title,pre .clojure .title,pre .nginx .title,pre .tex .command, +pre .request,pre .status{ + color: #000080; +} +pre .attribute,pre .rules .keyword,pre .number,pre .date,pre .regexp,pre .tex .special{ + color: #00a; +} +pre .number,pre .regexp{ + font-weight: normal; +} +pre .string,pre .value,pre .filter .argument,pre .css .function .params,pre .apache .tag{ + color: #0a0; +} +pre .symbol,pre .ruby .symbol .string,pre .char,pre .tex .formula{ + color: #505050; + background: #d0eded; + font-style: italic; +} +pre .phpdoc,pre .yardoctag,pre .javadoctag{ + text-decoration: underline; +} +pre .variable,pre .envvar,pre .apache .sqbracket,pre .nginx .built_in{ + color: #a00; +} +pre .addition{ + background: #baeeba; +} +pre .deletion{ + background: #ffc8bd; +} +pre .diff .change{ + background: #bccff9; +} + </style> +</head> + +<body> + <div class="header"> + <?php if ($exception instanceof \yii\base\ErrorException): ?> + <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEwAAABACAIAAAA/NwYHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjU4QjIxQTgwQzU1MDExRTJBNEMxRURGMUEzMjA1M0UxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjU4QjIxQTgxQzU1MDExRTJBNEMxRURGMUEzMjA1M0UxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NThCMjFBN0VDNTUwMTFFMkE0QzFFREYxQTMyMDUzRTEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NThCMjFBN0ZDNTUwMTFFMkE0QzFFREYxQTMyMDUzRTEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz74UrDRAAAOWUlEQVR42txb2W8T1xf2LF5mvIzjOJvjxFmbQMJOSGJqFpVAKX0rSOWhf1sfqlbqS4UoS9WWlkJYhQQJtJANsoeEJF7j2Wd+Z+YGM7bHWxKk8LuqKseeuXO/c8/yfecOmKqqlk9hyOm0RZIIl8uC45XeS34aCGU5dvUq/+QJ2dmJ4ThGUbjV6j53zur1/v+AFFIpfn0dPkgTE5kvrQMDJMNgGPaJgVQkKXHjhsJx7vPnrW43+hICip+ft7x+bbxSam6WJAl++sRACu/exW7ckJ4/1zCk01XffINwKorCr67mX4/7/Z9STMKGsG/eJG/dksfGNjfqxYuoxQI4SZeLm52V5udz9gv+JAgis40wQ+r+fWFmhjp8mOrqytneXQFS4vnk7dsZhBmc64oCmVQWBGxyMv8u/uFDR18f4fEIKyupv/7io1FwaSmVwlwuR2OjESe2G0oIz/Nrz54JDx7YIPYqGURXl62rSxgbMxqI2Lu36uJFm8ezu0CKopiA8ccf9idPdmRCa1+f7+JF8OfN6N0N7kqSpNPptPX18aHQDiSwYFBkGCitH1LUbgAJ8WOz2Vx+P1ZXt82puEBADoWsR44YY3JXgEQ48fV1cmVlmwjV1lZXOOxyuTK+uovqJBRDbmQEn542z0x+v+TzKVYrznFkLGZfWzO3lNdLDw56PB673Y4bKC65nWUlh4eF7LyPhvvsWXtzc3EuAjGz/v33lvdpT1UUUZLyb+Crq2WfTz5xwuZwwLrhLuB3yr171NycSb5Jp2lVzUG4LZDwPC4Wk81Asn191mDQ6DAm1ywtiYJgJGumJlEZxvrll36GgaWD1cCyLMPEKYq/edP+9m0uB5qelpeWLHmBva2dlDjOdGV8LFa8MsG9Isfl0FHTgff2+nw+yL2ZzYEUBZ+jLS2WPJBaWM7OUoGAo75+Z0BqMFjW/CdRLA4SfpVisdL1s6aG6uykQFgZA4wkaZreCATMZx4eji8vJ3U5Zq2pcezfb6uvJ4vbO37jBkaSnrNn8TypqmqMkzX3sVTKFKQYi6Xu3iXcbkc4rIngktuI45TTme/28I0VKmGhOJqYQD8JsJL6emtdHVkE4fovv0g6/Zd//tkD2idboWowJMncnByXAxJR8NTff0svX+IdHZIoEvv3p774Ql5ZIZJJDFzXYqHM3E9ZXra811zGemMpwxE0AkxR8GiyCEIgvihseIslmk67Tp+mWlszOVNeW4OUaA6S540g4fPG6OjGo0fy+Lg2+eQkNzlJiKLn6FFgrZI+LKOj+TFGLC8Lk5NKW1vOZsLy5JmZ0t7e1OTweGDBZEmEmyZ5+TIpy0p/P71vHzcxwY6OimtrxOys+fSpVPyHH9imJvrAAfAWUBjs+LiSrSSk168dDod/cHCTu4ZCIJRsi4u5C/3vPwg/9/79RlW18fSpurRUUizj4NVOpwlIU4SbWzc+npCk9LNnCssqZtonK5VDkRgbg3XjVVUi+HzebNj0tIBhsEWeSAQSptzennj40KS+Ly2Bkytra+5wGLfbwbLgEdz4OHyff3E6GJSqq5E5IJ4dXi9kKROQkiCIPF8wuYNgq6iWvs8B5u4kSXg8DvUWlkLyPF6ors7Pp+fnOfBnWL2qKoXlGBYKgWhGvg3YIC3DxCYlRMUwdXAQBKi9AMPaSSoHDDMcBveDrJu+fp2Ymip2cSmpybe0UIcOMX4/Aobpwxykpnqqq4V9+3hRtC8sfDyEbChkGxoCYyupVOLKFfnly+1S/MZGl8+XU1TNyQDsNZRaqbc3lkhAUv1IODkgfeEwAzGDYfGrV6X//tsBUbqwgEejWF69MZdasJkgVdz9/ZaGho/S7GhstBw8yLS1gTXBqJAqQOZuf1rIdul79xSzqoYXkrBut5s8cuSjSEeGcff0OHUqo4VNf7+abf50S8va2bMbnZ1cAe5WMJNFo8kHD/LJFllIwlphzM0pZW5OTQ36YH/3rnT3KZUiFxdJPdcjr+FPnGDTaWpmBlBJwML6+yG2pK4ufmmJf/AgJ2RY8C/QXG63arNZV1cdhtKKvXnDkiSkH7vTWRZB11jF5CRWCptM00JHB9HUBEVMSSaFqSlyZsaUoH0AOTsrADfo6cH1ARrKU1sbPXYsDZaC5NHeDk4EX0JpSdE0+/hxbv5vbrYADKsVVqjeumUxgIQEaxsYUPNKEVlIJXBTU6ogFAEp1NbKLS1EX181TQN3Ad+TamrS9fUb3d3pu3fpojJKWlmBskHqLXC4EVzX0t3Nt7aC90B6RGIK1qBJ1rx77XV1TG0tXAM8KXbyJKuq1NgYuIDidmPhMB0IoLJhDlIBVi0IEsvKq6uaNn3xAi9cKjWEoZAzEkFWR8QC7qL1ET19GkhpkcwMfpV8+FBpadGYlx54ToqCG9HeIsqidX3gc3Y9gBRF1dXBlfBEsIilvn4tEuGgtoNpIhGGYZxmqoUEg6VfvoS8BGzbeGZUInnY7VQk4vV6jb0G+AAGRs9YP3xYEEVb4caUdPt23LgO/UzO8/XXtmy9m5snbTbYSfREVO3Aw1MXLiB3QA5lUi+04xSOE826GMVGczPaw/zKi54n7t2b1o9uyj0p0O3LDw1Zsw+q1Koq2ChFVXGIHUhUPh9yZmNVh2WgyocXOJ/VdlKUpErLAOH3Gx+WX2khtNI+n6VCbghhlmMv+9CQJq8hxwB44NwuV07IEfoowRNQp6xiesEwRaZGtYGo/Nw7p8RB1EFEgMtkvif1UfFqt1jRNzbKaRhvl8ToQb4DZGiLXSz9cLvEiEZ3hCGpHLddWrvFB8/MIB1YsMxCIimjVVXSe9nJyY3btzW3gCpls1Vdvoxv4e0Pjal2dKx/950AjEwQ8EQCwp+MxSyyTBeWcFBUk3/+aT1/3vSRQiLB3buH5/UyctiZAjrL6QR2BnXcAoXO76dra8E6hN1uC4U2W8nLyx8yf1sbZCaUSysDCat0ut1ES4sQCMj60Dq/oii9emUpDBIACDieoCjPqVM5OMVkMn7tWvH+iMYlBgetDQ2EYYBfSHfupO/cIbu73efOOYJBWIlgeFtAY5qyvBV3BYOBbSCPgW8oeqbWOr+SFMdxfmLCbnbkkOlKsBgmLSxox/QdHcBBpXgcaiP7/LlcVONro6qqqrMzI0RQYYxfuSLoJAnsm4CE/9VX3MyMsrKSSV9aFdALgfaGwOPH4tyco7c3/w2BgjGZ4RCZ3K1UV4sg/IqAhIfNzQnw38hIKhRCDZhyOoWwjURHB4gPICiZ9QmCIMIuvfcdeXw8qvcvsWxmn/zpJ+nQIfH1a1BV4Czwf6K62v5eA1WWeDYJWk+PxayDZtKwKgPbh8oChL63F9HdrH78gQOwkzazNpyxY5jSwW/+OTYWv3Wr+tKl4nygYKbSgqRo5ti6aOZ52JOcZWkkqb5ebWmpaCrg6zJFlQxUvGDufvVKqZTQlpkGdCnMz88b+Q1gBu+18nwFjaJAQIXUpbenK3ZXrcotLKTv3y/naG2LBXB6OnHzJnH5stXtBqmNsp24uIgtL+eLcj4YJBIJ+9u3uW0HhnGcOKG1aUoRPZOfoQYkfv9dNrj+xxjyxETs11+9ly7FfvxRep+Nc7Kk0tCAf/45Y7NpB8yvXmEjI0btZmNZFygvMyVUAqQsivGbN+Wd6BGW1hwrK9Fr1wqd5AoNDbbjx701NTYdZIqiksmkxQASND3/9CkdDltAVVYEUtKZAAS06ctRQFNUqGyyTBXNgVnX2+2QaUyvxxYW5IWFgjUOHLG6Gp0xo7chBShpOYb47bfVqSlgSPauLtfAQKGCSearJPzMGfX6ddN+qRIKqb292NQUS1EWli0CVcsKDofc3o61teH374PwdWTnatSYgwnJ8XEMtkiScmYD6Z1xRdQ9hNhTzNwecqvi9zskSWuIlAMSJgILimfOsIpCGbIrFwyq+/Z5envhwWJVVXrvXs6sX7i5gR0dUjhMeb2M3oxJDw2xjx7xQK3eX6w1VBsaHMePw7KkUAh8RwS29Pgxbei/YKurKssCp/1QSAs3jWSHo8gBPplPA0DUe32+tUgERASt42Sbmy39/Ux7O9AUq94LhA+m/cLNSWm6KhiEeeBiRBuJcDjl8Wj7OT8PCLFAwHHyJMMwaDYIOZZhksCuDCDxubnk9ev2b78ldMrJ/vuv8uYNlh0LmVM6u8NRWXaFe2iaVgOB9UiEBRMSBD4w4K2vRwgx/VARLY4r4B6400nrfUr0J6JvxIEDcZeLGxnBPB6E0KG/mpOpk5zPl7s/IyOrqZQNmHoyCSIJy9nJhga5r09r71mtlMdTJMeShegORDzW2Ji8cAH+BHiwaONLtChO1AL2w/TzXeOVKLqIzz5jm5pQ19yIUJMEsI1mjS/g+mwBum/ds4eprUV217othZkdWYTWZRphps0iTD8zMQeZzUszmcPj8cCcaE1GhGlgpP/8g5XULtnD4XbnvEO3lc5A8UaYBqOAfsVdrvxsjvw8f0JNNLKsCHq9UjoxMmJpbf2YjSxUbGha0KVtjmkcFIVV0sXCW1vlxUVRP/ZNB4N8ZyfwOHJ5mS56uqy8fbsxOuo+eLBcPbm1Vpqjv1/q6VGz27Yg0qjy/rnGh3kcDuro0Y2NjXRtLXbsmJemId9Kw8PG1kT8yBEoNta5OdvCAiq5IPAlHAcTl9V33dqAqaGiank4u0Ch8Cu/3ZQ5DsVOnUKnKZAIQENHDQEPPITas4dqaBCbm/nhYSi5CmSaSIT82O66U03RTPpFYgJsl+FxHwxqt7tDIUgzkiQlTp9OLy5anU53TY3pGwI7CXJnBzrPyvrG72e7u1WOI0SR9PlQIwpM4PV6EaEFE5fZTcd257+6gx1L6QP8VusnOp1AHjKOg9ZcftjvUpCoYwjpBy0PNSy30FZG438CDAABlQXh2BuRRgAAAABJRU5ErkJggg==" alt="Gears"/> + <h1> + <span><?php echo $handler->htmlEncode($exception->getName()); ?></span> + – <?php echo $handler->addTypeLinks(get_class($exception)); ?> + </h1> + <?php else: ?> + <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAA6CAIAAACPssguAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjcwRURBOUQxQzQ3RDExRTJCRjVFODJDQkFGODFDN0VBIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjcwRURBOUQyQzQ3RDExRTJCRjVFODJDQkFGODFDN0VBIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NzBFREE5Q0ZDNDdEMTFFMkJGNUU4MkNCQUY4MUM3RUEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzBFREE5RDBDNDdEMTFFMkJGNUU4MkNCQUY4MUM3RUEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4L6mYsAAAE9klEQVR42tRa227aTBDGiwkhJtD8EanSQ6RKqKpSBbWqql5U7QP0TfsSvWhvStMEO6cqDUpjRylSQ1QC+IQN/mdrhBIb413vgsheIDDr9XwzOzPfzFrwPC/Fe8Cauiybe3uersNPQZJylYr04oUgCNyfJXJfcTAYXH/5YoH0qjq66LbbTrtdfPcOITTXALDuf/40FSWlabeuq6rpeZkHD6Ryma8dOOvDdV19Zycg/XBoml6r9ft9vk9EnNV/dOT8+RM1oXdxof/4wdfrEF/1G7Va+uIiagL8BfaBafMIAHy38/17/+/fydNgAkyDyXMHwLEsS1EmqH9kBOvgACbPFwDQaPfr11S9TjT7+JijEfgAsFst8/CQYv7BQe/6el4AQGTsfPuGbqSt2CGcnrarVS4hlRUAxETz9+/eyQntjXAL3MgeUhG7+vXtbSr1Dx+sqoYss3sCYlS/cXLinJ8nu713emqoKqMRmAD4xAElBQA3GpA62DwBsagfiIN7ecnyeHZyIbKoP4o46KurY2+Rrq7CeQ08YenZs0wmM1MAI+KQDv3VKpd7r1+L2WwQsGH0q9VCyN3dZrOrKPdevUpGsxMCmEAcMqVSaWNjcXExcN00zRbQ7BAAWMSs1ZY2N7NLSzMCMJk4ZBFaXl4OAxBF0Yza68fH3Z2dzNu3Ceo1lMB3raurScQhgqjBDhGiibS1v98zzVlEIax+CJ0TM9fYqAIXB9HRBsiFXq0myGuIVv2xxGGQNCaCVcG2tCGVDkBi4jBU80ThYFlwLVojICr1kxAHFLHRsWoNgzu5oABASBw8247ygVScZJhcyDIVuUDk6tf39oiIg+exUAOwMNiZfAVSAI7jGPv7sSUvhwLl/Jyqc0EEwA+dsR2H+EFWy4OdwdqERiACQNhxGIaaaCcWyDY3Zni7u2BzPgDoOg64Yo92YvJ43e2CzUlCKmIlDsSZ2FcG4Qq4faQoJO0jRKJ+uszFq+tWr5PkNRRPHM7O2IWBpdxGYxrkAk2DODihbAqKdJpNkRIDJhdxniDGEgdavgpSWp8+XX7+HADg9vsJCnDcPnr+XHr0KKpeE6fScdA0l1P7AIwAu2BxfR3qIYotREEcpj8mkwsxkjjs7iYmDuRdCXJykSuXx3YuxEji0O2mE0lvlEpWpZK6vWsxv2s0BCjeE1kVk4ujo2KlEvYEkZE4jMlBCK1sbUFdf7NCx22YlRVHURKuCeSiVpM2N8NGEFmJwzgAIH2hUAhYAD6vGQ6J/bOpe2/eBDoXiJ04jNm1CAVsDT8Zj7ijyAViJQ6zHPV6Z3s7kNfQrcylqlCVpuZ42IeHgbMpdJM4QD2auFc+mxE+m0JUHQfSqHd2Fsg7mMxxWjxwNiX43yBzNT9+9GSZj6I2NsKZH5e5Y9+hSODQ79+vfvjgP0K8SRzSvCw9jgvxJBe/fpmaln/yBAe3WXYcOHYuRmdTCFctmsah4zBjhtdo+D08BGHVhs1zd9Q/MgKIDcJjCwzyefv+/dRdG86/zItzvri+7knS3ZLefPgQPX7sMxSUy+W8ly+NtbW7Ir2+tjaoVHL5fDqdFgFENpstlMtQ/bdlOc3pJZLpjX6x6G1t/ff0KegdH1v96yV7EEk7nU6r1bJtm+PrVPx9FyFQd7FYBMa+sLAwBODnMgirAAM+p/EqLDcuJAiwbaCsgU+fsf8vwADjle3ME1OGEwAAAABJRU5ErkJggg==" alt="Attention"/> + <h1><?php + if ($exception instanceof \yii\web\HttpException) { + echo '<span>' . $handler->createHttpStatusLink($exception->statusCode, $handler->htmlEncode($exception->getName())) . '</span>'; + echo ' – ' . $handler->addTypeLinks(get_class($exception)); + } elseif ($exception instanceof \yii\base\Exception) { + echo '<span>' . $handler->htmlEncode($exception->getName()) . '</span>'; + echo ' – ' . $handler->addTypeLinks(get_class($exception)); + } else { + echo '<span>' . $handler->htmlEncode(get_class($exception)) . '</span>'; + } + ?></h1> + <?php endif; ?> + <h2><?php echo $handler->htmlEncode($exception->getMessage()); ?></h2> + <?php echo $handler->renderPreviousExceptions($exception); ?> + </div> + + <div class="call-stack"> + <ul> + <?php echo $handler->renderCallStackItem($exception->getFile(), $exception->getLine(), null, null, 1); ?> + <?php for ($i = 0, $trace = $exception->getTrace(), $length = count($trace); $i < $length; ++$i): ?> + <?php echo $handler->renderCallStackItem(@$trace[$i]['file'] ?: null, @$trace[$i]['line'] ?: null, + @$trace[$i]['class'] ?: null, @$trace[$i]['function'] ?: null, $i + 2); ?> + <?php endfor; ?> + </ul> + </div> + + <div class="request"> + <div class="code"> + <?php echo $handler->renderRequest(); ?> + </div> + </div> + + <div class="footer"> + <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAS4AAADeCAIAAAAMx3q5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkE5NkEyOTAzQzQ3RDExRTI4NjIzOEE2RkU5QTc4RTU0IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkE5NkEyOTA0QzQ3RDExRTI4NjIzOEE2RkU5QTc4RTU0Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QTk2QTI5MDFDNDdEMTFFMjg2MjM4QTZGRTlBNzhFNTQiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QTk2QTI5MDJDNDdEMTFFMjg2MjM4QTZGRTlBNzhFNTQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4DYCAjAAAx4UlEQVR42ux9aZvjuJEmgqTOvCrrrmr3jO3xjD37/3/FftiP3me965nxbl/VVdWVl1IXyVhSJ0EEgABJSZQEtLoyUwJBisSLN24AIgohlv+W2vLN6f/47/kfkP0nlr9s/6V/gfJA8hsAxJvEgUQH5Q/QdgD+UeQ75BexXk/2I+oGly/h+g0EkWA0sJ6l9c0wee6m/xjPv9hHaOpKTMMhsq8Dqwyf/9OLrt5d/EX3ZM3POjLfSjGbCd+cZkM8S+5+gcfPwdUbDiCX9/l4AWlaxJP7cfw1X9HQAkAor4VYDaVFtigfiNtbjLpR1z0AyVPDaiDy2hbvTpPHBGchdJc3ofRY1XeKLbDc6Nk0O3b5EuRrex2Lv1Vmgy3HSOOU+AeAPIp6B1bPDrT3E/hHgYafDdgAamT5I0yT5P6X5Me/pnc/YxpXm9BHjUMUyd3kv2zAAWF4kPrnw4QlVB7TMsVM7W7yY7WbGZzSzGhdS5M0A+RPf03v7YDERTsZYfVu8o9UJAToOC8FpcVlvAIglSFhQw1MQAK1gpDXM5p/wfUXVx+o4REHDsSvuxNg1LjMfQAsKiKTD4ufgMKM0AQfqsOWnhVoZtQGkI+/WsF2RIA0XGcmmk6Su7NdgZ9mXyocFXgC3B9Dfvsx+fmv6fM9B5BHLZo+TH+AClxIiq5QXEm3XASyxsOnRygvpFB8GY6kTlS8nm2Hx9mnCg83sHc1kIkkQQiTlqiyEJgUS4bGqOdDIZsDgKcrWPlQpxmSCipxyOIVz9Ov/5l+/jvOxma8HZ28urna0fzXBKfnvOrG6XSaPLquqgF3eLNqXV1Brteg4mcEDpsYhjtrp0/Jp/+Fdz9hmlgBeVyiaYKzp9nPlR+mhTAJniSp0vLQCIYsTGXtCEUuJrRHab6MjDIqeQMtUITBkJ6Oxe9h4EPZviwqA0dPoTqxwY0PWVfF40OD4i93SJ8+J7/8T3z+dgL0uLnCyjjcwVp8iHHWgz3Hd67rqY0Vw5ASR2t/T3DssMNnyceh8dqqXXCuQP6/9MvfMZ4dCz0armSWPo3j3ypriE6EKUxUCWbfGT2yk4mVWO2l4xHj0fyrm9mm4eUE7JqXXYIAmwpnZemafAgWHKq2dp34Y5sDS3l1lP76N3z8ZCbA1tLjQSgRGu/Y5PqfjzSef3Na0aJ937D6fNiYTALO38HOh8C6VOXUudL48AnGD8Ht99jpG2IyzBEbB6TEefo0T56g4hTB1f+CDmUxPxjUDrn9uxiBg9arKYywJkbqmy/XZU1ozsZyw3xqQZNIO8gMoS26VS/MHKOx64k+Hyef/4ajL1Z6bCEljue/NbpyO0iwoNfTTQul+8rtdHyKyTh2cK7aoQiX18R0N7jmtZdsM24RrggwG4QAbBhy8luYjS6ksQw0diRdcJ9O3i4ckt7/lH79D5HM2yasGs6Y4mycfBUVnImEw5DUDFlAADsAy2qkaSZSegiw9MZVm8aPhrtXeic42HLKUaaO0RbXSJs+pZ//JiYPLbflFHyJn9sxaZx40+VkZjRqJtE4/tYoK4YhtVZo74+uV1n6MH1pOx/S4swu+LD0KHXeLsPD5gYPFO9eHlGefvsHPvxs9j3uDY3mE03irzuFX+Xo0/orMDier9g/TmcJzpqD4tK16NtBaGf0Jf36d6uwelhKnMS/oUigjhOD1w+EKeCXRYfUSmqNC5fZxejnUEKeJwUZ1fzs3KNtrCI2Jweq0mpGRXszAuisfAh2PtQMa8ul0V0bkHpv3ohD5pP0y/8W04cDqo7mwafJvV80tTcnfmDeRrszAy6u/A09cEuT9Ld/wNU7cflW6POM9+nn2MyqFGcLKDqfVz6ACi7XLdyojeBCxY9Bng43C2WhL5ivRu/noBOR136OokvD3BhQDCNt9FqBKukuVu+82dYMBh4FniAPPOcEcSZjyJ7u7ODkCwHdXaL64dMnEY/h5nsUQRvQ6CmR05J0tpDeQ68rnpbqOHlIf/sPEU/2qTqaB5ylT03ZZsjkKHOii8EdYlAjNQlTW9XAoD1KfaU0K+2ZJjyXBktXhG6vrCwBn0A0zGDNCa4f3aa7n6CpPcWKYtednXp8lPJs0QxNNoe16rh3NOrGz9b7aexZ0dLmyTOnGw+KnZ7pU/cAsr00l8uqH+ENTr3q3Ys0Tb/8HzG+M1hr9mNWXUa61Xk5EaaWJ4U9YMDMkKSlrZJllThgnjYIxdxyA25z0bzes6cy7aVkRnu7n5UVDQQVgQhWfVJLhoQNP334AUe/GlDXCBr3Ip3ufLndI01AZVaMMjHJ+sxydbFOZGazQeF1SJgZkQgufMaKGXCz0HBx8vSrSOZw/Z3OWrMLK05xtmSsWHMWy8UJyV/NByFlGi29tVrMC+HmtIkVS2dAajDN1RQsq3l8RrFnzPPyB5TAqXBFf0gTBxiyhJi3H3TTFoxGR8vZgO27ZMU4QhUSVTsBMC+KVk2pDjj+hg8/7pobdS3G8aHpDpiPTujz7rVjATfThlxxi51LWRrkM+JVsO728hziJGHdL2j0IYC7VsqjKfpDNz50+foAO5mKCzSKeAK3f9A5OXbk4ZivpFPzyFj5S4NytJUqgaxlXEimkinO4oSU3Y9IkCd5CEg/Vto92mvhcqNtgv5wV6vfAeOwYRfQcP+OdarnLisgx2P89p8C02a50ZxMEKdjMKZTwD4fEFPZ3+LU8YYAOChPCjfO03FjUCzH3ICmtKk1JttM//zOTrYiXb22ynFthog21Z7HqmSukUgtctn2PBhP8K55NBqBmjKQAeRrx+s2cCxwsPuFoXCvkp1BsdrlQY3VsXL9m8bMrU3PnaYL0ON8r2ic1zCfOuATKoWBK6548u4BcQL9nS7gl5vruP4xYxhR2QLqxTVh7CAyfdlmSWOuLwjHbEYnPrSjwp0PVWoE4ZbQbA8u16ZpHQqNTS9c1Qlzp9pD40Kztvgic/epwMeFH0Pbm6Q6341TkV+u22ZdLtOZIVVqqXEvrD5gMZQX9AI+N06TJ6ta7pDFD8Mr0zLCKiJe5E89H1TIgXIXTbUFxU1xbdaEZlv9eKvvnvFgdVNzO3kzNN7/V000Om294lv95gDF4PrW36+jafMxPvxfsUt/IzT3ctIqDddhHFNT1QbIyDUONwpHbmwOitAfQhhqItGqxrhVsJpalTdhiTU31S91ivMm+JCvMdjmjpoCQKqWOmtr9sH8GZ9+2hEaN/uW+cZscTplQZGpLsLVbXOmwEoSpvNR0Gi3pi6VGf5W+6omdztCY5JONH4Kw4tZKcONJ4W7vE+zGS8EqzI3pjhvjBXz3lcv6mqJ5uh4p5hve8Q58AQG/W038q3Eh1YZzn46E2Wro9FMqCZeTe7E5DdxoMi4ystVK/fC2O3p3KqD1yuuYcu7b5ZYBLOaziH4EOoO6nROfPoFIBS9G37UOAef1aogoul4qc420B8TJFnogOr1ATH29hPYFiYv9JYix4n4ODkyTpQuu9ra5saKEEZwfbsrLdHwmdN+lrtfGCuF0dhwaFMLNXKGvtJxgUjx6UeRTBvkxii42BVvHCIQEhroAjW/QWBQDmnv4s0rEw4bID1w/OIu3aDRiB9wl7mg+jShJVL2pefuDR4ady2y6jVDIIxR2nBCrRrpmO5DaS3a/fvMeqMJjWgjS+fq4MHVrQgid9IDwdeM7DgE13lviWG1xu7U2nXDVgHEVhhSQ7FMBXXdDdPchLMIHDWg8SDOQzBpxs5LMVTJGbJ47bnlF6A6N1Yp1E8bb5wojF84nDkF1aLqfEmoctrEToUrqL8YKZceT5YG1WYuEMJdAJJWTvSb/doYkrsqltgNKDuZnRvZooq65FWC4st34swb1OTJ3ShFnHjK+aMYf2lECo1gsAchlq2CawHZ4MpYgRsrsiLXu9gfQv/CQYQDHu/ZnfXOkxEMqgCnkLluzeQalqzhb1oXm76ivOEu2dx2y/V4/FnMn+qjMYDuYRY9fciNExphj0LMrgTU/LDbt8K3o225mJrOa6IxgA64u/mhajmwsgWHWpX08od8aipWTiumsvV3VXx2+r4VoQiZuhiEDIONtjwImdZlUyl5tAmOBVeLD0szTkGpZ/tVAPTXpteJDL57gxXSHMCiXltuwvnBbMKxthD6flFrsAVWiZSWUcNI8mr4dnQtnornT7V0xWBQLfpbpkhntlRWofKq52bLMQXKSYUS+JFx5DreC692woqS8YZZUMO1ggsnxs0a813b/ShHt3GMt0CrJwb+V6dCSaazqIVWvxsQdsiMGmf3dZTGTFfMZFS/pu2KFTX2FIoYOz1PjEevNI5qKY0RXNawxZTNU+uVx02xlAhKqu2t32TQzc8BuloaumxjYld4iKpAkV+oL3j9UU+JYHHrO+VyuRitHHOgRHnvR372E6kVW/RD6+PX68O2cejrA2PRqkxdHP24+tV9s/FO1fC3c2udYGBFWVDnBDkxXr/eybXvLRCx0XLdDXSGpu4VN34Qk4mYfOGgkZph140mDYNa+amoUjIVSEM8QA1uFOVQmm16FIIS1FZSN4JqrMg33iyI8YNdSxR71RLZJwIBNQiZcwFm8ybpNuQQb2n2GQQ/+7RdIDCDYjKtgEaAMAwGos0NWnEVHcZdCup+01xjfO18O2D3984aAQui7Q32N8NwtPJtuKKxG7yo+RVtScPblcWJIcvB5Q1xoy512Oxp5IRDuBTU0BHjqw8cxUW4WjXtc1SfmAzu05auQMWjKX62l3DhQyEcUts5xc8MKmUab8RUJzR2g9u9rT8lCDU4KtROTjXHxEVBDSjyjTc5MTKCb2BvzwqaOi3U6gXVNThTQY0Kdi5gZSvg9JuIn4to5KRNZTJqJ7gBTatWtp/DkMJWeNcQpa2hO4eIV6NeBRQUh9Wh6EiMH6XgG992Ef9tgFND8ZQ4/kUIqfw+J22qG75whBj3chlrCAijvOqUMwHcWHM3bgxEGDCyWIIKwCN6hlGYoZGzbPP3zLBEZoN9gmq9C2BxYNhtRVYc0hF/hj1uLZYew7QGlm3GZN3ZqFTpvCimsm0S14HouquCh926aKfnRvn+sLZ+Cpo6eXD7DnpD4ZuLjFGbD6uVjSfG3P6aiamJ866JUXDFNMsocUQsnjQuKZi9DLYcg1pOiqnA2kZTSWvcfoClzp3wsgEo8r0aWQvf/97BpVFZ56FKHFSPF4BqYNCY2PRGN7bfwrq/pm2rKUPgWzEuRQ+CePq1JKZaWy+sGHcFnFoLlo+hTsWcPdTa6fD8PY2xYn7NvWHw8qNnu6Nn63SWzB/dZCLohtUyiQmqBKZ4TS1QPL1R4UaLh8MSE1e6rPLFc0JtsnfsUHQixuDFW+h0tct98Zs2YhUUNbRE+jZar83Mh0R8dkU+BP5+HuTUJRRC0GRuSde2uLxkfo+2Erpl403w0q9ihKgIvTDo7ZAVdWjM7Tcf/sU/gKNuwcLXn0x/czXe1N8ho7QmAHDscg7cCC7caFc0LEpJ/rPDjkYK6gBPL6Z+qGVpcNk9qn5UeXMGOah6kIt+yNUMLfKBZH0o3/DcAJim0zR2EFMXDka/wZGyQoWXTULRFZ/hq4/QHdaY04dD0/677f6CwfFDWDsS49m9k/1mYUe18yLI1Zetpe1WmVMADJNUmRvBWnHIFE/ATGiWizvIo3UbhyI4Rk6HH/4oghAYG7FUYZ7mtMTtYGD1e+lygrVhayYt0RYpzuJDodUMTWqhuaC4hMY0mX1zklF9MrGsKHY5TsUluOolSRmeZacXvv19FX6A3XcQJA4bH7T+YPp1Z8eUu8n6SeIR2nYjk4gRrlxukxtPummPtnzT8rKtVQWZ1lRidJIStSFrNYFnsqZevoCXtkhx6z6N+9ASnePFLQo7J9rbPr4Gh7a9yIweANuVbJl1Gy+SzO74t7IbvmpoLdiHdM7vB+xIPfluOFQ5aNKvSBD0y49w+cK+xnOpqSorQINj7VFtbcr9DJzN6kufb6G4sN+MuPMJumHQ122pCKzVyJLAo3ggmWualUidrak6ati0XvhiV1B0Jcb8BG/+WWwC4qDO1Kyc5gv2aC/rw2Sa05nVIik2q8SH8uykp0eFMgXlt+JcY+Tab8wORpdgP+amDTyMG5kNak9Q9ftFwSBw2csgcJ/YbmhceBr/TbvjTeNcAtxHvxMtkRO7ZZMNnPmw8u6R+tC5AEvAS/nxNxFc0zxn2SdKi0be7iG2XXVZu7SbZHhebOq2DcKXHKRs3gn2AZAwDD/+6+lnUUHLTtro9STzO8SYt1iHCzRaUeZg2oGqmg00ekOcNEYn6bQiFCuIqdAbVkGja9nvhqeji2PdSmaG6Da300GR0U3bgPO3YSXKrqo5ipDM75k3Lk8mVqtZGLW09pc3cWqdYBAG3Z1DsTIac73Rt6Ml+DR3bLACU6MgY8WwSCZyaVO6jDLY7OXV/Bw67z+jmy2LSm826ofOEbnBPh9wcPFii0apcDjwNzAGvhm7Qn4wML2CNhcC021Qhw+ZwQZCY8RkOPoJMXXGDUzVZjCKusYTaMGqZG2kdGrOrGg48M0alBNcvfLceLwtTadMj38neGnb6ZviScW0w5DyWdwo91tmGxtWSOD7NtRPO+Glq3QqdhFtw0Ljuz+y9UarEqgPmK+5AIJj2Ui+lgiu37seH1YiT9DsHc/0+IfQB9E9zwVrEFYJi69dB7UaGi9ehO//1Vemare6iDpiTOMnlowKl2XPBZj33SF2QTRvYmHjRt2iBNb6VGY2JK9zNbch7LpLpzvUFa1ozG2qORoj5ryQngrP9avnJ6tqyivqR0anE3HnhsACcKjXVoHiBEMzBGMRX5IY5w9cGfX8Wje8cfLsNwlFbRoxB42/+2+CTKeq4y6DSnmDVbtBzVM3fT1u/cB9sLwiY8wJhVvs+tYT5BYYpo3OBWgq8ZDrjZP7UpH60WhNLXfn3OUKttMmWbE6GsMofP8nGN7s0EbWlNOq4UFcsp90RlrOfo/GbbAsmDARI8vHGMFNxeW0sZsO+1z6OsGAmY3R8E5STqgzoDF484fg5n3LNac9QZcZHgJ7zUvQEKNdYwzhyrxCSiRJhz/oFyMjN8pbLdb44i5L+SB6U3mKNVrxrZIJZ9kHXrwP3v5BNuRU2R2qGQ61Rvxraar0iF0iSuwbZjpOGgDeV6RqWzHOx9EYcxlV9A8gfZTvQe1IYw5mc4PNTSugWBeNg5vw419Ed+D2xPYUNAXMZ+4wGjOMG2rNQYfv5XgiJjFGwQ17DVH2V6zNjcJhCyKWxih3kOyo/fCWNNgwaybuKdqGi8aoG374c9uF1f0Kxtx0gJrqTiXAc4gxZOf1ExsP1I8nZ65LtiwqlnTaeVvn8GBvqGMqkzkgb96H7/6UwZISumy3HMDEneBegrX5iuPAKuKqdXtpN4rT3oQyzyjqGeh71ybGADqZjNrkzj2tjBzvhy9D6LYLimY0MulR9C6D9/8GV6/FCbRdi9C7GTxlmlKTZ56M6lLlTRtyzfFwuOviLndZd1KdwcZhA+/9S6RcYTWMgtvvwrc5PVbcIxUagQ2Rqq8tXQPWR8bjOb43H2xCgWSfMWcVWOuHS51x8UqTaZpM2DJqG9T65ls3uIxqb4S+Q12xJhrX9HgRfPj3TGT1UXKtbUn8YJdRoVdaHhhbJOuC4Az2FO2izYiwWqwtBmDrr3UQva8zz/dhtmkEjXm7fhe8/7MYvmxS8CPSlBqrM6Y9H1NLZJY5MmeZlb8bUGogEfuZK4HGuL/SnM2I0ZrgH8LNqa5EGSXyK7uZKiTuXFqojcZVz7ATvPw+ePOnTI30RLTrho7rkNWUmsmoyg5RwKmXoYYLlQyths0PHRV3EJqC4ubQn0YoUeypto3eWuOExpW8+uZfIANkZLNWlTjAMUdWq8JZhZ5abn3dOxUvXtL8GHXHgQhxLwuSSEl+SfxsJsZMRs0e3umtWZ2GKFHsM4u/pllVklczQL7/C9x+L8IzzYjbPSuWi6JuXnqN8dkqoxKkKPOYpSAPsAN02cqEozu2TMXD6F1T9zza5wPOQISIrh/pOsPwNnuJ2QgfPuFsVFejg6pvNfDY9T4za50R/XYdesGKiPUpxYctHoVL6CZkUHyKOtdGKA5jPKkFqxve6DaKctqVdN+s2KDqKHFp9wJe/zF4/UcYvKhtWWng++kx08jqwO9Rq9T60jCjYUIQKmMuzTfGzKkgz+vvlCVxym3ILPFTJfiiqh2VvJSLqMk9toMDzFYjGqsD8vb74N2f4eotOEutzWY5EI/XvHU0V7EkLBo8IY5puSiYUhCrBMgltiTGEC73/Dh2tyz3wpe6AjbVspQicYi2vNZGhFVptLALV+/E1TuYPOD4G04euA+kpVU4wQGHrAE0fyvpCymH0xXEpzhPk2kQ9vTEeCXwrmj9WRVdLddezZ4oMQOW3xnLPA1b3RbV60P9fUHDgqSeCFYH5D8BwmHno+sUbSMUOaqjAavmb5sf1b+G7JUmOL4T428iHrsBr2KhAGgSdpbAmkpXzzsq5ZxJo94mybMJimKQbwKQb78BtOSL9B+Mm4c1b77L8dAPX+uqZlRO3D0kFK0EWAGQ0lFBCBevRPZKZjh9zDE5n+xM0Gl0A8a9ES6Vzpfa4bf9W342kCTjCG8AAt2jCeAywYcSNVG/ceFR7EaSXV2YEtze6bukCDPBGR1+Ttjw5iqvEiSZCa7DV2KYY1LMnnH6IGYjgYk47QYVF4AUay0VGTFG0aWeGIeJeOB/A6zbDZbyb4NovIg+Nk6JrYDi7uiRxKQYdFe21vkoVybnzxRV1lPogP8nsEawysZAD0wqlgY+XN2oDC8g116UO6PxWpN4ZIBiJqjMcTdS5l5aJ7hwStXngzNqz5fk0GNlQJYxmd/UC+hcLObOTEwfcT7K/j1FMnRryE6P0o6AidF4EwbQQ5yiHY3ajXTWP1FrfGFo+uhOltkBF53v66Ou7VBkiqM1AUlgMqPK4SsQi52rl5icPYpkXn7ue4UFVBoGGHRpORoxWM1PFyZUZNSx0XhzmYhpLYnzQJOzH711yg92AmfroMgEW31AEpjMWu8KeldCvF9olU95EM/8JKiSr+nRIqvTwrA03lyTxps8ETWDIn5dU57ix9jYb9DJEgMbngQkLxg5yxjq+TaAbj98syNKbCkU9wzI0n3cUuXgJQwWaVmzjCqfF1Q5q0uEjRc1BiP3maLb6MCgJGPFQgc2CBWNMZlE0VDTtbfY8u3IzGbD6INT2W9XcEYt//4c8ylBbs1SZfcKulfi4l0utWYkmWFy/ixOsS0T8xuRtBd21KFWRoVBik+1HpMg7EpYdxht6wY3BmtNhYjT44OiE/U1RZJ6quyI8KXov8wdIUuejJ9FvvvnXioWg42U9N4Lq9V02RZujGbKlqTpPMVERyMBXNaE4n5nYDjs/G53ounRQLEaIBvEJDFmNsMynuwuSrbEkxyQ86ccnLuoxl0x5LvK6pAsYpIrC6Vld388CjS5GoEYymwma2osO2oTZNcO0fTIoFiB93aKye2wUT9/LakyHheokpGcsdOi1W5BqrBQFEOsBWbpwCSZdjq6HhFAB3He/inXCS57+h1pmqLE44NiNUF0F5ikqbJzmb/yaThZwPJeJNMd3ofGSHERzL3NDnIwPRkoNHcwpvMg6OiIMYF7E3nZ6XBrNbWSoGuU6fLcgVE0bRacRwnFygDbEyZzrbKfv3q3C63yaSHBjhq2GTplMTO2jshtp444tLY4fu52NZtJwUDg/X4kzMpj9sN3Bkdig5R43FCsY63ZNSZlrfJGLKdjxpPzTHx9EmncKA7BHSMUbDB0quOvpdCiaySdkncJ80iCYcvnVTe86UevnXB4CjGoByFJGjy7u55okL/E21yTzAA5f4JkXDygcSnV1ANU6XQXCSNpkkzCsE+ri6KDoqXqIojwItqTaHpqUKzPePujykx96t5mL8wtPSPIJdin2sJgtQGgQIl8JjTzoZzBmM5IKK7URXHfzlk0jL4DvdW0WdH0ZKHYICb3Yum5xqXFP0NjBsvsX0x3gTlzWyuKzbeMFYXGpdHacoy98JWTQ1+cduBbGzC5V0tPdJm9ULzLAJnBEuKCmQfq+SFtR8cYrKVTcKNBYU62Wv1MkmlIRYcHueWGvqQDZkuF0B+E7/Z/3rOAYrOYFDsNHsifyUX2yv9YYjIZ5arcLhslnTZKjCkNxQUrBtpvdyA4DqPfuYqmjcir5wXFprhuHwE9RUwmC8E1w+RadnXM3DJ1y3eDyvEA7kxI/EEem6Ra20wA/RRbFNA7CD+47gl1doFvu8Zke6iSin29wDDPcs4ZMlcpm8zbmpt8GG641mHY4OsHkc37ylB0SB3m9OoE1wbvxY6sNR6Kx0SVmwFzQGav7pucIeNHSMdVYCPtGCVJp1w+RFDHMR8YJ5MORKoHOIBhgl/b8PQD6A6N3ovdiaYeijunysZ5cm13DTC6EtEV4hySZ4jvRdVgzsSJEhkhO3p1cdahbgiIrkpt6K4yYm01cxj900FURA/Fo7T0KJjsYHSTvSCdiPgB0meXMnawkE4jefoCSy102zlkyYRp9lLz+kGEC0f/7LBPeRB9qL9tsIfiOVJlaTQM+qLbz+d6OlqQJGtmJyJIuXWO61fZyO2oUTggJcMEnaGIzfTNP+qGt73QWUVsXG/0UDwYVTaIya3gGl5lL5FOIXmA5NFmsIkYWKqTxyVXOk5jEZKd+kIcLI04hMEg/HBwHHooHpIqGxRfy/AOehi8wegVJPeQPgG1CWlGiYsIm2peew5Ky06OOJ12xRXFisNtNIByJuTSHMr/MO9baA5w8wKqp8qGSDK6RXELyROkj7k+WaZEt6XdJasYdMYbdfclEF0dDmuLpjZTTfi70Kgi7o0SPRRPE5OKC+Qye4l0HCQL084i4jQpRtjoi+K4ll00U2iSzikohvoCcBY1rw4u++GbTnjdEhx6KB6B+FqnGrp0bDBIMwbAOEi+xclhLJYp0omageilhKMfLbxYFYfZXVl489+1B4fiIFudni0sXTdyLR5Yfy1YvxWN4fWX+O0U+yuuWu7vsqr1BsVy4LJEarDrQKkDripHFV6Lf9I0Idkvjwun8MbEmlWexbKppp+piG2bIR6Kx4HJOkguvfM4wyQNn5Kru/jlNO3v+evHVAWDhWuxuADQOETNm/wOSyRaTTX7p0QvoB6fSlmtos+m8zTB6RoLicgBOU6Hg+C5F0zZmqGjZVWOlctdGkFPucJOCUb274YVTTkX0T+1x1TjWfF0eNIVwPcT3BLPYgInuGTI21na3cOXTagYvbzODQuHuNqWGyn7DdpJdBB+FwUXLcShZ8Wj50mnElvPc5ylSNLaApDXEc4HwTgCQ0Qrz7gKxT2Epc+1EXC7L4vaCW674W1rn75nxVPgSU7PDBr3q00NcWFEzF9F5sh+ztPOQ3w9Si5Tur6Gy2bp+ksiiTFTF7dXxdYS0dihuF6EcGE11RyQEj0UTweT1j6PU4xT5GhV07T3kLyYpMM1sEC1oGLpIyiVkIPiTxmmkFCWm2CXdW5CGAyj79uMQy+gnpTsath1KwPh4yxdQggKO5mufsUy56UI43SQYXIYPnUgZptHgGNqSan0kYWAaqC1skPfGOcmGWAhN5l+H0DUZhx6VjxWnnQ96ts4TVFrCCkZctaYz4NyMgXyKbnE8l6LGiakvY9lXs2gqLLzjlhxUc7094Fxq+A9Q85D8TRJkvP+JMZxjGUVC8tBn0gSH2YKZPc+fpExJCmprt5BYajdhkrAgEqMC12RVAUJqykW3kQjVw+i78JKiYj7x6eH4qmhUW1fnxNV9lMAKaMRZSAhPCcXT8lV6jZhQL/7YqwKqMxBke1FHITfdYLrlquIHorn0u4naZyuBE77JDcyTIzRY3w9x44ULkeYdQBXL3U0WO8eR8fc0CqiTfkkWzd4ZXVdtAeHHoon3jIQ3k2SLQViGZBY8GwUVEfCz7H8IEUYJZej+BKx1nxNUtpyQ5tqtn9Lrg7Uq76d4HYQfTgiHHoonnj78hzzKIT4XKuDZdojdjJ6zEhy67jfMiGgzWyzBFWq7EewtNzoTKZ8Qozgoh9+bJsq6KF4vu1hmk7miAq9rRvBkIotZ92tPERGj8FTcjlJqkeTI2G5CQ2uC7QsEis+DGAwiL43I83J6OWh6Fu1+Y1b0XSc7Pp0k7Q/ii+U0BwwBAZs8qcSFYorAZWVE0x+vnDl/7PZhdhanvQu/qNHHdk+j+IEl0aVwtyWKgkvtUcQhRi1jcIomWDWb5RjAxZ/ZsJqguEweg7BDfkEFPPZqPIh6dcnm92V30IV0bPiCeJw82Yumsbp3i4mY8WMG+erxA7QkBhgKTVZMaKCqFPuye7KbzMOPRRPsGUY/PYcr93fskpIesuLCqEq/hX8j6vRkNDiMpg9J4NxMqB1OBLAyoISQF9rLzV583MNMcOh1ZXfZhx6AfUEKTETTVPKoV/ObgJjH3nS08nCxT0zFr1naTfDZD+cBKs3NLMcVv+nmITQwAwc2rKBRStNpp4VTxmH95N0PE/LnIYrTpPYTqXLlXFVYyRRD1VIKnt3nnSe42FqqoWzhWiq7KwYwBA3fLj5zcSH2LdlA4u2mkw9FE+2zRL8OoppsVDjGKfzAJFV0IIOIl9kIa/RKKNv9YKCkllXoc1wWC2kpoU86aF4IpSYUeGvT7HxYCoym65DQWzMhFyILlI60nA0v0gkJ8fWZrO5BhWKkE9Ia5w3bvjwZHDooXg6oum353i6TA1WAtYEZYMxcmPBlkPCVLLl0NyYR5DHwwRNE0z1Z+SWGzYfdoIXJ4NDD8UTweFoli5iTWm4seLAUSusImO7Q1TIdEF6SzSG2dyXCXnFkKghVRsf5iGmGQ6rIaq19hsPxaNvcZqJpvOy4V/+VRVKN34O1dVhVCRVeVVGmXxEHj4eD5I00BNjLAuo9lSpDIf98KMVUS13XXgoniAl/vwwS1I2AXIYsmpfpBRNXKAxTcOCxigbfyQBtWMgbjxdHHooHj0Ov4wyFZHwg0ukJ3OjTGZKuLco+Q9MEeQk4W4JczNEjsb+Go0yK4qUgffVeN089em7k8Shh+Lxq4jPyZF8kRyNqhWHX/F1icPjVQU9FE+2ZWT46XGOatiaZINRUoEp4+qmBz9KDpWsYqQsqxInIzzPy2gseflDuBAyfxf5sDIOjwKfHopHSYkpigyHSYrH9o1gMu8VEz8QTfvNYBN8eCw86aF4lKLpr0/zQu7FmpaKpKZEjaFKj0i6+1HVMzVRcubyHEiW58hY8XnWx0IhRqFRP5djd84Dhx6KR4nDu3HyMEmO99tlaJzErK1yMj4cngcOPRSPr43n6a+P8y1ZKblNhJ1Tdhiq+1JgqQgj6TAkdrTYGlephYOqELV+c55EGzTGMjEGK3XxjPjQQ/H4KDFO8ce7GbmjWVnRovzzxK6ghl0NjTEvKvA4ptAi4GdJJwOkrmcIg0H4/nxwKHy+4nGZan74NksK2IMiGkGulLH6tTAn1wcCKIDCbaWNctZiYVeNUnkOUPazQQQQcsUOsanGUb42QBjPe0GQ7/Em5D2AQ+hfRL83bwx8Yjj0rHhMKuKnx/nUUCZDH0SqpSbujtk2iuMcq+HY51k/kd+PguFF9Idzw6GH4tG0+0lyv9z4QhvvqfME6uLTJBRzIsiRqpTKUVZ1oy0TOIpiagakfvjuDHHooXgclJh78x/mZSShYnHRDUxyY9nqg6gMr6Y1opq0qElStpUhl4w7TkA6SRx6KB6HivjT/SzF6rtH6CRV0ximLXxR1Iw7p6B75jj0ZpsjUBF/eZhN5qlYm0mKIiSgUth0nQW4nuDLf9afFGs/gWTyWb6Ba6sNyB9tjyju/FYYCFE93Qb8ki1ne63u2DlhHHpWbHt7nGYqos2bz2Uo5NNURdLjOT/UjRzr7HB+Gjj0rNhqSpwn+NPCiwiKhLlmSJTICgq/CoL0Sn4OUXRIrMcqdYPSwQUyI/wcy3Ekqhb2MuQM3J88Dj0rtlo0zVTEBP1NOgscelZsb/ttFD9NE7HeU1RinjUjydrjWhdcMk1Rx2PvmSH5K0BRSoWkPcrEuz01yA6UYkRB+XSLdzuhx6FnxRaLpnmg6Xm0QA+q88GhZ8WWiqY/3k0TVF0HWyosaY9m42qBKwvdCtbOzRbCkqlTsqCWSY8kXtWyKijjKhauAJqzo3pW9K1hcH4dxaNZej5fvB85UN+p4tCzYosocdly0fRhVlQFhcRSK4UQ5SlJ9S+GjUt2SoXnZNotD70lQFgz9PYvIL9dkSiBOl1ZezxzPvSs2EbR9Ge21dS814UhGs4eDm4NpdvYd+yns+Rd9aNzRJ2HYtvbw0RKz0fU5wEqBTRIfBZKZJjKehMh3RRk6SBvHf55pwvYkDt5cHoBtS2UuIg1ncrehK1wB9QMhxUorcLqeozSDt7KhopyxiMVr4ZFj8jyL81wgvJzKMIq6ck4Q0r0rNgicH5+ms/i0uYzUjkKvaxJSJREfURz4BtqhV6SeQ0iLvKqASypOgpqRYF7VvSteWvNl6cZhZFy+nw5omxlHyEMLuRoIJRAOGOUnCbmTbXlFPwc5MUSoeh5p24AfmJ4VmyRtSZ3JKZGm4wwqXtaXRHtxh6yR1M5UNZxuoonQ11PzkRe9ax4eHCOFukXK699gXm2vxZCrjl+DqEGbUuh2ChkX0NZ/6QcGJu3wMSNaxUXldSt4sWsv47Oo+hZ0bcDiKZikZHoPmAtsqr3XXjnYXTrhh6KnhVbA87fnuPHaSEjscA8hTjvLTeuuQtkY2YRA1utTIogF8XIOTmbWOij5Mjk5CIzU8F0q88LUQVEQAFgx0PRs2KLKPF+erZ3ZtCpnjHsWdG3RilxsTtiKXxabJKJSpQi2Se3Md+oGCaFVDSqXNGCSrCiI8iFqTxH+SJV7XGrpVIR5B0A1ZNxnh5Fz4qHp8SfH86XEvsdL516VmwNJc7icgqUUJx1AkvaXVEBW5kiFQokdDs6glxITCuHjWvLc6hlyEFxdxb9oVveLhBvP/I04Fnx6CiRjPM0lg+2DUO/g7wDzcE9ghpQPeSi61nRs2ILwPl1NJ/OUU0ykotfIBTZTo5lAUGbQdHIjUJTngMILZEwrhZpExnlOWR63Kq43QgC8FD0rNgCSvzpfkYWOSyRHsoSpZnBikfYQ0M19GXl1Ur3onz2yy4x8c42zsZD8ZCUOMs3oqFL6AstGjXCKrmLBmqj56jEKELItMfZsffMUIE58DYbD8U2UOKP9zNUpnNpY1GK3VDI+3LrJjoWvf064l1lPFrCVkvbd2suitj5Bin9cvleFEDP22w8FA8OzsdpsqBESyq9kGFpkDbRKlI2VUy1oWi7i66fdd5s0wZKvJtuzCRrw8i2DhqA7LVf/q4EXm92qiiOo40gF1Y/x1o1U4iRKkMOJQOPoK+wsN2qkGIArnpeUfSseGhwzhJ8XJTMQJtBBSvthYHVaIwXwW33nqDBnrRqvchLp54VW0CJP9xNUJTDwCSywkLyEpDbn21pCDaFRxVvBJQ8GJsIciEFkRdVPlhxqak8ByuIvBT8Lfs5rnqhnySeFQ8MziQV357jM78bXjr1rHj49vlpFifFeDOJHose81UsNUI5n1jRzaSNnwrx2avRlAhyUQgiJ7RHLIRxK2XIy/tA6cpzlATZwkVedMPQV9DwrHgQ6bT4zs/uKcLnQIm+eVbcaxvN0nGcgiiTFcgZT7IWKfFLUWajIsiFUCo+SYFz28FN5TlKgW/FKDnJsrpVZe3lOZbn6wRwSSmK4CPgPCvu02BzzvlQy/ZiwDXYnCc4PSvuA5xJil9G83Sh3IEcV13KOdrSImwMpBuFrUxhUODBwvaJUmEoKs67yI3LkSlbLtFZomVATaUNNYI8rwIurvvedupZ8dDty3M8T1dQQlLILLxpcfJpYtk2P5h5T+RgTu5N5FW7Wo5w3aMNNl469VDcq8Emo8RVfOk67FP3Wk/f4j6E2g1nUIPG0ttIRpCSG1wYy5Ajrww56em/HXLlr7MFp4fizluc4ufnFRQX6RiYLn8pxXzLcdVYyHLA8kTHZRNKBDkVKU5tR0MCjUKXgl57TKu0z9Ti500/9MXdPBTbIJ3ORYn6CikRGlbcorHEdjKZaTtgOZtq/Q5qubEEQD0gCTQiqty7HffVRcQkwHOWV73ZZg/SaSw0Fo615aRsLYF1fW3Vz1E0h6wiysjpW4g0lw0o1F4YcoS5beMNws9RRHypDPnNwFOiZ8V2SKeZoijKdFd4IWszDIOJBVGbua+PYTWcD5u9A68vOl4n9KzYAuk0xyGRQYTbGDepHE2gEFcZHnL58KKfA9bbVdBoXEepITWO2NAjyNu/qcVOhZSTRfg5Cj6X20HEp8QzB6dnxR1Lp89zYWQmkvo2ihzqeIpKWOJkCTN3khJGQrZosGtABgCvLyOPOs+K7WFFRXvb8k3e0gKbQJEbFzS1VQuB1gKLKmShMqliESlXaqPLkENZmbRtYoWF0eQy5LdDh+BvD07PirvEYYkSz6llcumby46fA54VWyGd3o1jk/yHUsnsJTcGi8PTJROumUINIhfKnhkbjpW0x0J5DiXwrcBmmzLk8gaP29GUCHJhK8/x9qrLJ0BPiZ4V9yadnle76IY+4tSzYlvaJE4nccqwkEDRwppKNLhSvgLqIIJjNRHkIDsmVW6UsqnW+cqq43GzwaO5DHkI8OGm4ynRs2KbpVMDILd/lLyOZPJVybKKawsLGsoQK28Wo234EeSidIRiPH05jLph4G0znhXb0u4m/DI25Q1+ixiDFT2uZnGwpqmSirglt0KZC1GyrIK85ZOQTiNVglS26BDlTaxkPXPdtx8Fb68crDUenJ4Vdw/FcaWKUkqdbVzzLRmtqnamq4yvO5GpIeo4vDLkxL4A373oetR5VjwuRZELTLBVFhVk2KjKaVadFYlOkua5+V0z+JvLTr/jsLh7cHootkdR1OCL3ou76MAvSosCCxuOlsLWiln8qp+jDHssRZAL069yLPlF1ySaetRZ2/8XYADH/GSQm72TNQAAAABJRU5ErkJggg==" alt="Yii Framework"/> + <p class="timestamp"><?php echo date('Y-m-d, H:i:s'); ?></p> + <p><?php echo $handler->createServerInformationLink(); ?></p> + <p><a href="http://yiiframework.com/">Yii Framework</a>/<?php echo $handler->createFrameworkVersionLink(); ?></p> + </div> + + <script type="text/javascript"> +var hljs=new function(){function l(o){return o.replace(/&/gm,"&").replace(/</gm,"<").replace(/>/gm,">")}function b(p){for(var o=p.firstChild;o;o=o.nextSibling){if(o.nodeName=="CODE"){return o}if(!(o.nodeType==3&&o.nodeValue.match(/\s+/))){break}}}function h(p,o){return Array.prototype.map.call(p.childNodes,function(q){if(q.nodeType==3){return o?q.nodeValue.replace(/\n/g,""):q.nodeValue}if(q.nodeName=="BR"){return"\n"}return h(q,o)}).join("")}function a(q){var p=(q.className+" "+q.parentNode.className).split(/\s+/);p=p.map(function(r){return r.replace(/^language-/,"")});for(var o=0;o<p.length;o++){if(e[p[o]]||p[o]=="no-highlight"){return p[o]}}}function c(q){var o=[];(function p(r,s){for(var t=r.firstChild;t;t=t.nextSibling){if(t.nodeType==3){s+=t.nodeValue.length}else{if(t.nodeName=="BR"){s+=1}else{if(t.nodeType==1){o.push({event:"start",offset:s,node:t});s=p(t,s);o.push({event:"stop",offset:s,node:t})}}}}return s})(q,0);return o}function j(x,v,w){var p=0;var y="";var r=[];function t(){if(x.length&&v.length){if(x[0].offset!=v[0].offset){return(x[0].offset<v[0].offset)?x:v}else{return v[0].event=="start"?x:v}}else{return x.length?x:v}}function s(A){function z(B){return" "+B.nodeName+'="'+l(B.value)+'"'}return"<"+A.nodeName+Array.prototype.map.call(A.attributes,z).join("")+">"}while(x.length||v.length){var u=t().splice(0,1)[0];y+=l(w.substr(p,u.offset-p));p=u.offset;if(u.event=="start"){y+=s(u.node);r.push(u.node)}else{if(u.event=="stop"){var o,q=r.length;do{q--;o=r[q];y+=("</"+o.nodeName.toLowerCase()+">")}while(o!=u.node);r.splice(q,1);while(q<r.length){y+=s(r[q]);q++}}}}return y+l(w.substr(p))}function f(q){function o(s,r){return RegExp(s,"m"+(q.cI?"i":"")+(r?"g":""))}function p(y,w){if(y.compiled){return}y.compiled=true;var s=[];if(y.k){var r={};function z(A,t){t.split(" ").forEach(function(B){var C=B.split("|");r[C[0]]=[A,C[1]?Number(C[1]):1];s.push(C[0])})}y.lR=o(y.l||hljs.IR,true);if(typeof y.k=="string"){z("keyword",y.k)}else{for(var x in y.k){if(!y.k.hasOwnProperty(x)){continue}z(x,y.k[x])}}y.k=r}if(w){if(y.bWK){y.b="\\b("+s.join("|")+")\\s"}y.bR=o(y.b?y.b:"\\B|\\b");if(!y.e&&!y.eW){y.e="\\B|\\b"}if(y.e){y.eR=o(y.e)}y.tE=y.e||"";if(y.eW&&w.tE){y.tE+=(y.e?"|":"")+w.tE}}if(y.i){y.iR=o(y.i)}if(y.r===undefined){y.r=1}if(!y.c){y.c=[]}for(var v=0;v<y.c.length;v++){if(y.c[v]=="self"){y.c[v]=y}p(y.c[v],y)}if(y.starts){p(y.starts,w)}var u=[];for(var v=0;v<y.c.length;v++){u.push(y.c[v].b)}if(y.tE){u.push(y.tE)}if(y.i){u.push(y.i)}y.t=u.length?o(u.join("|"),true):{exec:function(t){return null}}}p(q)}function d(D,E){function o(r,M){for(var L=0;L<M.c.length;L++){var K=M.c[L].bR.exec(r);if(K&&K.index==0){return M.c[L]}}}function s(K,r){if(K.e&&K.eR.test(r)){return K}if(K.eW){return s(K.parent,r)}}function t(r,K){return K.i&&K.iR.test(r)}function y(L,r){var K=F.cI?r[0].toLowerCase():r[0];return L.k.hasOwnProperty(K)&&L.k[K]}function G(){var K=l(w);if(!A.k){return K}var r="";var N=0;A.lR.lastIndex=0;var L=A.lR.exec(K);while(L){r+=K.substr(N,L.index-N);var M=y(A,L);if(M){v+=M[1];r+='<span class="'+M[0]+'">'+L[0]+"</span>"}else{r+=L[0]}N=A.lR.lastIndex;L=A.lR.exec(K)}return r+K.substr(N)}function z(){if(A.sL&&!e[A.sL]){return l(w)}var r=A.sL?d(A.sL,w):g(w);if(A.r>0){v+=r.keyword_count;B+=r.r}return'<span class="'+r.language+'">'+r.value+"</span>"}function J(){return A.sL!==undefined?z():G()}function I(L,r){var K=L.cN?'<span class="'+L.cN+'">':"";if(L.rB){x+=K;w=""}else{if(L.eB){x+=l(r)+K;w=""}else{x+=K;w=r}}A=Object.create(L,{parent:{value:A}});B+=L.r}function C(K,r){w+=K;if(r===undefined){x+=J();return 0}var L=o(r,A);if(L){x+=J();I(L,r);return L.rB?0:r.length}var M=s(A,r);if(M){if(!(M.rE||M.eE)){w+=r}x+=J();do{if(A.cN){x+="</span>"}A=A.parent}while(A!=M.parent);if(M.eE){x+=l(r)}w="";if(M.starts){I(M.starts,"")}return M.rE?0:r.length}if(t(r,A)){throw"Illegal"}w+=r;return r.length||1}var F=e[D];f(F);var A=F;var w="";var B=0;var v=0;var x="";try{var u,q,p=0;while(true){A.t.lastIndex=p;u=A.t.exec(E);if(!u){break}q=C(E.substr(p,u.index-p),u[0]);p=u.index+q}C(E.substr(p));return{r:B,keyword_count:v,value:x,language:D}}catch(H){if(H=="Illegal"){return{r:0,keyword_count:0,value:l(E)}}else{throw H}}}function g(s){var o={keyword_count:0,r:0,value:l(s)};var q=o;for(var p in e){if(!e.hasOwnProperty(p)){continue}var r=d(p,s);r.language=p;if(r.keyword_count+r.r>q.keyword_count+q.r){q=r}if(r.keyword_count+r.r>o.keyword_count+o.r){q=o;o=r}}if(q.language){o.second_best=q}return o}function i(q,p,o){if(p){q=q.replace(/^((<[^>]+>|\t)+)/gm,function(r,v,u,t){return v.replace(/\t/g,p)})}if(o){q=q.replace(/\n/g,"<br>")}return q}function m(r,u,p){var v=h(r,p);var t=a(r);if(t=="no-highlight"){return}var w=t?d(t,v):g(v);t=w.language;var o=c(r);if(o.length){var q=document.createElement("pre");q.innerHTML=w.value;w.value=j(o,c(q),v)}w.value=i(w.value,u,p);var s=r.className;if(!s.match("(\\s|^)(language-)?"+t+"(\\s|$)")){s=s?(s+" "+t):t}r.innerHTML=w.value;r.className=s;r.result={language:t,kw:w.keyword_count,re:w.r};if(w.second_best){r.second_best={language:w.second_best.language,kw:w.second_best.keyword_count,re:w.second_best.r}}}function n(){if(n.called){return}n.called=true;Array.prototype.map.call(document.getElementsByTagName("pre"),b).filter(Boolean).forEach(function(o){m(o,hljs.tabReplace)})}function k(){window.addEventListener("DOMContentLoaded",n,false);window.addEventListener("load",n,false)}var e={};this.LANGUAGES=e;this.highlight=d;this.highlightAuto=g;this.fixMarkup=i;this.highlightBlock=m;this.initHighlighting=n;this.initHighlightingOnLoad=k;this.IR="[a-zA-Z][a-zA-Z0-9_]*";this.UIR="[a-zA-Z_][a-zA-Z0-9_]*";this.NR="\\b\\d+(\\.\\d+)?";this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";this.BNR="\\b(0b[01]+)";this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|\\.|-|-=|/|/=|:|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\[\\s\\S]",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE],r:0};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE],r:0};this.CLCM={cN:"comment",b:"//",e:"$"};this.CBLCLM={cN:"comment",b:"/\\*",e:"\\*/"};this.HCM={cN:"comment",b:"#",e:"$"};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.inherit=function(q,r){var o={};for(var p in q){o[p]=q[p]}if(r){for(var p in r){o[p]=r[p]}}return o}}();hljs.LANGUAGES.php=function(a){var e={cN:"variable",b:"\\$+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*"};var b=[a.inherit(a.ASM,{i:null}),a.inherit(a.QSM,{i:null}),{cN:"string",b:'b"',e:'"',c:[a.BE]},{cN:"string",b:"b'",e:"'",c:[a.BE]}];var c=[a.BNM,a.CNM];var d={cN:"title",b:a.UIR};return{cI:true,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return implements parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception php_user_filter default die require __FUNCTION__ enddeclare final try this switch continue endfor endif declare unset true false namespace trait goto instanceof insteadof __DIR__ __NAMESPACE__ __halt_compiler",c:[a.CLCM,a.HCM,{cN:"comment",b:"/\\*",e:"\\*/",c:[{cN:"phpdoc",b:"\\s@[A-Za-z]+"}]},{cN:"comment",eB:true,b:"__halt_compiler.+?;",eW:true},{cN:"string",b:"<<<['\"]?\\w+['\"]?$",e:"^\\w+;",c:[a.BE]},{cN:"preprocessor",b:"<\\?php",r:10},{cN:"preprocessor",b:"\\?>"},e,{cN:"function",bWK:true,e:"{",k:"function",i:"\\$|\\[|%",c:[d,{cN:"params",b:"\\(",e:"\\)",c:["self",e,a.CBLCLM].concat(b).concat(c)}]},{cN:"class",bWK:true,e:"{",k:"class",i:"[:\\(\\$]",c:[{bWK:true,eW:true,k:"extends",c:[d]},d]},{b:"=>"}].concat(b).concat(c)}}(hljs); + </script> + + <script type="text/javascript"> +/*! Sizzle v1.9.4-pre | (c) 2013 jQuery Foundation, Inc. | jquery.org/license +//@ sourceMappingURL=sizzle.min.map +*/(function(e,t){function n(e,t,n,r){var o,i,u,l,a,c,s,f,p,d;if((t?t.ownerDocument||t:U)!==H&&q(t),t=t||H,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(O&&!r){if(o=Ct.exec(e))if(u=o[1]){if(9===l){if(i=t.getElementById(u),!i||!i.parentNode)return n;if(i.id===u)return n.push(i),n}else if(t.ownerDocument&&(i=t.ownerDocument.getElementById(u))&&j(t,i)&&i.id===u)return n.push(i),n}else{if(o[2])return ot.apply(n,t.getElementsByTagName(e)),n;if((u=o[3])&&S.getElementsByClassName&&t.getElementsByClassName)return ot.apply(n,t.getElementsByClassName(u)),n}if(S.qsa&&(!k||!k.test(e))){if(f=s=G,p=t,d=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){for(c=g(e),(s=t.getAttribute("id"))?f=s.replace(Tt,"\\$&"):t.setAttribute("id",f),f="[id='"+f+"'] ",a=c.length;a--;)c[a]=f+m(c[a]);p=mt.test(e)&&t.parentNode||t,d=c.join(",")}if(d)try{return ot.apply(n,p.querySelectorAll(d)),n}catch(h){}finally{s||t.removeAttribute("id")}}}return w(e.replace(dt,"$1"),t,n,r)}function r(e){return xt.test(e+"")}function o(){function e(n,r){return t.push(n+=" ")>L.cacheLength&&delete e[t.shift()],e[n]=r}var t=[];return e}function i(e){return e[G]=!0,e}function u(e){var t=H.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function l(e,t,n){e=e.split("|");for(var r,o=e.length,i=n?null:t;o--;)(r=L.attrHandle[e[o]])&&r!==t||(L.attrHandle[e[o]]=i)}function a(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:e[t]===!0?t.toLowerCase():null}function c(e,t){return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}function s(e){return"input"===e.nodeName.toLowerCase()?e.defaultValue:t}function f(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||_)-(~e.sourceIndex||_);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function p(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function d(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function h(e){return i(function(t){return t=+t,i(function(n,r){for(var o,i=e([],n.length,t),u=i.length;u--;)n[o=i[u]]&&(n[o]=!(r[o]=n[o]))})})}function g(e,t){var r,o,i,u,l,a,c,s=K[e+" "];if(s)return t?0:s.slice(0);for(l=e,a=[],c=L.preFilter;l;){(!r||(o=ht.exec(l)))&&(o&&(l=l.slice(o[0].length)||l),a.push(i=[])),r=!1,(o=gt.exec(l))&&(r=o.shift(),i.push({value:r,type:o[0].replace(dt," ")}),l=l.slice(r.length));for(u in L.filter)!(o=bt[u].exec(l))||c[u]&&!(o=c[u](o))||(r=o.shift(),i.push({value:r,type:u,matches:o}),l=l.slice(r.length));if(!r)break}return t?l.length:l?n.error(e):K(e,a).slice(0)}function m(e){for(var t=0,n=e.length,r="";n>t;t++)r+=e[t].value;return r}function y(e,t,n){var r=t.dir,o=n&&"parentNode"===r,i=X++;return t.first?function(t,n,i){for(;t=t[r];)if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,u){var l,a,c,s=V+" "+i;if(u){for(;t=t[r];)if((1===t.nodeType||o)&&e(t,n,u))return!0}else for(;t=t[r];)if(1===t.nodeType||o)if(c=t[G]||(t[G]={}),(a=c[r])&&a[0]===s){if((l=a[1])===!0||l===D)return l===!0}else if(a=c[r]=[s],a[1]=e(t,n,u)||D,a[1]===!0)return!0}}function v(e){return e.length>1?function(t,n,r){for(var o=e.length;o--;)if(!e[o](t,n,r))return!1;return!0}:e[0]}function N(e,t,n,r,o){for(var i,u=[],l=0,a=e.length,c=null!=t;a>l;l++)(i=e[l])&&(!n||n(i,r,o))&&(u.push(i),c&&t.push(l));return u}function b(e,t,n,r,o,u){return r&&!r[G]&&(r=b(r)),o&&!o[G]&&(o=b(o,u)),i(function(i,u,l,a){var c,s,f,p=[],d=[],h=u.length,g=i||E(t||"*",l.nodeType?[l]:l,[]),m=!e||!i&&t?g:N(g,p,e,l,a),y=n?o||(i?e:h||r)?[]:u:m;if(n&&n(m,y,l,a),r)for(c=N(y,d),r(c,[],l,a),s=c.length;s--;)(f=c[s])&&(y[d[s]]=!(m[d[s]]=f));if(i){if(o||e){if(o){for(c=[],s=y.length;s--;)(f=y[s])&&c.push(m[s]=f);o(null,y=[],c,a)}for(s=y.length;s--;)(f=y[s])&&(c=o?ut.call(i,f):p[s])>-1&&(i[c]=!(u[c]=f))}}else y=N(y===u?y.splice(h,y.length):y),o?o(null,u,y,a):ot.apply(u,y)})}function x(e){for(var t,n,r,o=e.length,i=L.relative[e[0].type],u=i||L.relative[" "],l=i?1:0,a=y(function(e){return e===t},u,!0),c=y(function(e){return ut.call(t,e)>-1},u,!0),s=[function(e,n,r){return!i&&(r||n!==P)||((t=n).nodeType?a(e,n,r):c(e,n,r))}];o>l;l++)if(n=L.relative[e[l].type])s=[y(v(s),n)];else{if(n=L.filter[e[l].type].apply(null,e[l].matches),n[G]){for(r=++l;o>r&&!L.relative[e[r].type];r++);return b(l>1&&v(s),l>1&&m(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(dt,"$1"),n,r>l&&x(e.slice(l,r)),o>r&&x(e=e.slice(r)),o>r&&m(e))}s.push(n)}return v(s)}function C(e,t){var r=0,o=t.length>0,u=e.length>0,l=function(i,l,a,c,s){var f,p,d,h=[],g=0,m="0",y=i&&[],v=null!=s,b=P,x=i||u&&L.find.TAG("*",s&&l.parentNode||l),C=V+=null==b?1:Math.random()||.1;for(v&&(P=l!==H&&l,D=r);null!=(f=x[m]);m++){if(u&&f){for(p=0;d=e[p++];)if(d(f,l,a)){c.push(f);break}v&&(V=C,D=++r)}o&&((f=!d&&f)&&g--,i&&y.push(f))}if(g+=m,o&&m!==g){for(p=0;d=t[p++];)d(y,h,l,a);if(i){if(g>0)for(;m--;)y[m]||h[m]||(h[m]=nt.call(c));h=N(h)}ot.apply(c,h),v&&!i&&h.length>0&&g+t.length>1&&n.uniqueSort(c)}return v&&(V=C,P=b),y};return o?i(l):l}function E(e,t,r){for(var o=0,i=t.length;i>o;o++)n(e,t[o],r);return r}function w(e,t,n,r){var o,i,u,l,a,c=g(e);if(!r&&1===c.length){if(i=c[0]=c[0].slice(0),i.length>2&&"ID"===(u=i[0]).type&&S.getById&&9===t.nodeType&&O&&L.relative[i[1].type]){if(t=(L.find.ID(u.matches[0].replace(At,St),t)||[])[0],!t)return n;e=e.slice(i.shift().value.length)}for(o=bt.needsContext.test(e)?0:i.length;o--&&(u=i[o],!L.relative[l=u.type]);)if((a=L.find[l])&&(r=a(u.matches[0].replace(At,St),mt.test(i[0].type)&&t.parentNode||t))){if(i.splice(o,1),e=r.length&&m(i),!e)return ot.apply(n,r),n;break}}return R(e,c)(r,t,!O,n,mt.test(e)),n}function T(){}var A,S,D,L,B,I,R,P,$,q,H,M,O,k,F,z,j,G="sizzle"+-new Date,U=e.document,V=0,X=0,J=o(),K=o(),Q=o(),W=!1,Y=function(){return 0},Z=typeof t,_=1<<31,et={}.hasOwnProperty,tt=[],nt=tt.pop,rt=tt.push,ot=tt.push,it=tt.slice,ut=tt.indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(this[t]===e)return t;return-1},lt="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",at="[\\x20\\t\\r\\n\\f]",ct="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",st=ct.replace("w","w#"),ft="\\["+at+"*("+ct+")"+at+"*(?:([*^$|!~]?=)"+at+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+st+")|)|)"+at+"*\\]",pt=":("+ct+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+ft.replace(3,8)+")*)|.*)\\)|)",dt=RegExp("^"+at+"+|((?:^|[^\\\\])(?:\\\\.)*)"+at+"+$","g"),ht=RegExp("^"+at+"*,"+at+"*"),gt=RegExp("^"+at+"*([>+~]|"+at+")"+at+"*"),mt=RegExp(at+"*[+~]"),yt=RegExp("="+at+"*([^\\]'\"]*)"+at+"*\\]","g"),vt=RegExp(pt),Nt=RegExp("^"+st+"$"),bt={ID:RegExp("^#("+ct+")"),CLASS:RegExp("^\\.("+ct+")"),TAG:RegExp("^("+ct.replace("w","w*")+")"),ATTR:RegExp("^"+ft),PSEUDO:RegExp("^"+pt),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+at+"*(even|odd|(([+-]|)(\\d*)n|)"+at+"*(?:([+-]|)"+at+"*(\\d+)|))"+at+"*\\)|)","i"),bool:RegExp("^(?:"+lt+")$","i"),needsContext:RegExp("^"+at+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+at+"*((?:-\\d)?\\d*)"+at+"*\\)|)(?=[^-]|$)","i")},xt=/^[^{]+\{\s*\[native \w/,Ct=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Et=/^(?:input|select|textarea|button)$/i,wt=/^h\d$/i,Tt=/'|\\/g,At=RegExp("\\\\([\\da-f]{1,6}"+at+"?|("+at+")|.)","ig"),St=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{ot.apply(tt=it.call(U.childNodes),U.childNodes),tt[U.childNodes.length].nodeType}catch(Dt){ot={apply:tt.length?function(e,t){rt.apply(e,it.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}I=n.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},S=n.support={},q=n.setDocument=function(e){var n=e?e.ownerDocument||e:U;return n!==H&&9===n.nodeType&&n.documentElement?(H=n,M=n.documentElement,O=!I(n),S.attributes=u(function(e){return e.innerHTML="<a href='#'></a>",l("type|href|height|width",c,"#"===e.firstChild.getAttribute("href")),l(lt,a,null==e.getAttribute("disabled")),e.className="i",!e.getAttribute("className")}),S.input=u(function(e){return e.innerHTML="<input>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")}),l("value",s,S.attributes&&S.input),S.getElementsByTagName=u(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),S.getElementsByClassName=u(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),S.getById=u(function(e){return M.appendChild(e).id=G,!n.getElementsByName||!n.getElementsByName(G).length}),S.getById?(L.find.ID=function(e,t){if(typeof t.getElementById!==Z&&O){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},L.filter.ID=function(e){var t=e.replace(At,St);return function(e){return e.getAttribute("id")===t}}):(delete L.find.ID,L.filter.ID=function(e){var t=e.replace(At,St);return function(e){var n=typeof e.getAttributeNode!==Z&&e.getAttributeNode("id");return n&&n.value===t}}),L.find.TAG=S.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==Z?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],o=0,i=t.getElementsByTagName(e);if("*"===e){for(;n=i[o++];)1===n.nodeType&&r.push(n);return r}return i},L.find.CLASS=S.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==Z&&O?n.getElementsByClassName(e):t},F=[],k=[],(S.qsa=r(n.querySelectorAll))&&(u(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||k.push("\\["+at+"*(?:value|"+lt+")"),e.querySelectorAll(":checked").length||k.push(":checked")}),u(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&k.push("[*^$]="+at+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||k.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),k.push(",.*:")})),(S.matchesSelector=r(z=M.webkitMatchesSelector||M.mozMatchesSelector||M.oMatchesSelector||M.msMatchesSelector))&&u(function(e){S.disconnectedMatch=z.call(e,"div"),z.call(e,"[s!='']:x"),F.push("!=",pt)}),k=k.length&&RegExp(k.join("|")),F=F.length&&RegExp(F.join("|")),j=r(M.contains)||M.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},S.sortDetached=u(function(e){return 1&e.compareDocumentPosition(n.createElement("div"))}),Y=M.compareDocumentPosition?function(e,t){if(e===t)return W=!0,0;var r=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return r?1&r||!S.sortDetached&&t.compareDocumentPosition(e)===r?e===n||j(U,e)?-1:t===n||j(U,t)?1:$?ut.call($,e)-ut.call($,t):0:4&r?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,o=0,i=e.parentNode,u=t.parentNode,l=[e],a=[t];if(e===t)return W=!0,0;if(!i||!u)return e===n?-1:t===n?1:i?-1:u?1:$?ut.call($,e)-ut.call($,t):0;if(i===u)return f(e,t);for(r=e;r=r.parentNode;)l.unshift(r);for(r=t;r=r.parentNode;)a.unshift(r);for(;l[o]===a[o];)o++;return o?f(l[o],a[o]):l[o]===U?-1:a[o]===U?1:0},n):H},n.matches=function(e,t){return n(e,null,null,t)},n.matchesSelector=function(e,t){if((e.ownerDocument||e)!==H&&q(e),t=t.replace(yt,"='$1']"),!(!S.matchesSelector||!O||F&&F.test(t)||k&&k.test(t)))try{var r=z.call(e,t);if(r||S.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(o){}return n(t,H,null,[e]).length>0},n.contains=function(e,t){return(e.ownerDocument||e)!==H&&q(e),j(e,t)},n.attr=function(e,n){(e.ownerDocument||e)!==H&&q(e);var r=L.attrHandle[n.toLowerCase()],o=r&&et.call(L.attrHandle,n.toLowerCase())?r(e,n,!O):t;return o===t?S.attributes||!O?e.getAttribute(n):(o=e.getAttributeNode(n))&&o.specified?o.value:null:o},n.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},n.uniqueSort=function(e){var t,n=[],r=0,o=0;if(W=!S.detectDuplicates,$=!S.sortStable&&e.slice(0),e.sort(Y),W){for(;t=e[o++];)t===e[o]&&(r=n.push(o));for(;r--;)e.splice(n[r],1)}return e},B=n.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=B(e)}else if(3===o||4===o)return e.nodeValue}else for(;t=e[r];r++)n+=B(t);return n},L=n.selectors={cacheLength:50,createPseudo:i,match:bt,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(At,St),e[3]=(e[4]||e[5]||"").replace(At,St),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||n.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&n.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return bt.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&vt.test(r)&&(n=g(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(At,St).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=J[e+" "];return t||(t=RegExp("(^|"+at+")"+e+"("+at+"|$)"))&&J(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==Z&&e.getAttribute("class")||"")})},ATTR:function(e,t,r){return function(o){var i=n.attr(o,e);return null==i?"!="===t:t?(i+="","="===t?i===r:"!="===t?i!==r:"^="===t?r&&0===i.indexOf(r):"*="===t?r&&i.indexOf(r)>-1:"$="===t?r&&i.slice(-r.length)===r:"~="===t?(" "+i+" ").indexOf(r)>-1:"|="===t?i===r||i.slice(0,r.length+1)===r+"-":!1):!0}},CHILD:function(e,t,n,r,o){var i="nth"!==e.slice(0,3),u="last"!==e.slice(-4),l="of-type"===t;return 1===r&&0===o?function(e){return!!e.parentNode}:function(t,n,a){var c,s,f,p,d,h,g=i!==u?"nextSibling":"previousSibling",m=t.parentNode,y=l&&t.nodeName.toLowerCase(),v=!a&&!l;if(m){if(i){for(;g;){for(f=t;f=f[g];)if(l?f.nodeName.toLowerCase()===y:1===f.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[u?m.firstChild:m.lastChild],u&&v){for(s=m[G]||(m[G]={}),c=s[e]||[],d=c[0]===V&&c[1],p=c[0]===V&&c[2],f=d&&m.childNodes[d];f=++d&&f&&f[g]||(p=d=0)||h.pop();)if(1===f.nodeType&&++p&&f===t){s[e]=[V,d,p];break}}else if(v&&(c=(t[G]||(t[G]={}))[e])&&c[0]===V)p=c[1];else for(;(f=++d&&f&&f[g]||(p=d=0)||h.pop())&&((l?f.nodeName.toLowerCase()!==y:1!==f.nodeType)||!++p||(v&&((f[G]||(f[G]={}))[e]=[V,p]),f!==t)););return p-=o,p===r||0===p%r&&p/r>=0}}},PSEUDO:function(e,t){var r,o=L.pseudos[e]||L.setFilters[e.toLowerCase()]||n.error("unsupported pseudo: "+e);return o[G]?o(t):o.length>1?(r=[e,e,"",t],L.setFilters.hasOwnProperty(e.toLowerCase())?i(function(e,n){for(var r,i=o(e,t),u=i.length;u--;)r=ut.call(e,i[u]),e[r]=!(n[r]=i[u])}):function(e){return o(e,0,r)}):o}},pseudos:{not:i(function(e){var t=[],n=[],r=R(e.replace(dt,"$1"));return r[G]?i(function(e,t,n,o){for(var i,u=r(e,null,o,[]),l=e.length;l--;)(i=u[l])&&(e[l]=!(t[l]=i))}):function(e,o,i){return t[0]=e,r(t,null,i,n),!n.pop()}}),has:i(function(e){return function(t){return n(e,t).length>0}}),contains:i(function(e){return function(t){return(t.textContent||t.innerText||B(t)).indexOf(e)>-1}}),lang:i(function(e){return Nt.test(e||"")||n.error("unsupported lang: "+e),e=e.replace(At,St).toLowerCase(),function(t){var n;do if(n=O?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===M},focus:function(e){return e===H.activeElement&&(!H.hasFocus||H.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!L.pseudos.empty(e)},header:function(e){return wt.test(e.nodeName)},input:function(e){return Et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:h(function(){return[0]}),last:h(function(e,t){return[t-1]}),eq:h(function(e,t,n){return[0>n?n+t:n]}),even:h(function(e,t){for(var n=0;t>n;n+=2)e.push(n);return e}),odd:h(function(e,t){for(var n=1;t>n;n+=2)e.push(n);return e}),lt:h(function(e,t,n){for(var r=0>n?n+t:n;--r>=0;)e.push(r);return e}),gt:h(function(e,t,n){for(var r=0>n?n+t:n;t>++r;)e.push(r);return e})}};for(A in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})L.pseudos[A]=p(A);for(A in{submit:!0,reset:!0})L.pseudos[A]=d(A);R=n.compile=function(e,t){var n,r=[],o=[],i=Q[e+" "];if(!i){for(t||(t=g(e)),n=t.length;n--;)i=x(t[n]),i[G]?r.push(i):o.push(i);i=Q(e,C(o,r))}return i},L.pseudos.nth=L.pseudos.eq,T.prototype=L.filters=L.pseudos,L.setFilters=new T,S.sortStable=G.split("").sort(Y).join("")===G,q(),[0,0].sort(Y),S.detectDuplicates=W,"function"==typeof define&&define.amd?define(function(){return n}):e.Sizzle=n})(window); + </script> + + <script type="text/javascript"> +window.onload = function() { + var codeBlocks = Sizzle('pre'), + callStackItems = Sizzle('.call-stack-item'); + + // highlight code blocks + for (var i = 0, imax = codeBlocks.length; i < imax; ++i) { + hljs.highlightBlock(codeBlocks[i], ' '); + } + + // code block hover line + document.onmousemove = function(e) { + var event = e || window.event, + clientY = event.clientY, + lineFound = false, + hoverLines = Sizzle('.hover-line'); + + for (var i = 0, imax = codeBlocks.length - 1; i < imax; ++i) { + var lines = codeBlocks[i].getClientRects(); + for (var j = 0, jmax = lines.length; j < jmax; ++j) { + if (clientY >= lines[j].top && clientY <= lines[j].bottom) { + lineFound = true; + break; + } + } + if (lineFound) { + break; + } + } + + for (var k = 0, kmax = hoverLines.length; k < kmax; ++k) { + hoverLines[k].className = 'hover-line'; + } + if (lineFound) { + var line = Sizzle('.call-stack-item:eq(' + i + ') .hover-line:eq(' + j + ')')[0]; + if (line) { + line.className = 'hover-line hover'; + } + } + }; + + var refreshCallStackItemCode = function(callStackItem) { + if (!Sizzle('pre', callStackItem)[0]) { + return; + } + var top = callStackItem.offsetTop - window.pageYOffset, + lines = Sizzle('pre', callStackItem)[0].getClientRects(), + lineNumbers = Sizzle('.lines-item', callStackItem), + errorLine = Sizzle('.error-line', callStackItem)[0], + hoverLines = Sizzle('.hover-line', callStackItem); + for (var i = 0, imax = lines.length; i < imax; ++i) { + if (!lineNumbers[i]) { + continue; + } + lineNumbers[i].style.top = parseInt(lines[i].top - top) + 'px'; + hoverLines[i].style.top = parseInt(lines[i].top - top - 3) + 'px'; + hoverLines[i].style.height = parseInt(lines[i].bottom - lines[i].top + 6) + 'px'; + if (parseInt(callStackItem.getAttribute('data-line')) == i) { + errorLine.style.top = parseInt(lines[i].top - top - 3) + 'px'; + errorLine.style.height = parseInt(lines[i].bottom - lines[i].top + 6) + 'px'; + } + } + }; + + for (var i = 0, imax = callStackItems.length; i < imax; ++i) { + refreshCallStackItemCode(callStackItems[i]); + + // toggle code block visibility + Sizzle('.element-wrap', callStackItems[i])[0].addEventListener('click', function() { + var callStackItem = this.parentNode, + code = Sizzle('.code-wrap', callStackItem)[0]; + code.style.display = window.getComputedStyle(code).display == 'block' ? 'none' : 'block'; + refreshCallStackItemCode(callStackItem); + }); + } +}; + </script> + <?php if (method_exists($this, 'endBody')) $this->endBody(); // to allow injecting code into body (mostly by Yii Debug Toolbar) ?> +</body> + +</html> +<?php if (method_exists($this, 'endPage')) $this->endPage(); ?> diff --git a/framework/yii/views/errorHandler/previousException.php b/framework/yii/views/errorHandler/previousException.php new file mode 100644 index 0000000..e6dcf87 --- /dev/null +++ b/framework/yii/views/errorHandler/previousException.php @@ -0,0 +1,21 @@ +<?php +/** + * @var \yii\base\Exception $exception + * @var \yii\base\ErrorHandler $handler + */ +?> +<div class="previous"> + <span class="arrow">↵</span> + <h2> + <span>Caused by:</span> + <?php if ($exception instanceof \yii\base\Exception): ?> + <span><?php echo $handler->htmlEncode($exception->getName()); ?></span> – + <?php echo $handler->addTypeLinks(get_class($exception)); ?> + <?php else: ?> + <span><?php echo $handler->htmlEncode(get_class($exception)); ?></span> + <?php endif; ?> + </h2> + <h3><?php echo $handler->htmlEncode($exception->getMessage()); ?></h3> + <p>in <span class="file"><?php echo $exception->getFile(); ?></span> at line <span class="line"><?php echo $exception->getLine(); ?></span></p> + <?php echo $handler->renderPreviousExceptions($exception); ?> +</div> diff --git a/framework/yii/views/messageConfig.php b/framework/yii/views/messageConfig.php new file mode 100644 index 0000000..d0d515a --- /dev/null +++ b/framework/yii/views/messageConfig.php @@ -0,0 +1,45 @@ +<?php + +return array( + // string, required, root directory of all source files + 'sourcePath' => __DIR__, + // string, required, root directory containing message translations. + 'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages', + // array, required, list of language codes that the extracted messages + // should be translated to. For example, array('zh_cn', 'de'). + 'languages' => array('de'), + // string, the name of the function for translating messages. + // Defaults to 'Yii::t'. This is used as a mark to find the messages to be + // translated. You may use a string for single function name or an array for + // multiple function names. + 'translator' => 'Yii::t', + // boolean, whether to sort messages by keys when merging new messages + // with the existing ones. Defaults to false, which means the new (untranslated) + // messages will be separated from the old (translated) ones. + 'sort' => false, + // boolean, whether the message file should be overwritten with the merged messages + 'overwrite' => true, + // boolean, whether to remove messages that no longer appear in the source code. + // Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks. + 'removeUnused' => false, + // array, list of patterns that specify which files/directories should be processed. + // If empty or not set, all files/directories will be processed. + // A path matches a pattern if it contains the pattern string at its end. For example, + // '/a/b' will match all files and directories ending with '/a/b'; + // and the '.svn' will match all files and directories whose name ends with '.svn'. + // Note, the '/' characters in a pattern matches both '/' and '\'. + // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed. + 'only' => array('.php'), + // array, list of patterns that specify which files/directories should NOT be processed. + // If empty or not set, all files/directories will be processed. + // Please refer to "only" for details about the patterns. + 'except' => array( + '.svn', + '.git', + '.gitignore', + '.gitkeep', + '.hgignore', + '.hgkeep', + '/messages', + ), +); diff --git a/yii/views/migration.php b/framework/yii/views/migration.php similarity index 100% rename from yii/views/migration.php rename to framework/yii/views/migration.php diff --git a/yii/web/AccessControl.php b/framework/yii/web/AccessControl.php similarity index 74% rename from yii/web/AccessControl.php rename to framework/yii/web/AccessControl.php index e890510..75e2dc9 100644 --- a/yii/web/AccessControl.php +++ b/framework/yii/web/AccessControl.php @@ -10,9 +10,43 @@ namespace yii\web; use Yii; use yii\base\Action; use yii\base\ActionFilter; -use yii\base\HttpException; /** + * AccessControl provides simple access control based on a set of rules. + * + * AccessControl is an action filter. It will check its [[rules]] to find + * the first rule that matches the current context variables (such as user IP address, user role). + * The matching rule will dictate whether to allow or deny the access to the requested controller + * action. If no rule matches, the access will be denied. + * + * To use AccessControl, declare it in the `behaviors()` method of your controller class. + * For example, the following declarations will allow authenticated users to access the "create" + * and "update" actions and deny all other users from accessing these two actions. + * + * ~~~ + * public function behaviors() + * { + * return array( + * 'access' => array( + * 'class' => \yii\web\AccessControl::className(), + * 'only' => array('create', 'update'), + * 'rules' => array( + * // deny all POST requests + * array( + * 'allow' => false, + * 'verbs' => array('POST') + * ), + * // allow authenticated users + * array( + * 'allow' => true, + * 'roles' => array('@'), + * ), + * // everything else is denied + * ), + * ), + * ); + * } + * ~~~ * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -73,7 +107,7 @@ class AccessControl extends ActionFilter /** @var $rule AccessRule */ foreach ($this->rules as $rule) { if ($allow = $rule->allows($action, $user, $request)) { - break; + return true; } elseif ($allow === false) { if (isset($rule->denyCallback)) { call_user_func($rule->denyCallback, $rule); @@ -85,7 +119,13 @@ class AccessControl extends ActionFilter return false; } } - return true; + if (isset($this->denyCallback)) { + call_user_func($this->denyCallback, $rule); + } + else { + $this->denyAccess($user); + } + return false; } /** @@ -100,7 +140,7 @@ class AccessControl extends ActionFilter if ($user->getIsGuest()) { $user->loginRequired(); } else { - throw new HttpException(403, Yii::t('yii|You are not allowed to perform this action.')); + throw new HttpException(403, Yii::t('yii', 'You are not allowed to perform this action.')); } } } diff --git a/yii/web/AccessRule.php b/framework/yii/web/AccessRule.php similarity index 96% rename from yii/web/AccessRule.php rename to framework/yii/web/AccessRule.php index e565e18..7aeaac1 100644 --- a/yii/web/AccessRule.php +++ b/framework/yii/web/AccessRule.php @@ -11,6 +11,7 @@ use yii\base\Component; use yii\base\Action; /** + * This class represents an access rule defined by the [[AccessControl]] action filter * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -39,7 +40,7 @@ class AccessRule extends Component * - `@`: matches an authenticated user * * Using additional role names requires RBAC (Role-Based Access Control), and - * [[User::hasAccess()]] will be called. + * [[User::checkAccess()]] will be called. * * If this property is not set or empty, it means this rule applies to all roles. */ @@ -99,7 +100,7 @@ class AccessRule extends Component if ($this->matchAction($action) && $this->matchRole($user) && $this->matchIP($request->getUserIP()) - && $this->matchVerb($request->getRequestMethod()) + && $this->matchVerb($request->getMethod()) && $this->matchController($action->controller) && $this->matchCustom($action) ) { @@ -124,7 +125,7 @@ class AccessRule extends Component */ protected function matchController($controller) { - return empty($this->controllers) || in_array($controller->id, $this->controllers, true); + return empty($this->controllers) || in_array($controller->uniqueId, $this->controllers, true); } /** diff --git a/yii/web/Application.php b/framework/yii/web/Application.php similarity index 60% rename from yii/web/Application.php rename to framework/yii/web/Application.php index 3387044..b0638a7 100644 --- a/yii/web/Application.php +++ b/framework/yii/web/Application.php @@ -8,9 +8,17 @@ namespace yii\web; use Yii; +use yii\base\InvalidRouteException; /** - * Application is the base class for all application classes. + * Application is the base class for all web application classes. + * + * @property AssetManager $assetManager The asset manager component. This property is read-only. + * @property string $homeUrl The homepage URL. + * @property Request $request The request component. This property is read-only. + * @property Response $response The response component. This property is read-only. + * @property Session $session The session component. This property is read-only. + * @property User $user The user component. This property is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -21,18 +29,64 @@ class Application extends \yii\base\Application * @var string the default route of this application. Defaults to 'site'. */ public $defaultRoute = 'site'; + /** + * @var array the configuration specifying a controller action which should handle + * all user requests. This is mainly used when the application is in maintenance mode + * and needs to handle all incoming requests via a single action. + * The configuration is an array whose first element specifies the route of the action. + * The rest of the array elements (key-value pairs) specify the parameters to be bound + * to the action. For example, + * + * ~~~ + * array( + * 'offline/notice', + * 'param1' => 'value1', + * 'param2' => 'value2', + * ) + * ~~~ + * + * Defaults to null, meaning catch-all is not used. + */ + public $catchAll; + /** + * @var Controller the currently active controller instance + */ + public $controller; + /** - * Processes the request. - * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) + * Handles the specified request. + * @param Request $request the request to be handled + * @return Response the resulting response + * @throws HttpException if the requested route is invalid */ - public function processRequest() + public function handleRequest($request) { - $request = $this->getRequest(); - Yii::setAlias('@wwwroot', dirname($request->getScriptFile())); - Yii::setAlias('@www', $request->getBaseUrl()); - list ($route, $params) = $request->resolve(); - return $this->runAction($route, $params); + Yii::setAlias('@webroot', dirname($request->getScriptFile())); + Yii::setAlias('@web', $request->getBaseUrl()); + + if (empty($this->catchAll)) { + list ($route, $params) = $request->resolve(); + } else { + $route = $this->catchAll[0]; + $params = array_splice($this->catchAll, 1); + } + try { + Yii::trace("Route requested: '$route'", __METHOD__); + $this->requestedRoute = $route; + $result = $this->runAction($route, $params); + if ($result instanceof Response) { + return $result; + } else { + $response = $this->getResponse(); + if ($result !== null) { + $response->data = $result; + } + return $response; + } + } catch (InvalidRouteException $e) { + throw new HttpException(404, $e->getMessage(), $e->getCode(), $e); + } } private $_homeUrl; diff --git a/yii/web/AssetBundle.php b/framework/yii/web/AssetBundle.php similarity index 88% rename from yii/web/AssetBundle.php rename to framework/yii/web/AssetBundle.php index 37577dd..aa2d02b 100644 --- a/yii/web/AssetBundle.php +++ b/framework/yii/web/AssetBundle.php @@ -10,6 +10,7 @@ namespace yii\web; use Yii; use yii\base\InvalidConfigException; use yii\base\Object; +use yii\base\View; /** * AssetBundle represents a collection of asset files, such as CSS, JS, images. @@ -45,7 +46,7 @@ class AssetBundle extends Object * 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 + * then this property must be set either manually or automatically (by [[AssetManager]] via * asset publishing). * * You can use either a directory or an alias of the directory. @@ -102,7 +103,17 @@ class AssetBundle extends Object public $publishOptions = array(); /** + * @param View $view + * @return AssetBundle the registered asset bundle instance + */ + public static function register($view) + { + return $view->registerAssetBundle(get_called_class()); + } + + /** * Initializes the bundle. + * If you override this method, make sure you call the parent implementation in the last. */ public function init() { @@ -119,7 +130,6 @@ class AssetBundle extends Object /** * 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. @@ -128,17 +138,13 @@ class AssetBundle extends Object */ public function registerAssets($view) { - foreach ($this->depends as $name) { - $view->registerAssetBundle($name); - } - $this->publish($view->getAssetManager()); foreach ($this->js as $js) { - $view->registerJsFile($js, $this->jsOptions); + $view->registerJsFile($this->baseUrl . '/' . $js, $this->jsOptions); } foreach ($this->css as $css) { - $view->registerCssFile($css, $this->cssOptions); + $view->registerCssFile($this->baseUrl . '/' . $css, $this->cssOptions); } } @@ -150,7 +156,7 @@ class AssetBundle extends Object */ public function publish($am) { - if ($this->sourcePath !== null) { + if ($this->sourcePath !== null && !isset($this->basePath, $this->baseUrl)) { list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOptions); } $converter = $am->getConverter(); diff --git a/yii/web/AssetConverter.php b/framework/yii/web/AssetConverter.php similarity index 84% rename from yii/web/AssetConverter.php rename to framework/yii/web/AssetConverter.php index 4fde1fc..420a5bc 100644 --- a/yii/web/AssetConverter.php +++ b/framework/yii/web/AssetConverter.php @@ -16,7 +16,7 @@ use yii\base\Component; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class AssetConverter extends Component implements IAssetConverter +class AssetConverter extends Component implements AssetConverterInterface { /** * @var array the commands that are used to perform the asset conversion. @@ -34,10 +34,9 @@ class AssetConverter extends Component implements 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. + * @return string the converted asset file path, relative to $basePath. */ - public function convert($asset, $basePath, $baseUrl) + public function convert($asset, $basePath) { $pos = strrpos($asset, '.'); if ($pos !== false) { @@ -48,15 +47,15 @@ class AssetConverter extends Component implements IAssetConverter if (@filemtime("$basePath/$result") < filemtime("$basePath/$asset")) { $output = array(); $command = strtr($command, array( - '{from}' => "$basePath/$asset", - '{to}' => "$basePath/$result", + '{from}' => escapeshellarg("$basePath/$asset"), + '{to}' => escapeshellarg("$basePath/$result"), )); exec($command, $output); - Yii::info("Converted $asset into $result: " . implode("\n", $output), __METHOD__); + Yii::trace("Converted $asset into $result: " . implode("\n", $output), __METHOD__); } - return "$baseUrl/$result"; + return $result; } } - return "$baseUrl/$asset"; + return $asset; } } diff --git a/yii/web/IAssetConverter.php b/framework/yii/web/AssetConverterInterface.php similarity index 77% rename from yii/web/IAssetConverter.php rename to framework/yii/web/AssetConverterInterface.php index d1d1da0..51309c6 100644 --- a/yii/web/IAssetConverter.php +++ b/framework/yii/web/AssetConverterInterface.php @@ -8,20 +8,18 @@ namespace yii\web; /** - * The IAssetConverter interface must be implemented by asset converter classes. + * The AssetConverterInterface must be implemented by asset converter classes. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -interface IAssetConverter +interface AssetConverterInterface { /** * 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. + * @return string the converted asset file path, relative to $basePath. */ - public function convert($asset, $basePath, $baseUrl); + public function convert($asset, $basePath); } diff --git a/yii/web/AssetManager.php b/framework/yii/web/AssetManager.php similarity index 73% rename from yii/web/AssetManager.php rename to framework/yii/web/AssetManager.php index 95dcbd2..35e555f 100644 --- a/yii/web/AssetManager.php +++ b/framework/yii/web/AssetManager.php @@ -16,24 +16,28 @@ use yii\helpers\FileHelper; /** * AssetManager manages asset bundles and asset publishing. * + * @property AssetConverterInterface $converter The asset converter. Note that the type of this property + * differs in getter and setter. See [[getConverter()]] and [[setConverter()]] for details. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ class AssetManager extends Component { /** - * @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. + * @var array list of available asset bundles. The keys are the class names of the asset bundles, + * and the values are either the configuration arrays for creating the [[AssetBundle]] objects + * or the corresponding asset bundle instances. */ - public $bundles; + public $bundles = array(); /** * @return string the root directory storing the published asset files. */ - public $basePath = '@wwwroot/assets'; + public $basePath = '@webroot/assets'; /** * @return string the base URL through which the published asset files can be accessed. */ - public $baseUrl = '@www/assets'; + public $baseUrl = '@web/assets'; /** * @var boolean whether to use symbolic link to publish asset files. Defaults to false, meaning * asset files are copied to [[basePath]]. Using symbolic links has the benefit that the published @@ -54,16 +58,17 @@ class AssetManager extends Component public $linkAssets = false; /** * @var integer the permission to be set for newly published asset files. - * This value will be used by PHP chmod() function. + * This value will be used by PHP chmod() function. No umask will be applied. * If not set, the permission will be determined by the current environment. */ public $fileMode; /** * @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. + * This value will be used by PHP chmod() function. No umask will be applied. + * Defaults to 0775, meaning the directory is read-writable by owner and group, + * but read-only for other users. */ - public $dirMode = 0777; + public $dirMode = 0775; /** * Initializes the component. @@ -81,55 +86,29 @@ class AssetManager extends Component $this->basePath = realpath($this->basePath); } $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); - - foreach (require(YII_PATH . '/assets.php') as $name => $bundle) { - if (!isset($this->bundles[$name])) { - $this->bundles[$name] = $bundle; - } - } } /** - * 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. + * Returns the named asset bundle. * - * For example, given the bundle name "foo/button", the method will first convert it - * into the path alias "@foo/button"; since "@foo" is the root alias, it will look - * for the bundle manifest file "@foo/assets.php". The manifest file should return an array - * that lists the bundles used by the "foo/button" extension. The array format is the same as [[bundles]]. + * This method will first look for the bundle in [[bundles]]. If not found, + * it will treat `$name` as the class of the asset bundle and create a new instance of it. * - * @param string $name the bundle name - * @return AssetBundle the loaded bundle object. Null is returned if the bundle does not exist. + * @param string $name the class name of the asset bundle + * @return AssetBundle the asset bundle instance + * @throws InvalidConfigException if $name does not refer to a valid asset bundle */ public function getBundle($name) { - if (!isset($this->bundles[$name])) { - $rootAlias = Yii::getRootAlias("@$name"); - if ($rootAlias !== false) { - $manifest = Yii::getAlias("$rootAlias/assets.php", false); - if ($manifest !== false && is_file($manifest)) { - foreach (require($manifest) as $bn => $config) { - $this->bundles[$bn] = $config; - } - } - } - if (!isset($this->bundles[$name])) { - return null; - } - } - if (is_array($this->bundles[$name])) { - $config = $this->bundles[$name]; - if (!isset($config['class'])) { - $config['class'] = 'yii\\web\\AssetBundle'; - $this->bundles[$name] = Yii::createObject($config); + if (isset($this->bundles[$name])) { + if (is_array($this->bundles[$name])) { + $this->bundles[$name] = Yii::createObject(array_merge(array('class' => $name), $this->bundles[$name])); + } elseif (!$this->bundles[$name] instanceof AssetBundle) { + throw new InvalidConfigException("Invalid asset bundle: $name"); } + } else { + $this->bundles[$name] = Yii::createObject($name); } - return $this->bundles[$name]; } @@ -137,14 +116,12 @@ class AssetManager extends Component /** * Returns the asset converter. - * @return IAssetConverter the asset converter. + * @return AssetConverterInterface the asset converter. */ public function getConverter() { if ($this->_converter === null) { - $this->_converter = Yii::createObject(array( - 'class' => 'yii\\web\\AssetConverter', - )); + $this->_converter = Yii::createObject(AssetConverter::className()); } elseif (is_array($this->_converter) || is_string($this->_converter)) { $this->_converter = Yii::createObject($this->_converter); } @@ -153,8 +130,8 @@ class AssetManager extends Component /** * Sets the asset converter. - * @param array|IAssetConverter $value the asset converter. This can be either - * an object implementing the [[IAssetConverter]] interface, or a configuration + * @param array|AssetConverterInterface $value the asset converter. This can be either + * an object implementing the [[AssetConverterInterface]], or a configuration * array that can be used to create the asset converter object. */ public function setConverter($value) @@ -229,7 +206,7 @@ class AssetManager extends Component $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName; if (!is_dir($dstDir)) { - mkdir($dstDir, $this->dirMode, true); + FileHelper::createDirectory($dstDir, $this->dirMode, true); } if ($this->linkAssets) { @@ -282,6 +259,9 @@ class AssetManager extends Component */ public function getPublishedPath($path) { + if (isset($this->_published[$path])) { + return $this->_published[$path][0]; + } if (($path = realpath($path)) !== false) { $base = $this->basePath . DIRECTORY_SEPARATOR; if (is_file($path)) { @@ -304,7 +284,7 @@ class AssetManager extends Component public function getPublishedUrl($path) { if (isset($this->_published[$path])) { - return $this->_published[$path]; + return $this->_published[$path][1]; } if (($path = realpath($path)) !== false) { if (is_file($path)) { diff --git a/framework/yii/web/CacheSession.php b/framework/yii/web/CacheSession.php new file mode 100644 index 0000000..b4ce2ae --- /dev/null +++ b/framework/yii/web/CacheSession.php @@ -0,0 +1,108 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\caching\Cache; +use yii\base\InvalidConfigException; + +/** + * CacheSession implements a session component using cache as storage medium. + * + * The cache being used can be any cache application component. + * The ID of the cache application component is specified via [[cache]], which defaults to 'cache'. + * + * Beware, by definition cache storage are volatile, which means the data stored on them + * may be swapped out and get lost. Therefore, you must make sure the cache used by this component + * is NOT volatile. If you want to use database as storage medium, use [[DbSession]] is a better choice. + * + * @property boolean $useCustomStorage Whether to use custom storage. This property is read-only. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class CacheSession extends Session +{ + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * The session data will be stored using this cache object. + * + * After the CacheSession object is created, if you want to change this property, + * you should only assign it with a cache object. + */ + public $cache = 'cache'; + + /** + * Initializes the application component. + */ + public function init() + { + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + if (!$this->cache instanceof Cache) { + throw new InvalidConfigException('CacheSession::cache must refer to the application component ID of a cache object.'); + } + parent::init(); + } + + /** + * Returns a value indicating whether to use custom session storage. + * This method overrides the parent implementation and always returns true. + * @return boolean whether to use custom storage. + */ + public function getUseCustomStorage() + { + return true; + } + + /** + * Session read handler. + * Do not call this method directly. + * @param string $id session ID + * @return string the session data + */ + public function readSession($id) + { + $data = $this->cache->get($this->calculateKey($id)); + return $data === false ? '' : $data; + } + + /** + * Session write handler. + * Do not call this method directly. + * @param string $id session ID + * @param string $data session data + * @return boolean whether session write is successful + */ + public function writeSession($id, $data) + { + return $this->cache->set($this->calculateKey($id), $data, $this->getTimeout()); + } + + /** + * Session destroy handler. + * Do not call this method directly. + * @param string $id session ID + * @return boolean whether session is destroyed successfully + */ + public function destroySession($id) + { + return $this->cache->delete($this->calculateKey($id)); + } + + /** + * Generates a unique key used for storing session data in cache. + * @param string $id session variable name + * @return mixed a safe cache key associated with the session variable name + */ + protected function calculateKey($id) + { + return array(__CLASS__, $id); + } +} diff --git a/framework/yii/web/Controller.php b/framework/yii/web/Controller.php new file mode 100644 index 0000000..a6eaf3f --- /dev/null +++ b/framework/yii/web/Controller.php @@ -0,0 +1,172 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\InlineAction; +use yii\helpers\Html; + +/** + * Controller is the base class of web controllers. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Controller extends \yii\base\Controller +{ + /** + * @var boolean whether to enable CSRF validation for the actions in this controller. + * CSRF validation is enabled only when both this property and [[Request::enableCsrfValidation]] are true. + */ + public $enableCsrfValidation = true; + + /** + * Binds the parameters to the action. + * This method is invoked by [[Action]] when it begins to run with the given parameters. + * This method will check the parameter names that the action requires and return + * the provided parameters according to the requirement. If there is any missing parameter, + * an exception will be thrown. + * @param \yii\base\Action $action the action to be bound with parameters + * @param array $params the parameters to be bound to the action + * @return array the valid parameters that the action can run with. + * @throws HttpException if there are missing parameters. + */ + public function bindActionParams($action, $params) + { + if ($action instanceof InlineAction) { + $method = new \ReflectionMethod($this, $action->actionMethod); + } else { + $method = new \ReflectionMethod($action, 'run'); + } + + $args = array(); + $missing = array(); + foreach ($method->getParameters() as $param) { + $name = $param->getName(); + if (array_key_exists($name, $params)) { + $args[] = $params[$name]; + unset($params[$name]); + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } else { + $missing[] = $name; + } + } + + if (!empty($missing)) { + throw new HttpException(400, Yii::t('yii', 'Missing required parameters: {params}', array( + 'params' => implode(', ', $missing), + ))); + } + + return $args; + } + + /** + * @inheritdoc + */ + public function beforeAction($action) + { + if (parent::beforeAction($action)) { + if ($this->enableCsrfValidation && !Yii::$app->getRequest()->validateCsrfToken()) { + throw new HttpException(400, Yii::t('yii', 'Unable to verify your data submission.')); + } + return true; + } else { + return false; + } + } + + /** + * Creates a URL using the given route and parameters. + * + * This method enhances [[UrlManager::createUrl()]] by supporting relative routes. + * A relative route is a route without a leading slash, such as "view", "post/view". + * + * - If the route is an empty string, the current [[route]] will be used; + * - If the route contains no slashes at all, it is considered to be an action ID + * of the current controller and will be prepended with [[uniqueId]]; + * - If the route has no leading slash, it is considered to be a route relative + * to the current module and will be prepended with the module's uniqueId. + * + * After this route conversion, the method calls [[UrlManager::createUrl()]] to create a URL. + * + * @param string $route the route. This can be either an absolute route or a relative route. + * @param array $params the parameters (name-value pairs) to be included in the generated URL + * @return string the created URL + */ + public function createUrl($route, $params = array()) + { + if (strpos($route, '/') === false) { + // empty or an action ID + $route = $route === '' ? $this->getRoute() : $this->getUniqueId() . '/' . $route; + } elseif ($route[0] !== '/') { + // relative to module + $route = ltrim($this->module->getUniqueId() . '/' . $route, '/'); + } + return Yii::$app->getUrlManager()->createUrl($route, $params); + } + + /** + * Redirects the browser to the specified URL. + * This method is a shortcut to [[Response::redirect()]]. + * + * @param string|array $url the URL to be redirected to. This can be in one of the following formats: + * + * - a string representing a URL (e.g. "http://example.com") + * - a string representing a URL alias (e.g. "@example.com") + * - an array in the format of `array($route, ...name-value pairs...)` (e.g. `array('site/index', 'ref' => 1)`) + * [[Html::url()]] will be used to convert the array into a URL. + * + * Any relative URL will be converted into an absolute one by prepending it with the host info + * of the current request. + * + * @param integer $statusCode the HTTP status code. If null, it will use 302. + * See [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html]] + * for details about HTTP status code + * @return Response the current response object + */ + public function redirect($url, $statusCode = null) + { + return Yii::$app->getResponse()->redirect(Html::url($url), $statusCode); + } + + /** + * Redirects the browser to the home page. + * @return Response the current response object + */ + public function goHome() + { + return Yii::$app->getResponse()->redirect(Yii::$app->getHomeUrl()); + } + + /** + * Redirects the browser to the last visited page. + * @param string|array $defaultUrl the default return URL in case it was not set previously. + * If this is null and the return URL was not set previously, [[Application::homeUrl]] will be redirected to. + * Please refer to [[User::setReturnUrl()]] on accepted format of the URL. + * @return Response the current response object + * @see User::getReturnUrl() + */ + public function goBack($defaultUrl = null) + { + return Yii::$app->getResponse()->redirect(Yii::$app->getUser()->getReturnUrl($defaultUrl)); + } + + /** + * Refreshes the current page. + * This method is a shortcut to [[Response::refresh()]]. + * @param string $anchor the anchor that should be appended to the redirection URL. + * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it. + * @return Response the response object itself + */ + public function refresh($anchor = '') + { + return Yii::$app->getResponse()->redirect(Yii::$app->getRequest()->getUrl() . $anchor); + } +} diff --git a/yii/web/Cookie.php b/framework/yii/web/Cookie.php similarity index 98% rename from yii/web/Cookie.php rename to framework/yii/web/Cookie.php index 610e5aa..8cbb412 100644 --- a/yii/web/Cookie.php +++ b/framework/yii/web/Cookie.php @@ -45,7 +45,7 @@ class Cookie extends \yii\base\Object * By setting this property to true, the cookie will not be accessible by scripting languages, * such as JavaScript, which can effectively help to reduce identity theft through XSS attacks. */ - public $httponly = false; + public $httpOnly = false; /** * Magic method to turn a cookie object into a string without having to explicitly access [[value]]. diff --git a/yii/web/CookieCollection.php b/framework/yii/web/CookieCollection.php similarity index 69% rename from yii/web/CookieCollection.php rename to framework/yii/web/CookieCollection.php index c76926b..6940493 100644 --- a/yii/web/CookieCollection.php +++ b/framework/yii/web/CookieCollection.php @@ -8,28 +8,26 @@ namespace yii\web; use Yii; -use yii\base\DictionaryIterator; -use yii\helpers\SecurityHelper; +use ArrayIterator; +use yii\base\InvalidCallException; +use yii\base\Object; /** * CookieCollection maintains the cookies available in the current request. * - * @property integer $count the number of cookies in the collection + * @property integer $count The number of cookies in the collection. This property is read-only. + * @property ArrayIterator $iterator An iterator for traversing the cookies in the collection. This property + * is read-only. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ArrayAccess, \Countable +class CookieCollection extends Object implements \IteratorAggregate, \ArrayAccess, \Countable { /** - * @var boolean whether to enable cookie validation. By setting this property to true, - * if a cookie is tampered on the client side, it will be ignored when received on the server side. + * @var boolean whether this collection is read only. */ - public $enableValidation = true; - /** - * @var string the secret key used for cookie validation. If not set, a random key will be generated and used. - */ - public $validationKey; + public $readOnly = false; /** * @var Cookie[] the cookies in this collection (indexed by the cookie names) @@ -38,23 +36,25 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ /** * Constructor. + * @param array $cookies the cookies that this collection initially contains. This should be + * an array of name-value pairs.s * @param array $config name-value pairs that will be used to initialize the object properties */ - public function __construct($config = array()) + public function __construct($cookies = array(), $config = array()) { + $this->_cookies = $cookies; parent::__construct($config); - $this->_cookies = $this->loadCookies(); } /** * Returns an iterator for traversing the cookies in the collection. * This method is required by the SPL interface `IteratorAggregate`. * It will be implicitly called when you use `foreach` to traverse the collection. - * @return DictionaryIterator an iterator for traversing the cookies in the collection. + * @return ArrayIterator an iterator for traversing the cookies in the collection. */ public function getIterator() { - return new DictionaryIterator($this->_cookies); + return new ArrayIterator($this->_cookies); } /** @@ -101,53 +101,66 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ } /** + * Returns whether there is a cookie with the specified name. + * @param string $name the cookie name + * @return boolean whether the named cookie exists + */ + public function has($name) + { + return isset($this->_cookies[$name]); + } + + /** * Adds a cookie to the collection. * If there is already a cookie with the same name in the collection, it will be removed first. * @param Cookie $cookie the cookie to be added + * @throws InvalidCallException if the cookie collection is read only */ public function add($cookie) { - if (isset($this->_cookies[$cookie->name])) { - $c = $this->_cookies[$cookie->name]; - setcookie($c->name, '', 0, $c->path, $c->domain, $c->secure, $c->httponly); - } - - $value = $cookie->value; - if ($this->enableValidation) { - if ($this->validationKey === null) { - $key = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id); - } else { - $key = $this->validationKey; - } - $value = SecurityHelper::hashData(serialize($value), $key); + if ($this->readOnly) { + throw new InvalidCallException('The cookie collection is read only.'); } - - setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly); $this->_cookies[$cookie->name] = $cookie; } /** - * Removes a cookie from the collection. + * Removes a cookie. + * If `$removeFromBrowser` is true, the cookie will be removed from the browser. + * In this case, a cookie with outdated expiry will be added to the collection. * @param Cookie|string $cookie the cookie object or the name of the cookie to be removed. + * @param boolean $removeFromBrowser whether to remove the cookie from browser + * @throws InvalidCallException if the cookie collection is read only */ - public function remove($cookie) + public function remove($cookie, $removeFromBrowser = true) { - if (is_string($cookie) && isset($this->_cookies[$cookie])) { - $cookie = $this->_cookies[$cookie]; + if ($this->readOnly) { + throw new InvalidCallException('The cookie collection is read only.'); } if ($cookie instanceof Cookie) { - setcookie($cookie->name, '', 0, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly); + $cookie->expire = 1; + $cookie->value = ''; + } else { + $cookie = new Cookie(array( + 'name' => $cookie, + 'expire' => 1, + )); + } + if ($removeFromBrowser) { + $this->_cookies[$cookie->name] = $cookie; + } else { unset($this->_cookies[$cookie->name]); } } /** * Removes all cookies. + * @throws InvalidCallException if the cookie collection is read only */ public function removeAll() { - foreach ($this->_cookies as $cookie) { - setcookie($cookie->name, '', 0, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly); + if ($this->readOnly) { + throw new InvalidCallException('The cookie collection is read only.'); } $this->_cookies = array(); } @@ -172,7 +185,7 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ */ public function offsetExists($name) { - return isset($this->_cookies[$name]); + return $this->has($name); } /** @@ -212,36 +225,4 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ { $this->remove($name); } - - /** - * Returns the current cookies in terms of [[Cookie]] objects. - * @return Cookie[] list of current cookies - */ - protected function loadCookies() - { - $cookies = array(); - if ($this->enableValidation) { - if ($this->validationKey === null) { - $key = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id); - } else { - $key = $this->validationKey; - } - foreach ($_COOKIE as $name => $value) { - if (is_string($value) && ($value = SecurityHelper::validateData($value, $key)) !== false) { - $cookies[$name] = new Cookie(array( - 'name' => $name, - 'value' => @unserialize($value), - )); - } - } - } else { - foreach ($_COOKIE as $name => $value) { - $cookies[$name] = new Cookie(array( - 'name' => $name, - 'value' => $value, - )); - } - } - return $cookies; - } } diff --git a/yii/web/DbSession.php b/framework/yii/web/DbSession.php similarity index 91% rename from yii/web/DbSession.php rename to framework/yii/web/DbSession.php index e81aa7f..c508bb8 100644 --- a/yii/web/DbSession.php +++ b/framework/yii/web/DbSession.php @@ -17,9 +17,9 @@ use yii\base\InvalidConfigException; * * By default, DbSession stores session data in a DB table named 'tbl_session'. This table * must be pre-created. The table name can be changed by setting [[sessionTable]]. - * + * * The following example shows how you can configure the application to use DbSession: - * + * * ~~~ * 'session' => array( * 'class' => 'yii\web\DbSession', @@ -28,6 +28,8 @@ use yii\base\InvalidConfigException; * ) * ~~~ * + * @property boolean $useCustomStorage Whether to use custom storage. This property is read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -111,7 +113,7 @@ class DbSession extends Session $row = $query->from($this->sessionTable) ->where(array('id' => $oldID)) ->createCommand($this->db) - ->queryRow(); + ->queryOne(); if ($row !== false) { if ($deleteOldSession) { $this->db->createCommand() diff --git a/framework/yii/web/ErrorAction.php b/framework/yii/web/ErrorAction.php new file mode 100644 index 0000000..3dd2823 --- /dev/null +++ b/framework/yii/web/ErrorAction.php @@ -0,0 +1,108 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\Action; +use yii\base\Exception; +use yii\base\UserException; + +/** + * ErrorAction displays application errors using a specified view. + * + * To use ErrorAction, you need to do the following steps: + * + * First, declare an action of ErrorAction type in the `actions()` method of your `SiteController` + * class (or whatever controller you prefer), like the following: + * + * ```php + * public function actions() + * { + * return array( + * 'error' => array( + * 'class' => 'yii\web\ErrorAction', + * ), + * ); + * } + * ``` + * + * Then, create a view file for this action. If the route of your error action is `site/error`, then + * the view file should be `views/site/error.php`. In this view file, the following variables are available: + * + * - `$name`: the error name + * - `$message`: the error message + * - `$exception`: the exception being handled + * + * Finally, configure the "errorHandler" application component as follows, + * + * ```php + * 'errorHandler' => array( + * 'errorAction' => 'site/error', + * ) + * ``` + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ErrorAction extends Action +{ + /** + * @var string the view file to be rendered. If not set, it will take the value of [[id]]. + * That means, if you name the action as "error" in "SiteController", then the view name + * would be "error", and the corresponding view file would be "views/site/error.php". + */ + public $view; + /** + * @var string the name of the error when the exception name cannot be determined. + * Defaults to "Error". + */ + public $defaultName; + /** + * @var string the message to be displayed when the exception message contains sensitive information. + * Defaults to "An internal server error occurred.". + */ + public $defaultMessage; + + + public function run() + { + if (!($exception = Yii::$app->getErrorHandler()->exception)) { + return ''; + } + + if ($exception instanceof HttpException) { + $code = $exception->statusCode; + } else { + $code = $exception->getCode(); + } + if ($exception instanceof Exception) { + $name = $exception->getName(); + } else { + $name = $this->defaultName ?: Yii::t('yii', 'Error'); + } + if ($code) { + $name .= " (#$code)"; + } + + if ($exception instanceof UserException) { + $message = $exception->getMessage(); + } else { + $message = $this->defaultMessage ?: Yii::t('yii', 'An internal server error occurred.'); + } + + if (Yii::$app->getRequest()->getIsAjax()) { + return "$name: $message"; + } else { + return $this->controller->render($this->view ?: $this->id, array( + 'name' => $name, + 'message' => $message, + 'exception' => $exception, + )); + } + } +} diff --git a/framework/yii/web/HeaderCollection.php b/framework/yii/web/HeaderCollection.php new file mode 100644 index 0000000..a729f6b --- /dev/null +++ b/framework/yii/web/HeaderCollection.php @@ -0,0 +1,221 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\Object; +use ArrayIterator; + +/** + * HeaderCollection is used by [[Response]] to maintain the currently registered HTTP headers. + * + * @property integer $count The number of headers in the collection. This property is read-only. + * @property ArrayIterator $iterator An iterator for traversing the headers in the collection. This property + * is read-only. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class HeaderCollection extends Object implements \IteratorAggregate, \ArrayAccess, \Countable +{ + /** + * @var array the headers in this collection (indexed by the header names) + */ + private $_headers = array(); + + /** + * Returns an iterator for traversing the headers in the collection. + * This method is required by the SPL interface `IteratorAggregate`. + * It will be implicitly called when you use `foreach` to traverse the collection. + * @return ArrayIterator an iterator for traversing the headers in the collection. + */ + public function getIterator() + { + return new ArrayIterator($this->_headers); + } + + /** + * Returns the number of headers in the collection. + * This method is required by the SPL `Countable` interface. + * It will be implicitly called when you use `count($collection)`. + * @return integer the number of headers in the collection. + */ + public function count() + { + return $this->getCount(); + } + + /** + * Returns the number of headers in the collection. + * @return integer the number of headers in the collection. + */ + public function getCount() + { + return count($this->_headers); + } + + /** + * Returns the named header(s). + * @param string $name the name of the header to return + * @param mixed $default the value to return in case the named header does not exist + * @param boolean $first whether to only return the first header of the specified name. + * If false, all headers of the specified name will be returned. + * @return string|array the named header(s). If `$first` is true, a string will be returned; + * If `$first` is false, an array will be returned. + */ + public function get($name, $default = null, $first = true) + { + $name = strtolower($name); + if (isset($this->_headers[$name])) { + return $first ? reset($this->_headers[$name]) : $this->_headers[$name]; + } else { + return $default; + } + } + + /** + * Adds a new header. + * If there is already a header with the same name, it will be replaced. + * @param string $name the name of the header + * @param string $value the value of the header + * @return static the collection object itself + */ + public function set($name, $value = '') + { + $name = strtolower($name); + $this->_headers[$name] = (array)$value; + return $this; + } + + /** + * Adds a new header. + * If there is already a header with the same name, the new one will + * be appended to it instead of replacing it. + * @param string $name the name of the header + * @param string $value the value of the header + * @return static the collection object itself + */ + public function add($name, $value) + { + $name = strtolower($name); + $this->_headers[$name][] = $value; + return $this; + } + + /** + * Sets a new header only if it does not exist yet. + * If there is already a header with the same name, the new one will be ignored. + * @param string $name the name of the header + * @param string $value the value of the header + * @return static the collection object itself + */ + public function setDefault($name, $value) + { + $name = strtolower($name); + if (empty($this->_headers[$name])) { + $this->_headers[$name][] = $value; + } + return $this; + } + + /** + * Returns a value indicating whether the named header exists. + * @param string $name the name of the header + * @return boolean whether the named header exists + */ + public function has($name) + { + $name = strtolower($name); + return isset($this->_headers[$name]); + } + + /** + * Removes a header. + * @param string $name the name of the header to be removed. + * @return string the value of the removed header. Null is returned if the header does not exist. + */ + public function remove($name) + { + $name = strtolower($name); + if (isset($this->_headers[$name])) { + $value = $this->_headers[$name]; + unset($this->_headers[$name]); + return $value; + } else { + return null; + } + } + + /** + * Removes all headers. + */ + public function removeAll() + { + $this->_headers = array(); + } + + /** + * Returns the collection as a PHP array. + * @return array the array representation of the collection. + * The array keys are header names, and the array values are the corresponding header values. + */ + public function toArray() + { + return $this->_headers; + } + + /** + * Returns whether there is a header with the specified name. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `isset($collection[$name])`. + * @param string $name the header name + * @return boolean whether the named header exists + */ + public function offsetExists($name) + { + return $this->has($name); + } + + /** + * Returns the header with the specified name. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `$header = $collection[$name];`. + * This is equivalent to [[get()]]. + * @param string $name the header name + * @return string the header value with the specified name, null if the named header does not exist. + */ + public function offsetGet($name) + { + return $this->get($name); + } + + /** + * Adds the header to the collection. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `$collection[$name] = $header;`. + * This is equivalent to [[add()]]. + * @param string $name the header name + * @param string $value the header value to be added + */ + public function offsetSet($name, $value) + { + $this->set($name, $value); + } + + /** + * Removes the named header. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `unset($collection[$name])`. + * This is equivalent to [[remove()]]. + * @param string $name the header name + */ + public function offsetUnset($name) + { + $this->remove($name); + } +} diff --git a/framework/yii/web/HttpCache.php b/framework/yii/web/HttpCache.php new file mode 100644 index 0000000..d2f3923 --- /dev/null +++ b/framework/yii/web/HttpCache.php @@ -0,0 +1,135 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\ActionFilter; +use yii\base\Action; + +/** + * The HttpCache provides functionality for caching via HTTP Last-Modified and Etag headers + * + * @author Da:Sourcerer <webmaster@dasourcerer.net> + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class HttpCache extends ActionFilter +{ + /** + * @var callback a PHP callback that returns the UNIX timestamp of the last modification time. + * The callback's signature should be: + * + * ~~~ + * function ($action, $params) + * ~~~ + * + * where `$action` is the [[Action]] object that this filter is currently handling; + * `$params` takes the value of [[params]]. The callback should return a UNIX timestamp. + */ + public $lastModified; + /** + * @var callback a PHP callback that generates the Etag seed string. + * The callback's signature should be: + * + * ~~~ + * function ($action, $params) + * ~~~ + * + * where `$action` is the [[Action]] object that this filter is currently handling; + * `$params` takes the value of [[params]]. The callback should return a string serving + * as the seed for generating an Etag. + */ + public $etagSeed; + /** + * @var mixed additional parameters that should be passed to the [[lastModified]] and [[etagSeed]] callbacks. + */ + public $params; + /** + * @var string HTTP cache control header. If null, the header will not be sent. + */ + public $cacheControlHeader = 'max-age=3600, public'; + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $verb = Yii::$app->getRequest()->getMethod(); + if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { + return true; + } + + $lastModified = $etag = null; + if ($this->lastModified !== null) { + $lastModified = call_user_func($this->lastModified, $action, $this->params); + } + if ($this->etagSeed !== null) { + $seed = call_user_func($this->etagSeed, $action, $this->params); + $etag = $this->generateEtag($seed); + } + + $this->sendCacheControlHeader(); + $response = Yii::$app->getResponse(); + if ($etag !== null) { + $response->getHeaders()->set('Etag', $etag); + } + + if ($this->validateCache($lastModified, $etag)) { + $response->setStatusCode(304); + return false; + } + + if ($lastModified !== null) { + $response->getHeaders()->set('Last-Modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); + } + return true; + } + + /** + * Validates if the HTTP cache contains valid content. + * @param integer $lastModified the calculated Last-Modified value in terms of a UNIX timestamp. + * If null, the Last-Modified header will not be validated. + * @param string $etag the calculated ETag value. If null, the ETag header will not be validated. + * @return boolean whether the HTTP cache is still valid. + */ + protected function validateCache($lastModified, $etag) + { + if ($lastModified !== null && (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) < $lastModified)) { + return false; + } else { + return $etag === null || isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag; + } + } + + /** + * Sends the cache control header to the client + * @see cacheControl + */ + protected function sendCacheControlHeader() + { + session_cache_limiter('public'); + $headers = Yii::$app->getResponse()->getHeaders(); + $headers->set('Pragma'); + if ($this->cacheControlHeader !== null) { + $headers->set('Cache-Control', $this->cacheControlHeader); + } + } + + /** + * Generates an Etag from the given seed string. + * @param string $seed Seed for the ETag + * @return string the generated Etag + */ + protected function generateEtag($seed) + { + return '"' . base64_encode(sha1($seed, true)) . '"'; + } +} diff --git a/yii/base/HttpException.php b/framework/yii/web/HttpException.php similarity index 53% rename from yii/base/HttpException.php rename to framework/yii/web/HttpException.php index 948d96b..2e677d5 100644 --- a/yii/base/HttpException.php +++ b/framework/yii/web/HttpException.php @@ -5,7 +5,9 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\base; +namespace yii\web; + +use yii\base\UserException; /** * HttpException represents an exception caused by an improper request of the end-user. @@ -42,67 +44,21 @@ class HttpException extends UserException */ public function getName() { - static $httpCodes = array( - 100 => 'Continue', - 101 => 'Switching Protocols', - 102 => 'Processing', - 118 => 'Connection timed out', - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 207 => 'Multi-Status', - 210 => 'Content Different', - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Found', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 307 => 'Temporary Redirect', - 310 => 'Too many Redirect', - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Time-out', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Long', - 415 => 'Unsupported Media Type', - 416 => 'Requested range unsatisfiable', - 417 => 'Expectation failed', - 418 => 'I’m a teapot', - 422 => 'Unprocessable entity', - 423 => 'Locked', - 424 => 'Method failure', - 425 => 'Unordered Collection', - 426 => 'Upgrade Required', - 449 => 'Retry With', - 450 => 'Blocked by Windows Parental Controls', - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway ou Proxy Error', - 503 => 'Service Unavailable', - 504 => 'Gateway Time-out', - 505 => 'HTTP Version not supported', - 507 => 'Insufficient storage', - 509 => 'Bandwidth Limit Exceeded', - ); + if (isset(Response::$httpStatuses[$this->statusCode])) { + return Response::$httpStatuses[$this->statusCode]; + } else { + return 'Error'; + } + } - if(isset($httpCodes[$this->statusCode])) - return $httpCodes[$this->statusCode]; - else - return \Yii::t('yii|Error'); + /** + * Returns the array representation of this object. + * @return array the array representation of this object. + */ + public function toArray() + { + $array = parent::toArray(); + $array['status'] = $this->statusCode; + return $array; } } diff --git a/framework/yii/web/IdentityInterface.php b/framework/yii/web/IdentityInterface.php new file mode 100644 index 0000000..c796b50 --- /dev/null +++ b/framework/yii/web/IdentityInterface.php @@ -0,0 +1,81 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +/** + * IdentityInterface 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 IdentityInterface + * { + * 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 <qiang.xue@gmail.com> + * @since 2.0 + */ +interface IdentityInterface +{ + /** + * Finds an identity by the given ID. + * @param string|integer $id the ID to be looked for + * @return IdentityInterface 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. + */ + public function getId(); + /** + * Returns a key that can be used to check the validity of a given identity ID. + * + * 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. + * @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); +} diff --git a/framework/yii/web/JqueryAsset.php b/framework/yii/web/JqueryAsset.php new file mode 100644 index 0000000..9991eb4 --- /dev/null +++ b/framework/yii/web/JqueryAsset.php @@ -0,0 +1,22 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +/** + * This asset bundle provides the [jquery javascript library](http://jquery.com/) + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class JqueryAsset extends AssetBundle +{ + public $sourcePath = '@yii/assets'; + public $js = array( + 'jquery.js', + ); +} diff --git a/yii/web/JsExpression.php b/framework/yii/web/JsExpression.php similarity index 94% rename from yii/web/JsExpression.php rename to framework/yii/web/JsExpression.php index 027c065..7daac08 100644 --- a/yii/web/JsExpression.php +++ b/framework/yii/web/JsExpression.php @@ -11,8 +11,10 @@ use yii\base\Object; /** * JsExpression marks a string as a JavaScript expression. - * When using [[Json::encode()]] to encode a value, JsonExpression objects + * + * When using [[yii\helpers\Json::encode()]] to encode a value, JsonExpression objects * will be specially handled and encoded as a JavaScript expression instead of a string. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ diff --git a/framework/yii/web/PageCache.php b/framework/yii/web/PageCache.php new file mode 100644 index 0000000..9bc8981 --- /dev/null +++ b/framework/yii/web/PageCache.php @@ -0,0 +1,121 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\ActionFilter; +use yii\base\Action; +use yii\base\View; +use yii\caching\Dependency; + +/** + * The PageCache provides functionality for whole page caching + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class PageCache extends ActionFilter +{ + /** + * @var boolean whether the content being cached should be differentiated according to the route. + * A route consists of the requested controller ID and action ID. Defaults to true. + */ + public $varyByRoute = true; + /** + * @var string the application component ID of the [[\yii\caching\Cache|cache]] object. + */ + public $cache = 'cache'; + /** + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + */ + public $duration = 60; + /** + * @var array|Dependency the dependency that the cached content depends on. + * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. + * For example, + * + * ~~~ + * array( + * 'class' => 'yii\caching\DbDependency', + * 'sql' => 'SELECT MAX(lastModified) FROM Post', + * ) + * ~~~ + * + * would make the output cache depends on the last modified time of all posts. + * If any post has its modification time changed, the cached content would be invalidated. + */ + public $dependency; + /** + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * array( + * Yii::$app->language, + * ) + */ + public $variations; + /** + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). + */ + public $enabled = true; + /** + * @var View the view component to use for caching. If not set, the default application view component + * [[Application::view]] will be used. + */ + public $view; + + + public function init() + { + parent::init(); + if ($this->view === null) { + $this->view = Yii::$app->getView(); + } + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $properties = array(); + foreach (array('cache', 'duration', 'dependency', 'variations', 'enabled') as $name) { + $properties[$name] = $this->$name; + } + $id = $this->varyByRoute ? $action->getUniqueId() : __CLASS__; + ob_start(); + ob_implicit_flush(false); + if ($this->view->beginCache($id, $properties)) { + return true; + } else { + Yii::$app->getResponse()->content = ob_get_clean(); + return false; + } + } + + /** + * This method is invoked right after an action is executed. + * You may override this method to do some postprocessing for the action. + * @param Action $action the action just executed. + * @param mixed $result the action execution result + */ + public function afterAction($action, &$result) + { + echo $result; + $this->view->endCache(); + $result = ob_get_clean(); + } +} diff --git a/yii/web/Request.php b/framework/yii/web/Request.php similarity index 57% rename from yii/web/Request.php rename to framework/yii/web/Request.php index d3f419b..312adc1 100644 --- a/yii/web/Request.php +++ b/framework/yii/web/Request.php @@ -8,48 +8,110 @@ namespace yii\web; use Yii; -use yii\base\HttpException; use yii\base\InvalidConfigException; +use yii\helpers\Security; /** + * The web Request class represents an HTTP request + * + * It encapsulates the $_SERVER variable and resolves its inconsistency among different Web servers. + * Also it provides an interface to retrieve request parameters from $_POST, $_GET, $_COOKIES and REST + * parameters sent via other HTTP methods like PUT or DELETE. + * + * @property string $absoluteUrl The currently requested absolute URL. This property is read-only. + * @property string $acceptTypes User browser accept types, null if not present. This property is read-only. + * @property array $acceptedContentTypes The content types ordered by the preference level. The first element + * represents the most preferred content type. + * @property array $acceptedLanguages The languages ordered by the preference level. The first element + * represents the most preferred language. + * @property string $baseUrl The relative URL for the application. + * @property string $cookieValidationKey The secret key used for cookie validation. If it was not set + * previously, a random key will be generated and used. + * @property CookieCollection $cookies The cookie collection. This property is read-only. + * @property string $csrfToken The random token for CSRF validation. This property is read-only. + * @property string $csrfTokenFromHeader The CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned + * if no such header is sent. This property is read-only. + * @property string $hostInfo Schema and hostname part (with port number if needed) of the request URL (e.g. + * `http://www.yiiframework.com`). + * @property boolean $isAjax Whether this is an AJAX (XMLHttpRequest) request. This property is read-only. + * @property boolean $isDelete Whether this is a DELETE request. This property is read-only. + * @property boolean $isFlash Whether this is an Adobe Flash or Adobe Flex request. This property is + * read-only. + * @property boolean $isGet Whether this is a GET request. This property is read-only. + * @property boolean $isHead Whether this is a HEAD request. This property is read-only. + * @property boolean $isOptions Whether this is a OPTIONS request. This property is read-only. + * @property boolean $isPatch Whether this is a PATCH request. This property is read-only. + * @property boolean $isPost Whether this is a POST request. This property is read-only. + * @property boolean $isPut Whether this is a PUT request. This property is read-only. + * @property boolean $isSecureConnection If the request is sent via secure channel (https). This property is + * read-only. + * @property string $method Request method, such as GET, POST, HEAD, PUT, PATCH, DELETE. The value returned is + * turned into upper case. This property is read-only. + * @property string $pathInfo Part of the request URL that is after the entry script and before the question + * mark. Note, the returned path info is already URL-decoded. + * @property integer $port Port number for insecure requests. + * @property string $preferredLanguage The language that the application should use. Null is returned if both + * [[getAcceptedLanguages()]] and `$languages` are empty. This property is read-only. + * @property string $queryString Part of the request URL that is after the question mark. This property is + * read-only. + * @property string $rawBody The request body. This property is read-only. + * @property string $referrer URL referrer, null if not present. This property is read-only. + * @property array $restParams The RESTful request parameters. + * @property string $scriptFile The entry script file path. + * @property string $scriptUrl The relative URL of the entry script. + * @property integer $securePort Port number for secure requests. + * @property string $serverName Server name. This property is read-only. + * @property integer $serverPort Server port number. This property is read-only. + * @property string $url The currently requested relative URL. Note that the URI returned is URL-encoded. + * @property string $userAgent User agent, null if not present. This property is read-only. + * @property string $userHost User host name, null if cannot be determined. This property is read-only. + * @property string $userIP User IP address. This property is read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ class Request extends \yii\base\Request { /** - * @var boolean whether to enable CSRF (Cross-Site Request Forgery) validation. Defaults to false. - * By setting this property to true, forms submitted to an Yii Web application must be originated + * The name of the HTTP header for sending CSRF token. + */ + const CSRF_HEADER = 'X-CSRF-Token'; + + + /** + * @var boolean whether to enable CSRF (Cross-Site Request Forgery) validation. Defaults to true. + * When CSRF validation is enabled, forms submitted to an Yii Web application must be originated * from the same application. If not, a 400 HTTP exception will be raised. * * Note, this feature requires that the user client accepts cookie. Also, to use this feature, - * forms submitted via POST method must contain a hidden input whose name is specified by [[csrfTokenName]]. + * forms submitted via POST method must contain a hidden input whose name is specified by [[csrfVar]]. * You may use [[\yii\web\Html::beginForm()]] to generate his hidden input. + * + * In JavaScript, you may get the values of [[csrfVar]] and [[csrfToken]] via `yii.getCsrfVar()` and + * `yii.getCsrfToken()`, respectively. The [[\yii\web\YiiAsset]] asset must be registered. + * + * @see Controller::enableCsrfValidation * @see http://en.wikipedia.org/wiki/Cross-site_request_forgery */ - public $enableCsrfValidation = false; + public $enableCsrfValidation = true; /** - * @var string the name of the token used to prevent CSRF. Defaults to 'YII_CSRF_TOKEN'. - * This property is effectively only when {@link enableCsrfValidation} is true. + * @var string the name of the token used to prevent CSRF. Defaults to '_csrf'. + * This property is used only when [[enableCsrfValidation]] is true. */ - public $csrfTokenName = '_csrf'; + public $csrfVar = '_csrf'; /** * @var array the configuration of the CSRF cookie. This property is used only when [[enableCsrfValidation]] is true. * @see Cookie */ - public $csrfCookie = array('httponly' => true); + public $csrfCookie = array('httpOnly' => true); /** * @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to true. */ public $enableCookieValidation = true; /** - * @var string the secret key used for cookie validation. If not set, a random key will be generated and used. - */ - public $cookieValidationKey; - /** - * @var string|boolean the name of the POST parameter that is used to indicate if a request is a PUT or DELETE + * @var string|boolean the name of the POST parameter that is used to indicate if a request is a PUT, PATCH or DELETE * request tunneled through POST. Default to '_method'. - * @see getRequestMethod + * @see getMethod * @see getRestParams */ public $restVar = '_method'; @@ -64,24 +126,22 @@ class Request extends \yii\base\Request */ public function resolve() { - $this->validateCsrfToken(); - $result = Yii::$app->getUrlManager()->parseRequest($this); if ($result !== false) { list ($route, $params) = $result; $_GET = array_merge($_GET, $params); return array($route, $_GET); } else { - throw new HttpException(404, Yii::t('yii|Page not found.')); + throw new HttpException(404, Yii::t('yii', 'Page not found.')); } } /** - * Returns the method of the current request (e.g. GET, POST, HEAD, PUT, DELETE). - * @return string request method, such as GET, POST, HEAD, PUT, DELETE. + * Returns the method of the current request (e.g. GET, POST, HEAD, PUT, PATCH, DELETE). + * @return string request method, such as GET, POST, HEAD, PUT, PATCH, DELETE. * The value returned is turned into upper case. */ - public function getRequestMethod() + public function getMethod() { if (isset($_POST[$this->restVar])) { return strtoupper($_POST[$this->restVar]); @@ -91,37 +151,73 @@ class Request extends \yii\base\Request } /** + * Returns whether this is a GET request. + * @return boolean whether this is a GET request. + */ + public function getIsGet() + { + return $this->getMethod() === 'GET'; + } + + /** + * Returns whether this is an OPTIONS request. + * @return boolean whether this is a OPTIONS request. + */ + public function getIsOptions() + { + return $this->getMethod() === 'OPTIONS'; + } + + /** + * Returns whether this is a HEAD request. + * @return boolean whether this is a HEAD request. + */ + public function getIsHead() + { + return $this->getMethod() === 'HEAD'; + } + + /** * Returns whether this is a POST request. * @return boolean whether this is a POST request. */ - public function getIsPostRequest() + public function getIsPost() { - return $this->getRequestMethod() === 'POST'; + return $this->getMethod() === 'POST'; } /** * Returns whether this is a DELETE request. * @return boolean whether this is a DELETE request. */ - public function getIsDeleteRequest() + public function getIsDelete() { - return $this->getRequestMethod() === 'DELETE'; + return $this->getMethod() === 'DELETE'; } /** * Returns whether this is a PUT request. * @return boolean whether this is a PUT request. */ - public function getIsPutRequest() + public function getIsPut() { - return $this->getRequestMethod() === 'PUT'; + return $this->getMethod() === 'PUT'; + } + + /** + * Returns whether this is a PATCH request. + * @return boolean whether this is a PATCH request. + */ + public function getIsPatch() + { + return $this->getMethod() === 'PATCH'; } /** * Returns whether this is an AJAX (XMLHttpRequest) request. * @return boolean whether this is an AJAX (XMLHttpRequest) request. */ - public function getIsAjaxRequest() + public function getIsAjax() { return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest'; } @@ -130,7 +226,7 @@ class Request extends \yii\base\Request * Returns whether this is an Adobe Flash or Flex request. * @return boolean whether this is an Adobe Flash or Adobe Flex request. */ - public function getIsFlashRequest() + public function getIsFlash() { return isset($_SERVER['HTTP_USER_AGENT']) && (stripos($_SERVER['HTTP_USER_AGENT'], 'Shockwave') !== false || stripos($_SERVER['HTTP_USER_AGENT'], 'Flash') !== false); @@ -141,7 +237,7 @@ class Request extends \yii\base\Request /** * Returns the request parameters for the RESTful request. * @return array the RESTful request parameters - * @see getRequestMethod + * @see getMethod */ public function getRestParams() { @@ -203,7 +299,7 @@ class Request extends \yii\base\Request * @return mixed the GET parameter value * @see getPost */ - public function getParam($name, $defaultValue = null) + public function get($name, $defaultValue = null) { return isset($_GET[$name]) ? $_GET[$name] : $defaultValue; } @@ -229,7 +325,7 @@ class Request extends \yii\base\Request */ public function getDelete($name, $defaultValue = null) { - return $this->getIsDeleteRequest() ? $this->getRestParam($name, $defaultValue) : null; + return $this->getIsDelete() ? $this->getRestParam($name, $defaultValue) : null; } /** @@ -240,7 +336,18 @@ class Request extends \yii\base\Request */ public function getPut($name, $defaultValue = null) { - return $this->getIsPutRequest() ? $this->getRestParam($name, $defaultValue) : null; + return $this->getIsPut() ? $this->getRestParam($name, $defaultValue) : null; + } + + /** + * Returns the named PATCH parameter value. + * @param string $name the PATCH parameter name + * @param mixed $defaultValue the default parameter value if the PATCH parameter does not exist. + * @return mixed the PATCH parameter value + */ + public function getPatch($name, $defaultValue = null) + { + return $this->getIsPatch() ? $this->getRestParam($name, $defaultValue) : null; } private $_hostInfo; @@ -401,13 +508,13 @@ class Request extends \yii\base\Request */ public function setPathInfo($value) { - $this->_pathInfo = trim($value, '/'); + $this->_pathInfo = ltrim($value, '/'); } /** * Resolves the path info part of the currently requested URL. * A path info refers to the part that is after the entry script and before the question mark (query string). - * The starting and ending slashes are both removed. + * The starting slashes are both removed (ending slashes will be kept). * @return string part of the request URL that is after the entry script and before the question mark. * Note, the returned path info is decoded. * @throws InvalidConfigException if the path info cannot be determined due to unexpected server configuration @@ -449,7 +556,7 @@ class Request extends \yii\base\Request throw new InvalidConfigException('Unable to determine the path info of the current request.'); } - return trim($pathInfo, '/'); + return ltrim($pathInfo, '/'); } /** @@ -533,7 +640,8 @@ class Request extends \yii\base\Request */ public function getIsSecureConnection() { - return !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off'); + return isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1) + || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'; } /** @@ -661,40 +769,133 @@ class Request extends \yii\base\Request } } - private $_preferredLanguages; + private $_contentTypes; /** - * Returns the user preferred languages. - * The languages returned are ordered by user's preference, starting with the language that the user - * prefers the most. - * @return string the user preferred languages. An empty array may be returned if the user has no preference. + * Returns the content types accepted by the end user. + * This is determined by the `Accept` HTTP header. + * @return array the content types ordered by the preference level. The first element + * represents the most preferred content type. */ - public function getPreferredLanguages() + public function getAcceptedContentTypes() { - if ($this->_preferredLanguages === null) { - if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) && ($n = preg_match_all('/([\w\-_]+)\s*(;\s*q\s*=\s*(\d*\.\d*))?/', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches)) > 0) { - $languages = array(); - for ($i = 0; $i < $n; ++$i) { - $languages[$matches[1][$i]] = empty($matches[3][$i]) ? 1.0 : floatval($matches[3][$i]); - } - arsort($languages); - $this->_preferredLanguages = array_keys($languages); + if ($this->_contentTypes === null) { + if (isset($_SERVER['HTTP_ACCEPT'])) { + $this->_contentTypes = $this->parseAcceptHeader($_SERVER['HTTP_ACCEPT']); + } else { + $this->_contentTypes = array(); + } + } + return $this->_contentTypes; + } + + /** + * @param array $value the content types that are accepted by the end user. They should + * be ordered by the preference level. + */ + public function setAcceptedContentTypes($value) + { + $this->_contentTypes = $value; + } + + private $_languages; + + /** + * Returns the languages accepted by the end user. + * This is determined by the `Accept-Language` HTTP header. + * @return array the languages ordered by the preference level. The first element + * represents the most preferred language. + */ + public function getAcceptedLanguages() + { + if ($this->_languages === null) { + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + $this->_languages = $this->parseAcceptHeader($_SERVER['HTTP_ACCEPT_LANGUAGE']); + } else { + $this->_languages = array(); + } + } + return $this->_languages; + } + + /** + * @param array $value the languages that are accepted by the end user. They should + * be ordered by the preference level. + */ + public function setAcceptedLanguages($value) + { + $this->_languages = $value; + } + + /** + * Parses the given `Accept` (or `Accept-Language`) header. + * This method will return the accepted values ordered by their preference level. + * @param string $header the header to be parsed + * @return array the accept values ordered by their preference level. + */ + protected function parseAcceptHeader($header) + { + $accepts = array(); + $n = preg_match_all('/\s*([\w\/\-\*]+)\s*(?:;\s*q\s*=\s*([\d\.]+))?[^,]*/', $header, $matches, PREG_SET_ORDER); + for ($i = 0; $i < $n; ++$i) { + if (!empty($matches[$i][1])) { + $accepts[] = array($matches[$i][1], isset($matches[$i][2]) ? (float)$matches[$i][2] : 1, $i); + } + } + usort($accepts, function ($a, $b) { + if ($a[1] > $b[1]) { + return -1; + } elseif ($a[1] < $b[1]) { + return 1; + } elseif ($a[0] === $b[0]) { + return $a[2] > $b[2] ? 1 : -1; + } elseif ($a[0] === '*/*') { + return 1; + } elseif ($b[0] === '*/*') { + return -1; } else { - $this->_preferredLanguages = array(); + $wa = $a[0][strlen($a[0]) - 1] === '*'; + $wb = $b[0][strlen($b[0]) - 1] === '*'; + if ($wa xor $wb) { + return $wa ? 1 : -1; + } else { + return $a[2] > $b[2] ? 1 : -1; + } } + }); + $result = array(); + foreach ($accepts as $accept) { + $result[] = $accept[0]; } - return $this->_preferredLanguages; + return array_unique($result); } /** - * Returns the language most preferred by the user. - * @return string|boolean the language most preferred by the user. If the user has no preference, false - * will be returned. + * Returns the user-preferred language that should be used by this application. + * The language resolution is based on the user preferred languages and the languages + * supported by the application. The method will try to find the best match. + * @param array $languages a list of the languages supported by the application. + * If empty, this method will return the first language returned by [[getAcceptedLanguages()]]. + * @return string the language that the application should use. Null is returned if both [[getAcceptedLanguages()]] + * and `$languages` are empty. */ - public function getPreferredLanguage() + public function getPreferredLanguage($languages = array()) { - $languages = $this->getPreferredLanguages(); - return isset($languages[0]) ? $languages[0] : false; + $acceptedLanguages = $this->getAcceptedLanguages(); + if (empty($languages)) { + return isset($acceptedLanguages[0]) ? $acceptedLanguages[0] : null; + } + foreach ($acceptedLanguages as $acceptedLanguage) { + $acceptedLanguage = str_replace('-', '_', strtolower($acceptedLanguage)); + foreach ($languages as $language) { + $language = str_replace('-', '_', strtolower($language)); + // en_us==en_us, en==en_us, en_us==en + if ($language === $acceptedLanguage || strpos($acceptedLanguage, $language . '_') === 0 || strpos($language, $acceptedLanguage . '_') === 0) { + return $language; + } + } + } + return reset($languages); } /** @@ -716,15 +917,68 @@ class Request extends \yii\base\Request public function getCookies() { if ($this->_cookies === null) { - $this->_cookies = new CookieCollection(array( - 'enableValidation' => $this->enableCookieValidation, - 'validationKey' => $this->cookieValidationKey, + $this->_cookies = new CookieCollection($this->loadCookies(), array( + 'readOnly' => true, )); } return $this->_cookies; } - private $_csrfToken; + /** + * Converts `$_COOKIE` into an array of [[Cookie]]. + * @return array the cookies obtained from request + */ + protected function loadCookies() + { + $cookies = array(); + if ($this->enableCookieValidation) { + $key = $this->getCookieValidationKey(); + foreach ($_COOKIE as $name => $value) { + if (is_string($value) && ($value = Security::validateData($value, $key)) !== false) { + $cookies[$name] = new Cookie(array( + 'name' => $name, + 'value' => @unserialize($value), + )); + } + } + } else { + foreach ($_COOKIE as $name => $value) { + $cookies[$name] = new Cookie(array( + 'name' => $name, + 'value' => $value, + )); + } + } + return $cookies; + } + + private $_cookieValidationKey; + + /** + * @return string the secret key used for cookie validation. If it was not set previously, + * a random key will be generated and used. + */ + public function getCookieValidationKey() + { + if ($this->_cookieValidationKey === null) { + $this->_cookieValidationKey = Security::getSecretKey(__CLASS__ . '/' . Yii::$app->id); + } + return $this->_cookieValidationKey; + } + + /** + * Sets the secret key used for cookie validation. + * @param string $value the secret key used for cookie validation. + */ + public function setCookieValidationKey($value) + { + $this->_cookieValidationKey = $value; + } + + /** + * @var Cookie + */ + private $_csrfCookie; /** * Returns the random token used to perform CSRF validation. @@ -734,16 +988,24 @@ class Request extends \yii\base\Request */ public function getCsrfToken() { - if ($this->_csrfToken === null) { - $cookies = $this->getCookies(); - if (($this->_csrfToken = $cookies->getValue($this->csrfTokenName)) === null) { - $cookie = $this->createCsrfCookie(); - $this->_csrfToken = $cookie->value; - $cookies->add($cookie); + if ($this->_csrfCookie === null) { + $this->_csrfCookie = $this->getCookies()->get($this->csrfVar); + if ($this->_csrfCookie === null) { + $this->_csrfCookie = $this->createCsrfCookie(); + Yii::$app->getResponse()->getCookies()->add($this->_csrfCookie); } } - return $this->_csrfToken; + return $this->_csrfCookie->value; + } + + /** + * @return string the CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned if no such header is sent. + */ + public function getCsrfTokenFromHeader() + { + $key = 'HTTP_' . str_replace('-', '_', strtoupper(self::CSRF_HEADER)); + return isset($_SERVER[$key]) ? $_SERVER[$key] : null; } /** @@ -755,7 +1017,7 @@ class Request extends \yii\base\Request protected function createCsrfCookie() { $options = $this->csrfCookie; - $options['name'] = $this->csrfTokenName; + $options['name'] = $this->csrfVar; $options['value'] = sha1(uniqid(mt_rand(), true)); return new Cookie($options); } @@ -764,31 +1026,30 @@ class Request extends \yii\base\Request * Performs the CSRF validation. * The method will compare the CSRF token obtained from a cookie and from a POST field. * If they are different, a CSRF attack is detected and a 400 HTTP exception will be raised. - * @throws HttpException if the validation fails + * This method is called in [[Controller::beforeAction()]]. + * @return boolean whether CSRF token is valid. If [[enableCsrfValidation]] is false, this method will return true. */ public function validateCsrfToken() { - if (!$this->enableCsrfValidation) { - return; + $method = $this->getMethod(); + if (!$this->enableCsrfValidation || !in_array($method, array('POST', 'PUT', 'PATCH', 'DELETE'), true)) { + return true; } - $method = $this->getRequestMethod(); - if ($method === 'POST' || $method === 'PUT' || $method === 'DELETE') { - $cookies = $this->getCookies(); - switch ($method) { - case 'POST': - $token = $this->getPost($this->csrfTokenName); - break; - case 'PUT': - $token = $this->getPut($this->csrfTokenName); - break; - case 'DELETE': - $token = $this->getDelete($this->csrfTokenName); - } - - if (empty($token) || $cookies->getValue($this->csrfTokenName) !== $token) { - throw new HttpException(400, Yii::t('yii|Unable to verify your data submission.')); - } + $trueToken = $this->getCookies()->getValue($this->csrfVar); + switch ($method) { + case 'PUT': + $token = $this->getPut($this->csrfVar); + break; + case 'PATCH': + $token = $this->getPatch($this->csrfVar); + break; + case 'DELETE': + $token = $this->getDelete($this->csrfVar); + break; + default: + $token = $this->getPost($this->csrfVar); + break; } + return $token === $trueToken || $this->getCsrfTokenFromHeader() === $trueToken; } } - diff --git a/framework/yii/web/Response.php b/framework/yii/web/Response.php new file mode 100644 index 0000000..2dc8721 --- /dev/null +++ b/framework/yii/web/Response.php @@ -0,0 +1,817 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; +use yii\helpers\FileHelper; +use yii\helpers\Html; +use yii\helpers\Json; +use yii\helpers\Security; +use yii\helpers\StringHelper; + +/** + * The web Response class represents an HTTP response + * + * It holds the [[headers]], [[cookies]] and [[content]] that is to be sent to the client. + * It also controls the HTTP [[statusCode|status code]]. + * + * @property CookieCollection $cookies The cookie collection. This property is read-only. + * @property HeaderCollection $headers The header collection. This property is read-only. + * @property boolean $isClientError Whether this response indicates a client error. This property is + * read-only. + * @property boolean $isEmpty Whether this response is empty. This property is read-only. + * @property boolean $isForbidden Whether this response indicates the current request is forbidden. This + * property is read-only. + * @property boolean $isInformational Whether this response is informational. This property is read-only. + * @property boolean $isInvalid Whether this response has a valid [[statusCode]]. This property is read-only. + * @property boolean $isNotFound Whether this response indicates the currently requested resource is not + * found. This property is read-only. + * @property boolean $isOk Whether this response is OK. This property is read-only. + * @property boolean $isRedirection Whether this response is a redirection. This property is read-only. + * @property boolean $isServerError Whether this response indicates a server error. This property is + * read-only. + * @property boolean $isSuccessful Whether this response is successful. This property is read-only. + * @property integer $statusCode The HTTP status code to send with the response. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Carsten Brandt <mail@cebe.cc> + * @since 2.0 + */ +class Response extends \yii\base\Response +{ + /** + * @event ResponseEvent an event that is triggered at the beginning of [[send()]]. + */ + const EVENT_BEFORE_SEND = 'beforeSend'; + /** + * @event ResponseEvent an event that is triggered at the end of [[send()]]. + */ + const EVENT_AFTER_SEND = 'afterSend'; + /** + * @event ResponseEvent an event that is triggered right after [[prepare()]] is called in [[send()]]. + * You may respond to this event to filter the response content before it is sent to the client. + */ + const EVENT_AFTER_PREPARE = 'afterPrepare'; + + const FORMAT_RAW = 'raw'; + const FORMAT_HTML = 'html'; + const FORMAT_JSON = 'json'; + const FORMAT_JSONP = 'jsonp'; + const FORMAT_XML = 'xml'; + + /** + * @var string the response format. This determines how to convert [[data]] into [[content]] + * when the latter is not set. By default, the following formats are supported: + * + * - [[FORMAT_RAW]]: the data will be treated as the response content without any conversion. + * No extra HTTP header will be added. + * - [[FORMAT_HTML]]: the data will be treated as the response content without any conversion. + * The "Content-Type" header will set as "text/html" if it is not set previously. + * - [[FORMAT_JSON]]: the data will be converted into JSON format, and the "Content-Type" + * header will be set as "application/json". + * - [[FORMAT_JSONP]]: the data will be converted into JSONP format, and the "Content-Type" + * header will be set as "text/javascript". Note that in this case `$data` must be an array + * with "data" and "callback" elements. The former refers to the actual data to be sent, + * while the latter refers to the name of the JavaScript callback. + * - [[FORMAT_XML]]: the data will be converted into XML format. Please refer to [[XmlResponseFormatter]] + * for more details. + * + * You may customize the formatting process or support additional formats by configuring [[formatters]]. + * @see formatters + */ + public $format = self::FORMAT_HTML; + /** + * @var array the formatters for converting data into the response content of the specified [[format]]. + * The array keys are the format names, and the array values are the corresponding configurations + * for creating the formatter objects. + * @see format + */ + public $formatters; + /** + * @var mixed the original response data. When this is not null, it will be converted into [[content]] + * according to [[format]] when the response is being sent out. + * @see content + */ + public $data; + /** + * @var string the response content. When [[data]] is not null, it will be converted into [[content]] + * according to [[format]] when the response is being sent out. + * @see data + */ + public $content; + /** + * @var string the charset of the text response. If not set, it will use + * the value of [[Application::charset]]. + */ + public $charset; + /** + * @var string + */ + public $statusText; + /** + * @var string the version of the HTTP protocol to use. If not set, it will be determined via `$_SERVER['SERVER_PROTOCOL']`, + * or '1.1' if that is not available. + */ + public $version; + /** + * @var array list of HTTP status codes and the corresponding texts + */ + public static $httpStatuses = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 118 => 'Connection timed out', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + 210 => 'Content Different', + 226 => 'IM Used', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Reserved', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + 310 => 'Too many Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested range unsatisfiable', + 417 => 'Expectation failed', + 418 => 'I\'m a teapot', + 422 => 'Unprocessable entity', + 423 => 'Locked', + 424 => 'Method failure', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 449 => 'Retry With', + 450 => 'Blocked by Windows Parental Controls', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway ou Proxy Error', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 507 => 'Insufficient storage', + 508 => 'Loop Detected', + 509 => 'Bandwidth Limit Exceeded', + 510 => 'Not Extended', + 511 => 'Network Authentication Required', + ); + + /** + * @var integer the HTTP status code to send with the response. + */ + private $_statusCode; + /** + * @var HeaderCollection + */ + private $_headers; + + /** + * Initializes this component. + */ + public function init() + { + if ($this->version === null) { + if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === '1.0') { + $this->version = '1.0'; + } else { + $this->version = '1.1'; + } + } + if ($this->charset === null) { + $this->charset = Yii::$app->charset; + } + } + + /** + * @return integer the HTTP status code to send with the response. + */ + public function getStatusCode() + { + return $this->_statusCode; + } + + /** + * Sets the response status code. + * This method will set the corresponding status text if `$text` is null. + * @param integer $value the status code + * @param string $text the status text. If not set, it will be set automatically based on the status code. + * @throws InvalidParamException if the status code is invalid. + */ + public function setStatusCode($value, $text = null) + { + if ($value === null) { + $this->_statusCode = null; + $this->statusText = null; + return; + } + $this->_statusCode = (int)$value; + if ($this->getIsInvalid()) { + throw new InvalidParamException("The HTTP status code is invalid: $value"); + } + if ($text === null) { + $this->statusText = isset(self::$httpStatuses[$this->_statusCode]) ? self::$httpStatuses[$this->_statusCode] : ''; + } else { + $this->statusText = $text; + } + } + + /** + * Returns the header collection. + * The header collection contains the currently registered HTTP headers. + * @return HeaderCollection the header collection + */ + public function getHeaders() + { + if ($this->_headers === null) { + $this->_headers = new HeaderCollection; + } + return $this->_headers; + } + + /** + * Sends the response to the client. + */ + public function send() + { + $this->trigger(self::EVENT_BEFORE_SEND, new ResponseEvent($this)); + $this->prepare(); + $this->trigger(self::EVENT_AFTER_PREPARE, new ResponseEvent($this)); + $this->sendHeaders(); + $this->sendContent(); + $this->trigger(self::EVENT_AFTER_SEND, new ResponseEvent($this)); + } + + /** + * Clears the headers, cookies, content, status code of the response. + */ + public function clear() + { + $this->_headers = null; + $this->_cookies = null; + $this->_statusCode = null; + $this->data = null; + $this->content = null; + $this->statusText = null; + } + + /** + * Sends the response headers to the client + */ + protected function sendHeaders() + { + if (headers_sent()) { + return; + } + $statusCode = $this->getStatusCode(); + if ($statusCode !== null) { + header("HTTP/{$this->version} $statusCode {$this->statusText}"); + } + if ($this->_headers) { + $headers = $this->getHeaders(); + foreach ($headers as $name => $values) { + $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name))); + foreach ($values as $value) { + header("$name: $value", false); + } + } + } + $this->sendCookies(); + } + + /** + * Sends the cookies to the client. + */ + protected function sendCookies() + { + if ($this->_cookies === null) { + return; + } + $request = Yii::$app->getRequest(); + if ($request->enableCookieValidation) { + $validationKey = $request->getCookieValidationKey(); + } + foreach ($this->getCookies() as $cookie) { + $value = $cookie->value; + if ($cookie->expire != 1 && isset($validationKey)) { + $value = Security::hashData(serialize($value), $validationKey); + } + setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); + } + $this->getCookies()->removeAll(); + } + + /** + * Sends the response content to the client + */ + protected function sendContent() + { + echo $this->content; + } + + /** + * Sends a file to the browser. + * @param string $filePath the path of the file to be sent. + * @param string $attachmentName the file name shown to the user. If null, it will be determined from `$filePath`. + * @param string $mimeType the MIME type of the content. If null, it will be guessed based on `$filePath` + */ + public function sendFile($filePath, $attachmentName = null, $mimeType = null) + { + if ($mimeType === null && ($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) { + $mimeType = 'application/octet-stream'; + } + if ($attachmentName === null) { + $attachmentName = basename($filePath); + } + $handle = fopen($filePath, 'rb'); + $this->sendStreamAsFile($handle, $attachmentName, $mimeType); + } + + /** + * Sends the specified content as a file to the browser. + * @param string $content the content to be sent. The existing [[content]] will be discarded. + * @param string $attachmentName the file name shown to the user. + * @param string $mimeType the MIME type of the content. + * @throws HttpException if the requested range is not satisfiable + */ + public function sendContentAsFile($content, $attachmentName, $mimeType = 'application/octet-stream') + { + $headers = $this->getHeaders(); + $contentLength = StringHelper::strlen($content); + $range = $this->getHttpRange($contentLength); + if ($range === false) { + $headers->set('Content-Range', "bytes */$contentLength"); + throw new HttpException(416, Yii::t('yii', 'Requested range not satisfiable')); + } + + $headers->setDefault('Pragma', 'public') + ->setDefault('Accept-Ranges', 'bytes') + ->setDefault('Expires', '0') + ->setDefault('Content-Type', $mimeType) + ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->setDefault('Content-Transfer-Encoding', 'binary') + ->setDefault('Content-Length', StringHelper::strlen($content)) + ->setDefault('Content-Disposition', "attachment; filename=\"$attachmentName\""); + + list($begin, $end) = $range; + if ($begin !=0 || $end != $contentLength - 1) { + $this->setStatusCode(206); + $headers->set('Content-Range', "bytes $begin-$end/$contentLength"); + $this->content = StringHelper::substr($content, $begin, $end - $begin + 1); + } else { + $this->setStatusCode(200); + $this->content = $content; + } + + $this->format = self::FORMAT_RAW; + $this->send(); + } + + /** + * Sends the specified stream as a file to the browser. + * @param resource $handle the handle of the stream to be sent. + * @param string $attachmentName the file name shown to the user. + * @param string $mimeType the MIME type of the stream content. + * @throws HttpException if the requested range cannot be satisfied. + */ + public function sendStreamAsFile($handle, $attachmentName, $mimeType = 'application/octet-stream') + { + $headers = $this->getHeaders(); + fseek($handle, 0, SEEK_END); + $fileSize = ftell($handle); + + $range = $this->getHttpRange($fileSize); + if ($range === false) { + $headers->set('Content-Range', "bytes */$fileSize"); + throw new HttpException(416, Yii::t('yii', 'Requested range not satisfiable')); + } + + list($begin, $end) = $range; + if ($begin !=0 || $end != $fileSize - 1) { + $this->setStatusCode(206); + $headers->set('Content-Range', "bytes $begin-$end/$fileSize"); + } else { + $this->setStatusCode(200); + } + + $length = $end - $begin + 1; + + $headers->setDefault('Pragma', 'public') + ->setDefault('Accept-Ranges', 'bytes') + ->setDefault('Expires', '0') + ->setDefault('Content-Type', $mimeType) + ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->setDefault('Content-Transfer-Encoding', 'binary') + ->setDefault('Content-Length', $length) + ->setDefault('Content-Disposition', "attachment; filename=\"$attachmentName\""); + $this->format = self::FORMAT_RAW; + $this->data = $this->content = null; + $this->send(); + + fseek($handle, $begin); + set_time_limit(0); // Reset time limit for big files + $chunkSize = 8 * 1024 * 1024; // 8MB per chunk + while (!feof($handle) && ($pos = ftell($handle)) <= $end) { + if ($pos + $chunkSize > $end) { + $chunkSize = $end - $pos + 1; + } + echo fread($handle, $chunkSize); + flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. + } + fclose($handle); + } + + /** + * Determines the HTTP range given in the request. + * @param integer $fileSize the size of the file that will be used to validate the requested HTTP range. + * @return array|boolean the range (begin, end), or false if the range request is invalid. + */ + protected function getHttpRange($fileSize) + { + if (!isset($_SERVER['HTTP_RANGE']) || $_SERVER['HTTP_RANGE'] === '-') { + return array(0, $fileSize - 1); + } + if (!preg_match('/^bytes=(\d*)-(\d*)$/', $_SERVER['HTTP_RANGE'], $matches)) { + return false; + } + if ($matches[1] === '') { + $start = $fileSize - $matches[2]; + $end = $fileSize - 1; + } elseif ($matches[2] !== '') { + $start = $matches[1]; + $end = $matches[2]; + if ($end >= $fileSize) { + $end = $fileSize - 1; + } + } else { + $start = $matches[1]; + $end = $fileSize - 1; + } + if ($start < 0 || $start > $end) { + return false; + } else { + return array($start, $end); + } + } + + /** + * Sends existing file to a browser as a download using x-sendfile. + * + * X-Sendfile is a feature allowing a web application to redirect the request for a file to the webserver + * that in turn processes the request, this way eliminating the need to perform tasks like reading the file + * and sending it to the user. When dealing with a lot of files (or very big files) this can lead to a great + * increase in performance as the web application is allowed to terminate earlier while the webserver is + * handling the request. + * + * The request is sent to the server through a special non-standard HTTP-header. + * When the web server encounters the presence of such header it will discard all output and send the file + * specified by that header using web server internals including all optimizations like caching-headers. + * + * As this header directive is non-standard different directives exists for different web servers applications: + * + * - Apache: [X-Sendfile](http://tn123.org/mod_xsendfile) + * - Lighttpd v1.4: [X-LIGHTTPD-send-file](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file) + * - Lighttpd v1.5: [X-Sendfile](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file) + * - Nginx: [X-Accel-Redirect](http://wiki.nginx.org/XSendfile) + * - Cherokee: [X-Sendfile and X-Accel-Redirect](http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile) + * + * So for this method to work the X-SENDFILE option/module should be enabled by the web server and + * a proper xHeader should be sent. + * + * **Note** + * + * This option allows to download files that are not under web folders, and even files that are otherwise protected + * (deny from all) like `.htaccess`. + * + * **Side effects** + * + * If this option is disabled by the web server, when this method is called a download configuration dialog + * will open but the downloaded file will have 0 bytes. + * + * **Known issues** + * + * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show + * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site + * is either unavailable or cannot be found.". You can work around this problem by removing the `Pragma`-header. + * + * **Example** + * + * ~~~ + * Yii::$app->request->xSendFile('/home/user/Pictures/picture1.jpg'); + * ~~~ + * + * @param string $filePath file name with full path + * @param string $mimeType the MIME type of the file. If null, it will be determined based on `$filePath`. + * @param string $attachmentName file name shown to the user. If null, it will be determined from `$filePath`. + * @param string $xHeader the name of the x-sendfile header. + */ + public function xSendFile($filePath, $attachmentName = null, $mimeType = null, $xHeader = 'X-Sendfile') + { + if ($mimeType === null && ($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) { + $mimeType = 'application/octet-stream'; + } + if ($attachmentName === null) { + $attachmentName = basename($filePath); + } + + $this->getHeaders() + ->setDefault($xHeader, $filePath) + ->setDefault('Content-Type', $mimeType) + ->setDefault('Content-Disposition', "attachment; filename=\"$attachmentName\""); + + $this->send(); + } + + /** + * Redirects the browser to the specified URL. + * + * This method adds a "Location" header to the current response. Note that it does not send out + * the header until [[send()]] is called. In a controller action you may use this method as follows: + * + * ~~~ + * return Yii::$app->getResponse()->redirect($url); + * ~~~ + * + * In other places, if you want to send out the "Location" header immediately, you should use + * the following code: + * + * ~~~ + * Yii::$app->getResponse()->redirect($url)->send(); + * return; + * ~~~ + * + * 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 send out a "X-Redirect" header instead of "Location". + * + * If you use the "yii" JavaScript module, it will handle the AJAX redirection as + * described above. Otherwise, you should write the following JavaScript code to + * handle the redirection: + * + * ~~~ + * $document.ajaxComplete(function (event, xhr, settings) { + * var url = xhr.getResponseHeader('X-Redirect'); + * if (url) { + * window.location = url; + * } + * }); + * ~~~ + * + * @param string|array $url the URL to be redirected to. This can be in one of the following formats: + * + * - a string representing a URL (e.g. "http://example.com") + * - a string representing a URL alias (e.g. "@example.com") + * - an array in the format of `array($route, ...name-value pairs...)` (e.g. `array('site/index', 'ref' => 1)`). + * Note that the route is with respect to the whole application, instead of relative to a controller or module. + * [[Html::url()]] will be used to convert the array into a URL. + * + * Any relative URL will be converted into an absolute one by prepending it with the host info + * of the current request. + * + * @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 + * @return static the response object itself + */ + public function redirect($url, $statusCode = 302) + { + if (is_array($url) && isset($url[0])) { + // ensure the route is absolute + $url[0] = '/' . ltrim($url[0], '/'); + } + $url = Html::url($url); + if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { + $url = Yii::$app->getRequest()->getHostInfo() . $url; + } + + if (Yii::$app->getRequest()->getIsAjax()) { + $this->getHeaders()->set('X-Redirect', $url); + } else { + $this->getHeaders()->set('Location', $url); + } + $this->setStatusCode($statusCode); + + return $this; + } + + /** + * Refreshes the current page. + * The effect of this method call is the same as the user pressing the refresh button of his browser + * (without re-posting data). + * + * In a controller action you may use this method like this: + * + * ~~~ + * return Yii::$app->getResponse()->refresh(); + * ~~~ + * + * @param string $anchor the anchor that should be appended to the redirection URL. + * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it. + * @return Response the response object itself + */ + public function refresh($anchor = '') + { + return $this->redirect(Yii::$app->getRequest()->getUrl() . $anchor); + } + + private $_cookies; + + /** + * Returns the cookie collection. + * Through the returned cookie collection, you add or remove cookies as follows, + * + * ~~~ + * // add a cookie + * $response->cookies->add(new Cookie(array( + * 'name' => $name, + * 'value' => $value, + * )); + * + * // remove a cookie + * $response->cookies->remove('name'); + * // alternatively + * unset($response->cookies['name']); + * ~~~ + * + * @return CookieCollection the cookie collection. + */ + public function getCookies() + { + if ($this->_cookies === null) { + $this->_cookies = new CookieCollection; + } + return $this->_cookies; + } + + /** + * @return boolean whether this response has a valid [[statusCode]]. + */ + public function getIsInvalid() + { + return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600; + } + + /** + * @return boolean whether this response is informational + */ + public function getIsInformational() + { + return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200; + } + + /** + * @return boolean whether this response is successful + */ + public function getIsSuccessful() + { + return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; + } + + /** + * @return boolean whether this response is a redirection + */ + public function getIsRedirection() + { + return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; + } + + /** + * @return boolean whether this response indicates a client error + */ + public function getIsClientError() + { + return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500; + } + + /** + * @return boolean whether this response indicates a server error + */ + public function getIsServerError() + { + return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; + } + + /** + * @return boolean whether this response is OK + */ + public function getIsOk() + { + return $this->getStatusCode() == 200; + } + + /** + * @return boolean whether this response indicates the current request is forbidden + */ + public function getIsForbidden() + { + return $this->getStatusCode() == 403; + } + + /** + * @return boolean whether this response indicates the currently requested resource is not found + */ + public function getIsNotFound() + { + return $this->getStatusCode() == 404; + } + + /** + * @return boolean whether this response is empty + */ + public function getIsEmpty() + { + return in_array($this->getStatusCode(), array(201, 204, 304)); + } + + /** + * Prepares for sending the response. + * The default implementation will convert [[data]] into [[content]] and set headers accordingly. + * @throws InvalidConfigException if the formatter for the specified format is invalid or [[format]] is not supported + */ + protected function prepare() + { + if ($this->data === null) { + return; + } + + if (isset($this->formatters[$this->format])) { + $formatter = $this->formatters[$this->format]; + if (!is_object($formatter)) { + $formatter = Yii::createObject($formatter); + } + if ($formatter instanceof ResponseFormatterInterface) { + $formatter->format($this); + } else { + throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface."); + } + } else { + switch ($this->format) { + case self::FORMAT_HTML: + $this->getHeaders()->setDefault('Content-Type', 'text/html; charset=' . $this->charset); + $this->content = $this->data; + break; + case self::FORMAT_RAW: + $this->content = $this->data; + break; + case self::FORMAT_JSON: + $this->getHeaders()->set('Content-Type', 'application/json'); + $this->content = Json::encode($this->data); + break; + case self::FORMAT_JSONP: + $this->getHeaders()->set('Content-Type', 'text/javascript; charset=' . $this->charset); + if (is_array($this->data) && isset($this->data['data'], $this->data['callback'])) { + $this->content = sprintf('%s(%s);', $this->data['callback'], Json::encode($this->data['data'])); + } else { + $this->content = ''; + Yii::warning("The 'jsonp' response requires that the data be an array consisting of both 'data' and 'callback' elements.", __METHOD__); + } + break; + case self::FORMAT_XML: + Yii::createObject(XmlResponseFormatter::className())->format($this); + break; + default: + throw new InvalidConfigException("Unsupported response format: {$this->format}"); + } + } + + if (is_array($this->content)) { + $this->content = 'array()'; + } elseif (is_object($this->content)) { + $this->content = method_exists($this->content, '__toString') ? $this->content->__toString() : get_class($this->content); + } + } +} diff --git a/framework/yii/web/ResponseEvent.php b/framework/yii/web/ResponseEvent.php new file mode 100644 index 0000000..e5d4210 --- /dev/null +++ b/framework/yii/web/ResponseEvent.php @@ -0,0 +1,39 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use yii\base\Event; + +/** + * ResponseEvent represents the event data for the [[Application::EVENT_RESPONSE]] event. + * + * Event handlers can modify the content in [[response]] or replace [[response]] + * with a new response object. The updated or new response will + * be used as the final out of the application. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ResponseEvent extends Event +{ + /** + * @var Response the response object associated with this event. + */ + public $response; + + /** + * Constructor. + * @param Response $response the response object associated with this event. + * @param array $config the configuration array for initializing the newly created object. + */ + public function __construct($response, $config = array()) + { + $this->response = $response; + parent::__construct($config); + } +} diff --git a/framework/yii/web/ResponseFormatterInterface.php b/framework/yii/web/ResponseFormatterInterface.php new file mode 100644 index 0000000..689ee1e --- /dev/null +++ b/framework/yii/web/ResponseFormatterInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +/** + * ResponseFormatterInterface specifies the interface needed to format a response before it is sent out. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +interface ResponseFormatterInterface +{ + /** + * Formats the specified response. + * @param Response $response the response to be formatted. + */ + public function format($response); +} diff --git a/yii/web/Session.php b/framework/yii/web/Session.php similarity index 85% rename from yii/web/Session.php rename to framework/yii/web/Session.php index 1b48433..92ec3ad 100644 --- a/yii/web/Session.php +++ b/framework/yii/web/Session.php @@ -45,6 +45,28 @@ use yii\base\InvalidParamException; * useful for displaying confirmation messages. To use flash messages, simply * call methods such as [[setFlash()]], [[getFlash()]]. * + * @property array $allFlashes Flash messages (key => message). This property is read-only. + * @property array $cookieParams The session cookie parameters. + * @property integer $count The number of session variables. This property is read-only. + * @property string $flash The key identifying the flash message. Note that flash messages and normal session + * variables share the same name space. If you have a normal session variable using the same name, its value will + * be overwritten by this method. This property is write-only. + * @property float $gCProbability The probability (percentage) that the GC (garbage collection) process is + * started on every session initialization, defaults to 1 meaning 1% chance. + * @property string $id The current session ID. + * @property boolean $isActive Whether the session has started. This property is read-only. + * @property SessionIterator $iterator An iterator for traversing the session variables. This property is + * read-only. + * @property string $name The current session name. + * @property string $savePath The current session save path, defaults to '/tmp'. + * @property integer $timeout The number of seconds after which data will be seen as 'garbage' and cleaned up. + * The default value is 1440 seconds (or the value of "session.gc_maxlifetime" set in php.ini). + * @property boolean|null $useCookies The value indicating whether cookies should be used to store session + * IDs. + * @property boolean $useCustomStorage Whether to use custom storage. This property is read-only. + * @property boolean $useTransparentSessionID Whether transparent sid support is enabled or not, defaults to + * false. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -63,7 +85,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co * @var array parameter-value pairs to override default session cookie parameters */ public $cookieParams = array( - 'httponly' => true + 'httpOnly' => true ); /** @@ -241,26 +263,31 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function getCookieParams() { - return session_get_cookie_params(); + $params = session_get_cookie_params(); + if (isset($params['httponly'])) { + $params['httpOnly'] = $params['httponly']; + unset($params['httponly']); + } + return $params; } /** * Sets the session cookie parameters. * The effect of this method only lasts for the duration of the script. * Call this method before the session starts. - * @param array $value cookie parameters, valid keys include: lifetime, path, domain, secure and httponly. + * @param array $value cookie parameters, valid keys include: `lifetime`, `path`, `domain`, `secure` and `httpOnly`. * @throws InvalidParamException if the parameters are incomplete. * @see http://us2.php.net/manual/en/function.session-set-cookie-params.php */ public function setCookieParams($value) { - $data = session_get_cookie_params(); + $data = $this->getCookieParams(); extract($data); extract($value); - if (isset($lifetime, $path, $domain, $secure, $httponly)) { - session_set_cookie_params($lifetime, $path, $domain, $secure, $httponly); + if (isset($lifetime, $path, $domain, $secure, $httpOnly)) { + session_set_cookie_params($lifetime, $path, $domain, $secure, $httpOnly); } else { - throw new InvalidParamException('Please make sure these parameters are provided: lifetime, path, domain, secure and httponly.'); + throw new InvalidParamException('Please make sure these parameters are provided: lifetime, path, domain, secure and httpOnly.'); } } @@ -555,12 +582,22 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co * A flash message is available only in the current request and the next request. * @param string $key the key identifying the flash message * @param mixed $defaultValue value to be returned if the flash message does not exist. + * @param boolean $delete whether to delete this flash message right after this method is called. + * If false, the flash message will be automatically deleted after the next request. * @return mixed the flash message */ - public function getFlash($key, $defaultValue = null) + public function getFlash($key, $defaultValue = null, $delete = false) { $counters = $this->get($this->flashVar, array()); - return isset($counters[$key]) ? $this->get($key, $defaultValue) : $defaultValue; + if (isset($counters[$key])) { + $value = $this->get($key, $defaultValue); + if ($delete) { + $this->removeFlash($key); + } + return $value; + } else { + return $defaultValue; + } } /** diff --git a/yii/base/DictionaryIterator.php b/framework/yii/web/SessionIterator.php similarity index 74% rename from yii/base/DictionaryIterator.php rename to framework/yii/web/SessionIterator.php index 0d15bb0..c960dd4 100644 --- a/yii/base/DictionaryIterator.php +++ b/framework/yii/web/SessionIterator.php @@ -5,24 +5,17 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\base; +namespace yii\web; /** - * DictionaryIterator implements the SPL `Iterator` interface for [[Dictionary]]. - * - * It allows [[Dictionary]] to return a new iterator for data traversing purpose. - * You normally do not use this class directly. + * SessionIterator implements an iterator for traversing session variables managed by [[Session]]. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class DictionaryIterator implements \Iterator +class SessionIterator implements \Iterator { /** - * @var array the data to be iterated through - */ - private $_d; - /** * @var array list of keys in the map */ private $_keys; @@ -33,18 +26,15 @@ class DictionaryIterator implements \Iterator /** * Constructor. - * @param array $data the data to be iterated through */ - public function __construct(&$data) + public function __construct() { - $this->_d = &$data; - $this->_keys = array_keys($data); - $this->_key = reset($this->_keys); + $this->_keys = array_keys($_SESSION); } /** - * Rewinds the index of the current item. - * This method is required by the SPL interface `Iterator`. + * Rewinds internal array pointer. + * This method is required by the interface Iterator. */ public function rewind() { @@ -53,7 +43,7 @@ class DictionaryIterator implements \Iterator /** * Returns the key of the current array element. - * This method is required by the SPL interface `Iterator`. + * This method is required by the interface Iterator. * @return mixed the key of the current array element */ public function key() @@ -63,27 +53,29 @@ class DictionaryIterator implements \Iterator /** * Returns the current array element. - * This method is required by the SPL interface `Iterator`. + * This method is required by the interface Iterator. * @return mixed the current array element */ public function current() { - return $this->_d[$this->_key]; + return isset($_SESSION[$this->_key]) ? $_SESSION[$this->_key] : null; } /** - * Moves the internal pointer to the next element. - * This method is required by the SPL interface `Iterator`. + * Moves the internal pointer to the next array element. + * This method is required by the interface Iterator. */ public function next() { - $this->_key = next($this->_keys); + do { + $this->_key = next($this->_keys); + } while (!isset($_SESSION[$this->_key]) && $this->_key !== false); } /** * Returns whether there is an element at current position. - * This method is required by the SPL interface `Iterator`. - * @return boolean whether there is an item at current position. + * This method is required by the interface Iterator. + * @return boolean */ public function valid() { diff --git a/yii/web/UploadedFile.php b/framework/yii/web/UploadedFile.php similarity index 75% rename from yii/web/UploadedFile.php rename to framework/yii/web/UploadedFile.php index 6e685a3..c4e2f0f 100644 --- a/yii/web/UploadedFile.php +++ b/framework/yii/web/UploadedFile.php @@ -7,41 +7,53 @@ namespace yii\web; -use yii\widgets\Html; +use yii\base\Object; +use yii\helpers\Html; /** + * UploadedFile represents the information for an uploaded file. + * + * You can call [[getInstance()]] to retrieve the instance of an uploaded file, + * and then use [[saveAs()]] to save it on the server. + * You may also query other information about the file, including [[name]], + * [[tempName]], [[type]], [[size]] and [[error]]. + * + * @property boolean $hasError Whether there is an error with the uploaded file. Check [[error]] for detailed + * error code information. This property is read-only. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class UploadedFile extends \yii\base\Object +class UploadedFile extends 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 + * @var string the original name of the file being uploaded */ - public function __construct($name, $tempName, $type, $size, $error) - { - $this->_name = $name; - $this->_tempName = $tempName; - $this->_type = $type; - $this->_size = $size; - $this->_error = $error; - } + public $name; + /** + * @var 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 $tempName; + /** + * @var 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 $type; + /** + * @var integer the actual size of the uploaded file in bytes + */ + public $size; + /** + * @var integer an error code describing the status of this file uploading. + * @see http://www.php.net/manual/en/features.file-upload.errors.php + */ + public $error; + /** * String output. @@ -51,7 +63,7 @@ class UploadedFile extends \yii\base\Object */ public function __toString() { - return $this->_name; + return $this->name; } /** @@ -142,69 +154,23 @@ class UploadedFile extends \yii\base\Object */ public function saveAs($file, $deleteTempFile = true) { - if ($this->_error == UPLOAD_ERR_OK) { + 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 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; + return $this->error != UPLOAD_ERR_OK; } /** @@ -240,7 +206,13 @@ class UploadedFile extends \yii\base\Object self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i]); } } else { - self::$_files[$key] = new self($names, $tempNames, $types, $sizes, $errors); + self::$_files[$key] = new static(array( + 'name' => $names, + 'tempName' => $tempNames, + 'type' => $types, + 'size' => $sizes, + 'error' => $errors, + )); } } } diff --git a/yii/web/UrlManager.php b/framework/yii/web/UrlManager.php similarity index 70% rename from yii/web/UrlManager.php rename to framework/yii/web/UrlManager.php index aab7979..0d9547f 100644 --- a/yii/web/UrlManager.php +++ b/framework/yii/web/UrlManager.php @@ -14,6 +14,10 @@ use yii\caching\Cache; /** * UrlManager handles HTTP request parsing and creation of URLs based on a set of rules. * + * @property string $baseUrl The base URL that is used by [[createUrl()]] to prepend URLs it creates. + * @property string $hostInfo The host info (e.g. "http://www.example.com") that is used by + * [[createAbsoluteUrl()]] to prepend URLs it creates. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -27,6 +31,13 @@ class UrlManager extends Component */ public $enablePrettyUrl = false; /** + * @var boolean whether to enable strict parsing. If strict parsing is enabled, the incoming + * requested URL must match at least one of the [[rules]] in order to be treated as a valid request. + * Otherwise, the path info part of the request will be treated as the requested route. + * This property is used only when [[enablePrettyUrl]] is true. + */ + public $enableStrictParsing = false; + /** * @var array the rules for creating and parsing URLs when [[enablePrettyUrl]] is true. * This property is used only if [[enablePrettyUrl]] is true. Each element in the array * is the configuration array for creating a single URL rule. The configuration will @@ -37,6 +48,31 @@ class UrlManager extends Component * array, one can use the key to represent the pattern and the value the corresponding route. * For example, `'post/<id:\d+>' => 'post/view'`. * + * For RESTful routing the mentioned shortcut format also allows you to specify the + * [[UrlRule::verb|HTTP verb]] that the rule should apply for. + * You can do that by prepending it to the pattern, separated by space. + * For example, `'PUT post/<id:\d+>' => 'post/update'`. + * You may specify multiple verbs by separating them with comma + * like this: `'POST,PUT post/index' => 'post/create'`. + * The supported verbs in the shortcut format are: GET, HEAD, POST, PUT, PATCH and DELETE. + * Note that [[UrlRule::mode|mode]] will be set to PARSING_ONLY when specifying verb in this way + * so you normally would not specify a verb for normal GET request. + * + * Here is an example configuration for RESTful CRUD controller: + * + * ~~~php + * array( + * 'dashboard' => 'site/index', + * + * 'POST <controller:\w+>s' => '<controller>/create', + * '<controller:\w+>s' => '<controller>/index', + * + * 'PUT <controller:\w+>/<id:\d+>' => '<controller>/update', + * 'DELETE <controller:\w+>/<id:\d+>' => '<controller>/delete', + * '<controller:\w+>/<id:\d+>' => '<controller>/view', + * ); + * ~~~ + * * Note that if you modify this property after the UrlManager object is created, make sure * you populate the array with rule objects instead of rule configurations. */ @@ -51,7 +87,7 @@ class UrlManager extends Component * @var boolean whether to show entry script name in the constructed URL. Defaults to true. * This property is used only if [[enablePrettyUrl]] is true. */ - public $showScriptName = false; + public $showScriptName = true; /** * @var string the GET variable name for route. This property is used only if [[enablePrettyUrl]] is false. */ @@ -97,7 +133,7 @@ class UrlManager extends Component $this->cache = Yii::$app->getComponent($this->cache); } if ($this->cache instanceof Cache) { - $key = $this->cache->buildKey(__CLASS__); + $key = __CLASS__; $hash = md5(json_encode($this->rules)); if (($data = $this->cache->get($key)) !== false && isset($data[1]) && $data[1] === $hash) { $this->rules = $data[0]; @@ -109,9 +145,14 @@ class UrlManager extends Component foreach ($this->rules as $key => $rule) { if (!is_array($rule)) { $rule = array( - 'pattern' => $key, 'route' => $rule, ); + if (preg_match('/^((?:(GET|HEAD|POST|PUT|PATCH|DELETE),)*(GET|HEAD|POST|PUT|PATCH|DELETE))\s+(.*)$/', $key, $matches)) { + $rule['verb'] = explode(',', $matches[1]); + $rule['mode'] = UrlRule::PARSING_ONLY; + $key = $matches[4]; + } + $rule['pattern'] = $key; } $rules[] = Yii::createObject(array_merge($this->ruleConfig, $rule)); } @@ -131,16 +172,21 @@ class UrlManager extends Component public function parseRequest($request) { if ($this->enablePrettyUrl) { - $pathInfo = $request->pathInfo; + $pathInfo = $request->getPathInfo(); /** @var $rule UrlRule */ foreach ($this->rules as $rule) { if (($result = $rule->parseRequest($this, $request)) !== false) { + Yii::trace("Request parsed with URL rule: {$rule->name}", __METHOD__); return $result; } } + if ($this->enableStrictParsing) { + return false; + } + $suffix = (string)$this->suffix; - if ($suffix !== '' && $suffix !== '/' && $pathInfo !== '') { + if ($suffix !== '' && $pathInfo !== '') { $n = strlen($this->suffix); if (substr($pathInfo, -$n) === $this->suffix) { $pathInfo = substr($pathInfo, 0, -$n); @@ -154,12 +200,14 @@ class UrlManager extends Component } } + Yii::trace('No matching URL rules. Using default URL parsing logic.', __METHOD__); return array($pathInfo, array()); } else { - $route = $request->getParam($this->routeVar); + $route = $request->get($this->routeVar); if (is_array($route)) { $route = ''; } + Yii::trace('Pretty URL not enabled. Using default URL parsing logic.', __METHOD__); return array((string)$route, array()); } } @@ -183,7 +231,15 @@ class UrlManager extends Component /** @var $rule UrlRule */ foreach ($this->rules as $rule) { if (($url = $rule->createUrl($this, $route, $params)) !== false) { - return rtrim($baseUrl, '/') . '/' . $url . $anchor; + if ($rule->host !== null) { + if ($baseUrl !== '' && ($pos = strpos($url, '/', 8)) !== false) { + return substr($url, 0, $pos) . $baseUrl . substr($url, $pos); + } else { + return $url . $baseUrl . $anchor; + } + } else { + return "$baseUrl/{$url}{$anchor}"; + } } } @@ -193,9 +249,9 @@ class UrlManager extends Component if (!empty($params)) { $route .= '?' . http_build_query($params); } - return rtrim($baseUrl, '/') . '/' . $route . $anchor; + return "$baseUrl/{$route}{$anchor}"; } else { - $url = $baseUrl . '?' . $this->routeVar . '=' . $route; + $url = "$baseUrl?{$this->routeVar}=$route"; if (!empty($params)) { $url .= '&' . http_build_query($params); } @@ -213,7 +269,12 @@ class UrlManager extends Component */ public function createAbsoluteUrl($route, $params = array()) { - return $this->getHostInfo() . $this->createUrl($route, $params); + $url = $this->createUrl($route, $params); + if (strpos($url, '://') !== false) { + return $url; + } else { + return $this->getHostInfo() . $url; + } } /** @@ -238,7 +299,7 @@ class UrlManager extends Component */ public function setBaseUrl($value) { - $this->_baseUrl = $value; + $this->_baseUrl = rtrim($value, '/'); } /** diff --git a/yii/web/UrlRule.php b/framework/yii/web/UrlRule.php similarity index 82% rename from yii/web/UrlRule.php rename to framework/yii/web/UrlRule.php index 96cb994..10f0627 100644 --- a/yii/web/UrlRule.php +++ b/framework/yii/web/UrlRule.php @@ -28,15 +28,25 @@ class UrlRule extends Object const CREATION_ONLY = 2; /** - * @var string regular expression used to parse a URL + * @var string the name of this rule. If not set, it will use [[pattern]] as the name. + */ + public $name; + /** + * @var string the pattern used to parse and create the path info part of a URL. + * @see host */ public $pattern; /** + * @var string the pattern used to parse and create the host info part of a URL. + * @see pattern + */ + public $host; + /** * @var string the route to the controller action */ public $route; /** - * @var array the default GET parameters (name=>value) that this rule provides. + * @var array the default GET parameters (name => value) that this rule provides. * When this rule is used to parse the incoming request, the values declared in this property * will be injected into $_GET. */ @@ -100,9 +110,15 @@ class UrlRule extends Object $this->verb = array(strtoupper($this->verb)); } } + if ($this->name === null) { + $this->name = $this->pattern; + } $this->pattern = trim($this->pattern, '/'); - if ($this->pattern === '') { + + if ($this->host !== null) { + $this->pattern = rtrim($this->host, '/') . rtrim('/' . $this->pattern, '/') . '/'; + } elseif ($this->pattern === '') { $this->_template = ''; $this->pattern = '#^$#u'; return; @@ -140,6 +156,7 @@ class UrlRule extends Object } } } + $tr['.'] = '\\.'; $this->_template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u'; @@ -162,11 +179,11 @@ class UrlRule extends Object return false; } - if ($this->verb !== null && !in_array($request->verb, $this->verb, true)) { + if ($this->verb !== null && !in_array($request->getMethod(), $this->verb, true)) { return false; } - $pathInfo = $request->pathInfo; + $pathInfo = $request->getPathInfo(); $suffix = (string)($this->suffix === null ? $manager->suffix : $this->suffix); if ($suffix !== '' && $pathInfo !== '') { $n = strlen($suffix); @@ -176,12 +193,15 @@ class UrlRule extends Object // suffix alone is not allowed return false; } - } elseif ($suffix !== '/') { - // we allow the ending '/' to be optional if it is a suffix + } else { return false; } } + if ($this->host !== null) { + $pathInfo = strtolower($request->getHostInfo()) . '/' . $pathInfo; + } + if (!preg_match($this->pattern, $pathInfo, $matches)) { return false; } @@ -267,7 +287,12 @@ class UrlRule extends Object } $url = trim(strtr($this->_template, $tr), '/'); - if (strpos($url, '//') !== false) { + if ($this->host !== null) { + $pos = strpos($url, '/', 8); + if ($pos !== false) { + $url = substr($url, 0, $pos) . preg_replace('#/+#', '/', substr($url, $pos)); + } + } elseif (strpos($url, '//') !== false) { $url = preg_replace('#/+#', '/', $url); } diff --git a/yii/web/User.php b/framework/yii/web/User.php similarity index 85% rename from yii/web/User.php rename to framework/yii/web/User.php index 7d8e300..0dd16d0 100644 --- a/yii/web/User.php +++ b/framework/yii/web/User.php @@ -9,8 +9,8 @@ namespace yii\web; use Yii; use yii\base\Component; -use yii\base\HttpException; use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; /** * User is the class for the "user" application component that manages the user authentication status. @@ -18,9 +18,17 @@ use yii\base\InvalidConfigException; * 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 + * User works with a class implementing the [[IdentityInterface]]. This class implements * the actual user authentication logic and is often backed by a user database table. * + * @property string|integer $id The unique identifier for the user. If null, it means the user is a guest. + * This property is read-only. + * @property IdentityInterface $identity The identity object associated with the currently logged user. Null + * is returned if the user is not logged in (not authenticated). + * @property boolean $isGuest Whether the current user is a guest. This property is read-only. + * @property string $returnUrl The URL that the user should be redirected to after login. Note that the type + * of this property differs in getter and setter. See [[getReturnUrl()]] and [[setReturnUrl()]] for details. + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -56,7 +64,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', 'httponly' => true); + 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 @@ -120,7 +128,7 @@ class User extends Component /** * Returns the identity object associated with the currently logged user. - * @return Identity the identity object associated with the currently logged user. + * @return IdentityInterface 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 @@ -132,8 +140,8 @@ class User extends Component if ($id === null) { $this->_identity = null; } else { - /** @var $class Identity */ - $class = Yii::import($this->identityClass); + /** @var $class IdentityInterface */ + $class = $this->identityClass; $this->_identity = $class::findIdentity($id); } } @@ -148,7 +156,7 @@ class User extends Component * 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. + * @param IdentityInterface $identity the identity object associated with the currently logged user. */ public function setIdentity($identity) { @@ -163,7 +171,7 @@ class User extends Component * 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 IdentityInterface $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 or the session is manually destroyed. * If greater than 0 and [[enableAutoLogin]] is true, cookie-based login will be supported. @@ -192,7 +200,7 @@ class User extends Component $data = json_decode($value, true); if (count($data) === 3 && isset($data[0], $data[1], $data[2])) { list ($id, $authKey, $duration) = $data; - /** @var $class Identity */ + /** @var $class IdentityInterface */ $class = $this->identityClass; $identity = $class::findIdentity($id); if ($identity !== null && $identity->validateAuthKey($authKey)) { @@ -221,7 +229,7 @@ class User extends Component if ($destroySession) { Yii::$app->getSession()->destroy(); } - $this->afterLogout($identity); + $this->afterLogout($identity); } } @@ -248,20 +256,34 @@ class User extends Component * 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|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. + * If this is null and the return URL was not set previously, [[Application::homeUrl]] will be redirected to. + * Please refer to [[setReturnUrl()]] on accepted format of the URL. * @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->returnUrlVar, $defaultUrl); + if (is_array($url)) { + if (isset($url[0])) { + $route = array_shift($url); + return Yii::$app->getUrlManager()->createUrl($route, $url); + } else { + $url = null; + } + } return $url === null ? Yii::$app->getHomeUrl() : $url; } /** * @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. + * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL. + * The first element of the array should be the route, and the rest of + * the name-value pairs are GET parameters used to construct the URL. For example, + * + * ~~~ + * array('admin/index', 'ref' => 1) + * ~~~ */ public function setReturnUrl($url) { @@ -280,13 +302,14 @@ class User extends Component public function loginRequired() { $request = Yii::$app->getRequest(); - if (!$request->getIsAjaxRequest()) { + if (!$request->getIsAjax()) { $this->setReturnUrl($request->getUrl()); } if ($this->loginUrl !== null) { - Yii::$app->getResponse()->redirect($this->loginUrl); + Yii::$app->getResponse()->redirect($this->loginUrl)->send(); + exit(); } else { - throw new HttpException(403, Yii::t('yii|Login Required')); + throw new HttpException(403, Yii::t('yii', 'Login Required')); } } @@ -295,7 +318,7 @@ class User extends Component * 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 IdentityInterface $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 */ @@ -314,7 +337,7 @@ class User extends Component * 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 IdentityInterface $identity the user identity information * @param boolean $cookieBased whether the login is cookie-based */ protected function afterLogin($identity, $cookieBased) @@ -330,7 +353,7 @@ class User extends Component * 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 + * @param IdentityInterface $identity the user identity information * @return boolean whether the user should continue to be logged out */ protected function beforeLogout($identity) @@ -347,7 +370,7 @@ class User extends Component * 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 + * @param IdentityInterface $identity the user identity information */ protected function afterLogout($identity) { @@ -379,9 +402,9 @@ class User extends Component /** * 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 + * It saves [[id]], [[IdentityInterface::getAuthKey()|auth key]], and the duration of cookie-based login * information in the cookie. - * @param Identity $identity + * @param IdentityInterface $identity * @param integer $duration number of seconds that the user can remain in logged-in status. * @see loginByCookie */ @@ -407,7 +430,7 @@ class User extends Component * 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. + * @param IdentityInterface $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. @@ -415,11 +438,13 @@ class User extends Component public function switchIdentity($identity, $duration = 0) { $session = Yii::$app->getSession(); - $session->regenerateID(true); + if (!YII_ENV_TEST) { + $session->regenerateID(true); + } $this->setIdentity($identity); $session->remove($this->idVar); $session->remove($this->authTimeoutVar); - if ($identity instanceof Identity) { + if ($identity instanceof IdentityInterface) { $session->set($this->idVar, $identity->getId()); if ($this->authTimeout !== null) { $session->set($this->authTimeoutVar, time() + $this->authTimeout); diff --git a/yii/web/UserEvent.php b/framework/yii/web/UserEvent.php similarity index 95% rename from yii/web/UserEvent.php rename to framework/yii/web/UserEvent.php index 4e39380..8577ef5 100644 --- a/yii/web/UserEvent.php +++ b/framework/yii/web/UserEvent.php @@ -10,6 +10,7 @@ namespace yii\web; use yii\base\Event; /** + * This event class is used for Events triggered by the [[User]] class. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -17,7 +18,7 @@ use yii\base\Event; class UserEvent extends Event { /** - * @var Identity the identity object associated with this event + * @var IdentityInterface the identity object associated with this event */ public $identity; /** diff --git a/framework/yii/web/VerbFilter.php b/framework/yii/web/VerbFilter.php new file mode 100644 index 0000000..4f45190 --- /dev/null +++ b/framework/yii/web/VerbFilter.php @@ -0,0 +1,89 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\ActionEvent; +use yii\base\Behavior; + +/** + * VerbFilter is an action filter that filters by HTTP request methods. + * + * It allows to define allowed HTTP request methods for each action and will throw + * an HTTP 405 error when the method is not allowed. + * + * To use VerbFilter, declare it in the `behaviors()` method of your controller class. + * For example, the following declarations will define a typical set of allowed + * request methods for REST CRUD actions. + * + * ~~~ + * public function behaviors() + * { + * return array( + * 'verbs' => array( + * 'class' => \yii\web\VerbFilter::className(), + * 'actions' => array( + * 'index' => array('get'), + * 'view' => array('get'), + * 'create' => array('get', 'post'), + * 'update' => array('get', 'put', 'post'), + * 'delete' => array('post', 'delete'), + * ), + * ), + * ); + * } + * ~~~ + * + * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7 + * @author Carsten Brandt <mail@cebe.cc> + * @since 2.0 + */ +class VerbFilter extends Behavior +{ + /** + * @var array this property defines the allowed request methods for each action. + * For each action that should only support limited set of request methods + * you add an entry with the action id as array key and an array of + * allowed methods (e.g. GET, HEAD, PUT) as the value. + * If an action is not listed all request methods are considered allowed. + */ + public $actions = array(); + + + /** + * Declares event handlers for the [[owner]]'s events. + * @return array events (array keys) and the corresponding event handler methods (array values). + */ + public function events() + { + return array( + Controller::EVENT_BEFORE_ACTION => 'beforeAction', + ); + } + + /** + * @param ActionEvent $event + * @return boolean + * @throws HttpException when the request method is not allowed. + */ + public function beforeAction($event) + { + $action = $event->action->id; + if (isset($this->actions[$action])) { + $verb = Yii::$app->getRequest()->getMethod(); + $allowed = array_map('strtoupper', $this->actions[$action]); + if (!in_array($verb, $allowed)) { + $event->isValid = false; + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7 + Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $allowed)); + throw new HttpException(405, 'Method Not Allowed. This url can only handle the following request methods: ' . implode(', ', $allowed)); + } + } + return $event->isValid; + } +} diff --git a/framework/yii/web/XmlResponseFormatter.php b/framework/yii/web/XmlResponseFormatter.php new file mode 100644 index 0000000..737011d --- /dev/null +++ b/framework/yii/web/XmlResponseFormatter.php @@ -0,0 +1,96 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use DOMDocument; +use DOMElement; +use DOMText; +use yii\base\Arrayable; +use yii\base\Component; +use yii\helpers\StringHelper; + +/** + * XmlResponseFormatter formats the given data into an XML response content. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class XmlResponseFormatter extends Component implements ResponseFormatterInterface +{ + /** + * @var string the Content-Type header for the response + */ + public $contentType = 'application/xml'; + /** + * @var string the XML version + */ + public $version = '1.0'; + /** + * @var string the XML encoding. If not set, it will use the value of [[Response::charset]]. + */ + public $encoding; + /** + * @var string the name of the root element. + */ + public $rootTag = 'response'; + /** + * @var string the name of the elements that represent the array elements with numeric keys. + */ + public $itemTag = 'item'; + + /** + * Formats the specified response. + * @param Response $response the response to be formatted. + */ + public function format($response) + { + $response->getHeaders()->set('Content-Type', $this->contentType); + $dom = new DOMDocument($this->version, $this->encoding === null ? $response->charset : $this->encoding); + $root = new DOMElement($this->rootTag); + $dom->appendChild($root); + $this->buildXml($root, $response->data); + $response->content = $dom->saveXML(); + } + + /** + * @param DOMElement $element + * @param mixed $data + */ + protected function buildXml($element, $data) + { + if (is_object($data)) { + $child = new DOMElement(StringHelper::basename(get_class($data))); + $element->appendChild($child); + if ($data instanceof Arrayable) { + $this->buildXml($child, $data->toArray()); + } else { + $array = array(); + foreach ($data as $name => $value) { + $array[$name] = $value; + } + $this->buildXml($child, $array); + } + } elseif (is_array($data)) { + foreach ($data as $name => $value) { + if (is_int($name) && is_object($value)) { + $this->buildXml($element, $value); + } elseif (is_array($value) || is_object($value)) { + $child = new DOMElement(is_int($name) ? $this->itemTag : $name); + $element->appendChild($child); + $this->buildXml($child, $value); + } else { + $child = new DOMElement(is_int($name) ? $this->itemTag : $name); + $element->appendChild($child); + $child->appendChild(new DOMText((string)$value)); + } + } + } else { + $element->appendChild(new DOMText((string)$data)); + } + } +} diff --git a/framework/yii/web/YiiAsset.php b/framework/yii/web/YiiAsset.php new file mode 100644 index 0000000..2ad5384 --- /dev/null +++ b/framework/yii/web/YiiAsset.php @@ -0,0 +1,26 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\View; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class YiiAsset extends AssetBundle +{ + public $sourcePath = '@yii/assets'; + public $js = array( + 'yii.js', + ); + public $depends = array( + 'yii\web\JqueryAsset', + ); +} diff --git a/yii/widgets/ActiveField.php b/framework/yii/widgets/ActiveField.php similarity index 55% rename from yii/widgets/ActiveField.php rename to framework/yii/widgets/ActiveField.php index 9f3f201..d31c50b 100644 --- a/yii/widgets/ActiveField.php +++ b/framework/yii/widgets/ActiveField.php @@ -6,8 +6,10 @@ */ namespace yii\widgets; +use Yii; use yii\base\Component; use yii\db\ActiveRecord; +use yii\helpers\ArrayHelper; use yii\helpers\Html; use yii\base\Model; use yii\web\JsExpression; @@ -31,38 +33,48 @@ class ActiveField extends Component */ public $attribute; /** - * @var string the tag name for the field container. - */ - public $tag = 'div'; - /** * @var array the HTML attributes (name-value pairs) for the field container tag. * The values will be HTML-encoded using [[Html::encode()]]. * If a value is null, the corresponding attribute will not be rendered. + * The following special options are recognized: + * + * - tag: the tag name of the container element. Defaults to "div". */ public $options = array( - 'class' => 'control-group', + 'class' => 'form-group', ); /** - * @var string the template that is used to arrange the label, the input and the error message. - * The following tokens will be replaced when [[render()]] is called: `{label}`, `{input}` and `{error}`. + * @var string the template that is used to arrange the label, the input field, the error message and the hint text. + * The following tokens will be replaced when [[render()]] is called: `{label}`, `{input}`, `{error}` and `{hint}`. */ - public $template = "{label}\n<div class=\"controls\">\n{input}\n{error}\n</div>"; + public $template = "{label}\n{input}\n{error}\n{hint}"; /** * @var array the default options for the input tags. The parameter passed to individual input methods * (e.g. [[textInput()]]) will be merged with this property when rendering the input tag. */ - public $inputOptions = array(); + public $inputOptions = array('class' => 'form-control'); /** * @var array the default options for the error tags. The parameter passed to [[error()]] will be * merged with this property when rendering the error tag. + * The following special options are recognized: + * + * - tag: the tag name of the container element. Defaults to "div". */ - public $errorOptions = array('tag' => 'span', 'class' => 'help-inline'); + public $errorOptions = array('class' => 'help-block'); /** * @var array the default options for the label tags. The parameter passed to [[label()]] will be * merged with this property when rendering the label tag. */ public $labelOptions = array('class' => 'control-label'); /** + * @var array the default options for the hint tags. The parameter passed to [[hint()]] will be + * merged with this property when rendering the hint tag. + * The following special options are recognized: + * + * - tag: the tag name of the container element. Defaults to "div". + */ + public $hintOptions = array('class' => 'hint-block'); + /** * @var boolean whether to enable client-side data validation. * If not set, it will take the value of [[ActiveForm::enableClientValidation]]. */ @@ -100,12 +112,80 @@ class ActiveField extends Component * You normally do not need to set this property as the default selectors should work well for most cases. */ public $selectors; + /** + * @var array different parts of the field (e.g. input, label). This will be used together with + * [[template]] to generate the final field HTML code. The keys are the token names in [[template]], + * while the values are the corresponding HTML code. Valid tokens include `{input}`, `{label}`, + * `{error}`, and `{error}`. Note that you normally don't need to access this property directly as + * it is maintained by various methods of this class. + */ + public $parts = array(); + + + /** + * PHP magic method that returns the string representation of this object. + * @return string the string representation of this object. + */ + public function __toString() + { + // __toString cannot throw exception + // use trigger_error to bypass this limitation + try { + return $this->render(); + } catch (\Exception $e) { + trigger_error($e->getMessage()); + return ''; + } + } + + /** + * Renders the whole field. + * This method will generate the label, error tag, input tag and hint tag (if any), and + * assemble them into HTML according to [[template]]. + * @param string|callable $content the content within the field container. + * If null (not set), the default methods will be called to generate the label, error tag and input tag, + * and use them as the content. + * If a callable, it will be called to generate the content. The signature of the callable should be: + * + * ~~~ + * function ($field) { + * return $html; + * } + * ~~~ + * + * @return string the rendering result + */ + public function render($content = null) + { + if ($content === null) { + if (!isset($this->parts['{input}'])) { + $this->parts['{input}'] = Html::activeTextInput($this->model, $this->attribute, $this->inputOptions); + } + if (!isset($this->parts['{label}'])) { + $this->parts['{label}'] = Html::activeLabel($this->model, $this->attribute, $this->labelOptions); + } + if (!isset($this->parts['{error}'])) { + $this->parts['{error}'] = Html::error($this->model, $this->attribute, $this->errorOptions); + } + if (!isset($this->parts['{hint}'])) { + $this->parts['{hint}'] = ''; + } + $content = strtr($this->template, $this->parts); + } elseif (!is_string($content)) { + $content = call_user_func($content, $this); + } + return $this->begin() . "\n" . $content . "\n" . $this->end(); + } + /** + * Renders the opening tag of the field container. + * @return string the rendering result. + */ public function begin() { - $options = $this->getClientOptions(); - if (!empty($options)) { - $this->form->attributes[$this->attribute] = $options; + $clientOptions = $this->getClientOptions(); + if (!empty($clientOptions)) { + $this->form->attributes[$this->attribute] = $clientOptions; } $inputID = Html::getInputId($this->model, $this->attribute); @@ -120,81 +200,37 @@ class ActiveField extends Component $class[] = $this->form->errorCssClass; } $options['class'] = implode(' ', $class); + $tag = ArrayHelper::remove($options, 'tag', 'div'); - return Html::beginTag($this->tag, $options); + return Html::beginTag($tag, $options); } + /** + * Renders the closing tag of the field container. + * @return string the rendering result. + */ public function end() { - return Html::endTag($this->tag); - } - - protected function getClientOptions() - { - $enableClientValidation = $this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation; - $enableAjaxValidation = $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation; - if ($enableClientValidation) { - $attribute = Html::getAttributeName($this->attribute); - $validators = array(); - foreach ($this->model->getActiveValidators($attribute) as $validator) { - /** @var \yii\validators\Validator $validator */ - $js = $validator->clientValidateAttribute($this->model, $attribute); - if ($validator->enableClientValidation && $js != '') { - $validators[] = $js; - } - } - if (!empty($validators)) { - $options['validate'] = new JsExpression("function(attribute,value,messages){" . implode('', $validators) . '}'); - } - } - - if ($enableAjaxValidation) { - $options['enableAjaxValidation'] = 1; - } - - if ($enableClientValidation && !empty($options['validate']) || $enableAjaxValidation) { - $inputID = Html::getInputId($this->model, $this->attribute); - $options['name'] = $inputID; - $names = array( - 'validateOnChange', - 'validateOnType', - 'validationDelay', - ); - foreach ($names as $name) { - $options[$name] = $this->$name === null ? $this->form->$name : $this->$name; - } - $options['container'] = isset($this->selectors['container']) ? $this->selectors['container'] : ".field-$inputID"; - $options['input'] = isset($this->selectors['input']) ? $this->selectors['input'] : "#$inputID"; - if (isset($this->errorOptions['class'])) { - $options['error'] = '.' . implode('.', preg_split('/\s+/', $this->errorOptions['class'], -1, PREG_SPLIT_NO_EMPTY)); - } else { - $options['error'] = isset($this->errorOptions['tag']) ? $this->errorOptions['tag'] : 'span'; - } - return $options; - } else { - return array(); - } + return Html::endTag(isset($this->options['tag']) ? $this->options['tag'] : 'div'); } /** * Generates a label tag for [[attribute]]. - * The label text is the label associated with the attribute, obtained via [[Model::getAttributeLabel()]]. + * @param string $label the label to use. If null, it will be generated via [[Model::getAttributeLabel()]]. + * Note that this will NOT be [[Html::encode()|encoded]]. * @param array $options the tag options in terms of name-value pairs. It will be merged with [[labelOptions]]. * 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. - * - * The following options are specially handled: - * - * - label: this specifies the label to be displayed. Note that this will NOT be [[encoded()]]. - * If this is not set, [[Model::getAttributeLabel()]] will be called to get the label for display - * (after encoding). - * - * @return string the generated label tag + * using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. + * @return static the field object itself */ - public function label($options = array()) + public function label($label = null, $options = array()) { $options = array_merge($this->labelOptions, $options); - return Html::activeLabel($this->model, $this->attribute, $options); + if ($label !== null) { + $options['label'] = $label; + } + $this->parts['{label}'] = Html::activeLabel($this->model, $this->attribute, $options); + return $this; } /** @@ -202,125 +238,118 @@ class ActiveField extends Component * Note that even if there is no validation error, this method will still return an empty error tag. * @param array $options the tag options in terms of name-value pairs. It will be merged with [[errorOptions]]. * 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. + * using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. * * The following options are specially handled: * - * - tag: this specifies the tag name. If not set, "span" will be used. + * - tag: this specifies the tag name. If not set, "div" will be used. * - * @return string the generated label tag + * @return static the field object itself */ public function error($options = array()) { $options = array_merge($this->errorOptions, $options); - $attribute = Html::getAttributeName($this->attribute); - $error = $this->model->getFirstError($attribute); - $tag = isset($options['tag']) ? $options['tag'] : 'span'; - unset($options['tag']); - return Html::tag($tag, Html::encode($error), $options); + $this->parts['{error}'] = Html::error($this->model, $this->attribute, $options); + return $this; } /** - * Renders the field with the given input HTML. - * This method will generate the label and error tags, and return them together with the given - * input HTML according to [[template]]. - * @param string $input the input HTML - * @return string the rendering result + * Renders the hint tag. + * @param string $content the hint content. It will NOT be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the hint tag. The values will be HTML-encoded using [[Html::encode()]]. + * + * The following options are specially handled: + * + * - tag: this specifies the tag name. If not set, "div" will be used. + * + * @return static the field object itself */ - public function render($input) + public function hint($content, $options = array()) { - return $this->begin() . "\n" . strtr($this->template, array( - '{input}' => $input, - '{label}' => $this->label(), - '{error}' => $this->error(), - )) . "\n" . $this->end(); + $options = array_merge($this->hintOptions, $options); + $tag = ArrayHelper::remove($options, 'tag', 'div'); + $this->parts['{hint}'] = Html::tag($tag, $content, $options); + return $this; } /** - * Generates an input tag for the given model attribute. + * Renders an input tag. * @param string $type the input type (e.g. 'text', 'password') * @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()]]. - * @return string the generated input tag + * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. + * @return static the field object itself */ public function input($type, $options = array()) { $options = array_merge($this->inputOptions, $options); - return $this->render(Html::activeInput($type, $this->model, $this->attribute, $options)); + $this->parts['{input}'] = Html::activeInput($type, $this->model, $this->attribute, $options); + return $this; } /** - * Generates a text input tag for the given model attribute. + * Renders a text input. * This method will generate the "name" and "value" tag attributes automatically for the model attribute * unless they are explicitly specified in `$options`. * @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()]]. - * @return string the generated input tag + * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. + * @return static the field object itself */ public function textInput($options = array()) { $options = array_merge($this->inputOptions, $options); - return $this->render(Html::activeTextInput($this->model, $this->attribute, $options)); + $this->parts['{input}'] = Html::activeTextInput($this->model, $this->attribute, $options); + return $this; } /** - * Generates a hidden input tag for the given model attribute. + * Renders a password input. * This method will generate the "name" and "value" tag attributes automatically for the model attribute * unless they are explicitly specified in `$options`. * @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()]]. - * @return string the generated input tag - */ - public function hiddenInput($options = array()) - { - $options = array_merge($this->inputOptions, $options); - return $this->render(Html::activeHiddenInput($this->model, $this->attribute, $options)); - } - - /** - * Generates a password input tag for the given model attribute. - * This method will generate the "name" and "value" tag attributes automatically for the model attribute - * unless they are explicitly specified in `$options`. - * @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()]]. - * @return string the generated input tag + * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. + * @return static the field object itself */ public function passwordInput($options = array()) { $options = array_merge($this->inputOptions, $options); - return $this->render(Html::activePasswordInput($this->model, $this->attribute, $options)); + $this->parts['{input}'] = Html::activePasswordInput($this->model, $this->attribute, $options); + return $this; } /** - * Generates a file input tag for the given model attribute. + * Renders a file input. * This method will generate the "name" and "value" tag attributes automatically for the model attribute * unless they are explicitly specified in `$options`. * @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()]]. - * @return string the generated input tag + * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. + * @return static the field object itself */ public function fileInput($options = array()) { - $options = array_merge($this->inputOptions, $options); - return $this->render(Html::activeFileInput($this->model, $this->attribute, $options)); + if ($this->inputOptions !== array('class' => 'form-control')) { + $options = array_merge($this->inputOptions, $options); + } + $this->parts['{input}'] = Html::activeFileInput($this->model, $this->attribute, $options); + return $this; } /** - * Generates a textarea tag for the given model attribute. + * Renders a text area. * The model attribute value will be used as the content in the textarea. * @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()]]. - * @return string the generated textarea tag + * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. + * @return static the field object itself */ public function textarea($options = array()) { $options = array_merge($this->inputOptions, $options); - return $this->render(Html::activeTextarea($this->model, $this->attribute, $options)); + $this->parts['{input}'] = Html::activeTextarea($this->model, $this->attribute, $options); + return $this; } /** - * Generates a radio button tag for the given model attribute. - * This method will generate the "name" tag attribute automatically unless it is explicitly specified in `$options`. + * Renders a radio button. * This method will generate the "checked" tag attribute according to the model attribute value. * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: * @@ -328,38 +357,34 @@ class ActiveField extends Component * it will take the default value '0'. This method will render a hidden input 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. + * - label: string, a label displayed next to the radio button. 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 [[Html::encode()]] it to prevent XSS attacks. + * When this option is specified, the radio button will be enclosed by a label tag. + * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. * * 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. + * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. * @param boolean $enclosedByLabel whether to enclose the radio within the label. * If true, the method will still use [[template]] to layout the checkbox and the error message * except that the radio is enclosed by the label tag. - * @return string the generated radio button tag + * @return static the field object itself */ public function radio($options = array(), $enclosedByLabel = true) { - $options = array_merge($this->inputOptions, $options); if ($enclosedByLabel) { - $hidden = ''; - $radio = Html::activeRadio($this->model, $this->attribute, $options); - if (($pos = strpos($radio, '><')) !== false) { - $hidden = substr($radio, 0, $pos + 1); - $radio = substr($radio, $pos + 1); + if (!isset($options['label'])) { + $options['label'] = Html::encode($this->model->getAttributeLabel($this->attribute)); } - $label = isset($this->labelOptions['label']) ? $this->labelOptions['label'] : Html::encode($this->model->getAttributeLabel($this->attribute)); - return $this->begin() . "\n" . $hidden . strtr($this->template, array( - '{input}' => Html::label("$radio $label", null, array('class' => 'radio')), - '{label}' => '', - '{error}' => $this->error(), - )) . "\n" . $this->end(); + $this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options); + $this->parts['{label}'] = ''; } else { - return $this->render(Html::activeRadio($this->model, $this->attribute, $options)); + $this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options); } + return $this; } /** - * Generates a checkbox tag for the given model attribute. - * This method will generate the "name" tag attribute automatically unless it is explicitly specified in `$options`. + * Renders a checkbox. * This method will generate the "checked" tag attribute according to the model attribute value. * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: * @@ -367,43 +392,40 @@ class ActiveField extends Component * it will take the default value '0'. This method will render a hidden input 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. + * - label: string, a label displayed next to the checkbox. 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 [[Html::encode()]] it to prevent XSS attacks. + * When this option is specified, the checkbox will be enclosed by a label tag. + * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. * * 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. + * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. * @param boolean $enclosedByLabel whether to enclose the checkbox within the label. * If true, the method will still use [[template]] to layout the checkbox and the error message * except that the checkbox is enclosed by the label tag. - * @return string the generated checkbox tag + * @return static the field object itself */ public function checkbox($options = array(), $enclosedByLabel = true) { - $options = array_merge($this->inputOptions, $options); if ($enclosedByLabel) { - $hidden = ''; - $checkbox = Html::activeCheckbox($this->model, $this->attribute, $options); - if (($pos = strpos($checkbox, '><')) !== false) { - $hidden = substr($checkbox, 0, $pos + 1); - $checkbox = substr($checkbox, $pos + 1); + if (!isset($options['label'])) { + $options['label'] = Html::encode($this->model->getAttributeLabel($this->attribute)); } - $label = isset($this->labelOptions['label']) ? $this->labelOptions['label'] : Html::encode($this->model->getAttributeLabel($this->attribute)); - return $this->begin() . "\n" . $hidden . strtr($this->template, array( - '{input}' => Html::label("$checkbox $label", null, array('class' => 'checkbox')), - '{label}' => '', - '{error}' => $this->error(), - )) . "\n" . $this->end(); + $this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options); + $this->parts['{label}'] = ''; } else { - return $this->render(Html::activeCheckbox($this->model, $this->attribute, $options)); + $this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options); } + return $this; } /** - * Generates a drop-down list for the given model attribute. + * Renders a drop-down list. * The selection of the drop-down list is taken from the value of the model attribute. * @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()]]. + * [[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. @@ -424,18 +446,19 @@ class ActiveField extends Component * 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. + * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. * - * @return string the generated drop-down list tag + * @return static the field object itself */ public function dropDownList($items, $options = array()) { $options = array_merge($this->inputOptions, $options); - return $this->render(Html::activeDropDownList($this->model, $this->attribute, $items, $options)); + $this->parts['{input}'] = Html::activeDropDownList($this->model, $this->attribute, $items, $options); + return $this; } /** - * Generates a list box. + * Renders a list box. * The selection of the list box is taken from the value of the model attribute. * @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). @@ -465,18 +488,19 @@ class ActiveField extends Component * 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. + * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. * - * @return string the generated list box tag + * @return static the field object itself */ public function listBox($items, $options = array()) { $options = array_merge($this->inputOptions, $options); - return $this->render(Html::activeListBox($this->model, $this->attribute, $items, $options)); + $this->parts['{input}'] = Html::activeListBox($this->model, $this->attribute, $items, $options); + return $this; } /** - * Generates a list of checkboxes. + * Renders a list of checkboxes. * A checkbox list allows multiple selection, like [[listBox()]]. * As a result, the corresponding submitted value is an array. * The selection of the checkbox list is taken from the value of the model attribute. @@ -498,19 +522,16 @@ class ActiveField extends Component * 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 + * @return static the field object itself */ public function checkboxList($items, $options = array()) { - return $this->render( - '<div id="' . Html::getInputId($this->model, $this->attribute) . '">' - . Html::activeCheckboxList($this->model, $this->attribute, $items, $options) - . '</div>' - ); + $this->parts['{input}'] = Html::activeCheckboxList($this->model, $this->attribute, $items, $options); + return $this; } /** - * Generates a list of radio buttons. + * Renders a list of radio buttons. * A radio button list is like a checkbox list, except that it only allows single selection. * The selection of the radio buttons is taken from the value of the model attribute. * @param array $items the data item used to generate the radio buttons. @@ -531,14 +552,84 @@ class ActiveField extends Component * 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 + * @return static the field object itself */ public function radioList($items, $options = array()) { - return $this->render( - '<div id="' . Html::getInputId($this->model, $this->attribute) . '">' - . Html::activeRadioList($this->model, $this->attribute, $items, $options) - . '</div>' - ); + $this->parts['{input}'] = Html::activeRadioList($this->model, $this->attribute, $items, $options); + return $this; + } + + /** + * Renders a widget as the input of the field. + * + * Note that the widget must have both `model` and `attribute` properties. They will + * be initialized with [[model]] and [[attribute]] of this field, respectively. + * + * If you want to use a widget that does not have `model` and `attribute` properties, + * please use [[render()]] instead. + * + * @param string $class the widget class name + * @param array $config name-value pairs that will be used to initialize the widget + * @return static the field object itself + */ + public function widget($class, $config = array()) + { + /** @var \yii\base\Widget $class */ + $config['model'] = $this->model; + $config['attribute'] = $this->attribute; + $config['view'] = $this->form->getView(); + $this->parts['{input}'] = $class::widget($config); + return $this; + } + + /** + * Returns the JS options for the field. + * @return array the JS options + */ + protected function getClientOptions() + { + $attribute = Html::getAttributeName($this->attribute); + if (!in_array($attribute, $this->model->activeAttributes(), true)) { + return array(); + } + + $enableClientValidation = $this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation; + if ($enableClientValidation) { + $validators = array(); + foreach ($this->model->getActiveValidators($attribute) as $validator) { + /** @var \yii\validators\Validator $validator */ + $js = $validator->clientValidateAttribute($this->model, $attribute, $this->form->getView()); + if ($validator->enableClientValidation && $js != '') { + $validators[] = $js; + } + } + if (!empty($validators)) { + $options['validate'] = new JsExpression("function(attribute, value, messages) {" . implode('', $validators) . '}'); + } + } + + $enableAjaxValidation = $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation; + if ($enableAjaxValidation) { + $options['enableAjaxValidation'] = 1; + } + + if ($enableClientValidation && !empty($options['validate']) || $enableAjaxValidation) { + $inputID = Html::getInputId($this->model, $this->attribute); + $options['name'] = $inputID; + foreach (array('validateOnChange', 'validateOnType', 'validationDelay') as $name) { + $options[$name] = $this->$name === null ? $this->form->$name : $this->$name; + } + $options['container'] = isset($this->selectors['container']) ? $this->selectors['container'] : ".field-$inputID"; + $options['input'] = isset($this->selectors['input']) ? $this->selectors['input'] : "#$inputID"; + if (isset($this->errorOptions['class'])) { + $options['error'] = '.' . implode('.', preg_split('/\s+/', $this->errorOptions['class'], -1, PREG_SPLIT_NO_EMPTY)); + } else { + $options['error'] = isset($this->errorOptions['tag']) ? $this->errorOptions['tag'] : 'div'; + } + return $options; + } else { + return array(); + } } } diff --git a/yii/widgets/ActiveForm.php b/framework/yii/widgets/ActiveForm.php similarity index 83% rename from yii/widgets/ActiveForm.php rename to framework/yii/widgets/ActiveForm.php index 24451b9..b46ad46 100644 --- a/yii/widgets/ActiveForm.php +++ b/framework/yii/widgets/ActiveForm.php @@ -12,6 +12,7 @@ use yii\base\Widget; use yii\base\Model; use yii\helpers\Html; use yii\helpers\Json; +use yii\web\JsExpression; /** * ActiveForm ... @@ -52,11 +53,11 @@ class ActiveForm extends Widget /** * @var string the CSS class that is added to a field container when the associated attribute has validation error. */ - public $errorCssClass = 'error'; + public $errorCssClass = 'has-error'; /** * @var string the CSS class that is added to a field container when the associated attribute is successfully validated. */ - public $successCssClass = 'success'; + public $successCssClass = 'has-success'; /** * @var string the CSS class that is added to a field container when the associated attribute is being validated. */ @@ -103,6 +104,38 @@ class ActiveForm extends Widget */ public $ajaxVar = 'ajax'; /** + * @var string|JsExpression a JS callback that will be called when the form is being submitted. + * The signature of the callback should be: + * + * ~~~ + * function ($form) { + * ...return false to cancel submission... + * } + * ~~~ + */ + public $beforeSubmit; + /** + * @var string|JsExpression a JS callback that is called before validating an attribute. + * The signature of the callback should be: + * + * ~~~ + * function ($form, attribute, messages) { + * ...return false to cancel the validation... + * } + * ~~~ + */ + public $beforeValidate; + /** + * @var string|JsExpression a JS callback that is called after validating an attribute. + * The signature of the callback should be: + * + * ~~~ + * function ($form, attribute, messages) { + * } + * ~~~ + */ + public $afterValidate; + /** * @var array the client validation options for individual attributes. Each element of the array * represents the validation options for a particular attribute. * @internal @@ -119,7 +152,7 @@ class ActiveForm extends Widget $this->options['id'] = $this->getId(); } if (!isset($this->fieldConfig['class'])) { - $this->fieldConfig['class'] = 'yii\widgets\ActiveField'; + $this->fieldConfig['class'] = ActiveField::className(); } echo Html::beginForm($this->action, $this->method, $this->options); } @@ -134,8 +167,9 @@ class ActiveForm extends Widget $id = $this->options['id']; $options = Json::encode($this->getClientOptions()); $attributes = Json::encode($this->attributes); - $this->view->registerAssetBundle('yii/form'); - $this->view->registerJs("jQuery('#$id').yiiActiveForm($attributes, $options);"); + $view = $this->getView(); + ActiveFormAsset::register($view); + $view->registerJs("jQuery('#$id').yiiActiveForm($attributes, $options);"); } echo Html::endForm(); } @@ -157,6 +191,11 @@ class ActiveForm extends Widget if ($this->validationUrl !== null) { $options['validationUrl'] = Html::url($this->validationUrl); } + foreach (array('beforeSubmit', 'beforeValidate', 'afterValidate') as $name) { + if (($value = $this->$name) !== null) { + $options[$name] = $value instanceof JsExpression ? $value : new JsExpression($value); + } + } return $options; } @@ -187,7 +226,7 @@ class ActiveForm extends Widget } } - $header = isset($options['header']) ? $options['header'] : '<p>' . Yii::t('yii|Please fix the following errors:') . '</p>'; + $header = isset($options['header']) ? $options['header'] : '<p>' . Yii::t('yii', 'Please fix the following errors:') . '</p>'; $footer = isset($options['footer']) ? $options['footer'] : ''; unset($options['header'], $options['footer']); diff --git a/framework/yii/widgets/ActiveFormAsset.php b/framework/yii/widgets/ActiveFormAsset.php new file mode 100644 index 0000000..cbd3f9a --- /dev/null +++ b/framework/yii/widgets/ActiveFormAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ActiveFormAsset extends AssetBundle +{ + public $sourcePath = '@yii/assets'; + public $js = array( + 'yii.activeForm.js', + ); + public $depends = array( + 'yii\web\YiiAsset', + ); +} diff --git a/framework/yii/widgets/BaseListView.php b/framework/yii/widgets/BaseListView.php new file mode 100644 index 0000000..d59c197 --- /dev/null +++ b/framework/yii/widgets/BaseListView.php @@ -0,0 +1,191 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use Yii; +use yii\base\InvalidConfigException; +use yii\base\Widget; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +abstract class BaseListView extends Widget +{ + /** + * @var array the HTML attributes for the container tag of the list view. + * The "tag" element specifies the tag name of the container element and defaults to "div". + */ + public $options = array(); + /** + * @var \yii\data\DataProviderInterface the data provider for the view. This property is required. + */ + public $dataProvider; + /** + * @var array the configuration for the pager widget. By default, [[LinkPager]] will be + * used to render the pager. You can use a different widget class by configuring the "class" element. + */ + public $pager = array(); + /** + * @var array the configuration for the sorter widget. By default, [[LinkSorter]] will be + * used to render the sorter. You can use a different widget class by configuring the "class" element. + */ + public $sorter = array(); + /** + * @var string the HTML content to be displayed as the summary of the list view. + * If you do not want to show the summary, you may set it with an empty string. + * + * The following tokens will be replaced with the corresponding values: + * + * - `{begin}`: the starting row number (1-based) currently being displayed + * - `{end}`: the ending row number (1-based) currently being displayed + * - `{count}`: the number of rows currently being displayed + * - `{totalCount}`: the total number of rows available + * - `{page}`: the page number (1-based) current being displayed + * - `{pageCount}`: the number of pages available + */ + public $summary; + /** + * @var string|boolean the HTML content to be displayed when [[dataProvider]] does not have any data. + * If false, the list view will still be displayed (without body content though). + */ + public $empty; + /** + * @var string the layout that determines how different sections of the list view should be organized. + * The following tokens will be replaced with the corresponding section contents: + * + * - `{summary}`: the summary section. See [[renderSummary()]]. + * - `{items}`: the list items. See [[renderItems()]]. + * - `{sorter}`: the sorter. See [[renderSorter()]]. + * - `{pager}`: the pager. See [[renderPager()]]. + */ + public $layout = "{summary}\n{items}\n{pager}"; + + + /** + * Renders the data models. + * @return string the rendering result. + */ + abstract public function renderItems(); + + /** + * Initializes the view. + */ + public function init() + { + if ($this->dataProvider === null) { + throw new InvalidConfigException('The "dataProvider" property must be set.'); + } + $this->dataProvider->prepare(); + } + + /** + * Runs the widget. + */ + public function run() + { + if ($this->dataProvider->getCount() > 0 || $this->empty === false) { + $content = preg_replace_callback("/{\\w+}/", function ($matches) { + $content = $this->renderSection($matches[0]); + return $content === false ? $matches[0] : $content; + }, $this->layout); + } else { + $content = '<div class="empty">' . ($this->empty === null ? Yii::t('yii', 'No results found.') : $this->empty) . '</div>'; + } + $tag = ArrayHelper::remove($this->options, 'tag', 'div'); + echo Html::tag($tag, $content, $this->options); + } + + /** + * Renders a section of the specified name. + * If the named section is not supported, false will be returned. + * @param string $name the section name, e.g., `{summary}`, `{items}`. + * @return string|boolean the rendering result of the section, or false if the named section is not supported. + */ + public function renderSection($name) + { + switch ($name) { + case '{summary}': + return $this->renderSummary(); + case '{items}': + return $this->renderItems(); + case '{pager}': + return $this->renderPager(); + case '{sorter}': + return $this->renderSorter(); + default: + return false; + } + } + + /** + * Renders the summary text. + */ + public function renderSummary() + { + $count = $this->dataProvider->getCount(); + if (($pagination = $this->dataProvider->getPagination()) !== false && $count > 0) { + $totalCount = $this->dataProvider->getTotalCount(); + $begin = $pagination->getPage() * $pagination->pageSize + 1; + $end = $begin + $count - 1; + $page = $pagination->getPage() + 1; + $pageCount = $pagination->pageCount; + if (($summaryContent = $this->summary) === null) { + $summaryContent = '<div class="summary">' . Yii::t('yii', 'Showing <b>{begin}-{end}</b> of <b>{totalCount}</b> {0, plural, =1{item} other{items}}.', $totalCount) . '</div>'; + } + } else { + $begin = $page = $pageCount = 1; + $end = $totalCount = $count; + if (($summaryContent = $this->summary) === null) { + $summaryContent = '<div class="summary">' . Yii::t('yii', 'Total <b>{count}</b> {0, plural, =1{item} other{items}}.', $count) . '</div>'; + } + } + return strtr($summaryContent, array( + '{begin}' => $begin, + '{end}' => $end, + '{count}' => $count, + '{totalCount}' => $totalCount, + '{page}' => $page, + '{pageCount}' => $pageCount, + )); + } + + /** + * Renders the pager. + * @return string the rendering result + */ + public function renderPager() + { + $pagination = $this->dataProvider->getPagination(); + if ($pagination === false || $this->dataProvider->getCount() <= 0) { + return ''; + } + /** @var LinkPager $class */ + $class = ArrayHelper::remove($this->pager, 'class', LinkPager::className()); + $this->pager['pagination'] = $pagination; + return $class::widget($this->pager); + } + + /** + * Renders the sorter. + * @return string the rendering result + */ + public function renderSorter() + { + $sort = $this->dataProvider->getSort(); + if ($sort === false || empty($sort->attributes) || $this->dataProvider->getCount() <= 0) { + return ''; + } + /** @var LinkSorter $class */ + $class = ArrayHelper::remove($this->sorter, 'class', LinkSorter::className()); + $this->sorter['sort'] = $sort; + return $class::widget($this->sorter); + } +} diff --git a/yii/widgets/Block.php b/framework/yii/widgets/Block.php similarity index 100% rename from yii/widgets/Block.php rename to framework/yii/widgets/Block.php diff --git a/framework/yii/widgets/Breadcrumbs.php b/framework/yii/widgets/Breadcrumbs.php new file mode 100644 index 0000000..f1427aa --- /dev/null +++ b/framework/yii/widgets/Breadcrumbs.php @@ -0,0 +1,141 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use Yii; +use yii\base\Widget; +use yii\base\InvalidConfigException; +use yii\helpers\Html; + +/** + * Breadcrumbs displays a list of links indicating the position of the current page in the whole site hierarchy. + * + * For example, breadcrumbs like "Home / Sample Post / Edit" means the user is viewing an edit page + * for the "Sample Post". He can click on "Sample Post" to view that page, or he can click on "Home" + * to return to the homepage. + * + * To use Breadcrumbs, you need to configure its [[links]] property, which specifies the links to be displayed. For example, + * + * ~~~ + * // $this is the view object currently being used + * echo Breadcrumbs::widget(array( + * 'links' => array( + * array('label' => 'Sample Post', 'url' => array('post/edit', 'id' => 1)), + * 'Edit', + * ), + * )); + * ~~~ + * + * Because breadcrumbs usually appears in nearly every page of a website, you may consider placing it in a layout view. + * You can use a view parameter (e.g. `$this->params['breadcrumbs']`) to configure the links in different + * views. In the layout view, you assign this view parameter to the [[links]] property like the following: + * + * ~~~ + * // $this is the view object currently being used + * echo Breadcrumbs::widget(array( + * 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : array(), + * )); + * ~~~ + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Breadcrumbs extends Widget +{ + /** + * @var string the name of the breadcrumb container tag. + */ + public $tag = 'ul'; + /** + * @var array the HTML attributes for the breadcrumb container tag. + */ + public $options = array('class' => 'breadcrumb'); + /** + * @var boolean whether to HTML-encode the link labels. + */ + public $encodeLabels = true; + /** + * @var string the first hyperlink in the breadcrumbs (called home link). + * If this property is not set, it will default to a link pointing to [[\yii\web\Application::homeUrl]] + * with the label 'Home'. If this property is false, the home link will not be rendered. + */ + public $homeLink; + /** + * @var array list of links to appear in the breadcrumbs. If this property is empty, + * the widget will not render anything. Each array element represents a single link in the breadcrumbs + * with the following structure: + * + * ~~~ + * array( + * 'label' => 'label of the link', // required + * 'url' => 'url of the link', // optional, will be processed by Html::url() + * ) + * ~~~ + * + * If a link is active, you only need to specify its "label", and instead of writing `array('label' => $label)`, + * you should simply use `$label`. + */ + public $links = array(); + /** + * @var string the template used to render each inactive item in the breadcrumbs. The token `{link}` + * will be replaced with the actual HTML link for each inactive item. + */ + public $itemTemplate = "<li>{link}</li>\n"; + /** + * @var string the template used to render each active item in the breadcrumbs. The token `{link}` + * will be replaced with the actual HTML link for each active item. + */ + public $activeItemTemplate = "<li class=\"active\">{link}</li>\n"; + + /** + * Renders the widget. + */ + public function run() + { + if (empty($this->links)) { + return; + } + $links = array(); + if ($this->homeLink === null) { + $links[] = $this->renderItem(array( + 'label' => Yii::t('yii', 'Home'), + 'url' => Yii::$app->homeUrl, + ), $this->itemTemplate); + } elseif ($this->homeLink !== false) { + $links[] = $this->renderItem($this->homeLink, $this->itemTemplate); + } + foreach ($this->links as $link) { + if (!is_array($link)) { + $link = array('label' => $link); + } + $links[] = $this->renderItem($link, isset($link['url']) ? $this->itemTemplate : $this->activeItemTemplate); + } + echo Html::tag($this->tag, implode('', $links), $this->options); + } + + /** + * Renders a single breadcrumb item. + * @param array $link the link to be rendered. It must contain the "label" element. The "url" element is optional. + * @param string $template the template to be used to rendered the link. The token "{link}" will be replaced by the link. + * @return string the rendering result + * @throws InvalidConfigException if `$link` does not have "label" element. + */ + protected function renderItem($link, $template) + { + if (isset($link['label'])) { + $label = $this->encodeLabels ? Html::encode($link['label']) : $link['label']; + } else { + throw new InvalidConfigException('The "label" element is required for each link.'); + } + if (isset($link['url'])) { + return strtr($template, array('{link}' => Html::a($label, $link['url']))); + } else { + return strtr($template, array('{link}' => $label)); + } + } +} diff --git a/yii/base/Response.php b/framework/yii/widgets/ContentDecorator.php similarity index 51% rename from yii/base/Response.php rename to framework/yii/widgets/ContentDecorator.php index 396b073..f91117a 100644 --- a/yii/base/Response.php +++ b/framework/yii/widgets/ContentDecorator.php @@ -5,55 +5,48 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\base; +namespace yii\widgets; + +use yii\base\InvalidConfigException; +use yii\base\Widget; /** * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Response extends Component +class ContentDecorator extends Widget { /** - * Starts output buffering + * @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 function beginOutput() - { - ob_start(); - ob_implicit_flush(false); - } - + public $viewFile; /** - * Returns contents of the output buffer and discards it - * @return string output buffer contents + * @var array the parameters (name => value) to be extracted and made available in the decorative view. */ - public function endOutput() - { - return ob_get_clean(); - } + public $params = array(); /** - * Returns contents of the output buffer - * @return string output buffer contents + * Starts recording a clip. */ - public function getOutput() + public function init() { - return ob_get_contents(); + if ($this->viewFile === null) { + throw new InvalidConfigException('ContentDecorator::viewFile must be set.'); + } + ob_start(); + ob_implicit_flush(false); } /** - * Discards the output buffer - * @param boolean $all if true recursively discards all output buffers used + * Ends recording a clip. + * This method stops output buffering and saves the rendering result as a named clip in the controller. */ - public function cleanOutput($all = true) + public function run() { - if ($all) { - for ($level = ob_get_level(); $level > 0; --$level) { - if (!@ob_end_clean()) { - ob_clean(); - } - } - } else { - ob_end_clean(); - } + $params = $this->params; + $params['content'] = ob_get_clean(); + // render under the existing context + echo $this->view->renderFile($this->viewFile, $params); } } diff --git a/framework/yii/widgets/DetailView.php b/framework/yii/widgets/DetailView.php new file mode 100644 index 0000000..c3bf864 --- /dev/null +++ b/framework/yii/widgets/DetailView.php @@ -0,0 +1,207 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use Yii; +use yii\base\Arrayable; +use yii\base\Formatter; +use yii\base\InvalidConfigException; +use yii\base\Model; +use yii\base\Widget; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; +use yii\helpers\Inflector; + +/** + * DetailView displays the detail of a single data [[model]]. + * + * DetailView is best used for displaying a model in a regular format (e.g. each model attribute + * is displayed as a row in a table.) The model can be either an instance of [[Model]] + * or an associative array. + * + * DetailView uses the [[attributes]] property to determines which model attributes + * should be displayed and how they should be formatted. + * + * A typical usage of DetailView is as follows: + * + * ~~~ + * echo DetailView::widget(array( + * 'model' => $model, + * 'attributes' => array( + * 'title', // title attribute (in plain text) + * 'description:html', // description attribute in HTML + * array( // the owner name of the model + * 'label' => 'Owner', + * 'value' => $model->owner->name, + * ), + * ), + * )); + * ~~~ + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DetailView extends Widget +{ + /** + * @var array|object the data model whose details are to be displayed. This can be either a [[Model]] instance + * or an associative array. + */ + public $model; + /** + * @var array a list of attributes to be displayed in the detail view. Each array element + * represents the specification for displaying one particular attribute. + * + * An attribute can be specified as a string in the format of "Name" or "Name:Type", where "Name" refers to + * the attribute name, and "Type" represents the type of the attribute. The "Type" is passed to the [[Formatter::format()]] + * method to format an attribute value into a displayable text. Please refer to [[Formatter]] for the supported types. + * + * An attribute can also be specified in terms of an array with the following elements: + * + * - name: the attribute name. This is required if either "label" or "value" is not specified. + * - label: the label associated with the attribute. If this is not specified, it will be generated from the attribute name. + * - value: the value to be displayed. If this is not specified, it will be retrieved from [[model]] using the attribute name + * by calling [[ArrayHelper::getValue()]]. Note that this value will be formatted into a displayable text + * according to the "type" option. + * - type: the type of the value that determines how the value would be formatted into a displayable text. + * Please refer to [[Formatter]] for supported types. + * - visible: whether the attribute is visible. If set to `false`, the attribute will be displayed. + */ + public $attributes; + /** + * @var string|callback the template used to render a single attribute. If a string, the token `{label}` + * and `{value}` will be replaced with the label and the value of the corresponding attribute. + * If a callback (e.g. an anonymous function), the signature must be as follows: + * + * ~~~ + * function ($attribute, $index, $widget) + * ~~~ + * + * where `$attribute` refer to the specification of the attribute being rendered, `$index` is the zero-based + * index of the attribute in the [[attributes]] array, and `$widget` refers to this widget instance. + */ + public $template = "<tr><th>{label}</th><td>{value}</td></tr>"; + /** + * @var array the HTML attributes for the container tag of this widget. The "tag" option specifies + * what container tag should be used. It defaults to "table" if not set. + */ + public $options = array('class' => 'table table-striped table-bordered'); + /** + * @var array|Formatter the formatter used to format model attribute values into displayable texts. + * This can be either an instance of [[Formatter]] or an configuration array for creating the [[Formatter]] + * instance. If this property is not set, the "formatter" application component will be used. + */ + public $formatter; + + /** + * Initializes the detail view. + * This method will initialize required property values. + */ + public function init() + { + if ($this->model === null) { + throw new InvalidConfigException('Please specify the "model" property.'); + } + if ($this->formatter == null) { + $this->formatter = Yii::$app->getFormatter(); + } elseif (is_array($this->formatter)) { + $this->formatter = Yii::createObject($this->formatter); + } + if (!$this->formatter instanceof Formatter) { + throw new InvalidConfigException('The "formatter" property must be either a Format object or a configuration array.'); + } + $this->normalizeAttributes(); + } + + /** + * Renders the detail view. + * This is the main entry of the whole detail view rendering. + */ + public function run() + { + $rows = array(); + $i = 0; + foreach ($this->attributes as $attribute) { + $rows[] = $this->renderAttribute($attribute, $i++); + } + + $tag = ArrayHelper::remove($this->options, 'tag', 'table'); + echo Html::tag($tag, implode("\n", $rows), $this->options); + } + + /** + * Renders a single attribute. + * @param array $attribute the specification of the attribute to be rendered. + * @param integer $index the zero-based index of the attribute in the [[attributes]] array + * @return string the rendering result + */ + protected function renderAttribute($attribute, $index) + { + if (is_string($this->template)) { + return strtr($this->template, array( + '{label}' => $attribute['label'], + '{value}' => $this->formatter->format($attribute['value'], $attribute['type']), + )); + } else { + return call_user_func($this->template, $attribute, $index, $this); + } + } + + /** + * Normalizes the attribute specifications. + * @throws InvalidConfigException + */ + protected function normalizeAttributes() + { + if ($this->attributes === null) { + if ($this->model instanceof Model) { + $this->attributes = $this->model->attributes(); + } elseif (is_object($this->model)) { + $this->attributes = $this->model instanceof Arrayable ? $this->model->toArray() : array_keys(get_object_vars($this->model)); + } elseif (is_array($this->model)) { + $this->attributes = array_keys($this->model); + } else { + throw new InvalidConfigException('The "model" property must be either an array or an object.'); + } + sort($this->attributes); + } + + foreach ($this->attributes as $i => $attribute) { + if (is_string($attribute)) { + if (!preg_match('/^(\w+)(\s*:\s*(\w+))?$/', $attribute, $matches)) { + throw new InvalidConfigException('The attribute must be specified in the format of "Name" or "Name:Type"'); + } + $attribute = array( + 'name' => $matches[1], + 'type' => isset($matches[3]) ? $matches[3] : 'text', + ); + } + + if (!is_array($attribute)) { + throw new InvalidConfigException('The attribute configuration must be an array.'); + } + + if (!isset($attribute['type'])) { + $attribute['type'] = 'text'; + } + if (isset($attribute['name'])) { + $name = $attribute['name']; + if (!isset($attribute['label'])) { + $attribute['label'] = $this->model instanceof Model ? $this->model->getAttributeLabel($name) : Inflector::camel2words($name, true); + } + if (!array_key_exists('value', $attribute)) { + $attribute['value'] = ArrayHelper::getValue($this->model, $name); + } + } elseif (!isset($attribute['label']) || !array_key_exists('value', $attribute)) { + throw new InvalidConfigException('The attribute configuration requires the "name" element to determine the value and display label.'); + } + + $this->attributes[$i] = $attribute; + } + } +} diff --git a/yii/web/PageCache.php b/framework/yii/widgets/FragmentCache.php similarity index 55% rename from yii/web/PageCache.php rename to framework/yii/widgets/FragmentCache.php index 2fe36b3..3f4027b 100644 --- a/yii/web/PageCache.php +++ b/framework/yii/widgets/FragmentCache.php @@ -1,104 +1,178 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\web; - -use Yii; -use yii\base\ActionFilter; -use yii\base\Action; -use yii\base\View; -use yii\caching\Dependency; - -/** - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class PageCache extends ActionFilter -{ - /** - * @var boolean whether the content being cached should be differentiated according to the route. - * A route consists of the requested controller ID and action ID. Defaults to true. - */ - public $varyByRoute = true; - /** - * @var string the application component ID of the [[\yii\caching\Cache|cache]] object. - */ - public $cache = 'cache'; - /** - * @var integer number of seconds that the data can remain valid in cache. - * Use 0 to indicate that the cached data will never expire. - */ - public $duration = 60; - /** - * @var array|Dependency the dependency that the cached content depends on. - * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. - * For example, - * - * ~~~ - * array( - * 'class' => 'yii\caching\DbDependency', - * 'sql' => 'SELECT MAX(lastModified) FROM Post', - * ) - * ~~~ - * - * would make the output cache depends on the last modified time of all posts. - * If any post has its modification time changed, the cached content would be invalidated. - */ - public $dependency; - /** - * @var array list of factors that would cause the variation of the content being cached. - * Each factor is a string representing a variation (e.g. the language, a GET parameter). - * The following variation setting will cause the content to be cached in different versions - * according to the current application language: - * - * ~~~ - * array( - * Yii::$app->language, - * ) - */ - public $variations; - /** - * @var boolean whether to enable the fragment cache. You may use this property to turn on and off - * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). - */ - public $enabled = true; - - - public function init() - { - parent::init(); - if ($this->view === null) { - $this->view = Yii::$app->getView(); - } - } - - /** - * This method is invoked right before an action is to be executed (after all possible filters.) - * You may override this method to do last-minute preparation for the action. - * @param Action $action the action to be executed. - * @return boolean whether the action should continue to be executed. - */ - public function beforeAction($action) - { - $properties = array(); - foreach (array('cache', 'duration', 'dependency', 'variations', 'enabled') as $name) { - $properties[$name] = $this->$name; - } - $id = $this->varyByRoute ? $action->getUniqueId() : __CLASS__; - return $this->view->beginCache($id, $properties); - } - - /** - * This method is invoked right after an action is executed. - * You may override this method to do some postprocessing for the action. - * @param Action $action the action just executed. - */ - public function afterAction($action) - { - $this->view->endCache(); - } +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use Yii; +use yii\base\Widget; +use yii\caching\Cache; +use yii\caching\Dependency; + +/** + * + * @property string|boolean $cachedContent The cached content. False is returned if valid content is not found + * in the cache. This property is read-only. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class FragmentCache extends Widget +{ + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * After the FragmentCache object is created, if you want to change this property, + * you should only assign it with a cache object. + */ + public $cache = 'cache'; + /** + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + */ + public $duration = 60; + /** + * @var array|Dependency the dependency that the cached content depends on. + * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. + * For example, + * + * ~~~ + * array( + * 'class' => 'yii\caching\DbDependency', + * 'sql' => 'SELECT MAX(lastModified) FROM Post', + * ) + * ~~~ + * + * would make the output cache depends on the last modified time of all posts. + * If any post has its modification time changed, the cached content would be invalidated. + */ + public $dependency; + /** + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * array( + * Yii::$app->language, + * ) + */ + public $variations; + /** + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). + */ + public $enabled = true; + /** + * @var array a list of placeholders for embedding dynamic contents. This property + * is used internally to implement the content caching feature. Do not modify it. + */ + public $dynamicPlaceholders; + + /** + * Initializes the FragmentCache object. + */ + public function init() + { + parent::init(); + + if (!$this->enabled) { + $this->cache = null; + } elseif (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + + if ($this->getCachedContent() === false) { + $this->getView()->cacheStack[] = $this; + ob_start(); + ob_implicit_flush(false); + } + } + + /** + * Marks the end of content to be cached. + * Content displayed before this method call and after {@link init()} + * will be captured and saved in cache. + * This method does nothing if valid content is already found in cache. + */ + public function run() + { + if (($content = $this->getCachedContent()) !== false) { + echo $content; + } elseif ($this->cache instanceof Cache) { + $content = ob_get_clean(); + array_pop($this->getView()->cacheStack); + if (is_array($this->dependency)) { + $this->dependency = Yii::createObject($this->dependency); + } + $data = array($content, $this->dynamicPlaceholders); + $this->cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); + + if (empty($this->getView()->cacheStack) && !empty($this->dynamicPlaceholders)) { + $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); + } + echo $content; + } + } + + /** + * @var string|boolean the cached content. False if the content is not cached. + */ + private $_content; + + /** + * Returns the cached content if available. + * @return string|boolean the cached content. False is returned if valid content is not found in the cache. + */ + public function getCachedContent() + { + if ($this->_content === null) { + $this->_content = false; + if ($this->cache instanceof Cache) { + $key = $this->calculateKey(); + $data = $this->cache->get($key); + if (is_array($data) && count($data) === 2) { + list ($content, $placeholders) = $data; + if (is_array($placeholders) && count($placeholders) > 0) { + if (empty($this->getView()->cacheStack)) { + // outermost cache: replace placeholder with dynamic content + $content = $this->updateDynamicContent($content, $placeholders); + } + foreach ($placeholders as $name => $statements) { + $this->getView()->addDynamicPlaceholder($name, $statements); + } + } + $this->_content = $content; + } + } + } + return $this->_content; + } + + protected function updateDynamicContent($content, $placeholders) + { + foreach ($placeholders as $name => $statements) { + $placeholders[$name] = $this->getView()->evaluateDynamicContent($statements); + } + return strtr($content, $placeholders); + } + + /** + * Generates a unique key used for storing the content in cache. + * The key generated depends on both [[id]] and [[variations]]. + * @return mixed a valid cache key + */ + protected function calculateKey() + { + $factors = array(__CLASS__, $this->getId()); + if (is_array($this->variations)) { + foreach ($this->variations as $factor) { + $factors[] = $factor; + } + } + return $factors; + } } diff --git a/framework/yii/widgets/InputWidget.php b/framework/yii/widgets/InputWidget.php new file mode 100644 index 0000000..e1981c9 --- /dev/null +++ b/framework/yii/widgets/InputWidget.php @@ -0,0 +1,64 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use Yii; +use yii\base\Widget; +use yii\base\Model; +use yii\base\InvalidConfigException; + +/** + * InputWidget is the base class for widgets that collect user inputs. + * + * An input widget can be associated with a data model and an attribute, + * or a name and a value. If the former, the name and the value will + * be generated automatically. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class InputWidget extends Widget +{ + /** + * @var Model the data model that this widget is associated with. + */ + public $model; + /** + * @var string the model attribute that this widget is associated with. + */ + public $attribute; + /** + * @var string the input name. This must be set if [[model]] and [[attribute]] are not set. + */ + public $name; + /** + * @var string the input value. + */ + public $value; + + + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + if (!$this->hasModel() && $this->name === null) { + throw new InvalidConfigException("Either 'name' or 'model' and 'attribute' properties must be specified."); + } + parent::init(); + } + + /** + * @return boolean whether this widget is associated with a data model. + */ + protected function hasModel() + { + return $this->model instanceof Model && $this->attribute !== null; + } +} diff --git a/framework/yii/widgets/LinkPager.php b/framework/yii/widgets/LinkPager.php new file mode 100644 index 0000000..d375f46 --- /dev/null +++ b/framework/yii/widgets/LinkPager.php @@ -0,0 +1,193 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use Yii; +use yii\base\InvalidConfigException; +use yii\helpers\Html; +use yii\base\Widget; +use yii\data\Pagination; + +/** + * LinkPager displays a list of hyperlinks that lead to different pages of target. + * + * LinkPager works with a [[Pagination]] object which specifies the totally number + * of pages and the current page number. + * + * Note that LinkPager only generates the necessary HTML markups. In order for it + * to look like a real pager, you should provide some CSS styles for it. + * With the default configuration, LinkPager should look good using Twitter Bootstrap CSS framework. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class LinkPager extends Widget +{ + /** + * @var Pagination the pagination object that this pager is associated with. + * You must set this property in order to make LinkPager work. + */ + public $pagination; + /** + * @var array HTML attributes for the pager container tag. + */ + public $options = array('class' => 'pagination'); + /** + * @var string the CSS class for the "first" page button. + */ + public $firstPageCssClass = 'first'; + /** + * @var string the CSS class for the "last" page button. + */ + public $lastPageCssClass = 'last'; + /** + * @var string the CSS class for the "previous" page button. + */ + public $prevPageCssClass = 'prev'; + /** + * @var string the CSS class for the "next" page button. + */ + public $nextPageCssClass = 'next'; + /** + * @var string the CSS class for the active (currently selected) page button. + */ + public $activePageCssClass = 'active'; + /** + * @var string the CSS class for the disabled page buttons. + */ + public $disabledPageCssClass = 'disabled'; + /** + * @var integer maximum number of page buttons that can be displayed. Defaults to 10. + */ + public $maxButtonCount = 10; + /** + * @var string the label for the "next" page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "next" page button will not be displayed. + */ + public $nextPageLabel = '»'; + /** + * @var string the text label for the previous page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "previous" page button will not be displayed. + */ + public $prevPageLabel = '«'; + /** + * @var string the text label for the "first" page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "first" page button will not be displayed. + */ + public $firstPageLabel; + /** + * @var string the text label for the "last" page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "last" page button will not be displayed. + */ + public $lastPageLabel; + + + /** + * Initializes the pager. + */ + public function init() + { + if ($this->pagination === null) { + throw new InvalidConfigException('The "pagination" property must be set.'); + } + } + + /** + * Executes the widget. + * This overrides the parent implementation by displaying the generated page buttons. + */ + public function run() + { + echo $this->renderPageButtons(); + } + + /** + * Renders the page buttons. + * @return string the rendering result + */ + protected function renderPageButtons() + { + $buttons = array(); + + $pageCount = $this->pagination->getPageCount(); + $currentPage = $this->pagination->getPage(); + + // first page + if ($this->firstPageLabel !== null) { + $buttons[] = $this->renderPageButton($this->firstPageLabel, 0, $this->firstPageCssClass, $currentPage <= 0, false); + } + + // prev page + if ($this->prevPageLabel !== null) { + if (($page = $currentPage - 1) < 0) { + $page = 0; + } + $buttons[] = $this->renderPageButton($this->prevPageLabel, $page, $this->prevPageCssClass, $currentPage <= 0, false); + } + + // internal pages + list($beginPage, $endPage) = $this->getPageRange(); + for ($i = $beginPage; $i <= $endPage; ++$i) { + $buttons[] = $this->renderPageButton($i + 1, $i, null, false, $i == $currentPage); + } + + // next page + if ($this->nextPageLabel !== null) { + if (($page = $currentPage + 1) >= $pageCount - 1) { + $page = $pageCount - 1; + } + $buttons[] = $this->renderPageButton($this->nextPageLabel, $page, $this->nextPageCssClass, $currentPage >= $pageCount - 1, false); + } + + // last page + if ($this->lastPageLabel !== null) { + $buttons[] = $this->renderPageButton($this->lastPageLabel, $pageCount - 1, $this->lastPageCssClass, $currentPage >= $pageCount - 1, false); + } + + return Html::tag('ul', implode("\n", $buttons), $this->options); + } + + /** + * Renders a page button. + * You may override this method to customize the generation of page buttons. + * @param string $label the text label for the button + * @param integer $page the page number + * @param string $class the CSS class for the page button. + * @param boolean $disabled whether this page button is disabled + * @param boolean $active whether this page button is active + * @return string the rendering result + */ + protected function renderPageButton($label, $page, $class, $disabled, $active) + { + if ($active) { + $class .= ' ' . $this->activePageCssClass; + } + if ($disabled) { + $class .= ' ' . $this->disabledPageCssClass; + } + $class = trim($class); + $options = array('class' => $class === '' ? null : $class); + return Html::tag('li', Html::a($label, $this->pagination->createUrl($page), array('data-page' => $page)), $options); + } + + /** + * @return array the begin and end pages that need to be displayed. + */ + protected function getPageRange() + { + $currentPage = $this->pagination->getPage(); + $pageCount = $this->pagination->getPageCount(); + + $beginPage = max(0, $currentPage - (int)($this->maxButtonCount / 2)); + if (($endPage = $beginPage + $this->maxButtonCount - 1) >= $pageCount) { + $endPage = $pageCount - 1; + $beginPage = max(0, $endPage - $this->maxButtonCount + 1); + } + return array($beginPage, $endPage); + } +} diff --git a/framework/yii/widgets/LinkSorter.php b/framework/yii/widgets/LinkSorter.php new file mode 100644 index 0000000..c8b30e5 --- /dev/null +++ b/framework/yii/widgets/LinkSorter.php @@ -0,0 +1,73 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use Yii; +use yii\base\InvalidConfigException; +use yii\base\Widget; +use yii\data\Sort; +use yii\helpers\Html; + +/** + * LinkSorter renders a list of sort links for the given sort definition. + * + * LinkSorter will generate a hyperlink for every attribute declared in [[sort]]. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class LinkSorter extends Widget +{ + /** + * @var Sort the sort definition + */ + public $sort; + /** + * @var array list of the attributes that support sorting. If not set, it will be determined + * using [[Sort::attributes]]. + */ + public $attributes; + /** + * @var array HTML attributes for the sorter container tag. + */ + public $options = array('class' => 'sorter'); + + + /** + * Initializes the sorter. + */ + public function init() + { + if ($this->sort === null) { + throw new InvalidConfigException('The "sort" property must be set.'); + } + } + + /** + * Executes the widget. + * This method renders the sort links. + */ + public function run() + { + echo $this->renderSortLinks(); + } + + /** + * Renders the sort links. + * @return string the rendering result + */ + protected function renderSortLinks() + { + $attributes = empty($this->atttributes) ? array_keys($this->sort->attributes) : $this->attributes; + $links = array(); + foreach ($attributes as $name) { + $links[] = $this->sort->link($name); + } + return Html::ul($links, array('encode' => false)); + } +} diff --git a/framework/yii/widgets/ListView.php b/framework/yii/widgets/ListView.php new file mode 100644 index 0000000..1d8745d --- /dev/null +++ b/framework/yii/widgets/ListView.php @@ -0,0 +1,97 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use Yii; +use yii\helpers\ArrayHelper; +use yii\helpers\Html; + +/** + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ListView extends BaseListView +{ + /** + * @var array the HTML attributes for the container of the rendering result of each data model. + * The "tag" element specifies the tag name of the container element and defaults to "div". + * If "tag" is false, it means no container element will be rendered. + */ + public $itemOptions = array(); + /** + * @var string|callback the name of the view for rendering each data item, or a callback (e.g. an anonymous function) + * for rendering each data item. If it specifies a view name, the following variables will + * be available in the view: + * + * - `$model`: mixed, the data model + * - `$key`: mixed, the key value associated with the data item + * - `$index`: integer, the zero-based index of the data item in the items array returned by [[dataProvider]]. + * - `$widget`: ListView, this widget instance + * + * Note that the view name is resolved into the view file by the current context of the [[view]] object. + * + * If this property is specified as a callback, it should have the following signature: + * + * ~~~ + * function ($model, $key, $index, $widget) + * ~~~ + */ + public $itemView; + /** + * @var string the HTML code to be displayed between any two consecutive items. + */ + public $separator = "\n"; + + + /** + * Renders all data models. + * @return string the rendering result + */ + public function renderItems() + { + $models = $this->dataProvider->getModels(); + $keys = $this->dataProvider->getKeys(); + $rows = array(); + foreach (array_values($models) as $index => $model) { + $rows[] = $this->renderItem($model, $keys[$index], $index); + } + return implode($this->separator, $rows); + } + + /** + * Renders a single data model. + * @param mixed $model the data model to be rendered + * @param mixed $key the key value associated with the data model + * @param integer $index the zero-based index of the data model in the model array returned by [[dataProvider]]. + * @return string the rendering result + */ + public function renderItem($model, $key, $index) + { + if ($this->itemView === null) { + $content = $key; + } elseif (is_string($this->itemView)) { + $content = $this->getView()->render($this->itemView, array( + 'model' => $model, + 'key' => $key, + 'index' => $index, + 'widget' => $this, + )); + } else { + $content = call_user_func($this->itemView, $model, $key, $index, $this); + } + $options = $this->itemOptions; + $tag = ArrayHelper::remove($options, 'tag', 'div'); + if ($tag !== false) { + $options['data-key'] = $key; + return Html::tag($tag, $content, $options); + } else { + return $content; + } + } +} diff --git a/framework/yii/widgets/MaskedInput.php b/framework/yii/widgets/MaskedInput.php new file mode 100644 index 0000000..8b8cbc6 --- /dev/null +++ b/framework/yii/widgets/MaskedInput.php @@ -0,0 +1,137 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use yii\base\InvalidConfigException; +use yii\helpers\Html; +use yii\helpers\Json; +use yii\web\JsExpression; + +/** + * MaskedInput generates a masked text input. + * + * MaskedInput is similar to [[Html::textInput()]] except that + * an input mask will be used to force users to enter properly formatted data, + * such as phone numbers, social security numbers. + * + * To use MaskedInput, you must set the [[mask]] property. The following example + * shows how to use MaskedInput to collect phone numbers: + * + * ~~~ + * echo MaskedInput::widget(array( + * 'name' => 'phone', + * 'mask' => '999-999-9999', + * )); + * ~~~ + * + * The masked text field is implemented based on the [jQuery masked input plugin](http://digitalbush.com/projects/masked-input-plugin). + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class MaskedInput extends InputWidget +{ + /** + * @var string the input mask (e.g. '99/99/9999' for date input). The following characters are predefined: + * + * - `a`: represents an alpha character (A-Z,a-z) + * - `9`: represents a numeric character (0-9) + * - `*`: represents an alphanumeric character (A-Z,a-z,0-9) + * - `?`: anything listed after '?' within the mask is considered optional user input + * + * Additional characters can be defined by specifying the [[charMap]] property. + */ + public $mask; + /** + * @var array the mapping between mask characters and the corresponding patterns. + * For example, `array('~' => '[+-]')` specifies that the '~' character expects '+' or '-' input. + * Defaults to null, meaning using the map as described in [[mask]]. + */ + public $charMap; + /** + * @var string the character prompting for user input. Defaults to underscore '_'. + */ + public $placeholder; + /** + * @var string a JavaScript function callback that will be invoked when user finishes the input. + */ + public $completed; + /** + * @var array the HTML attributes for the input tag. + */ + public $options = array(); + + + /** + * Initializes the widget. + * @throws InvalidConfigException if the "mask" property is not set. + */ + public function init() + { + parent::init(); + if (empty($this->mask)) { + throw new InvalidConfigException('The "mask" property must be set.'); + } + + if (!isset($this->options['id'])) { + $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId(); + } + } + + /** + * Runs the widget. + */ + public function run() + { + if ($this->hasModel()) { + echo Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + echo Html::textInput($this->name, $this->value, $this->options); + } + $this->registerClientScript(); + } + + /** + * Registers the needed JavaScript. + */ + public function registerClientScript() + { + $options = $this->getClientOptions(); + $options = empty($options) ? '' : ',' . Json::encode($options); + $js = ''; + if (is_array($this->charMap) && !empty($this->charMap)) { + $js .= 'jQuery.mask.definitions=' . Json::encode($this->charMap) . ";\n"; + } + $id = $this->options['id']; + $js .= "jQuery(\"#{$id}\").mask(\"{$this->mask}\"{$options});"; + $view = $this->getView(); + MaskedInputAsset::register($view); + $view->registerJs($js); + } + + /** + * @return array the options for the text field + */ + protected function getClientOptions() + { + $options = array(); + if ($this->placeholder !== null) { + $options['placeholder'] = $this->placeholder; + } + + if ($this->completed !== null) { + if ($this->completed instanceof JsExpression) { + $options['completed'] = $this->completed; + } else { + $options['completed'] = new JsExpression($this->completed); + } + } + + return $options; + } +} diff --git a/framework/yii/widgets/MaskedInputAsset.php b/framework/yii/widgets/MaskedInputAsset.php new file mode 100644 index 0000000..d091505 --- /dev/null +++ b/framework/yii/widgets/MaskedInputAsset.php @@ -0,0 +1,24 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; +use yii\web\AssetBundle; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class MaskedInputAsset extends AssetBundle +{ + public $sourcePath = '@yii/assets'; + public $js = array( + 'jquery.maskedinput.js', + ); + public $depends = array( + 'yii\web\YiiAsset', + ); +} diff --git a/yii/widgets/Menu.php b/framework/yii/widgets/Menu.php similarity index 81% rename from yii/widgets/Menu.php rename to framework/yii/widgets/Menu.php index b8f69e1..8bf845e 100644 --- a/yii/widgets/Menu.php +++ b/framework/yii/widgets/Menu.php @@ -9,6 +9,7 @@ namespace yii\widgets; use Yii; use yii\base\Widget; +use yii\helpers\ArrayHelper; use yii\helpers\Html; /** @@ -16,31 +17,31 @@ use yii\helpers\Html; * * The main property of Menu is [[items]], which specifies the possible items in the menu. * A menu item can contain sub-items which specify the sub-menu under that menu item. - * + * * Menu checks the current route and request parameters to toggle certain menu items * with active state. - * + * * Note that Menu only renders the HTML tags about the menu. It does do any styling. * You are responsible to provide CSS styles to make it look like a real menu. * * The following example shows how to use Menu: - * + * * ~~~ - * $this->widget('yii\widgets\Menu', array( + * echo Menu::widget(array( * 'items' => array( * // Important: you need to specify url as 'controller/action', - * // not just as 'controller' even if default acion is used. + * // not just as 'controller' even if default action is used. * array('label' => 'Home', 'url' => array('site/index')), * // 'Products' menu item will be selected as long as the route is 'product/index' * array('label' => 'Products', 'url' => array('product/index'), 'items' => array( * array('label' => 'New Arrivals', 'url' => array('product/index', 'tag' => 'new')), * array('label' => 'Most Popular', 'url' => array('product/index', 'tag' => 'popular')), * )), - * array('label' => 'Login', 'url' => array('site/login'), 'visible' => Yii::app()->user->isGuest), + * array('label' => 'Login', 'url' => array('site/login'), 'visible' => Yii::$app->user->isGuest), * ), * )); * ~~~ - * + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -63,10 +64,18 @@ class Menu extends Widget * - template: string, optional, the template used to render the content of this menu item. * The token `{url}` will be replaced by the URL associated with this menu item, * and the token `{label}` will be replaced by the label of the menu item. - * If this option is not set, [[linkTemplate]] or [[labelTemplate]] will be used instead. + * If this option is not set, [[linkTemplate]] or [[labelTemplate]] will be used instead. + * - options: array, optional, the HTML attributes for the menu container tag. */ public $items = array(); /** + * @var array list of HTML attributes for the menu container tag. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "li", the tag name of the item container tags. + */ + public $itemOptions = array(); + /** * @var string the template used to render the body of a menu which is a link. * In this template, the token `{url}` will be replaced with the corresponding link URL; * while `{label}` will be replaced with the link text. @@ -109,7 +118,9 @@ class Menu extends Widget */ public $hideEmptyItems = true; /** - * @var array the HTML attributes for the menu's container tag. + * @var array the HTML attributes for the menu's container tag. The following special options are recognized: + * + * - tag: string, defaults to "ul", the tag name of the item container tags. */ public $options = array(); /** @@ -124,7 +135,7 @@ class Menu extends Widget public $lastItemCssClass; /** * @var string the route used to determine if a menu item is active or not. - * If not set, it will use the route of the current request. + * If not set, it will use the route of the current request. * @see params * @see isItemActive */ @@ -150,7 +161,9 @@ class Menu extends Widget $this->params = $_GET; } $items = $this->normalizeItems($this->items, $hasActiveChild); - echo Html::tag('ul', $this->renderItems($items), $this->options); + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'ul'); + echo Html::tag($tag, $this->renderItems($items), $options); } /** @@ -163,7 +176,8 @@ class Menu extends Widget $n = count($items); $lines = array(); foreach ($items as $i => $item) { - $options = isset($item['itemOptions']) ? $item['itemOptions'] : array(); + $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', array())); + $tag = ArrayHelper::remove($options, 'tag', 'li'); $class = array(); if ($item['active']) { $class[] = $this->activeCssClass; @@ -188,7 +202,7 @@ class Menu extends Widget '{items}' => $this->renderItems($item['items']), )); } - $lines[] = Html::tag('li', $menu, $options); + $lines[] = Html::tag($tag, $menu, $options); } return implode("\n", $lines); } @@ -202,13 +216,13 @@ class Menu extends Widget protected function renderItem($item) { if (isset($item['url'])) { - $template = isset($item['template']) ? $item['template'] : $this->linkTemplate; + $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate); return strtr($template, array( '{url}' => Html::url($item['url']), '{label}' => $item['label'], )); } else { - $template = isset($item['template']) ? $item['template'] : $this->labelTemplate; + $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate); return strtr($template, array( '{label}' => $item['label'], )); @@ -236,7 +250,7 @@ class Menu extends Widget } $hasActiveChild = false; if (isset($item['items'])) { - $items[$i]['items'] = $this->normalizeItems($item['items'], $route, $hasActiveChild); + $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild); if (empty($items[$i]['items']) && $this->hideEmptyItems) { unset($items[$i]['items']); if (!isset($item['url'])) { @@ -270,7 +284,14 @@ class Menu extends Widget */ protected function isItemActive($item) { - if (isset($item['url']) && is_array($item['url']) && trim($item['url'][0], '/') === $this->route) { + if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) { + $route = $item['url'][0]; + if ($route[0] !== '/' && Yii::$app->controller) { + $route = Yii::$app->controller->module->getUniqueId() . '/' . $route; + } + if (ltrim($route, '/') !== $this->route) { + return false; + } unset($item['url']['#']); if (count($item['url']) > 1) { foreach (array_splice($item['url'], 1) as $name => $value) { @@ -283,5 +304,4 @@ class Menu extends Widget } return false; } - } diff --git a/framework/yii/widgets/Spaceless.php b/framework/yii/widgets/Spaceless.php new file mode 100644 index 0000000..8115f85 --- /dev/null +++ b/framework/yii/widgets/Spaceless.php @@ -0,0 +1,69 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use yii\base\Widget; + +/** + * Spaceless widget removes whitespace characters between HTML tags. Whitespaces within HTML tags + * or in a plain text are always left untouched. + * + * Usage example: + * + * ```php + * <body> + * <?php Spaceless::begin(); ?> + * <div class="nav-bar"> + * <!-- tags --> + * </div> + * <div class="content"> + * <!-- tags --> + * </div> + * <?php Spaceless::end(); ?> + * </body> + * ``` + * + * This example will generate the following HTML: + * + * ```html + * <body> + * <div class="navbar"><!-- other tags --></div><div class="content"><!-- other tags --></div></body> + * ``` + * + * This method is not designed for content compression (you should use `gzip` output compression to + * achieve it). Main intention is to strip out extra whitespace characters between HTML tags in order + * to avoid browser rendering quirks in some circumstances (e.g. newlines between inline-block elements). + * + * Note, never use this method with `pre` or `textarea` tags. It's not that trivial to deal with such tags + * as it may seem at first sight. For this case you should consider using + * [HTML Tidy Project](http://tidy.sourceforge.net/) instead. + * + * @see http://tidy.sourceforge.net/ + * @author resurtm <resurtm@gmail.com> + * @since 2.0 + */ +class Spaceless extends Widget +{ + /** + * Starts capturing an output to be cleaned from whitespace characters between HTML tags. + */ + public function init() + { + ob_start(); + ob_implicit_flush(false); + } + + /** + * Marks the end of content to be cleaned from whitespace characters between HTML tags. + * Stops capturing an output and echoes cleaned result. + */ + public function run() + { + echo trim(preg_replace('/>\s+</', '><', ob_get_clean())); + } +} diff --git a/yii/yiic.php b/framework/yii/yii similarity index 84% rename from yii/yiic.php rename to framework/yii/yii old mode 100644 new mode 100755 index 7cd0c40..ebba1a6 --- a/yii/yiic.php +++ b/framework/yii/yii @@ -1,3 +1,4 @@ +#!/usr/bin/env php <?php /** * Yii console bootstrap file. @@ -15,8 +16,9 @@ defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); require(__DIR__ . '/Yii.php'); $application = new yii\console\Application(array( - 'id' => 'yiic', + 'id' => 'yii-console', 'basePath' => __DIR__ . '/console', 'controllerPath' => '@yii/console/controllers', )); -$application->run(); +$exitCode = $application->run(); +exit($exitCode); diff --git a/framework/yii/yii.bat b/framework/yii/yii.bat new file mode 100644 index 0000000..5e21e2e --- /dev/null +++ b/framework/yii/yii.bat @@ -0,0 +1,20 @@ +@echo off + +rem ------------------------------------------------------------- +rem Yii command line bootstrap script for Windows. +rem +rem @author Qiang Xue <qiang.xue@gmail.com> +rem @link http://www.yiiframework.com/ +rem @copyright Copyright © 2012 Yii Software LLC +rem @license http://www.yiiframework.com/license/ +rem ------------------------------------------------------------- + +@setlocal + +set YII_PATH=%~dp0 + +if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe + +"%PHP_COMMAND%" "%YII_PATH%yii" %* + +@endlocal diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bf37a26..1f3056e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,4 +10,21 @@ <directory>./tests/unit</directory> </testsuite> </testsuites> + <filter> + <blacklist> + <file>framework/yii/helpers/Json.php</file> + <file>framework/yii/helpers/StringHelper.php</file> + <file>framework/yii/helpers/VarDumper.php</file> + <file>framework/yii/helpers/Html.php</file> + <file>framework/yii/helpers/Inflector.php</file> + <file>framework/yii/helpers/FileHelper.php</file> + <file>framework/yii/helpers/ArrayHelper.php</file> + <file>framework/yii/helpers/Console.php</file> + <file>framework/yii/i18n/GettextFile.php</file> + <file>framework/yii/web/ResponseFormatterInterface.php</file> + <directory suffix="Exception.php">framework/yii/base</directory> + <directory suffix=".php">framework/yii/db/mssql</directory> + <directory suffix=".php">framework/yii/bootstrap</directory> + </blacklist> + </filter> </phpunit> \ No newline at end of file diff --git a/tests/unit/MysqlTestCase.php b/tests/unit/MysqlTestCase.php deleted file mode 100644 index c7ef970..0000000 --- a/tests/unit/MysqlTestCase.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php - -namespace yiiunit; - -class MysqlTestCase extends TestCase -{ - protected function setUp() - { - if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) { - $this->markTestSkipped('pdo and pdo_mysql extensions are required.'); - } - } - - /** - * @param bool $reset whether to clean up the test database - * @return \yii\db\Connection - */ - public function getConnection($reset = true) - { - $params = $this->getParam('mysql'); - $db = new \yii\db\Connection; - $db->dsn = $params['dsn']; - $db->username = $params['username']; - $db->password = $params['password']; - if ($reset) { - $db->open(); - $lines = explode(';', file_get_contents($params['fixture'])); - foreach ($lines as $line) { - if (trim($line) !== '') { - $db->pdo->exec($line); - } - } - } - return $db; - } -} diff --git a/tests/unit/TestCase.php b/tests/unit/TestCase.php index dccd3af..bd95465 100644 --- a/tests/unit/TestCase.php +++ b/tests/unit/TestCase.php @@ -2,15 +2,58 @@ namespace yiiunit; -class TestCase extends \yii\test\TestCase +/** + * This is the base class for all yii framework unit tests. + */ +abstract class TestCase extends \yii\test\TestCase { public static $params; - public function getParam($name) + /** + * Clean up after test. + * By default the application created with [[mockApplication]] will be destroyed. + */ + protected function tearDown() + { + parent::tearDown(); + $this->destroyApplication(); + } + + /** + * Returns a test configuration param from /data/config.php + * @param string $name params name + * @param mixed $default default value to use when param is not set. + * @return mixed the value of the configuration param + */ + public function getParam($name, $default = null) { if (self::$params === null) { self::$params = require(__DIR__ . '/data/config.php'); } - return isset(self::$params[$name]) ? self::$params[$name] : null; + return isset(self::$params[$name]) ? self::$params[$name] : $default; + } + + /** + * Populates Yii::$app with a new application + * The application will be destroyed on tearDown() automatically. + * @param array $config The application configuration, if needed + * @param string $appClass name of the application class to create + */ + protected function mockApplication($config = array(), $appClass = '\yii\console\Application') + { + static $defaultConfig = array( + 'id' => 'testapp', + 'basePath' => __DIR__, + ); + + new $appClass(array_merge($defaultConfig, $config)); + } + + /** + * Destroys application in Yii::$app by setting it to null. + */ + protected function destroyApplication() + { + \Yii::$app = null; } } diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 285b55b..0580db6 100644 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -5,10 +5,8 @@ define('YII_DEBUG', true); $_SERVER['SCRIPT_NAME'] = '/' . __DIR__; $_SERVER['SCRIPT_FILENAME'] = __FILE__; -require_once(__DIR__ . '/../../yii/Yii.php'); +require_once(__DIR__ . '/../../framework/yii/Yii.php'); Yii::setAlias('@yiiunit', __DIR__); -new \yii\web\Application(array('id' => 'testapp', 'basePath' => __DIR__)); - require_once(__DIR__ . '/TestCase.php'); diff --git a/tests/unit/data/ar/Customer.php b/tests/unit/data/ar/Customer.php index b26b51b..d4702b1 100644 --- a/tests/unit/data/ar/Customer.php +++ b/tests/unit/data/ar/Customer.php @@ -1,8 +1,15 @@ <?php - namespace yiiunit\data\ar; -use yii\db\ActiveQuery; +/** + * Class Customer + * + * @property integer $id + * @property string $name + * @property string $email + * @property string $address + * @property integer $status + */ class Customer extends ActiveRecord { const STATUS_ACTIVE = 1; diff --git a/tests/unit/data/ar/Item.php b/tests/unit/data/ar/Item.php index 5d23378..e725be9 100644 --- a/tests/unit/data/ar/Item.php +++ b/tests/unit/data/ar/Item.php @@ -2,6 +2,13 @@ namespace yiiunit\data\ar; +/** + * Class Item + * + * @property integer $id + * @property string $name + * @property integer $category_id + */ class Item extends ActiveRecord { public static function tableName() diff --git a/tests/unit/data/ar/Order.php b/tests/unit/data/ar/Order.php index f9dd715..063bb67 100644 --- a/tests/unit/data/ar/Order.php +++ b/tests/unit/data/ar/Order.php @@ -2,6 +2,14 @@ namespace yiiunit\data\ar; +/** + * Class Order + * + * @property integer $id + * @property integer $customer_id + * @property integer $create_time + * @property string $total + */ class Order extends ActiveRecord { public static function tableName() @@ -22,7 +30,7 @@ class Order extends ActiveRecord public function getItems() { return $this->hasMany('Item', array('id' => 'item_id')) - ->via('orderItems', function($q) { + ->via('orderItems', function ($q) { // additional query configuration })->orderBy('id'); } diff --git a/tests/unit/data/ar/OrderItem.php b/tests/unit/data/ar/OrderItem.php index 607133e..297432b 100644 --- a/tests/unit/data/ar/OrderItem.php +++ b/tests/unit/data/ar/OrderItem.php @@ -2,6 +2,14 @@ namespace yiiunit\data\ar; +/** + * Class OrderItem + * + * @property integer $order_id + * @property integer $item_id + * @property integer $quantity + * @property string $subtotal + */ class OrderItem extends ActiveRecord { public static function tableName() diff --git a/tests/unit/data/base/InvalidRulesModel.php b/tests/unit/data/base/InvalidRulesModel.php index f5a8438..13f428d 100644 --- a/tests/unit/data/base/InvalidRulesModel.php +++ b/tests/unit/data/base/InvalidRulesModel.php @@ -1,5 +1,6 @@ <?php namespace yiiunit\data\base; + use yii\base\Model; /** @@ -13,5 +14,4 @@ class InvalidRulesModel extends Model array('test'), ); } - } diff --git a/tests/unit/data/base/Singer.php b/tests/unit/data/base/Singer.php index f1b91e1..96c4850 100644 --- a/tests/unit/data/base/Singer.php +++ b/tests/unit/data/base/Singer.php @@ -1,5 +1,6 @@ <?php namespace yiiunit\data\base; + use yii\base\Model; /** @@ -15,7 +16,7 @@ class Singer extends Model return array( array('lastName', 'default', 'value' => 'Lennon'), array('lastName', 'required'), - array('underscore_style', 'yii\validators\CaptchaValidator'), + array('underscore_style', 'yii\captcha\CaptchaValidator'), ); } } diff --git a/tests/unit/data/base/Speaker.php b/tests/unit/data/base/Speaker.php index 93dd496..b0acc6b 100644 --- a/tests/unit/data/base/Speaker.php +++ b/tests/unit/data/base/Speaker.php @@ -1,5 +1,6 @@ <?php namespace yiiunit\data\base; + use yii\base\Model; /** @@ -16,6 +17,13 @@ class Speaker extends Model protected $protectedProperty; private $_privateProperty; + public static $formName = 'Speaker'; + + public function formName() + { + return static::$formName; + } + public function attributeLabels() { return array( diff --git a/tests/unit/data/config.php b/tests/unit/data/config.php index c980c57..fda2be1 100644 --- a/tests/unit/data/config.php +++ b/tests/unit/data/config.php @@ -1,16 +1,33 @@ <?php - return array( - 'databases' => array( - 'mysql' => array( - 'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest', - 'username' => 'travis', - 'password' => '', - 'fixture' => __DIR__ . '/mysql.sql', - ), - 'sqlite' => array( - 'dsn' => 'sqlite::memory:', - 'fixture' => __DIR__ . '/sqlite.sql', - ), - ) + 'databases' => array( + 'cubrid' => array( + 'dsn' => 'cubrid:dbname=demodb;host=localhost;port=33000', + 'username' => 'dba', + 'password' => '', + 'fixture' => __DIR__ . '/cubrid.sql', + ), + 'mysql' => array( + 'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest', + 'username' => 'travis', + 'password' => '', + 'fixture' => __DIR__ . '/mysql.sql', + ), + 'sqlite' => array( + 'dsn' => 'sqlite::memory:', + 'fixture' => __DIR__ . '/sqlite.sql', + ), + 'sqlsrv' => array( + 'dsn' => 'sqlsrv:Server=localhost;Database=test', + 'username' => '', + 'password' => '', + 'fixture' => __DIR__ . '/mssql.sql', + ), + 'pgsql' => array( + 'dsn' => 'pgsql:host=localhost;dbname=yiitest;port=5432;', + 'username' => 'postgres', + 'password' => 'postgres', + 'fixture' => __DIR__ . '/postgres.sql', + ), + ), ); diff --git a/tests/unit/data/cubrid.sql b/tests/unit/data/cubrid.sql new file mode 100644 index 0000000..3dcfa37 --- /dev/null +++ b/tests/unit/data/cubrid.sql @@ -0,0 +1,108 @@ +/** + * This is the database schema for testing CUBRID support of Yii DAO and Active Record. + * The database setup in config.php is required to perform then relevant tests: + */ + +DROP TABLE IF EXISTS tbl_composite_fk; +DROP TABLE IF EXISTS tbl_order_item; +DROP TABLE IF EXISTS tbl_item; +DROP TABLE IF EXISTS tbl_order; +DROP TABLE IF EXISTS tbl_category; +DROP TABLE IF EXISTS tbl_customer; +DROP TABLE IF EXISTS tbl_type; +DROP TABLE IF EXISTS tbl_constraints; + +CREATE TABLE `tbl_constraints` +( + `id` integer not null, + `field1` varchar(255) +); + + +CREATE TABLE `tbl_customer` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `email` varchar(128) NOT NULL, + `name` varchar(128) NOT NULL, + `address` string, + `status` int (11) DEFAULT 0, + PRIMARY KEY (`id`) +); + +CREATE TABLE `tbl_category` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `tbl_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL, + `category_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_item_category_id` FOREIGN KEY (`category_id`) REFERENCES `tbl_category` (`id`) ON DELETE CASCADE +); + +CREATE TABLE `tbl_order` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `customer_id` int(11) NOT NULL, + `create_time` int(11) NOT NULL, + `total` decimal(10,0) NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_order_customer_id` FOREIGN KEY (`customer_id`) REFERENCES `tbl_customer` (`id`) ON DELETE CASCADE +); + +CREATE TABLE `tbl_order_item` ( + `order_id` int(11) NOT NULL, + `item_id` int(11) NOT NULL, + `quantity` int(11) NOT NULL, + `subtotal` decimal(10,0) NOT NULL, + PRIMARY KEY (`order_id`,`item_id`), + CONSTRAINT `FK_order_item_order_id` FOREIGN KEY (`order_id`) REFERENCES `tbl_order` (`id`) ON DELETE CASCADE, + CONSTRAINT `FK_order_item_item_id` FOREIGN KEY (`item_id`) REFERENCES `tbl_item` (`id`) ON DELETE CASCADE +); + +CREATE TABLE `tbl_type` ( + `int_col` int(11) NOT NULL, + `int_col2` int(11) DEFAULT '1', + `char_col` char(100) NOT NULL, + `char_col2` varchar(100) DEFAULT 'something', + `char_col3` string, + `enum_col` enum('a', 'b'), + `float_col` double NOT NULL, + `float_col2` double DEFAULT '1.23', + `blob_col` blob, + `numeric_col` decimal(5,2) DEFAULT '33.22', + `time` timestamp NOT NULL DEFAULT '2002-01-01 00:00:00' +); + +CREATE TABLE `tbl_composite_fk` ( + `id` int(11) NOT NULL, + `order_id` int(11) NOT NULL, + `item_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_composite_fk_order_item` FOREIGN KEY (`order_id`,`item_id`) REFERENCES `tbl_order_item` (`order_id`,`item_id`) ON DELETE CASCADE +); + +INSERT INTO tbl_customer (email, name, address, status) VALUES ('user1@example.com', 'user1', 'address1', 1); +INSERT INTO tbl_customer (email, name, address, status) VALUES ('user2@example.com', 'user2', 'address2', 1); +INSERT INTO tbl_customer (email, name, address, status) VALUES ('user3@example.com', 'user3', 'address3', 2); + +INSERT INTO tbl_category (name) VALUES ('Books'); +INSERT INTO tbl_category (name) VALUES ('Movies'); + +INSERT INTO tbl_item (name, category_id) VALUES ('Agile Web Application Development with Yii1.1 and PHP5', 1); +INSERT INTO tbl_item (name, category_id) VALUES ('Yii 1.1 Application Development Cookbook', 1); +INSERT INTO tbl_item (name, category_id) VALUES ('Ice Age', 2); +INSERT INTO tbl_item (name, category_id) VALUES ('Toy Story', 2); +INSERT INTO tbl_item (name, category_id) VALUES ('Cars', 2); + +INSERT INTO tbl_order (customer_id, create_time, total) VALUES (1, 1325282384, 110.0); +INSERT INTO tbl_order (customer_id, create_time, total) VALUES (2, 1325334482, 33.0); +INSERT INTO tbl_order (customer_id, create_time, total) VALUES (2, 1325502201, 40.0); + +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (1, 1, 1, 30.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (1, 2, 2, 40.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 4, 1, 10.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 5, 1, 15.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 3, 1, 8.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (3, 2, 1, 40.0); diff --git a/tests/unit/data/i18n/messages/de_DE/test.php b/tests/unit/data/i18n/messages/de_DE/test.php new file mode 100644 index 0000000..9cb1369 --- /dev/null +++ b/tests/unit/data/i18n/messages/de_DE/test.php @@ -0,0 +1,9 @@ +<?php +/** + * + */ +return array( + 'The dog runs fast.' => 'Der Hund rennt schnell.', + 'His speed is about {n} km/h.' => 'Seine Geschwindigkeit beträgt {n} km/h.', + 'His name is {name} and his speed is about {n, number} km/h.' => 'Er heißt {name} und ist {n, number} km/h schnell.', +); \ No newline at end of file diff --git a/tests/unit/data/i18n/messages/en_US/test.php b/tests/unit/data/i18n/messages/en_US/test.php new file mode 100644 index 0000000..dcd3bc9 --- /dev/null +++ b/tests/unit/data/i18n/messages/en_US/test.php @@ -0,0 +1,7 @@ +<?php +/** + * + */ +return array( + 'The dog runs fast.' => 'Der Hund rennt schell.', +); \ No newline at end of file diff --git a/tests/unit/data/mssql.sql b/tests/unit/data/mssql.sql index 38967b2..959c6c6 100644 --- a/tests/unit/data/mssql.sql +++ b/tests/unit/data/mssql.sql @@ -1,306 +1,94 @@ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[categories]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[categories]( +IF OBJECT_ID('[dbo].[tbl_order_item]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_order_item]; +IF OBJECT_ID('[dbo].[tbl_item]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_item]; +IF OBJECT_ID('[dbo].[tbl_order]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_order]; +IF OBJECT_ID('[dbo].[tbl_category]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_category]; +IF OBJECT_ID('[dbo].[tbl_customer]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_customer]; +IF OBJECT_ID('[dbo].[tbl_type]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_type]; + +CREATE TABLE [dbo].[tbl_customer] ( [id] [int] IDENTITY(1,1) NOT NULL, + [email] [varchar](128) NOT NULL, [name] [varchar](128) NOT NULL, - [parent_id] [int] NULL, - CONSTRAINT [PK_categories] PRIMARY KEY CLUSTERED -( - [id] ASC -) ON [PRIMARY] -) ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[orders]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[orders]( - [key1] [int] NOT NULL, - [key2] [int] NOT NULL, + [address] [text], + [status] [int] DEFAULT 0, + CONSTRAINT [PK_customer] PRIMARY KEY CLUSTERED ( + [id] ASC + ) ON [PRIMARY] +); + +CREATE TABLE [dbo].[tbl_category] ( + [id] [int] IDENTITY(1,1) NOT NULL, [name] [varchar](128) NOT NULL, - CONSTRAINT [PK_orders] PRIMARY KEY CLUSTERED -( - [key1] ASC, - [key2] ASC -) ON [PRIMARY] -) ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[types]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[types]( - [int_col] [int] NOT NULL, - [int_col2] [int] NULL CONSTRAINT [DF_types_int_col2] DEFAULT (1), - [char_col] [char](100) NOT NULL, - [char_col2] [varchar](100) NULL CONSTRAINT [DF_types_char_col2] DEFAULT ('something'), - [char_col3] [text] NULL, - [float_col] [real] NOT NULL, - [float_col2] [float] NULL CONSTRAINT [DF_types_float_col2] DEFAULT (1.23), - [blob_col] [image] NULL, - [numeric_col] [numeric](5, 2) NULL CONSTRAINT [DF_types_numeric_col] DEFAULT (33.22), - [time] [datetime] NULL CONSTRAINT [DF_types_time] DEFAULT ('2002-01-01 00:00:00'), - [bool_col] [bit] NOT NULL, - [bool_col2] [bit] NOT NULL CONSTRAINT [DF_types_bool_col2] DEFAULT (1) -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[users]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[users]( + CONSTRAINT [PK_category] PRIMARY KEY CLUSTERED ( + [id] ASC + ) ON [PRIMARY] +); + +CREATE TABLE [dbo].[tbl_item] ( [id] [int] IDENTITY(1,1) NOT NULL, - [username] [varchar](128) NOT NULL, - [password] [varchar](128) NOT NULL, - [email] [varchar](128) NOT NULL, - CONSTRAINT [PK_users] PRIMARY KEY CLUSTERED -( - [id] ASC -) ON [PRIMARY] -) ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[post_category]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[post_category]( + [name] [varchar](128) NOT NULL, [category_id] [int] NOT NULL, - [post_id] [int] NOT NULL, - CONSTRAINT [PK_post_category] PRIMARY KEY CLUSTERED -( - [category_id] ASC, - [post_id] ASC -) ON [PRIMARY] -) ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[items]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[items]( - [id] [int] IDENTITY(1,1) NOT NULL, - [name] [varchar](128) NULL, - [col1] [int] NOT NULL, - [col2] [int] NOT NULL, - CONSTRAINT [PK_items] PRIMARY KEY CLUSTERED -( - [id] ASC -) ON [PRIMARY] -) ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[comments]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[comments]( - [id] [int] IDENTITY(1,1) NOT NULL, - [content] [text] NOT NULL, - [post_id] [int] NOT NULL, - [author_id] [int] NOT NULL, - CONSTRAINT [PK_comments] PRIMARY KEY CLUSTERED -( - [id] ASC -) ON [PRIMARY] -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[posts]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[posts]( - [id] [int] IDENTITY(1,1) NOT NULL, - [title] [varchar](128) NOT NULL, - [create_time] [datetime] NOT NULL, - [author_id] [int] NOT NULL, - [content] [text] NULL, - CONSTRAINT [PK_posts] PRIMARY KEY CLUSTERED -( - [id] ASC -) ON [PRIMARY] -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[profiles]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[profiles]( - [id] [int] IDENTITY(1,1) NOT NULL, - [first_name] [varchar](128) NOT NULL, - [last_name] [varchar](128) NOT NULL, - [user_id] [int] NOT NULL, - CONSTRAINT [PK_profiles] PRIMARY KEY CLUSTERED -( - [id] ASC -) ON [PRIMARY] -) ON [PRIMARY] -END -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_categories_categories]') AND type = 'F') -ALTER TABLE [dbo].[categories] WITH CHECK ADD CONSTRAINT [FK_categories_categories] FOREIGN KEY([parent_id]) -REFERENCES [dbo].[categories] ([id]) -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_post_category_categories]') AND type = 'F') -ALTER TABLE [dbo].[post_category] WITH CHECK ADD CONSTRAINT [FK_post_category_categories] FOREIGN KEY([category_id]) -REFERENCES [dbo].[categories] ([id]) -ON DELETE CASCADE -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_post_category_posts]') AND type = 'F') -ALTER TABLE [dbo].[post_category] WITH NOCHECK ADD CONSTRAINT [FK_post_category_posts] FOREIGN KEY([post_id]) -REFERENCES [dbo].[posts] ([id]) -ON DELETE CASCADE -GO -ALTER TABLE [dbo].[post_category] CHECK CONSTRAINT [FK_post_category_posts] -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_items_orders]') AND type = 'F') -ALTER TABLE [dbo].[items] WITH CHECK ADD CONSTRAINT [FK_items_orders] FOREIGN KEY([col1], [col2]) -REFERENCES [dbo].[orders] ([key1], [key2]) -ON DELETE CASCADE -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_comments_users]') AND type = 'F') -ALTER TABLE [dbo].[comments] WITH NOCHECK ADD CONSTRAINT [FK_comments_users] FOREIGN KEY([author_id]) -REFERENCES [dbo].[users] ([id]) -ON DELETE CASCADE -GO -ALTER TABLE [dbo].[comments] CHECK CONSTRAINT [FK_comments_users] -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_post_comment]') AND type = 'F') -ALTER TABLE [dbo].[comments] WITH NOCHECK ADD CONSTRAINT [FK_post_comment] FOREIGN KEY([post_id]) -REFERENCES [dbo].[posts] ([id]) -ON DELETE CASCADE -GO -ALTER TABLE [dbo].[comments] CHECK CONSTRAINT [FK_post_comment] -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_posts_users]') AND type = 'F') -ALTER TABLE [dbo].[posts] WITH NOCHECK ADD CONSTRAINT [FK_posts_users] FOREIGN KEY([author_id]) -REFERENCES [dbo].[users] ([id]) -GO -ALTER TABLE [dbo].[posts] CHECK CONSTRAINT [FK_posts_users] -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_profile_user]') AND type = 'F') -ALTER TABLE [dbo].[profiles] WITH NOCHECK ADD CONSTRAINT [FK_profile_user] FOREIGN KEY([user_id]) -REFERENCES [dbo].[users] ([id]) -ON DELETE CASCADE -GO -ALTER TABLE [dbo].[profiles] CHECK CONSTRAINT [FK_profile_user] - -INSERT INTO users (username, password, email) VALUES ('user1','pass1','email1') -GO -INSERT INTO users (username, password, email) VALUES ('user2','pass2','email2') -GO -INSERT INTO users (username, password, email) VALUES ('user3','pass3','email3') -GO + CONSTRAINT [PK_item] PRIMARY KEY CLUSTERED ( + [id] ASC + ) ON [PRIMARY] +); -INSERT INTO profiles (first_name, last_name, user_id) VALUES ('first 1','last 1',1) -GO -INSERT INTO profiles (first_name, last_name, user_id) VALUES ('first 2','last 2',2) -GO - -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 1','2000-01-01',1,'content 1') -GO -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 2','2000-01-02',2,'content 2') -GO -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 3','2000-01-03',2,'content 3') -GO -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 4','2000-01-04',2,'content 4') -GO -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 5','2000-01-05',3,'content 5') -GO +CREATE TABLE [dbo].[tbl_order] ( + [id] [int] IDENTITY(1,1) NOT NULL, + [customer_id] [int] NOT NULL, + [create_time] [int] NOT NULL, + [total] [decimal](10,0) NOT NULL, + CONSTRAINT [PK_order] PRIMARY KEY CLUSTERED ( + [id] ASC + ) ON [PRIMARY] +); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 1',1, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 2',1, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 3',1, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 4',2, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 5',2, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 6',3, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 7',3, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 8',3, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 9',3, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 10',5, 3) -GO +CREATE TABLE [dbo].[tbl_order_item] ( + [order_id] [int] NOT NULL, + [item_id] [int] NOT NULL, + [quantity] [int] NOT NULL, + [subtotal] [decimal](10,0) NOT NULL, + CONSTRAINT [PK_order_item] PRIMARY KEY CLUSTERED ( + [order_id] ASC, + [item_id] ASC + ) ON [PRIMARY] +); -INSERT INTO categories (name, parent_id) VALUES ('cat 1',NULL) -GO -INSERT INTO categories (name, parent_id) VALUES ('cat 2',NULL) -GO -INSERT INTO categories (name, parent_id) VALUES ('cat 3',NULL) -GO -INSERT INTO categories (name, parent_id) VALUES ('cat 4',1) -GO -INSERT INTO categories (name, parent_id) VALUES ('cat 5',1) -GO -INSERT INTO categories (name, parent_id) VALUES ('cat 6',5) -GO -INSERT INTO categories (name, parent_id) VALUES ('cat 7',5) -GO +CREATE TABLE [dbo].[tbl_type] ( + [int_col] [int] NOT NULL, + [int_col2] [int] DEFAULT '1', + [char_col] [char](100) NOT NULL, + [char_col2] [varchar](100) DEFAULT 'something', + [char_col3] [text], + [float_col] [decimal](4,3) NOT NULL, + [float_col2] [float] DEFAULT '1.23', + [blob_col] [varbinary](MAX), + [numeric_col] [decimal](5,2) DEFAULT '33.22', + [time] [datetime] NOT NULL DEFAULT '2002-01-01 00:00:00', + [bool_col] [tinyint] NOT NULL, + [bool_col2] [tinyint] DEFAULT '1' +); -INSERT INTO post_category (category_id, post_id) VALUES (1,1) -GO -INSERT INTO post_category (category_id, post_id) VALUES (2,1) -GO -INSERT INTO post_category (category_id, post_id) VALUES (3,1) -GO -INSERT INTO post_category (category_id, post_id) VALUES (4,2) -GO -INSERT INTO post_category (category_id, post_id) VALUES (1,2) -GO -INSERT INTO post_category (category_id, post_id) VALUES (1,3) -GO +INSERT INTO [dbo].[tbl_customer] ([email], [name], [address], [status]) VALUES ('user1@example.com', 'user1', 'address1', 1); +INSERT INTO [dbo].[tbl_customer] ([email], [name], [address], [status]) VALUES ('user2@example.com', 'user2', 'address2', 1); +INSERT INTO [dbo].[tbl_customer] ([email], [name], [address], [status]) VALUES ('user3@example.com', 'user3', 'address3', 2); +INSERT INTO [dbo].[tbl_category] ([name]) VALUES ('Books'); +INSERT INTO [dbo].[tbl_category] ([name]) VALUES ('Movies'); -INSERT INTO orders (key1,key2,name) VALUES (1,2,'order 12') -GO -INSERT INTO orders (key1,key2,name) VALUES (1,3,'order 13') -GO -INSERT INTO orders (key1,key2,name) VALUES (2,1,'order 21') -GO -INSERT INTO orders (key1,key2,name) VALUES (2,2,'order 22') -GO +INSERT INTO [dbo].[tbl_item] ([name], [category_id]) VALUES ('Agile Web Application Development with Yii1.1 and PHP5', 1); +INSERT INTO [dbo].[tbl_item] ([name], [category_id]) VALUES ('Yii 1.1 Application Development Cookbook', 1); +INSERT INTO [dbo].[tbl_item] ([name], [category_id]) VALUES ('Ice Age', 2); +INSERT INTO [dbo].[tbl_item] ([name], [category_id]) VALUES ('Toy Story', 2); +INSERT INTO [dbo].[tbl_item] ([name], [category_id]) VALUES ('Cars', 2); +INSERT INTO [dbo].[tbl_order] ([customer_id], [create_time], [total]) VALUES (1, 1325282384, 110.0); +INSERT INTO [dbo].[tbl_order] ([customer_id], [create_time], [total]) VALUES (2, 1325334482, 33.0); +INSERT INTO [dbo].[tbl_order] ([customer_id], [create_time], [total]) VALUES (2, 1325502201, 40.0); -INSERT INTO items (name,col1,col2) VALUES ('item 1',1,2) -GO -INSERT INTO items (name,col1,col2) VALUES ('item 2',1,2) -GO -INSERT INTO items (name,col1,col2) VALUES ('item 3',1,3) -GO -INSERT INTO items (name,col1,col2) VALUES ('item 4',2,2) -GO -INSERT INTO items (name,col1,col2) VALUES ('item 5',2,2) -GO +INSERT INTO [dbo].[tbl_order_item] ([order_id], [item_id], [quantity], [subtotal]) VALUES (1, 1, 1, 30.0); +INSERT INTO [dbo].[tbl_order_item] ([order_id], [item_id], [quantity], [subtotal]) VALUES (1, 2, 2, 40.0); +INSERT INTO [dbo].[tbl_order_item] ([order_id], [item_id], [quantity], [subtotal]) VALUES (2, 4, 1, 10.0); +INSERT INTO [dbo].[tbl_order_item] ([order_id], [item_id], [quantity], [subtotal]) VALUES (2, 5, 1, 15.0); +INSERT INTO [dbo].[tbl_order_item] ([order_id], [item_id], [quantity], [subtotal]) VALUES (2, 3, 1, 8.0); +INSERT INTO [dbo].[tbl_order_item] ([order_id], [item_id], [quantity], [subtotal]) VALUES (3, 2, 1, 40.0); diff --git a/tests/unit/data/mysql.sql b/tests/unit/data/mysql.sql index 1bb5558..6c5c296 100644 --- a/tests/unit/data/mysql.sql +++ b/tests/unit/data/mysql.sql @@ -3,12 +3,21 @@ * The database setup in config.php is required to perform then relevant tests: */ +DROP TABLE IF EXISTS tbl_composite_fk CASCADE; DROP TABLE IF EXISTS tbl_order_item CASCADE; DROP TABLE IF EXISTS tbl_item CASCADE; DROP TABLE IF EXISTS tbl_order CASCADE; DROP TABLE IF EXISTS tbl_category CASCADE; DROP TABLE IF EXISTS tbl_customer CASCADE; DROP TABLE IF EXISTS tbl_type CASCADE; +DROP TABLE IF EXISTS tbl_constraints CASCADE; + +CREATE TABLE `tbl_constraints` +( + `id` integer not null, + `field1` varchar(255) +); + CREATE TABLE `tbl_customer` ( `id` int(11) NOT NULL AUTO_INCREMENT, @@ -54,6 +63,14 @@ CREATE TABLE `tbl_order_item` ( CONSTRAINT `FK_order_item_item_id` FOREIGN KEY (`item_id`) REFERENCES `tbl_item` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `tbl_composite_fk` ( + `id` int(11) NOT NULL, + `order_id` int(11) NOT NULL, + `item_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_composite_fk_order_item` FOREIGN KEY (`order_id`,`item_id`) REFERENCES `tbl_order_item` (`order_id`,`item_id`) ON DELETE CASCADE +); + CREATE TABLE `tbl_type` ( `int_col` int(11) NOT NULL, `int_col2` int(11) DEFAULT '1', @@ -92,3 +109,35 @@ INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 4, INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 5, 1, 15.0); INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 3, 1, 8.0); INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (3, 2, 1, 40.0); + + +/** + * (MySQL-)Database Schema for validator tests + */ + +DROP TABLE IF EXISTS tbl_validator_main CASCADE; +DROP TABLE IF EXISTS tbl_validator_ref CASCADE; + +CREATE TABLE tbl_validator_main ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `field1` VARCHAR(255), + PRIMARY KEY (`id`) +) ENGINE =InnoDB DEFAULT CHARSET =utf8; + +CREATE TABLE tbl_validator_ref ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `a_field` VARCHAR(255), + `ref` INT(11), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO tbl_validator_main (id, field1) VALUES (1, 'just a string1'); +INSERT INTO tbl_validator_main (id, field1) VALUES (2, 'just a string2'); +INSERT INTO tbl_validator_main (id, field1) VALUES (3, 'just a string3'); +INSERT INTO tbl_validator_main (id, field1) VALUES (4, 'just a string4'); +INSERT INTO tbl_validator_ref (a_field, ref) VALUES ('ref_to_2', 2); +INSERT INTO tbl_validator_ref (a_field, ref) VALUES ('ref_to_2', 2); +INSERT INTO tbl_validator_ref (a_field, ref) VALUES ('ref_to_3', 3); +INSERT INTO tbl_validator_ref (a_field, ref) VALUES ('ref_to_4', 4); +INSERT INTO tbl_validator_ref (a_field, ref) VALUES ('ref_to_4', 4); +INSERT INTO tbl_validator_ref (a_field, ref) VALUES ('ref_to_5', 5); diff --git a/tests/unit/data/postgres.sql b/tests/unit/data/postgres.sql index e46b284..c2702c9 100644 --- a/tests/unit/data/postgres.sql +++ b/tests/unit/data/postgres.sql @@ -1,165 +1,123 @@ /** * This is the database schema for testing PostgreSQL support of yii Active Record. - * To test this feature, you need to create a database named 'yii' on 'localhost' - * and create an account 'test/test' which owns this test database. + * To test this feature, you need to create a database named 'yiitest' on 'localhost' + * and create an account 'postgres/postgres' which owns this test database. */ -CREATE SCHEMA test; -CREATE TABLE test.users +DROP TABLE IF EXISTS tbl_order_item CASCADE; +DROP TABLE IF EXISTS tbl_item CASCADE; +DROP TABLE IF EXISTS tbl_order CASCADE; +DROP TABLE IF EXISTS tbl_category CASCADE; +DROP TABLE IF EXISTS tbl_customer CASCADE; +DROP TABLE IF EXISTS tbl_type CASCADE; +DROP TABLE IF EXISTS tbl_constraints CASCADE; + +CREATE TABLE tbl_constraints ( - id SERIAL NOT NULL PRIMARY KEY, - username VARCHAR(128) NOT NULL, - password VARCHAR(128) NOT NULL, - email VARCHAR(128) NOT NULL + id integer not null, + field1 varchar(255) ); -INSERT INTO test.users (username, password, email) VALUES ('user1','pass1','email1'); -INSERT INTO test.users (username, password, email) VALUES ('user2','pass2','email2'); -INSERT INTO test.users (username, password, email) VALUES ('user3','pass3','email3'); - -CREATE TABLE test.user_friends -( - id INTEGER NOT NULL, - friend INTEGER NOT NULL, - PRIMARY KEY (id, friend), - CONSTRAINT FK_user_id FOREIGN KEY (id) - REFERENCES test.users (id) ON DELETE CASCADE ON UPDATE RESTRICT, - CONSTRAINT FK_friend_id FOREIGN KEY (friend) - REFERENCES test.users (id) ON DELETE CASCADE ON UPDATE RESTRICT +CREATE TABLE tbl_customer ( + id serial not null primary key, + email varchar(128) NOT NULL, + name varchar(128) NOT NULL, + address text, + status integer DEFAULT 0 ); -INSERT INTO test.user_friends VALUES (1,2); -INSERT INTO test.user_friends VALUES (1,3); -INSERT INTO test.user_friends VALUES (2,3); +comment on column public.tbl_customer.email is 'someone@example.com'; -CREATE TABLE test.profiles -( - id SERIAL NOT NULL PRIMARY KEY, - first_name VARCHAR(128) NOT NULL, - last_name VARCHAR(128) NOT NULL, - user_id INTEGER NOT NULL, - CONSTRAINT FK_profile_user FOREIGN KEY (user_id) - REFERENCES test.users (id) ON DELETE CASCADE ON UPDATE RESTRICT +CREATE TABLE tbl_category ( + id serial not null primary key, + name varchar(128) NOT NULL ); -INSERT INTO test.profiles (first_name, last_name, user_id) VALUES ('first 1','last 1',1); -INSERT INTO test.profiles (first_name, last_name, user_id) VALUES ('first 2','last 2',2); - -CREATE TABLE test.posts -( - id SERIAL NOT NULL PRIMARY KEY, - title VARCHAR(128) NOT NULL, - create_time TIMESTAMP NOT NULL, - author_id INTEGER NOT NULL, - content TEXT, - CONSTRAINT FK_post_author FOREIGN KEY (author_id) - REFERENCES test.users (id) ON DELETE CASCADE ON UPDATE RESTRICT +CREATE TABLE tbl_item ( + id serial not null primary key, + name varchar(128) NOT NULL, + category_id integer NOT NULL references tbl_category(id) on UPDATE CASCADE on DELETE CASCADE ); -INSERT INTO test.posts (title, create_time, author_id, content) VALUES ('post 1',TIMESTAMP '2004-10-19 10:23:54',1,'content 1'); -INSERT INTO test.posts (title, create_time, author_id, content) VALUES ('post 2',TIMESTAMP '2004-10-19 10:23:54',2,'content 2'); -INSERT INTO test.posts (title, create_time, author_id, content) VALUES ('post 3',TIMESTAMP '2004-10-19 10:23:54',2,'content 3'); -INSERT INTO test.posts (title, create_time, author_id, content) VALUES ('post 4',TIMESTAMP '2004-10-19 10:23:54',2,'content 4'); -INSERT INTO test.posts (title, create_time, author_id, content) VALUES ('post 5',TIMESTAMP '2004-10-19 10:23:54',3,'content 5'); +CREATE TABLE tbl_order ( + id serial not null primary key, + customer_id integer NOT NULL references tbl_customer(id) on UPDATE CASCADE on DELETE CASCADE, + create_time integer NOT NULL, + total decimal(10,0) NOT NULL +); -CREATE TABLE test.comments -( - id SERIAL NOT NULL PRIMARY KEY, - content TEXT NOT NULL, - post_id INTEGER NOT NULL, - author_id INTEGER NOT NULL, - CONSTRAINT FK_post_comment FOREIGN KEY (post_id) - REFERENCES test.posts (id) ON DELETE CASCADE ON UPDATE RESTRICT, - CONSTRAINT FK_user_comment FOREIGN KEY (author_id) - REFERENCES test.users (id) ON DELETE CASCADE ON UPDATE RESTRICT +CREATE TABLE tbl_order_item ( + order_id integer NOT NULL references tbl_order(id) on UPDATE CASCADE on DELETE CASCADE, + item_id integer NOT NULL references tbl_item(id) on UPDATE CASCADE on DELETE CASCADE, + quantity integer NOT NULL, + subtotal decimal(10,0) NOT NULL, + PRIMARY KEY (order_id,item_id) ); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 1',1, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 2',1, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 3',1, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 4',2, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 5',2, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 6',3, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 7',3, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 8',3, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 9',3, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 10',5, 3); - -CREATE TABLE test.categories -( - id SERIAL NOT NULL PRIMARY KEY, - name VARCHAR(128) NOT NULL, - parent_id INTEGER, - CONSTRAINT FK_category_category FOREIGN KEY (parent_id) - REFERENCES test.categories (id) ON DELETE CASCADE ON UPDATE RESTRICT +CREATE TABLE tbl_type ( + int_col integer NOT NULL, + int_col2 integer DEFAULT '1', + char_col char(100) NOT NULL, + char_col2 varchar(100) DEFAULT 'something', + char_col3 text, + float_col double precision NOT NULL, + float_col2 double precision DEFAULT '1.23', + blob_col bytea, + numeric_col decimal(5,2) DEFAULT '33.22', + time timestamp NOT NULL DEFAULT '2002-01-01 00:00:00', + bool_col smallint NOT NULL, + bool_col2 smallint DEFAULT '1' ); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 1',NULL); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 2',NULL); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 3',NULL); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 4',1); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 5',1); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 6',5); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 7',5); +INSERT INTO tbl_customer (email, name, address, status) VALUES ('user1@example.com', 'user1', 'address1', 1); +INSERT INTO tbl_customer (email, name, address, status) VALUES ('user2@example.com', 'user2', 'address2', 1); +INSERT INTO tbl_customer (email, name, address, status) VALUES ('user3@example.com', 'user3', 'address3', 2); -CREATE TABLE test.post_category -( - category_id INTEGER NOT NULL, - post_id INTEGER NOT NULL, - PRIMARY KEY (category_id, post_id), - CONSTRAINT FK_post_category_post FOREIGN KEY (post_id) - REFERENCES test.posts (id) ON DELETE CASCADE ON UPDATE RESTRICT, - CONSTRAINT FK_post_category_category FOREIGN KEY (category_id) - REFERENCES test.categories (id) ON DELETE CASCADE ON UPDATE RESTRICT -); +INSERT INTO tbl_category (name) VALUES ('Books'); +INSERT INTO tbl_category (name) VALUES ('Movies'); -INSERT INTO test.post_category (category_id, post_id) VALUES (1,1); -INSERT INTO test.post_category (category_id, post_id) VALUES (2,1); -INSERT INTO test.post_category (category_id, post_id) VALUES (3,1); -INSERT INTO test.post_category (category_id, post_id) VALUES (4,2); -INSERT INTO test.post_category (category_id, post_id) VALUES (1,2); -INSERT INTO test.post_category (category_id, post_id) VALUES (1,3); +INSERT INTO tbl_item (name, category_id) VALUES ('Agile Web Application Development with Yii1.1 and PHP5', 1); +INSERT INTO tbl_item (name, category_id) VALUES ('Yii 1.1 Application Development Cookbook', 1); +INSERT INTO tbl_item (name, category_id) VALUES ('Ice Age', 2); +INSERT INTO tbl_item (name, category_id) VALUES ('Toy Story', 2); +INSERT INTO tbl_item (name, category_id) VALUES ('Cars', 2); -CREATE TABLE test.orders -( - key1 INTEGER NOT NULL, - key2 INTEGER NOT NULL, - name VARCHAR(128), - PRIMARY KEY (key1, key2) -); +INSERT INTO tbl_order (customer_id, create_time, total) VALUES (1, 1325282384, 110.0); +INSERT INTO tbl_order (customer_id, create_time, total) VALUES (2, 1325334482, 33.0); +INSERT INTO tbl_order (customer_id, create_time, total) VALUES (2, 1325502201, 40.0); -INSERT INTO test.orders (key1,key2,name) VALUES (1,2,'order 12'); -INSERT INTO test.orders (key1,key2,name) VALUES (1,3,'order 13'); -INSERT INTO test.orders (key1,key2,name) VALUES (2,1,'order 21'); -INSERT INTO test.orders (key1,key2,name) VALUES (2,2,'order 22'); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (1, 1, 1, 30.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (1, 2, 2, 40.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 4, 1, 10.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 5, 1, 15.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 3, 1, 8.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (3, 2, 1, 40.0); -CREATE TABLE test.items -( - id SERIAL NOT NULL PRIMARY KEY, - name VARCHAR(128), - col1 INTEGER NOT NULL, - col2 INTEGER NOT NULL, - CONSTRAINT FK_order_item FOREIGN KEY (col1,col2) - REFERENCES test.orders (key1,key2) ON DELETE CASCADE ON UPDATE RESTRICT +/** + * (Postgres-)Database Schema for validator tests + */ + +DROP TABLE IF EXISTS tbl_validator_main CASCADE; +DROP TABLE IF EXISTS tbl_validator_ref CASCADE; + +CREATE TABLE tbl_validator_main ( + id integer not null primary key, + field1 VARCHAR(255) ); -INSERT INTO test.items (name,col1,col2) VALUES ('item 1',1,2); -INSERT INTO test.items (name,col1,col2) VALUES ('item 2',1,2); -INSERT INTO test.items (name,col1,col2) VALUES ('item 3',1,3); -INSERT INTO test.items (name,col1,col2) VALUES ('item 4',2,2); -INSERT INTO test.items (name,col1,col2) VALUES ('item 5',2,2); +CREATE TABLE tbl_validator_ref ( + id integer not null primary key, + a_field VARCHAR(255), + ref integer +); -CREATE TABLE public.yii_types -( - int_col INT NOT NULL, - int_col2 INTEGER DEFAULT 1, - char_col CHAR(100) NOT NULL, - char_col2 VARCHAR(100) DEFAULT 'something', - char_col3 TEXT, - numeric_col NUMERIC(4,3) NOT NULL, - real_col REAL DEFAULT 1.23, - blob_col BYTEA, - time TIMESTAMP, - bool_col BOOL NOT NULL, - bool_col2 BOOLEAN DEFAULT TRUE -); \ No newline at end of file +INSERT INTO tbl_validator_main (id, field1) VALUES (1, 'just a string1'); +INSERT INTO tbl_validator_main (id, field1) VALUES (2, 'just a string2'); +INSERT INTO tbl_validator_main (id, field1) VALUES (3, 'just a string3'); +INSERT INTO tbl_validator_main (id, field1) VALUES (4, 'just a string4'); +INSERT INTO tbl_validator_ref (id, a_field, ref) VALUES (1, 'ref_to_2', 2); +INSERT INTO tbl_validator_ref (id, a_field, ref) VALUES (2, 'ref_to_2', 2); +INSERT INTO tbl_validator_ref (id, a_field, ref) VALUES (3, 'ref_to_3', 3); +INSERT INTO tbl_validator_ref (id, a_field, ref) VALUES (4, 'ref_to_4', 4); +INSERT INTO tbl_validator_ref (id, a_field, ref) VALUES (5, 'ref_to_4', 4); +INSERT INTO tbl_validator_ref (id, a_field, ref) VALUES (6, 'ref_to_5', 5); \ No newline at end of file diff --git a/tests/unit/data/sqlite.sql b/tests/unit/data/sqlite.sql index f75bfa6..03f1562 100644 --- a/tests/unit/data/sqlite.sql +++ b/tests/unit/data/sqlite.sql @@ -3,6 +3,7 @@ * The database setup in config.php is required to perform then relevant tests: */ +DROP TABLE IF EXISTS tbl_composite_fk; DROP TABLE IF EXISTS tbl_order_item; DROP TABLE IF EXISTS tbl_item; DROP TABLE IF EXISTS tbl_order; @@ -48,6 +49,14 @@ CREATE TABLE tbl_order_item ( PRIMARY KEY (order_id, item_id) ); +CREATE TABLE `tbl_composite_fk` ( + `id` int(11) NOT NULL, + `order_id` int(11) NOT NULL, + `item_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_composite_fk_order_item` FOREIGN KEY (`order_id`,`item_id`) REFERENCES `tbl_order_item` (`order_id`,`item_id`) ON DELETE CASCADE +); + CREATE TABLE tbl_type ( int_col INTEGER NOT NULL, int_col2 INTEGER DEFAULT '1', @@ -85,4 +94,33 @@ INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (1, 2, INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 4, 1, 10.0); INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 5, 1, 15.0); INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 3, 1, 8.0); -INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (3, 2, 1, 40.0); \ No newline at end of file +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (3, 2, 1, 40.0); + +/** + * (SqLite-)Database Schema for validator tests + */ + +DROP TABLE IF EXISTS tbl_validator_main; +DROP TABLE IF EXISTS tbl_validator_ref; + +CREATE TABLE tbl_validator_main ( + id INTEGER PRIMARY KEY , + field1 VARCHAR(255) +); + +CREATE TABLE tbl_validator_ref ( + id INTEGER PRIMARY KEY , + a_field VARCHAR(255), + ref INT(11) +); + +INSERT INTO tbl_validator_main (id, field1) VALUES (1, 'just a string1'); +INSERT INTO tbl_validator_main (id, field1) VALUES (2, 'just a string2'); +INSERT INTO tbl_validator_main (id, field1) VALUES (3, 'just a string3'); +INSERT INTO tbl_validator_main (id, field1) VALUES (4, 'just a string4'); +INSERT INTO tbl_validator_ref (id, a_field, ref) VALUES (1, 'ref_to_2', 2); +INSERT INTO tbl_validator_ref (id, a_field, ref) VALUES (2, 'ref_to_2', 2); +INSERT INTO tbl_validator_ref (id, a_field, ref) VALUES (3, 'ref_to_3', 3); +INSERT INTO tbl_validator_ref (id, a_field, ref) VALUES (4, 'ref_to_4', 4); +INSERT INTO tbl_validator_ref (id, a_field, ref) VALUES (5, 'ref_to_4', 4); +INSERT INTO tbl_validator_ref (id, a_field, ref) VALUES (6, 'ref_to_5', 5); diff --git a/tests/unit/data/travis/README.md b/tests/unit/data/travis/README.md new file mode 100644 index 0000000..c86497e --- /dev/null +++ b/tests/unit/data/travis/README.md @@ -0,0 +1,12 @@ +This directory contains scripts for automated test runs via the [Travis CI](http://travis-ci.org) build service. They are used for the preparation of worker instances by setting up needed extensions and configuring database access. + +These scripts might be used to configure your own system for test runs. But since their primary purpose remains to support Travis in running the test cases, you would be best advised to stick to the setup notes in the tests themselves. + +The scripts are: + + - [`apc-setup.sh`](apc-setup.sh) + Installs and configures the [apc pecl extension](http://pecl.php.net/package/apc) + - [`memcache-setup.sh`](memcache-setup.sh) + Compiles and installs the [memcache pecl extension](http://pecl.php.net/package/memcache) + - [`cubrid-setup.sh`](cubrid-setup.sh) + Prepares the [CUBRID](http://www.cubrid.org/) server instance by installing the server and PHP PDO driver diff --git a/tests/unit/data/travis/apc-setup.sh b/tests/unit/data/travis/apc-setup.sh new file mode 100755 index 0000000..e5e8734 --- /dev/null +++ b/tests/unit/data/travis/apc-setup.sh @@ -0,0 +1,2 @@ +echo "extension = apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini +echo "apc.enable_cli = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini \ No newline at end of file diff --git a/tests/unit/data/travis/cubrid-setup.sh b/tests/unit/data/travis/cubrid-setup.sh new file mode 100755 index 0000000..9c3bb74 --- /dev/null +++ b/tests/unit/data/travis/cubrid-setup.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# +# install CUBRID DBMS + +# cubrid dbms +echo 'yes' | sudo add-apt-repository ppa:cubrid/cubrid +sudo apt-get update +sudo apt-get install cubrid +/etc/profile.d/cubrid.sh +sudo apt-get install cubrid-demodb + +# cubrid pdo +install_pdo_cubrid() { + wget "http://pecl.php.net/get/PDO_CUBRID-9.2.0.0001.tgz" && + tar -zxf "PDO_CUBRID-9.2.0.0001.tgz" && + sh -c "cd PDO_CUBRID-9.2.0.0001 && phpize && ./configure && make && sudo make install" + + echo "extension=pdo_cubrid.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + + return $? +} + +install_pdo_cubrid > ~/pdo_cubrid.log || ( echo "=== PDO CUBRID BUILD FAILED ==="; cat ~/pdo_cubrid.log ) + +echo "Installed CUBRID `dpkg -s cubrid |grep Version`" diff --git a/tests/unit/data/travis/cubrid-solo.rb b/tests/unit/data/travis/cubrid-solo.rb new file mode 100644 index 0000000..f5f0004 --- /dev/null +++ b/tests/unit/data/travis/cubrid-solo.rb @@ -0,0 +1,5 @@ +file_cache_path "/tmp/chef-solo" +data_bag_path "/tmp/chef-solo/data_bags" +encrypted_data_bag_secret "/tmp/chef-solo/data_bag_key" +cookbook_path [ "/tmp/chef-solo/cookbooks" ] +role_path "/tmp/chef-solo/roles" \ No newline at end of file diff --git a/tests/unit/data/travis/memcache-setup.sh b/tests/unit/data/travis/memcache-setup.sh new file mode 100755 index 0000000..6b623d6 --- /dev/null +++ b/tests/unit/data/travis/memcache-setup.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +echo "extension=memcache.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini +echo "extension=memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini diff --git a/tests/unit/data/validators/TestValidator.php b/tests/unit/data/validators/TestValidator.php new file mode 100644 index 0000000..13f12dc --- /dev/null +++ b/tests/unit/data/validators/TestValidator.php @@ -0,0 +1,44 @@ +<?php + +namespace yiiunit\data\validators; + + +use yii\validators\Validator; + +class TestValidator extends Validator +{ + private $_validatedAttributes = array(); + private $_setErrorOnValidateAttribute = false; + + public function validateAttribute($object, $attribute) + { + $this->markAttributeValidated($attribute); + if ($this->_setErrorOnValidateAttribute == true) { + $this->addError($object, $attribute, sprintf('%s##%s', $attribute, get_class($object))); + } + } + + protected function markAttributeValidated($attr, $increaseBy = 1) + { + if (!isset($this->_validatedAttributes[$attr])) { + $this->_validatedAttributes[$attr] = 1; + } else { + $this->_validatedAttributes[$attr] = $this->_validatedAttributes[$attr] + $increaseBy; + } + } + + public function countAttributeValidations($attr) + { + return isset($this->_validatedAttributes[$attr]) ? $this->_validatedAttributes[$attr] : 0; + } + + public function isAttributeValidated($attr) + { + return isset($this->_validatedAttributes[$attr]); + } + + public function enableErrorOnValidateAttribute() + { + $this->_setErrorOnValidateAttribute = true; + } +} \ No newline at end of file diff --git a/tests/unit/data/validators/models/FakedValidationModel.php b/tests/unit/data/validators/models/FakedValidationModel.php new file mode 100644 index 0000000..2951ca8 --- /dev/null +++ b/tests/unit/data/validators/models/FakedValidationModel.php @@ -0,0 +1,63 @@ +<?php + +namespace yiiunit\data\validators\models; + +use yii\base\Model; + +class FakedValidationModel extends Model +{ + public $val_attr_a; + public $val_attr_b; + public $val_attr_c; + public $val_attr_d; + private $attr = array(); + + /** + * @param array $attributes + * @return self + */ + public static function createWithAttributes($attributes = array()) + { + $m = new static(); + foreach ($attributes as $attribute => $value) { + $m->$attribute = $value; + } + return $m; + } + + public function rules() + { + return array( + array('val_attr_a, val_attr_b', 'required', 'on' => 'reqTest'), + array('val_attr_c', 'integer'), + ); + } + + public function inlineVal($attribute, $params = array()) + { + return true; + } + + public function __get($name) + { + if (stripos($name, 'attr') === 0) { + return isset($this->attr[$name]) ? $this->attr[$name] : null; + } + + return parent::__get($name); + } + + public function __set($name, $value) + { + if (stripos($name, 'attr') === 0) { + $this->attr[$name] = $value; + } else { + parent::__set($name, $value); + } + } + + public function getAttributeLabel($attr) + { + return $attr; + } +} \ No newline at end of file diff --git a/tests/unit/data/validators/models/ValidatorTestMainModel.php b/tests/unit/data/validators/models/ValidatorTestMainModel.php new file mode 100644 index 0000000..00f977b --- /dev/null +++ b/tests/unit/data/validators/models/ValidatorTestMainModel.php @@ -0,0 +1,21 @@ +<?php + +namespace yiiunit\data\validators\models; + + +use yiiunit\data\ar\ActiveRecord; + +class ValidatorTestMainModel extends ActiveRecord +{ + public $testMainVal = 1; + + public static function tableName() + { + return 'tbl_validator_main'; + } + + public function getReferences() + { + return $this->hasMany(ValidatorTestRefModel::className(), array('ref' => 'id')); + } +} \ No newline at end of file diff --git a/tests/unit/data/validators/models/ValidatorTestRefModel.php b/tests/unit/data/validators/models/ValidatorTestRefModel.php new file mode 100644 index 0000000..fc4019d --- /dev/null +++ b/tests/unit/data/validators/models/ValidatorTestRefModel.php @@ -0,0 +1,23 @@ +<?php + +namespace yiiunit\data\validators\models; + + +use yiiunit\data\ar\ActiveRecord; + +class ValidatorTestRefModel extends ActiveRecord +{ + + public $test_val = 2; + public $test_val_fail = 99; + + public static function tableName() + { + return 'tbl_validator_ref'; + } + + public function getMain() + { + return $this->hasOne(ValidatorTestMainModel::className(), array('id' => 'ref')); + } +} \ No newline at end of file diff --git a/tests/unit/data/views/layout.php b/tests/unit/data/views/layout.php new file mode 100644 index 0000000..cd9d9d6 --- /dev/null +++ b/tests/unit/data/views/layout.php @@ -0,0 +1,22 @@ +<?php +/** + * @var $this \yii\base\View + * @var $content string + */ +?> +<?php $this->beginPage(); ?> +<!DOCTYPE html> +<html> +<head> + <title>Test</title> + <?php $this->head(); ?> +</head> +<body> +<?php $this->beginBody(); ?> + +<?php echo $content; ?> + +<?php $this->endBody(); ?> +</body> +</html> +<?php $this->endPage(); ?> \ No newline at end of file diff --git a/tests/unit/data/views/rawlayout.php b/tests/unit/data/views/rawlayout.php new file mode 100644 index 0000000..6864642 --- /dev/null +++ b/tests/unit/data/views/rawlayout.php @@ -0,0 +1,5 @@ +<?php +/** + * @var $this \yii\base\View + */ +?><?php $this->beginPage(); ?>1<?php $this->head(); ?>2<?php $this->beginBody(); ?>3<?php $this->endBody(); ?>4<?php $this->endPage(); ?> \ No newline at end of file diff --git a/tests/unit/data/views/simple.php b/tests/unit/data/views/simple.php new file mode 100644 index 0000000..437ba90 --- /dev/null +++ b/tests/unit/data/views/simple.php @@ -0,0 +1 @@ +This is a damn simple view file. \ No newline at end of file diff --git a/tests/unit/data/web/assets/.gitignore b/tests/unit/data/web/assets/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/tests/unit/data/web/assets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/tests/unit/data/web/data.txt b/tests/unit/data/web/data.txt new file mode 100644 index 0000000..8e58281 --- /dev/null +++ b/tests/unit/data/web/data.txt @@ -0,0 +1 @@ +12ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=? \ No newline at end of file diff --git a/tests/unit/framework/YiiBaseTest.php b/tests/unit/framework/BaseYiiTest.php similarity index 87% rename from tests/unit/framework/YiiBaseTest.php rename to tests/unit/framework/BaseYiiTest.php index 4ebf45c..a04de99 100644 --- a/tests/unit/framework/YiiBaseTest.php +++ b/tests/unit/framework/BaseYiiTest.php @@ -5,19 +5,22 @@ use Yii; use yiiunit\TestCase; /** - * YiiBaseTest + * BaseYiiTest + * @group base */ -class YiiBaseTest extends TestCase +class BaseYiiTest extends TestCase { public $aliases; - public function setUp() + protected function setUp() { + parent::setUp(); $this->aliases = Yii::$aliases; } - public function tearDown() + protected function tearDown() { + parent::tearDown(); Yii::$aliases = $this->aliases; } @@ -43,6 +46,9 @@ class YiiBaseTest extends TestCase Yii::setAlias('@yii', null); $this->assertFalse(Yii::getAlias('@yii', false)); $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); + + Yii::setAlias('@some/alias', '/www'); + $this->assertEquals('/www', Yii::getAlias('@some/alias')); } public function testGetVersion() diff --git a/tests/unit/framework/base/BehaviorTest.php b/tests/unit/framework/base/BehaviorTest.php index 11fbe7f..b6eda09 100644 --- a/tests/unit/framework/base/BehaviorTest.php +++ b/tests/unit/framework/base/BehaviorTest.php @@ -8,7 +8,6 @@ use yiiunit\TestCase; class BarClass extends Component { - } class FooClass extends Component @@ -29,10 +28,35 @@ class BarBehavior extends Behavior { return 'behavior method'; } + + public function __call($name, $params) + { + if ($name == 'magicBehaviorMethod') { + return 'Magic Behavior Method Result!'; + } + return parent::__call($name, $params); + } + + public function hasMethod($name) + { + if ($name == 'magicBehaviorMethod') { + return true; + } + return parent::hasMethod($name); + } } +/** + * @group base + */ class BehaviorTest extends TestCase { + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + public function testAttachAndAccessing() { $bar = new BarClass(); @@ -54,4 +78,29 @@ class BehaviorTest extends TestCase $this->assertEquals('behavior property', $foo->behaviorProperty); $this->assertEquals('behavior method', $foo->behaviorMethod()); } + + public function testMagicMethods() + { + $bar = new BarClass(); + $behavior = new BarBehavior(); + + $this->assertFalse($bar->hasMethod('magicBehaviorMethod')); + $bar->attachBehavior('bar', $behavior); + $this->assertFalse($bar->hasMethod('magicBehaviorMethod', false)); + $this->assertTrue($bar->hasMethod('magicBehaviorMethod')); + + $this->assertEquals('Magic Behavior Method Result!', $bar->magicBehaviorMethod()); + } + + public function testCallUnknownMethod() + { + $bar = new BarClass(); + $behavior = new BarBehavior(); + $this->setExpectedException('yii\base\UnknownMethodException'); + + $this->assertFalse($bar->hasMethod('nomagicBehaviorMethod')); + $bar->attachBehavior('bar', $behavior); + $bar->nomagicBehaviorMethod(); + } + } diff --git a/tests/unit/framework/base/ComponentTest.php b/tests/unit/framework/base/ComponentTest.php index 7c860e3..98786e2 100644 --- a/tests/unit/framework/base/ComponentTest.php +++ b/tests/unit/framework/base/ComponentTest.php @@ -17,6 +17,9 @@ function globalEventHandler2($event) $event->handled = true; } +/** + * @group base + */ class ComponentTest extends TestCase { /** @@ -24,13 +27,16 @@ class ComponentTest extends TestCase */ protected $component; - public function setUp() + protected function setUp() { + parent::setUp(); + $this->mockApplication(); $this->component = new NewComponent(); } - public function tearDown() + protected function tearDown() { + parent::tearDown(); $this->component = null; } @@ -191,7 +197,7 @@ class ComponentTest extends TestCase $this->assertFalse($this->component->event->handled); $eventRaised = false; - $this->component->on('click', function($event) use (&$eventRaised) { + $this->component->on('click', function ($event) use (&$eventRaised) { $eventRaised = true; }); $this->component->raiseEvent(); @@ -199,7 +205,7 @@ class ComponentTest extends TestCase // raise event w/o parameters $eventRaised = false; - $this->component->on('test', function($event) use (&$eventRaised) { + $this->component->on('test', function ($event) use (&$eventRaised) { $eventRaised = true; }); $this->component->trigger('test'); @@ -328,7 +334,7 @@ class NewComponent extends Component public function getExecute() { - return function($param) { + return function ($param) { return $param * 2; }; } diff --git a/tests/unit/framework/base/DictionaryTest.php b/tests/unit/framework/base/DictionaryTest.php deleted file mode 100644 index 9e55547..0000000 --- a/tests/unit/framework/base/DictionaryTest.php +++ /dev/null @@ -1,204 +0,0 @@ -<?php - -namespace yiiunit\framework\base; - -use yii\base\Dictionary; - -class MapItem -{ - public $data='data'; -} - -class DictionaryTest extends \yiiunit\TestCase -{ - /** - * @var \yii\base\Dictionary - */ - protected $dictionary; - protected $item1,$item2,$item3; - - public function setUp() - { - $this->dictionary=new Dictionary; - $this->item1=new MapItem; - $this->item2=new MapItem; - $this->item3=new MapItem; - $this->dictionary->add('key1',$this->item1); - $this->dictionary->add('key2',$this->item2); - } - - public function tearDown() - { - $this->dictionary=null; - $this->item1=null; - $this->item2=null; - $this->item3=null; - } - - public function testConstruct() - { - $a=array(1,2,'key3'=>3); - $dictionary=new Dictionary($a); - $this->assertEquals(3,$dictionary->getCount()); - $dictionary2=new Dictionary($this->dictionary); - $this->assertEquals(2,$dictionary2->getCount()); - } - - public function testGetCount() - { - $this->assertEquals(2,$this->dictionary->getCount()); - } - - public function testGetKeys() - { - $keys=$this->dictionary->getKeys(); - $this->assertEquals(2,count($keys)); - $this->assertEquals('key1',$keys[0]); - $this->assertEquals('key2',$keys[1]); - } - - public function testAdd() - { - $this->dictionary->add('key3',$this->item3); - $this->assertEquals(3,$this->dictionary->getCount()); - $this->assertTrue($this->dictionary->has('key3')); - - $this->dictionary[] = 'test'; - } - - public function testRemove() - { - $this->dictionary->remove('key1'); - $this->assertEquals(1,$this->dictionary->getCount()); - $this->assertTrue(!$this->dictionary->has('key1')); - $this->assertTrue($this->dictionary->remove('unknown key')===null); - } - - public function testRemoveAll() - { - $this->dictionary->add('key3',$this->item3); - $this->dictionary->removeAll(); - $this->assertEquals(0,$this->dictionary->getCount()); - $this->assertTrue(!$this->dictionary->has('key1') && !$this->dictionary->has('key2')); - - $this->dictionary->add('key3',$this->item3); - $this->dictionary->removeAll(true); - $this->assertEquals(0,$this->dictionary->getCount()); - $this->assertTrue(!$this->dictionary->has('key1') && !$this->dictionary->has('key2')); - } - - public function testHas() - { - $this->assertTrue($this->dictionary->has('key1')); - $this->assertTrue($this->dictionary->has('key2')); - $this->assertFalse($this->dictionary->has('key3')); - } - - public function testFromArray() - { - $array=array('key3'=>$this->item3,'key4'=>$this->item1); - $this->dictionary->copyFrom($array); - - $this->assertEquals(2, $this->dictionary->getCount()); - $this->assertEquals($this->item3, $this->dictionary['key3']); - $this->assertEquals($this->item1, $this->dictionary['key4']); - - $this->setExpectedException('yii\base\InvalidParamException'); - $this->dictionary->copyFrom($this); - } - - public function testMergeWith() - { - $a=array('a'=>'v1','v2',array('2'),'c'=>array('3','c'=>'a')); - $b=array('v22','a'=>'v11',array('2'),'c'=>array('c'=>'3','a')); - $c=array('a'=>'v11','v2',array('2'),'c'=>array('3','c'=>'3','a'),'v22',array('2')); - $dictionary=new Dictionary($a); - $dictionary2=new Dictionary($b); - $dictionary->mergeWith($dictionary2); - $this->assertTrue($dictionary->toArray()===$c); - - $array=array('key2'=>$this->item1,'key3'=>$this->item3); - $this->dictionary->mergeWith($array,false); - $this->assertEquals(3,$this->dictionary->getCount()); - $this->assertEquals($this->item1,$this->dictionary['key2']); - $this->assertEquals($this->item3,$this->dictionary['key3']); - $this->setExpectedException('yii\base\InvalidParamException'); - $this->dictionary->mergeWith($this,false); - } - - public function testRecursiveMergeWithTraversable(){ - $dictionary = new Dictionary(); - $obj = new \ArrayObject(array( - 'k1' => $this->item1, - 'k2' => $this->item2, - 'k3' => new \ArrayObject(array( - 'k4' => $this->item3, - )) - )); - $dictionary->mergeWith($obj,true); - - $this->assertEquals(3, $dictionary->getCount()); - $this->assertEquals($this->item1, $dictionary['k1']); - $this->assertEquals($this->item2, $dictionary['k2']); - $this->assertEquals($this->item3, $dictionary['k3']['k4']); - } - - public function testArrayRead() - { - $this->assertEquals($this->item1,$this->dictionary['key1']); - $this->assertEquals($this->item2,$this->dictionary['key2']); - $this->assertEquals(null,$this->dictionary['key3']); - } - - public function testArrayWrite() - { - $this->dictionary['key3']=$this->item3; - $this->assertEquals(3,$this->dictionary->getCount()); - $this->assertEquals($this->item3,$this->dictionary['key3']); - - $this->dictionary['key1']=$this->item3; - $this->assertEquals(3,$this->dictionary->getCount()); - $this->assertEquals($this->item3,$this->dictionary['key1']); - - unset($this->dictionary['key2']); - $this->assertEquals(2,$this->dictionary->getCount()); - $this->assertTrue(!$this->dictionary->has('key2')); - - unset($this->dictionary['unknown key']); - } - - public function testArrayForeach() - { - $n=0; - $found=0; - foreach($this->dictionary as $index=>$item) - { - $n++; - if($index==='key1' && $item===$this->item1) - $found++; - if($index==='key2' && $item===$this->item2) - $found++; - } - $this->assertTrue($n==2 && $found==2); - } - - public function testArrayMisc() - { - $this->assertEquals($this->dictionary->Count,count($this->dictionary)); - $this->assertTrue(isset($this->dictionary['key1'])); - $this->assertFalse(isset($this->dictionary['unknown key'])); - } - - public function testToArray() - { - $dictionary = new Dictionary(array('key' => 'value')); - $this->assertEquals(array('key' => 'value'), $dictionary->toArray()); - } - - public function testIteratorCurrent() - { - $dictionary = new Dictionary(array('key1' => 'value1', 'key2' => 'value2')); - $val = $dictionary->getIterator()->current(); - $this->assertEquals('value1', $val); - } -} diff --git a/tests/unit/framework/base/ExceptionTest.php b/tests/unit/framework/base/ExceptionTest.php new file mode 100644 index 0000000..af4293a --- /dev/null +++ b/tests/unit/framework/base/ExceptionTest.php @@ -0,0 +1,23 @@ +<?php +namespace yiiunit\framework\base; + +use yii\test\TestCase; +use yii\base\UserException; +use yii\base\InvalidCallException; + + +class ExceptionTest extends TestCase +{ + public function testToArrayWithPrevious() + { + $e = new InvalidCallException('bar', 0 ,new InvalidCallException('foo')); + $array = $e->toArray(); + $this->assertEquals('bar', $array['message']); + $this->assertEquals('foo', $array['previous']['message']); + + $e = new InvalidCallException('bar', 0 ,new UserException('foo')); + $array = $e->toArray(); + $this->assertEquals('bar', $array['message']); + $this->assertEquals('foo', $array['previous']['message']); + } +} diff --git a/tests/unit/framework/base/FormatterTest.php b/tests/unit/framework/base/FormatterTest.php new file mode 100644 index 0000000..ae71a5c --- /dev/null +++ b/tests/unit/framework/base/FormatterTest.php @@ -0,0 +1,200 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ +namespace yiiunit\framework\base; + +use yii\base\Formatter; +use yiiunit\TestCase; + +/** + * @group base + */ +class FormatterTest extends TestCase +{ + /** + * @var Formatter + */ + protected $formatter; + + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + $this->formatter = new Formatter(); + } + + protected function tearDown() + { + parent::tearDown(); + $this->formatter = null; + } + + public function testAsRaw() + { + $value = '123'; + $this->assertSame($value, $this->formatter->asRaw($value)); + $value = 123; + $this->assertSame($value, $this->formatter->asRaw($value)); + $value = '<>'; + $this->assertSame($value, $this->formatter->asRaw($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asRaw(null)); + } + + public function testAsText() + { + $value = '123'; + $this->assertSame($value, $this->formatter->asText($value)); + $value = 123; + $this->assertSame("$value", $this->formatter->asText($value)); + $value = '<>'; + $this->assertSame('<>', $this->formatter->asText($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asText(null)); + } + + public function testAsNtext() + { + $value = '123'; + $this->assertSame($value, $this->formatter->asNtext($value)); + $value = 123; + $this->assertSame("$value", $this->formatter->asNtext($value)); + $value = '<>'; + $this->assertSame('<>', $this->formatter->asNtext($value)); + $value = "123\n456"; + $this->assertSame("123<br />\n456", $this->formatter->asNtext($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asNtext(null)); + } + + public function testAsParagraphs() + { + $value = '123'; + $this->assertSame("<p>$value</p>", $this->formatter->asParagraphs($value)); + $value = 123; + $this->assertSame("<p>$value</p>", $this->formatter->asParagraphs($value)); + $value = '<>'; + $this->assertSame('<p><></p>', $this->formatter->asParagraphs($value)); + $value = "123\n456"; + $this->assertSame("<p>123\n456</p>", $this->formatter->asParagraphs($value)); + $value = "123\n\n456"; + $this->assertSame("<p>123</p>\n<p>456</p>", $this->formatter->asParagraphs($value)); + $value = "123\n\n\n456"; + $this->assertSame("<p>123</p>\n<p>456</p>", $this->formatter->asParagraphs($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asParagraphs(null)); + } + + public function testAsHtml() + { + // todo: dependency on HtmlPurifier + } + + public function testAsEmail() + { + $value = 'test@sample.com'; + $this->assertSame("<a href=\"mailto:$value\">$value</a>", $this->formatter->asEmail($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asEmail(null)); + } + + public function testAsImage() + { + $value = 'http://sample.com/img.jpg'; + $this->assertSame("<img src=\"$value\" alt=\"\">", $this->formatter->asImage($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asImage(null)); + } + + public function testAsBoolean() + { + $value = true; + $this->assertSame('Yes', $this->formatter->asBoolean($value)); + $value = false; + $this->assertSame('No', $this->formatter->asBoolean($value)); + $value = "111"; + $this->assertSame('Yes', $this->formatter->asBoolean($value)); + $value = ""; + $this->assertSame('No', $this->formatter->asBoolean($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asBoolean(null)); + } + + public function testAsDate() + { + $value = time(); + $this->assertSame(date('Y/m/d', $value), $this->formatter->asDate($value)); + $this->assertSame(date('Y-m-d', $value), $this->formatter->asDate($value, 'Y-m-d')); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null)); + } + + public function testAsTime() + { + $value = time(); + $this->assertSame(date('h:i:s A', $value), $this->formatter->asTime($value)); + $this->assertSame(date('h:i:s', $value), $this->formatter->asTime($value, 'h:i:s')); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asTime(null)); + } + + public function testAsDatetime() + { + $value = time(); + $this->assertSame(date('Y/m/d h:i:s A', $value), $this->formatter->asDatetime($value)); + $this->assertSame(date('Y-m-d h:i:s', $value), $this->formatter->asDatetime($value, 'Y-m-d h:i:s')); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDatetime(null)); + } + + public function testAsInteger() + { + $value = 123; + $this->assertSame("$value", $this->formatter->asInteger($value)); + $value = 123.23; + $this->assertSame("123", $this->formatter->asInteger($value)); + $value = 'a'; + $this->assertSame("0", $this->formatter->asInteger($value)); + $value = -123.23; + $this->assertSame("-123", $this->formatter->asInteger($value)); + $value = "-123abc"; + $this->assertSame("-123", $this->formatter->asInteger($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asInteger(null)); + } + + public function testAsDouble() + { + $value = 123.12; + $this->assertSame("123.12", $this->formatter->asDouble($value)); + $this->assertSame("123.1", $this->formatter->asDouble($value, 1)); + $this->assertSame("123", $this->formatter->asDouble($value, 0)); + $value = 123; + $this->assertSame("123.00", $this->formatter->asDouble($value)); + $this->formatter->decimalSeparator = ','; + $value = 123.12; + $this->assertSame("123,12", $this->formatter->asDouble($value)); + $this->assertSame("123,1", $this->formatter->asDouble($value, 1)); + $this->assertSame("123", $this->formatter->asDouble($value, 0)); + $value = 123123.123; + $this->assertSame("123123,12", $this->formatter->asDouble($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDouble(null)); + } + + public function testAsNumber() + { + $value = 123123.123; + $this->assertSame("123,123", $this->formatter->asNumber($value)); + $this->assertSame("123,123.12", $this->formatter->asNumber($value, 2)); + $this->formatter->decimalSeparator = ','; + $this->formatter->thousandSeparator = ' '; + $this->assertSame("123 123", $this->formatter->asNumber($value)); + $this->assertSame("123 123,12", $this->formatter->asNumber($value, 2)); + $this->formatter->thousandSeparator = ''; + $this->assertSame("123123", $this->formatter->asNumber($value)); + $this->assertSame("123123,12", $this->formatter->asNumber($value, 2)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asNumber(null)); + } + + public function testFormat() + { + $value = time(); + $this->assertSame(date('Y/m/d', $value), $this->formatter->format($value, 'date')); + $this->assertSame(date('Y/m/d', $value), $this->formatter->format($value, 'DATE')); + $this->assertSame(date('Y-m-d', $value), $this->formatter->format($value, array('date', 'Y-m-d'))); + $this->setExpectedException('\yii\base\InvalidParamException'); + $this->assertSame(date('Y-m-d', $value), $this->formatter->format($value, 'data')); + } +} diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php index f04e550..d500933 100644 --- a/tests/unit/framework/base/ModelTest.php +++ b/tests/unit/framework/base/ModelTest.php @@ -1,6 +1,7 @@ <?php namespace yiiunit\framework\base; + use yii\base\Model; use yiiunit\TestCase; use yiiunit\data\base\Speaker; @@ -8,11 +9,17 @@ use yiiunit\data\base\Singer; use yiiunit\data\base\InvalidRulesModel; /** - * ModelTest + * @group base */ class ModelTest extends TestCase { - public function testGetAttributeLalel() + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + + public function testGetAttributeLabel() { $speaker = new Speaker(); $this->assertEquals('First Name', $speaker->getAttributeLabel('firstName')); @@ -68,6 +75,32 @@ class ModelTest extends TestCase $this->assertEquals('Qiang', $speaker->firstName); } + public function testLoad() + { + $singer = new Singer(); + $this->assertEquals('Singer', $singer->formName()); + + $post = array('firstName' => 'Qiang'); + + Speaker::$formName = ''; + $model = new Speaker(); + $model->setScenario('test'); + $this->assertTrue($model->load($post)); + $this->assertEquals('Qiang', $model->firstName); + + Speaker::$formName = 'Speaker'; + $model = new Speaker(); + $model->setScenario('test'); + $this->assertTrue($model->load(array('Speaker' => $post))); + $this->assertEquals('Qiang', $model->firstName); + + Speaker::$formName = 'Speaker'; + $model = new Speaker(); + $model->setScenario('test'); + $this->assertFalse($model->load(array('Example' => array()))); + $this->assertEquals('', $model->firstName); + } + public function testActiveAttributes() { // by default mass assignment doesn't work at all @@ -155,7 +188,7 @@ class ModelTest extends TestCase // iteration $attributes = array(); - foreach($speaker as $key => $attribute) { + foreach ($speaker as $key => $attribute) { $attributes[$key] = $attribute; } $this->assertEquals(array( diff --git a/tests/unit/framework/base/ObjectTest.php b/tests/unit/framework/base/ObjectTest.php index 14856e2..0bd9b1d 100644 --- a/tests/unit/framework/base/ObjectTest.php +++ b/tests/unit/framework/base/ObjectTest.php @@ -5,7 +5,7 @@ use yii\base\Object; use yiiunit\TestCase; /** - * ObjectTest + * @group base */ class ObjectTest extends TestCase { @@ -14,13 +14,16 @@ class ObjectTest extends TestCase */ protected $object; - public function setUp() + protected function setUp() { + parent::setUp(); + $this->mockApplication(); $this->object = new NewObject; } - public function tearDown() + protected function tearDown() { + parent::tearDown(); $this->object = null; } @@ -131,11 +134,6 @@ class ObjectTest extends TestCase $this->assertEquals('new text', $this->object->object->text); } - public function testAnonymousFunctionProperty() - { - $this->assertEquals(2, $this->object->execute(1)); - } - public function testConstruct() { $object = new NewObject(array('text' => 'test text')); @@ -172,7 +170,7 @@ class NewObject extends Object public function getExecute() { - return function($param) { + return function ($param) { return $param * 2; }; } diff --git a/tests/unit/framework/base/VectorTest.php b/tests/unit/framework/base/VectorTest.php deleted file mode 100644 index 5c44d17..0000000 --- a/tests/unit/framework/base/VectorTest.php +++ /dev/null @@ -1,221 +0,0 @@ -<?php - -namespace yiiunit\framework\base; - -use yii\base\Vector; - -class ListItem -{ - public $data='data'; -} - -class VectorTest extends \yiiunit\TestCase -{ - /** - * @var Vector - */ - protected $vector; - protected $item1, $item2, $item3; - - public function setUp() - { - $this->vector=new Vector; - $this->item1=new ListItem; - $this->item2=new ListItem; - $this->item3=new ListItem; - $this->vector->add($this->item1); - $this->vector->add($this->item2); - } - - public function tearDown() - { - $this->vector=null; - $this->item1=null; - $this->item2=null; - $this->item3=null; - } - - public function testConstruct() - { - $a=array(1,2,3); - $vector=new Vector($a); - $this->assertEquals(3,$vector->getCount()); - $vector2=new Vector($this->vector); - $this->assertEquals(2,$vector2->getCount()); - } - - public function testItemAt() - { - $a=array(1, 2, null, 4); - $vector=new Vector($a); - $this->assertEquals(1, $vector->itemAt(0)); - $this->assertEquals(2, $vector->itemAt(1)); - $this->assertNull($vector->itemAt(2)); - $this->assertEquals(4, $vector->itemAt(3)); - } - - public function testGetCount() - { - $this->assertEquals(2,$this->vector->getCount()); - $this->assertEquals(2,$this->vector->Count); - } - - public function testAdd() - { - $this->vector->add(null); - $this->vector->add($this->item3); - $this->assertEquals(4,$this->vector->getCount()); - $this->assertEquals(3,$this->vector->indexOf($this->item3)); - } - - public function testInsertAt() - { - $this->vector->insertAt(0,$this->item3); - $this->assertEquals(3,$this->vector->getCount()); - $this->assertEquals(2,$this->vector->indexOf($this->item2)); - $this->assertEquals(0,$this->vector->indexOf($this->item3)); - $this->assertEquals(1,$this->vector->indexOf($this->item1)); - $this->setExpectedException('yii\base\InvalidParamException'); - $this->vector->insertAt(4,$this->item3); - } - - public function testRemove() - { - $this->vector->remove($this->item1); - $this->assertEquals(1,$this->vector->getCount()); - $this->assertEquals(-1,$this->vector->indexOf($this->item1)); - $this->assertEquals(0,$this->vector->indexOf($this->item2)); - - $this->assertEquals(false,$this->vector->remove($this->item1)); - - } - - public function testRemoveAt() - { - $this->vector->add($this->item3); - $this->vector->removeAt(1); - $this->assertEquals(-1,$this->vector->indexOf($this->item2)); - $this->assertEquals(1,$this->vector->indexOf($this->item3)); - $this->assertEquals(0,$this->vector->indexOf($this->item1)); - $this->setExpectedException('yii\base\InvalidParamException'); - $this->vector->removeAt(2); - } - - public function testRemoveAll() - { - $this->vector->add($this->item3); - $this->vector->removeAll(); - $this->assertEquals(0,$this->vector->getCount()); - $this->assertEquals(-1,$this->vector->indexOf($this->item1)); - $this->assertEquals(-1,$this->vector->indexOf($this->item2)); - - $this->vector->add($this->item3); - $this->vector->removeAll(true); - $this->assertEquals(0,$this->vector->getCount()); - $this->assertEquals(-1,$this->vector->indexOf($this->item1)); - $this->assertEquals(-1,$this->vector->indexOf($this->item2)); - } - - public function testHas() - { - $this->assertTrue($this->vector->has($this->item1)); - $this->assertTrue($this->vector->has($this->item2)); - $this->assertFalse($this->vector->has($this->item3)); - } - - public function testIndexOf() - { - $this->assertEquals(0,$this->vector->indexOf($this->item1)); - $this->assertEquals(1,$this->vector->indexOf($this->item2)); - $this->assertEquals(-1,$this->vector->indexOf($this->item3)); - } - - public function testFromArray() - { - $array=array($this->item3,$this->item1); - $this->vector->copyFrom($array); - $this->assertTrue(count($array)==2 && $this->vector[0]===$this->item3 && $this->vector[1]===$this->item1); - $this->setExpectedException('yii\base\InvalidParamException'); - $this->vector->copyFrom($this); - } - - public function testMergeWith() - { - $array=array($this->item3,$this->item1); - $this->vector->mergeWith($array); - $this->assertTrue($this->vector->getCount()==4 && $this->vector[0]===$this->item1 && $this->vector[3]===$this->item1); - - $a=array(1); - $vector=new Vector($a); - $this->vector->mergeWith($vector); - $this->assertTrue($this->vector->getCount()==5 && $this->vector[0]===$this->item1 && $this->vector[3]===$this->item1 && $this->vector[4]===1); - - $this->setExpectedException('yii\base\InvalidParamException'); - $this->vector->mergeWith($this); - } - - public function testToArray() - { - $array=$this->vector->toArray(); - $this->assertTrue(count($array)==2 && $array[0]===$this->item1 && $array[1]===$this->item2); - } - - public function testArrayRead() - { - $this->assertTrue($this->vector[0]===$this->item1); - $this->assertTrue($this->vector[1]===$this->item2); - $this->setExpectedException('yii\base\InvalidParamException'); - $a=$this->vector[2]; - } - - public function testGetIterator() - { - $n=0; - $found=0; - foreach($this->vector as $index=>$item) - { - foreach($this->vector as $a=>$b); // test of iterator - $n++; - if($index===0 && $item===$this->item1) - $found++; - if($index===1 && $item===$this->item2) - $found++; - } - $this->assertTrue($n==2 && $found==2); - } - - public function testArrayMisc() - { - $this->assertEquals($this->vector->Count,count($this->vector)); - $this->assertTrue(isset($this->vector[1])); - $this->assertFalse(isset($this->vector[2])); - } - - public function testOffsetSetAdd() - { - $vector = new Vector(array(1, 2, 3)); - $vector->offsetSet(null, 4); - $this->assertEquals(array(1, 2, 3, 4), $vector->toArray()); - } - - public function testOffsetSetReplace() - { - $vector = new Vector(array(1, 2, 3)); - $vector->offsetSet(1, 4); - $this->assertEquals(array(1, 4, 3), $vector->toArray()); - } - - public function testOffsetUnset() - { - $vector = new Vector(array(1, 2, 3)); - $vector->offsetUnset(1); - $this->assertEquals(array(1, 3), $vector->toArray()); - } - - public function testIteratorCurrent() - { - $vector = new Vector(array('value1', 'value2')); - $val = $vector->getIterator()->current(); - $this->assertEquals('value1', $val); - } -} diff --git a/tests/unit/framework/behaviors/AutoTimestampTest.php b/tests/unit/framework/behaviors/AutoTimestampTest.php new file mode 100644 index 0000000..c26d912 --- /dev/null +++ b/tests/unit/framework/behaviors/AutoTimestampTest.php @@ -0,0 +1,118 @@ +<?php + +namespace yiiunit\framework\behaviors; + +use Yii; +use yiiunit\TestCase; +use yii\db\Connection; +use yii\db\ActiveRecord; +use yii\behaviors\AutoTimestamp; + +/** + * Unit test for [[\yii\behaviors\AutoTimestamp]]. + * @see AutoTimestamp + * + * @group behaviors + */ +class AutoTimestampTest extends TestCase +{ + /** + * @var Connection test db connection + */ + protected $dbConnection; + + public static function setUpBeforeClass() + { + if (!extension_loaded('pdo') || !extension_loaded('pdo_sqlite')) { + static::markTestSkipped('PDO and SQLite extensions are required.'); + } + } + + public function setUp() + { + $this->mockApplication( + array( + 'components' => array( + 'db' => array( + 'class' => '\yii\db\Connection', + 'dsn' => 'sqlite::memory:', + ) + ) + ) + ); + + $columns = array( + 'id' => 'pk', + 'create_time' => 'integer', + 'update_time' => 'integer', + ); + Yii::$app->getDb()->createCommand()->createTable('test_auto_timestamp', $columns)->execute(); + } + + public function tearDown() + { + Yii::$app->getDb()->close(); + parent::tearDown(); + } + + // Tests : + + public function testNewRecord() + { + $currentTime = time(); + + $model = new ActiveRecordAutoTimestamp(); + $model->save(false); + + $this->assertTrue($model->create_time >= $currentTime); + $this->assertTrue($model->update_time >= $currentTime); + } + + /** + * @depends testNewRecord + */ + public function testUpdateRecord() + { + $currentTime = time(); + + $model = new ActiveRecordAutoTimestamp(); + $model->save(false); + + $enforcedTime = $currentTime - 100; + + $model->create_time = $enforcedTime; + $model->update_time = $enforcedTime; + $model->save(false); + + $this->assertEquals($enforcedTime, $model->create_time, 'Create time has been set on update!'); + $this->assertTrue($model->update_time >= $currentTime, 'Update time has NOT been set on update!'); + } +} + +/** + * Test Active Record class with [[AutoTimestamp]] behavior attached. + * + * @property integer $id + * @property integer $create_time + * @property integer $update_time + */ +class ActiveRecordAutoTimestamp extends ActiveRecord +{ + public function behaviors() + { + return array( + 'timestamp' => array( + 'class' => AutoTimestamp::className(), + 'attributes' => array( + static::EVENT_BEFORE_INSERT => array('create_time', 'update_time'), + static::EVENT_BEFORE_UPDATE => 'update_time', + ), + ), + ); + } + + public static function tableName() + { + return 'test_auto_timestamp'; + } +} diff --git a/tests/unit/framework/caching/ApcCacheTest.php b/tests/unit/framework/caching/ApcCacheTest.php index 6018ce7..adda151 100644 --- a/tests/unit/framework/caching/ApcCacheTest.php +++ b/tests/unit/framework/caching/ApcCacheTest.php @@ -1,12 +1,14 @@ <?php namespace yiiunit\framework\caching; + use yii\caching\ApcCache; -use yiiunit\TestCase; /** * Class for testing APC cache backend + * @group apc + * @group caching */ -class ApcCacheTest extends CacheTest +class ApcCacheTest extends CacheTestCase { private $_cacheInstance = null; @@ -21,9 +23,18 @@ class ApcCacheTest extends CacheTest $this->markTestSkipped("APC cli is not enabled. Skipping."); } + if (!ini_get("apc.enabled") || !ini_get("apc.enable_cli")) { + $this->markTestSkipped("APC is installed but not enabled. Skipping."); + } + if ($this->_cacheInstance === null) { $this->_cacheInstance = new ApcCache(); } return $this->_cacheInstance; } + + public function testExpire() + { + $this->markTestSkipped("APC keys are expiring only on the next request."); + } } diff --git a/tests/unit/framework/caching/CacheTest.php b/tests/unit/framework/caching/CacheTestCase.php similarity index 54% rename from tests/unit/framework/caching/CacheTest.php rename to tests/unit/framework/caching/CacheTestCase.php index f9db4f4..542999a 100644 --- a/tests/unit/framework/caching/CacheTest.php +++ b/tests/unit/framework/caching/CacheTestCase.php @@ -1,30 +1,88 @@ <?php + +namespace yii\caching; + +/** + * Mock for the time() function for caching classes + * @return int + */ +function time() +{ + return \yiiunit\framework\caching\CacheTestCase::$time ?: \time(); +} + namespace yiiunit\framework\caching; + +use yii\helpers\StringHelper; use yiiunit\TestCase; use yii\caching\Cache; /** * Base class for testing cache backends */ -abstract class CacheTest extends TestCase +abstract class CacheTestCase extends TestCase { /** + * @var int virtual time to be returned by mocked time() function. + * Null means normal time() behavior. + */ + public static $time; + + /** * @return Cache */ abstract protected function getCacheInstance(); + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + + protected function tearDown() + { + static::$time = null; + } + + /** + * @return Cache + */ + public function prepare() + { + $cache = $this->getCacheInstance(); + + $cache->flush(); + $cache->set('string_test', 'string_test'); + $cache->set('number_test', 42); + $cache->set('array_test', array('array_test' => 'array_test')); + $cache['arrayaccess_test'] = new \stdClass(); + + return $cache; + } + + /** + * default value of cache prefix is application id + */ + public function testKeyPrefix() + { + $cache = $this->getCacheInstance(); + $this->assertNotNull(\Yii::$app->id); + $this->assertNotNull($cache->keyPrefix); + } + public function testSet() { $cache = $this->getCacheInstance(); + $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(); } public function testGet() { - $cache = $this->getCacheInstance(); + $cache = $this->prepare(); + $this->assertEquals('string_test', $cache->get('string_test')); $this->assertEquals(42, $cache->get('number_test')); @@ -32,51 +90,94 @@ abstract class CacheTest extends TestCase $array = $cache->get('array_test'); $this->assertArrayHasKey('array_test', $array); $this->assertEquals('array_test', $array['array_test']); + } + + public function testExists() + { + $cache = $this->prepare(); + + $this->assertTrue($cache->exists('string_test')); + // check whether exists affects the value + $this->assertEquals('string_test', $cache->get('string_test')); + + $this->assertTrue($cache->exists('number_test')); + $this->assertFalse($cache->exists('not_exists')); + } + + public function testArrayAccess() + { + $cache = $this->getCacheInstance(); + $cache['arrayaccess_test'] = new \stdClass(); $this->assertInstanceOf('stdClass', $cache['arrayaccess_test']); } - public function testMget() + public function testGetNonExistent() { $cache = $this->getCacheInstance(); + + $this->assertFalse($cache->get('non_existent_key')); + } + + public function testStoreSpecialValues() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->set('null_value', null)); + $this->assertNull($cache->get('null_value')); + + $this->assertTrue($cache->set('bool_value', true)); + $this->assertTrue($cache->get('bool_value')); + } + + public function testMget() + { + $cache = $this->prepare(); + $this->assertEquals(array('string_test' => 'string_test', 'number_test' => 42), $cache->mget(array('string_test', 'number_test'))); + // ensure that order does not matter + $this->assertEquals(array('number_test' => 42, 'string_test' => 'string_test'), $cache->mget(array('number_test', 'string_test'))); + $this->assertEquals(array('number_test' => 42, 'non_existent_key' => null), $cache->mget(array('number_test', 'non_existent_key'))); } public function testExpire() { $cache = $this->getCacheInstance(); + $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); - sleep(1); + usleep(500000); $this->assertEquals('expire_test', $cache->get('expire_test')); - sleep(2); - $this->assertEquals(false, $cache->get('expire_test')); + usleep(2500000); + $this->assertFalse($cache->get('expire_test')); } public function testAdd() { - $cache = $this->getCacheInstance(); + $cache = $this->prepare(); // should not change existing keys $this->assertFalse($cache->add('number_test', 13)); $this->assertEquals(42, $cache->get('number_test')); - // should store data is it's not there yet + // should store data if it's not there yet + $this->assertFalse($cache->get('add_test')); $this->assertTrue($cache->add('add_test', 13)); $this->assertEquals(13, $cache->get('add_test')); } public function testDelete() { - $cache = $this->getCacheInstance(); + $cache = $this->prepare(); + $this->assertNotNull($cache->get('number_test')); $this->assertTrue($cache->delete('number_test')); - $this->assertEquals(null, $cache->get('number_test')); + $this->assertFalse($cache->get('number_test')); } public function testFlush() { - $cache = $this->getCacheInstance(); + $cache = $this->prepare(); $this->assertTrue($cache->flush()); - $this->assertEquals(null, $cache->get('add_test')); + $this->assertFalse($cache->get('number_test')); } } diff --git a/tests/unit/framework/caching/DbCacheTest.php b/tests/unit/framework/caching/DbCacheTest.php index 253240d..1e94d58 100644 --- a/tests/unit/framework/caching/DbCacheTest.php +++ b/tests/unit/framework/caching/DbCacheTest.php @@ -1,12 +1,15 @@ <?php + namespace yiiunit\framework\caching; + use yii\caching\DbCache; -use yiiunit\TestCase; /** * Class for testing file cache backend + * @group db + * @group caching */ -class DbCacheTest extends CacheTest +class DbCacheTest extends CacheTestCase { private $_cacheInstance; private $_connection; @@ -17,6 +20,8 @@ class DbCacheTest extends CacheTest $this->markTestSkipped('pdo and pdo_mysql extensions are required.'); } + parent::setUp(); + $this->getConnection()->createCommand(" CREATE TABLE IF NOT EXISTS tbl_cache ( id char(128) NOT NULL, @@ -32,11 +37,11 @@ class DbCacheTest extends CacheTest * @param bool $reset whether to clean up the test database * @return \yii\db\Connection */ - function getConnection($reset = true) + public function getConnection($reset = true) { - if($this->_connection === null) { + if ($this->_connection === null) { $databases = $this->getParam('databases'); - $params = $databases['mysql']; + $params = $databases['mysql']; $db = new \yii\db\Connection; $db->dsn = $params['dsn']; $db->username = $params['username']; @@ -61,11 +66,23 @@ class DbCacheTest extends CacheTest */ protected function getCacheInstance() { - if($this->_cacheInstance === null) { + if ($this->_cacheInstance === null) { $this->_cacheInstance = new DbCache(array( 'db' => $this->getConnection(), )); } return $this->_cacheInstance; } + + public function testExpire() + { + $cache = $this->getCacheInstance(); + + static::$time = \time(); + $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); + static::$time++; + $this->assertEquals('expire_test', $cache->get('expire_test')); + static::$time++; + $this->assertFalse($cache->get('expire_test')); + } } diff --git a/tests/unit/framework/caching/FileCacheTest.php b/tests/unit/framework/caching/FileCacheTest.php index 37d3222..263ecb4 100644 --- a/tests/unit/framework/caching/FileCacheTest.php +++ b/tests/unit/framework/caching/FileCacheTest.php @@ -1,12 +1,13 @@ <?php namespace yiiunit\framework\caching; + use yii\caching\FileCache; -use yiiunit\TestCase; /** * Class for testing file cache backend + * @group caching */ -class FileCacheTest extends CacheTest +class FileCacheTest extends CacheTestCase { private $_cacheInstance = null; @@ -15,11 +16,23 @@ class FileCacheTest extends CacheTest */ protected function getCacheInstance() { - if($this->_cacheInstance === null) { + if ($this->_cacheInstance === null) { $this->_cacheInstance = new FileCache(array( 'cachePath' => '@yiiunit/runtime/cache', )); } return $this->_cacheInstance; } + + public function testExpire() + { + $cache = $this->getCacheInstance(); + + static::$time = \time(); + $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); + static::$time++; + $this->assertEquals('expire_test', $cache->get('expire_test')); + static::$time++; + $this->assertFalse($cache->get('expire_test')); + } } diff --git a/tests/unit/framework/caching/MemCacheTest.php b/tests/unit/framework/caching/MemCacheTest.php index 40dba12..32374b5 100644 --- a/tests/unit/framework/caching/MemCacheTest.php +++ b/tests/unit/framework/caching/MemCacheTest.php @@ -1,12 +1,14 @@ <?php namespace yiiunit\framework\caching; + use yii\caching\MemCache; -use yiiunit\TestCase; /** * Class for testing memcache cache backend + * @group memcache + * @group caching */ -class MemCacheTest extends CacheTest +class MemCacheTest extends CacheTestCase { private $_cacheInstance = null; @@ -15,11 +17,11 @@ class MemCacheTest extends CacheTest */ protected function getCacheInstance() { - if(!extension_loaded("memcache")) { + if (!extension_loaded("memcache")) { $this->markTestSkipped("memcache not installed. Skipping."); } - if($this->_cacheInstance === null) { + if ($this->_cacheInstance === null) { $this->_cacheInstance = new MemCache(); } return $this->_cacheInstance; diff --git a/tests/unit/framework/caching/MemCachedTest.php b/tests/unit/framework/caching/MemCachedTest.php index c9e437c..f39ed17 100644 --- a/tests/unit/framework/caching/MemCachedTest.php +++ b/tests/unit/framework/caching/MemCachedTest.php @@ -1,12 +1,14 @@ <?php namespace yiiunit\framework\caching; + use yii\caching\MemCache; -use yiiunit\TestCase; /** - * Class for testing memcache cache backend + * Class for testing memcached cache backend + * @group memcached + * @group caching */ -class MemCachedTest extends CacheTest +class MemCachedTest extends CacheTestCase { private $_cacheInstance = null; @@ -15,11 +17,11 @@ class MemCachedTest extends CacheTest */ protected function getCacheInstance() { - if(!extension_loaded("memcached")) { + if (!extension_loaded("memcached")) { $this->markTestSkipped("memcached not installed. Skipping."); } - if($this->_cacheInstance === null) { + if ($this->_cacheInstance === null) { $this->_cacheInstance = new MemCache(array( 'useMemcached' => true, )); diff --git a/tests/unit/framework/caching/RedisCacheTest.php b/tests/unit/framework/caching/RedisCacheTest.php new file mode 100644 index 0000000..d02773d --- /dev/null +++ b/tests/unit/framework/caching/RedisCacheTest.php @@ -0,0 +1,85 @@ +<?php +namespace yiiunit\framework\caching; +use yii\caching\MemCache; +use yii\caching\RedisCache; +use yiiunit\TestCase; + +/** + * Class for testing redis cache backend + * @group redis + * @group caching + */ +class RedisCacheTest extends CacheTestCase +{ + private $_cacheInstance = null; + + /** + * @return MemCache + */ + protected function getCacheInstance() + { + $config = array( + 'hostname' => 'localhost', + 'port' => 6379, + 'database' => 0, + 'dataTimeout' => 0.1, + ); + $dsn = $config['hostname'] . ':' .$config['port']; + if(!@stream_socket_client($dsn, $errorNumber, $errorDescription, 0.5)) { + $this->markTestSkipped('No redis server running at ' . $dsn .' : ' . $errorNumber . ' - ' . $errorDescription); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new RedisCache($config); + } + return $this->_cacheInstance; + } + + public function testExpireMilliseconds() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->set('expire_test_ms', 'expire_test_ms', 0.2)); + usleep(100000); + $this->assertEquals('expire_test_ms', $cache->get('expire_test_ms')); + usleep(300000); + $this->assertFalse($cache->get('expire_test_ms')); + } + + /** + * Store a value that is 2 times buffer size big + * https://github.com/yiisoft/yii2/issues/743 + */ + public function testLargeData() + { + $cache = $this->getCacheInstance(); + + $data=str_repeat('XX',8192); // http://www.php.net/manual/en/function.fread.php + $key='bigdata1'; + + $this->assertFalse($cache->get($key)); + $cache->set($key,$data); + $this->assertTrue($cache->get($key)===$data); + + // try with multibyte string + $data=str_repeat('ЖЫ',8192); // http://www.php.net/manual/en/function.fread.php + $key='bigdata2'; + + $this->assertFalse($cache->get($key)); + $cache->set($key,$data); + $this->assertTrue($cache->get($key)===$data); + } + + public function testMultiByteGetAndSet() + { + $cache = $this->getCacheInstance(); + + $data=array('abc'=>'ежик',2=>'def'); + $key='data1'; + + $this->assertFalse($cache->get($key)); + $cache->set($key,$data); + $this->assertTrue($cache->get($key)===$data); + } + +} \ No newline at end of file diff --git a/tests/unit/framework/caching/WinCacheTest.php b/tests/unit/framework/caching/WinCacheTest.php index c9470bd..1bce102 100644 --- a/tests/unit/framework/caching/WinCacheTest.php +++ b/tests/unit/framework/caching/WinCacheTest.php @@ -1,12 +1,14 @@ <?php namespace yiiunit\framework\caching; -use yii\caching\FileCache; -use yiiunit\TestCase; + +use yii\caching\WinCache; /** * Class for testing wincache backend + * @group wincache + * @group caching */ -class WinCacheTest extends CacheTest +class WinCacheTest extends CacheTestCase { private $_cacheInstance = null; @@ -15,15 +17,15 @@ class WinCacheTest extends CacheTest */ protected function getCacheInstance() { - if(!extension_loaded('wincache')) { + if (!extension_loaded('wincache')) { $this->markTestSkipped("Wincache not installed. Skipping."); } - if(!ini_get('wincache.ucenabled')) { + if (!ini_get('wincache.ucenabled')) { $this->markTestSkipped("Wincache user cache disabled. Skipping."); } - if($this->_cacheInstance === null) { + if ($this->_cacheInstance === null) { $this->_cacheInstance = new WinCache(); } return $this->_cacheInstance; diff --git a/tests/unit/framework/caching/XCacheTest.php b/tests/unit/framework/caching/XCacheTest.php index b5e41a6..989765d 100644 --- a/tests/unit/framework/caching/XCacheTest.php +++ b/tests/unit/framework/caching/XCacheTest.php @@ -1,12 +1,14 @@ <?php namespace yiiunit\framework\caching; + use yii\caching\XCache; -use yiiunit\TestCase; /** * Class for testing xcache backend + * @group xcache + * @group caching */ -class XCacheTest extends CacheTest +class XCacheTest extends CacheTestCase { private $_cacheInstance = null; @@ -15,11 +17,11 @@ class XCacheTest extends CacheTest */ protected function getCacheInstance() { - if(!function_exists("xcache_isset")) { + if (!function_exists("xcache_isset")) { $this->markTestSkipped("XCache not installed. Skipping."); } - if($this->_cacheInstance === null) { + if ($this->_cacheInstance === null) { $this->_cacheInstance = new XCache(); } return $this->_cacheInstance; diff --git a/tests/unit/framework/caching/ZendDataCacheTest.php b/tests/unit/framework/caching/ZendDataCacheTest.php index 86c06c8..96354cd 100644 --- a/tests/unit/framework/caching/ZendDataCacheTest.php +++ b/tests/unit/framework/caching/ZendDataCacheTest.php @@ -1,12 +1,15 @@ <?php namespace yiiunit\framework\caching; + +use yii\caching\Cache; use yii\caching\ZendDataCache; -use yiiunit\TestCase; /** * Class for testing Zend cache backend + * @group zenddata + * @group caching */ -class ZendDataCacheTest extends CacheTest +class ZendDataCacheTest extends CacheTestCase { private $_cacheInstance = null; @@ -15,11 +18,11 @@ class ZendDataCacheTest extends CacheTest */ protected function getCacheInstance() { - if(!function_exists("zend_shm_cache_store")) { + if (!function_exists("zend_shm_cache_store")) { $this->markTestSkipped("Zend Data cache not installed. Skipping."); } - if($this->_cacheInstance === null) { + if ($this->_cacheInstance === null) { $this->_cacheInstance = new ZendDataCache(); } return $this->_cacheInstance; diff --git a/tests/unit/framework/console/controllers/AssetControllerTest.php b/tests/unit/framework/console/controllers/AssetControllerTest.php new file mode 100644 index 0000000..67dbdaa --- /dev/null +++ b/tests/unit/framework/console/controllers/AssetControllerTest.php @@ -0,0 +1,319 @@ +<?php + +use yiiunit\TestCase; +use yii\console\controllers\AssetController; + +/** + * Unit test for [[\yii\console\controllers\AssetController]]. + * @see AssetController + * + * @group console + */ +class AssetControllerTest extends TestCase +{ + /** + * @var string path for the test files. + */ + protected $testFilePath = ''; + /** + * @var string test assets path. + */ + protected $testAssetsBasePath = ''; + + public function setUp() + { + $this->mockApplication(); + $this->testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . get_class($this); + $this->createDir($this->testFilePath); + $this->testAssetsBasePath = $this->testFilePath . DIRECTORY_SEPARATOR . 'assets'; + $this->createDir($this->testAssetsBasePath); + } + + public function tearDown() + { + $this->removeDir($this->testFilePath); + } + + /** + * Creates directory. + * @param string $dirName directory full name. + */ + protected function createDir($dirName) + { + if (!file_exists($dirName)) { + mkdir($dirName, 0777, true); + } + } + + /** + * Removes directory. + * @param string $dirName directory full name + */ + protected function removeDir($dirName) + { + if (!empty($dirName) && file_exists($dirName)) { + exec("rm -rf {$dirName}"); + } + } + + /** + * Creates test asset controller instance. + * @return AssetController + */ + protected function createAssetController() + { + $module = $this->getMock('yii\\base\\Module', array('fake'), array('console')); + $assetController = new AssetController('asset', $module); + $assetController->interactive = false; + $assetController->jsCompressor = 'cp {from} {to}'; + $assetController->cssCompressor = 'cp {from} {to}'; + return $assetController; + } + + /** + * Emulates running of the asset controller action. + * @param string $actionId id of action to be run. + * @param array $args action arguments. + * @return string command output. + */ + protected function runAssetControllerAction($actionId, array $args = array()) + { + $controller = $this->createAssetController(); + ob_start(); + ob_implicit_flush(false); + $controller->run($actionId, $args); + return ob_get_clean(); + } + + /** + * Creates test compress config. + * @param array[] $bundles asset bundles config. + * @return array config array. + */ + protected function createCompressConfig(array $bundles) + { + $baseUrl = '/test'; + $config = array( + 'bundles' => $this->createBundleConfig($bundles), + 'targets' => array( + 'all' => array( + 'basePath' => $this->testAssetsBasePath, + 'baseUrl' => $baseUrl, + 'js' => 'all.js', + 'css' => 'all.css', + ), + ), + 'assetManager' => array( + 'basePath' => $this->testAssetsBasePath, + 'baseUrl' => '', + ), + ); + return $config; + } + + /** + * Creates test bundle configuration. + * @param array[] $bundles asset bundles config. + * @return array bundle config. + */ + protected function createBundleConfig(array $bundles) + { + foreach ($bundles as $name => $config) { + if (!array_key_exists('basePath', $config)) { + $bundles[$name]['basePath'] = $this->testFilePath; + } + if (!array_key_exists('baseUrl', $config)) { + $bundles[$name]['baseUrl'] = ''; + } + } + return $bundles; + } + + /** + * Creates test compress config file. + * @param string $fileName output file name. + * @param array[] $bundles asset bundles config. + * @throws Exception on failure. + */ + protected function createCompressConfigFile($fileName, array $bundles) + { + $content = '<?php return ' . var_export($this->createCompressConfig($bundles), true) . ';'; + if (file_put_contents($fileName, $content) <= 0) { + throw new \Exception("Unable to create file '{$fileName}'!"); + } + } + + /** + * Creates test asset file. + * @param string $fileRelativeName file name relative to [[testFilePath]] + * @param string $content file content + * @throws Exception on failure. + */ + protected function createAssetSourceFile($fileRelativeName, $content) + { + $fileFullName = $this->testFilePath . DIRECTORY_SEPARATOR . $fileRelativeName; + $this->createDir(dirname($fileFullName)); + if (file_put_contents($fileFullName, $content) <= 0) { + throw new \Exception("Unable to create file '{$fileFullName}'!"); + } + } + + /** + * Creates a list of asset source files. + * @param array $files assert source files in format: file/relative/name => fileContent + */ + protected function createAssetSourceFiles(array $files) + { + foreach ($files as $name => $content) { + $this->createAssetSourceFile($name, $content); + } + } + + /** + * Invokes the asset controller method even if it is protected. + * @param string $methodName name of the method to be invoked. + * @param array $args method arguments. + * @return mixed method invoke result. + */ + protected function invokeAssetControllerMethod($methodName, array $args = array()) + { + $controller = $this->createAssetController(); + $controllerClassReflection = new ReflectionClass(get_class($controller)); + $methodReflection = $controllerClassReflection->getMethod($methodName); + $methodReflection->setAccessible(true); + $result = $methodReflection->invokeArgs($controller, $args); + $methodReflection->setAccessible(false); + return $result; + } + + // Tests : + + public function testActionTemplate() + { + $configFileName = $this->testFilePath . DIRECTORY_SEPARATOR . 'config.php'; + $this->runAssetControllerAction('template', array($configFileName)); + $this->assertTrue(file_exists($configFileName), 'Unable to create config file template!'); + } + + public function atestActionCompress() + { + // Given : + $cssFiles = array( + 'css/test_body.css' => 'body { + padding-top: 20px; + padding-bottom: 60px; + }', + 'css/test_footer.css' => '.footer { + margin: 20px; + display: block; + }', + ); + $this->createAssetSourceFiles($cssFiles); + + $jsFiles = array( + 'js/test_alert.js' => "function test() { + alert('Test message'); + }", + 'js/test_sum_ab.js' => "function sumAB(a, b) { + return a + b; + }", + ); + $this->createAssetSourceFiles($jsFiles); + + $bundles = array( + 'app' => array( + 'css' => array_keys($cssFiles), + 'js' => array_keys($jsFiles), + 'depends' => array( + 'yii', + ), + ), + ); + $bundleFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'bundle.php'; + + $configFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'config.php'; + $this->createCompressConfigFile($configFile, $bundles); + + // When : + $this->runAssetControllerAction('compress', array($configFile, $bundleFile)); + + // Then : + $this->assertTrue(file_exists($bundleFile), 'Unable to create output bundle file!'); + $this->assertTrue(is_array(require($bundleFile)), 'Output bundle file has incorrect format!'); + + $compressedCssFileName = $this->testAssetsBasePath . DIRECTORY_SEPARATOR . 'all.css'; + $this->assertTrue(file_exists($compressedCssFileName), 'Unable to compress CSS files!'); + $compressedJsFileName = $this->testAssetsBasePath . DIRECTORY_SEPARATOR . 'all.js'; + $this->assertTrue(file_exists($compressedJsFileName), 'Unable to compress JS files!'); + + $compressedCssFileContent = file_get_contents($compressedCssFileName); + foreach ($cssFiles as $name => $content) { + $this->assertContains($content, $compressedCssFileContent, "Source of '{$name}' is missing in combined file!"); + } + $compressedJsFileContent = file_get_contents($compressedJsFileName); + foreach ($jsFiles as $name => $content) { + $this->assertContains($content, $compressedJsFileContent, "Source of '{$name}' is missing in combined file!"); + } + } + + /** + * Data provider for [[testAdjustCssUrl()]]. + * @return array test data. + */ + public function adjustCssUrlDataProvider() + { + return array( + array( + '.published-same-dir-class {background-image: url(published_same_dir.png);}', + '/test/base/path/assets/input', + '/test/base/path/assets/output', + '.published-same-dir-class {background-image: url(../input/published_same_dir.png);}', + ), + array( + '.published-relative-dir-class {background-image: url(../img/published_relative_dir.png);}', + '/test/base/path/assets/input', + '/test/base/path/assets/output', + '.published-relative-dir-class {background-image: url(../img/published_relative_dir.png);}', + ), + array( + '.static-same-dir-class {background-image: url(\'static_same_dir.png\');}', + '/test/base/path/css', + '/test/base/path/assets/output', + '.static-same-dir-class {background-image: url(\'../../css/static_same_dir.png\');}', + ), + array( + '.static-relative-dir-class {background-image: url("../img/static_relative_dir.png");}', + '/test/base/path/css', + '/test/base/path/assets/output', + '.static-relative-dir-class {background-image: url("../../img/static_relative_dir.png");}', + ), + array( + '.absolute-url-class {background-image: url(http://domain.com/img/image.gif);}', + '/test/base/path/assets/input', + '/test/base/path/assets/output', + '.absolute-url-class {background-image: url(http://domain.com/img/image.gif);}', + ), + array( + '.absolute-url-secure-class {background-image: url(https://secure.domain.com/img/image.gif);}', + '/test/base/path/assets/input', + '/test/base/path/assets/output', + '.absolute-url-secure-class {background-image: url(https://secure.domain.com/img/image.gif);}', + ), + ); + } + + /** + * @dataProvider adjustCssUrlDataProvider + * + * @param $cssContent + * @param $inputFilePath + * @param $outputFilePath + * @param $expectedCssContent + */ + public function testAdjustCssUrl($cssContent, $inputFilePath, $outputFilePath, $expectedCssContent) + { + $adjustedCssContent = $this->invokeAssetControllerMethod('adjustCssUrl', array($cssContent, $inputFilePath, $outputFilePath)); + + $this->assertEquals($expectedCssContent, $adjustedCssContent, 'Unable to adjust CSS correctly!'); + } +} diff --git a/tests/unit/framework/console/controllers/MessageControllerTest.php b/tests/unit/framework/console/controllers/MessageControllerTest.php new file mode 100644 index 0000000..b0c697c --- /dev/null +++ b/tests/unit/framework/console/controllers/MessageControllerTest.php @@ -0,0 +1,369 @@ +<?php + +use yiiunit\TestCase; +use yii\console\controllers\MessageController; + +/** + * Unit test for [[\yii\console\controllers\MessageController]]. + * @see MessageController + * + * @group console + */ +class MessageControllerTest extends TestCase +{ + protected $sourcePath = ''; + protected $messagePath = ''; + protected $configFileName = ''; + + public function setUp() + { + $this->mockApplication(); + $this->sourcePath = Yii::getAlias('@yiiunit/runtime/test_source'); + $this->createDir($this->sourcePath); + if (!file_exists($this->sourcePath)) { + $this->markTestIncomplete('Unit tests runtime directory should have writable permissions!'); + } + $this->messagePath = Yii::getAlias('@yiiunit/runtime/test_messages'); + $this->createDir($this->messagePath); + $this->configFileName = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . 'message_controller_test_config.php'; + } + + public function tearDown() + { + $this->removeDir($this->sourcePath); + $this->removeDir($this->messagePath); + if (file_exists($this->configFileName)) { + unlink($this->configFileName); + } + } + + /** + * Creates directory. + * @param $dirName directory full name + */ + protected function createDir($dirName) + { + if (!file_exists($dirName)) { + mkdir($dirName, 0777, true); + } + } + + /** + * Removes directory. + * @param $dirName directory full name + */ + protected function removeDir($dirName) + { + if (!empty($dirName) && file_exists($dirName)) { + $this->removeFileSystemObject($dirName); + } + } + + /** + * Removes file system object: directory or file. + * @param string $fileSystemObjectFullName file system object full name. + */ + protected function removeFileSystemObject($fileSystemObjectFullName) + { + if (!is_dir($fileSystemObjectFullName)) { + unlink($fileSystemObjectFullName); + } else { + $dirHandle = opendir($fileSystemObjectFullName); + while (($fileSystemObjectName = readdir($dirHandle)) !== false) { + if ($fileSystemObjectName==='.' || $fileSystemObjectName==='..') { + continue; + } + $this->removeFileSystemObject($fileSystemObjectFullName . DIRECTORY_SEPARATOR . $fileSystemObjectName); + } + closedir($dirHandle); + rmdir($fileSystemObjectFullName); + } + } + + /** + * Creates test message controller instance. + * @return MessageController message command instance. + */ + protected function createMessageController() + { + $module = $this->getMock('yii\\base\\Module', array('fake'), array('console')); + $messageController = new MessageController('message', $module); + $messageController->interactive = false; + return $messageController; + } + + /** + * Emulates running of the message controller action. + * @param string $actionId id of action to be run. + * @param array $args action arguments. + * @return string command output. + */ + protected function runMessageControllerAction($actionId, array $args = array()) + { + $controller = $this->createMessageController(); + ob_start(); + ob_implicit_flush(false); + $controller->run($actionId, $args); + return ob_get_clean(); + } + + /** + * Creates message command config file at {@link configFileName} + * @param array $config message command config. + */ + protected function composeConfigFile(array $config) + { + if (file_exists($this->configFileName)) { + unlink($this->configFileName); + } + $fileContent = '<?php return ' . var_export($config, true) . ';'; + file_put_contents($this->configFileName, $fileContent); + } + + /** + * Creates source file with given content + * @param string $content file content + * @param string|null $name file self name + */ + protected function createSourceFile($content, $name = null) + { + if (empty($name)) { + $name = md5(uniqid()) . '.php'; + } + file_put_contents($this->sourcePath . DIRECTORY_SEPARATOR . $name, $content); + } + + /** + * Creates message file with given messages. + * @param string $name file name + * @param array $messages messages. + */ + protected function createMessageFile($name, array $messages = array()) + { + $fileName = $this->messagePath . DIRECTORY_SEPARATOR . $name; + if (file_exists($fileName)) { + unlink($fileName); + } else { + $dirName = dirname($fileName); + if (!file_exists($dirName)) { + mkdir($dirName, 0777, true); + } + } + $fileContent = '<?php return ' . var_export($messages, true) . ';'; + file_put_contents($fileName, $fileContent); + } + + // Tests: + + public function testActionConfig() + { + $configFileName = $this->configFileName; + $this->runMessageControllerAction('config', array($configFileName)); + $this->assertTrue(file_exists($configFileName), 'Unable to create config file template!'); + } + + public function testConfigFileNotExist() + { + $this->setExpectedException('yii\\console\\Exception'); + $this->runMessageControllerAction('extract', array('not_existing_file.php')); + } + + public function testCreateTranslation() + { + $language = 'en'; + + $category = 'test_category'; + $message = 'test message'; + $sourceFileContent = "Yii::t('{$category}', '{$message}')"; + $this->createSourceFile($sourceFileContent); + + $this->composeConfigFile(array( + 'languages' => array($language), + 'sourcePath' => $this->sourcePath, + 'messagePath' => $this->messagePath, + )); + $this->runMessageControllerAction('extract', array($this->configFileName)); + + $this->assertTrue(file_exists($this->messagePath . DIRECTORY_SEPARATOR . $language), 'No language dir created!'); + $messageFileName = $this->messagePath . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php'; + $this->assertTrue(file_exists($messageFileName), 'No message file created!'); + $messages = require($messageFileName); + $this->assertTrue(is_array($messages), 'Unable to compose messages!'); + $this->assertTrue(array_key_exists($message, $messages), 'Source message is missing!'); + } + + /** + * @depends testCreateTranslation + */ + public function testNothingNew() + { + $language = 'en'; + + $category = 'test_category'; + $message = 'test message'; + $sourceFileContent = "Yii::t('{$category}', '{$message}')"; + $this->createSourceFile($sourceFileContent); + + $this->composeConfigFile(array( + 'languages' => array($language), + 'sourcePath' => $this->sourcePath, + 'messagePath' => $this->messagePath, + )); + $this->runMessageControllerAction('extract', array($this->configFileName)); + + $messageFileName = $this->messagePath . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php'; + + // check file not overwritten: + $messageFileContent = file_get_contents($messageFileName); + $messageFileContent .= '// some not generated by command content'; + file_put_contents($messageFileName, $messageFileContent); + + $this->runMessageControllerAction('extract', array($this->configFileName)); + + $this->assertEquals($messageFileContent, file_get_contents($messageFileName)); + } + + /** + * @depends testCreateTranslation + */ + public function testMerge() + { + $language = 'en'; + $category = 'test_category'; + $messageFileName = $language . DIRECTORY_SEPARATOR . $category . '.php'; + + $existingMessage = 'test existing message'; + $existingMessageContent = 'test existing message content'; + $this->createMessageFile($messageFileName, array( + $existingMessage => $existingMessageContent + )); + + $newMessage = 'test new message'; + $sourceFileContent = "Yii::t('{$category}', '{$existingMessage}')"; + $sourceFileContent .= "Yii::t('{$category}', '{$newMessage}')"; + $this->createSourceFile($sourceFileContent); + + $this->composeConfigFile(array( + 'languages' => array($language), + 'sourcePath' => $this->sourcePath, + 'messagePath' => $this->messagePath, + 'overwrite' => true, + )); + $this->runMessageControllerAction('extract', array($this->configFileName)); + + $messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName); + $this->assertTrue(array_key_exists($newMessage, $messages), 'Unable to add new message!'); + $this->assertTrue(array_key_exists($existingMessage, $messages), 'Unable to keep existing message!'); + $this->assertEquals('', $messages[$newMessage], 'Wrong new message content!'); + $this->assertEquals($existingMessageContent, $messages[$existingMessage], 'Unable to keep existing message content!'); + } + + /** + * @depends testMerge + */ + public function testNoLongerNeedTranslation() + { + $language = 'en'; + $category = 'test_category'; + $messageFileName = $language . DIRECTORY_SEPARATOR . $category . '.php'; + + $oldMessage = 'test old message'; + $oldMessageContent = 'test old message content'; + $this->createMessageFile($messageFileName, array( + $oldMessage => $oldMessageContent + )); + + $sourceFileContent = "Yii::t('{$category}', 'some new message')"; + $this->createSourceFile($sourceFileContent); + + $this->composeConfigFile(array( + 'languages' => array($language), + 'sourcePath' => $this->sourcePath, + 'messagePath' => $this->messagePath, + 'overwrite' => true, + 'removeUnused' => false, + )); + $this->runMessageControllerAction('extract', array($this->configFileName)); + + $messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName); + + $this->assertTrue(array_key_exists($oldMessage, $messages), 'No longer needed message removed!'); + $this->assertEquals('@@' . $oldMessageContent . '@@', $messages[$oldMessage], 'No longer needed message content does not marked properly!'); + } + + /** + * @depends testMerge + */ + public function testMergeWithContentZero() + { + $language = 'en'; + $category = 'test_category'; + $messageFileName = $language . DIRECTORY_SEPARATOR . $category . '.php'; + + $zeroMessage = 'test zero message'; + $zeroMessageContent = '0'; + $falseMessage = 'test false message'; + $falseMessageContent = 'false'; + $this->createMessageFile($messageFileName, array( + $zeroMessage => $zeroMessageContent, + $falseMessage => $falseMessageContent, + )); + + $newMessage = 'test new message'; + $sourceFileContent = "Yii::t('{$category}', '{$zeroMessage}')"; + $sourceFileContent .= "Yii::t('{$category}', '{$falseMessage}')"; + $sourceFileContent .= "Yii::t('{$category}', '{$newMessage}')"; + $this->createSourceFile($sourceFileContent); + + $this->composeConfigFile(array( + 'languages' => array($language), + 'sourcePath' => $this->sourcePath, + 'messagePath' => $this->messagePath, + 'overwrite' => true, + )); + $this->runMessageControllerAction('extract', array($this->configFileName)); + + $messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName); + $this->assertTrue($zeroMessageContent === $messages[$zeroMessage], 'Message content "0" is lost!'); + $this->assertTrue($falseMessageContent === $messages[$falseMessage], 'Message content "false" is lost!'); + } + + /** + * @depends testCreateTranslation + */ + public function testMultiplyTranslators() + { + $language = 'en'; + $category = 'test_category'; + + $translators = array( + 'Yii::t', + 'Custom::translate', + ); + + $sourceMessages = array( + 'first message', + 'second message', + ); + $sourceFileContent = ''; + foreach ($sourceMessages as $key => $message) { + $sourceFileContent .= $translators[$key] . "('{$category}', '{$message}');\n"; + } + $this->createSourceFile($sourceFileContent); + + $this->composeConfigFile(array( + 'languages' => array($language), + 'sourcePath' => $this->sourcePath, + 'messagePath' => $this->messagePath, + 'translator' => $translators, + )); + $this->runMessageControllerAction('extract', array($this->configFileName)); + + $messageFileName = $this->messagePath . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php'; + $messages = require($messageFileName); + + foreach ($sourceMessages as $sourceMessage) { + $this->assertTrue(array_key_exists($sourceMessage, $messages)); + } + } +} diff --git a/tests/unit/framework/data/ActiveDataProviderTest.php b/tests/unit/framework/data/ActiveDataProviderTest.php new file mode 100644 index 0000000..276c525 --- /dev/null +++ b/tests/unit/framework/data/ActiveDataProviderTest.php @@ -0,0 +1,106 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yiiunit\framework\data; + +use yii\data\ActiveDataProvider; +use yii\db\Query; +use yiiunit\data\ar\ActiveRecord; +use yiiunit\framework\db\DatabaseTestCase; +use yiiunit\data\ar\Order; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + * + * @group data + */ +class ActiveDataProviderTest extends DatabaseTestCase +{ + protected function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + } + + public function testActiveQuery() + { + $provider = new ActiveDataProvider(array( + 'query' => Order::find()->orderBy('id'), + )); + $orders = $provider->getModels(); + $this->assertEquals(3, count($orders)); + $this->assertTrue($orders[0] instanceof Order); + $this->assertEquals(array(1, 2, 3), $provider->getKeys()); + + $provider = new ActiveDataProvider(array( + 'query' => Order::find(), + 'pagination' => array( + 'pageSize' => 2, + ) + )); + $orders = $provider->getModels(); + $this->assertEquals(2, count($orders)); + } + + public function testQuery() + { + $query = new Query; + $provider = new ActiveDataProvider(array( + 'db' => $this->getConnection(), + 'query' => $query->from('tbl_order')->orderBy('id'), + )); + $orders = $provider->getModels(); + $this->assertEquals(3, count($orders)); + $this->assertTrue(is_array($orders[0])); + $this->assertEquals(array(0, 1, 2), $provider->getKeys()); + + $query = new Query; + $provider = new ActiveDataProvider(array( + 'db' => $this->getConnection(), + 'query' => $query->from('tbl_order'), + 'pagination' => array( + 'pageSize' => 2, + ) + )); + $orders = $provider->getModels(); + $this->assertEquals(2, count($orders)); + } + + public function testRefresh() + { + $query = new Query; + $provider = new ActiveDataProvider(array( + 'db' => $this->getConnection(), + 'query' => $query->from('tbl_order')->orderBy('id'), + )); + $this->assertEquals(3, count($provider->getModels())); + + $provider->getPagination()->pageSize = 2; + $this->assertEquals(3, count($provider->getModels())); + $provider->refresh(); + $this->assertEquals(2, count($provider->getModels())); + } + + public function testPaginationBeforeModels() + { + $query = new Query; + $provider = new ActiveDataProvider(array( + 'db' => $this->getConnection(), + 'query' => $query->from('tbl_order')->orderBy('id'), + )); + $pagination = $provider->getPagination(); + $this->assertEquals(0, $pagination->getPageCount()); + $this->assertCount(3, $provider->getModels()); + $this->assertEquals(1, $pagination->getPageCount()); + + $provider->getPagination()->pageSize = 2; + $this->assertEquals(3, count($provider->getModels())); + $provider->refresh(); + $this->assertEquals(2, count($provider->getModels())); + } +} diff --git a/tests/unit/framework/data/SortTest.php b/tests/unit/framework/data/SortTest.php new file mode 100644 index 0000000..c4cc6aa --- /dev/null +++ b/tests/unit/framework/data/SortTest.php @@ -0,0 +1,172 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yiiunit\framework\data; + +use yii\web\UrlManager; +use yiiunit\TestCase; +use yii\data\Sort; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + * + * @group data + */ +class SortTest extends TestCase +{ + public function testGetOrders() + { + $sort = new Sort(array( + 'attributes' => array( + 'age', + 'name' => array( + 'asc' => array('first_name' => Sort::ASC, 'last_name' => Sort::ASC), + 'desc' => array('first_name' => Sort::DESC, 'last_name' => Sort::DESC), + ), + ), + 'params' => array( + 'sort' => 'age.name-desc' + ), + 'enableMultiSort' => true, + )); + + $orders = $sort->getOrders(); + $this->assertEquals(3, count($orders)); + $this->assertEquals(Sort::ASC, $orders['age']); + $this->assertEquals(Sort::DESC, $orders['first_name']); + $this->assertEquals(Sort::DESC, $orders['last_name']); + + $sort->enableMultiSort = false; + $orders = $sort->getOrders(true); + $this->assertEquals(1, count($orders)); + $this->assertEquals(Sort::ASC, $orders['age']); + } + + public function testGetAttributeOrders() + { + $sort = new Sort(array( + 'attributes' => array( + 'age', + 'name' => array( + 'asc' => array('first_name' => Sort::ASC, 'last_name' => Sort::ASC), + 'desc' => array('first_name' => Sort::DESC, 'last_name' => Sort::DESC), + ), + ), + 'params' => array( + 'sort' => 'age.name-desc' + ), + 'enableMultiSort' => true, + )); + + $orders = $sort->getAttributeOrders(); + $this->assertEquals(2, count($orders)); + $this->assertEquals(Sort::ASC, $orders['age']); + $this->assertEquals(Sort::DESC, $orders['name']); + + $sort->enableMultiSort = false; + $orders = $sort->getAttributeOrders(true); + $this->assertEquals(1, count($orders)); + $this->assertEquals(Sort::ASC, $orders['age']); + } + + public function testGetAttributeOrder() + { + $sort = new Sort(array( + 'attributes' => array( + 'age', + 'name' => array( + 'asc' => array('first_name' => Sort::ASC, 'last_name' => Sort::ASC), + 'desc' => array('first_name' => Sort::DESC, 'last_name' => Sort::DESC), + ), + ), + 'params' => array( + 'sort' => 'age.name-desc' + ), + 'enableMultiSort' => true, + )); + + $this->assertEquals(Sort::ASC, $sort->getAttributeOrder('age')); + $this->assertEquals(Sort::DESC, $sort->getAttributeOrder('name')); + $this->assertNull($sort->getAttributeOrder('xyz')); + } + + public function testCreateSortVar() + { + $sort = new Sort(array( + 'attributes' => array( + 'age', + 'name' => array( + 'asc' => array('first_name' => Sort::ASC, 'last_name' => Sort::ASC), + 'desc' => array('first_name' => Sort::DESC, 'last_name' => Sort::DESC), + ), + ), + 'params' => array( + 'sort' => 'age.name-desc' + ), + 'enableMultiSort' => true, + 'route' => 'site/index', + )); + + $this->assertEquals('age-desc.name-desc', $sort->createSortVar('age')); + $this->assertEquals('name.age', $sort->createSortVar('name')); + } + + public function testCreateUrl() + { + $manager = new UrlManager(array( + 'baseUrl' => '/index.php', + 'cache' => null, + )); + + $sort = new Sort(array( + 'attributes' => array( + 'age', + 'name' => array( + 'asc' => array('first_name' => Sort::ASC, 'last_name' => Sort::ASC), + 'desc' => array('first_name' => Sort::DESC, 'last_name' => Sort::DESC), + ), + ), + 'params' => array( + 'sort' => 'age.name-desc' + ), + 'enableMultiSort' => true, + 'urlManager' => $manager, + 'route' => 'site/index', + )); + + $this->assertEquals('/index.php?r=site/index&sort=age-desc.name-desc', $sort->createUrl('age')); + $this->assertEquals('/index.php?r=site/index&sort=name.age', $sort->createUrl('name')); + } + + public function testLink() + { + $this->mockApplication(); + $manager = new UrlManager(array( + 'baseUrl' => '/index.php', + 'cache' => null, + )); + + $sort = new Sort(array( + 'attributes' => array( + 'age', + 'name' => array( + 'asc' => array('first_name' => Sort::ASC, 'last_name' => Sort::ASC), + 'desc' => array('first_name' => Sort::DESC, 'last_name' => Sort::DESC), + ), + ), + 'params' => array( + 'sort' => 'age.name-desc' + ), + 'enableMultiSort' => true, + 'urlManager' => $manager, + 'route' => 'site/index', + )); + + $this->assertEquals('<a class="asc" href="/index.php?r=site/index&sort=age-desc.name-desc" data-sort="age-desc.name-desc">Age</a>', $sort->link('age')); + } +} diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index e337a5d..2f9b345 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -1,5 +1,4 @@ <?php - namespace yiiunit\framework\db; use yii\db\Query; @@ -10,11 +9,15 @@ use yiiunit\data\ar\OrderItem; use yiiunit\data\ar\Order; use yiiunit\data\ar\Item; -class ActiveRecordTest extends \yiiunit\DatabaseTestCase +/** + * @group db + * @group mysql + */ +class ActiveRecordTest extends DatabaseTestCase { - public function setUp() + protected function setUp() { - parent::setUp(); + parent::setUp(); ActiveRecord::$db = $this->getConnection(); } @@ -84,6 +87,15 @@ class ActiveRecordTest extends \yiiunit\DatabaseTestCase $this->assertTrue($customers['user1'] instanceof Customer); $this->assertTrue($customers['user2'] instanceof Customer); $this->assertTrue($customers['user3'] instanceof Customer); + + // indexBy callable + $customers = Customer::find()->indexBy(function ($customer) { + return $customer->id . '-' . $customer->name; + })->orderBy('id')->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers['1-user1'] instanceof Customer); + $this->assertTrue($customers['2-user2'] instanceof Customer); + $this->assertTrue($customers['3-user3'] instanceof Customer); } public function testFindBySql() diff --git a/tests/unit/framework/db/CommandTest.php b/tests/unit/framework/db/CommandTest.php index 0d9652b..7b16c76 100644 --- a/tests/unit/framework/db/CommandTest.php +++ b/tests/unit/framework/db/CommandTest.php @@ -7,9 +7,13 @@ use yii\db\Command; use yii\db\Query; use yii\db\DataReader; -class CommandTest extends \yiiunit\DatabaseTestCase +/** + * @group db + * @group mysql + */ +class CommandTest extends DatabaseTestCase { - function testConstruct() + public function testConstruct() { $db = $this->getConnection(false); @@ -23,7 +27,7 @@ class CommandTest extends \yiiunit\DatabaseTestCase $this->assertEquals($sql, $command->sql); } - function testGetSetSql() + public function testGetSetSql() { $db = $this->getConnection(false); @@ -36,7 +40,7 @@ class CommandTest extends \yiiunit\DatabaseTestCase $this->assertEquals($sql2, $command->sql); } - function testAutoQuoting() + public function testAutoQuoting() { $db = $this->getConnection(false); @@ -45,7 +49,7 @@ class CommandTest extends \yiiunit\DatabaseTestCase $this->assertEquals("SELECT `id`, `t`.`name` FROM `tbl_customer` t", $command->sql); } - function testPrepareCancel() + public function testPrepareCancel() { $db = $this->getConnection(false); @@ -57,7 +61,7 @@ class CommandTest extends \yiiunit\DatabaseTestCase $this->assertEquals(null, $command->pdoStatement); } - function testExecute() + public function testExecute() { $db = $this->getConnection(); @@ -74,7 +78,7 @@ class CommandTest extends \yiiunit\DatabaseTestCase $command->execute(); } - function testQuery() + public function testQuery() { $db = $this->getConnection(); @@ -93,22 +97,22 @@ class CommandTest extends \yiiunit\DatabaseTestCase $rows = $db->createCommand('SELECT * FROM tbl_customer WHERE id=10')->queryAll(); $this->assertEquals(array(), $rows); - // queryRow + // queryOne $sql = 'SELECT * FROM tbl_customer ORDER BY id'; - $row = $db->createCommand($sql)->queryRow(); + $row = $db->createCommand($sql)->queryOne(); $this->assertEquals(1, $row['id']); $this->assertEquals('user1', $row['name']); $sql = 'SELECT * FROM tbl_customer ORDER BY id'; $command = $db->createCommand($sql); $command->prepare(); - $row = $command->queryRow(); + $row = $command->queryOne(); $this->assertEquals(1, $row['id']); $this->assertEquals('user1', $row['name']); $sql = 'SELECT * FROM tbl_customer WHERE id=10'; $command = $db->createCommand($sql); - $this->assertFalse($command->queryRow()); + $this->assertFalse($command->queryOne()); // queryColumn $sql = 'SELECT * FROM tbl_customer'; @@ -135,12 +139,12 @@ class CommandTest extends \yiiunit\DatabaseTestCase $command->query(); } - function testBindParamValue() + public function testBindParamValue() { $db = $this->getConnection(); // bindParam - $sql = 'INSERT INTO tbl_customer(email,name,address) VALUES (:email, :name, :address)'; + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, :name, :address)'; $command = $db->createCommand($sql); $email = 'user4@example.com'; $name = 'user4'; @@ -172,7 +176,7 @@ class CommandTest extends \yiiunit\DatabaseTestCase $this->assertEquals(1, $command->execute()); $sql = 'SELECT * FROM tbl_type'; - $row = $db->createCommand($sql)->queryRow(); + $row = $db->createCommand($sql)->queryOne(); $this->assertEquals($intCol, $row['int_col']); $this->assertEquals($charCol, $row['char_col']); $this->assertEquals($floatCol, $row['float_col']); @@ -191,102 +195,87 @@ class CommandTest extends \yiiunit\DatabaseTestCase $this->assertEquals('user5@example.com', $command->queryScalar()); } - function testFetchMode() + public function testFetchMode() { $db = $this->getConnection(); // default: FETCH_ASSOC $sql = 'SELECT * FROM tbl_customer'; $command = $db->createCommand($sql); - $result = $command->queryRow(); + $result = $command->queryOne(); $this->assertTrue(is_array($result) && isset($result['id'])); // FETCH_OBJ, customized via fetchMode property $sql = 'SELECT * FROM tbl_customer'; $command = $db->createCommand($sql); $command->fetchMode = \PDO::FETCH_OBJ; - $result = $command->queryRow(); + $result = $command->queryOne(); $this->assertTrue(is_object($result)); // FETCH_NUM, customized in query method $sql = 'SELECT * FROM tbl_customer'; $command = $db->createCommand($sql); - $result = $command->queryRow(array(), \PDO::FETCH_NUM); + $result = $command->queryOne(array(), \PDO::FETCH_NUM); $this->assertTrue(is_array($result) && isset($result[0])); } - function testInsert() + public function testInsert() { - } - function testUpdate() + public function testUpdate() { - } - function testDelete() + public function testDelete() { - } - function testCreateTable() + public function testCreateTable() { - } - function testRenameTable() + public function testRenameTable() { - } - function testDropTable() + public function testDropTable() { - } - function testTruncateTable() + public function testTruncateTable() { - } - function testAddColumn() + public function testAddColumn() { - } - function testDropColumn() + public function testDropColumn() { - } - function testRenameColumn() + public function testRenameColumn() { - } - function testAlterColumn() + public function testAlterColumn() { - } - function testAddForeignKey() + public function testAddForeignKey() { - } - function testDropForeignKey() + public function testDropForeignKey() { - } - function testCreateIndex() + public function testCreateIndex() { - } - function testDropIndex() + public function testDropIndex() { - } } diff --git a/tests/unit/framework/db/ConnectionTest.php b/tests/unit/framework/db/ConnectionTest.php index c837b95..04c5d53 100644 --- a/tests/unit/framework/db/ConnectionTest.php +++ b/tests/unit/framework/db/ConnectionTest.php @@ -4,9 +4,13 @@ namespace yiiunit\framework\db; use yii\db\Connection; -class ConnectionTest extends \yiiunit\DatabaseTestCase +/** + * @group db + * @group mysql + */ +class ConnectionTest extends DatabaseTestCase { - function testConstruct() + public function testConstruct() { $connection = $this->getConnection(false); $params = $this->database; @@ -16,7 +20,7 @@ class ConnectionTest extends \yiiunit\DatabaseTestCase $this->assertEquals($params['password'], $connection->password); } - function testOpenClose() + public function testOpenClose() { $connection = $this->getConnection(false, false); @@ -37,13 +41,13 @@ class ConnectionTest extends \yiiunit\DatabaseTestCase $connection->open(); } - function testGetDriverName() + public function testGetDriverName() { $connection = $this->getConnection(false, false); $this->assertEquals($this->driverName, $connection->driverName); } - function testQuoteValue() + public function testQuoteValue() { $connection = $this->getConnection(false); $this->assertEquals(123, $connection->quoteValue(123)); @@ -51,7 +55,7 @@ class ConnectionTest extends \yiiunit\DatabaseTestCase $this->assertEquals("'It\\'s interesting'", $connection->quoteValue("It's interesting")); } - function testQuoteTableName() + public function testQuoteTableName() { $connection = $this->getConnection(false); $this->assertEquals('`table`', $connection->quoteTableName('table')); @@ -62,7 +66,7 @@ class ConnectionTest extends \yiiunit\DatabaseTestCase $this->assertEquals('(table)', $connection->quoteTableName('(table)')); } - function testQuoteColumnName() + public function testQuoteColumnName() { $connection = $this->getConnection(false); $this->assertEquals('`column`', $connection->quoteColumnName('column')); diff --git a/tests/unit/DatabaseTestCase.php b/tests/unit/framework/db/DatabaseTestCase.php similarity index 76% rename from tests/unit/DatabaseTestCase.php rename to tests/unit/framework/db/DatabaseTestCase.php index b39c2e5..8ef1234 100644 --- a/tests/unit/DatabaseTestCase.php +++ b/tests/unit/framework/db/DatabaseTestCase.php @@ -1,50 +1,68 @@ <?php +namespace yiiunit\framework\db; -namespace yiiunit; +use yii\db\Connection; +use yiiunit\TestCase as TestCase; -class DatabaseTestCase extends TestCase +abstract class DatabaseTestCase extends TestCase { - protected $database; - protected $driverName = 'mysql'; - protected $db; + protected $database; + protected $driverName = 'mysql'; + /** + * @var Connection + */ + protected $db; - protected function setUp() - { - $databases = $this->getParam('databases'); - $this->database = $databases[$this->driverName]; - $pdo_database = 'pdo_'.$this->driverName; + protected function setUp() + { + parent::setUp(); + $databases = $this->getParam('databases'); + $this->database = $databases[$this->driverName]; + $pdo_database = 'pdo_'.$this->driverName; - if (!extension_loaded('pdo') || !extension_loaded($pdo_database)) { - $this->markTestSkipped('pdo and pdo_'.$pdo_database.' extension are required.'); - } - } + if (!extension_loaded('pdo') || !extension_loaded($pdo_database)) { + $this->markTestSkipped('pdo and '.$pdo_database.' extension are required.'); + } + $this->mockApplication(); + } - /** - * @param bool $reset whether to clean up the test database - * @param bool $open whether to open and populate test database - * @return \yii\db\Connection - */ - public function getConnection($reset = true, $open = true) - { - if (!$reset && $this->db) { - return $this->db; - } - $db = new \yii\db\Connection; - $db->dsn = $this->database['dsn']; - if (isset($this->database['username'])) { - $db->username = $this->database['username']; - $db->password = $this->database['password']; - } - if ($open) { - $db->open(); - $lines = explode(';', file_get_contents($this->database['fixture'])); - foreach ($lines as $line) { - if (trim($line) !== '') { - $db->pdo->exec($line); - } - } - } - $this->db = $db; - return $db; - } + protected function tearDown() + { + if ($this->db) { + $this->db->close(); + } + $this->destroyApplication(); + } + + /** + * @param bool $reset whether to clean up the test database + * @param bool $open whether to open and populate test database + * @return \yii\db\Connection + */ + public function getConnection($reset = true, $open = true) + { + if (!$reset && $this->db) { + return $this->db; + } + $db = new \yii\db\Connection; + $db->dsn = $this->database['dsn']; + if (isset($this->database['username'])) { + $db->username = $this->database['username']; + $db->password = $this->database['password']; + } + if (isset($this->database['attributes'])) { + $db->attributes = $this->database['attributes']; + } + if ($open) { + $db->open(); + $lines = explode(';', file_get_contents($this->database['fixture'])); + foreach ($lines as $line) { + if (trim($line) !== '') { + $db->pdo->exec($line); + } + } + } + $this->db = $db; + return $db; + } } diff --git a/tests/unit/framework/db/QueryBuilderTest.php b/tests/unit/framework/db/QueryBuilderTest.php new file mode 100644 index 0000000..d43c901 --- /dev/null +++ b/tests/unit/framework/db/QueryBuilderTest.php @@ -0,0 +1,133 @@ +<?php + +namespace yiiunit\framework\db; + +use yii\db\QueryBuilder; +use yii\db\Schema; +use yii\db\mysql\QueryBuilder as MysqlQueryBuilder; +use yii\db\sqlite\QueryBuilder as SqliteQueryBuilder; +use yii\db\mssql\QueryBuilder as MssqlQueryBuilder; +use yii\db\pgsql\QueryBuilder as PgsqlQueryBuilder; +use yii\db\cubrid\QueryBuilder as CubridQueryBuilder; + +/** + * @group db + * @group mysql + */ +class QueryBuilderTest extends DatabaseTestCase +{ + /** + * @throws \Exception + * @return QueryBuilder + */ + protected function getQueryBuilder() + { + switch ($this->driverName) { + case 'mysql': + return new MysqlQueryBuilder($this->getConnection()); + case 'sqlite': + return new SqliteQueryBuilder($this->getConnection()); + case 'mssql': + return new MssqlQueryBuilder($this->getConnection()); + case 'pgsql': + return new PgsqlQueryBuilder($this->getConnection()); + case 'cubrid': + return new CubridQueryBuilder($this->getConnection()); + } + throw new \Exception('Test is not implemented for ' . $this->driverName); + } + + /** + * this is not used as a dataprovider for testGetColumnType to speed up the test + * when used as dataprovider every single line will cause a reconnect with the database which is not needed here + */ + public function columnTypes() + { + return array( + array(Schema::TYPE_PK, 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY'), + array(Schema::TYPE_PK . '(8)', 'int(8) NOT NULL AUTO_INCREMENT PRIMARY KEY'), + array(Schema::TYPE_PK . ' CHECK (value > 5)', 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'), + array(Schema::TYPE_PK . '(8) CHECK (value > 5)', 'int(8) NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'), + array(Schema::TYPE_STRING, 'varchar(255)'), + array(Schema::TYPE_STRING . '(32)', 'varchar(32)'), + array(Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'), + array(Schema::TYPE_TEXT, 'text'), + array(Schema::TYPE_TEXT . '(255)', 'text'), + array(Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'), + array(Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'), + array(Schema::TYPE_SMALLINT, 'smallint(6)'), + array(Schema::TYPE_SMALLINT . '(8)', 'smallint(8)'), + array(Schema::TYPE_INTEGER, 'int(11)'), + array(Schema::TYPE_INTEGER . '(8)', 'int(8)'), + array(Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'int(11) CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'int(8) CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . ' NOT NULL', 'int(11) NOT NULL'), + array(Schema::TYPE_BIGINT, 'bigint(20)'), + array(Schema::TYPE_BIGINT . '(8)', 'bigint(8)'), + array(Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint(20) CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint(8) CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . ' NOT NULL', 'bigint(20) NOT NULL'), + array(Schema::TYPE_FLOAT, 'float'), + array(Schema::TYPE_FLOAT . '(16,5)', 'float'), + array(Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'float CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . ' NOT NULL', 'float NOT NULL'), + array(Schema::TYPE_DECIMAL, 'decimal(10,0)'), + array(Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'), + array(Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'), + array(Schema::TYPE_DATETIME, 'datetime'), + array(Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'), + array(Schema::TYPE_TIMESTAMP, 'timestamp'), + array(Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'), + array(Schema::TYPE_TIME, 'time'), + array(Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"), + array(Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'), + array(Schema::TYPE_DATE, 'date'), + array(Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'), + array(Schema::TYPE_BINARY, 'blob'), + array(Schema::TYPE_BOOLEAN, 'tinyint(1)'), + array(Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'tinyint(1) NOT NULL DEFAULT 1'), + array(Schema::TYPE_MONEY, 'decimal(19,4)'), + array(Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'), + array(Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'), + ); + } + + public function testGetColumnType() + { + $qb = $this->getQueryBuilder(); + foreach ($this->columnTypes() as $item) { + list ($column, $expected) = $item; + $this->assertEquals($expected, $qb->getColumnType($column)); + } + } + + public function testAddDropPrimaryKey() + { + $tableName = 'tbl_constraints'; + $pkeyName = $tableName . "_pkey"; + + // ADD + $qb = $this->getQueryBuilder(); + $qb->db->createCommand()->addPrimaryKey($pkeyName, $tableName, array('id'))->execute(); + $tableSchema = $qb->db->getSchema()->getTableSchema($tableName); + $this->assertEquals(1, count($tableSchema->primaryKey)); + + //DROP + $qb->db->createCommand()->dropPrimaryKey($pkeyName, $tableName)->execute(); + $qb = $this->getQueryBuilder(); // resets the schema + $tableSchema = $qb->db->getSchema()->getTableSchema($tableName); + $this->assertEquals(0, count($tableSchema->primaryKey)); + } +} diff --git a/tests/unit/framework/db/QueryTest.php b/tests/unit/framework/db/QueryTest.php index 398d0d9..a275afd 100644 --- a/tests/unit/framework/db/QueryTest.php +++ b/tests/unit/framework/db/QueryTest.php @@ -7,9 +7,13 @@ use yii\db\Command; use yii\db\Query; use yii\db\DataReader; -class QueryTest extends \yiiunit\DatabaseTestCase +/** + * @group db + * @group mysql + */ +class QueryTest extends DatabaseTestCase { - function testSelect() + public function testSelect() { // default $query = new Query; @@ -20,19 +24,19 @@ class QueryTest extends \yiiunit\DatabaseTestCase $query = new Query; $query->select('id, name', 'something')->distinct(true); - $this->assertEquals(array('id','name'), $query->select); + $this->assertEquals(array('id', 'name'), $query->select); $this->assertTrue($query->distinct); $this->assertEquals('something', $query->selectOption); } - function testFrom() + public function testFrom() { $query = new Query; $query->from('tbl_user'); $this->assertEquals(array('tbl_user'), $query->from); } - function testWhere() + public function testWhere() { $query = new Query; $query->where('id = :id', array(':id' => 1)); @@ -48,12 +52,11 @@ class QueryTest extends \yiiunit\DatabaseTestCase $this->assertEquals(array(':id' => 1, ':name' => 'something', ':age' => '30'), $query->params); } - function testJoin() + public function testJoin() { - } - function testGroup() + public function testGroup() { $query = new Query; $query->groupBy('team'); @@ -66,7 +69,7 @@ class QueryTest extends \yiiunit\DatabaseTestCase $this->assertEquals(array('team', 'company', 'age'), $query->groupBy); } - function testHaving() + public function testHaving() { $query = new Query; $query->having('id = :id', array(':id' => 1)); @@ -82,7 +85,7 @@ class QueryTest extends \yiiunit\DatabaseTestCase $this->assertEquals(array(':id' => 1, ':name' => 'something', ':age' => '30'), $query->params); } - function testOrder() + public function testOrder() { $query = new Query; $query->orderBy('team'); @@ -101,7 +104,7 @@ class QueryTest extends \yiiunit\DatabaseTestCase $this->assertEquals(array('team' => false, 'company' => true, 'age' => false), $query->orderBy); } - function testLimitOffset() + public function testLimitOffset() { $query = new Query; $query->limit(10)->offset(5); @@ -109,8 +112,7 @@ class QueryTest extends \yiiunit\DatabaseTestCase $this->assertEquals(5, $query->offset); } - function testUnion() + public function testUnion() { - } } diff --git a/tests/unit/framework/db/SchemaTest.php b/tests/unit/framework/db/SchemaTest.php new file mode 100644 index 0000000..d3cb2fd --- /dev/null +++ b/tests/unit/framework/db/SchemaTest.php @@ -0,0 +1,93 @@ +<?php + +namespace yiiunit\framework\db; + +use yii\caching\FileCache; +use yii\db\Schema; + +/** + * @group db + * @group mysql + */ +class SchemaTest extends DatabaseTestCase +{ + public function testFindTableNames() + { + /** @var Schema $schema */ + $schema = $this->getConnection()->schema; + + $tables = $schema->getTableNames(); + $this->assertTrue(in_array('tbl_customer', $tables)); + $this->assertTrue(in_array('tbl_category', $tables)); + $this->assertTrue(in_array('tbl_item', $tables)); + $this->assertTrue(in_array('tbl_order', $tables)); + $this->assertTrue(in_array('tbl_order_item', $tables)); + $this->assertTrue(in_array('tbl_type', $tables)); + } + + public function testGetTableSchemas() + { + /** @var Schema $schema */ + $schema = $this->getConnection()->schema; + + $tables = $schema->getTableSchemas(); + $this->assertEquals(count($schema->getTableNames()), count($tables)); + foreach($tables as $table) { + $this->assertInstanceOf('yii\db\TableSchema', $table); + } + } + + public function testGetNonExistingTableSchema() + { + $this->assertNull($this->getConnection()->schema->getTableSchema('nonexisting_table')); + } + + public function testSchemaCache() + { + /** @var Schema $schema */ + $schema = $this->getConnection()->schema; + + $schema->db->enableSchemaCache = true; + $schema->db->schemaCache = new FileCache(); + $noCacheTable = $schema->getTableSchema('tbl_type', true); + $cachedTable = $schema->getTableSchema('tbl_type', true); + $this->assertEquals($noCacheTable, $cachedTable); + } + + public function testCompositeFk() + { + /** @var Schema $schema */ + $schema = $this->getConnection()->schema; + + $table = $schema->getTableSchema('tbl_composite_fk'); + + $this->assertCount(1, $table->foreignKeys); + $this->assertTrue(isset($table->foreignKeys[0])); + $this->assertEquals('tbl_order_item', $table->foreignKeys[0][0]); + $this->assertEquals('order_id', $table->foreignKeys[0]['order_id']); + $this->assertEquals('item_id', $table->foreignKeys[0]['item_id']); + } + + public function testGetPDOType() + { + $values = array( + array(null, \PDO::PARAM_NULL), + array('', \PDO::PARAM_STR), + array('hello', \PDO::PARAM_STR), + array(0, \PDO::PARAM_INT), + array(1, \PDO::PARAM_INT), + array(1337, \PDO::PARAM_INT), + array(true, \PDO::PARAM_BOOL), + array(false, \PDO::PARAM_BOOL), + array($fp=fopen(__FILE__, 'rb'), \PDO::PARAM_LOB), + ); + + /** @var Schema $schema */ + $schema = $this->getConnection()->schema; + + foreach($values as $value) { + $this->assertEquals($value[1], $schema->getPdoType($value[0])); + } + fclose($fp); + } +} diff --git a/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php b/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php new file mode 100644 index 0000000..49f9bc0 --- /dev/null +++ b/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php @@ -0,0 +1,42 @@ +<?php +namespace yiiunit\framework\db\cubrid; + +use yiiunit\data\ar\Customer; +use yiiunit\framework\db\ActiveRecordTest; + +/** + * @group db + * @group cubrid + */ +class CubridActiveRecordTest extends ActiveRecordTest +{ + public $driverName = 'cubrid'; + + /** + * cubrid PDO does not support boolean values. + * Make sure this does not affect AR layer. + */ + public function testBooleanAttribute() + { + $customer = new Customer(); + $customer->name = 'boolean customer'; + $customer->email = 'mail@example.com'; + $customer->status = true; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(1, $customer->status); + + $customer->status = false; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(0, $customer->status); + + $customers = Customer::find()->where(array('status' => true))->all(); + $this->assertEquals(2, count($customers)); + + $customers = Customer::find()->where(array('status' => false))->all(); + $this->assertEquals(1, count($customers)); + } +} diff --git a/tests/unit/framework/db/cubrid/CubridCommandTest.php b/tests/unit/framework/db/cubrid/CubridCommandTest.php new file mode 100644 index 0000000..45d3c1c --- /dev/null +++ b/tests/unit/framework/db/cubrid/CubridCommandTest.php @@ -0,0 +1,79 @@ +<?php +namespace yiiunit\framework\db\cubrid; + +use yiiunit\framework\db\CommandTest; + +/** + * @group db + * @group cubrid + */ +class CubridCommandTest extends CommandTest +{ + public $driverName = 'cubrid'; + + public function testBindParamValue() + { + $db = $this->getConnection(); + + // bindParam + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, :name, :address)'; + $command = $db->createCommand($sql); + $email = 'user4@example.com'; + $name = 'user4'; + $address = 'address4'; + $command->bindParam(':email', $email); + $command->bindParam(':name', $name); + $command->bindParam(':address', $address); + $command->execute(); + + $sql = 'SELECT name FROM tbl_customer WHERE email=:email'; + $command = $db->createCommand($sql); + $command->bindParam(':email', $email); + $this->assertEquals($name, $command->queryScalar()); + + $sql = "INSERT INTO tbl_type (int_col, char_col, char_col2, enum_col, float_col, blob_col, numeric_col) VALUES (:int_col, '', :char_col, :enum_col, :float_col, CHAR_TO_BLOB(:blob_col), :numeric_col)"; + $command = $db->createCommand($sql); + $intCol = 123; + $charCol = 'abc'; + $enumCol = 'a'; + $floatCol = 1.23; + $blobCol = "\x10\x11\x12"; + $numericCol = '1.23'; + $command->bindParam(':int_col', $intCol); + $command->bindParam(':char_col', $charCol); + $command->bindParam(':enum_col', $enumCol); + $command->bindParam(':float_col', $floatCol); + $command->bindParam(':blob_col', $blobCol); + $command->bindParam(':numeric_col', $numericCol); + $this->assertEquals(1, $command->execute()); + + $sql = 'SELECT * FROM tbl_type'; + $row = $db->createCommand($sql)->queryOne(); + $this->assertEquals($intCol, $row['int_col']); + $this->assertEquals($enumCol, $row['enum_col']); + $this->assertEquals($charCol, $row['char_col2']); + $this->assertEquals($floatCol, $row['float_col']); + $this->assertEquals($blobCol, fread($row['blob_col'], 3)); + $this->assertEquals($numericCol, $row['numeric_col']); + + // bindValue + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, \'user5\', \'address5\')'; + $command = $db->createCommand($sql); + $command->bindValue(':email', 'user5@example.com'); + $command->execute(); + + $sql = 'SELECT email FROM tbl_customer WHERE name=:name'; + $command = $db->createCommand($sql); + $command->bindValue(':name', 'user5'); + $this->assertEquals('user5@example.com', $command->queryScalar()); + } + + public function testAutoQuoting() + { + $db = $this->getConnection(false); + + $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; + $command = $db->createCommand($sql); + $this->assertEquals('SELECT "id", "t"."name" FROM "tbl_customer" t', $command->sql); + } +} diff --git a/tests/unit/framework/db/cubrid/CubridConnectionTest.php b/tests/unit/framework/db/cubrid/CubridConnectionTest.php new file mode 100644 index 0000000..4cd6e20 --- /dev/null +++ b/tests/unit/framework/db/cubrid/CubridConnectionTest.php @@ -0,0 +1,44 @@ +<?php +namespace yiiunit\framework\db\cubrid; + +use yiiunit\framework\db\ConnectionTest; + +/** + * @group db + * @group cubrid + */ +class CubridConnectionTest extends ConnectionTest +{ + public $driverName = 'cubrid'; + + public function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } + + public function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"table"', $connection->quoteTableName('table')); + $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)')); + } + + public function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"column"', $connection->quoteColumnName('column')); + $this->assertEquals('"column"', $connection->quoteColumnName('"column"')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('table.column')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('table."column"')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + } +} diff --git a/tests/unit/framework/db/cubrid/CubridQueryBuilderTest.php b/tests/unit/framework/db/cubrid/CubridQueryBuilderTest.php new file mode 100644 index 0000000..107b73b --- /dev/null +++ b/tests/unit/framework/db/cubrid/CubridQueryBuilderTest.php @@ -0,0 +1,84 @@ +<?php + +namespace yiiunit\framework\db\cubrid; + +use yii\base\NotSupportedException; +use yii\db\sqlite\Schema; +use yiiunit\framework\db\QueryBuilderTest; + +/** + * @group db + * @group cubrid + */ +class CubridQueryBuilderTest extends QueryBuilderTest +{ + public $driverName = 'cubrid'; + + /** + * this is not used as a dataprovider for testGetColumnType to speed up the test + * when used as dataprovider every single line will cause a reconnect with the database which is not needed here + */ + public function columnTypes() + { + return array( + array(Schema::TYPE_PK, 'int NOT NULL AUTO_INCREMENT PRIMARY KEY'), + array(Schema::TYPE_PK . '(8)', 'int NOT NULL AUTO_INCREMENT PRIMARY KEY'), + array(Schema::TYPE_PK . ' CHECK (value > 5)', 'int NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'), + array(Schema::TYPE_PK . '(8) CHECK (value > 5)', 'int NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'), + array(Schema::TYPE_STRING, 'varchar(255)'), + array(Schema::TYPE_STRING . '(32)', 'varchar(32)'), + array(Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'), + array(Schema::TYPE_TEXT, 'varchar'), + array(Schema::TYPE_TEXT . '(255)', 'varchar'), + array(Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'varchar CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'varchar CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . ' NOT NULL', 'varchar NOT NULL'), + array(Schema::TYPE_TEXT . '(255) NOT NULL', 'varchar NOT NULL'), + array(Schema::TYPE_SMALLINT, 'smallint'), + array(Schema::TYPE_SMALLINT . '(8)', 'smallint'), + array(Schema::TYPE_INTEGER, 'int'), + array(Schema::TYPE_INTEGER . '(8)', 'int'), + array(Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'int CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'int CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . ' NOT NULL', 'int NOT NULL'), + array(Schema::TYPE_BIGINT, 'bigint'), + array(Schema::TYPE_BIGINT . '(8)', 'bigint'), + array(Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'), + array(Schema::TYPE_FLOAT, 'float(7)'), + array(Schema::TYPE_FLOAT . '(16)', 'float(16)'), + array(Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float(7) CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . '(16) CHECK (value > 5.6)', 'float(16) CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . ' NOT NULL', 'float(7) NOT NULL'), + array(Schema::TYPE_DECIMAL, 'decimal(10,0)'), + array(Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'), + array(Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'), + array(Schema::TYPE_DATETIME, 'datetime'), + array(Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'), + array(Schema::TYPE_TIMESTAMP, 'timestamp'), + array(Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'), + array(Schema::TYPE_TIME, 'time'), + array(Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"), + array(Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'), + array(Schema::TYPE_DATE, 'date'), + array(Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'), + array(Schema::TYPE_BINARY, 'blob'), + array(Schema::TYPE_BOOLEAN, 'smallint'), + array(Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'smallint NOT NULL DEFAULT 1'), + array(Schema::TYPE_MONEY, 'decimal(19,4)'), + array(Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'), + array(Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'), + ); + } + +} diff --git a/tests/unit/framework/db/cubrid/CubridQueryTest.php b/tests/unit/framework/db/cubrid/CubridQueryTest.php new file mode 100644 index 0000000..b7c9009 --- /dev/null +++ b/tests/unit/framework/db/cubrid/CubridQueryTest.php @@ -0,0 +1,13 @@ +<?php +namespace yiiunit\framework\db\cubrid; + +use yiiunit\framework\db\QueryTest; + +/** + * @group db + * @group cubrid + */ +class CubridQueryTest extends QueryTest +{ + public $driverName = 'cubrid'; +} diff --git a/tests/unit/framework/db/cubrid/CubridSchemaTest.php b/tests/unit/framework/db/cubrid/CubridSchemaTest.php new file mode 100644 index 0000000..cdfe3c8 --- /dev/null +++ b/tests/unit/framework/db/cubrid/CubridSchemaTest.php @@ -0,0 +1,36 @@ +<?php +namespace yiiunit\framework\db\cubrid; + +use yiiunit\framework\db\SchemaTest; + +/** + * @group db + * @group cubrid + */ +class CubridSchemaTest extends SchemaTest +{ + public $driverName = 'cubrid'; + + public function testGetPDOType() + { + $values = array( + array(null, \PDO::PARAM_NULL), + array('', \PDO::PARAM_STR), + array('hello', \PDO::PARAM_STR), + array(0, \PDO::PARAM_INT), + array(1, \PDO::PARAM_INT), + array(1337, \PDO::PARAM_INT), + array(true, \PDO::PARAM_INT), + array(false, \PDO::PARAM_INT), + array($fp=fopen(__FILE__, 'rb'), \PDO::PARAM_LOB), + ); + + /** @var Schema $schema */ + $schema = $this->getConnection()->schema; + + foreach($values as $value) { + $this->assertEquals($value[1], $schema->getPdoType($value[0])); + } + fclose($fp); + } +} diff --git a/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php b/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php new file mode 100644 index 0000000..c21efc6 --- /dev/null +++ b/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php @@ -0,0 +1,14 @@ +<?php + +namespace yiiunit\framework\db\mssql; + +use yiiunit\framework\db\ActiveRecordTest; + +/** + * @group db + * @group mssql + */ +class MssqlActiveRecordTest extends ActiveRecordTest +{ + protected $driverName = 'sqlsrv'; +} diff --git a/tests/unit/framework/db/mssql/MssqlCommandTest.php b/tests/unit/framework/db/mssql/MssqlCommandTest.php new file mode 100644 index 0000000..86f7f45 --- /dev/null +++ b/tests/unit/framework/db/mssql/MssqlCommandTest.php @@ -0,0 +1,84 @@ +<?php + +namespace yiiunit\framework\db\mssql; + +use yiiunit\framework\db\CommandTest; + +/** + * @group db + * @group mssql + */ +class MssqlCommandTest extends CommandTest +{ + protected $driverName = 'sqlsrv'; + + public function testAutoQuoting() + { + $db = $this->getConnection(false); + + $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; + $command = $db->createCommand($sql); + $this->assertEquals("SELECT [id], [t].[name] FROM [tbl_customer] t", $command->sql); + } + + public function testPrepareCancel() + { + $this->markTestSkipped('MSSQL driver does not support this feature.'); + } + + public function testBindParamValue() + { + $db = $this->getConnection(); + + // bindParam + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, :name, :address)'; + $command = $db->createCommand($sql); + $email = 'user4@example.com'; + $name = 'user4'; + $address = 'address4'; + $command->bindParam(':email', $email); + $command->bindParam(':name', $name); + $command->bindParam(':address', $address); + $command->execute(); + + $sql = 'SELECT name FROM tbl_customer WHERE email=:email'; + $command = $db->createCommand($sql); + $command->bindParam(':email', $email); + $this->assertEquals($name, $command->queryScalar()); + + $sql = 'INSERT INTO tbl_type (int_col, char_col, float_col, blob_col, numeric_col, bool_col) VALUES (:int_col, :char_col, :float_col, CONVERT([varbinary], :blob_col), :numeric_col, :bool_col)'; + $command = $db->createCommand($sql); + $intCol = 123; + $charCol = 'abc'; + $floatCol = 1.23; + $blobCol = "\x10\x11\x12"; + $numericCol = '1.23'; + $boolCol = false; + $command->bindParam(':int_col', $intCol); + $command->bindParam(':char_col', $charCol); + $command->bindParam(':float_col', $floatCol); + $command->bindParam(':blob_col', $blobCol); + $command->bindParam(':numeric_col', $numericCol); + $command->bindParam(':bool_col', $boolCol); + $this->assertEquals(1, $command->execute()); + + $sql = 'SELECT int_col, char_col, float_col, CONVERT([nvarchar], blob_col) AS blob_col, numeric_col FROM tbl_type'; + $row = $db->createCommand($sql)->queryOne(); + $this->assertEquals($intCol, $row['int_col']); + $this->assertEquals($charCol, trim($row['char_col'])); + $this->assertEquals($floatCol, $row['float_col']); + $this->assertEquals($blobCol, $row['blob_col']); + $this->assertEquals($numericCol, $row['numeric_col']); + + // bindValue + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, \'user5\', \'address5\')'; + $command = $db->createCommand($sql); + $command->bindValue(':email', 'user5@example.com'); + $command->execute(); + + $sql = 'SELECT email FROM tbl_customer WHERE name=:name'; + $command = $db->createCommand($sql); + $command->bindValue(':name', 'user5'); + $this->assertEquals('user5@example.com', $command->queryScalar()); + } +} diff --git a/tests/unit/framework/db/mssql/MssqlConnectionTest.php b/tests/unit/framework/db/mssql/MssqlConnectionTest.php new file mode 100644 index 0000000..6531f83 --- /dev/null +++ b/tests/unit/framework/db/mssql/MssqlConnectionTest.php @@ -0,0 +1,45 @@ +<?php + +namespace yiiunit\framework\db\mssql; + +use yiiunit\framework\db\ConnectionTest; + +/** + * @group db + * @group mssql + */ +class MssqlConnectionTest extends ConnectionTest +{ + protected $driverName = 'sqlsrv'; + + public function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } + + public function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals('[table]', $connection->quoteTableName('table')); + $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)')); + } + + public function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('[column]', $connection->quoteColumnName('column')); + $this->assertEquals('[column]', $connection->quoteColumnName('[column]')); + $this->assertEquals('[table].[column]', $connection->quoteColumnName('table.column')); + $this->assertEquals('[table].[column]', $connection->quoteColumnName('table.[column]')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + } +} diff --git a/tests/unit/framework/db/mssql/MssqlQueryTest.php b/tests/unit/framework/db/mssql/MssqlQueryTest.php new file mode 100644 index 0000000..a2cb019 --- /dev/null +++ b/tests/unit/framework/db/mssql/MssqlQueryTest.php @@ -0,0 +1,14 @@ +<?php + +namespace yiiunit\framework\db\mssql; + +use yiiunit\framework\db\QueryTest; + +/** + * @group db + * @group mssql + */ +class MssqlQueryTest extends QueryTest +{ + protected $driverName = 'sqlsrv'; +} diff --git a/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php b/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php new file mode 100644 index 0000000..1fffad7 --- /dev/null +++ b/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php @@ -0,0 +1,14 @@ +<?php + +namespace yiiunit\framework\db\pgsql; + +use yiiunit\framework\db\ActiveRecordTest; + +/** + * @group db + * @group pgsql + */ +class PostgreSQLActiveRecordTest extends ActiveRecordTest +{ + protected $driverName = 'pgsql'; +} diff --git a/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php b/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php new file mode 100644 index 0000000..26ac0e0 --- /dev/null +++ b/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php @@ -0,0 +1,51 @@ +<?php +namespace yiiunit\framework\db\pgsql; + +use yiiunit\framework\db\ConnectionTest; + +/** + * @group db + * @group pgsql + */ +class PostgreSQLConnectionTest extends ConnectionTest +{ + protected $driverName = 'pgsql'; + + public function testConnection() + { + $connection = $this->getConnection(true); + } + + public function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } + + public function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"table"', $connection->quoteTableName('table')); + $this->assertEquals('"table"', $connection->quoteTableName('"table"')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema.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)')); + } + + public function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"column"', $connection->quoteColumnName('column')); + $this->assertEquals('"column"', $connection->quoteColumnName('"column"')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('table.column')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('table."column"')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('"table"."column"')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + } +} diff --git a/tests/unit/framework/db/pgsql/PostgreSQLQueryBuilderTest.php b/tests/unit/framework/db/pgsql/PostgreSQLQueryBuilderTest.php new file mode 100644 index 0000000..3ef329e --- /dev/null +++ b/tests/unit/framework/db/pgsql/PostgreSQLQueryBuilderTest.php @@ -0,0 +1,77 @@ +<?php + +namespace yiiunit\framework\db\pgsql; + +use yii\db\pgsql\Schema; +use yiiunit\framework\db\QueryBuilderTest; + +/** + * @group db + * @group pgsql + */ +class PostgreSQLQueryBuilderTest extends QueryBuilderTest +{ + public $driverName = 'pgsql'; + + public function columnTypes() + { + return array( + array(Schema::TYPE_PK, 'serial NOT NULL PRIMARY KEY'), + array(Schema::TYPE_PK . '(8)', 'serial NOT NULL PRIMARY KEY'), + array(Schema::TYPE_PK . ' CHECK (value > 5)', 'serial NOT NULL PRIMARY KEY CHECK (value > 5)'), + array(Schema::TYPE_PK . '(8) CHECK (value > 5)', 'serial NOT NULL PRIMARY KEY CHECK (value > 5)'), + array(Schema::TYPE_STRING, 'varchar(255)'), + array(Schema::TYPE_STRING . '(32)', 'varchar(32)'), + array(Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'), + array(Schema::TYPE_TEXT, 'text'), + array(Schema::TYPE_TEXT . '(255)', 'text'), + array(Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'), + array(Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'), + array(Schema::TYPE_SMALLINT, 'smallint'), + array(Schema::TYPE_SMALLINT . '(8)', 'smallint'), + array(Schema::TYPE_INTEGER, 'integer'), + array(Schema::TYPE_INTEGER . '(8)', 'integer'), + array(Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'integer CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'integer CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . ' NOT NULL', 'integer NOT NULL'), + array(Schema::TYPE_BIGINT, 'bigint'), + array(Schema::TYPE_BIGINT . '(8)', 'bigint'), + array(Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'), + array(Schema::TYPE_FLOAT, 'double precision'), + array(Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'double precision CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'double precision CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . ' NOT NULL', 'double precision NOT NULL'), + array(Schema::TYPE_DECIMAL, 'numeric(10,0)'), + array(Schema::TYPE_DECIMAL . '(12,4)', 'numeric(12,4)'), + array(Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'numeric(10,0) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'numeric(12,4) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . ' NOT NULL', 'numeric(10,0) NOT NULL'), + array(Schema::TYPE_DATETIME, 'timestamp'), + array(Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATETIME . ' NOT NULL', 'timestamp NOT NULL'), + array(Schema::TYPE_TIMESTAMP, 'timestamp'), + array(Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'), + array(Schema::TYPE_TIME, 'time'), + array(Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"), + array(Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'), + array(Schema::TYPE_DATE, 'date'), + array(Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'), + array(Schema::TYPE_BINARY, 'bytea'), + array(Schema::TYPE_BOOLEAN, 'boolean'), + array(Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'boolean NOT NULL DEFAULT 1'), + array(Schema::TYPE_MONEY, 'numeric(19,4)'), + array(Schema::TYPE_MONEY . '(16,2)', 'numeric(16,2)'), + array(Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'numeric(19,4) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'numeric(16,2) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . ' NOT NULL', 'numeric(19,4) NOT NULL'), + ); + } +} diff --git a/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php index 539e879..a689e5d 100644 --- a/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php +++ b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php @@ -1,12 +1,13 @@ <?php - namespace yiiunit\framework\db\sqlite; -class SqliteActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest +use yiiunit\framework\db\ActiveRecordTest; + +/** + * @group db + * @group sqlite + */ +class SqliteActiveRecordTest extends ActiveRecordTest { - public function setUp() - { - $this->driverName = 'sqlite'; - parent::setUp(); - } + protected $driverName = 'sqlite'; } diff --git a/tests/unit/framework/db/sqlite/SqliteCommandTest.php b/tests/unit/framework/db/sqlite/SqliteCommandTest.php index 4110cd3..1f9ddc2 100644 --- a/tests/unit/framework/db/sqlite/SqliteCommandTest.php +++ b/tests/unit/framework/db/sqlite/SqliteCommandTest.php @@ -1,21 +1,22 @@ <?php - namespace yiiunit\framework\db\sqlite; -class SqliteCommandTest extends \yiiunit\framework\db\CommandTest +use yiiunit\framework\db\CommandTest; + +/** + * @group db + * @group sqlite + */ +class SqliteCommandTest extends CommandTest { - public function setUp() - { - $this->driverName = 'sqlite'; - parent::setUp(); - } + protected $driverName = 'sqlite'; - function testAutoQuoting() - { - $db = $this->getConnection(false); + public function testAutoQuoting() + { + $db = $this->getConnection(false); - $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; - $command = $db->createCommand($sql); - $this->assertEquals("SELECT \"id\", 't'.\"name\" FROM 'tbl_customer' t", $command->sql); - } + $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; + $command = $db->createCommand($sql); + $this->assertEquals("SELECT \"id\", 't'.\"name\" FROM 'tbl_customer' t", $command->sql); + } } diff --git a/tests/unit/framework/db/sqlite/SqliteConnectionTest.php b/tests/unit/framework/db/sqlite/SqliteConnectionTest.php index eeb5aae..e1a2961 100644 --- a/tests/unit/framework/db/sqlite/SqliteConnectionTest.php +++ b/tests/unit/framework/db/sqlite/SqliteConnectionTest.php @@ -1,47 +1,48 @@ <?php - namespace yiiunit\framework\db\sqlite; -class SqliteConnectionTest extends \yiiunit\framework\db\ConnectionTest +use yiiunit\framework\db\ConnectionTest; + +/** + * @group db + * @group sqlite + */ +class SqliteConnectionTest extends ConnectionTest { - public function setUp() - { - $this->driverName = 'sqlite'; - parent::setUp(); - } + protected $driverName = 'sqlite'; - function testConstruct() - { - $connection = $this->getConnection(false); - $params = $this->database; + public function testConstruct() + { + $connection = $this->getConnection(false); + $params = $this->database; - $this->assertEquals($params['dsn'], $connection->dsn); - } + $this->assertEquals($params['dsn'], $connection->dsn); + } - function testQuoteValue() - { - $connection = $this->getConnection(false); - $this->assertEquals(123, $connection->quoteValue(123)); - $this->assertEquals("'string'", $connection->quoteValue('string')); - $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); - } + public function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } - function testQuoteTableName() - { - $connection = $this->getConnection(false); - $this->assertEquals("'table'", $connection->quoteTableName('table')); - $this->assertEquals("'schema'.'table'", $connection->quoteTableName('schema.table')); - $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); - $this->assertEquals('(table)', $connection->quoteTableName('(table)')); - } + public function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals("'table'", $connection->quoteTableName('table')); + $this->assertEquals("'schema'.'table'", $connection->quoteTableName('schema.table')); + $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); + $this->assertEquals('(table)', $connection->quoteTableName('(table)')); + } - function testQuoteColumnName() - { - $connection = $this->getConnection(false); - $this->assertEquals('"column"', $connection->quoteColumnName('column')); - $this->assertEquals("'table'.\"column\"", $connection->quoteColumnName('table.column')); - $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); - $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); - $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); - } + public function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"column"', $connection->quoteColumnName('column')); + $this->assertEquals("'table'.\"column\"", $connection->quoteColumnName('table.column')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + } } diff --git a/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php b/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php new file mode 100644 index 0000000..b20acad --- /dev/null +++ b/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php @@ -0,0 +1,84 @@ +<?php + +namespace yiiunit\framework\db\sqlite; + +use yii\db\sqlite\Schema; +use yiiunit\framework\db\QueryBuilderTest; + +/** + * @group db + * @group sqlite + */ +class SqliteQueryBuilderTest extends QueryBuilderTest +{ + protected $driverName = 'sqlite'; + + public function columnTypes() + { + return array( + array(Schema::TYPE_PK, 'integer PRIMARY KEY AUTOINCREMENT NOT NULL'), + array(Schema::TYPE_PK . '(8)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL'), + array(Schema::TYPE_PK . ' CHECK (value > 5)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (value > 5)'), + array(Schema::TYPE_PK . '(8) CHECK (value > 5)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (value > 5)'), + array(Schema::TYPE_STRING, 'varchar(255)'), + array(Schema::TYPE_STRING . '(32)', 'varchar(32)'), + array(Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'), + array(Schema::TYPE_TEXT, 'text'), + array(Schema::TYPE_TEXT . '(255)', 'text'), + array(Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'), + array(Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'), + array(Schema::TYPE_SMALLINT, 'smallint'), + array(Schema::TYPE_SMALLINT . '(8)', 'smallint'), + array(Schema::TYPE_INTEGER, 'integer'), + array(Schema::TYPE_INTEGER . '(8)', 'integer'), + array(Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'integer CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'integer CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . ' NOT NULL', 'integer NOT NULL'), + array(Schema::TYPE_BIGINT, 'bigint'), + array(Schema::TYPE_BIGINT . '(8)', 'bigint'), + array(Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'), + array(Schema::TYPE_FLOAT, 'float'), + array(Schema::TYPE_FLOAT . '(16,5)', 'float'), + array(Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'float CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . ' NOT NULL', 'float NOT NULL'), + array(Schema::TYPE_DECIMAL, 'decimal(10,0)'), + array(Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'), + array(Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'), + array(Schema::TYPE_DATETIME, 'datetime'), + array(Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'), + array(Schema::TYPE_TIMESTAMP, 'timestamp'), + array(Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'), + array(Schema::TYPE_TIME, 'time'), + array(Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"), + array(Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'), + array(Schema::TYPE_DATE, 'date'), + array(Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'), + array(Schema::TYPE_BINARY, 'blob'), + array(Schema::TYPE_BOOLEAN, 'boolean'), + array(Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'boolean NOT NULL DEFAULT 1'), + array(Schema::TYPE_MONEY, 'decimal(19,4)'), + array(Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'), + array(Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'), + ); + } + + public function testAddDropPrimaryKey() + { + $this->setExpectedException('yii\base\NotSupportedException'); + parent::testAddDropPrimaryKey(); + } +} diff --git a/tests/unit/framework/db/sqlite/SqliteQueryTest.php b/tests/unit/framework/db/sqlite/SqliteQueryTest.php index f00e59e..f1db36b 100644 --- a/tests/unit/framework/db/sqlite/SqliteQueryTest.php +++ b/tests/unit/framework/db/sqlite/SqliteQueryTest.php @@ -1,20 +1,13 @@ <?php -/** - * Created by JetBrains PhpStorm. - * User: RusMaxim - * Date: 09.05.13 - * Time: 21:41 - * To change this template use File | Settings | File Templates. - */ - namespace yiiunit\framework\db\sqlite; +use yiiunit\framework\db\QueryTest; -class SqliteQueryTest extends \yiiunit\framework\db\QueryTest +/** + * @group db + * @group sqlite + */ +class SqliteQueryTest extends QueryTest { - public function setUp() - { - $this->driverName = 'sqlite'; - parent::setUp(); - } + protected $driverName = 'sqlite'; } diff --git a/tests/unit/framework/db/sqlite/SqliteSchemaTest.php b/tests/unit/framework/db/sqlite/SqliteSchemaTest.php new file mode 100644 index 0000000..260bb4c --- /dev/null +++ b/tests/unit/framework/db/sqlite/SqliteSchemaTest.php @@ -0,0 +1,13 @@ +<?php +namespace yiiunit\framework\db\sqlite; + +use yiiunit\framework\db\SchemaTest; + +/** + * @group db + * @group sqlite + */ +class SqliteSchemaTest extends SchemaTest +{ + protected $driverName = 'sqlite'; +} diff --git a/tests/unit/framework/helpers/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php index 8c83278..ca8c95a 100644 --- a/tests/unit/framework/helpers/ArrayHelperTest.php +++ b/tests/unit/framework/helpers/ArrayHelperTest.php @@ -2,14 +2,85 @@ namespace yiiunit\framework\helpers; +use yii\base\Object; use yii\helpers\ArrayHelper; +use yii\test\TestCase; +use yii\data\Sort; -class ArrayHelperTest extends \yii\test\TestCase +class Post1 { - public function testMerge() + public $id = 23; + public $title = 'tt'; +} + +class Post2 extends Object +{ + public $id = 123; + public $content = 'test'; + private $secret = 's'; + public function getSecret() { + return $this->secret; + } +} +class Post3 extends Object +{ + public $id = 33; + public $subObject; + public function init() + { + $this->subObject = new Post2(); + } +} + +/** + * @group helpers + */ +class ArrayHelperTest extends TestCase +{ + public function testToArray() + { + $object = new Post1; + $this->assertEquals(get_object_vars($object), ArrayHelper::toArray($object)); + $object = new Post2; + $this->assertEquals(get_object_vars($object), ArrayHelper::toArray($object)); + + $object1 = new Post1; + $object2 = new Post2; + $this->assertEquals(array( + get_object_vars($object1), + get_object_vars($object2), + ), ArrayHelper::toArray(array( + $object1, + $object2, + ))); + + $object = new Post2; + $this->assertEquals(array( + 'id' => 123, + 'secret' => 's', + '_content' => 'test', + 'length' => 4, + ), ArrayHelper::toArray($object, array( + $object->className() => array( + 'id', 'secret', + '_content' => 'content', + 'length' => function ($post) { + return strlen($post->content); + } + )))); + + $object = new Post3(); + $this->assertEquals(get_object_vars($object), ArrayHelper::toArray($object, array(), false)); + $this->assertEquals(array( + 'id' => 33, + 'subObject' => array( + 'id' => 123, + 'content' => 'test', + ), + ), ArrayHelper::toArray($object)); } public function testRemove() @@ -19,6 +90,9 @@ class ArrayHelperTest extends \yii\test\TestCase $this->assertEquals($name, 'b'); $this->assertEquals($array, array('age' => 3)); + + $default = ArrayHelper::remove($array, 'nonExisting', 'defaultValue'); + $this->assertEquals('defaultValue', $default); } @@ -54,16 +128,172 @@ class ArrayHelperTest extends \yii\test\TestCase array('name' => 'A', 'age' => 1), ); - ArrayHelper::multisort($array, array('name', 'age'), SORT_ASC, array(SORT_STRING, SORT_REGULAR)); + ArrayHelper::multisort($array, array('name', 'age'), false, array(SORT_STRING, SORT_REGULAR)); $this->assertEquals(array('name' => 'A', 'age' => 1), $array[0]); $this->assertEquals(array('name' => 'B', 'age' => 4), $array[1]); $this->assertEquals(array('name' => 'a', 'age' => 3), $array[2]); $this->assertEquals(array('name' => 'b', 'age' => 2), $array[3]); - ArrayHelper::multisort($array, array('name', 'age'), SORT_ASC, array(SORT_STRING, SORT_REGULAR), false); + ArrayHelper::multisort($array, array('name', 'age'), false, array(SORT_STRING, SORT_REGULAR), false); $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]); $this->assertEquals(array('name' => 'B', 'age' => 4), $array[3]); } + + public function testMultisortUseSort() + { + // single key + $sort = new Sort(array( + 'attributes' => array('name', 'age'), + 'defaultOrder' => array('name' => Sort::ASC), + )); + $orders = $sort->getOrders(); + + $array = array( + array('name' => 'b', 'age' => 3), + array('name' => 'a', 'age' => 1), + array('name' => 'c', 'age' => 2), + ); + ArrayHelper::multisort($array, array_keys($orders), array_values($orders)); + $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 + $sort = new Sort(array( + 'attributes' => array('name', 'age'), + 'defaultOrder' => array('name' => Sort::ASC, 'age' => Sort::DESC), + )); + $orders = $sort->getOrders(); + + $array = array( + array('name' => 'b', 'age' => 3), + array('name' => 'a', 'age' => 2), + array('name' => 'a', 'age' => 1), + ); + ArrayHelper::multisort($array, array_keys($orders), array_values($orders)); + $this->assertEquals(array('name' => 'a', 'age' => 2), $array[0]); + $this->assertEquals(array('name' => 'a', 'age' => 1), $array[1]); + $this->assertEquals(array('name' => 'b', 'age' => 3), $array[2]); + } + + public function testMerge() + { + $a = array( + 'name' => 'Yii', + 'version' => '1.0', + 'options' => array( + 'namespace' => false, + 'unittest' => false, + ), + 'features' => array( + 'mvc', + ), + ); + $b = array( + 'version' => '1.1', + 'options' => array( + 'unittest' => true, + ), + 'features' => array( + 'gii', + ), + ); + $c = array( + 'version' => '2.0', + 'options' => array( + 'namespace' => true, + ), + 'features' => array( + 'debug', + ), + ); + + $result = ArrayHelper::merge($a, $b, $c); + $expected = array( + 'name' => 'Yii', + 'version' => '2.0', + 'options' => array( + 'namespace' => true, + 'unittest' => true, + ), + 'features' => array( + 'mvc', + 'gii', + 'debug', + ), + ); + + $this->assertEquals($expected, $result); + } + + public function testIndex() + { + $array = array( + array('id' => '123', 'data' => 'abc'), + array('id' => '345', 'data' => 'def'), + ); + $result = ArrayHelper::index($array, 'id'); + $this->assertEquals(array( + '123' => array('id' => '123', 'data' => 'abc'), + '345' => array('id' => '345', 'data' => 'def'), + ), $result); + + $result = ArrayHelper::index($array, function ($element) { + return $element['data']; + }); + $this->assertEquals(array( + 'abc' => array('id' => '123', 'data' => 'abc'), + 'def' => array('id' => '345', 'data' => 'def'), + ), $result); + } + + public function testGetColumn() + { + $array = array( + 'a' => array('id' => '123', 'data' => 'abc'), + 'b' => array('id' => '345', 'data' => 'def'), + ); + $result = ArrayHelper::getColumn($array, 'id'); + $this->assertEquals(array('a' => '123', 'b' => '345'), $result); + $result = ArrayHelper::getColumn($array, 'id', false); + $this->assertEquals(array('123', '345'), $result); + + $result = ArrayHelper::getColumn($array, function ($element) { + return $element['data']; + }); + $this->assertEquals(array('a' => 'abc', 'b' => 'def'), $result); + $result = ArrayHelper::getColumn($array, function ($element) { + return $element['data']; + }, false); + $this->assertEquals(array('abc', 'def'), $result); + } + + public function testMap() + { + $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'); + $this->assertEquals(array( + '123' => 'aaa', + '124' => 'bbb', + '345' => 'ccc', + ), $result); + + $result = ArrayHelper::map($array, 'id', 'name', 'class'); + $this->assertEquals(array( + 'x' => array( + '123' => 'aaa', + '124' => 'bbb', + ), + 'y' => array( + '345' => 'ccc', + ), + ), $result); + } } diff --git a/tests/unit/framework/helpers/ConsoleTest.php b/tests/unit/framework/helpers/ConsoleTest.php new file mode 100644 index 0000000..4b983f8 --- /dev/null +++ b/tests/unit/framework/helpers/ConsoleTest.php @@ -0,0 +1,82 @@ +<?php + +namespace yiiunit\framework\helpers; + +use Yii; +use yii\helpers\Console; +use yiiunit\TestCase; + +/** + * @group helpers + * @group console + */ +class ConsoleTest extends TestCase +{ + public function testStripAnsiFormat() + { + ob_start(); + ob_implicit_flush(false); + echo 'a'; + Console::moveCursorForward(1); + echo 'a'; + Console::moveCursorDown(1); + echo 'a'; + Console::moveCursorUp(1); + echo 'a'; + Console::moveCursorBackward(1); + echo 'a'; + Console::moveCursorNextLine(1); + echo 'a'; + Console::moveCursorPrevLine(1); + echo 'a'; + Console::moveCursorTo(1); + echo 'a'; + Console::moveCursorTo(1, 2); + echo 'a'; + Console::clearLine(); + echo 'a'; + Console::clearLineAfterCursor(); + echo 'a'; + Console::clearLineBeforeCursor(); + echo 'a'; + Console::clearScreen(); + echo 'a'; + Console::clearScreenAfterCursor(); + echo 'a'; + Console::clearScreenBeforeCursor(); + echo 'a'; + Console::scrollDown(); + echo 'a'; + Console::scrollUp(); + echo 'a'; + Console::hideCursor(); + echo 'a'; + Console::showCursor(); + echo 'a'; + Console::saveCursorPosition(); + echo 'a'; + Console::restoreCursorPosition(); + echo 'a'; + Console::beginAnsiFormat(array(Console::FG_GREEN, Console::BG_BLUE, Console::UNDERLINE)); + echo 'a'; + Console::endAnsiFormat(); + echo 'a'; + Console::beginAnsiFormat(array(Console::xtermBgColor(128), Console::xtermFgColor(55))); + echo 'a'; + Console::endAnsiFormat(); + echo 'a'; + $ouput = Console::stripAnsiFormat(ob_get_clean()); + ob_implicit_flush(true); + // $output = str_replace("\033", 'X003', $ouput );// uncomment for debugging + $this->assertEquals(str_repeat('a', 25), $ouput); + } + +/* public function testScreenSize() + { + for($i = 1; $i < 20; $i++) { + echo implode(', ', Console::getScreenSize(true)) . "\n"; + ob_flush(); + sleep(1); + } + }*/ +} diff --git a/tests/unit/framework/helpers/FileHelperTest.php b/tests/unit/framework/helpers/FileHelperTest.php new file mode 100644 index 0000000..05bd7af --- /dev/null +++ b/tests/unit/framework/helpers/FileHelperTest.php @@ -0,0 +1,316 @@ +<?php + +use yii\helpers\FileHelper; +use yii\test\TestCase; + +/** + * Unit test for [[yii\helpers\FileHelper]] + * @see FileHelper + * @group helpers + */ +class FileHelperTest extends TestCase +{ + /** + * @var string test files path. + */ + private $testFilePath = ''; + + public function setUp() + { + $this->testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . get_class($this); + $this->createDir($this->testFilePath); + if (!file_exists($this->testFilePath)) { + $this->markTestIncomplete('Unit tests runtime directory should have writable permissions!'); + } + } + + public function tearDown() + { + $this->removeDir($this->testFilePath); + } + + /** + * Creates directory. + * @param string $dirName directory full name. + */ + protected function createDir($dirName) + { + if (!file_exists($dirName)) { + mkdir($dirName, 0777, true); + } + } + + /** + * Removes directory. + * @param string $dirName directory full name. + */ + protected function removeDir($dirName) + { + if (!empty($dirName) && is_dir($dirName)) { + if ($handle = opendir($dirName)) { + while (false !== ($entry = readdir($handle))) { + if ($entry != '.' && $entry != '..') { + if (is_dir($dirName . DIRECTORY_SEPARATOR . $entry) === true) { + $this->removeDir($dirName . DIRECTORY_SEPARATOR . $entry); + } else { + unlink($dirName . DIRECTORY_SEPARATOR . $entry); + } + } + } + closedir($handle); + rmdir($dirName); + } + } + } + + /** + * Get file permission mode. + * @param string $file file name. + * @return string permission mode. + */ + protected function getMode($file) + { + return substr(sprintf('%o', fileperms($file)), -4); + } + + /** + * Creates test files structure, + * @param array $items file system objects to be created in format: objectName => objectContent + * Arrays specifies directories, other values - files. + * @param string $basePath structure base file path. + */ + protected function createFileStructure(array $items, $basePath = '') + { + if (empty($basePath)) { + $basePath = $this->testFilePath; + } + foreach ($items as $name => $content) { + $itemName = $basePath . DIRECTORY_SEPARATOR . $name; + if (is_array($content)) { + mkdir($itemName, 0777, true); + $this->createFileStructure($content, $itemName); + } else { + file_put_contents($itemName, $content); + } + } + } + + /** + * Asserts that file has specific permission mode. + * @param integer $expectedMode expected file permission mode. + * @param string $fileName file name. + * @param string $message error message + */ + protected function assertFileMode($expectedMode, $fileName, $message = '') + { + $expectedMode = sprintf('%o', $expectedMode); + $this->assertEquals($expectedMode, $this->getMode($fileName), $message); + } + + // Tests : + + public function testCopyDirectory() + { + $srcDirName = 'test_src_dir'; + $files = array( + 'file1.txt' => 'file 1 content', + 'file2.txt' => 'file 2 content', + ); + $this->createFileStructure(array( + $srcDirName => $files + )); + + $basePath = $this->testFilePath; + $srcDirName = $basePath . DIRECTORY_SEPARATOR . $srcDirName; + $dstDirName = $basePath . DIRECTORY_SEPARATOR . 'test_dst_dir'; + + FileHelper::copyDirectory($srcDirName, $dstDirName); + + $this->assertTrue(file_exists($dstDirName), 'Destination directory does not exist!'); + foreach ($files as $name => $content) { + $fileName = $dstDirName . DIRECTORY_SEPARATOR . $name; + $this->assertTrue(file_exists($fileName), 'Directory file is missing!'); + $this->assertEquals($content, file_get_contents($fileName), 'Incorrect file content!'); + } + } + + /** + * @depends testCopyDirectory + */ + public function testCopyDirectoryPermissions() + { + if (substr(PHP_OS, 0, 3) == 'WIN') { + $this->markTestSkipped("Can't reliably test it on Windows because fileperms() always return 0777."); + } + + $srcDirName = 'test_src_dir'; + $subDirName = 'test_sub_dir'; + $fileName = 'test_file.txt'; + $this->createFileStructure(array( + $srcDirName => array( + $subDirName => array(), + $fileName => 'test file content', + ), + )); + + $basePath = $this->testFilePath; + $srcDirName = $basePath . DIRECTORY_SEPARATOR . $srcDirName; + $dstDirName = $basePath . DIRECTORY_SEPARATOR . 'test_dst_dir'; + + $dirMode = 0755; + $fileMode = 0755; + $options = array( + 'dirMode' => $dirMode, + 'fileMode' => $fileMode, + ); + FileHelper::copyDirectory($srcDirName, $dstDirName, $options); + + $this->assertFileMode($dirMode, $dstDirName, 'Destination directory has wrong mode!'); + $this->assertFileMode($dirMode, $dstDirName . DIRECTORY_SEPARATOR . $subDirName, 'Copied sub directory has wrong mode!'); + $this->assertFileMode($fileMode, $dstDirName . DIRECTORY_SEPARATOR . $fileName, 'Copied file has wrong mode!'); + } + + public function testRemoveDirectory() + { + $dirName = 'test_dir_for_remove'; + $this->createFileStructure(array( + $dirName => array( + 'file1.txt' => 'file 1 content', + 'file2.txt' => 'file 2 content', + 'test_sub_dir' => array( + 'sub_dir_file_1.txt' => 'sub dir file 1 content', + 'sub_dir_file_2.txt' => 'sub dir file 2 content', + ), + ), + )); + + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; + + FileHelper::removeDirectory($dirName); + + $this->assertFalse(file_exists($dirName), 'Unable to remove directory!'); + + // should be silent about non-existing directories + FileHelper::removeDirectory($basePath . DIRECTORY_SEPARATOR . 'nonExisting'); + } + + public function testFindFiles() + { + $dirName = 'test_dir'; + $this->createFileStructure(array( + $dirName => array( + 'file_1.txt' => 'file 1 content', + 'file_2.txt' => 'file 2 content', + 'test_sub_dir' => array( + 'file_1_1.txt' => 'sub dir file 1 content', + 'file_1_2.txt' => 'sub dir file 2 content', + ), + ), + )); + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; + $expectedFiles = array( + $dirName . DIRECTORY_SEPARATOR . 'file_1.txt', + $dirName . DIRECTORY_SEPARATOR . 'file_2.txt', + $dirName . DIRECTORY_SEPARATOR . 'test_sub_dir' . DIRECTORY_SEPARATOR . 'file_1_1.txt', + $dirName . DIRECTORY_SEPARATOR . 'test_sub_dir' . DIRECTORY_SEPARATOR . 'file_1_2.txt', + ); + + $foundFiles = FileHelper::findFiles($dirName); + sort($expectedFiles); + sort($foundFiles); + $this->assertEquals($expectedFiles, $foundFiles); + } + + /** + * @depends testFindFiles + */ + public function testFindFileFilter() + { + $dirName = 'test_dir'; + $passedFileName = 'passed.txt'; + $this->createFileStructure(array( + $dirName => array( + $passedFileName => 'passed file content', + 'declined.txt' => 'declined file content', + ), + )); + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; + + $options = array( + 'filter' => function ($path) use ($passedFileName) { + return $passedFileName == basename($path); + } + ); + $foundFiles = FileHelper::findFiles($dirName, $options); + $this->assertEquals(array($dirName . DIRECTORY_SEPARATOR . $passedFileName), $foundFiles); + } + + /** + * @depends testFindFiles + */ + public function testFindFilesExclude() + { + $dirName = 'test_dir'; + $fileName = 'test_file.txt'; + $excludeFileName = 'exclude_file.txt'; + $this->createFileStructure(array( + $dirName => array( + $fileName => 'file content', + $excludeFileName => 'exclude file content', + ), + )); + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; + + $options = array( + 'except' => array($excludeFileName), + ); + $foundFiles = FileHelper::findFiles($dirName, $options); + $this->assertEquals(array($dirName . DIRECTORY_SEPARATOR . $fileName), $foundFiles); + } + + public function testCreateDirectory() + { + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . 'test_dir_level_1' . DIRECTORY_SEPARATOR . 'test_dir_level_2'; + $this->assertTrue(FileHelper::createDirectory($dirName), 'FileHelper::createDirectory should return true if directory was created!'); + $this->assertTrue(file_exists($dirName), 'Unable to create directory recursively!'); + $this->assertTrue(FileHelper::createDirectory($dirName), 'FileHelper::createDirectory should return true for already existing directories!'); + } + + public function testGetMimeTypeByExtension() + { + $magicFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'mime_type.php'; + $mimeTypeMap = array( + 'txa' => 'application/json', + 'txb' => 'another/mime', + ); + $magicFileContent = '<?php return ' . var_export($mimeTypeMap, true) . ';'; + file_put_contents($magicFile, $magicFileContent); + + foreach ($mimeTypeMap as $extension => $mimeType) { + $fileName = 'test.' . $extension; + $this->assertNull(FileHelper::getMimeTypeByExtension($fileName)); + $this->assertEquals($mimeType, FileHelper::getMimeTypeByExtension($fileName, $magicFile)); + } + } + + public function testGetMimeType() + { + $file = $this->testFilePath . DIRECTORY_SEPARATOR . 'mime_type_test.txt'; + file_put_contents($file, 'some text'); + $this->assertEquals('text/plain', FileHelper::getMimeType($file)); + + $file = $this->testFilePath . DIRECTORY_SEPARATOR . 'mime_type_test.json'; + file_put_contents($file, '{"a": "b"}'); + $this->assertEquals('text/plain', FileHelper::getMimeType($file)); + } + + public function testNormalizePath() + { + $this->assertEquals(DIRECTORY_SEPARATOR.'home'.DIRECTORY_SEPARATOR.'demo', FileHelper::normalizePath('/home\demo/')); + } +} diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index 6011594..88aa33a 100644 --- a/tests/unit/framework/helpers/HtmlTest.php +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -4,19 +4,25 @@ namespace yiiunit\framework\helpers; use Yii; use yii\helpers\Html; -use yii\web\Application; +use yiiunit\TestCase; -class HtmlTest extends \yii\test\TestCase +/** + * @group helpers + */ +class HtmlTest extends TestCase { - public function setUp() + protected function setUp() { - new Application(array( - 'id' => 'test', - 'basePath' => '@yiiunit/runtime', + parent::setUp(); + $this->mockApplication(array( 'components' => array( 'request' => array( 'class' => 'yii\web\Request', 'url' => '/test', + 'enableCsrfValidation' => false, + ), + 'response' => array( + 'class' => 'yii\web\Response', ), ), )); @@ -30,11 +36,6 @@ class HtmlTest extends \yii\test\TestCase $this->assertEquals($expected, $actual); } - public function tearDown() - { - Yii::$app = null; - } - public function testEncode() { $this->assertEquals("a<>&"'", Html::encode("a<>&\"'")); @@ -47,24 +48,11 @@ class HtmlTest extends \yii\test\TestCase public function testTag() { - $this->assertEquals('<br />', Html::tag('br')); - $this->assertEquals('<span></span>', Html::tag('span')); - $this->assertEquals('<div>content</div>', Html::tag('div', 'content')); - $this->assertEquals('<input type="text" name="test" value="<>" />', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); - - Html::$closeVoidElements = false; - $this->assertEquals('<br>', Html::tag('br')); $this->assertEquals('<span></span>', Html::tag('span')); $this->assertEquals('<div>content</div>', Html::tag('div', 'content')); $this->assertEquals('<input type="text" name="test" value="<>">', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); - - Html::$closeVoidElements = true; - - $this->assertEquals('<span disabled="disabled"></span>', Html::tag('span', '', array('disabled' => true))); - Html::$showBooleanAttributeValues = false; $this->assertEquals('<span disabled></span>', Html::tag('span', '', array('disabled' => true))); - Html::$showBooleanAttributeValues = true; } public function testBeginTag() @@ -79,36 +67,30 @@ class HtmlTest extends \yii\test\TestCase $this->assertEquals('</span>', Html::endTag('span')); } - public function testCdata() - { - $data = 'test<>'; - $this->assertEquals('<![CDATA[' . $data . ']]>', Html::cdata($data)); - } - public function testStyle() { $content = 'a <>'; - $this->assertEquals("<style type=\"text/css\">/*<![CDATA[*/\n{$content}\n/*]]>*/</style>", Html::style($content)); - $this->assertEquals("<style type=\"text/less\">/*<![CDATA[*/\n{$content}\n/*]]>*/</style>", Html::style($content, array('type' => 'text/less'))); + $this->assertEquals("<style>$content</style>", Html::style($content)); + $this->assertEquals("<style type=\"text/less\">$content</style>", Html::style($content, array('type' => 'text/less'))); } public function testScript() { $content = 'a <>'; - $this->assertEquals("<script type=\"text/javascript\">/*<![CDATA[*/\n{$content}\n/*]]>*/</script>", Html::script($content)); - $this->assertEquals("<script type=\"text/js\">/*<![CDATA[*/\n{$content}\n/*]]>*/</script>", Html::script($content, array('type' => 'text/js'))); + $this->assertEquals("<script>$content</script>", Html::script($content)); + $this->assertEquals("<script type=\"text/js\">$content</script>", Html::script($content, array('type' => 'text/js'))); } public function testCssFile() { - $this->assertEquals('<link type="text/css" href="http://example.com" rel="stylesheet" />', Html::cssFile('http://example.com')); - $this->assertEquals('<link type="text/css" href="/test" rel="stylesheet" />', Html::cssFile('')); + $this->assertEquals('<link href="http://example.com" rel="stylesheet">', Html::cssFile('http://example.com')); + $this->assertEquals('<link href="/test" rel="stylesheet">', Html::cssFile('')); } public function testJsFile() { - $this->assertEquals('<script type="text/javascript" src="http://example.com"></script>', Html::jsFile('http://example.com')); - $this->assertEquals('<script type="text/javascript" src="/test"></script>', Html::jsFile('')); + $this->assertEquals('<script src="http://example.com"></script>', Html::jsFile('http://example.com')); + $this->assertEquals('<script src="/test"></script>', Html::jsFile('')); } public function testBeginForm() @@ -116,8 +98,8 @@ class HtmlTest extends \yii\test\TestCase $this->assertEquals('<form action="/test" method="post">', Html::beginForm()); $this->assertEquals('<form action="/example" method="get">', Html::beginForm('/example', 'get')); $hiddens = array( - '<input type="hidden" name="id" value="1" />', - '<input type="hidden" name="title" value="<" />', + '<input type="hidden" name="id" value="1">', + '<input type="hidden" name="title" value="<">', ); $this->assertEquals('<form action="/example" method="get">' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); } @@ -142,9 +124,9 @@ class HtmlTest extends \yii\test\TestCase public function testImg() { - $this->assertEquals('<img src="/example" alt="" />', Html::img('/example')); - $this->assertEquals('<img src="/test" alt="" />', Html::img('')); - $this->assertEquals('<img src="/example" width="10" alt="something" />', Html::img('/example', array('alt' => 'something', 'width' => 10))); + $this->assertEquals('<img src="/example" alt="">', Html::img('/example')); + $this->assertEquals('<img src="/test" alt="">', Html::img('')); + $this->assertEquals('<img src="/example" width="10" alt="something">', Html::img('/example', array('alt' => 'something', 'width' => 10))); } public function testLabel() @@ -156,69 +138,69 @@ class HtmlTest extends \yii\test\TestCase public function testButton() { - $this->assertEquals('<button type="button">Button</button>', Html::button()); - $this->assertEquals('<button type="button" name="test" value="value">content<></button>', Html::button('content<>', 'test', 'value')); - $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::button('content<>', 'test', 'value', array('type' => 'submit', 'class' => "t"))); + $this->assertEquals('<button>Button</button>', Html::button()); + $this->assertEquals('<button name="test" value="value">content<></button>', Html::button('content<>', array('name' => 'test', 'value' => 'value'))); + $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::button('content<>', array('type' => 'submit', 'name' => 'test', 'value' => 'value', 'class' => "t"))); } public function testSubmitButton() { $this->assertEquals('<button type="submit">Submit</button>', Html::submitButton()); - $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::submitButton('content<>', 'test', 'value', array('class' => 't'))); + $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::submitButton('content<>', array('name' => 'test', 'value' => 'value', 'class' => 't'))); } public function testResetButton() { $this->assertEquals('<button type="reset">Reset</button>', Html::resetButton()); - $this->assertEquals('<button type="reset" class="t" name="test" value="value">content<></button>', Html::resetButton('content<>', 'test', 'value', array('class' => 't'))); + $this->assertEquals('<button type="reset" class="t" name="test" value="value">content<></button>', Html::resetButton('content<>', array('name' => 'test', 'value' => 'value', 'class' => 't'))); } public function testInput() { - $this->assertEquals('<input type="text" />', Html::input('text')); - $this->assertEquals('<input type="text" class="t" name="test" value="value" />', Html::input('text', 'test', 'value', array('class' => 't'))); + $this->assertEquals('<input type="text">', Html::input('text')); + $this->assertEquals('<input type="text" class="t" name="test" value="value">', Html::input('text', 'test', 'value', array('class' => 't'))); } public function testButtonInput() { - $this->assertEquals('<input type="button" name="test" value="Button" />', Html::buttonInput('test')); - $this->assertEquals('<input type="button" class="a" name="test" value="text" />', Html::buttonInput('test', 'text', array('class' => 'a'))); + $this->assertEquals('<input type="button" value="Button">', Html::buttonInput()); + $this->assertEquals('<input type="button" class="a" name="test" value="text">', Html::buttonInput('text', array('name' => 'test', 'class' => 'a'))); } public function testSubmitInput() { - $this->assertEquals('<input type="submit" value="Submit" />', Html::submitInput()); - $this->assertEquals('<input type="submit" class="a" name="test" value="text" />', Html::submitInput('test', 'text', array('class' => 'a'))); + $this->assertEquals('<input type="submit" value="Submit">', Html::submitInput()); + $this->assertEquals('<input type="submit" class="a" name="test" value="text">', Html::submitInput('text', array('name' => 'test', 'class' => 'a'))); } public function testResetInput() { - $this->assertEquals('<input type="reset" value="Reset" />', Html::resetInput()); - $this->assertEquals('<input type="reset" class="a" name="test" value="text" />', Html::resetInput('test', 'text', array('class' => 'a'))); + $this->assertEquals('<input type="reset" value="Reset">', Html::resetInput()); + $this->assertEquals('<input type="reset" class="a" name="test" value="text">', Html::resetInput('text', array('name' => 'test', 'class' => 'a'))); } public function testTextInput() { - $this->assertEquals('<input type="text" name="test" />', Html::textInput('test')); - $this->assertEquals('<input type="text" class="t" name="test" value="value" />', Html::textInput('test', 'value', array('class' => 't'))); + $this->assertEquals('<input type="text" name="test">', Html::textInput('test')); + $this->assertEquals('<input type="text" class="t" name="test" value="value">', Html::textInput('test', 'value', array('class' => 't'))); } public function testHiddenInput() { - $this->assertEquals('<input type="hidden" name="test" />', Html::hiddenInput('test')); - $this->assertEquals('<input type="hidden" class="t" name="test" value="value" />', Html::hiddenInput('test', 'value', array('class' => 't'))); + $this->assertEquals('<input type="hidden" name="test">', Html::hiddenInput('test')); + $this->assertEquals('<input type="hidden" class="t" name="test" value="value">', Html::hiddenInput('test', 'value', array('class' => 't'))); } public function testPasswordInput() { - $this->assertEquals('<input type="password" name="test" />', Html::passwordInput('test')); - $this->assertEquals('<input type="password" class="t" name="test" value="value" />', Html::passwordInput('test', 'value', array('class' => 't'))); + $this->assertEquals('<input type="password" name="test">', Html::passwordInput('test')); + $this->assertEquals('<input type="password" class="t" name="test" value="value">', Html::passwordInput('test', 'value', array('class' => 't'))); } public function testFileInput() { - $this->assertEquals('<input type="file" name="test" />', Html::fileInput('test')); - $this->assertEquals('<input type="file" class="t" name="test" value="value" />', Html::fileInput('test', 'value', array('class' => 't'))); + $this->assertEquals('<input type="file" name="test">', Html::fileInput('test')); + $this->assertEquals('<input type="file" class="t" name="test" value="value">', Html::fileInput('test', 'value', array('class' => 't'))); } public function testTextarea() @@ -229,16 +211,42 @@ class HtmlTest extends \yii\test\TestCase public function testRadio() { - $this->assertEquals('<input type="radio" name="test" value="1" />', Html::radio('test')); - $this->assertEquals('<input type="radio" class="a" name="test" checked="checked" />', Html::radio('test', true, array('class' => 'a', 'value' => null))); - $this->assertEquals('<input type="hidden" name="test" value="0" /><input type="radio" class="a" name="test" value="2" checked="checked" />', Html::radio('test', true, array('class' => 'a' , 'uncheck' => '0', 'value' => 2))); + $this->assertEquals('<input type="radio" name="test" value="1">', Html::radio('test')); + $this->assertEquals('<input type="radio" class="a" name="test" checked>', Html::radio('test', true, array('class' => 'a', 'value' => null))); + $this->assertEquals('<input type="hidden" name="test" value="0"><input type="radio" class="a" name="test" value="2" checked>', Html::radio('test', true, array('class' => 'a' , 'uncheck' => '0', 'value' => 2))); + + $this->assertEquals('<div class="radio"><label class="bbb"><input type="radio" class="a" name="test" checked> ccc</label></div>', Html::radio('test', true, array( + 'class' => 'a', + 'value' => null, + 'label' => 'ccc', + 'labelOptions' => array('class' =>'bbb'), + ))); + $this->assertEquals('<input type="hidden" name="test" value="0"><div class="radio"><label><input type="radio" class="a" name="test" value="2" checked> ccc</label></div>', Html::radio('test', true, array( + 'class' => 'a', + 'uncheck' => '0', + 'label' => 'ccc', + 'value' => 2, + ))); } public function testCheckbox() { - $this->assertEquals('<input type="checkbox" name="test" value="1" />', Html::checkbox('test')); - $this->assertEquals('<input type="checkbox" class="a" name="test" checked="checked" />', Html::checkbox('test', true, array('class' => 'a', 'value' => null))); - $this->assertEquals('<input type="hidden" name="test" value="0" /><input type="checkbox" class="a" name="test" value="2" checked="checked" />', Html::checkbox('test', true, array('class' => 'a', 'uncheck' => '0', 'value' => 2))); + $this->assertEquals('<input type="checkbox" name="test" value="1">', Html::checkbox('test')); + $this->assertEquals('<input type="checkbox" class="a" name="test" checked>', Html::checkbox('test', true, array('class' => 'a', 'value' => null))); + $this->assertEquals('<input type="hidden" name="test" value="0"><input type="checkbox" class="a" name="test" value="2" checked>', Html::checkbox('test', true, array('class' => 'a', 'uncheck' => '0', 'value' => 2))); + + $this->assertEquals('<div class="checkbox"><label class="bbb"><input type="checkbox" class="a" name="test" checked> ccc</label></div>', Html::checkbox('test', true, array( + 'class' => 'a', + 'value' => null, + 'label' => 'ccc', + 'labelOptions' => array('class' =>'bbb'), + ))); + $this->assertEquals('<input type="hidden" name="test" value="0"><div class="checkbox"><label><input type="checkbox" class="a" name="test" value="2" checked> ccc</label></div>', Html::checkbox('test', true, array( + 'class' => 'a', + 'uncheck' => '0', + 'label' => 'ccc', + 'value' => 2, + ))); } public function testDropDownList() @@ -259,7 +267,7 @@ EOD; $expected = <<<EOD <select name="test"> <option value="value1">text1</option> -<option value="value2" selected="selected">text2</option> +<option value="value2" selected>text2</option> </select> EOD; $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); @@ -290,26 +298,26 @@ EOD; $expected = <<<EOD <select name="test" size="4"> <option value="value1">text1</option> -<option value="value2" selected="selected">text2</option> +<option value="value2" selected>text2</option> </select> EOD; $this->assertEqualsWithoutLE($expected, Html::listBox('test', 'value2', $this->getDataItems())); $expected = <<<EOD <select name="test" size="4"> -<option value="value1" selected="selected">text1</option> -<option value="value2" selected="selected">text2</option> +<option value="value1" selected>text1</option> +<option value="value2" selected>text2</option> </select> EOD; $this->assertEqualsWithoutLE($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); $expected = <<<EOD -<select name="test[]" multiple="multiple" size="4"> +<select name="test[]" multiple size="4"> </select> EOD; $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, array(), array('multiple' => true))); $expected = <<<EOD -<input type="hidden" name="test" value="0" /><select name="test" size="4"> +<input type="hidden" name="test" value="0"><select name="test" size="4"> </select> EOD; @@ -318,32 +326,32 @@ EOD; public function testCheckboxList() { - $this->assertEquals('', Html::checkboxList('test')); + $this->assertEquals('<div></div>', Html::checkboxList('test')); $expected = <<<EOD -<label><input type="checkbox" name="test[]" value="value1" /> text1</label> -<label><input type="checkbox" name="test[]" value="value2" checked="checked" /> text2</label> +<div><div class="checkbox"><label><input type="checkbox" name="test[]" value="value1"> text1</label></div> +<div class="checkbox"><label><input type="checkbox" name="test[]" value="value2" checked> text2</label></div></div> EOD; $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); $expected = <<<EOD -<label><input type="checkbox" name="test[]" value="value1<>" /> text1<></label> -<label><input type="checkbox" name="test[]" value="value 2" /> text 2</label> +<div><div class="checkbox"><label><input type="checkbox" name="test[]" value="value1<>"> text1<></label></div> +<div class="checkbox"><label><input type="checkbox" name="test[]" value="value 2"> text 2</label></div></div> EOD; $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); $expected = <<<EOD -<input type="hidden" name="test" value="0" /><label><input type="checkbox" name="test[]" value="value1" /> text1</label><br /> -<label><input type="checkbox" name="test[]" value="value2" checked="checked" /> text2</label> +<input type="hidden" name="test" value="0"><div><div class="checkbox"><label><input type="checkbox" name="test[]" value="value1"> text1</label></div><br> +<div class="checkbox"><label><input type="checkbox" name="test[]" value="value2" checked> text2</label></div></div> EOD; $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( - 'separator' => "<br />\n", + 'separator' => "<br>\n", 'unselect' => '0', ))); $expected = <<<EOD -0<label>text1 <input type="checkbox" name="test[]" value="value1" /></label> -1<label>text2 <input type="checkbox" name="test[]" value="value2" checked="checked" /></label> +<div>0<label>text1 <input type="checkbox" name="test[]" value="value1"></label> +1<label>text2 <input type="checkbox" name="test[]" value="value2" checked></label></div> EOD; $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( 'item' => function ($index, $label, $name, $checked, $value) { @@ -354,32 +362,32 @@ EOD; public function testRadioList() { - $this->assertEquals('', Html::radioList('test')); + $this->assertEquals('<div></div>', Html::radioList('test')); $expected = <<<EOD -<label><input type="radio" name="test" value="value1" /> text1</label> -<label><input type="radio" name="test" value="value2" checked="checked" /> text2</label> +<div><div class="radio"><label><input type="radio" name="test" value="value1"> text1</label></div> +<div class="radio"><label><input type="radio" name="test" value="value2" checked> text2</label></div></div> EOD; $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems())); $expected = <<<EOD -<label><input type="radio" name="test" value="value1<>" /> text1<></label> -<label><input type="radio" name="test" value="value 2" /> text 2</label> +<div><div class="radio"><label><input type="radio" name="test" value="value1<>"> text1<></label></div> +<div class="radio"><label><input type="radio" name="test" value="value 2"> text 2</label></div></div> EOD; - $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); $expected = <<<EOD -<input type="hidden" name="test" value="0" /><label><input type="radio" name="test" value="value1" /> text1</label><br /> -<label><input type="radio" name="test" value="value2" checked="checked" /> text2</label> +<input type="hidden" name="test" value="0"><div><div class="radio"><label><input type="radio" name="test" value="value1"> text1</label></div><br> +<div class="radio"><label><input type="radio" name="test" value="value2" checked> text2</label></div></div> EOD; $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( - 'separator' => "<br />\n", + 'separator' => "<br>\n", 'unselect' => '0', ))); $expected = <<<EOD -0<label>text1 <input type="radio" name="test" value="value1" /></label> -1<label>text2 <input type="radio" name="test" value="value2" checked="checked" /></label> +<div>0<label>text1 <input type="radio" name="test" value="value1"></label> +1<label>text2 <input type="radio" name="test" value="value2" checked></label></div> EOD; $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( 'item' => function ($index, $label, $name, $checked, $value) { @@ -388,6 +396,64 @@ EOD; ))); } + public function testUl() + { + $data = array( + 1, 'abc', '<>', + ); + $expected = <<<EOD +<ul> +<li>1</li> +<li>abc</li> +<li><></li> +</ul> +EOD; + $this->assertEqualsWithoutLE($expected, Html::ul($data)); + $expected = <<<EOD +<ul class="test"> +<li class="item-0">1</li> +<li class="item-1">abc</li> +<li class="item-2"><></li> +</ul> +EOD; + $this->assertEqualsWithoutLE($expected, Html::ul($data, array( + 'class' => 'test', + 'item' => function ($item, $index) { + return "<li class=\"item-$index\">$item</li>"; + } + ))); + } + + public function testOl() + { + $data = array( + 1, 'abc', '<>', + ); + $expected = <<<EOD +<ol> +<li class="ti">1</li> +<li class="ti">abc</li> +<li class="ti"><></li> +</ol> +EOD; + $this->assertEqualsWithoutLE($expected, Html::ol($data, array( + 'itemOptions' => array('class' => 'ti'), + ))); + $expected = <<<EOD +<ol class="test"> +<li class="item-0">1</li> +<li class="item-1">abc</li> +<li class="item-2"><></li> +</ol> +EOD; + $this->assertEqualsWithoutLE($expected, Html::ol($data, array( + 'class' => 'test', + 'item' => function ($item, $index) { + return "<li class=\"item-$index\">$item</li>"; + } + ))); + } + public function testRenderOptions() { $data = array( @@ -404,11 +470,11 @@ EOD; ); $expected = <<<EOD <option value="">please select<></option> -<option value="value1" selected="selected">label1</option> +<option value="value1" selected>label1</option> <optgroup label="group1"> <option value="value11">label11</option> <optgroup label="group11"> -<option class="option" value="value111" selected="selected">label111</option> +<option class="option" value="value111" selected>label111</option> </optgroup> <optgroup class="group" label="group12"> @@ -435,9 +501,39 @@ EOD; { $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; + $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => true, 'disabled' => true, 'hidden' => false))); + } + + public function testAddCssClass() + { + $options = array(); + Html::addCssClass($options, 'test'); + $this->assertEquals(array('class' => 'test'), $options); + Html::addCssClass($options, 'test'); + $this->assertEquals(array('class' => 'test'), $options); + Html::addCssClass($options, 'test2'); + $this->assertEquals(array('class' => 'test test2'), $options); + Html::addCssClass($options, 'test'); + $this->assertEquals(array('class' => 'test test2'), $options); + Html::addCssClass($options, 'test2'); + $this->assertEquals(array('class' => 'test test2'), $options); + Html::addCssClass($options, 'test3'); + $this->assertEquals(array('class' => 'test test2 test3'), $options); + Html::addCssClass($options, 'test2'); + $this->assertEquals(array('class' => 'test test2 test3'), $options); + } + + public function testRemoveCssClass() + { + $options = array('class' => 'test test2 test3'); + Html::removeCssClass($options, 'test2'); + $this->assertEquals(array('class' => 'test test3'), $options); + Html::removeCssClass($options, 'test2'); + $this->assertEquals(array('class' => 'test test3'), $options); + Html::removeCssClass($options, 'test'); + $this->assertEquals(array('class' => 'test3'), $options); + Html::removeCssClass($options, 'test3'); + $this->assertEquals(array(), $options); } protected function getDataItems() diff --git a/tests/unit/framework/helpers/InflectorTest.php b/tests/unit/framework/helpers/InflectorTest.php new file mode 100644 index 0000000..de7fe01 --- /dev/null +++ b/tests/unit/framework/helpers/InflectorTest.php @@ -0,0 +1,144 @@ +<?php + +namespace yiiunit\framework\helpers; + +use Yii; +use yii\helpers\Inflector; +use yiiunit\TestCase; + +/** + * @group helpers + */ +class InflectorTest extends TestCase +{ + 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, Inflector::pluralize($testIn)); + $this->assertEquals(ucfirst($testOut), ucfirst(Inflector::pluralize($testIn))); + } + } + + public function testSingularize() + { + $testData = array( + 'moves' => 'move', + 'feet' => 'foot', + 'children' => 'child', + 'humans' => 'human', + 'men' => 'man', + 'staff' => 'staff', + 'teeth' => 'tooth', + 'people' => 'person', + 'mice' => 'mouse', + 'touches' => 'touch', + 'hashes' => 'hash', + 'shelves' => 'shelf', + 'potatoes' => 'potato', + 'buses' => 'bus', + 'tests' => 'test', + 'cars' => 'car', + ); + foreach ($testData as $testIn => $testOut) { + $this->assertEquals($testOut, Inflector::singularize($testIn)); + $this->assertEquals(ucfirst($testOut), ucfirst(Inflector::singularize($testIn))); + } + } + + public function testTitleize() + { + $this->assertEquals("Me my self and i", Inflector::titleize('MeMySelfAndI')); + $this->assertEquals("Me My Self And I", Inflector::titleize('MeMySelfAndI', true)); + } + + public function testCamelize() + { + $this->assertEquals("MeMySelfAndI", Inflector::camelize('me my_self-andI')); + } + + public function testUnderscore() + { + $this->assertEquals("me_my_self_and_i", Inflector::underscore('MeMySelfAndI')); + } + + public function testCamel2words() + { + $this->assertEquals('Camel Case', Inflector::camel2words('camelCase')); + $this->assertEquals('Lower Case', Inflector::camel2words('lower_case')); + $this->assertEquals('Tricky Stuff It Is Testing', Inflector::camel2words(' tricky_stuff.it-is testing... ')); + } + + public function testCamel2id() + { + $this->assertEquals('post-tag', Inflector::camel2id('PostTag')); + $this->assertEquals('post_tag', Inflector::camel2id('PostTag', '_')); + + $this->assertEquals('post-tag', Inflector::camel2id('postTag')); + $this->assertEquals('post_tag', Inflector::camel2id('postTag', '_')); + } + + public function testId2camel() + { + $this->assertEquals('PostTag', Inflector::id2camel('post-tag')); + $this->assertEquals('PostTag', Inflector::id2camel('post_tag', '_')); + + $this->assertEquals('PostTag', Inflector::id2camel('post-tag')); + $this->assertEquals('PostTag', Inflector::id2camel('post_tag', '_')); + } + + public function testHumanize() + { + $this->assertEquals("Me my self and i", Inflector::humanize('me_my_self_and_i')); + $this->assertEquals("Me My Self And I", Inflector::humanize('me_my_self_and_i', true)); + } + + public function testVariablize() + { + $this->assertEquals("customerTable", Inflector::variablize('customer_table')); + } + + public function testTableize() + { + $this->assertEquals("customer_tables", Inflector::tableize('customerTable')); + } + + public function testSlug() + { + $this->assertEquals("this-is-a-title", Inflector::slug('this is a title')); + } + + public function testClassify() + { + $this->assertEquals("CustomerTable", Inflector::classify('customer_tables')); + } + + public function testOrdinalize() + { + $this->assertEquals('21st', Inflector::ordinalize('21')); + $this->assertEquals('22nd', Inflector::ordinalize('22')); + $this->assertEquals('23rd', Inflector::ordinalize('23')); + $this->assertEquals('24th', Inflector::ordinalize('24')); + $this->assertEquals('25th', Inflector::ordinalize('25')); + $this->assertEquals('111th', Inflector::ordinalize('111')); + $this->assertEquals('113th', Inflector::ordinalize('113')); + } +} diff --git a/tests/unit/framework/helpers/JsonTest.php b/tests/unit/framework/helpers/JsonTest.php index 1795ce6..069fdc3 100644 --- a/tests/unit/framework/helpers/JsonTest.php +++ b/tests/unit/framework/helpers/JsonTest.php @@ -4,9 +4,13 @@ namespace yiiunit\framework\helpers; use yii\helpers\Json; +use yii\test\TestCase; use yii\web\JsExpression; -class JsonTest extends \yii\test\TestCase +/** + * @group helpers + */ +class JsonTest extends TestCase { public function testEncode() { @@ -22,7 +26,8 @@ class JsonTest extends \yii\test\TestCase // simple object encoding $data = new \stdClass(); - $data->a = 1; $data->b = 2; + $data->a = 1; + $data->b = 2; $this->assertSame('{"a":1,"b":2}', Json::encode($data)); // expression encoding @@ -40,6 +45,10 @@ class JsonTest extends \yii\test\TestCase 'b' => new JsExpression($expression2), ); $this->assertSame("{\"a\":[1,$expression1],\"b\":$expression2}", Json::encode($data)); + + // https://github.com/yiisoft/yii2/issues/957 + $data = (object)null; + $this->assertSame('{}', Json::encode($data)); } public function testDecode() diff --git a/tests/unit/framework/helpers/StringHelperTest.php b/tests/unit/framework/helpers/StringHelperTest.php index 64811d1..8af731d 100644 --- a/tests/unit/framework/helpers/StringHelperTest.php +++ b/tests/unit/framework/helpers/StringHelperTest.php @@ -1,11 +1,14 @@ <?php namespace yiiunit\framework\helpers; + use \yii\helpers\StringHelper as StringHelper; +use yii\test\TestCase; /** * StringHelperTest + * @group helpers */ -class StringHelperTest extends \yii\test\TestCase +class StringHelperTest extends TestCase { public function testStrlen() { @@ -19,58 +22,6 @@ class StringHelperTest extends \yii\test\TestCase $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', '_')); - } - public function testBasename() { $this->assertEquals('', StringHelper::basename('')); diff --git a/tests/unit/framework/helpers/VarDumperTest.php b/tests/unit/framework/helpers/VarDumperTest.php index a797121..d41a69d 100644 --- a/tests/unit/framework/helpers/VarDumperTest.php +++ b/tests/unit/framework/helpers/VarDumperTest.php @@ -1,12 +1,20 @@ <?php namespace yiiunit\framework\helpers; + use \yii\helpers\VarDumper; +use yii\test\TestCase; -class VarDumperTest extends \yii\test\TestCase +/** + * @group helpers + */ +class VarDumperTest extends TestCase { public function testDumpObject() { $obj = new \StdClass(); + ob_start(); VarDumper::dump($obj); + $this->assertEquals("stdClass#1\n(\n)", ob_get_contents()); + ob_end_clean(); } } diff --git a/tests/unit/framework/i18n/FormatterTest.php b/tests/unit/framework/i18n/FormatterTest.php new file mode 100644 index 0000000..6966853 --- /dev/null +++ b/tests/unit/framework/i18n/FormatterTest.php @@ -0,0 +1,94 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yiiunit\framework\i18n; + +use yii\i18n\Formatter; +use yiiunit\TestCase; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + * @group i18n + */ +class FormatterTest extends TestCase +{ + /** + * @var Formatter + */ + protected $formatter; + + protected function setUp() + { + parent::setUp(); + if (!extension_loaded('intl')) { + $this->markTestSkipped('intl extension is required.'); + } + $this->mockApplication(); + $this->formatter = new Formatter(array( + 'locale' => 'en_US', + )); + } + + protected function tearDown() + { + parent::tearDown(); + $this->formatter = null; + } + + public function testAsDecimal() + { + $value = '123'; + $this->assertSame($value, $this->formatter->asDecimal($value)); + $value = '123456'; + $this->assertSame("123,456", $this->formatter->asDecimal($value)); + $value = '-123456.123'; + $this->assertSame("-123,456.123", $this->formatter->asDecimal($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDecimal(null)); + } + + public function testAsPercent() + { + $value = '123'; + $this->assertSame('12,300%', $this->formatter->asPercent($value)); + $value = '0.1234'; + $this->assertSame("12%", $this->formatter->asPercent($value)); + $value = '-0.009343'; + $this->assertSame("-1%", $this->formatter->asPercent($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asPercent(null)); + } + + public function testAsScientific() + { + $value = '123'; + $this->assertSame('1.23E2', $this->formatter->asScientific($value)); + $value = '123456'; + $this->assertSame("1.23456E5", $this->formatter->asScientific($value)); + $value = '-123456.123'; + $this->assertSame("-1.23456123E5", $this->formatter->asScientific($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asScientific(null)); + } + + public function testAsCurrency() + { + $value = '123'; + $this->assertSame('$123.00', $this->formatter->asCurrency($value)); + $value = '123.456'; + $this->assertSame("$123.46", $this->formatter->asCurrency($value)); + $value = '-123456.123'; + $this->assertSame("($123,456.12)", $this->formatter->asCurrency($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asCurrency(null)); + } + + public function testDate() + { + $time = time(); + $this->assertSame(date('n/j/y', $time), $this->formatter->asDate($time)); + $this->assertSame(date('F j, Y', $time), $this->formatter->asDate($time, 'long')); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null)); + } +} diff --git a/tests/unit/framework/i18n/GettextMessageSourceTest.php b/tests/unit/framework/i18n/GettextMessageSourceTest.php index 7b499f4..5c10ff2 100644 --- a/tests/unit/framework/i18n/GettextMessageSourceTest.php +++ b/tests/unit/framework/i18n/GettextMessageSourceTest.php @@ -5,10 +5,13 @@ namespace yiiunit\framework\i18n; use yii\i18n\GettextMessageSource; use yiiunit\TestCase; +/** + * @group i18n + */ class GettextMessageSourceTest extends TestCase { public function testLoadMessages() { - $this->markTestSkipped(); + $this->markTestIncomplete(); } } diff --git a/tests/unit/framework/i18n/GettextMoFileTest.php b/tests/unit/framework/i18n/GettextMoFileTest.php index 0aa22da..9b61145 100644 --- a/tests/unit/framework/i18n/GettextMoFileTest.php +++ b/tests/unit/framework/i18n/GettextMoFileTest.php @@ -5,6 +5,9 @@ namespace yiiunit\framework\i18n; use yii\i18n\GettextMoFile; use yiiunit\TestCase; +/** + * @group i18n + */ class GettextMoFileTest extends TestCase { public function testLoad() diff --git a/tests/unit/framework/i18n/GettextPoFileTest.php b/tests/unit/framework/i18n/GettextPoFileTest.php index 8dddb40..4165b81 100644 --- a/tests/unit/framework/i18n/GettextPoFileTest.php +++ b/tests/unit/framework/i18n/GettextPoFileTest.php @@ -5,6 +5,9 @@ namespace yiiunit\framework\i18n; use yii\i18n\GettextPoFile; use yiiunit\TestCase; +/** + * @group i18n + */ class GettextPoFileTest extends TestCase { public function testLoad() diff --git a/tests/unit/framework/i18n/I18NTest.php b/tests/unit/framework/i18n/I18NTest.php new file mode 100644 index 0000000..ac19c02 --- /dev/null +++ b/tests/unit/framework/i18n/I18NTest.php @@ -0,0 +1,85 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yiiunit\framework\i18n; + +use yii\base\Model; +use yii\i18n\I18N; +use yii\i18n\MessageFormatter; +use yii\i18n\PhpMessageSource; +use yiiunit\TestCase; + +/** + * @author Carsten Brandt <mail@cebe.cc> + * @since 2.0 + * @group i18n + */ +class I18NTest extends TestCase +{ + /** + * @var I18N + */ + public $i18n; + + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + $this->i18n = new I18N(array( + 'translations' => array( + 'test' => new PhpMessageSource(array( + 'basePath' => '@yiiunit/data/i18n/messages', + )) + ) + )); + } + + public function testTranslate() + { + $msg = 'The dog runs fast.'; + $this->assertEquals('The dog runs fast.', $this->i18n->translate('test', $msg, array(), 'en_US')); + $this->assertEquals('Der Hund rennt schnell.', $this->i18n->translate('test', $msg, array(), 'de_DE')); + } + + public function testTranslateParams() + { + $msg = 'His speed is about {n} km/h.'; + $params = array( + 'n' => 42, + ); + $this->assertEquals('His speed is about 42 km/h.', $this->i18n->translate('test', $msg, $params, 'en_US')); + $this->assertEquals('Seine Geschwindigkeit beträgt 42 km/h.', $this->i18n->translate('test', $msg, $params, 'de_DE')); + + $msg = 'His name is {name} and his speed is about {n, number} km/h.'; + $params = array( + 'n' => 42, + 'name' => 'DA VINCI', // http://petrix.com/dognames/d.html + ); + $this->assertEquals('His name is DA VINCI and his speed is about 42 km/h.', $this->i18n->translate('test', $msg, $params, 'en_US')); + $this->assertEquals('Er heißt DA VINCI und ist 42 km/h schnell.', $this->i18n->translate('test', $msg, $params, 'de_DE')); + } + + public function testSpecialParams() + { + $msg = 'His speed is about {0} km/h.'; + + $this->assertEquals('His speed is about 0 km/h.', $this->i18n->translate('test', $msg, 0, 'en_US')); + $this->assertEquals('His speed is about 42 km/h.', $this->i18n->translate('test', $msg, 42, 'en_US')); + $this->assertEquals('His speed is about {0} km/h.', $this->i18n->translate('test', $msg, null, 'en_US')); + $this->assertEquals('His speed is about {0} km/h.', $this->i18n->translate('test', $msg, array(), 'en_US')); + + $msg = 'His name is {name} and he is {age} years old.'; + $model = new ParamModel(); + $this->assertEquals('His name is peer and he is 5 years old.', $this->i18n->translate('test', $msg, $model, 'en_US')); + } +} + +class ParamModel extends Model +{ + public $name = 'peer'; + public $age = 5; +} \ No newline at end of file diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php new file mode 100644 index 0000000..2465409 --- /dev/null +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -0,0 +1,177 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yiiunit\framework\i18n; + +use yii\i18n\MessageFormatter; +use yiiunit\TestCase; + +/** + * @author Alexander Makarov <sam@rmcreative.ru> + * @since 2.0 + * @group i18n + */ +class MessageFormatterTest extends TestCase +{ + const N = 'n'; + const N_VALUE = 42; + const SUBJECT = 'сабж'; + const SUBJECT_VALUE = 'Answer to the Ultimate Question of Life, the Universe, and Everything'; + + protected function setUp() + { + if (!extension_loaded("intl")) { + $this->markTestSkipped("intl not installed. Skipping."); + } + } + + public function patterns() + { + return array( + array( + '{'.self::SUBJECT.'} is {'.self::N.', number}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + array( // params + self::N => self::N_VALUE, + self::SUBJECT => self::SUBJECT_VALUE, + ) + ), + + // This one was provided by Aura.Intl. Thanks! + array(<<<_MSG_ +{gender_of_host, select, + female {{num_guests, plural, offset:1 + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to her party.} + =2 {{host} invites {guest} and one other person to her party.} + other {{host} invites {guest} and # other people to her party.}}} + male {{num_guests, plural, offset:1 + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to his party.} + =2 {{host} invites {guest} and one other person to his party.} + other {{host} invites {guest} and # other people to his party.}}} + other {{num_guests, plural, offset:1 + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to their party.} + =2 {{host} invites {guest} and one other person to their party.} + other {{host} invites {guest} and # other people to their party.}}}} +_MSG_ + , + 'ralph invites beep and 3 other people to his party.', + array( + 'gender_of_host' => 'male', + 'num_guests' => 4, + 'host' => 'ralph', + 'guest' => 'beep' + ) + ), + + array( + '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', + 'Alexander is male and he loves Yii!', + array( + 'name' => 'Alexander', + 'gender' => 'male', + ), + ), + + // verify pattern in select does not get replaced + array( + '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', + 'Alexander is male and he loves Yii!', + array( + 'name' => 'Alexander', + 'gender' => 'male', + // following should not be replaced + 'he' => 'wtf', + 'she' => 'wtf', + 'it' => 'wtf', + ) + ), + + // verify pattern in select message gets replaced + array( + '{name} is {gender} and {gender, select, female{she} male{{he}} other{it}} loves Yii!', + 'Alexander is male and wtf loves Yii!', + array( + 'name' => 'Alexander', + 'gender' => 'male', + 'he' => 'wtf', + 'she' => 'wtf', + ), + ), + + // some parser specific verifications + array( + '{gender} and {gender, select, female{she} male{{he}} other{it}} loves {nr, number} is {gender}!', + 'male and wtf loves 42 is male!', + array( + 'nr' => 42, + 'gender' => 'male', + 'he' => 'wtf', + 'she' => 'wtf', + ), + ), + ); + } + + /** + * @dataProvider patterns + */ + public function testNamedArgumentsStatic($pattern, $expected, $args) + { + $result = MessageFormatter::formatMessage('en_US', $pattern, $args); + $this->assertEquals($expected, $result, intl_get_error_message()); + } + + /** + * @dataProvider patterns + */ + public function testNamedArgumentsObject($pattern, $expected, $args) + { + $formatter = new MessageFormatter('en_US', $pattern); + $result = $formatter->format($args); + $this->assertEquals($expected, $result, $formatter->getErrorMessage()); + } + + public function testInsufficientArguments() + { + $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; + + $result = MessageFormatter::formatMessage('en_US', '{'.self::SUBJECT.'} is {'.self::N.', number}', array( + self::N => self::N_VALUE, + )); + + $this->assertEquals($expected, $result, intl_get_error_message()); + } + + /** + * When instantiating a MessageFormatter with invalid pattern it should be null with default settings. + * It will be IntlException if intl.use_exceptions=1 and PHP 5.5 or newer or an error if intl.error_level is not 0. + */ + public function testNullConstructor() + { + if(ini_get('intl.use_exceptions')) { + $this->setExpectedException('IntlException'); + } + + if (!ini_get('intl.error_level') || ini_get('intl.use_exceptions')) { + $this->assertNull(new MessageFormatter('en_US', '')); + } + } + + public function testNoParams() + { + $pattern = '{'.self::SUBJECT.'} is '.self::N; + $result = MessageFormatter::formatMessage('en_US', $pattern, array()); + $this->assertEquals($pattern, $result, intl_get_error_message()); + + $formatter = new MessageFormatter('en_US', $pattern); + $result = $formatter->format(array()); + $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); + } +} \ No newline at end of file diff --git a/tests/unit/framework/rbac/ManagerTestBase.php b/tests/unit/framework/rbac/ManagerTestCase.php similarity index 88% rename from tests/unit/framework/rbac/ManagerTestBase.php rename to tests/unit/framework/rbac/ManagerTestCase.php index 03d5354..7cb4941 100644 --- a/tests/unit/framework/rbac/ManagerTestBase.php +++ b/tests/unit/framework/rbac/ManagerTestCase.php @@ -6,7 +6,7 @@ use yii\rbac\Assignment; use yii\rbac\Item; use yiiunit\TestCase; -abstract class ManagerTestBase extends TestCase +abstract class ManagerTestCase extends TestCase { /** @var \yii\rbac\PhpManager|\yii\rbac\DbManager */ protected $auth; @@ -32,7 +32,7 @@ abstract class ManagerTestBase extends TestCase $this->assertEquals($item2->type, Item::TYPE_ROLE); // test adding an item with the same name - $this->setExpectedException('Exception'); + $this->setExpectedException('\yii\base\Exception'); $this->auth->createItem($name, $type, $description, $bizRule, $data); } @@ -57,6 +57,7 @@ abstract class ManagerTestBase extends TestCase $this->assertTrue($item instanceof Item); $this->assertTrue($this->auth->hasItemChild('reader', 'readPost')); $item->name = 'readPost2'; + $item->save(); $this->assertNull($this->auth->getItem('readPost')); $this->assertEquals($this->auth->getItem('readPost2'), $item); $this->assertFalse($this->auth->hasItemChild('reader', 'readPost')); @@ -68,14 +69,14 @@ abstract class ManagerTestBase extends TestCase $this->auth->addItemChild('createPost', 'updatePost'); // test adding upper level item to lower one - $this->setExpectedException('Exception'); + $this->setExpectedException('\yii\base\Exception'); $this->auth->addItemChild('readPost', 'reader'); } public function testAddItemChild2() { // test adding inexistent items - $this->setExpectedException('Exception'); + $this->setExpectedException('\yii\base\Exception'); $this->assertFalse($this->auth->addItemChild('createPost2', 'updatePost')); } @@ -104,7 +105,7 @@ abstract class ManagerTestBase extends TestCase $this->assertEquals($auth->bizRule, 'rule'); $this->assertEquals($auth->data, 'data'); - $this->setExpectedException('Exception'); + $this->setExpectedException('\yii\base\Exception'); $this->auth->assign('new user', 'createPost2', 'rule', 'data'); } @@ -157,16 +158,16 @@ abstract class ManagerTestBase extends TestCase public function testDetectLoop() { - $this->setExpectedException('Exception'); + $this->setExpectedException('\yii\base\Exception'); $this->auth->addItemChild('readPost', 'readPost'); } public function testExecuteBizRule() { $this->assertTrue($this->auth->executeBizRule(null, array(), null)); - $this->assertTrue($this->auth->executeBizRule('return 1==true;', array(), null)); - $this->assertTrue($this->auth->executeBizRule('return $params[0]==$params[1];', array(1, '1'), null)); - $this->assertFalse($this->auth->executeBizRule('invalid', array(), null)); + $this->assertTrue($this->auth->executeBizRule('return 1 == true;', array(), null)); + $this->assertTrue($this->auth->executeBizRule('return $params[0] == $params[1];', array(1, '1'), null)); + $this->assertFalse($this->auth->executeBizRule('invalid;', array(), null)); } public function testCheckAccess() @@ -220,7 +221,7 @@ abstract class ManagerTestBase extends TestCase $this->auth->createOperation('updatePost', 'update a post'); $this->auth->createOperation('deletePost', 'delete a post'); - $task = $this->auth->createTask('updateOwnPost', 'update a post by author himself', 'return $params["authorID"]==$params["userID"];'); + $task = $this->auth->createTask('updateOwnPost', 'update a post by author himself', 'return $params["authorID"] == $params["userID"];'); $task->addChild('updatePost'); $role = $this->auth->createRole('reader'); diff --git a/tests/unit/framework/rbac/PhpManagerTest.php b/tests/unit/framework/rbac/PhpManagerTest.php index 69fdd41..8c5d366 100644 --- a/tests/unit/framework/rbac/PhpManagerTest.php +++ b/tests/unit/framework/rbac/PhpManagerTest.php @@ -5,12 +5,15 @@ namespace yiiunit\framework\rbac; use Yii; use yii\rbac\PhpManager; -require_once(__DIR__ . '/ManagerTestBase.php'); - -class PhpManagerTest extends ManagerTestBase +/** + * @group rbac + */ +class PhpManagerTest extends ManagerTestCase { - public function setUp() + protected function setUp() { + parent::setUp(); + $this->mockApplication(); $authFile = Yii::$app->getRuntimePath() . '/rbac.php'; @unlink($authFile); $this->auth = new PhpManager; @@ -19,8 +22,9 @@ class PhpManagerTest extends ManagerTestBase $this->prepareData(); } - public function tearDown() + protected function tearDown() { + parent::tearDown(); @unlink($this->auth->authFile); } diff --git a/tests/unit/framework/requirements/YiiRequirementCheckerTest.php b/tests/unit/framework/requirements/YiiRequirementCheckerTest.php new file mode 100644 index 0000000..652d003 --- /dev/null +++ b/tests/unit/framework/requirements/YiiRequirementCheckerTest.php @@ -0,0 +1,197 @@ +<?php + +require_once(__DIR__.'/../../../../framework/yii/requirements/YiiRequirementChecker.php'); + +use yiiunit\TestCase; + +/** + * Test case for [[YiiRequirementChecker]]. + * @see YiiRequirementChecker + * @group requirements + */ +class YiiRequirementCheckerTest extends TestCase +{ + public function testCheck() + { + $requirementsChecker = new YiiRequirementChecker(); + + $requirements = array( + 'requirementPass' => array( + 'name' => 'Requirement 1', + 'mandatory' => true, + 'condition' => true, + 'by' => 'Requirement 1', + 'memo' => 'Requirement 1', + ), + 'requirementError' => array( + 'name' => 'Requirement 2', + 'mandatory' => true, + 'condition' => false, + 'by' => 'Requirement 2', + 'memo' => 'Requirement 2', + ), + 'requirementWarning' => array( + 'name' => 'Requirement 3', + 'mandatory' => false, + 'condition' => false, + 'by' => 'Requirement 3', + 'memo' => 'Requirement 3', + ), + ); + + $checkResult = $requirementsChecker->check($requirements)->getResult(); + $summary = $checkResult['summary']; + + $this->assertEquals(count($requirements), $summary['total'], 'Wrong summary total!'); + $this->assertEquals(1, $summary['errors'], 'Wrong summary errors!'); + $this->assertEquals(1, $summary['warnings'], 'Wrong summary warnings!'); + + $checkedRequirements = $checkResult['requirements']; + $requirementsKeys = array_flip(array_keys($requirements)); + + $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementPass']]['error'], 'Passed requirement has an error!'); + $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementPass']]['warning'], 'Passed requirement has a warning!'); + + $this->assertEquals(true, $checkedRequirements[$requirementsKeys['requirementError']]['error'], 'Error requirement has no error!'); + + $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementWarning']]['error'], 'Error requirement has an error!'); + $this->assertEquals(true, $checkedRequirements[$requirementsKeys['requirementWarning']]['warning'], 'Error requirement has no warning!'); + } + + /** + * @depends testCheck + */ + public function testCheckEval() + { + $requirementsChecker = new YiiRequirementChecker(); + + $requirements = array( + 'requirementPass' => array( + 'name' => 'Requirement 1', + 'mandatory' => true, + 'condition' => 'eval:2>1', + 'by' => 'Requirement 1', + 'memo' => 'Requirement 1', + ), + 'requirementError' => array( + 'name' => 'Requirement 2', + 'mandatory' => true, + 'condition' => 'eval:2<1', + 'by' => 'Requirement 2', + 'memo' => 'Requirement 2', + ), + ); + + $checkResult = $requirementsChecker->check($requirements)->getResult(); + $checkedRequirements = $checkResult['requirements']; + $requirementsKeys = array_flip(array_keys($requirements)); + + $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementPass']]['error'], 'Passed requirement has an error!'); + $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementPass']]['warning'], 'Passed requirement has a warning!'); + + $this->assertEquals(true, $checkedRequirements[$requirementsKeys['requirementError']]['error'], 'Error requirement has no error!'); + } + + /** + * @depends testCheck + */ + public function testCheckChained() + { + $requirementsChecker = new YiiRequirementChecker(); + + $requirements1 = array( + array( + 'name' => 'Requirement 1', + 'mandatory' => true, + 'condition' => true, + 'by' => 'Requirement 1', + 'memo' => 'Requirement 1', + ), + ); + $requirements2 = array( + array( + 'name' => 'Requirement 2', + 'mandatory' => true, + 'condition' => true, + 'by' => 'Requirement 2', + 'memo' => 'Requirement 2', + ), + ); + $checkResult = $requirementsChecker->check($requirements1)->check($requirements2)->getResult(); + + $mergedRequirements = array_merge($requirements1, $requirements2); + + $this->assertEquals(count($mergedRequirements), $checkResult['summary']['total'], 'Wrong total checks count!'); + foreach ($mergedRequirements as $key => $mergedRequirement) { + $this->assertEquals($mergedRequirement['name'], $checkResult['requirements'][$key]['name'], 'Wrong requirements list!'); + } + } + + public function testCheckPhpExtensionVersion() + { + $requirementsChecker = new YiiRequirementChecker(); + + $this->assertFalse($requirementsChecker->checkPhpExtensionVersion('some_unexisting_php_extension', '0.1'), 'No fail while checking unexisting extension!'); + $this->assertTrue($requirementsChecker->checkPhpExtensionVersion('pdo', '1.0'), 'Unable to check PDO version!'); + } + + /** + * Data provider for [[testGetByteSize()]]. + * @return array + */ + public function dataProviderGetByteSize() + { + return array( + array('456', 456), + array('5K', 5*1024), + array('16KB', 16*1024), + array('4M', 4*1024*1024), + array('14MB', 14*1024*1024), + array('7G', 7*1024*1024*1024), + array('12GB', 12*1024*1024*1024), + ); + } + + /** + * @dataProvider dataProviderGetByteSize + * + * @param string $verboseValue verbose value. + * @param integer $expectedByteSize expected byte size. + */ + public function testGetByteSize($verboseValue, $expectedByteSize) + { + $requirementsChecker = new YiiRequirementChecker(); + + $this->assertEquals($expectedByteSize, $requirementsChecker->getByteSize($verboseValue), "Wrong byte size for '{$verboseValue}'!"); + } + + /** + * Data provider for [[testCompareByteSize()]] + * @return array + */ + public function dataProviderCompareByteSize() + { + return array( + array('2M', '2K', '>', true), + array('2M', '2K', '>=', true), + array('1K', '1024', '==', true), + array('10M', '11M', '<', true), + array('10M', '11M', '<=', true), + ); + } + + /** + * @depends testGetByteSize + * @dataProvider dataProviderCompareByteSize + * + * @param string $a first value. + * @param string $b second value. + * @param string $compare comparison. + * @param boolean $expectedComparisonResult expected comparison result. + */ + public function testCompareByteSize($a, $b, $compare, $expectedComparisonResult) + { + $requirementsChecker = new YiiRequirementChecker(); + $this->assertEquals($expectedComparisonResult, $requirementsChecker->compareByteSize($a, $b, $compare), "Wrong compare '{$a}{$compare}{$b}'"); + } +} diff --git a/tests/unit/framework/validators/BooleanValidatorTest.php b/tests/unit/framework/validators/BooleanValidatorTest.php new file mode 100644 index 0000000..f37b39a --- /dev/null +++ b/tests/unit/framework/validators/BooleanValidatorTest.php @@ -0,0 +1,53 @@ +<?php +namespace yiiunit\framework\validators; + +use yiiunit\data\validators\models\FakedValidationModel; +use yii\validators\BooleanValidator; +use yiiunit\TestCase; + +/** + * BooleanValidatorTest + */ +class BooleanValidatorTest extends TestCase +{ + public function testValidateValue() + { + $val = new BooleanValidator; + $this->assertTrue($val->validateValue(true)); + $this->assertTrue($val->validateValue(false)); + $this->assertTrue($val->validateValue('0')); + $this->assertTrue($val->validateValue('1')); + $this->assertFalse($val->validateValue(null)); + $this->assertFalse($val->validateValue(array())); + $val->strict = true; + $this->assertTrue($val->validateValue('0')); + $this->assertTrue($val->validateValue('1')); + $this->assertFalse($val->validateValue(true)); + $this->assertFalse($val->validateValue(false)); + $val->trueValue = true; + $val->falseValue = false; + $this->assertFalse($val->validateValue('0')); + $this->assertFalse($val->validateValue(array())); + $this->assertTrue($val->validateValue(true)); + $this->assertTrue($val->validateValue(false)); + } + + public function testValidateAttributeAndError() + { + $obj = new FakedValidationModel; + $obj->attrA = true; + $obj->attrB = '1'; + $obj->attrC = '0'; + $obj->attrD = array(); + $val = new BooleanValidator; + $val->validateAttribute($obj, 'attrA'); + $this->assertFalse($obj->hasErrors('attrA')); + $val->validateAttribute($obj, 'attrC'); + $this->assertFalse($obj->hasErrors('attrC')); + $val->strict = true; + $val->validateAttribute($obj, 'attrB'); + $this->assertFalse($obj->hasErrors('attrB')); + $val->validateAttribute($obj, 'attrD'); + $this->assertTrue($obj->hasErrors('attrD')); + } +} diff --git a/tests/unit/framework/validators/CompareValidatorTest.php b/tests/unit/framework/validators/CompareValidatorTest.php new file mode 100644 index 0000000..5a16a30 --- /dev/null +++ b/tests/unit/framework/validators/CompareValidatorTest.php @@ -0,0 +1,171 @@ +<?php +namespace yiiunit\framework\validators; + +use yii\base\InvalidConfigException; +use yii\validators\CompareValidator; +use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\TestCase; + + + +class CompareValidatorTest extends TestCase +{ + + public function testValidateValueException() + { + $this->setExpectedException('yii\base\InvalidConfigException'); + $val = new CompareValidator; + $val->validateValue('val'); + } + + public function testValidateValue() + { + $value = 18449; + // default config + $val = new CompareValidator(array('compareValue' => $value)); + $this->assertTrue($val->validateValue($value)); + $this->assertTrue($val->validateValue((string)$value)); + $this->assertFalse($val->validateValue($value + 1)); + foreach ($this->getOperationTestData($value) as $op => $tests) { + $val = new CompareValidator(array('compareValue' => $value)); + $val->operator = $op; + foreach ($tests as $test) { + $this->assertEquals($test[1], $val->validateValue($test[0])); + } + } + } + + protected function getOperationTestData($value) + { + return array( + '===' => array( + array($value, true), + array((string)$value, false), + array((float)$value, false), + array($value + 1, false), + ), + '!=' => array( + array($value, false), + array((string)$value, false), + array((float)$value, false), + array($value + 0.00001, true), + array(false, true), + ), + '!==' => array( + array($value, false), + array((string)$value, true), + array((float)$value, true), + array(false, true), + ), + '>' => array( + array($value, false), + array($value + 1, true), + array($value - 1, false), + ), + '>=' => array( + array($value, true), + array($value + 1, true), + array($value - 1, false), + ), + '<' => array( + array($value, false), + array($value + 1, false), + array($value - 1, true), + ), + '<=' => array( + array($value, true), + array($value + 1, false), + array($value - 1, true), + ), + //'non-op' => array( + // array($value, false), + // array($value + 1, false), + // array($value - 1, false), + //), + + ); + } + + public function testValidateAttribute() + { + // invalid-array + $val = new CompareValidator; + $model = new FakedValidationModel; + $model->attr = array('test_val'); + $val->validateAttribute($model, 'attr'); + $this->assertTrue($model->hasErrors('attr')); + $val = new CompareValidator(array('compareValue' => 'test-string')); + $model = new FakedValidationModel; + $model->attr_test = 'test-string'; + $val->validateAttribute($model, 'attr_test'); + $this->assertFalse($model->hasErrors('attr_test')); + $val = new CompareValidator(array('compareAttribute' => 'attr_test_val')); + $model = new FakedValidationModel; + $model->attr_test = 'test-string'; + $model->attr_test_val = 'test-string'; + $val->validateAttribute($model, 'attr_test'); + $this->assertFalse($model->hasErrors('attr_test')); + $this->assertFalse($model->hasErrors('attr_test_val')); + $val = new CompareValidator(array('compareAttribute' => 'attr_test_val')); + $model = new FakedValidationModel; + $model->attr_test = 'test-string'; + $model->attr_test_val = 'test-string-false'; + $val->validateAttribute($model, 'attr_test'); + $this->assertTrue($model->hasErrors('attr_test')); + $this->assertFalse($model->hasErrors('attr_test_val')); + // assume: _repeat + $val = new CompareValidator; + $model = new FakedValidationModel; + $model->attr_test = 'test-string'; + $model->attr_test_repeat = 'test-string'; + $val->validateAttribute($model, 'attr_test'); + $this->assertFalse($model->hasErrors('attr_test')); + $this->assertFalse($model->hasErrors('attr_test_repeat')); + $val = new CompareValidator; + $model = new FakedValidationModel; + $model->attr_test = 'test-string'; + $model->attr_test_repeat = 'test-string2'; + $val->validateAttribute($model, 'attr_test'); + $this->assertTrue($model->hasErrors('attr_test')); + $this->assertFalse($model->hasErrors('attr_test_repeat')); + // not existing op + $val = new CompareValidator(); + $val->operator = '<>'; + $model = FakedValidationModel::createWithAttributes(array('attr_o' => 5, 'attr_o_repeat' => 5 )); + $val->validateAttribute($model, 'attr_o'); + $this->assertTrue($model->hasErrors('attr_o')); + } + + public function testValidateAttributeOperators() + { + $value = 55; + foreach ($this->getOperationTestData($value) as $operator => $tests) { + $val = new CompareValidator(array('operator' => $operator, 'compareValue' => $value)); + foreach ($tests as $test) { + $model = new FakedValidationModel; + $model->attr_test = $test[0]; + $val->validateAttribute($model, 'attr_test'); + $this->assertEquals($test[1], !$model->hasErrors('attr_test')); + } + + } + } + + public function testEnsureMessageSetOnInit() + { + foreach ($this->getOperationTestData(1337) as $operator => $tests) { + $val = new CompareValidator(array('operator' => $operator)); + $this->assertTrue(strlen($val->message) > 1); + } + try { + $val = new CompareValidator(array('operator' => '<>')); + } catch (InvalidConfigException $e) { + return; + } + catch (\Exception $e) { + $this->fail('InvalidConfigException expected' . get_class($e) . 'received'); + return; + } + $this->fail('InvalidConfigException expected none received'); + } +} \ No newline at end of file diff --git a/tests/unit/framework/validators/DateValidatorTest.php b/tests/unit/framework/validators/DateValidatorTest.php new file mode 100644 index 0000000..a551889 --- /dev/null +++ b/tests/unit/framework/validators/DateValidatorTest.php @@ -0,0 +1,64 @@ +<?php + +namespace yiiunit\framework\validators; + +use DateTime; +use yii\validators\DateValidator; +use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\TestCase; + + +class DateValidatorTest extends TestCase +{ + public function testEnsureMessageIsSet() + { + $val = new DateValidator; + $this->assertTrue($val->message !== null && strlen($val->message) > 1); + } + + public function testValidateValue() + { + $val = new DateValidator; + $this->assertTrue($val->validateValue('2013-09-13')); + $this->assertFalse($val->validateValue('31.7.2013')); + $this->assertFalse($val->validateValue('31-7-2013')); + $this->assertFalse($val->validateValue(time())); + $val->format = 'U'; + $this->assertTrue($val->validateValue(time())); + $val->format = 'd.m.Y'; + $this->assertTrue($val->validateValue('31.7.2013')); + $val->format = 'Y-m-!d H:i:s'; + $this->assertTrue($val->validateValue('2009-02-15 15:16:17')); + } + + public function testValidateAttribute() + { + // error-array-add + $val = new DateValidator; + $model = new FakedValidationModel; + $model->attr_date = '2013-09-13'; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $model = new FakedValidationModel; + $model->attr_date = '1375293913'; + $val->validateAttribute($model, 'attr_date'); + $this->assertTrue($model->hasErrors('attr_date')); + //// timestamp attribute + $val = new DateValidator(array('timestampAttribute' => 'attr_timestamp')); + $model = new FakedValidationModel; + $model->attr_date = '2013-09-13'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertEquals( + DateTime::createFromFormat($val->format, '2013-09-13')->getTimestamp(), + $model->attr_timestamp + ); + $val = new DateValidator(); + $model = FakedValidationModel::createWithAttributes(array('attr_date' => array())); + $val->validateAttribute($model, 'attr_date'); + $this->assertTrue($model->hasErrors('attr_date')); + + } +} \ No newline at end of file diff --git a/tests/unit/framework/validators/DefaultValueValidatorTest.php b/tests/unit/framework/validators/DefaultValueValidatorTest.php new file mode 100644 index 0000000..05a71b5 --- /dev/null +++ b/tests/unit/framework/validators/DefaultValueValidatorTest.php @@ -0,0 +1,32 @@ +<?php +namespace yiiunit\framework\validators; +use yii\validators\DefaultValueValidator; +use yiiunit\TestCase; + +/** + * DefaultValueValidatorTest + */ +class DefaultValueValidatorTest extends TestCase +{ + public function testValidateAttribute() + { + $val = new DefaultValueValidator; + $val->value = 'test_value'; + $obj = new \stdclass; + $obj->attrA = 'attrA'; + $obj->attrB = null; + $obj->attrC = ''; + // original values to chek which attritubes where modified + $objB = clone $obj; + $val->validateAttribute($obj, 'attrB'); + $this->assertEquals($val->value, $obj->attrB); + $this->assertEquals($objB->attrA, $obj->attrA); + $val->value = 'new_test_value'; + $obj = clone $objB; // get clean object + $val->validateAttribute($obj, 'attrC'); + $this->assertEquals('new_test_value', $obj->attrC); + $this->assertEquals($objB->attrA, $obj->attrA); + $val->validateAttribute($obj, 'attrA'); + $this->assertEquals($objB->attrA, $obj->attrA); + } +} diff --git a/tests/unit/framework/validators/EmailValidatorTest.php b/tests/unit/framework/validators/EmailValidatorTest.php index 512eb5c..f423663 100644 --- a/tests/unit/framework/validators/EmailValidatorTest.php +++ b/tests/unit/framework/validators/EmailValidatorTest.php @@ -1,10 +1,13 @@ <?php namespace yiiunit\framework\validators; + use yii\validators\EmailValidator; +use yiiunit\data\validators\models\FakedValidationModel; use yiiunit\TestCase; /** * EmailValidatorTest + * @group validators */ class EmailValidatorTest extends TestCase { @@ -22,7 +25,36 @@ class EmailValidatorTest extends TestCase $validator = new EmailValidator(); $validator->checkMX = true; - $this->assertTrue($validator->validateValue('sam@rmcreative.ru')); + $this->assertTrue($validator->validateValue('5011@gmail.com')); $this->assertFalse($validator->validateValue('test@example.com')); } + + public function testValidateAttribute() + { + $val = new EmailValidator(); + $model = new FakedValidationModel(); + $model->attr_email = '5011@gmail.com'; + $val->validateAttribute($model, 'attr_email'); + $this->assertFalse($model->hasErrors('attr_email')); + } + + public function testValidateValueIdn() + { + if (!function_exists('idn_to_ascii')) { + $this->markTestSkipped('Intl extension required'); + return; + } + $val = new EmailValidator(array('enableIDN' => true)); + $this->assertTrue($val->validateValue('5011@example.com')); + $this->assertTrue($val->validateValue('example@äüößìà.de')); + $this->assertTrue($val->validateValue('example@xn--zcack7ayc9a.de')); + } + + public function testValidateValueWithName() + { + $val = new EmailValidator(array('allowName' => true)); + $this->assertTrue($val->validateValue('test@example.com')); + $this->assertTrue($val->validateValue('John Smith <john.smith@example.com>')); + $this->assertFalse($val->validateValue('John Smith <example.com>')); + } } diff --git a/tests/unit/framework/validators/ExistValidatorDriverTests/ExistValidatorPostgresTest.php b/tests/unit/framework/validators/ExistValidatorDriverTests/ExistValidatorPostgresTest.php new file mode 100644 index 0000000..539b458 --- /dev/null +++ b/tests/unit/framework/validators/ExistValidatorDriverTests/ExistValidatorPostgresTest.php @@ -0,0 +1,10 @@ +<?php +namespace yiiunit\framework\validators\ExistValidatorDriverTests; + + +use yiiunit\framework\validators\ExistValidatorTest; + +class ExistValidatorPostgresTest extends ExistValidatorTest +{ + protected $driverName = 'pgsql'; +} \ No newline at end of file diff --git a/tests/unit/framework/validators/ExistValidatorDriverTests/ExistValidatorSQliteTest.php b/tests/unit/framework/validators/ExistValidatorDriverTests/ExistValidatorSQliteTest.php new file mode 100644 index 0000000..cc83fd6 --- /dev/null +++ b/tests/unit/framework/validators/ExistValidatorDriverTests/ExistValidatorSQliteTest.php @@ -0,0 +1,10 @@ +<?php +namespace yiiunit\framework\validators\ExistValidatorDriverTests; + + +use yiiunit\framework\validators\ExistValidatorTest; + +class ExistValidatorSQliteTest extends ExistValidatorTest +{ + protected $driverName = 'sqlite'; +} \ No newline at end of file diff --git a/tests/unit/framework/validators/ExistValidatorTest.php b/tests/unit/framework/validators/ExistValidatorTest.php new file mode 100644 index 0000000..0df42e3 --- /dev/null +++ b/tests/unit/framework/validators/ExistValidatorTest.php @@ -0,0 +1,94 @@ +<?php + +namespace yiiunit\framework\validators; + + +use Yii; +use yii\base\Exception; +use yii\validators\ExistValidator; +use yiiunit\data\ar\ActiveRecord; +use yiiunit\data\validators\models\ValidatorTestMainModel; +use yiiunit\data\validators\models\ValidatorTestRefModel; +use yiiunit\framework\db\DatabaseTestCase; + +class ExistValidatorTest extends DatabaseTestCase +{ + protected $driverName = 'mysql'; + + public function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + } + + public function testValidateValueExpectedException() + { + try { + $val = new ExistValidator(); + $result = $val->validateValue('ref'); + $this->fail('Exception should have been thrown at this time'); + } catch (Exception $e) { + $this->assertInstanceOf('yii\base\InvalidConfigException', $e); + $this->assertEquals('The "className" property must be set.', $e->getMessage()); + } + // combine to save the time creating a new db-fixture set (likely ~5 sec) + try { + $val = new ExistValidator(array('className' => ValidatorTestMainModel::className())); + $val->validateValue('ref'); + $this->fail('Exception should have been thrown at this time'); + } catch (Exception $e) { + $this->assertInstanceOf('yii\base\InvalidConfigException', $e); + $this->assertEquals('The "attributeName" property must be set.', $e->getMessage()); + } + } + + public function testValidateValue() + { + $val = new ExistValidator(array('className' => ValidatorTestRefModel::className(), 'attributeName' => 'id')); + $this->assertTrue($val->validateValue(2)); + $this->assertTrue($val->validateValue(5)); + $this->assertFalse($val->validateValue(99)); + $this->assertFalse($val->validateValue(array('1'))); + } + + public function testValidateAttribute() + { + // existing value on different table + $val = new ExistValidator(array('className' => ValidatorTestMainModel::className(), 'attributeName' => 'id')); + $m = ValidatorTestRefModel::find(array('id' => 1)); + $val->validateAttribute($m, 'ref'); + $this->assertFalse($m->hasErrors()); + // non-existing value on different table + $val = new ExistValidator(array('className' => ValidatorTestMainModel::className(), 'attributeName' => 'id')); + $m = ValidatorTestRefModel::find(array('id' => 6)); + $val->validateAttribute($m, 'ref'); + $this->assertTrue($m->hasErrors('ref')); + // existing value on same table + $val = new ExistValidator(array('attributeName' => 'ref')); + $m = ValidatorTestRefModel::find(array('id' => 2)); + $val->validateAttribute($m, 'test_val'); + $this->assertFalse($m->hasErrors()); + // non-existing value on same table + $val = new ExistValidator(array('attributeName' => 'ref')); + $m = ValidatorTestRefModel::find(array('id' => 5)); + $val->validateAttribute($m, 'test_val_fail'); + $this->assertTrue($m->hasErrors('test_val_fail')); + // check for given value (true) + $val = new ExistValidator(); + $m = ValidatorTestRefModel::find(array('id' => 3)); + $val->validateAttribute($m, 'ref'); + $this->assertFalse($m->hasErrors()); + // check for given defaults (false) + $val = new ExistValidator(); + $m = ValidatorTestRefModel::find(array('id' => 4)); + $m->a_field = 'some new value'; + $val->validateAttribute($m, 'a_field'); + $this->assertTrue($m->hasErrors('a_field')); + // check array + $val = new ExistValidator(array('attributeName' => 'ref')); + $m = ValidatorTestRefModel::find(array('id' => 2)); + $m->test_val = array(1,2,3); + $val->validateAttribute($m, 'test_val'); + $this->assertTrue($m->hasErrors('test_val')); + } +} \ No newline at end of file diff --git a/tests/unit/framework/validators/FileValidatorTest.php b/tests/unit/framework/validators/FileValidatorTest.php new file mode 100644 index 0000000..b6674bb --- /dev/null +++ b/tests/unit/framework/validators/FileValidatorTest.php @@ -0,0 +1,304 @@ +<?php + +namespace yiiunit\framework\validators; + + +use yii\validators\FileValidator; +use yii\web\UploadedFile; +use Yii; +use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\TestCase; + +class FileValidatorTest extends TestCase +{ + public function setUp() + { + $this->mockApplication(); + } + + public function testAssureMessagesSetOnInit() + { + $val = new FileValidator(); + foreach (array('message', 'uploadRequired', 'tooMany', 'wrongType', 'tooBig', 'tooSmall') as $attr) { + $this->assertTrue(is_string($val->$attr)); + } + } + + public function testTypeSplitOnInit() + { + $val = new FileValidator(array('types' => 'jpeg, jpg, gif')); + $this->assertEquals(array('jpeg', 'jpg', 'gif'), $val->types); + $val = new FileValidator(array('types' => 'jpeg')); + $this->assertEquals(array('jpeg'), $val->types); + $val = new FileValidator(array('types' => '')); + $this->assertEquals(array(), $val->types); + $val = new FileValidator(array('types' => array())); + $this->assertEquals(array(), $val->types); + $val = new FileValidator(); + $this->assertEquals(array(), $val->types); + $val = new FileValidator(array('types' => array('jpeg', 'exe'))); + $this->assertEquals(array('jpeg', 'exe'), $val->types); + } + + public function testGetSizeLimit() + { + $size = $this->sizeToBytes(ini_get('upload_max_filesize')); + $val = new FileValidator(); + $this->assertEquals($size, $val->getSizeLimit()); + $val->maxSize = $size + 1; // set and test if value is overridden + $this->assertEquals($size, $val->getSizeLimit()); + $val->maxSize = abs($size - 1); + $this->assertEquals($size - 1, $val->getSizeLimit()); + $_POST['MAX_FILE_SIZE'] = $size + 1; + $this->assertEquals($size - 1, $val->getSizeLimit()); + $_POST['MAX_FILE_SIZE'] = abs($size - 2); + $this->assertSame($_POST['MAX_FILE_SIZE'], $val->getSizeLimit()); + } + + protected 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; + } + } + + public function testValidateAttributeMultiple() + { + $val = new FileValidator(array('maxFiles' => 2)); + $m = FakedValidationModel::createWithAttributes(array('attr_files' => 'path')); + $val->validateAttribute($m, 'attr_files'); + $this->assertTrue($m->hasErrors('attr_files')); + $m = FakedValidationModel::createWithAttributes(array('attr_files' => array())); + $val->validateAttribute($m, 'attr_files'); + $this->assertTrue($m->hasErrors('attr_files')); + $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files'))); + $m = FakedValidationModel::createWithAttributes( + array( + 'attr_files' => $this->createTestFiles( + array( + array( + 'name' => 'test_up_1.txt', + 'size' => 1024, + ), + array( + 'error' => UPLOAD_ERR_NO_FILE, + ), + ) + ) + ) + ); + $val->validateAttribute($m, 'attr_files'); + $this->assertFalse($m->hasErrors('attr_files')); + $m = FakedValidationModel::createWithAttributes( + array('attr_files' => $this->createTestFiles(array(array(''), array(''), array(''),))) + ); + $val->validateAttribute($m, 'attr_files'); + $this->assertTrue($m->hasErrors()); + $this->assertTrue(stripos(current($m->getErrors('attr_files')), 'you can upload at most') !== false); + } + + /** + * @param array $params + * @return UploadedFile[] + */ + protected function createTestFiles($params = array()) + { + $rndString = function ($len = 10) { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $randomString = ''; + for ($i = 0; $i < $len; $i++) { + $randomString .= $characters[rand(0, strlen($characters) - 1)]; + } + return $randomString; + }; + $files = array(); + foreach ($params as $param) { + if (empty($param) && count($params) != 1) { + $files[] = array('no instance of UploadedFile'); + continue; + } + $name = isset($param['name']) ? $param['name'] : $rndString(); + $tempName = \Yii::getAlias('@yiiunit/runtime/validators/file/tmp') . $name; + if (is_readable($tempName)) { + $size = filesize($tempName); + } else { + $size = isset($param['size']) ? $param['size'] : rand( + 1, + $this->sizeToBytes(ini_get('upload_max_filesize')) + ); + } + $type = isset($param['type']) ? $param['type'] : 'text/plain'; + $error = isset($param['error']) ? $param['error'] : UPLOAD_ERR_OK; + if (count($params) == 1) { + $error = empty($param) ? UPLOAD_ERR_NO_FILE : $error; + return new UploadedFile(array( + 'name' => $name, + 'tempName' => $tempName, + 'type' => $type, + 'size' => $size, + 'error' => $error + )); + } + $files[] = new UploadedFile(array( + 'name' => $name, + 'tempName' => $tempName, + 'type' => $type, + 'size' => $size, + 'error' => $error + )); + } + return $files; + } + + public function testValidateAttribute() + { + // single File + $val = new FileValidator(); + $m = $this->createModelForAttributeTest(); + $val->validateAttribute($m, 'attr_files'); + $this->assertFalse($m->hasErrors()); + $val->validateAttribute($m, 'attr_files_empty'); + $this->assertTrue($m->hasErrors('attr_files_empty')); + $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files_empty'))); + $m = $this->createModelForAttributeTest(); + // too big + $val = new FileValidator(array('maxSize' => 128)); + $val->validateAttribute($m, 'attr_files'); + $this->assertTrue($m->hasErrors('attr_files')); + $this->assertTrue( + stripos( + current($m->getErrors('attr_files')), + str_ireplace(array('{file}', '{limit}'), array($m->attr_files->name, 128), $val->tooBig) + ) !== false + ); + // to Small + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(array('minSize' => 2048)); + $val->validateAttribute($m, 'attr_files'); + $this->assertTrue($m->hasErrors('attr_files')); + $this->assertTrue( + stripos( + current($m->getErrors('attr_files')), + str_ireplace(array('{file}', '{limit}'), array($m->attr_files->name, 2048), $val->tooSmall) + ) !== false + ); + // UPLOAD_ERR_INI_SIZE/UPLOAD_ERR_FORM_SIZE + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(); + $val->validateAttribute($m, 'attr_err_ini'); + $this->assertTrue($m->hasErrors('attr_err_ini')); + $this->assertTrue( + stripos( + current($m->getErrors('attr_err_ini')), + str_ireplace( + array('{file}', '{limit}'), + array($m->attr_err_ini->name, $val->getSizeLimit()), + $val->tooBig + ) + ) !== false + ); + // UPLOAD_ERR_PARTIAL + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(); + $val->validateAttribute($m, 'attr_err_part'); + $this->assertTrue($m->hasErrors('attr_err_part')); + $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_part'))); + $log = Yii::$app->getLog()->toArray(); + $this->assertSame(FileValidator::className() . '::validateFile', $log['messages'][0][2]); + } + + public function testValidateAttributeType() + { + $val = new FileValidator(array('types' => 'jpeg, jpg')); + $m = FakedValidationModel::createWithAttributes( + array( + 'attr_jpg' => $this->createTestFiles(array(array('name' => 'one.jpeg'))), + 'attr_exe' => $this->createTestFiles(array(array('name' => 'bad.exe'))), + ) + ); + $val->validateAttribute($m, 'attr_jpg'); + $this->assertFalse($m->hasErrors('attr_jpg')); + $val->validateAttribute($m, 'attr_exe'); + $this->assertTrue($m->hasErrors('attr_exe')); + $this->assertTrue(stripos(current($m->getErrors('attr_exe')), 'Only files with these extensions ') !== false); + } + + + protected function createModelForAttributeTest() + { + return FakedValidationModel::createWithAttributes( + array( + 'attr_files' => $this->createTestFiles( + array( + array('name' => 'abc.jpg', 'size' => 1024, 'type' => 'image/jpeg'), + ) + ), + 'attr_files_empty' => $this->createTestFiles(array(array())), + 'attr_err_ini' => $this->createTestFiles(array(array('error' => UPLOAD_ERR_INI_SIZE))), + 'attr_err_part' => $this->createTestFiles(array(array('error' => UPLOAD_ERR_PARTIAL))), + 'attr_err_tmp' => $this->createTestFiles(array(array('error' => UPLOAD_ERR_NO_TMP_DIR))), + 'attr_err_write' => $this->createTestFiles(array(array('error' => UPLOAD_ERR_CANT_WRITE))), + 'attr_err_ext' => $this->createTestFiles(array(array('error' => UPLOAD_ERR_EXTENSION))), + + ) + ); + } + + public function testValidateAttributeErrPartial() + { + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(); + $val->validateAttribute($m, 'attr_err_part'); + $this->assertTrue($m->hasErrors('attr_err_part')); + $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_part'))); + $log = Yii::$app->getLog()->toArray(); + $this->assertSame(FileValidator::className() . '::validateFile', $log['messages'][0][2]); + $this->assertContains('File was only', $log['messages'][0][0]); + } + + public function testValidateAttributeErrCantWrite() + { + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(); + $val->validateAttribute($m, 'attr_err_write'); + $this->assertTrue($m->hasErrors('attr_err_write')); + $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_write'))); + $log = Yii::$app->getLog()->toArray(); + $this->assertSame(FileValidator::className() . '::validateFile', $log['messages'][0][2]); + $this->assertContains('Failed to write', $log['messages'][0][0]); + } + + public function testValidateAttributeErrExtension() + { + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(); + $val->validateAttribute($m, 'attr_err_ext'); + $this->assertTrue($m->hasErrors('attr_err_ext')); + $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_ext'))); + $log = Yii::$app->getLog()->toArray(); + $this->assertSame(FileValidator::className() . '::validateFile', $log['messages'][0][2]); + $this->assertContains('PHP extension', $log['messages'][0][0]); + } + + public function testValidateAttributeErrNoTmpDir() + { + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(); + $val->validateAttribute($m, 'attr_err_tmp'); + $this->assertTrue($m->hasErrors('attr_err_tmp')); + $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_tmp'))); + $log = Yii::$app->getLog()->toArray(); + $this->assertSame(FileValidator::className() . '::validateFile', $log['messages'][0][2]); + $this->assertContains('Missing the temporary folder', $log['messages'][0][0]); + } +} \ No newline at end of file diff --git a/tests/unit/framework/validators/FilterValidatorTest.php b/tests/unit/framework/validators/FilterValidatorTest.php new file mode 100644 index 0000000..322f921 --- /dev/null +++ b/tests/unit/framework/validators/FilterValidatorTest.php @@ -0,0 +1,46 @@ +<?php + +namespace yiiunit\framework\validators; + + +use yii\validators\FilterValidator; +use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\TestCase; + +class FilterValidatorTest extends TestCase +{ + public function testAssureExceptionOnInit() + { + $this->setExpectedException('yii\base\InvalidConfigException'); + $val = new FilterValidator(); + } + + public function testValidateAttribute() + { + $m = FakedValidationModel::createWithAttributes(array( + 'attr_one' => ' to be trimmed ', + 'attr_two' => 'set this to null', + 'attr_empty1' => '', + 'attr_empty2' => null + )); + $val = new FilterValidator(array('filter' => 'trim')); + $val->validateAttribute($m, 'attr_one'); + $this->assertSame('to be trimmed', $m->attr_one); + $val->filter = function ($value) { + return null; + }; + $val->validateAttribute($m, 'attr_two'); + $this->assertNull($m->attr_two); + $val->filter = array($this, 'notToBeNull'); + $val->validateAttribute($m, 'attr_empty1'); + $this->assertSame($this->notToBeNull(''), $m->attr_empty1); + $val->skipOnEmpty = true; + $val->validateAttribute($m, 'attr_empty2'); + $this->assertNotNull($m->attr_empty2); + } + + public function notToBeNull($value) + { + return 'not null'; + } +} \ No newline at end of file diff --git a/tests/unit/framework/validators/NumberValidatorTest.php b/tests/unit/framework/validators/NumberValidatorTest.php new file mode 100644 index 0000000..fa158d3 --- /dev/null +++ b/tests/unit/framework/validators/NumberValidatorTest.php @@ -0,0 +1,160 @@ +<?php + +namespace yiiunit\framework\validators; + + +use yii\validators\NumberValidator; +use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\TestCase; + +class NumberValidatorTest extends TestCase +{ + public function testEnsureMessageOnInit() + { + $val = new NumberValidator; + $this->assertTrue(is_string($val->message)); + $this->assertTrue(is_null($val->max)); + $val = new NumberValidator(array('min' => -1, 'max' => 20, 'integerOnly' => true)); + $this->assertTrue(is_string($val->message)); + $this->assertTrue(is_string($val->tooSmall)); + $this->assertTrue(is_string($val->tooBig)); + } + + public function testValidateValueSimple() + { + $val = new NumberValidator(); + $this->assertTrue($val->validateValue(20)); + $this->assertTrue($val->validateValue(0)); + $this->assertTrue($val->validateValue(-20)); + $this->assertTrue($val->validateValue('20')); + $this->assertTrue($val->validateValue(25.45)); + $this->assertFalse($val->validateValue('25,45')); + $this->assertFalse($val->validateValue('12:45')); + $val = new NumberValidator(array('integerOnly' => true)); + $this->assertTrue($val->validateValue(20)); + $this->assertTrue($val->validateValue(0)); + $this->assertFalse($val->validateValue(25.45)); + $this->assertTrue($val->validateValue('20')); + $this->assertFalse($val->validateValue('25,45')); + $this->assertTrue($val->validateValue('020')); + $this->assertTrue($val->validateValue(0x14)); + $this->assertFalse($val->validateValue('0x14')); // todo check this + } + + public function testValidateValueAdvanced() + { + $val = new NumberValidator(); + $this->assertTrue($val->validateValue('-1.23')); // signed float + $this->assertTrue($val->validateValue('-4.423e-12')); // signed float + exponent + $this->assertTrue($val->validateValue('12E3')); // integer + exponent + $this->assertFalse($val->validateValue('e12')); // just exponent + $this->assertFalse($val->validateValue('-e3')); + $this->assertFalse($val->validateValue('-4.534-e-12')); // 'signed' exponent + $this->assertFalse($val->validateValue('12.23^4')); // expression instead of value + $val = new NumberValidator(array('integerOnly' => true)); + $this->assertFalse($val->validateValue('-1.23')); + $this->assertFalse($val->validateValue('-4.423e-12')); + $this->assertFalse($val->validateValue('12E3')); + $this->assertFalse($val->validateValue('e12')); + $this->assertFalse($val->validateValue('-e3')); + $this->assertFalse($val->validateValue('-4.534-e-12')); + $this->assertFalse($val->validateValue('12.23^4')); + } + + public function testValidateValueMin() + { + $val = new NumberValidator(array('min' => 1)); + $this->assertTrue($val->validateValue(1)); + $this->assertFalse($val->validateValue(-1)); + $this->assertFalse($val->validateValue('22e-12')); + $this->assertTrue($val->validateValue(PHP_INT_MAX + 1)); + $val = new NumberValidator(array('min' => 1), array('integerOnly' => true)); + $this->assertTrue($val->validateValue(1)); + $this->assertFalse($val->validateValue(-1)); + $this->assertFalse($val->validateValue('22e-12')); + $this->assertTrue($val->validateValue(PHP_INT_MAX + 1)); + } + + public function testValidateValueMax() + { + $val = new NumberValidator(array('max' => 1.25)); + $this->assertTrue($val->validateValue(1)); + $this->assertFalse($val->validateValue(1.5)); + $this->assertTrue($val->validateValue('22e-12')); + $this->assertTrue($val->validateValue('125e-2')); + $val = new NumberValidator(array('max' => 1.25, 'integerOnly' => true)); + $this->assertTrue($val->validateValue(1)); + $this->assertFalse($val->validateValue(1.5)); + $this->assertFalse($val->validateValue('22e-12')); + $this->assertFalse($val->validateValue('125e-2')); + } + + public function testValidateValueRange() + { + $val = new NumberValidator(array('min' => -10, 'max' => 20)); + $this->assertTrue($val->validateValue(0)); + $this->assertTrue($val->validateValue(-10)); + $this->assertFalse($val->validateValue(-11)); + $this->assertFalse($val->validateValue(21)); + $val = new NumberValidator(array('min' => -10, 'max' => 20, 'integerOnly' => true)); + $this->assertTrue($val->validateValue(0)); + $this->assertFalse($val->validateValue(-11)); + $this->assertFalse($val->validateValue(22)); + $this->assertFalse($val->validateValue('20e-1')); + } + + public function testValidateAttribute() + { + $val = new NumberValidator(); + $model = new FakedValidationModel(); + $model->attr_number = '5.5e1'; + $val->validateAttribute($model, 'attr_number'); + $this->assertFalse($model->hasErrors('attr_number')); + $model->attr_number = '43^32'; //expression + $val->validateAttribute($model, 'attr_number'); + $this->assertTrue($model->hasErrors('attr_number')); + $val = new NumberValidator(array('min' => 10)); + $model = new FakedValidationModel(); + $model->attr_number = 10; + $val->validateAttribute($model, 'attr_number'); + $this->assertFalse($model->hasErrors('attr_number')); + $model->attr_number = 5; + $val->validateAttribute($model, 'attr_number'); + $this->assertTrue($model->hasErrors('attr_number')); + $val = new NumberValidator(array('max' => 10)); + $model = new FakedValidationModel(); + $model->attr_number = 10; + $val->validateAttribute($model, 'attr_number'); + $this->assertFalse($model->hasErrors('attr_number')); + $model->attr_number = 15; + $val->validateAttribute($model, 'attr_number'); + $this->assertTrue($model->hasErrors('attr_number')); + $val = new NumberValidator(array('max' => 10, 'integerOnly' => true)); + $model = new FakedValidationModel(); + $model->attr_number = 10; + $val->validateAttribute($model, 'attr_number'); + $this->assertFalse($model->hasErrors('attr_number')); + $model->attr_number = 3.43; + $val->validateAttribute($model, 'attr_number'); + $this->assertTrue($model->hasErrors('attr_number')); + $val = new NumberValidator(array('min' => 1)); + $model = FakedValidationModel::createWithAttributes(array('attr_num' => array(1,2,3))); + $val->validateAttribute($model, 'attr_num'); + $this->assertTrue($model->hasErrors('attr_num')); + } + + public function testEnsureCustomMessageIsSetOnValidateAttribute() + { + $val = new NumberValidator(array( + 'tooSmall' => '{attribute} is to small.', + 'min' => 5 + )); + $model = new FakedValidationModel(); + $model->attr_number = 0; + $val->validateAttribute($model, 'attr_number'); + $this->assertTrue($model->hasErrors('attr_number')); + $this->assertEquals(1, count($model->getErrors('attr_number'))); + $msgs = $model->getErrors('attr_number'); + $this->assertSame('attr_number is to small.', $msgs[0]); + } +} \ No newline at end of file diff --git a/tests/unit/framework/validators/RangeValidatorTest.php b/tests/unit/framework/validators/RangeValidatorTest.php new file mode 100644 index 0000000..a22d190 --- /dev/null +++ b/tests/unit/framework/validators/RangeValidatorTest.php @@ -0,0 +1,70 @@ +<?php + +namespace yiiunit\framework\validators; + + +use yii\validators\RangeValidator; +use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\TestCase; + +class RangeValidatorTest extends TestCase +{ + public function testInitException() + { + $this->setExpectedException('yii\base\InvalidConfigException', 'The "range" property must be set.'); + $val = new RangeValidator(array('range' => 'not an array')); + } + + public function testAssureMessageSetOnInit() + { + $val = new RangeValidator(array('range' => array())); + $this->assertTrue(is_string($val->message)); + } + + public function testValidateValue() + { + $val = new RangeValidator(array('range' => range(1, 10, 1))); + $this->assertTrue($val->validateValue(1)); + $this->assertFalse($val->validateValue(0)); + $this->assertFalse($val->validateValue(11)); + $this->assertFalse($val->validateValue(5.5)); + $this->assertTrue($val->validateValue(10)); + $this->assertTrue($val->validateValue("10")); + $this->assertTrue($val->validateValue("5")); + } + + public function testValidateValueStrict() + { + $val = new RangeValidator(array('range' => range(1, 10, 1), 'strict' => true)); + $this->assertTrue($val->validateValue(1)); + $this->assertTrue($val->validateValue(5)); + $this->assertTrue($val->validateValue(10)); + $this->assertFalse($val->validateValue("1")); + $this->assertFalse($val->validateValue("10")); + $this->assertFalse($val->validateValue("5.5")); + } + + public function testValidateValueNot() + { + $val = new RangeValidator(array('range' => range(1, 10, 1), 'not' => true)); + $this->assertFalse($val->validateValue(1)); + $this->assertTrue($val->validateValue(0)); + $this->assertTrue($val->validateValue(11)); + $this->assertTrue($val->validateValue(5.5)); + $this->assertFalse($val->validateValue(10)); + $this->assertFalse($val->validateValue("10")); + $this->assertFalse($val->validateValue("5")); + } + + public function testValidateAttribute() + { + $val = new RangeValidator(array('range' => range(1, 10, 1))); + $m = FakedValidationModel::createWithAttributes(array('attr_r1' => 5, 'attr_r2' => 999)); + $val->validateAttribute($m, 'attr_r1'); + $this->assertFalse($m->hasErrors()); + $val->validateAttribute($m, 'attr_r2'); + $this->assertTrue($m->hasErrors('attr_r2')); + $err = $m->getErrors('attr_r2'); + $this->assertTrue(stripos($err[0], 'attr_r2') !== false); + } +} \ No newline at end of file diff --git a/tests/unit/framework/validators/RegularExpressionValidatorTest.php b/tests/unit/framework/validators/RegularExpressionValidatorTest.php new file mode 100644 index 0000000..fc89139 --- /dev/null +++ b/tests/unit/framework/validators/RegularExpressionValidatorTest.php @@ -0,0 +1,48 @@ +<?php + +namespace yiiunit\framework\validators; + + +use yii\validators\RegularExpressionValidator; +use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\TestCase; + +class RegularExpressionValidatorTest extends TestCase +{ + public function testValidateValue() + { + $val = new RegularExpressionValidator(array('pattern' => '/^[a-zA-Z0-9](\.)?([^\/]*)$/m')); + $this->assertTrue($val->validateValue('b.4')); + $this->assertFalse($val->validateValue('b./')); + $this->assertFalse($val->validateValue(array('a', 'b'))); + $val->not = true; + $this->assertFalse($val->validateValue('b.4')); + $this->assertTrue($val->validateValue('b./')); + $this->assertFalse($val->validateValue(array('a', 'b'))); + } + + public function testValidateAttribute() + { + $val = new RegularExpressionValidator(array('pattern' => '/^[a-zA-Z0-9](\.)?([^\/]*)$/m')); + $m = FakedValidationModel::createWithAttributes(array('attr_reg1' => 'b.4')); + $val->validateAttribute($m, 'attr_reg1'); + $this->assertFalse($m->hasErrors('attr_reg1')); + $m->attr_reg1 = 'b./'; + $val->validateAttribute($m, 'attr_reg1'); + $this->assertTrue($m->hasErrors('attr_reg1')); + } + + public function testMessageSetOnInit() + { + $val = new RegularExpressionValidator(array('pattern' => '/^[a-zA-Z0-9](\.)?([^\/]*)$/m')); + $this->assertTrue(is_string($val->message)); + } + + public function testInitException() + { + $this->setExpectedException('yii\base\InvalidConfigException'); + $val = new RegularExpressionValidator(); + $val->validateValue('abc'); + } + +} \ No newline at end of file diff --git a/tests/unit/framework/validators/RequiredValidatorTest.php b/tests/unit/framework/validators/RequiredValidatorTest.php new file mode 100644 index 0000000..22c9d0b --- /dev/null +++ b/tests/unit/framework/validators/RequiredValidatorTest.php @@ -0,0 +1,54 @@ +<?php +namespace yiiunit\framework\validators; + + +use yii\validators\RequiredValidator; +use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\TestCase; + +class RequiredValidatorTest extends TestCase +{ + public function testValidateValueWithDefaults() + { + $val = new RequiredValidator(); + $this->assertFalse($val->validateValue(null)); + $this->assertFalse($val->validateValue(array())); + $this->assertTrue($val->validateValue('not empty')); + $this->assertTrue($val->validateValue(array('with', 'elements'))); + } + + public function testValidateValueWithValue() + { + $val = new RequiredValidator(array('requiredValue' => 55)); + $this->assertTrue($val->validateValue(55)); + $this->assertTrue($val->validateValue("55")); + $this->assertTrue($val->validateValue("0x37")); + $this->assertFalse($val->validateValue("should fail")); + $this->assertTrue($val->validateValue(true)); + $val->strict = true; + $this->assertTrue($val->validateValue(55)); + $this->assertFalse($val->validateValue("55")); + $this->assertFalse($val->validateValue("0x37")); + $this->assertFalse($val->validateValue("should fail")); + $this->assertFalse($val->validateValue(true)); + } + + public function testValidateAttribute() + { + // empty req-value + $val = new RequiredValidator(); + $m = FakedValidationModel::createWithAttributes(array('attr_val' => null)); + $val->validateAttribute($m, 'attr_val'); + $this->assertTrue($m->hasErrors('attr_val')); + $this->assertTrue(stripos(current($m->getErrors('attr_val')), 'blank') !== false); + $val = new RequiredValidator(array('requiredValue' => 55)); + $m = FakedValidationModel::createWithAttributes(array('attr_val' => 56)); + $val->validateAttribute($m, 'attr_val'); + $this->assertTrue($m->hasErrors('attr_val')); + $this->assertTrue(stripos(current($m->getErrors('attr_val')), 'must be') !== false); + $val = new RequiredValidator(array('requiredValue' => 55)); + $m = FakedValidationModel::createWithAttributes(array('attr_val' => 55)); + $val->validateAttribute($m, 'attr_val'); + $this->assertFalse($m->hasErrors('attr_val')); + } +} \ No newline at end of file diff --git a/tests/unit/framework/validators/StringValidatorTest.php b/tests/unit/framework/validators/StringValidatorTest.php new file mode 100644 index 0000000..8beaf16 --- /dev/null +++ b/tests/unit/framework/validators/StringValidatorTest.php @@ -0,0 +1,116 @@ +<?php + +namespace yiiunit\framework\validators; + + +use yii\validators\StringValidator; +use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\TestCase; + +class StringValidatorTest extends TestCase +{ + public function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + + public function testValidateValue() + { + $val = new StringValidator(); + $this->assertFalse($val->validateValue(array('not a string'))); + $this->assertTrue($val->validateValue('Just some string')); + } + + public function testValidateValueLength() + { + $val = new StringValidator(array('length' => 25)); + $this->assertTrue($val->validateValue(str_repeat('x', 25))); + $this->assertTrue($val->validateValue(str_repeat('€', 25))); + $this->assertFalse($val->validateValue(str_repeat('x', 125))); + $this->assertFalse($val->validateValue('')); + $val = new StringValidator(array('length' => array(25))); + $this->assertTrue($val->validateValue(str_repeat('x', 25))); + $this->assertTrue($val->validateValue(str_repeat('x', 1250))); + $this->assertFalse($val->validateValue(str_repeat('Ä', 24))); + $this->assertFalse($val->validateValue('')); + $val = new StringValidator(array('length' => array(10, 20))); + $this->assertTrue($val->validateValue(str_repeat('x', 15))); + $this->assertTrue($val->validateValue(str_repeat('x', 10))); + $this->assertTrue($val->validateValue(str_repeat('x', 20))); + $this->assertFalse($val->validateValue(str_repeat('x', 5))); + $this->assertFalse($val->validateValue(str_repeat('x', 25))); + $this->assertFalse($val->validateValue('')); + // make sure min/max are overridden + $val = new StringValidator(array('length' => array(10, 20), 'min' => 25, 'max' => 35)); + $this->assertTrue($val->validateValue(str_repeat('x', 15))); + $this->assertFalse($val->validateValue(str_repeat('x', 30))); + } + + public function testValidateValueMinMax() + { + $val = new StringValidator(array('min' => 10)); + $this->assertTrue($val->validateValue(str_repeat('x', 10))); + $this->assertFalse($val->validateValue('xxxx')); + $val = new StringValidator(array('max' => 10)); + $this->assertTrue($val->validateValue('xxxx')); + $this->assertFalse($val->validateValue(str_repeat('y', 20))); + $val = new StringValidator(array('min' => 10, 'max' => 20)); + $this->assertTrue($val->validateValue(str_repeat('y', 15))); + $this->assertFalse($val->validateValue('abc')); + $this->assertFalse($val->validateValue(str_repeat('b', 25))); + } + + public function testValidateAttribute() + { + $val = new StringValidator(); + $model = new FakedValidationModel(); + $model->attr_string = 'a tet string'; + $val->validateAttribute($model, 'attr_string'); + $this->assertFalse($model->hasErrors()); + $val = new StringValidator(array('length' => 20)); + $model = new FakedValidationModel(); + $model->attr_string = str_repeat('x', 20); + $val->validateAttribute($model, 'attr_string'); + $this->assertFalse($model->hasErrors()); + $model = new FakedValidationModel(); + $model->attr_string = 'abc'; + $val->validateAttribute($model, 'attr_string'); + $this->assertTrue($model->hasErrors('attr_string')); + $val = new StringValidator(array('max' => 2)); + $model = new FakedValidationModel(); + $model->attr_string = 'a'; + $val->validateAttribute($model, 'attr_string'); + $this->assertFalse($model->hasErrors()); + $model = new FakedValidationModel(); + $model->attr_string = 'abc'; + $val->validateAttribute($model, 'attr_string'); + $this->assertTrue($model->hasErrors('attr_string')); + $val = new StringValidator(array('max' => 1)); + $model = FakedValidationModel::createWithAttributes(array('attr_str' => array('abc'))); + $val->validateAttribute($model, 'attr_str'); + $this->assertTrue($model->hasErrors('attr_str')); + } + + public function testEnsureMessagesOnInit() + { + $val = new StringValidator(array('min' => 1, 'max' => 2)); + $this->assertTrue(is_string($val->message)); + $this->assertTrue(is_string($val->tooLong)); + $this->assertTrue(is_string($val->tooShort)); + } + + public function testCustomErrorMessageInValidateAttribute() + { + $val = new StringValidator(array( + 'min' => 5, + 'tooShort' => '{attribute} to short. Min is {min}', + )); + $model = new FakedValidationModel(); + $model->attr_string = 'abc'; + $val->validateAttribute($model, 'attr_string'); + $this->assertTrue($model->hasErrors('attr_string')); + $errorMsg = $model->getErrors('attr_string'); + $this->assertEquals('attr_string to short. Min is 5', $errorMsg[0]); + } +} \ No newline at end of file diff --git a/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorPostgresTest.php b/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorPostgresTest.php new file mode 100644 index 0000000..9adc57c --- /dev/null +++ b/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorPostgresTest.php @@ -0,0 +1,11 @@ +<?php + +namespace yiiunit\framework\validators\UniqueValidatorDriverTests; + + +use yiiunit\framework\validators\UniqueValidatorTest; + +class UniqueValidatorPostgresTest extends UniqueValidatorTest +{ + protected $driverName = 'pgsql'; +} \ No newline at end of file diff --git a/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorSQliteTest.php b/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorSQliteTest.php new file mode 100644 index 0000000..9263404 --- /dev/null +++ b/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorSQliteTest.php @@ -0,0 +1,11 @@ +<?php + +namespace yiiunit\framework\validators\UniqueValidatorDriverTests; + + +use yiiunit\framework\validators\UniqueValidatorTest; + +class UniqueValidatorSQliteTest extends UniqueValidatorTest +{ + protected $driverName = 'sqlite'; +} \ No newline at end of file diff --git a/tests/unit/framework/validators/UniqueValidatorTest.php b/tests/unit/framework/validators/UniqueValidatorTest.php new file mode 100644 index 0000000..a39ee41 --- /dev/null +++ b/tests/unit/framework/validators/UniqueValidatorTest.php @@ -0,0 +1,88 @@ +<?php + +namespace yiiunit\framework\validators; + + +use yii\validators\UniqueValidator; +use Yii; +use yiiunit\data\ar\ActiveRecord; +use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\data\validators\models\ValidatorTestMainModel; +use yiiunit\data\validators\models\ValidatorTestRefModel; +use yiiunit\framework\db\DatabaseTestCase; +use yiiunit\TestCase; + +class UniqueValidatorTest extends DatabaseTestCase +{ + protected $driverName = 'mysql'; + + public function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + } + + public function testAssureMessageSetOnInit() + { + $val = new UniqueValidator(); + $this->assertTrue(is_string($val->message)); + } + + public function testValidateAttributeDefault() + { + $val = new UniqueValidator(); + $m = ValidatorTestMainModel::find()->one(); + $val->validateAttribute($m, 'id'); + $this->assertFalse($m->hasErrors('id')); + $m = ValidatorTestRefModel::find(1); + $val->validateAttribute($m, 'ref'); + $this->assertTrue($m->hasErrors('ref')); + // new record: + $m = new ValidatorTestRefModel(); + $m->ref = 5; + $val->validateAttribute($m, 'ref'); + $this->assertTrue($m->hasErrors('ref')); + $m = new ValidatorTestRefModel(); + $m->id = 7; + $m->ref = 12121; + $val->validateAttribute($m, 'ref'); + $this->assertFalse($m->hasErrors('ref')); + $m->save(false); + $val->validateAttribute($m, 'ref'); + $this->assertFalse($m->hasErrors('ref')); + // array error + $m = FakedValidationModel::createWithAttributes(array('attr_arr' => array('a', 'b'))); + $val->validateAttribute($m, 'attr_arr'); + $this->assertTrue($m->hasErrors('attr_arr')); + } + + public function testValidateAttributeOfNonARModel() + { + $val = new UniqueValidator(array('className' => ValidatorTestRefModel::className(), 'attributeName' => 'ref')); + $m = FakedValidationModel::createWithAttributes(array('attr_1' => 5, 'attr_2' => 1313)); + $val->validateAttribute($m, 'attr_1'); + $this->assertTrue($m->hasErrors('attr_1')); + $val->validateAttribute($m, 'attr_2'); + $this->assertFalse($m->hasErrors('attr_2')); + } + + public function testValidateNonDatabaseAttribute() + { + $val = new UniqueValidator(array('className' => ValidatorTestRefModel::className(), 'attributeName' => 'ref')); + $m = ValidatorTestMainModel::find(1); + $val->validateAttribute($m, 'testMainVal'); + $this->assertFalse($m->hasErrors('testMainVal')); + $m = ValidatorTestMainModel::find(1); + $m->testMainVal = 4; + $val->validateAttribute($m, 'testMainVal'); + $this->assertTrue($m->hasErrors('testMainVal')); + } + + public function testValidateAttributeAttributeNotInTableException() + { + $this->setExpectedException('yii\base\InvalidConfigException'); + $val = new UniqueValidator(); + $m = new ValidatorTestMainModel(); + $val->validateAttribute($m, 'testMainVal'); + } +} \ No newline at end of file diff --git a/tests/unit/framework/validators/UrlValidatorTest.php b/tests/unit/framework/validators/UrlValidatorTest.php new file mode 100644 index 0000000..239c92b --- /dev/null +++ b/tests/unit/framework/validators/UrlValidatorTest.php @@ -0,0 +1,94 @@ +<?php +namespace yiiunit\framework\validators; +use yiiunit\data\validators\models\FakedValidationModel; +use yii\validators\UrlValidator; +use yiiunit\TestCase; + +/** + * UrlValidatorTest + */ +class UrlValidatorTest extends TestCase +{ + public function testValidateValue() + { + $val = new UrlValidator; + $this->assertFalse($val->validateValue('google.de')); + $this->assertTrue($val->validateValue('http://google.de')); + $this->assertTrue($val->validateValue('https://google.de')); + $this->assertFalse($val->validateValue('htp://yiiframework.com')); + $this->assertTrue($val->validateValue('https://www.google.de/search?q=yii+framework&ie=utf-8&oe=utf-8' + .'&rls=org.mozilla:de:official&client=firefox-a&gws_rd=cr')); + $this->assertFalse($val->validateValue('ftp://ftp.ruhr-uni-bochum.de/')); + $this->assertFalse($val->validateValue('http://invalid,domain')); + $this->assertFalse($val->validateValue('http://äüö?=!"§$%&/()=}][{³²€.edu')); + } + + public function testValidateValueWithDefaultScheme() + { + $val = new UrlValidator(array('defaultScheme' => 'https')); + $this->assertTrue($val->validateValue('yiiframework.com')); + $this->assertTrue($val->validateValue('http://yiiframework.com')); + } + + public function testValidateValueWithoutScheme() + { + $val = new UrlValidator(array('pattern' => '/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)/i')); + $this->assertTrue($val->validateValue('yiiframework.com')); + } + + public function testValidateWithCustomScheme() + { + $val = new UrlValidator(array( + 'validSchemes' => array('http', 'https', 'ftp', 'ftps'), + 'defaultScheme' => 'http', + )); + $this->assertTrue($val->validateValue('ftp://ftp.ruhr-uni-bochum.de/')); + $this->assertTrue($val->validateValue('google.de')); + $this->assertTrue($val->validateValue('http://google.de')); + $this->assertTrue($val->validateValue('https://google.de')); + $this->assertFalse($val->validateValue('htp://yiiframework.com')); + // relative urls not supported + $this->assertFalse($val->validateValue('//yiiframework.com')); + } + + public function testValidateWithIdn() + { + if(!function_exists('idn_to_ascii')) { + $this->markTestSkipped('intl package required'); + return; + } + $val = new UrlValidator(array( + 'enableIDN' => true, + )); + $this->assertTrue($val->validateValue('http://äüößìà.de')); + // converted via http://mct.verisign-grs.com/convertServlet + $this->assertTrue($val->validateValue('http://xn--zcack7ayc9a.de')); + } + + public function testValidateLength() + { + $url = 'http://' . str_pad('base', 2000, 'url') . '.de'; + $val = new UrlValidator; + $this->assertFalse($val->validateValue($url)); + } + + public function testValidateAttributeAndError() + { + $obj = new FakedValidationModel; + $obj->attr_url = 'http://google.de'; + $val = new UrlValidator; + $val->validateAttribute($obj, 'attr_url'); + $this->assertFalse($obj->hasErrors('attr_url')); + $this->assertSame('http://google.de', $obj->attr_url); + $obj = new FakedValidationModel; + $val->defaultScheme = 'http'; + $obj->attr_url = 'google.de'; + $val->validateAttribute($obj, 'attr_url'); + $this->assertFalse($obj->hasErrors('attr_url')); + $this->assertTrue(stripos($obj->attr_url, 'http') !== false); + $obj = new FakedValidationModel; + $obj->attr_url = 'gttp;/invalid string'; + $val->validateAttribute($obj, 'attr_url'); + $this->assertTrue($obj->hasErrors('attr_url')); + } +} diff --git a/tests/unit/framework/validators/ValidatorTest.php b/tests/unit/framework/validators/ValidatorTest.php new file mode 100644 index 0000000..f0a114b --- /dev/null +++ b/tests/unit/framework/validators/ValidatorTest.php @@ -0,0 +1,227 @@ +<?php + +namespace yiiunit\framework\validators; + + +use yii\validators\BooleanValidator; +use yii\validators\InlineValidator; +use yii\validators\NumberValidator; +use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\data\validators\TestValidator; +use yiiunit\TestCase; + +class ValidatorTest extends TestCase +{ + + protected function getTestModel($additionalAttributes = array()) + { + $attributes = array_merge( + array('attr_runMe1' => true, 'attr_runMe2' => true, 'attr_skip' => true), + $additionalAttributes + ); + return FakedValidationModel::createWithAttributes($attributes); + } + + public function testCreateValidator() + { + $model = FakedValidationModel::createWithAttributes(array('attr_test1' => 'abc', 'attr_test2' => '2013')); + /** @var $numberVal NumberValidator */ + $numberVal = TestValidator::createValidator('number', $model, array('attr_test1')); + $this->assertInstanceOf(NumberValidator::className(), $numberVal); + $numberVal = TestValidator::createValidator('integer', $model, array('attr_test2')); + $this->assertInstanceOf(NumberValidator::className(), $numberVal); + $this->assertTrue($numberVal->integerOnly); + $val = TestValidator::createValidator( + 'boolean', + $model, + 'attr_test1, attr_test2', + array('on' => array('a', 'b')) + ); + $this->assertInstanceOf(BooleanValidator::className(), $val); + $this->assertSame(array('a', 'b'), $val->on); + $this->assertSame(array('attr_test1', 'attr_test2'), $val->attributes); + $val = TestValidator::createValidator( + 'boolean', + $model, + 'attr_test1, attr_test2', + array('on' => 'a, b', 'except' => 'c,d,e') + ); + $this->assertInstanceOf(BooleanValidator::className(), $val); + $this->assertSame(array('a', 'b'), $val->on); + $this->assertSame(array('c', 'd', 'e'), $val->except); + $val = TestValidator::createValidator('inlineVal', $model, 'val_attr_a'); + $this->assertInstanceOf(InlineValidator::className(), $val); + $this->assertSame('inlineVal', $val->method); + } + + public function testValidate() + { + $val = new TestValidator(array('attributes' => array('attr_runMe1', 'attr_runMe2'))); + $model = $this->getTestModel(); + $val->validate($model); + $this->assertTrue($val->isAttributeValidated('attr_runMe1')); + $this->assertTrue($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_skip')); + } + + public function testValidateWithAttributeIntersect() + { + $val = new TestValidator(array('attributes' => array('attr_runMe1', 'attr_runMe2'))); + $model = $this->getTestModel(); + $val->validate($model, array('attr_runMe1')); + $this->assertTrue($val->isAttributeValidated('attr_runMe1')); + $this->assertFalse($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_skip')); + } + + public function testValidateWithEmptyAttributes() + { + $val = new TestValidator(); + $model = $this->getTestModel(); + $val->validate($model, array('attr_runMe1')); + $this->assertFalse($val->isAttributeValidated('attr_runMe1')); + $this->assertFalse($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_skip')); + $val->validate($model); + $this->assertFalse($val->isAttributeValidated('attr_runMe1')); + $this->assertFalse($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_skip')); + } + + public function testValidateWithError() + { + $val = new TestValidator(array('attributes' => array('attr_runMe1', 'attr_runMe2'), 'skipOnError' => false)); + $model = $this->getTestModel(); + $val->validate($model); + $this->assertTrue($val->isAttributeValidated('attr_runMe1')); + $this->assertTrue($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_skip')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe2')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); + $val->validate($model, array('attr_runMe2')); + $this->assertEquals(2, $val->countAttributeValidations('attr_runMe2')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); + $this->assertEquals(0, $val->countAttributeValidations('attr_skip')); + $val = new TestValidator(array('attributes' => array('attr_runMe1', 'attr_runMe2'), 'skipOnError' => true)); + $model = $this->getTestModel(); + $val->enableErrorOnValidateAttribute(); + $val->validate($model); + $this->assertTrue($val->isAttributeValidated('attr_runMe1')); + $this->assertTrue($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_skip')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); + $this->assertEquals(0, $val->countAttributeValidations('attr_skip')); + $val->validate($model, array('attr_runMe2')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe2')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); + $this->assertEquals(0, $val->countAttributeValidations('attr_skip')); + } + + public function testValidateWithEmpty() + { + $val = new TestValidator(array( + 'attributes' => array( + 'attr_runMe1', + 'attr_runMe2', + 'attr_empty1', + 'attr_empty2' + ), + 'skipOnEmpty' => true, + )); + $model = $this->getTestModel(array('attr_empty1' => '', 'attr_emtpy2' => ' ')); + $val->validate($model); + $this->assertTrue($val->isAttributeValidated('attr_runMe1')); + $this->assertTrue($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_empty1')); + $this->assertFalse($val->isAttributeValidated('attr_empty2')); + $model->attr_empty1 = 'not empty anymore'; + $val->validate($model); + $this->assertTrue($val->isAttributeValidated('attr_empty1')); + $this->assertFalse($val->isAttributeValidated('attr_empty2')); + $val = new TestValidator(array( + 'attributes' => array( + 'attr_runMe1', + 'attr_runMe2', + 'attr_empty1', + 'attr_empty2' + ), + 'skipOnEmpty' => false, + )); + $model = $this->getTestModel(array('attr_empty1' => '', 'attr_emtpy2' => ' ')); + $val->validate($model); + $this->assertTrue($val->isAttributeValidated('attr_runMe1')); + $this->assertTrue($val->isAttributeValidated('attr_runMe2')); + $this->assertTrue($val->isAttributeValidated('attr_empty1')); + $this->assertTrue($val->isAttributeValidated('attr_empty2')); + } + + public function testIsEmpty() + { + $val = new TestValidator(); + $this->assertTrue($val->isEmpty(null)); + $this->assertTrue($val->isEmpty(array())); + $this->assertTrue($val->isEmpty('')); + $this->assertFalse($val->isEmpty(5)); + $this->assertFalse($val->isEmpty(0)); + $this->assertFalse($val->isEmpty(new \stdClass())); + $this->assertFalse($val->isEmpty(' ')); + // trim + $this->assertTrue($val->isEmpty(' ', true)); + $this->assertTrue($val->isEmpty('', true)); + $this->assertTrue($val->isEmpty(" \t\n\r\0\x0B", true)); + $this->assertTrue($val->isEmpty('', true)); + $this->assertFalse($val->isEmpty('0', true)); + $this->assertFalse($val->isEmpty(0, true)); + $this->assertFalse($val->isEmpty('this ain\'t an empty value', true)); + } + + public function testValidateValue() + { + $this->setExpectedException( + 'yii\base\NotSupportedException', + TestValidator::className() . ' does not support validateValue().' + ); + $val = new TestValidator(); + $val->validateValue('abc'); + } + + public function testClientValidateAttribute() + { + $val = new TestValidator(); + $this->assertNull( + $val->clientValidateAttribute($this->getTestModel(), 'attr_runMe1', array()) + ); //todo pass a view instead of array + } + + public function testIsActive() + { + $val = new TestValidator(); + $this->assertTrue($val->isActive('scenA')); + $this->assertTrue($val->isActive('scenB')); + $val->except = array('scenB'); + $this->assertTrue($val->isActive('scenA')); + $this->assertFalse($val->isActive('scenB')); + $val->on = array('scenC'); + $this->assertFalse($val->isActive('scenA')); + $this->assertFalse($val->isActive('scenB')); + $this->assertTrue($val->isActive('scenC')); + } + + public function testAddError() + { + $val = new TestValidator(); + $m = $this->getTestModel(array('attr_msg_val' => 'abc')); + $val->addError($m, 'attr_msg_val', '{attribute}::{value}'); + $errors = $m->getErrors('attr_msg_val'); + $this->assertEquals('attr_msg_val::abc', $errors[0]); + $m = $this->getTestModel(array('attr_msg_val' => array('bcc'))); + $val->addError($m, 'attr_msg_val', '{attribute}::{value}'); + $errors = $m->getErrors('attr_msg_val'); + $this->assertEquals('attr_msg_val::array()', $errors[0]); + $m = $this->getTestModel(array('attr_msg_val' => 'abc')); + $val->addError($m, 'attr_msg_val', '{attribute}::{value}::{param}', array('{param}' => 'param_value')); + $errors = $m->getErrors('attr_msg_val'); + $this->assertEquals('attr_msg_val::abc::param_value', $errors[0]); + } +} \ No newline at end of file diff --git a/tests/unit/framework/web/AssetBundleTest.php b/tests/unit/framework/web/AssetBundleTest.php new file mode 100644 index 0000000..6fa6238 --- /dev/null +++ b/tests/unit/framework/web/AssetBundleTest.php @@ -0,0 +1,256 @@ +<?php +/** + * + * + * @author Carsten Brandt <mail@cebe.cc> + */ + +namespace yiiunit\framework\web; + +use Yii; +use yii\base\View; +use yii\web\AssetBundle; +use yii\web\AssetManager; + +/** + * @group web + */ +class AssetBundleTest extends \yiiunit\TestCase +{ + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + + Yii::setAlias('@testWeb', '/'); + Yii::setAlias('@testWebRoot', '@yiiunit/data/web'); + } + + protected function getView() + { + $view = new View(); + $view->setAssetManager(new AssetManager(array( + 'basePath' => '@testWebRoot/assets', + 'baseUrl' => '@testWeb/assets', + ))); + + return $view; + } + + public function testRegister() + { + $view = $this->getView(); + + $this->assertEmpty($view->assetBundles); + TestSimpleAsset::register($view); + $this->assertEquals(1, count($view->assetBundles)); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestSimpleAsset', $view->assetBundles); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestSimpleAsset'] instanceof AssetBundle); + + $expected = <<<EOF +123<script src="/js/jquery.js"></script>4 +EOF; + $this->assertEquals($expected, $view->renderFile('@yiiunit/data/views/rawlayout.php')); + } + + public function testSimpleDependency() + { + $view = $this->getView(); + + $this->assertEmpty($view->assetBundles); + TestAssetBundle::register($view); + $this->assertEquals(3, count($view->assetBundles)); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetBundle', $view->assetBundles); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestJqueryAsset', $view->assetBundles); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetLevel3', $view->assetBundles); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle'] instanceof AssetBundle); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset'] instanceof AssetBundle); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3'] instanceof AssetBundle); + + $expected = <<<EOF +1<link href="/files/cssFile.css" rel="stylesheet">23<script src="/js/jquery.js"></script> +<script src="/files/jsFile.js"></script>4 +EOF; + $this->assertEquals($expected, $view->renderFile('@yiiunit/data/views/rawlayout.php')); + } + + public function positionProvider() + { + return array( + array(View::POS_HEAD, true), + array(View::POS_HEAD, false), + array(View::POS_BEGIN, true), + array(View::POS_BEGIN, false), + array(View::POS_END, true), + array(View::POS_END, false), + ); + } + + /** + * @dataProvider positionProvider + */ + public function testPositionDependency($pos, $jqAlreadyRegistered) + { + $view = $this->getView(); + + $view->getAssetManager()->bundles['yiiunit\\framework\\web\\TestAssetBundle'] = array( + 'jsOptions' => array( + 'position' => $pos, + ), + ); + + $this->assertEmpty($view->assetBundles); + if ($jqAlreadyRegistered) { + TestJqueryAsset::register($view); + } + TestAssetBundle::register($view); + $this->assertEquals(3, count($view->assetBundles)); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetBundle', $view->assetBundles); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestJqueryAsset', $view->assetBundles); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetLevel3', $view->assetBundles); + + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle'] instanceof AssetBundle); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset'] instanceof AssetBundle); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3'] instanceof AssetBundle); + + $this->assertArrayHasKey('position', $view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle']->jsOptions); + $this->assertEquals($pos, $view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle']->jsOptions['position']); + $this->assertArrayHasKey('position', $view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset']->jsOptions); + $this->assertEquals($pos, $view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset']->jsOptions['position']); + $this->assertArrayHasKey('position', $view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3']->jsOptions); + $this->assertEquals($pos, $view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3']->jsOptions['position']); + + switch($pos) + { + case View::POS_HEAD: + $expected = <<<EOF +1<link href="/files/cssFile.css" rel="stylesheet"> +<script src="/js/jquery.js"></script> +<script src="/files/jsFile.js"></script>234 +EOF; + break; + case View::POS_BEGIN: + $expected = <<<EOF +1<link href="/files/cssFile.css" rel="stylesheet">2<script src="/js/jquery.js"></script> +<script src="/files/jsFile.js"></script>34 +EOF; + break; + default: + case View::POS_END: + $expected = <<<EOF +1<link href="/files/cssFile.css" rel="stylesheet">23<script src="/js/jquery.js"></script> +<script src="/files/jsFile.js"></script>4 +EOF; + break; + } + $this->assertEquals($expected, $view->renderFile('@yiiunit/data/views/rawlayout.php')); + } + + public function positionProvider2() + { + return array( + array(View::POS_BEGIN, true), + array(View::POS_BEGIN, false), + array(View::POS_END, true), + array(View::POS_END, false), + ); + } + + /** + * @dataProvider positionProvider + */ + public function testPositionDependencyConflict($pos, $jqAlreadyRegistered) + { + $view = $this->getView(); + + $view->getAssetManager()->bundles['yiiunit\\framework\\web\\TestAssetBundle'] = array( + 'jsOptions' => array( + 'position' => $pos - 1, + ), + ); + $view->getAssetManager()->bundles['yiiunit\\framework\\web\\TestJqueryAsset'] = array( + 'jsOptions' => array( + 'position' => $pos, + ), + ); + + $this->assertEmpty($view->assetBundles); + if ($jqAlreadyRegistered) { + TestJqueryAsset::register($view); + } + $this->setExpectedException('yii\\base\\InvalidConfigException'); + TestAssetBundle::register($view); + } + + public function testCircularDependency() + { + $this->setExpectedException('yii\\base\\InvalidConfigException'); + TestAssetCircleA::register($this->getView()); + } +} + +class TestSimpleAsset extends AssetBundle +{ + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; + public $js = array( + 'jquery.js', + ); +} + +class TestAssetBundle extends AssetBundle +{ + public $basePath = '@testWebRoot/files'; + public $baseUrl = '@testWeb/files'; + public $css = array( + 'cssFile.css', + ); + public $js = array( + 'jsFile.js', + ); + public $depends = array( + 'yiiunit\\framework\\web\\TestJqueryAsset' + ); +} + +class TestJqueryAsset extends AssetBundle +{ + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; + public $js = array( + 'jquery.js', + ); + public $depends = array( + 'yiiunit\\framework\\web\\TestAssetLevel3' + ); +} + +class TestAssetLevel3 extends AssetBundle +{ + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; +} + +class TestAssetCircleA extends AssetBundle +{ + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; + public $js = array( + 'jquery.js', + ); + public $depends = array( + 'yiiunit\\framework\\web\\TestAssetCircleB' + ); +} + +class TestAssetCircleB extends AssetBundle +{ + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; + public $js = array( + 'jquery.js', + ); + public $depends = array( + 'yiiunit\\framework\\web\\TestAssetCircleA' + ); +} \ No newline at end of file diff --git a/tests/unit/framework/web/CacheSessionTest.php b/tests/unit/framework/web/CacheSessionTest.php new file mode 100644 index 0000000..e740596 --- /dev/null +++ b/tests/unit/framework/web/CacheSessionTest.php @@ -0,0 +1,39 @@ +<?php + +namespace yiiunit\framework\web; + +use Yii; +use yii\caching\FileCache; +use yii\web\CacheSession; + +/** + * @group web + */ +class CacheSessionTest extends \yiiunit\TestCase +{ + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + Yii::$app->setComponent('cache', new FileCache()); + } + + public function testCacheSession() + { + $session = new CacheSession(); + + $session->writeSession('test', 'sessionData'); + $this->assertEquals('sessionData', $session->readSession('test')); + $session->destroySession('test'); + $this->assertEquals('', $session->readSession('test')); + } + + public function testInvalidCache() + { + $this->setExpectedException('yii\base\InvalidConfigException'); + + $session = new CacheSession(array( + 'cache' => 'invalid', + )); + } +} diff --git a/tests/unit/framework/web/ResponseTest.php b/tests/unit/framework/web/ResponseTest.php new file mode 100644 index 0000000..2a9b4bf --- /dev/null +++ b/tests/unit/framework/web/ResponseTest.php @@ -0,0 +1,93 @@ +<?php + +namespace yiiunit\framework\web; + +use Yii; +use yii\helpers\StringHelper; + +class MockResponse extends \yii\web\Response +{ + public function send() + { + // does nothing to allow testing + } +} + +/** + * @group web + */ +class ResponseTest extends \yiiunit\TestCase +{ + /** + * @var MockResponse + */ + public $response; + + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + $this->response = new MockResponse; + } + + public function rightRanges() + { + // TODO test more cases for range requests and check for rfc compatibility + // http://www.w3.org/Protocols/rfc2616/rfc2616.txt + return array( + array('0-5', '0-5', 6, '12ёж'), + array('2-', '2-66', 65, 'ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?'), + array('-12', '55-66', 12, '(ёжик)=?'), + ); + } + + /** + * @dataProvider rightRanges + */ + public function testSendFileRanges($rangeHeader, $expectedHeader, $length, $expectedContent) + { + $dataFile = \Yii::getAlias('@yiiunit/data/web/data.txt'); + $fullContent = file_get_contents($dataFile); + $_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader; + ob_start(); + $this->response->sendFile($dataFile); + $content = ob_get_clean(); + + $this->assertEquals($expectedContent, $content); + $this->assertEquals(206, $this->response->statusCode); + $headers = $this->response->headers; + $this->assertEquals("bytes", $headers->get('Accept-Ranges')); + $this->assertEquals("bytes " . $expectedHeader . '/' . StringHelper::strlen($fullContent), $headers->get('Content-Range')); + $this->assertEquals('text/plain', $headers->get('Content-Type')); + $this->assertEquals("$length", $headers->get('Content-Length')); + } + + public function wrongRanges() + { + // TODO test more cases for range requests and check for rfc compatibility + // http://www.w3.org/Protocols/rfc2616/rfc2616.txt + return array( + array('1-2,3-5,6-10'), // multiple range request not supported + array('5-1'), // last-byte-pos value is less than its first-byte-pos value + array('-100000'), // last-byte-pos bigger then content length + array('10000-'), // first-byte-pos bigger then content length + ); + } + + /** + * @dataProvider wrongRanges + */ + public function testSendFileWrongRanges($rangeHeader) + { + $this->setExpectedException('yii\web\HttpException'); + + $dataFile = \Yii::getAlias('@yiiunit/data/web/data.txt'); + $_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader; + $this->response->sendFile($dataFile); + } + + protected function generateTestFileContent() + { + return '12ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?'; + } +} diff --git a/tests/unit/framework/web/UrlManagerTest.php b/tests/unit/framework/web/UrlManagerTest.php index 95b3bf6..a77a66d 100644 --- a/tests/unit/framework/web/UrlManagerTest.php +++ b/tests/unit/framework/web/UrlManagerTest.php @@ -3,9 +3,19 @@ namespace yiiunit\framework\web; use yii\web\Request; use yii\web\UrlManager; +use yiiunit\TestCase; -class UrlManagerTest extends \yiiunit\TestCase +/** + * @group web + */ +class UrlManagerTest extends TestCase { + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + public function testCreateUrl() { // default setting with '/' as base url @@ -14,9 +24,9 @@ class UrlManagerTest extends \yiiunit\TestCase 'cache' => null, )); $url = $manager->createUrl('post/view'); - $this->assertEquals('/?r=post/view', $url); + $this->assertEquals('?r=post/view', $url); $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); - $this->assertEquals('/?r=post/view&id=1&title=sample+post', $url); + $this->assertEquals('?r=post/view&id=1&title=sample+post', $url); // default setting with '/test/' as base url $manager = new UrlManager(array( @@ -24,7 +34,7 @@ class UrlManagerTest extends \yiiunit\TestCase 'cache' => null, )); $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); - $this->assertEquals('/test/?r=post/view&id=1&title=sample+post', $url); + $this->assertEquals('/test?r=post/view&id=1&title=sample+post', $url); // pretty URL without rules $manager = new UrlManager(array( @@ -85,6 +95,24 @@ class UrlManagerTest extends \yiiunit\TestCase $this->assertEquals('/post/1/sample+post.html', $url); $url = $manager->createUrl('post/index', array('page' => 1)); $this->assertEquals('/post/index.html?page=1', $url); + + // pretty URL with rules that have host info + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'cache' => null, + 'rules' => array( + array( + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + 'host' => 'http://<lang:en|fr>.example.com', + ), + ), + 'baseUrl' => '/test', + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post', 'lang' => 'en')); + $this->assertEquals('http://en.example.com/test/post/1/sample+post', $url); + $url = $manager->createUrl('post/index', array('page' => 1)); + $this->assertEquals('/test/post/index?page=1', $url); } public function testCreateAbsoluteUrl() @@ -95,7 +123,7 @@ class UrlManagerTest extends \yiiunit\TestCase 'cache' => null, )); $url = $manager->createAbsoluteUrl('post/view', array('id' => 1, 'title' => 'sample post')); - $this->assertEquals('http://www.example.com/?r=post/view&id=1&title=sample+post', $url); + $this->assertEquals('http://www.example.com?r=post/view&id=1&title=sample+post', $url); } public function testParseRequest() @@ -138,9 +166,9 @@ class UrlManagerTest extends \yiiunit\TestCase $result = $manager->parseRequest($request); $this->assertEquals(array('module/site/index', array()), $result); // pathinfo with trailing slashes - $request->pathInfo = 'module/site/index/'; + $request->pathInfo = '/module/site/index/'; $result = $manager->parseRequest($request); - $this->assertEquals(array('module/site/index', array()), $result); + $this->assertEquals(array('module/site/index/', array()), $result); // pretty URL rules $manager = new UrlManager(array( @@ -157,10 +185,10 @@ class UrlManagerTest extends \yiiunit\TestCase $request->pathInfo = 'post/123/this+is+sample'; $result = $manager->parseRequest($request); $this->assertEquals(array('post/view', array('id' => '123', 'title' => 'this+is+sample')), $result); - // matching pathinfo with trailing slashes + // trailing slash is significant $request->pathInfo = 'post/123/this+is+sample/'; $result = $manager->parseRequest($request); - $this->assertEquals(array('post/view', array('id' => '123', 'title' => 'this+is+sample')), $result); + $this->assertEquals(array('post/123/this+is+sample/', array()), $result); // empty pathinfo $request->pathInfo = ''; $result = $manager->parseRequest($request); @@ -206,5 +234,79 @@ class UrlManagerTest extends \yiiunit\TestCase $request->pathInfo = 'site/index'; $result = $manager->parseRequest($request); $this->assertFalse($result); + + // strict parsing + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'enableStrictParsing' => true, + 'suffix' => '.html', + 'cache' => null, + 'rules' => array( + array( + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ), + ), + )); + // matching pathinfo + $request->pathInfo = 'post/123/this+is+sample.html'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/view', array('id' => '123', 'title' => 'this+is+sample')), $result); + // unmatching pathinfo + $request->pathInfo = 'site/index.html'; + $result = $manager->parseRequest($request); + $this->assertFalse($result); + } + + public function testParseRESTRequest() + { + $request = new Request; + + // pretty URL rules + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'showScriptName' => false, + 'cache' => null, + 'rules' => array( + 'PUT,POST post/<id>/<title>' => 'post/create', + 'DELETE post/<id>' => 'post/delete', + 'post/<id>/<title>' => 'post/view', + 'POST/GET' => 'post/get', + ), + )); + // matching pathinfo GET request + $_SERVER['REQUEST_METHOD'] = 'GET'; + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/view', array('id' => '123', 'title' => 'this+is+sample')), $result); + // matching pathinfo PUT/POST request + $_SERVER['REQUEST_METHOD'] = 'PUT'; + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/create', array('id' => '123', 'title' => 'this+is+sample')), $result); + $_SERVER['REQUEST_METHOD'] = 'POST'; + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/create', array('id' => '123', 'title' => 'this+is+sample')), $result); + + // no wrong matching + $_SERVER['REQUEST_METHOD'] = 'POST'; + $request->pathInfo = 'POST/GET'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/get', array()), $result); + + // createUrl should ignore REST rules + $this->mockApplication(array( + 'components' => array( + 'request' => array( + 'hostInfo' => 'http://localhost/', + 'baseUrl' => '/app' + ) + ) + ), \yii\web\Application::className()); + $this->assertEquals('/app/post/delete?id=123', $manager->createUrl('post/delete', array('id' => 123))); + $this->destroyApplication(); + + unset($_SERVER['REQUEST_METHOD']); } } diff --git a/tests/unit/framework/web/UrlRuleTest.php b/tests/unit/framework/web/UrlRuleTest.php index 825199e..5e57cb6 100644 --- a/tests/unit/framework/web/UrlRuleTest.php +++ b/tests/unit/framework/web/UrlRuleTest.php @@ -5,8 +5,12 @@ namespace yiiunit\framework\web; use yii\web\UrlManager; use yii\web\UrlRule; use yii\web\Request; +use yiiunit\TestCase; -class UrlRuleTest extends \yiiunit\TestCase +/** + * @group web + */ +class UrlRuleTest extends TestCase { public function testCreateUrl() { @@ -26,7 +30,7 @@ class UrlRuleTest extends \yiiunit\TestCase public function testParseRequest() { $manager = new UrlManager(array('cache' => null)); - $request = new Request; + $request = new Request(array('hostInfo' => 'http://en.example.com')); $suites = $this->getTestsForParseRequest(); foreach ($suites as $i => $suite) { list ($name, $config, $tests) = $suite; @@ -327,6 +331,19 @@ class UrlRuleTest extends \yiiunit\TestCase array('post/index', array('page' => 1), 'posts/?page=1'), ), ), + array( + 'with host info', + array( + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + 'host' => 'http://<lang:en|fr>.example.com', + ), + array( + array('post/index', array('page' => 1, 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a', 'lang' => 'en'), 'http://en.example.com/post/a'), + ), + ), ); } @@ -363,6 +380,17 @@ class UrlRuleTest extends \yiiunit\TestCase ), ), array( + 'with dot', // https://github.com/yiisoft/yii/issues/2945 + array( + 'pattern' => 'posts.html', + 'route' => 'post/index', + ), + array( + array('posts.html', 'post/index'), + array('postsahtml', false), + ), + ), + array( 'creation only', array( 'pattern' => 'posts', @@ -447,6 +475,7 @@ class UrlRuleTest extends \yiiunit\TestCase ), array( array('post/1/a/yes', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'yes')), + array('post/1/a/no', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'no')), array('post/2/a/no', 'post/index', array('page' => '2', 'tag' => 'a', 'sort' => 'no')), array('post/2/a', 'post/index', array('page' => '2', 'tag' => 'a', 'sort' => 'yes')), array('post/a/no', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'no')), @@ -606,10 +635,24 @@ class UrlRuleTest extends \yiiunit\TestCase 'suffix' => '/', ), array( - array('posts', 'post/index'), + array('posts/', 'post/index'), + array('posts', false), array('a', false), ), ), + array( + 'with host info', + array( + 'pattern' => 'post/<page:\d+>', + 'route' => 'post/index', + 'host' => 'http://<lang:en|fr>.example.com', + ), + array( + array('post/1', 'post/index', array('page' => '1', 'lang' => 'en')), + array('post/a', false), + array('post/1/a', false), + ), + ), ); } } diff --git a/tests/unit/framework/web/XmlResponseFormatterTest.php b/tests/unit/framework/web/XmlResponseFormatterTest.php new file mode 100644 index 0000000..e97962a --- /dev/null +++ b/tests/unit/framework/web/XmlResponseFormatterTest.php @@ -0,0 +1,138 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yiiunit\framework\web; + +use yii\base\Object; +use yii\web\Response; +use yii\web\XmlResponseFormatter; + +class Post extends Object +{ + public $id; + public $title; + + public function __construct($id, $title) + { + $this->id = $id; + $this->title = $title; + } +} + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + * + * @group web + */ +class XmlResponseFormatterTest extends \yiiunit\TestCase +{ + /** + * @var Response + */ + public $response; + /** + * @var XmlResponseFormatter + */ + public $formatter; + + protected function setUp() + { + $this->mockApplication(); + $this->response = new Response; + $this->formatter = new XmlResponseFormatter; + } + + /** + * @param mixed $data the data to be formatted + * @param string $xml the expected XML body + * @dataProvider formatScalarDataProvider + */ + public function testFormatScalar($data, $xml) + { + $head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; + $this->response->data = $data; + $this->formatter->format($this->response); + $this->assertEquals($head . $xml, $this->response->content); + } + + public function formatScalarDataProvider() + { + return array( + array(null, "<response></response>\n"), + array(1, "<response>1</response>\n"), + array('abc', "<response>abc</response>\n"), + array(true, "<response>1</response>\n"), + array("<>", "<response><></response>\n"), + ); + } + + /** + * @param mixed $data the data to be formatted + * @param string $xml the expected XML body + * @dataProvider formatArrayDataProvider + */ + public function testFormatArrays($data, $xml) + { + $head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; + $this->response->data = $data; + $this->formatter->format($this->response); + $this->assertEquals($head . $xml, $this->response->content); + } + + public function formatArrayDataProvider() + { + return array( + array(array(), "<response/>\n"), + array(array(1, 'abc'), "<response><item>1</item><item>abc</item></response>\n"), + array(array( + 'a' => 1, + 'b' => 'abc', + ), "<response><a>1</a><b>abc</b></response>\n"), + array(array( + 1, + 'abc', + array(2, 'def'), + true, + ), "<response><item>1</item><item>abc</item><item><item>2</item><item>def</item></item><item>1</item></response>\n"), + array(array( + 'a' => 1, + 'b' => 'abc', + 'c' => array(2, '<>'), + true, + ), "<response><a>1</a><b>abc</b><c><item>2</item><item><></item></c><item>1</item></response>\n"), + ); + } + + /** + * @param mixed $data the data to be formatted + * @param string $xml the expected XML body + * @dataProvider formatObjectDataProvider + */ + public function testFormatObjects($data, $xml) + { + $head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; + $this->response->data = $data; + $this->formatter->format($this->response); + $this->assertEquals($head . $xml, $this->response->content); + } + + public function formatObjectDataProvider() + { + return array( + array(new Post(123, 'abc'), "<response><Post><id>123</id><title>abc</title></Post></response>\n"), + array(array( + new Post(123, 'abc'), + new Post(456, 'def'), + ), "<response><Post><id>123</id><title>abc</title></Post><Post><id>456</id><title>def</title></Post></response>\n"), + array(array( + new Post(123, '<>'), + 'a' => new Post(456, 'def'), + ), "<response><Post><id>123</id><title><></title></Post><a><Post><id>456</id><title>def</title></Post></a></response>\n"), + ); + } +} diff --git a/tests/unit/framework/widgets/SpacelessTest.php b/tests/unit/framework/widgets/SpacelessTest.php new file mode 100644 index 0000000..00f5a96 --- /dev/null +++ b/tests/unit/framework/widgets/SpacelessTest.php @@ -0,0 +1,41 @@ +<?php + +namespace yiiunit\framework\widgets; + +use yii\widgets\Spaceless; + +/** + * @group widgets + */ +class SpacelessTest extends \yiiunit\TestCase +{ + public function testWidget() + { + ob_start(); + ob_implicit_flush(false); + + echo "<body>\n"; + + Spaceless::begin(); + echo "\t<div class='wrapper'>\n"; + + Spaceless::begin(); + echo "\t\t<div class='left-column'>\n"; + echo "\t\t\t<p>This is a left bar!</p>\n"; + echo "\t\t</div>\n\n"; + echo "\t\t<div class='right-column'>\n"; + echo "\t\t\t<p>This is a right bar!</p>\n"; + echo "\t\t</div>\n"; + Spaceless::end(); + + echo "\t</div>\n"; + Spaceless::end(); + + echo "\t<p>Bye!</p>\n"; + echo "</body>\n"; + + $expected="<body>\n<div class='wrapper'><div class='left-column'><p>This is a left bar!</p>". + "</div><div class='right-column'><p>This is a right bar!</p></div></div>\t<p>Bye!</p>\n</body>\n"; + $this->assertEquals($expected, ob_get_clean()); + } +} diff --git a/tests/unit/runtime/.gitignore b/tests/unit/runtime/.gitignore index 72e8ffc..f59ec20 100644 --- a/tests/unit/runtime/.gitignore +++ b/tests/unit/runtime/.gitignore @@ -1 +1 @@ -* +* \ No newline at end of file diff --git a/tests/unit/runtime/coveralls/.gitkeep b/tests/unit/runtime/coveralls/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/unit/runtime/coveralls/.gitkeep diff --git a/tests/web/app/index.php b/tests/web/app/index.php index 7096665..5cc5b4b 100644 --- a/tests/web/app/index.php +++ b/tests/web/app/index.php @@ -1,6 +1,6 @@ <?php -require(__DIR__ . '/../../../yii/Yii.php'); +require(__DIR__ . '/../../../framework/yii/Yii.php'); $application = new yii\web\Application('test', __DIR__ . '/protected'); $application->run(); diff --git a/tests/web/app/protected/controllers/SiteController.php b/tests/web/app/protected/controllers/SiteController.php index 0cc92ae..89a4086 100644 --- a/tests/web/app/protected/controllers/SiteController.php +++ b/tests/web/app/protected/controllers/SiteController.php @@ -16,7 +16,7 @@ class DefaultController extends \yii\web\Controller 'value 1' => 'item 1', 'value 2' => 'item 2', 'value 3' => 'item 3', - ), isset($_POST['test']) ? $_POST['test'] : null, + ), isset($_POST['test']) ? $_POST['test'] : null, function ($index, $label, $name, $value, $checked) { return Html::label( $label . ' ' . Html::checkbox($name, $value, $checked), diff --git a/upgrade.md b/upgrade.md deleted file mode 100644 index 6dd89b7..0000000 --- a/upgrade.md +++ /dev/null @@ -1,46 +0,0 @@ -Upgrading Instructions for Yii Framework v2 -=========================================== - -!!!IMPORTANT!!! - -The following upgrading instructions are cumulative. That is, -if you want to upgrade from version A to version C and there is -version B between A and C, you need to following the instructions -for both A and B. - - -General upgrade instructions ----------------------------- - -- Make a backup. -- Clean up your 'assets' folder. -- Replace 'framework' dir with the new one or point Git to a fresh - release tag and checkout. -- Check if everything is OK, if not — revert to previous stable version and post - issues to [Yii issue tracker](https://github.com/yiisoft/yii2/issues). - - -Upgrading from v1.1.x ---------------------- - -- All framework classes are now namespaced, and the name prefix `C` is removed. - -- The format of path alias is changed to `@yii/base/Component`. - In 1.x, this would be `system.base.CComponent`. See guide for more details. - -- The root alias `@yii` now represents the framework installation directory. - In 1.x, this is named as `system`. We also removed `zii` root alias. - -- `Object` serves as the base class that supports properties. And `Component` extends - from `Object` and supports events and behaviors. Behaviors declared in - `Component::behaviors()` are attached on demand. - -- `CList` is renamed to `Vector`, and `CMap` is renamed to `Dictionary`. - Other collection classes are dropped in favor of SPL classes. - -- `CFormModel` is removed. Please use `yii\base\Model` instead. - -- `CDbCriteria` is replaced by `yii\db\Query` which includes methods for - building a query. `CDbCommandBuilder` is replaced by `yii\db\QueryBuilder` - which has cleaner and more complete support of query building capabilities. - diff --git a/yii/assets.php b/yii/assets.php deleted file mode 100644 index 7ee177d..0000000 --- a/yii/assets.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php - -return array( - 'jquery' => array( - 'sourcePath' => __DIR__ . '/assets', - 'js' => array( - 'jquery.min.js', - ), - ), - 'yii' => array( - 'sourcePath' => __DIR__ . '/assets', - 'js' => array( - 'yii.js', - ), - 'depends' => array('jquery'), - ), - 'yii/validation' => array( - 'sourcePath' => __DIR__ . '/assets', - 'js' => array( - 'yii.validation.js', - ), - 'depends' => array('yii'), - ), - 'yii/form' => array( - 'sourcePath' => __DIR__ . '/assets', - 'js' => array( - 'yii.activeForm.js', - ), - 'depends' => array('yii', 'yii/validation'), - ), - 'yii/captcha' => array( - 'sourcePath' => __DIR__ . '/assets', - 'js' => array( - 'yii.captcha.js', - ), - 'depends' => array('yii'), - ), - 'yii/debug' => array( - 'sourcePath' => __DIR__ . '/assets', - 'js' => array( - 'yii.debug.js', - ), - 'depends' => array('yii'), - ), -); diff --git a/yii/assets/yii.debug.js b/yii/assets/yii.debug.js deleted file mode 100644 index 4e32d89..0000000 --- a/yii/assets/yii.debug.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Yii debug module. - * - * This JavaScript module provides the functions needed by the Yii debug toolbar. - * - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ - -yii.debug = (function ($) { - return { - load: function (id, url) { - $.ajax({ - url: url, - //dataType: 'json', - success: function(data) { - var $e = $('#' + id); - $e.html(data); - } - }); - } - }; -})(jQuery); diff --git a/yii/base/Dictionary.php b/yii/base/Dictionary.php deleted file mode 100644 index 5807d93..0000000 --- a/yii/base/Dictionary.php +++ /dev/null @@ -1,297 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\base; - -use yii\helpers\ArrayHelper; - -/** - * Dictionary implements a collection that stores key-value pairs. - * - * You can access, add or remove an item with a key by using - * [[itemAt()]], [[add()]], and [[remove()]]. - * - * To get the number of the items in the dictionary, use [[getCount()]]. - * - * Because Dictionary implements a set of SPL interfaces, it can be used - * like a regular PHP array as follows, - * - * ~~~ - * $dictionary[$key] = $value; // add a key-value pair - * unset($dictionary[$key]); // remove the value with the specified key - * if (isset($dictionary[$key])) // if the dictionary contains the key - * foreach ($dictionary as $key=>$value) // traverse the items in the dictionary - * $n = count($dictionary); // returns the number of items in the dictionary - * ~~~ - * - * @property integer $count the number of items in the dictionary - * @property array $keys The keys in the dictionary - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Countable -{ - /** - * @var array internal data storage - */ - private $_d = array(); - - /** - * Constructor. - * Initializes the dictionary with an array or an iterable object. - * @param mixed $data the initial data to be populated into the dictionary. - * This can be an array or an iterable object. - * @param array $config name-value pairs that will be used to initialize the object properties - * @throws Exception if data is not well formed (neither an array nor an iterable object) - */ - public function __construct($data = array(), $config = array()) - { - if (!empty($data)) { - $this->copyFrom($data); - } - parent::__construct($config); - } - - /** - * Returns an iterator for traversing the items in the dictionary. - * This method is required by the SPL interface `IteratorAggregate`. - * It will be implicitly called when you use `foreach` to traverse the dictionary. - * @return DictionaryIterator an iterator for traversing the items in the dictionary. - */ - public function getIterator() - { - return new DictionaryIterator($this->_d); - } - - /** - * Returns the number of items in the dictionary. - * This method is required by the SPL `Countable` interface. - * It will be implicitly called when you use `count($dictionary)`. - * @return integer number of items in the dictionary. - */ - public function count() - { - return $this->getCount(); - } - - /** - * Returns the number of items in the dictionary. - * @return integer the number of items in the dictionary - */ - public function getCount() - { - return count($this->_d); - } - - /** - * Returns the keys stored in the dictionary. - * @return array the key list - */ - public function getKeys() - { - return array_keys($this->_d); - } - - /** - * Returns the item with the specified key. - * @param mixed $key the key - * @return mixed the element with the specified key. - * Null if the key cannot be found in the dictionary. - */ - public function itemAt($key) - { - return isset($this->_d[$key]) ? $this->_d[$key] : null; - } - - /** - * Adds an item into the dictionary. - * Note, if the specified key already exists, the old value will be overwritten. - * @param mixed $key key - * @param mixed $value value - * @throws Exception if the dictionary is read-only - */ - public function add($key, $value) - { - if ($key === null) { - $this->_d[] = $value; - } else { - $this->_d[$key] = $value; - } - } - - /** - * Removes an item from the dictionary by its key. - * @param mixed $key the key of the item to be removed - * @return mixed the removed value, null if no such key exists. - * @throws Exception if the dictionary is read-only - */ - public function remove($key) - { - if (isset($this->_d[$key])) { - $value = $this->_d[$key]; - unset($this->_d[$key]); - return $value; - } else { // the value is null - unset($this->_d[$key]); - return null; - } - } - - /** - * Removes all items from the dictionary. - * @param boolean $safeClear whether to clear every item by calling [[remove]]. - * Defaults to false, meaning all items in the dictionary will be cleared directly - * without calling [[remove]]. - */ - public function removeAll($safeClear = false) - { - if ($safeClear) { - foreach (array_keys($this->_d) as $key) { - $this->remove($key); - } - } else { - $this->_d = array(); - } - } - - /** - * Returns a value indicating whether the dictionary contains the specified key. - * @param mixed $key the key - * @return boolean whether the dictionary contains an item with the specified key - */ - public function has($key) - { - return isset($this->_d[$key]) || array_key_exists($key, $this->_d); - } - - /** - * Returns the dictionary as a PHP array. - * @return array the list of items in array - */ - public function toArray() - { - return $this->_d; - } - - /** - * Copies iterable data into the dictionary. - * Note, existing data in the dictionary will be cleared first. - * @param mixed $data the data to be copied from, must be an array or an object implementing `Traversable` - * @throws InvalidParamException if data is neither an array nor an iterator. - */ - public function copyFrom($data) - { - if (is_array($data) || $data instanceof \Traversable) { - if (!empty($this->_d)) { - $this->removeAll(); - } - if ($data instanceof self) { - $data = $data->_d; - } - foreach ($data as $key => $value) { - $this->add($key, $value); - } - } else { - throw new InvalidParamException('Data must be either an array or an object implementing Traversable.'); - } - } - - /** - * Merges iterable data into the dictionary. - * - * Existing elements in the dictionary will be overwritten if their keys are the same as those in the source. - * If the merge is recursive, the following algorithm is performed: - * - * - the dictionary data is saved as $a, and the source data is saved as $b; - * - if $a and $b both have an array indexed at the same string key, the arrays will be merged using this algorithm; - * - any integer-indexed elements in $b will be appended to $a; - * - any string-indexed elements in $b will overwrite elements in $a with the same index; - * - * @param array|\Traversable $data the data to be merged with. It must be an array or object implementing Traversable - * @param boolean $recursive whether the merging should be recursive. - * @throws InvalidParamException if data is neither an array nor an object implementing `Traversable`. - */ - public function mergeWith($data, $recursive = true) - { - if (is_array($data) || $data instanceof \Traversable) { - if ($data instanceof self) { - $data = $data->_d; - } - if ($recursive) { - if ($data instanceof \Traversable) { - $d = array(); - foreach ($data as $key => $value) { - $d[$key] = $value; - } - $this->_d = ArrayHelper::merge($this->_d, $d); - } else { - $this->_d = ArrayHelper::merge($this->_d, $data); - } - } else { - foreach ($data as $key => $value) { - $this->add($key, $value); - } - } - } else { - throw new InvalidParamException('The data to be merged with must be an array or an object implementing Traversable.'); - } - } - - /** - * Returns whether there is an element at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `isset($dictionary[$offset])`. - * This is equivalent to [[contains]]. - * @param mixed $offset the offset to check on - * @return boolean - */ - public function offsetExists($offset) - { - return $this->has($offset); - } - - /** - * Returns the element at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$value = $dictionary[$offset];`. - * This is equivalent to [[itemAt]]. - * @param mixed $offset the offset to retrieve element. - * @return mixed the element at the offset, null if no element is found at the offset - */ - public function offsetGet($offset) - { - return $this->itemAt($offset); - } - - /** - * Sets the element at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$dictionary[$offset] = $item;`. - * If the offset is null, the new item will be appended to the dictionary. - * Otherwise, the existing item at the offset will be replaced with the new item. - * This is equivalent to [[add]]. - * @param mixed $offset the offset to set element - * @param mixed $item the element value - */ - public function offsetSet($offset, $item) - { - $this->add($offset, $item); - } - - /** - * Unsets the element at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `unset($dictionary[$offset])`. - * This is equivalent to [[remove]]. - * @param mixed $offset the offset to unset element - */ - public function offsetUnset($offset) - { - $this->remove($offset); - } -} diff --git a/yii/base/ErrorHandler.php b/yii/base/ErrorHandler.php deleted file mode 100644 index 98a061d..0000000 --- a/yii/base/ErrorHandler.php +++ /dev/null @@ -1,280 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\base; - -/** - * ErrorHandler handles uncaught PHP errors and exceptions. - * - * ErrorHandler displays these errors using appropriate views based on the - * nature of the errors and the mode the application runs at. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class ErrorHandler extends Component -{ - /** - * @var integer maximum number of source code lines to be displayed. Defaults to 25. - */ - public $maxSourceLines = 25; - /** - * @var integer maximum number of trace source code lines to be displayed. Defaults to 10. - */ - public $maxTraceSourceLines = 10; - /** - * @var boolean whether to discard any existing page output before error display. Defaults to true. - */ - public $discardExistingOutput = true; - /** - * @var string the route (eg 'site/error') to the controller action that will be used to display external errors. - * Inside the action, it can retrieve the error information by \Yii::$app->errorHandler->error. - * This property defaults to null, meaning ErrorHandler will handle the error display. - */ - public $errorAction; - /** - * @var string the path of the view file for rendering exceptions - */ - public $exceptionView = '@yii/views/exception.php'; - /** - * @var string the path of the view file for rendering errors - */ - public $errorView = '@yii/views/error.php'; - /** - * @var \Exception the exception that is being handled currently - */ - public $exception; - - - /** - * Handles exception - * @param \Exception $exception - */ - public function handle($exception) - { - $this->exception = $exception; - - if ($this->discardExistingOutput) { - $this->clearOutput(); - } - - $this->renderException($exception); - } - - /** - * Renders exception - * @param \Exception $exception - */ - protected function renderException($exception) - { - if ($this->errorAction !== null) { - \Yii::$app->runAction($this->errorAction); - } elseif (\Yii::$app instanceof \yii\web\Application) { - if (!headers_sent()) { - $errorCode = $exception instanceof HttpException ? $exception->statusCode : 500; - header("HTTP/1.0 $errorCode " . get_class($exception)); - } - if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { - \Yii::$app->renderException($exception); - } else { - // if there is an error during error rendering it's useful to - // display PHP error in debug mode instead of a blank screen - if(YII_DEBUG) { - ini_set('display_errors', 1); - } - - $view = new View; - if (!YII_DEBUG || $exception instanceof UserException) { - $viewName = $this->errorView; - } else { - $viewName = $this->exceptionView; - } - echo $view->renderFile($viewName, array( - 'exception' => $exception, - ), $this); - } - } else { - \Yii::$app->renderException($exception); - } - } - - /** - * Returns server and Yii version information. - * @return string server version information. - */ - public function getVersionInfo() - { - $version = '<a href="http://www.yiiframework.com/">Yii Framework</a>/' . \Yii::getVersion(); - if (isset($_SERVER['SERVER_SOFTWARE'])) { - $version = $_SERVER['SERVER_SOFTWARE'] . ' ' . $version; - } - return $version; - } - - /** - * Converts arguments array to its string representation - * - * @param array $args arguments array to be converted - * @return string string representation of the arguments array - */ - public function argumentsToString($args) - { - $isAssoc = $args !== array_values($args); - $count = 0; - foreach ($args as $key => $value) { - $count++; - if ($count >= 5) { - if ($count > 5) { - unset($args[$key]); - } else { - $args[$key] = '...'; - } - continue; - } - - if (is_object($value)) { - $args[$key] = get_class($value); - } elseif (is_bool($value)) { - $args[$key] = $value ? 'true' : 'false'; - } elseif (is_string($value)) { - if (strlen($value) > 64) { - $args[$key] = '"' . substr($value, 0, 64) . '..."'; - } else { - $args[$key] = '"' . $value . '"'; - } - } elseif (is_array($value)) { - $args[$key] = 'array(' . $this->argumentsToString($value) . ')'; - } elseif ($value === null) { - $args[$key] = 'null'; - } elseif (is_resource($value)) { - $args[$key] = 'resource'; - } - - if (is_string($key)) { - $args[$key] = '"' . $key . '" => ' . $args[$key]; - } elseif ($isAssoc) { - $args[$key] = $key . ' => ' . $args[$key]; - } - } - return implode(', ', $args); - } - - /** - * Returns a value indicating whether the call stack is from application code. - * @param array $trace the trace data - * @return boolean whether the call stack is from application code. - */ - public function isCoreCode($trace) - { - if (isset($trace['file'])) { - return $trace['file'] === 'unknown' || strpos(realpath($trace['file']), YII_PATH . DIRECTORY_SEPARATOR) === 0; - } - return false; - } - - /** - * Renders the source code around the error line. - * @param string $file source file path - * @param integer $errorLine the error line number - * @param integer $maxLines maximum number of lines to display - */ - public function renderSourceCode($file, $errorLine, $maxLines) - { - $errorLine--; // adjust line number to 0-based from 1-based - if ($errorLine < 0 || ($lines = @file($file)) === false || ($lineCount = count($lines)) <= $errorLine) { - return; - } - - $halfLines = (int)($maxLines / 2); - $beginLine = $errorLine - $halfLines > 0 ? $errorLine - $halfLines : 0; - $endLine = $errorLine + $halfLines < $lineCount ? $errorLine + $halfLines : $lineCount - 1; - $lineNumberWidth = strlen($endLine + 1); - - $output = ''; - for ($i = $beginLine; $i <= $endLine; ++$i) { - $isErrorLine = $i === $errorLine; - $code = sprintf("<span class=\"ln" . ($isErrorLine ? ' error-ln' : '') . "\">%0{$lineNumberWidth}d</span> %s", $i + 1, $this->htmlEncode(str_replace("\t", ' ', $lines[$i]))); - if (!$isErrorLine) { - $output .= $code; - } else { - $output .= '<span class="error">' . $code . '</span>'; - } - } - echo '<div class="code"><pre>' . $output . '</pre></div>'; - } - - /** - * Renders calls stack trace - * @param array $trace - */ - public function renderTrace($trace) - { - $count = 0; - echo "<table>\n"; - foreach ($trace as $n => $t) { - if ($this->isCoreCode($t)) { - $cssClass = 'core collapsed'; - } elseif (++$count > 3) { - $cssClass = 'app collapsed'; - } else { - $cssClass = 'app expanded'; - } - - $hasCode = isset($t['file']) && $t['file'] !== 'unknown' && is_file($t['file']); - echo "<tr class=\"trace $cssClass\"><td class=\"number\">#$n</td><td class=\"content\">"; - echo '<div class="trace-file">'; - if ($hasCode) { - echo '<div class="plus">+</div><div class="minus">-</div>'; - } - echo ' '; - if(isset($t['file'])) { - echo $this->htmlEncode($t['file']) . '(' . $t['line'] . '): '; - } - if (!empty($t['class'])) { - echo '<strong>' . $t['class'] . '</strong>' . $t['type']; - } - echo '<strong>' . $t['function'] . '</strong>'; - echo '(' . (empty($t['args']) ? '' : $this->htmlEncode($this->argumentsToString($t['args']))) . ')'; - echo '</div>'; - if ($hasCode) { - $this->renderSourceCode($t['file'], $t['line'], $this->maxTraceSourceLines); - } - echo "</td></tr>\n"; - } - echo '</table>'; - } - - /** - * 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); - } - - public function clearOutput() - { - // the following manual level counting is to deal with zlib.output_compression set to On - for ($level = ob_get_level(); $level > 0; --$level) { - @ob_end_clean(); - } - } - - /** - * @param \Exception $exception - */ - public function renderAsHtml($exception) - { - $view = new View; - $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; - echo $view->renderFile($name, array( - 'exception' => $exception, - ), $this); - } -} diff --git a/yii/base/Vector.php b/yii/base/Vector.php deleted file mode 100644 index 6418077..0000000 --- a/yii/base/Vector.php +++ /dev/null @@ -1,341 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\base; - -/** - * Vector implements an integer-indexed collection class. - * - * You can access, append, insert, remove an item from the vector - * by calling methods such as [[itemAt()]], [[add()]], [[insertAt()]], - * [[remove()]] and [[removeAt()]]. - * - * To get the number of the items in the vector, use [[getCount()]]. - * - * Because Vector implements a set of SPL interfaces, it can be used - * like a regular PHP array as follows, - * - * ~~~ - * $vector[] = $item; // append new item at the end - * $vector[$index] = $item; // set new item at $index - * unset($vector[$index]); // remove the item at $index - * if (isset($vector[$index])) // if the vector has an item at $index - * foreach ($vector as $index=>$item) // traverse each item in the vector - * $n = count($vector); // count the number of items - * ~~~ - * - * Note that if you plan to extend Vector by performing additional operations - * with each addition or removal of an item (e.g. performing type check), - * please make sure you override [[insertAt()]] and [[removeAt()]]. - * - * @property integer $count the number of items in the vector - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Countable -{ - /** - * @var array internal data storage - */ - private $_d = array(); - /** - * @var integer number of items - */ - private $_c = 0; - - /** - * Constructor. - * Initializes the vector with an array or an iterable object. - * @param mixed $data the initial data to be populated into the vector. - * This can be an array or an iterable object. - * @param array $config name-value pairs that will be used to initialize the object properties - * @throws Exception if data is not well formed (neither an array nor an iterable object) - */ - public function __construct($data = array(), $config = array()) - { - if (!empty($data)) { - $this->copyFrom($data); - } - parent::__construct($config); - } - - /** - * Returns an iterator for traversing the items in the vector. - * This method is required by the SPL interface `IteratorAggregate`. - * It will be implicitly called when you use `foreach` to traverse the vector. - * @return VectorIterator an iterator for traversing the items in the vector. - */ - public function getIterator() - { - return new VectorIterator($this->_d); - } - - /** - * Returns the number of items in the vector. - * This method is required by the SPL `Countable` interface. - * It will be implicitly called when you use `count($vector)`. - * @return integer number of items in the vector. - */ - public function count() - { - return $this->getCount(); - } - - /** - * Returns the number of items in the vector. - * @return integer the number of items in the vector - */ - public function getCount() - { - return $this->_c; - } - - /** - * Returns the item at the specified index. - * @param integer $index the index of the item - * @return mixed the item at the index - * @throws InvalidParamException if the index is out of range - */ - public function itemAt($index) - { - if (isset($this->_d[$index])) { - return $this->_d[$index]; - } elseif ($index >= 0 && $index < $this->_c) { // in case the value is null - return $this->_d[$index]; - } else { - throw new InvalidParamException('Index out of range: ' . $index); - } - } - - /** - * Appends an item at the end of the vector. - * @param mixed $item new item - * @return integer the zero-based index at which the item is added - * @throws Exception if the vector is read-only. - */ - public function add($item) - { - $this->insertAt($this->_c, $item); - return $this->_c - 1; - } - - /** - * Inserts an item at the specified position. - * Original item at the position and the following items will be moved - * one step towards the end. - * @param integer $index the specified position. - * @param mixed $item new item to be inserted into the vector - * @throws InvalidParamException if the index specified is out of range, or the vector is read-only. - */ - public function insertAt($index, $item) - { - if ($index === $this->_c) { - $this->_d[$this->_c++] = $item; - } elseif ($index >= 0 && $index < $this->_c) { - array_splice($this->_d, $index, 0, array($item)); - $this->_c++; - } else { - throw new InvalidParamException('Index out of range: ' . $index); - } - } - - /** - * Removes an item from the vector. - * The vector will search for the item, and the first item found - * will be removed from the vector. - * @param mixed $item the item to be removed. - * @return mixed the index at which the item is being removed, or false - * if the item cannot be found in the vector. - * @throws Exception if the vector is read only. - */ - public function remove($item) - { - if (($index = $this->indexOf($item)) >= 0) { - $this->removeAt($index); - return $index; - } else { - return false; - } - } - - /** - * Removes an item at the specified position. - * @param integer $index the index of the item to be removed. - * @return mixed the removed item. - * @throws InvalidParamException if the index is out of range, or the vector is read only. - */ - public function removeAt($index) - { - if ($index >= 0 && $index < $this->_c) { - $this->_c--; - if ($index === $this->_c) { - return array_pop($this->_d); - } else { - $item = $this->_d[$index]; - array_splice($this->_d, $index, 1); - return $item; - } - } else { - throw new InvalidParamException('Index out of range: ' . $index); - } - } - - /** - * Removes all items from the vector. - * @param boolean $safeClear whether to clear every item by calling [[removeAt]]. - * Defaults to false, meaning all items in the vector will be cleared directly - * without calling [[removeAt]]. - */ - public function removeAll($safeClear = false) - { - if ($safeClear) { - for ($i = $this->_c - 1; $i >= 0; --$i) { - $this->removeAt($i); - } - } else { - $this->_d = array(); - $this->_c = 0; - } - } - - /** - * Returns a value indicating whether the vector contains the specified item. - * Note that the search is based on strict PHP comparison. - * @param mixed $item the item - * @return boolean whether the vector contains the item - */ - public function has($item) - { - return $this->indexOf($item) >= 0; - } - - /** - * Returns the index of the specified item in the vector. - * The index is zero-based. If the item is not found in the vector, -1 will be returned. - * Note that the search is based on strict PHP comparison. - * @param mixed $item the item - * @return integer the index of the item in the vector (0 based), -1 if not found. - */ - public function indexOf($item) - { - $index = array_search($item, $this->_d, true); - return $index === false ? -1 : $index; - } - - /** - * Returns the vector as a PHP array. - * @return array the items in the vector. - */ - public function toArray() - { - return $this->_d; - } - - /** - * Copies iterable data into the vector. - * Note, existing data in the vector will be cleared first. - * @param mixed $data the data to be copied from, must be an array or an object implementing `Traversable` - * @throws InvalidParamException if data is neither an array nor an object implementing `Traversable`. - */ - public function copyFrom($data) - { - if (is_array($data) || $data instanceof \Traversable) { - if ($this->_c > 0) { - $this->removeAll(); - } - if ($data instanceof self) { - $data = $data->_d; - } - foreach ($data as $item) { - $this->add($item); - } - } else { - throw new InvalidParamException('Data must be either an array or an object implementing Traversable.'); - } - } - - /** - * Merges iterable data into the vector. - * New items will be appended to the end of the existing items. - * @param array|\Traversable $data the data to be merged with. It must be an array or object implementing Traversable - * @throws InvalidParamException if data is neither an array nor an object implementing `Traversable`. - */ - public function mergeWith($data) - { - if (is_array($data) || ($data instanceof \Traversable)) { - if ($data instanceof Vector) { - $data = $data->_d; - } - foreach ($data as $item) { - $this->add($item); - } - } else { - throw new InvalidParamException('The data to be merged with must be an array or an object implementing Traversable.'); - } - } - - /** - * Returns a value indicating whether there is an item at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `isset($vector[$offset])`. - * @param integer $offset the offset to be checked - * @return boolean whether there is an item at the specified offset. - */ - public function offsetExists($offset) - { - return $offset >= 0 && $offset < $this->_c; - } - - /** - * Returns the item at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$value = $vector[$offset];`. - * This is equivalent to [[itemAt]]. - * @param integer $offset the offset to retrieve item. - * @return mixed the item at the offset - * @throws Exception if the offset is out of range - */ - public function offsetGet($offset) - { - return $this->itemAt($offset); - } - - /** - * Sets the item at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$vector[$offset] = $item;`. - * If the offset is null or equal to the number of the existing items, - * the new item will be appended to the vector. - * Otherwise, the existing item at the offset will be replaced with the new item. - * @param integer $offset the offset to set item - * @param mixed $item the item value - * @throws Exception if the offset is out of range, or the vector is read only. - */ - public function offsetSet($offset, $item) - { - if ($offset === null || $offset === $this->_c) { - $this->insertAt($this->_c, $item); - } else { - $this->removeAt($offset); - $this->insertAt($offset, $item); - } - } - - /** - * Unsets the item at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `unset($vector[$offset])`. - * This is equivalent to [[removeAt]]. - * @param integer $offset the offset to unset item - * @throws Exception if the offset is out of range, or the vector is read only. - */ - public function offsetUnset($offset) - { - $this->removeAt($offset); - } -} diff --git a/yii/base/VectorIterator.php b/yii/base/VectorIterator.php deleted file mode 100644 index f83d42d..0000000 --- a/yii/base/VectorIterator.php +++ /dev/null @@ -1,92 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\base; - -/** - * VectorIterator implements the SPL `Iterator` interface for [[Vector]]. - * - * It allows [[Vector]] to return a new iterator for data traversing purpose. - * You normally do not use this class directly. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class VectorIterator implements \Iterator -{ - /** - * @var array the data to be iterated through - */ - private $_d; - /** - * @var integer index of the current item - */ - private $_i; - /** - * @var integer count of the data items - */ - private $_c; - - /** - * Constructor. - * @param array $data the data to be iterated through - */ - public function __construct(&$data) - { - $this->_d = &$data; - $this->_i = 0; - $this->_c = count($this->_d); - } - - /** - * Rewinds the index of the current item. - * This method is required by the SPL interface `Iterator`. - */ - public function rewind() - { - $this->_i = 0; - } - - /** - * Returns the key of the current item. - * This method is required by the SPL interface `Iterator`. - * @return integer the key of the current item - */ - public function key() - { - return $this->_i; - } - - /** - * Returns the current item. - * This method is required by the SPL interface `Iterator`. - * @return mixed the current item - */ - public function current() - { - return $this->_d[$this->_i]; - } - - /** - * Moves the internal pointer to the next item. - * This method is required by the SPL interface `Iterator`. - */ - public function next() - { - $this->_i++; - } - - /** - * Returns a value indicating whether there is an item at current position. - * This method is required by the SPL interface `Iterator`. - * @return boolean whether there is an item at current position. - */ - public function valid() - { - return $this->_i < $this->_c; - } -} diff --git a/yii/console/controllers/AppController.php b/yii/console/controllers/AppController.php deleted file mode 100644 index 17f7420..0000000 --- a/yii/console/controllers/AppController.php +++ /dev/null @@ -1,324 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\console\controllers; - -use yii\console\Controller; -use yii\helpers\FileHelper; -use yii\base\Exception; - -/** - * This command creates an Yii Web application at the specified location. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @author Alexander Makarov <sam@rmcreative.ru> - * @since 2.0 - */ -class AppController extends Controller -{ - private $_rootPath; - private $_config; - - /** - * @var string custom template path. If specified, templates will be - * searched there additionally to `framework/console/webapp`. - */ - public $templatesPath; - - /** - * @var string application type. If not specified default application - * skeleton will be used. - */ - public $type = 'default'; - - public function init() - { - parent::init(); - - if($this->templatesPath && !is_dir($this->templatesPath)) { - throw new Exception('--templatesPath "'.$this->templatesPath.'" does not exist or can not be read.'); - } - } - - public function globalOptions() - { - return array('templatesPath', 'type'); - } - - public function actionIndex() - { - $this->forward('help/index', array('-args' => array('app/create'))); - } - - /** - * Generates Yii application at the path specified via appPath parameter. - * - * @param string $path the directory where the new application will be created. - * If the directory does not exist, it will be created. After the application - * is created, please make sure the directory has enough permissions. - * - * @throws \yii\base\Exception if path specified is not valid - * @return integer the exit status - */ - public function actionCreate($path) - { - $path = strtr($path, '/\\', DIRECTORY_SEPARATOR); - if(strpos($path, DIRECTORY_SEPARATOR) === false) { - $path = '.'.DIRECTORY_SEPARATOR.$path; - } - $dir = rtrim(realpath(dirname($path)), '\\/'); - if($dir === false || !is_dir($dir)) { - throw new Exception("The directory '$path' is not valid. Please make sure the parent directory exists."); - } - - if(basename($path) === '.') { - $this->_rootPath = $path = $dir; - } - else { - $this->_rootPath = $path = $dir.DIRECTORY_SEPARATOR.basename($path); - } - - if($this->confirm("Create \"$this->type\" application under '$path'?")) { - $sourceDir = $this->getSourceDir(); - $config = $this->getConfig(); - - $list = $this->buildFileList($sourceDir, $path); - - if(is_array($config)) { - foreach($config as $file => $settings) { - if(isset($settings['handler'])) { - $list[$file]['callback'] = $settings['handler']; - } - } - } - - $this->copyFiles($list); - - if(is_array($config)) { - foreach($config as $file => $settings) { - if(isset($settings['permissions'])) { - @chmod($path.'/'.$file, $settings['permissions']); - } - } - } - - echo "\nYour application has been created successfully under {$path}.\n"; - } - } - - /** - * @throws \yii\base\Exception if source directory wasn't located - * @return string - */ - protected function getSourceDir() - { - $customSource = realpath($this->templatesPath.'/'.$this->type); - $defaultSource = realpath($this->getDefaultTemplatesPath().'/'.$this->type); - - if($customSource) { - return $customSource; - } - elseif($defaultSource) { - return $defaultSource; - } - else { - throw new Exception('Unable to locate the source directory for "'.$this->type.'".'); - } - } - - /** - * @return string default templates path - */ - protected function getDefaultTemplatesPath() - { - return realpath(__DIR__.'/../webapp'); - } - - /** - * @return array|null template configuration - */ - protected function getConfig() - { - if($this->_config===null) { - $this->_config = require $this->getDefaultTemplatesPath().'/config.php'; - if($this->templatesPath && file_exists($this->templatesPath)) { - $this->_config = array_merge($this->_config, require $this->templatesPath.'/config.php'); - } - } - if(isset($this->_config[$this->type])) { - return $this->_config[$this->type]; - } - } - - /** - * @param string $source path to source file - * @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 contents - */ - public function replaceRelativePath($source, $pathTo, $varName) - { - $content = file_get_contents($source); - $relativeFile = str_replace($this->getSourceDir(), '', $source); - - $relativePath = $this->getRelativePath($pathTo, $this->_rootPath.$relativeFile); - $relativePath = str_replace('\\', '\\\\', $relativePath); - - return preg_replace('/\$'.$varName.'\s*=(.*?);/', "\$".$varName."=$relativePath;", $content); - } - - /** - * @param string $path1 absolute path - * @param string $path2 absolute path - * - * @return string relative path - */ - protected function getRelativePath($path1, $path2) - { - $segs1 = explode(DIRECTORY_SEPARATOR, $path1); - $segs2 = explode(DIRECTORY_SEPARATOR, $path2); - $n1 = count($segs1); - $n2 = count($segs2); - - for($i=0; $i<$n1 && $i<$n2; ++$i) { - if($segs1[$i] !== $segs2[$i]) { - break; - } - } - - if($i===0) { - return "'".$path1."'"; - } - $up=''; - for($j=$i;$j<$n2-1;++$j) { - $up.='/..'; - } - for(; $i<$n1-1; ++$i) { - $up.='/'.$segs1[$i]; - } - - 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: - * <ul> - * <li>source: required, the full path of the file/directory to be copied from</li> - * <li>target: required, the full path of the file/directory to be copied to</li> - * <li>callback: optional, the callback to be invoked when copying a file. The callback function - * should be declared as follows: - * <pre> - * function foo($source,$params) - * </pre> - * where $source parameter is the source file path, and the content returned - * by the function will be saved into the target file.</li> - * <li>params: optional, the parameters to be passed to the callback</li> - * </ul> - * @see buildFileList - */ - protected function copyFiles($fileList) - { - $overwriteAll = false; - foreach($fileList as $name=>$file) { - $source = strtr($file['source'], '/\\', DIRECTORY_SEPARATOR); - $target = strtr($file['target'], '/\\', DIRECTORY_SEPARATOR); - $callback = isset($file['callback']) ? $file['callback'] : null; - $params = isset($file['params']) ? $file['params'] : null; - - if(is_dir($source)) { - if (!is_dir($target)) { - mkdir($target, 0777, true); - } - continue; - } - - if($callback !== null) { - $content = call_user_func($callback, $source, $params); - } - else { - $content = file_get_contents($source); - } - if(is_file($target)) { - if($content === file_get_contents($target)) { - echo " unchanged $name\n"; - continue; - } - if($overwriteAll) { - echo " overwrite $name\n"; - } - else { - echo " exist $name\n"; - echo " ...overwrite? [Yes|No|All|Quit] "; - $answer = trim(fgets(STDIN)); - if(!strncasecmp($answer, 'q', 1)) { - return; - } - elseif(!strncasecmp($answer, 'y', 1)) { - echo " overwrite $name\n"; - } - elseif(!strncasecmp($answer, 'a', 1)) { - echo " overwrite $name\n"; - $overwriteAll = true; - } - else { - echo " skip $name\n"; - continue; - } - } - } - else { - if (!is_dir(dirname($target))) { - mkdir(dirname($target), 0777, true); - } - echo " generate $name\n"; - } - file_put_contents($target, $content); - } - } - - /** - * Builds the file list of a directory. - * This method traverses through the specified directory and builds - * a list of files and subdirectories that the directory contains. - * The result of this function can be passed to {@link copyFiles}. - * @param string $sourceDir the source directory - * @param string $targetDir the target directory - * @param string $baseDir base directory - * @param array $ignoreFiles list of the names of files that should - * be ignored in list building process. - * @param array $renameMap hash array of file names that should be - * renamed. Example value: array('1.old.txt'=>'2.new.txt'). - * @return array the file list (see {@link copyFiles}) - */ - protected function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array()) - { - $list = array(); - $handle = opendir($sourceDir); - while(($file = readdir($handle)) !== false) { - if(in_array($file, array('.', '..', '.svn', '.gitignore', '.hgignore')) || in_array($file, $ignoreFiles)) { - continue; - } - $sourcePath = $sourceDir.DIRECTORY_SEPARATOR.$file; - $targetPath = $targetDir.DIRECTORY_SEPARATOR.strtr($file, $renameMap); - $name = $baseDir === '' ? $file : $baseDir.'/'.$file; - $list[$name] = array( - 'source' => $sourcePath, - 'target' => $targetPath, - ); - if(is_dir($sourcePath)) { - $list = array_merge($list, self::buildFileList($sourcePath, $targetPath, $name, $ignoreFiles, $renameMap)); - } - } - closedir($handle); - return $list; - } -} diff --git a/yii/console/controllers/AssetController.php b/yii/console/controllers/AssetController.php deleted file mode 100644 index aab489b..0000000 --- a/yii/console/controllers/AssetController.php +++ /dev/null @@ -1,353 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\console\controllers; - -use Yii; -use yii\console\Exception; -use yii\console\Controller; - -/** - * @author Qiang Xue <qiang.xue@gmail.com> - * @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, <<<EOD -<?php -/** - * This file is generated by the "yiic script" command. - * DO NOT MODIFY THIS FILE DIRECTLY. - * @version $version - */ -return $array; -EOD - ); - } - - protected function compressJsFiles($inputFiles, $outputFile) - { - if (is_string($this->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 = <<<EOD -<?php - -return array( - // - 'bundles' => 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); - } -} diff --git a/yii/console/controllers/MessageController.php b/yii/console/controllers/MessageController.php deleted file mode 100644 index e010b55..0000000 --- a/yii/console/controllers/MessageController.php +++ /dev/null @@ -1,206 +0,0 @@ -<?php -/** - * @author Qiang Xue <qiang.xue@gmail.com> - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\console\controllers; - -use yii\console\Controller; - -/** - * This command extracts messages to be translated from source files. - * The extracted messages are saved as PHP message source files - * under the specified directory. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class MessageController extends Controller -{ - /** - * Searches for messages to be translated in the specified - * source files and compiles them into PHP arrays as message source. - * - * @param string $config the path of the configuration file. You can find - * an example in framework/messages/config.php. - * - * The file can be placed anywhere and must be a valid PHP script which - * returns an array of name-value pairs. Each name-value pair represents - * a configuration option. - * - * The following options are available: - * - * - sourcePath: string, root directory of all source files. - * - messagePath: string, root directory containing message translations. - * - languages: array, list of language codes that the extracted messages - * should be translated to. For example, array('zh_cn','en_au'). - * - fileTypes: array, a list of file extensions (e.g. 'php', 'xml'). - * Only the files whose extension name can be found in this list - * will be processed. If empty, all files will be processed. - * - exclude: array, a list of directory and file exclusions. Each - * exclusion can be either a name or a path. If a file or directory name - * or path matches the exclusion, it will not be copied. For example, - * an exclusion of '.svn' will exclude all files and directories whose - * name is '.svn'. And an exclusion of '/a/b' will exclude file or - * directory 'sourcePath/a/b'. - * - translator: the name of the function for translating messages. - * Defaults to 'Yii::t'. This is used as a mark to find messages to be - * translated. - * - overwrite: if message file must be overwritten with the merged messages. - * - removeOld: if message no longer needs translation it will be removed, - * instead of being enclosed between a pair of '@@' marks. - * - sort: sort messages by key when merging, regardless of their translation - * state (new, obsolete, translated.) - */ - public function actionIndex($config) - { - if(!is_file($config)) - $this->usageError("the configuration file {$config} does not exist."); - - $config=require_once($config); - - $translator='Yii::t'; - extract($config); - - if(!isset($sourcePath,$messagePath,$languages)) - $this->usageError('The configuration file must specify "sourcePath", "messagePath" and "languages".'); - if(!is_dir($sourcePath)) - $this->usageError("The source path $sourcePath is not a valid directory."); - if(!is_dir($messagePath)) - $this->usageError("The message path $messagePath is not a valid directory."); - if(empty($languages)) - $this->usageError("Languages cannot be empty."); - - if(!isset($overwrite)) - $overwrite = false; - - if(!isset($removeOld)) - $removeOld = false; - - if(!isset($sort)) - $sort = false; - - $options=array(); - if(isset($fileTypes)) - $options['fileTypes']=$fileTypes; - if(isset($exclude)) - $options['exclude']=$exclude; - $files=CFileHelper::findFiles(realpath($sourcePath),$options); - - $messages=array(); - foreach($files as $file) - $messages=array_merge_recursive($messages,$this->extractMessages($file,$translator)); - - foreach($languages as $language) - { - $dir=$messagePath.DIRECTORY_SEPARATOR.$language; - if(!is_dir($dir)) - @mkdir($dir); - foreach($messages as $category=>$msgs) - { - $msgs=array_values(array_unique($msgs)); - $this->generateMessageFile($msgs,$dir.DIRECTORY_SEPARATOR.$category.'.php',$overwrite,$removeOld,$sort); - } - } - } - - protected function extractMessages($fileName,$translator) - { - echo "Extracting messages from $fileName...\n"; - $subject=file_get_contents($fileName); - $n=preg_match_all('/\b'.$translator.'\s*\(\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*,\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*[,\)]/s',$subject,$matches,PREG_SET_ORDER); - $messages=array(); - for($i=0;$i<$n;++$i) - { - if(($pos=strpos($matches[$i][1],'.'))!==false) - $category=substr($matches[$i][1],$pos+1,-1); - else - $category=substr($matches[$i][1],1,-1); - $message=$matches[$i][2]; - $messages[$category][]=eval("return $message;"); // use eval to eliminate quote escape - } - return $messages; - } - - protected function generateMessageFile($messages,$fileName,$overwrite,$removeOld,$sort) - { - echo "Saving messages to $fileName..."; - if(is_file($fileName)) - { - $translated=require($fileName); - sort($messages); - ksort($translated); - if(array_keys($translated)==$messages) - { - echo "nothing new...skipped.\n"; - return; - } - $merged=array(); - $untranslated=array(); - foreach($messages as $message) - { - if(!empty($translated[$message])) - $merged[$message]=$translated[$message]; - else - $untranslated[]=$message; - } - ksort($merged); - sort($untranslated); - $todo=array(); - foreach($untranslated as $message) - $todo[$message]=''; - ksort($translated); - foreach($translated as $message=>$translation) - { - if(!isset($merged[$message]) && !isset($todo[$message]) && !$removeOld) - { - if(substr($translation,0,2)==='@@' && substr($translation,-2)==='@@') - $todo[$message]=$translation; - else - $todo[$message]='@@'.$translation.'@@'; - } - } - $merged=array_merge($todo,$merged); - if($sort) - ksort($merged); - if($overwrite === false) - $fileName.='.merged'; - echo "translation merged.\n"; - } - else - { - $merged=array(); - foreach($messages as $message) - $merged[$message]=''; - ksort($merged); - echo "saved.\n"; - } - $array=str_replace("\r",'',var_export($merged,true)); - $content=<<<EOD -<?php -/** - * Message translations. - * - * This file is automatically generated by 'yiic message' command. - * It contains the localizable messages extracted from source code. - * You may modify this file by translating the extracted messages. - * - * Each array element represents the translation (value) of a message (key). - * If the value is empty, the message is considered as not translated. - * Messages that no longer need translation will have their translations - * enclosed between a pair of '@@' marks. - * - * Message string can be used with plural forms format. Check i18n section - * of the guide for details. - * - * NOTE, this file must be saved in UTF-8 encoding. - */ -return $array; - -EOD; - file_put_contents($fileName, $content); - } -} diff --git a/yii/debug/Toolbar.php b/yii/debug/Toolbar.php deleted file mode 100644 index 84b55c8..0000000 --- a/yii/debug/Toolbar.php +++ /dev/null @@ -1,38 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\debug; - -use Yii; -use yii\base\Widget; -use yii\helpers\Html; - -/** - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class Toolbar extends Widget -{ - public $debugAction = 'debug'; - public $enabled = YII_DEBUG; - - public function run() - { - if ($this->enabled) { - $id = 'yii-debug-toolbar'; - $url = Yii::$app->getUrlManager()->createUrl($this->debugAction, array( - 'tag' => Yii::getLogger()->tag, - )); - $this->view->registerJs("yii.debug.load('$id', '$url');"); - $this->view->registerAssetBundle('yii/debug'); - echo Html::tag('div', '', array( - 'id' => $id, - 'style' => 'display: none', - )); - } - } -} diff --git a/yii/helpers/base/StringHelper.php b/yii/helpers/base/StringHelper.php deleted file mode 100644 index 646bcbb..0000000 --- a/yii/helpers/base/StringHelper.php +++ /dev/null @@ -1,148 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\helpers\base; - -/** - * StringHelper - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @author Alex Makarov <sam@rmcreative.ru> - * @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); - } - - /** - * Returns the trailing name component of a path. - * This method does the same as the php function basename() except that it will - * always use \ and / as directory separators, independent of the operating system. - * Note: basename() operates naively on the input string, and is not aware of the - * actual filesystem, or path components such as "..". - * @param string $path A path string. - * @param string $suffix If the name component ends in suffix this will also be cut off. - * @return string the trailing name component of the given path. - * @see http://www.php.net/manual/en/function.basename.php - */ - public static function basename($path, $suffix = '') - { - if (($len = mb_strlen($suffix)) > 0 && mb_substr($path, -$len) == $suffix) { - $path = mb_substr($path, 0, -$len); - } - $path = rtrim(str_replace('\\', '/', $path), '/\\'); - if (($pos = mb_strrpos($path, '/')) !== false) { - return mb_substr($path, $pos + 1); - } - return $path; - } - - /** - * 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('/(?<![A-Z])[A-Z]/', ' \0', $name)))); - return $ucwords ? ucwords($label) : $label; - } - - /** - * Converts a CamelCase name into an ID in lowercase. - * Words in the ID may be concatenated using the specified character (defaults to '-'). - * For example, 'PostTag' will be converted to 'post-tag'. - * @param string $name the string to be converted - * @param string $separator the character used to concatenate the words in the ID - * @return string the resulting ID - */ - public static function camel2id($name, $separator = '-') - { - if ($separator === '_') { - return trim(strtolower(preg_replace('/(?<![A-Z])[A-Z]/', '_\0', $name)), '_'); - } else { - return trim(strtolower(str_replace('_', $separator, preg_replace('/(?<![A-Z])[A-Z]/', $separator . '\0', $name))), $separator); - } - } - - /** - * Converts an ID into a CamelCase name. - * Words in the ID separated by `$separator` (defaults to '-') will be concatenated into a CamelCase name. - * For example, 'post-tag' is converted to 'PostTag'. - * @param string $id the ID to be converted - * @param string $separator the character used to separate the words in the ID - * @return string the resulting CamelCase name - */ - public static function id2camel($id, $separator = '-') - { - return str_replace(' ', '', ucwords(implode(' ', explode($separator, $id)))); - } -} diff --git a/yii/i18n/data/plurals.php b/yii/i18n/data/plurals.php deleted file mode 100644 index 468f7e2..0000000 --- a/yii/i18n/data/plurals.php +++ /dev/null @@ -1,627 +0,0 @@ -<?php -/** - * Plural rules. - * - * This file is automatically generated by the "yiic locale/plural" command under the "build" folder. - * Do not modify it directly. - * - * The original plural rule data used for generating this file has the following copyright terms: - * - * Copyright © 1991-2007 Unicode, Inc. All rights reserved. - * Distributed under the Terms of Use in http://www.unicode.org/copyright.html. - * - * @revision 6008 (of the original plural file) - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ -return array ( - 'ar' => - array ( - 0 => '$n==0', - 1 => '$n==1', - 2 => '$n==2', - 3 => 'in_array(fmod($n,100),range(3,10))', - 4 => 'in_array(fmod($n,100),range(11,99))', - ), - '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 => 'in_array($n,array(3,4,5,6))', - 3 => 'in_array($n,array(7,8,9,10))', - ), - 'ro' => - array ( - 0 => '$n==1', - 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', - ), - 'mo' => - array ( - 0 => '$n==1', - 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', - ), - 'lt' => - array ( - 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 => '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),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),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),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),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),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),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),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', - ), - 'cs' => - array ( - 0 => '$n==1', - 1 => 'in_array($n,array(2,3,4))', - ), - 'sk' => - array ( - 0 => '$n==1', - 1 => 'in_array($n,array(2,3,4))', - ), - 'pl' => - 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),array(5,6,7,8,9))||in_array(fmod($n,100),array(12,13,14))', - ), - 'sl' => - array ( - 0 => 'fmod($n,100)==1', - 1 => 'fmod($n,100)==2', - 2 => 'in_array(fmod($n,100),array(3,4))', - ), - 'mt' => - array ( - 0 => '$n==1', - 1 => '$n==0||in_array(fmod($n,100),range(2,10))', - 2 => 'in_array(fmod($n,100),range(11,19))', - ), - '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 => '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)))', - 3 => 'fmod($n,1000000)==0&&$n!=0', - ), - 'ksh' => - array ( - 0 => '$n==0', - 1 => '$n==1', - ), - 'tzm' => - array ( - 0 => '($n==0||$n==1)||in_array($n,range(11,99))', - ), - 'gv' => - array ( - 0 => 'in_array(fmod($n,10),array(1,2))||fmod($n,20)==0', - ), -); diff --git a/yii/i18n/data/plurals.xml b/yii/i18n/data/plurals.xml deleted file mode 100644 index 9227dc6..0000000 --- a/yii/i18n/data/plurals.xml +++ /dev/null @@ -1,109 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" ?> -<!DOCTYPE supplementalData SYSTEM "../../common/dtd/ldmlSupplemental.dtd"> -<supplementalData> - <version number="$Revision: 6008 $"/> - <generation date="$Date: 2011-07-12 13:18:01 -0500 (Tue, 12 Jul 2011) $"/> - <plurals> - <!-- if locale is known to have no plurals, there are no rules --> - <pluralRules locales="az bm bo dz fa id ig ii hu ja jv ka kde kea km kn ko lo ms my sah ses sg th to tr vi wo yo zh"/> - <pluralRules locales="ar"> - <pluralRule count="zero">n is 0</pluralRule> - <pluralRule count="one">n is 1</pluralRule> - <pluralRule count="two">n is 2</pluralRule> - <pluralRule count="few">n mod 100 in 3..10</pluralRule> - <pluralRule count="many">n mod 100 in 11..99</pluralRule> - </pluralRules> - <pluralRules locales="asa af bem bez bg bn brx ca cgg chr da de dv ee el en eo es et eu fi fo fur fy gl gsw gu ha haw he is it jmc kaj kcg kk kl ksb ku lb lg mas ml mn mr nah nb nd ne nl nn no nr ny nyn om or pa pap ps pt rof rm rwk saq seh sn so sq ss ssy st sv sw syr ta te teo tig tk tn ts ur wae ve vun xh xog zu"> - <pluralRule count="one">n is 1</pluralRule> - </pluralRules> - <pluralRules locales="ak am bh fil tl guw hi ln mg nso ti wa"> - <pluralRule count="one">n in 0..1</pluralRule> - </pluralRules> - <pluralRules locales="ff fr kab"> - <pluralRule count="one">n within 0..2 and n is not 2</pluralRule> - </pluralRules> - <pluralRules locales="lv"> - <pluralRule count="zero">n is 0</pluralRule> - <pluralRule count="one">n mod 10 is 1 and n mod 100 is not 11</pluralRule> - </pluralRules> - <pluralRules locales="iu kw naq se sma smi smj smn sms"> - <pluralRule count="one">n is 1</pluralRule> - <pluralRule count="two">n is 2</pluralRule> - </pluralRules> - <pluralRules locales="ga"> <!-- http://unicode.org/cldr/trac/ticket/3915 --> - <pluralRule count="one">n is 1</pluralRule> - <pluralRule count="two">n is 2</pluralRule> - <pluralRule count="few">n in 3..6</pluralRule> - <pluralRule count="many">n in 7..10</pluralRule> - </pluralRules> - <pluralRules locales="ro mo"> - <pluralRule count="one">n is 1</pluralRule> - <pluralRule count="few">n is 0 OR n is not 1 AND n mod 100 in 1..19</pluralRule> - </pluralRules> - <pluralRules locales="lt"> - <pluralRule count="one">n mod 10 is 1 and n mod 100 not in 11..19</pluralRule> - <pluralRule count="few">n mod 10 in 2..9 and n mod 100 not in 11..19</pluralRule> - </pluralRules> - <pluralRules locales="be bs hr ru sh sr uk"> - <pluralRule count="one">n mod 10 is 1 and n mod 100 is not 11</pluralRule> - <pluralRule count="few">n mod 10 in 2..4 and n mod 100 not in 12..14</pluralRule> - <pluralRule count="many">n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14</pluralRule> - <!-- others are fractions --> - </pluralRules> - <pluralRules locales="cs sk"> - <pluralRule count="one">n is 1</pluralRule> - <pluralRule count="few">n in 2..4</pluralRule> - </pluralRules> - <pluralRules locales="pl"> - <pluralRule count="one">n is 1</pluralRule> - <pluralRule count="few">n mod 10 in 2..4 and n mod 100 not in 12..14</pluralRule> - <pluralRule count="many">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</pluralRule> - <!-- others are fractions --> - <!-- and n mod 100 not in 22..24 from Tamplin --> - </pluralRules> - <pluralRules locales="sl"> - <pluralRule count="one">n mod 100 is 1</pluralRule> - <pluralRule count="two">n mod 100 is 2</pluralRule> - <pluralRule count="few">n mod 100 in 3..4</pluralRule> - </pluralRules> - <pluralRules locales="mt"> <!-- from Tamplin's data --> - <pluralRule count="one">n is 1</pluralRule> - <pluralRule count="few">n is 0 or n mod 100 in 2..10</pluralRule> - <pluralRule count="many">n mod 100 in 11..19</pluralRule> - </pluralRules> - <pluralRules locales="mk"> <!-- from Tamplin's data --> - <pluralRule count="one">n mod 10 is 1 and n is not 11</pluralRule> - </pluralRules> - <pluralRules locales="cy"> <!-- from http://www.saltcymru.org/wordpress/?p=99&lang=en --> - <pluralRule count="zero">n is 0</pluralRule> - <pluralRule count="one">n is 1</pluralRule> - <pluralRule count="two">n is 2</pluralRule> - <pluralRule count="few">n is 3</pluralRule> - <pluralRule count="many">n is 6</pluralRule> - </pluralRules> - <pluralRules locales="lag"> - <pluralRule count="zero">n is 0</pluralRule> - <pluralRule count="one">n within 0..2 and n is not 0 and n is not 2</pluralRule> - </pluralRules> - <pluralRules locales="shi"> - <pluralRule count="one">n within 0..1</pluralRule> - <pluralRule count="few">n in 2..10</pluralRule> - </pluralRules> - <pluralRules locales="br"> <!-- from http://unicode.org/cldr/trac/ticket/2886 --> - <pluralRule count="one">n mod 10 is 1 and n mod 100 not in 11,71,91</pluralRule> - <pluralRule count="two">n mod 10 is 2 and n mod 100 not in 12,72,92</pluralRule> - <pluralRule count="few">n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99</pluralRule> - <pluralRule count="many">n mod 1000000 is 0 and n is not 0</pluralRule> - </pluralRules> - <pluralRules locales="ksh"> - <pluralRule count="zero">n is 0</pluralRule> - <pluralRule count="one">n is 1</pluralRule> - </pluralRules> - <pluralRules locales="tzm"> - <pluralRule count="one">n in 0..1 or n in 11..99</pluralRule> - </pluralRules> - <pluralRules locales="gv"> - <pluralRule count="one">n mod 10 in 1..2 or n mod 20 is 0</pluralRule> - </pluralRules> - </plurals> -</supplementalData> diff --git a/yii/logging/ProfileTarget.php b/yii/logging/ProfileTarget.php deleted file mode 100644 index 335e172..0000000 --- a/yii/logging/ProfileTarget.php +++ /dev/null @@ -1,192 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -/** - * CProfileLogRoute displays the profiling results in Web page. - * - * The profiling is done by calling {@link YiiBase::beginProfile()} and {@link YiiBase::endProfile()}, - * which marks the begin and end of a code block. - * - * CProfileLogRoute supports two types of report by setting the {@link setReport report} property: - * <ul> - * <li>summary: list the execution time of every marked code block</li> - * <li>callstack: list the mark code blocks in a hierarchical view reflecting their calling sequence.</li> - * </ul> - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class CProfileLogRoute extends CWebLogRoute -{ - /** - * @var boolean whether to aggregate results according to profiling tokens. - * If false, the results will be aggregated by categories. - * Defaults to true. Note that this property only affects the summary report - * that is enabled when {@link report} is 'summary'. - */ - public $groupByToken = true; - /** - * @var string type of profiling report to display - */ - private $_report = 'summary'; - - /** - * Initializes the route. - * This method is invoked after the route is created by the route manager. - */ - public function init() - { - $this->levels = CLogger::LEVEL_PROFILE; - } - - /** - * @return string the type of the profiling report to display. Defaults to 'summary'. - */ - public function getReport() - { - return $this->_report; - } - - /** - * @param string $value the type of the profiling report to display. Valid values include 'summary' and 'callstack'. - */ - public function setReport($value) - { - if ($value === 'summary' || $value === 'callstack') - $this->_report = $value; - else - throw new CException(Yii::t('yii|CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".', - array('{report}' => $value))); - } - - /** - * Displays the log messages. - * @param array $logs list of log messages - */ - public function processLogs($logs) - { - $app = \Yii::$app; - if (!($app instanceof CWebApplication) || $app->getRequest()->getIsAjaxRequest()) - return; - - if ($this->getReport() === 'summary') - $this->displaySummary($logs); - else - $this->displayCallstack($logs); - } - - /** - * Displays the callstack of the profiling procedures for display. - * @param array $logs list of logs - */ - protected function displayCallstack($logs) - { - $stack = array(); - $results = array(); - $n = 0; - foreach ($logs as $log) - { - if ($log[1] !== CLogger::LEVEL_PROFILE) { - continue; - } - $message = $log[0]; - if (!strncasecmp($message, 'begin:', 6)) { - $log[0] = substr($message, 6); - $log[4] = $n; - $stack[] = $log; - $n++; - } elseif (!strncasecmp($message, 'end:', 4)) { - $token = substr($message, 4); - if (($last = array_pop($stack)) !== null && $last[0] === $token) { - $delta = $log[3] - $last[3]; - $results[$last[4]] = array($token, $delta, count($stack)); - } else - { - throw new CException(Yii::t('yii|CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', - array('{token}' => $token))); - } - } - } - // remaining entries should be closed here - $now = microtime(true); - while (($last = array_pop($stack)) !== null) { - $results[$last[4]] = array($last[0], $now - $last[3], count($stack)); - } - ksort($results); - $this->render('profile-callstack', $results); - } - - /** - * Displays the summary report of the profiling result. - * @param array $logs list of logs - */ - protected function displaySummary($logs) - { - $stack = array(); - foreach ($logs as $log) - { - if ($log[1] !== CLogger::LEVEL_PROFILE) - continue; - $message = $log[0]; - if (!strncasecmp($message, 'begin:', 6)) - { - $log[0] = substr($message, 6); - $stack[] = $log; - } elseif (!strncasecmp($message, 'end:', 4)) - { - $token = substr($message, 4); - if (($last = array_pop($stack)) !== null && $last[0] === $token) - { - $delta = $log[3] - $last[3]; - if (!$this->groupByToken) - $token = $log[2]; - if (isset($results[$token])) - $results[$token] = $this->aggregateResult($results[$token], $delta); - else - $results[$token] = array($token, 1, $delta, $delta, $delta); - } else - throw new CException(Yii::t('yii|CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', - array('{token}' => $token))); - } - } - - $now = microtime(true); - while (($last = array_pop($stack)) !== null) - { - $delta = $now - $last[3]; - $token = $this->groupByToken ? $last[0] : $last[2]; - if (isset($results[$token])) - $results[$token] = $this->aggregateResult($results[$token], $delta); - else - $results[$token] = array($token, 1, $delta, $delta, $delta); - } - - $entries = array_values($results); - $func = create_function('$a,$b', 'return $a[4]<$b[4]?1:0;'); - usort($entries, $func); - - $this->render('profile-summary', $entries); - } - - /** - * Aggregates the report result. - * @param array $result log result for this code block - * @param float $delta time spent for this code block - * @return array - */ - protected function aggregateResult($result, $delta) - { - list($token, $calls, $min, $max, $total) = $result; - if ($delta < $min) - $min = $delta; - elseif ($delta > $max) - $max = $delta; - $calls++; - $total += $delta; - return array($token, $calls, $min, $max, $total); - } -} diff --git a/yii/logging/Router.php b/yii/logging/Router.php deleted file mode 100644 index f544b72..0000000 --- a/yii/logging/Router.php +++ /dev/null @@ -1,99 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\logging; - -use Yii; -use yii\base\Component; -use yii\base\Application; - -/** - * Router manages [[Target|log targets]] that record log messages in different media. - * - * For example, a [[FileTarget|file log target]] records log messages - * in files; an [[EmailTarget|email log target]] sends log messages - * to specific email addresses. Each log target may specify filters on - * message levels and categories to record specific messages only. - * - * Router and the targets it manages may be configured in application configuration, - * like the following: - * - * ~~~ - * array( - * // preload log component when application starts - * 'preload' => array('log'), - * 'components' => array( - * 'log' => array( - * 'class' => 'yii\logging\Router', - * 'targets' => array( - * 'file' => array( - * 'class' => 'yii\logging\FileTarget', - * 'levels' => array('trace', 'info'), - * 'categories' => array('yii\*'), - * ), - * 'email' => array( - * 'class' => 'yii\logging\EmailTarget', - * 'levels' => array('error', 'warning'), - * 'emails' => array('admin@example.com'), - * ), - * ), - * ), - * ), - * ) - * ~~~ - * - * Each log target can have a name and can be referenced via the [[targets]] property - * as follows: - * - * ~~~ - * Yii::$app->log->targets['file']->enabled = false; - * ~~~ - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class Router extends Component -{ - /** - * @var Target[] list of log target objects or configurations. If the latter, target objects will - * be created in [[init()]] by calling [[Yii::createObject()]] with the corresponding object configuration. - */ - public $targets = array(); - - /** - * Initializes this application component. - * This method is invoked when the Router component is created by the application. - * The method attaches the [[processLogs]] method to both the [[Logger::EVENT_FLUSH]] event - * and the [[Logger::EVENT_FINAL_FLUSH]] event. - */ - public function init() - { - parent::init(); - foreach ($this->targets as $name => $target) { - if (!$target instanceof Target) { - $this->targets[$name] = Yii::createObject($target); - } - } - Yii::getLogger()->router = $this; - } - - /** - * Dispatches log messages to [[targets]]. - * This method is called by [[Logger]] when its [[Logger::flush()]] method is called. - * It will forward the messages to each log target registered in [[targets]]. - * @param array $messages the messages to be processed - * @param boolean $final whether this is the final call during a request cycle - */ - public function dispatch($messages, $final = false) - { - foreach ($this->targets as $target) { - if ($target->enabled) { - $target->collect($messages, $final); - } - } - } -} diff --git a/yii/logging/WebTarget.php b/yii/logging/WebTarget.php deleted file mode 100644 index c98fd9f..0000000 --- a/yii/logging/WebTarget.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -/** - * CWebLogRoute shows the log content in Web page. - * - * The log content can appear either at the end of the current Web page - * or in FireBug console window (if {@link showInFireBug} is set true). - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class CWebLogRoute extends CLogRoute -{ - /** - * @var boolean whether the log should be displayed in FireBug instead of browser window. Defaults to false. - */ - public $showInFireBug = false; - - /** - * @var boolean whether the log should be ignored in FireBug for ajax calls. Defaults to true. - * This option should be used carefully, because an ajax call returns all output as a result data. - * For example if the ajax call expects a json type result any output from the logger will cause ajax call to fail. - */ - public $ignoreAjaxInFireBug = true; - - /** - * Displays the log messages. - * @param array $logs list of log messages - */ - public function processLogs($logs) - { - $this->render('log', $logs); - } - - /** - * Renders the view. - * @param string $view the view name (file name without extension). The file is assumed to be located under framework/data/views. - * @param array $data data to be passed to the view - */ - protected function render($view, $data) - { - $app = \Yii::$app; - $isAjax = $app->getRequest()->getIsAjaxRequest(); - - if ($this->showInFireBug) - { - if ($isAjax && $this->ignoreAjaxInFireBug) - return; - $view .= '-firebug'; - } elseif (!($app instanceof CWebApplication) || $isAjax) - return; - - $viewFile = YII_PATH . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $view . '.php'; - include($app->findLocalizedFile($viewFile, 'en')); - } -} diff --git a/yii/rbac/Assignment.php b/yii/rbac/Assignment.php deleted file mode 100644 index 5b6a607..0000000 --- a/yii/rbac/Assignment.php +++ /dev/null @@ -1,106 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\rbac; - -use Yii; -use yii\base\Object; - -/** - * Assignment represents an assignment of a role to a user. - * It includes additional assignment information such as [[bizRule]] and [[data]]. - * Do not create a Assignment instance using the 'new' operator. - * Instead, call [[Manager::assign()]]. - * - * @property mixed $userId User ID (see [[User::id]]). - * @property string $itemName The authorization item name. - * @property string $bizRule The business rule associated with this assignment. - * @property mixed $data Additional data for this assignment. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @author Alexander Kochetov <creocoder@gmail.com> - * @since 2.0 - */ -class Assignment extends Object -{ - private $_auth; - private $_userId; - private $_itemName; - private $_bizRule; - private $_data; - - /** - * Constructor. - * @param Manager $auth the authorization manager - * @param mixed $userId user ID (see [[User::id]]) - * @param string $itemName authorization item name - * @param string $bizRule the business rule associated with this assignment - * @param mixed $data additional data for this assignment - */ - public function __construct($auth, $userId, $itemName, $bizRule = null, $data = null) - { - $this->_auth = $auth; - $this->_userId = $userId; - $this->_itemName = $itemName; - $this->_bizRule = $bizRule; - $this->_data = $data; - } - - /** - * @return mixed user ID (see [[User::id]]) - */ - public function getUserId() - { - return $this->_userId; - } - - /** - * @return string the authorization item name - */ - public function getItemName() - { - return $this->_itemName; - } - - /** - * @return string the business rule associated with this assignment - */ - public function getBizRule() - { - return $this->_bizRule; - } - - /** - * @param string $value the business rule associated with this assignment - */ - public function setBizRule($value) - { - if ($this->_bizRule !== $value) { - $this->_bizRule = $value; - $this->_auth->saveAssignment($this); - } - } - - /** - * @return mixed additional data for this assignment - */ - public function getData() - { - return $this->_data; - } - - /** - * @param mixed $value additional data for this assignment - */ - public function setData($value) - { - if ($this->_data !== $value) { - $this->_data = $value; - $this->_auth->saveAssignment($this); - } - } -} diff --git a/yii/views/exception.php b/yii/views/exception.php deleted file mode 100644 index f2aced0..0000000 --- a/yii/views/exception.php +++ /dev/null @@ -1,211 +0,0 @@ -<?php -/** - * @var \Exception $exception - * @var \yii\base\ErrorHandler $context - */ -$context = $this->context; -$title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $exception->getName().' ('.get_class($exception).')' : get_class($exception)); -?> -<!DOCTYPE html> -<html> -<head> - <meta charset="utf-8" /> - <title><?php echo $title?></title> - <style> - html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent;margin:0;padding:0;} - body{line-height:1;} - ol,ul{list-style:none;} - blockquote,q{quotes:none;} - blockquote:before,blockquote:after,q:before,q:after{content:none;} - :focus{outline:0;} - ins{text-decoration:none;} - del{text-decoration:line-through;} - table{border-collapse:collapse;border-spacing:0;} - - body { - font: normal 9pt "Verdana"; - color: #000; - background: #fff; - } - - h1 { - font: normal 18pt "Verdana"; - color: #f00; - margin-bottom: .5em; - } - - h2 { - font: normal 14pt "Verdana"; - color: #800000; - margin-bottom: .5em; - } - - h3 { - font: bold 11pt "Verdana"; - } - - pre { - font: normal 11pt Menlo, Consolas, "Lucida Console", Monospace; - } - - pre span.error { - display: block; - background: #fce3e3; - } - - pre span.ln { - color: #999; - padding-right: 0.5em; - border-right: 1px solid #ccc; - } - - pre span.error-ln { - font-weight: bold; - } - - .container { - margin: 1em 4em; - } - - .version { - color: gray; - font-size: 8pt; - border-top: 1px solid #aaa; - padding-top: 1em; - margin-bottom: 1em; - } - - .message { - color: #000; - padding: 1em; - font-size: 11pt; - background: #f3f3f3; - -webkit-border-radius: 10px; - -moz-border-radius: 10px; - border-radius: 10px; - margin-bottom: 1em; - line-height: 160%; - } - - .source { - margin-bottom: 1em; - } - - .code pre { - background-color: #ffe; - margin: 0.5em 0; - padding: 0.5em; - line-height: 125%; - border: 1px solid #eee; - } - - .source .file { - margin-bottom: 1em; - font-weight: bold; - } - - .traces { - margin: 2em 0; - } - - .trace { - margin: 0.5em 0; - padding: 0.5em; - } - - .trace.app { - border: 1px dashed #c00; - } - - .trace .number { - text-align: right; - width: 2em; - padding: 0.5em; - } - - .trace .content { - padding: 0.5em; - } - - .trace .plus, - .trace .minus { - display: inline; - vertical-align: middle; - text-align: center; - border: 1px solid #000; - color: #000; - font-size: 10px; - line-height: 10px; - margin: 0; - padding: 0 1px; - width: 10px; - height: 10px; - } - - .trace.collapsed .minus, - .trace.expanded .plus, - .trace.collapsed pre { - display: none; - } - - .trace-file { - cursor: pointer; - padding: 0.2em; - } - - .trace-file:hover { - background: #f0ffff; - } - </style> -</head> - -<body> -<div class="container"> - <h1><?php echo $title?></h1> - - <p class="message"> - <?php echo nl2br($context->htmlEncode($exception->getMessage()))?> - </p> - - <div class="source"> - <p class="file"> - <?php echo $context->htmlEncode($exception->getFile()) . '(' . $exception->getLine() . ')'?> - </p> - <?php if (YII_DEBUG) $context->renderSourceCode($exception->getFile(), $exception->getLine(), $context->maxSourceLines)?> - </div> - - <?php if (YII_DEBUG):?> - <div class="traces"> - <h2>Stack Trace</h2> - <?php $context->renderTrace($exception->getTrace())?> - </div> - <?php endif?> - - <div class="version"> - <?php echo date('Y-m-d H:i:s', time())?> - <?php echo YII_DEBUG ? $context->getVersionInfo() : ''?> - </div> -</div> - -<script> -var traceReg = new RegExp("(^|\\s)trace-file(\\s|$)"); -var collapsedReg = new RegExp("(^|\\s)collapsed(\\s|$)"); - -var e = document.getElementsByTagName("div"); -for(var j=0,len=e.length;j<len;j++){ - if(traceReg.test(e[j].className)){ - e[j].onclick = function(){ - var trace = this.parentNode.parentNode; - if(collapsedReg.test(trace.className)){ - trace.className = trace.className.replace("collapsed", "expanded"); - } - else{ - trace.className = trace.className.replace("expanded", "collapsed"); - } - } - } -} -</script> - -</body> -</html> diff --git a/yii/web/CacheSession.php b/yii/web/CacheSession.php deleted file mode 100644 index c125f01..0000000 --- a/yii/web/CacheSession.php +++ /dev/null @@ -1,106 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\web; - -use Yii; -use yii\caching\Cache; -use yii\base\InvalidConfigException; - -/** - * CacheSession implements a session component using cache as storage medium. - * - * The cache being used can be any cache application component. - * The ID of the cache application component is specified via [[cache]], which defaults to 'cache'. - * - * Beware, by definition cache storage are volatile, which means the data stored on them - * may be swapped out and get lost. Therefore, you must make sure the cache used by this component - * is NOT volatile. If you want to use database as storage medium, use [[DbSession]] is a better choice. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class CacheSession extends Session -{ - /** - * @var Cache|string the cache object or the application component ID of the cache object. - * The session data will be stored using this cache object. - * - * After the CacheSession object is created, if you want to change this property, - * you should only assign it with a cache object. - */ - public $cache = 'cache'; - - /** - * Initializes the application component. - */ - public function init() - { - parent::init(); - if (is_string($this->cache)) { - $this->cache = Yii::$app->getComponent($this->cache); - } - if (!$this->cache instanceof Cache) { - throw new InvalidConfigException('CacheSession::cache must refer to the application component ID of a cache object.'); - } - } - - /** - * Returns a value indicating whether to use custom session storage. - * This method overrides the parent implementation and always returns true. - * @return boolean whether to use custom storage. - */ - public function getUseCustomStorage() - { - return true; - } - - /** - * Session read handler. - * Do not call this method directly. - * @param string $id session ID - * @return string the session data - */ - public function readSession($id) - { - $data = $this->cache->get($this->calculateKey($id)); - return $data === false ? '' : $data; - } - - /** - * Session write handler. - * Do not call this method directly. - * @param string $id session ID - * @param string $data session data - * @return boolean whether session write is successful - */ - public function writeSession($id, $data) - { - return $this->cache->set($this->calculateKey($id), $data, $this->getTimeout()); - } - - /** - * Session destroy handler. - * Do not call this method directly. - * @param string $id session ID - * @return boolean whether session is destroyed successfully - */ - public function destroySession($id) - { - return $this->cache->delete($this->calculateKey($id)); - } - - /** - * Generates a unique key used for storing session data in cache. - * @param string $id session variable name - * @return string a safe cache key associated with the session variable name - */ - protected function calculateKey($id) - { - return $this->cache->buildKey(array(__CLASS__, $id)); - } -} diff --git a/yii/web/Controller.php b/yii/web/Controller.php deleted file mode 100644 index 517f4b4..0000000 --- a/yii/web/Controller.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\web; - -use Yii; - -/** - * Controller is the base class of Web controllers. - * - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class Controller extends \yii\base\Controller -{ - /** - * Creates a URL using the given route and parameters. - * - * This method enhances [[UrlManager::createUrl()]] by supporting relative routes. - * A relative route is a route without a slash, such as "view". If the route is an empty - * string, [[route]] will be used; Otherwise, [[uniqueId]] will be prepended to a relative route. - * - * After this route conversion, the method This method calls [[UrlManager::createUrl()]] - * to create a URL. - * - * @param string $route the route. This can be either an absolute route or a relative route. - * @param array $params the parameters (name-value pairs) to be included in the generated URL - * @return string the created URL - */ - public function createUrl($route, $params = array()) - { - if (strpos($route, '/') === false) { - // a relative route - $route = $route === '' ? $this->getRoute() : $this->getUniqueId() . '/' . $route; - } - return Yii::$app->getUrlManager()->createUrl($route, $params); - } -} diff --git a/yii/web/HttpCache.php b/yii/web/HttpCache.php deleted file mode 100644 index 0a3bb86..0000000 --- a/yii/web/HttpCache.php +++ /dev/null @@ -1,131 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\web; - -use Yii; -use yii\base\ActionFilter; -use yii\base\Action; - -/** - * @author Da:Sourcerer <webmaster@dasourcerer.net> - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class HttpCache extends ActionFilter -{ - /** - * @var callback a PHP callback that returns the UNIX timestamp of the last modification time. - * The callback's signature should be: - * - * ~~~ - * function ($action, $params) - * ~~~ - * - * where `$action` is the [[Action]] object that this filter is currently handling; - * `$params` takes the value of [[params]]. The callback should return a UNIX timestamp. - */ - public $lastModified; - /** - * @var callback a PHP callback that generates the Etag seed string. - * The callback's signature should be: - * - * ~~~ - * function ($action, $params) - * ~~~ - * - * where `$action` is the [[Action]] object that this filter is currently handling; - * `$params` takes the value of [[params]]. The callback should return a string serving - * as the seed for generating an Etag. - */ - public $etagSeed; - /** - * @var mixed additional parameters that should be passed to the [[lastModified]] and [[etagSeed]] callbacks. - */ - public $params; - /** - * @var string HTTP cache control header. If null, the header will not be sent. - */ - public $cacheControlHeader = 'Cache-Control: max-age=3600, public'; - - /** - * This method is invoked right before an action is to be executed (after all possible filters.) - * You may override this method to do last-minute preparation for the action. - * @param Action $action the action to be executed. - * @return boolean whether the action should continue to be executed. - */ - public function beforeAction($action) - { - $verb = Yii::$app->request->getRequestMethod(); - if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { - return true; - } - - $lastModified = $etag = null; - if ($this->lastModified !== null) { - $lastModified = call_user_func($this->lastModified, $action, $this->params); - } - if ($this->etagSeed !== null) { - $seed = call_user_func($this->etagSeed, $action, $this->params); - $etag = $this->generateEtag($seed); - } - - $this->sendCacheControlHeader(); - if ($etag !== null) { - header("ETag: $etag"); - } - - if ($this->validateCache($lastModified, $etag)) { - header('HTTP/1.1 304 Not Modified'); - return false; - } - - if ($lastModified !== null) { - header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); - } - return true; - } - - /** - * Validates if the HTTP cache contains valid content. - * @param integer $lastModified the calculated Last-Modified value in terms of a UNIX timestamp. - * If null, the Last-Modified header will not be validated. - * @param string $etag the calculated ETag value. If null, the ETag header will not be validated. - * @return boolean whether the HTTP cache is still valid. - */ - protected function validateCache($lastModified, $etag) - { - if ($lastModified !== null && (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) < $lastModified)) { - return false; - } else { - return $etag === null || isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag; - } - } - - /** - * Sends the cache control header to the client - * @see cacheControl - */ - protected function sendCacheControlHeader() - { - session_cache_limiter('public'); - header('Pragma:', true); - if ($this->cacheControlHeader !== null) { - header($this->cacheControlHeader, true); - } - } - - /** - * Generates an Etag from the given seed string. - * @param string $seed Seed for the ETag - * @return string the generated Etag - */ - protected function generateEtag($seed) - { - return '"' . base64_encode(sha1($seed, true)) . '"'; - } -} diff --git a/yii/web/Identity.php b/yii/web/Identity.php deleted file mode 100644 index 101ecdb..0000000 --- a/yii/web/Identity.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -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 <qiang.xue@gmail.com> - * @since 2.0 - */ -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. - */ - public function getId(); - /** - * Returns a key that can be used to check the validity of a given identity ID. - * - * 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. - * @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); -} diff --git a/yii/web/Response.php b/yii/web/Response.php deleted file mode 100644 index 0503755..0000000 --- a/yii/web/Response.php +++ /dev/null @@ -1,235 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\web; - -use Yii; -use yii\helpers\FileHelper; -use yii\helpers\Html; - -/** - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -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. - * @param string $mimeType mime type of the content. If null, it will be guessed automatically based on the given file name. - * @param boolean $terminate whether to terminate the current application after calling this method - * @todo - */ - public function sendFile($fileName, $content, $mimeType = null, $terminate = true) - { - if ($mimeType === null && ($mimeType = FileHelper::getMimeType($fileName)) === null) { - $mimeType = 'application/octet-stream'; - } - header('Pragma: public'); - header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header("Content-type: $mimeType"); - if (ob_get_length() === false) { - header('Content-Length: ' . (function_exists('mb_strlen') ? mb_strlen($content, '8bit') : strlen($content))); - } - header("Content-Disposition: attachment; filename=\"$fileName\""); - header('Content-Transfer-Encoding: binary'); - - if ($terminate) { - // clean up the application first because the file downloading could take long time - // which may cause timeout of some resources (such as DB connection) - Yii::app()->end(0, false); - echo $content; - exit(0); - } else { - echo $content; - } - } - - /** - * Sends existing file to a browser as a download using x-sendfile. - * - * X-Sendfile is a feature allowing a web application to redirect the request for a file to the webserver - * that in turn processes the request, this way eliminating the need to perform tasks like reading the file - * and sending it to the user. When dealing with a lot of files (or very big files) this can lead to a great - * increase in performance as the web application is allowed to terminate earlier while the webserver is - * handling the request. - * - * The request is sent to the server through a special non-standard HTTP-header. - * When the web server encounters the presence of such header it will discard all output and send the file - * specified by that header using web server internals including all optimizations like caching-headers. - * - * As this header directive is non-standard different directives exists for different web servers applications: - * <ul> - * <li>Apache: {@link http://tn123.org/mod_xsendfile X-Sendfile}</li> - * <li>Lighttpd v1.4: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-LIGHTTPD-send-file}</li> - * <li>Lighttpd v1.5: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-Sendfile}</li> - * <li>Nginx: {@link http://wiki.nginx.org/XSendfile X-Accel-Redirect}</li> - * <li>Cherokee: {@link http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile X-Sendfile and X-Accel-Redirect}</li> - * </ul> - * So for this method to work the X-SENDFILE option/module should be enabled by the web server and - * a proper xHeader should be sent. - * - * <b>Note:</b> - * This option allows to download files that are not under web folders, and even files that are otherwise protected (deny from all) like .htaccess - * - * <b>Side effects</b>: - * If this option is disabled by the web server, when this method is called a download configuration dialog - * will open but the downloaded file will have 0 bytes. - * - * <b>Known issues</b>: - * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show - * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site is either unavailable or cannot be found.". - * You can work around this problem by removing the <code>Pragma</code>-header. - * - * <b>Example</b>: - * <pre> - * <?php - * Yii::app()->request->xSendFile('/home/user/Pictures/picture1.jpg',array( - * 'saveName'=>'image1.jpg', - * 'mimeType'=>'image/jpeg', - * 'terminate'=>false, - * )); - * ?> - * </pre> - * @param string $filePath file name with full path - * @param array $options additional options: - * <ul> - * <li>saveName: file name shown to the user, if not set real file name will be used</li> - * <li>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.</li> - * <li>xHeader: appropriate x-sendfile header, defaults to "X-Sendfile"</li> - * <li>terminate: whether to terminate the current application after calling this method, defaults to true</li> - * <li>forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true</li> - * <li>addHeaders: an array of additional http headers in header-value pairs</li> - * </ul> - * @todo - */ - public function xSendFile($filePath, $options = array()) - { - if (!isset($options['forceDownload']) || $options['forceDownload']) { - $disposition = 'attachment'; - } else { - $disposition = 'inline'; - } - - 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['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); - } - } - header(trim($options['xHeader']) . ': ' . $filePath); - - if (!isset($options['terminate']) || $options['terminate']) { - Yii::$app->end(); - } - } - - /** - * Redirects the browser to the specified 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 [[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(); - } - } - - /** - * Refreshes the current page. - * The effect of this method call is the same as the user pressing the refresh button of his browser - * (without re-posting data). - * @param boolean $terminate whether to terminate the current application after calling this method - * @param string $anchor the anchor that should be appended to the redirection URL. - * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it. - */ - public function refresh($terminate = true, $anchor = '') - { - $this->redirect(Yii::$app->getRequest()->getUrl() . $anchor, $terminate); - } - - /** - * Returns the cookie collection. - * Through the returned cookie collection, you add or remove cookies as follows, - * - * ~~~ - * // add a cookie - * $response->cookies->add(new Cookie(array( - * 'name' => $name, - * 'value' => $value, - * )); - * - * // remove a cookie - * $response->cookies->remove('name'); - * // alternatively - * unset($response->cookies['name']); - * ~~~ - * - * @return CookieCollection the cookie collection. - */ - public function getCookies() - { - return Yii::$app->getRequest()->getCookies(); - } -} diff --git a/yii/web/SessionIterator.php b/yii/web/SessionIterator.php deleted file mode 100644 index c960dd4..0000000 --- a/yii/web/SessionIterator.php +++ /dev/null @@ -1,84 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\web; - -/** - * SessionIterator implements an iterator for traversing session variables managed by [[Session]]. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class SessionIterator implements \Iterator -{ - /** - * @var array list of keys in the map - */ - private $_keys; - /** - * @var mixed current key - */ - private $_key; - - /** - * Constructor. - */ - public function __construct() - { - $this->_keys = array_keys($_SESSION); - } - - /** - * Rewinds internal array pointer. - * This method is required by the interface Iterator. - */ - public function rewind() - { - $this->_key = reset($this->_keys); - } - - /** - * Returns the key of the current array element. - * This method is required by the interface Iterator. - * @return mixed the key of the current array element - */ - public function key() - { - return $this->_key; - } - - /** - * Returns the current array element. - * This method is required by the interface Iterator. - * @return mixed the current array element - */ - public function current() - { - return isset($_SESSION[$this->_key]) ? $_SESSION[$this->_key] : null; - } - - /** - * Moves the internal pointer to the next array element. - * This method is required by the interface Iterator. - */ - public function next() - { - do { - $this->_key = next($this->_keys); - } while (!isset($_SESSION[$this->_key]) && $this->_key !== false); - } - - /** - * Returns whether there is an element at current position. - * This method is required by the interface Iterator. - * @return boolean - */ - public function valid() - { - return $this->_key !== false; - } -} diff --git a/yii/widgets/Breadcrumbs.php b/yii/widgets/Breadcrumbs.php deleted file mode 100644 index 22d09b3..0000000 --- a/yii/widgets/Breadcrumbs.php +++ /dev/null @@ -1,139 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\widgets; - -use Yii; -use yii\base\Widget; -use yii\base\InvalidConfigException; -use yii\helpers\Html; - -/** - * Breadcrumbs displays a list of links indicating the position of the current page in the whole site hierarchy. - * - * For example, breadcrumbs like "Home / Sample Post / Edit" means the user is viewing an edit page - * for the "Sample Post". He can click on "Sample Post" to view that page, or he can click on "Home" - * to return to the homepage. - * - * To use Breadcrumbs, you need to configure its [[links]] property, which specifiesthe links to be displayed. For example, - * - * ~~~ - * $this->widget('yii\widgets\Breadcrumbs', array( - * 'links' => array( - * array('label' => 'Sample Post', 'url' => array('post/edit', 'id' => 1)), - * 'Edit', - * ), - * )); - * ~~~ - * - * Because breadcrumbs usually appears in nearly every page of a website, you may consider place it in a layout view. - * You can then use a view parameter (e.g. `$this->params['breadcrumbs']`) to configure the links in different - * views. In the layout view, you assign this view parameter to the [[links]] property like the following: - * - * ~~~ - * $this->widget('yii\widgets\Breadcrumbs', array( - * 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : array(), - * )); - * ~~~ - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class Breadcrumbs extends Widget -{ - /** - * @var string the name of the breadcrumb container tag. - */ - public $tag = 'ul'; - /** - * @var array the HTML attributes for the breadcrumb container tag. - */ - public $options = array('class' => 'breadcrumb'); - /** - * @var boolean whether to HTML-encode the link labels. - */ - public $encodeLabels = true; - /** - * @var string the first hyperlink in the breadcrumbs (called home link). - * If this property is not set, it will default to a link pointing to [[\yii\web\Application::homeUrl]] - * with the label 'Home'. If this property is false, the home link will not be rendered. - */ - public $homeLink; - /** - * @var array list of links to appear in the breadcrumbs. If this property is empty, - * the widget will not render anything. Each array element represents a single link in the breadcrumbs - * with the following structure: - * - * ~~~ - * array( - * 'label' => 'label of the link', // required - * 'url' => 'url of the link', // optional, will be processed by Html::url() - * ) - * ~~~ - * - * If a link is active, you only need to specify its "label", and instead of writing `array('label' => $label)`, - * you should simply use `$label`. - */ - public $links = array(); - /** - * @var string the template used to render each inactive item in the breadcrumbs. The token `{link}` - * will be replaced with the actual HTML link for each inactive item. - */ - public $itemTemplate = "<li>{link} <span class=\"divider\">/</span></li>\n"; - /** - * @var string the template used to render each active item in the breadcrumbs. The token `{link}` - * will be replaced with the actual HTML link for each active item. - */ - public $activeItemTemplate = "<li class=\"active\">{link}</li>\n"; - - /** - * Renders the widget. - */ - public function run() - { - if (empty($this->links)) { - return; - } - $links = array(); - if ($this->homeLink === null) { - $links[] = $this->renderItem(array( - 'label' => Yii::t('yii|Home'), - 'url' => Yii::$app->homeUrl, - ), $this->itemTemplate); - } elseif ($this->homeLink !== false) { - $links[] = $this->renderItem($this->homeLink, $this->itemTemplate); - } - foreach ($this->links as $link) { - if (!is_array($link)) { - $link = array('label' => $link); - } - $links[] = $this->renderItem($link, isset($link['url']) ? $this->itemTemplate : $this->activeItemTemplate); - } - echo Html::tag($this->tag, implode('', $links), $this->options); - } - - /** - * Renders a single breadcrumb item. - * @param array $link the link to be rendered. It must contain the "label" element. The "url" element is optional. - * @param string $template the template to be used to rendered the link. The token "{link}" will be replaced by the link. - * @return string the rendering result - * @throws InvalidConfigException if `$link` does not have "label" element. - */ - protected function renderItem($link, $template) - { - if (isset($link['label'])) { - $label = $this->encodeLabels ? Html::encode($link['label']) : $link['label']; - } else { - throw new InvalidConfigException('The "label" element is required for each link.'); - } - if (isset($link['url'])) { - return strtr($template, array('{link}' => Html::a($label, $link['url']))); - } else { - return strtr($template, array('{link}' => $label)); - } - } -} diff --git a/yii/widgets/Captcha.php b/yii/widgets/Captcha.php deleted file mode 100644 index 918e30c..0000000 --- a/yii/widgets/Captcha.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\widgets; - -use Yii; -use yii\base\InvalidConfigException; -use yii\base\Widget; -use yii\helpers\Html; -use yii\helpers\Json; -use yii\web\CaptchaAction; - -/** - * Captcha renders a CAPTCHA image element. - * - * Captcha is used together with [[CaptchaAction]] provide [CAPTCHA](http://en.wikipedia.org/wiki/Captcha) - * - a way of preventing Website spamming. - * - * The image element rendered by Captcha will display a CAPTCHA image generated by - * an action whose route is specified by [[captchaAction]]. This action must be an instance of [[CaptchaAction]]. - * - * When the user clicks on the CAPTCHA image, it will cause the CAPTCHA image - * to be refreshed with a new CAPTCHA. - * - * You may use [[\yii\validators\CaptchaValidator]] to validate the user input matches - * the current CAPTCHA verification code. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class Captcha extends Widget -{ - /** - * @var string the route of the action that generates the CAPTCHA images. - * The action represented by this route must be an action of [[CaptchaAction]]. - */ - public $captchaAction = 'site/captcha'; - /** - * @var array HTML attributes to be applied to the rendered image element. - */ - public $options = array(); - - - /** - * Renders the widget. - */ - public function run() - { - $this->checkRequirements(); - - if (!isset($this->options['id'])) { - $this->options['id'] = $this->getId(); - } - $id = $this->options['id']; - $options = Json::encode($this->getClientOptions()); - $this->view->registerAssetBundle('yii/captcha'); - $this->view->registerJs("jQuery('#$id').yiiCaptcha($options);"); - $url = Yii::$app->getUrlManager()->createUrl($this->captchaAction, array('v' => uniqid())); - echo Html::img($url, $this->options); - } - - /** - * Returns the options for the captcha JS widget. - * @return array the options - */ - protected function getClientOptions() - { - $options = array( - 'refreshUrl' => Html::url(array($this->captchaAction, CaptchaAction::REFRESH_GET_VAR => 1)), - 'hashKey' => "yiiCaptcha/{$this->captchaAction}", - ); - return $options; - } - - /** - * Checks if there is graphic extension available to generate CAPTCHA images. - * This method will check the existence of ImageMagick and GD extensions. - * @return string the name of the graphic extension, either "imagick" or "gd". - * @throws InvalidConfigException if neither ImageMagick nor GD is installed. - */ - public static function checkRequirements() - { - if (extension_loaded('imagick')) { - $imagick = new \Imagick(); - $imagickFormats = $imagick->queryFormats('PNG'); - if (in_array('PNG', $imagickFormats)) { - return 'imagick'; - } - } - if (extension_loaded('gd')) { - $gdInfo = gd_info(); - if (!empty($gdInfo['FreeType Support'])) { - return 'gd'; - } - } - throw new InvalidConfigException('GD with FreeType or ImageMagick PHP extensions are required.'); - } -} diff --git a/yii/widgets/ContentDecorator.php b/yii/widgets/ContentDecorator.php deleted file mode 100644 index 3f63621..0000000 --- a/yii/widgets/ContentDecorator.php +++ /dev/null @@ -1,52 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\widgets; - -use yii\base\InvalidConfigException; -use yii\base\Widget; - -/** - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class ContentDecorator extends Widget -{ - /** - * @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 $viewFile; - /** - * @var array the parameters (name=>value) to be extracted and made available in the decorative view. - */ - public $params = array(); - - /** - * Starts recording a clip. - */ - public function init() - { - if ($this->viewFile === null) { - throw new InvalidConfigException('ContentDecorator::viewFile must be set.'); - } - ob_start(); - ob_implicit_flush(false); - } - - /** - * Ends recording a clip. - * This method stops output buffering and saves the rendering result as a named clip in the controller. - */ - public function run() - { - $params = $this->params; - $params['content'] = ob_get_clean(); - // render under the existing context - echo $this->view->renderFile($this->viewFile, $params); - } -} diff --git a/yii/widgets/FragmentCache.php b/yii/widgets/FragmentCache.php deleted file mode 100644 index aa24acd..0000000 --- a/yii/widgets/FragmentCache.php +++ /dev/null @@ -1,174 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\widgets; - -use Yii; -use yii\base\Widget; -use yii\caching\Cache; -use yii\caching\Dependency; - -/** - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class FragmentCache extends Widget -{ - /** - * @var Cache|string the cache object or the application component ID of the cache object. - * After the FragmentCache object is created, if you want to change this property, - * you should only assign it with a cache object. - */ - public $cache = 'cache'; - /** - * @var integer number of seconds that the data can remain valid in cache. - * Use 0 to indicate that the cached data will never expire. - */ - public $duration = 60; - /** - * @var array|Dependency the dependency that the cached content depends on. - * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. - * For example, - * - * ~~~ - * array( - * 'class' => 'yii\caching\DbDependency', - * 'sql' => 'SELECT MAX(lastModified) FROM Post', - * ) - * ~~~ - * - * would make the output cache depends on the last modified time of all posts. - * If any post has its modification time changed, the cached content would be invalidated. - */ - public $dependency; - /** - * @var array list of factors that would cause the variation of the content being cached. - * Each factor is a string representing a variation (e.g. the language, a GET parameter). - * The following variation setting will cause the content to be cached in different versions - * according to the current application language: - * - * ~~~ - * array( - * Yii::$app->language, - * ) - */ - public $variations; - /** - * @var boolean whether to enable the fragment cache. You may use this property to turn on and off - * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). - */ - public $enabled = true; - /** - * @var array a list of placeholders for embedding dynamic contents. This property - * is used internally to implement the content caching feature. Do not modify it. - */ - public $dynamicPlaceholders; - - /** - * Initializes the FragmentCache object. - */ - public function init() - { - parent::init(); - - if (!$this->enabled) { - $this->cache = null; - } elseif (is_string($this->cache)) { - $this->cache = Yii::$app->getComponent($this->cache); - } - - if ($this->getCachedContent() === false) { - $this->view->cacheStack[] = $this; - ob_start(); - ob_implicit_flush(false); - } - } - - /** - * Marks the end of content to be cached. - * Content displayed before this method call and after {@link init()} - * will be captured and saved in cache. - * This method does nothing if valid content is already found in cache. - */ - public function run() - { - if (($content = $this->getCachedContent()) !== false) { - echo $content; - } elseif ($this->cache instanceof Cache) { - $content = ob_get_clean(); - array_pop($this->view->cacheStack); - if (is_array($this->dependency)) { - $this->dependency = Yii::createObject($this->dependency); - } - $data = array($content, $this->dynamicPlaceholders); - $this->cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); - - if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) { - $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); - } - echo $content; - } - } - - /** - * @var string|boolean the cached content. False if the content is not cached. - */ - private $_content; - - /** - * Returns the cached content if available. - * @return string|boolean the cached content. False is returned if valid content is not found in the cache. - */ - public function getCachedContent() - { - if ($this->_content === null) { - $this->_content = false; - if ($this->cache instanceof Cache) { - $key = $this->calculateKey(); - $data = $this->cache->get($key); - if (is_array($data) && count($data) === 2) { - list ($content, $placeholders) = $data; - if (is_array($placeholders) && count($placeholders) > 0) { - if (empty($this->view->cacheStack)) { - // outermost cache: replace placeholder with dynamic content - $content = $this->updateDynamicContent($content, $placeholders); - } - foreach ($placeholders as $name => $statements) { - $this->view->addDynamicPlaceholder($name, $statements); - } - } - $this->_content = $content; - } - } - } - return $this->_content; - } - - protected function updateDynamicContent($content, $placeholders) - { - foreach ($placeholders as $name => $statements) { - $placeholders[$name] = $this->view->evaluateDynamicContent($statements); - } - return strtr($content, $placeholders); - } - - /** - * Generates a unique key used for storing the content in cache. - * The key generated depends on both [[id]] and [[variations]]. - * @return string a valid cache key - */ - protected function calculateKey() - { - $factors = array(__CLASS__, $this->getId()); - if (is_array($this->variations)) { - foreach ($this->variations as $factor) { - $factors[] = $factor; - } - } - return $this->cache->buildKey($factors); - } -} diff --git a/yii/widgets/LinkPager.php b/yii/widgets/LinkPager.php deleted file mode 100644 index 1651246..0000000 --- a/yii/widgets/LinkPager.php +++ /dev/null @@ -1,201 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\widgets; - -use Yii; -use yii\base\InvalidConfigException; -use yii\helpers\Html; -use yii\base\Widget; -use yii\web\Pagination; - -/** - * LinkPager displays a list of hyperlinks that lead to different pages of target. - * - * LinkPager works with a [[Pagination]] object which specifies the totally number - * of pages and the current page number. - * - * Note that LinkPager only generates the necessary HTML markups. In order for it - * to look like a real pager, you should provide some CSS styles for it. - * With the default configuration, LinkPager should look good using Twitter Bootstrap CSS framework. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class LinkPager extends Widget -{ - /** - * @var Pagination the pagination object that this pager is associated with. - * You must set this property in order to make LinkPager work. - */ - public $pagination; - /** - * @var array HTML attributes for the pager container tag. - */ - public $options = array('class' => 'pagination'); - /** - * @var string the CSS class for the "first" page button. - */ - public $firstPageCssClass = 'first'; - /** - * @var string the CSS class for the "last" page button. - */ - public $lastPageCssClass = 'last'; - /** - * @var string the CSS class for the "previous" page button. - */ - public $prevPageCssClass = 'prev'; - /** - * @var string the CSS class for the "next" page button. - */ - public $nextPageCssClass = 'next'; - /** - * @var string the CSS class for the active (currently selected) page button. - */ - public $activePageCssClass = 'active'; - /** - * @var string the CSS class for the disabled page buttons. - */ - public $disabledPageCssClass = 'disabled'; - /** - * @var integer maximum number of page buttons that can be displayed. Defaults to 10. - */ - public $maxButtonCount = 10; - /** - * @var string the label for the "next" page button. Note that this will NOT be HTML-encoded. - * If this property is null, the "next" page button will not be displayed. - */ - public $nextPageLabel = '»'; - /** - * @var string the text label for the previous page button. Note that this will NOT be HTML-encoded. - * If this property is null, the "previous" page button will not be displayed. - */ - public $prevPageLabel = '«'; - /** - * @var string the text label for the "first" page button. Note that this will NOT be HTML-encoded. - * If this property is null, the "first" page button will not be displayed. - */ - public $firstPageLabel; - /** - * @var string the text label for the "last" page button. Note that this will NOT be HTML-encoded. - * If this property is null, the "last" page button will not be displayed. - */ - public $lastPageLabel; - /** - * @var string the template used to render the content within the pager container. - * The token "{buttons}" will be replaced with the actual page buttons. - */ - public $template = '{buttons}'; - - - /** - * Initializes the pager. - */ - public function init() - { - if ($this->pagination === null) { - throw new InvalidConfigException('The "pagination" property must be set.'); - } - } - - /** - * Executes the widget. - * This overrides the parent implementation by displaying the generated page buttons. - */ - public function run() - { - $buttons = strtr($this->template, array( - '{buttons}' => Html::tag('ul', implode("\n", $this->createPageButtons())), - )); - echo Html::tag('div', $buttons, $this->options); - } - - /** - * Creates the page buttons. - * @return array a list of page buttons (in HTML code). - */ - protected function createPageButtons() - { - $buttons = array(); - - $pageCount = $this->pagination->pageCount; - $currentPage = $this->pagination->getPage(); - - // first page - if ($this->firstPageLabel !== null) { - $buttons[] = $this->createPageButton($this->firstPageLabel, 0, $this->firstPageCssClass, $currentPage <= 0, false); - } - - // prev page - if ($this->prevPageLabel !== null) { - if (($page = $currentPage - 1) < 0) { - $page = 0; - } - $buttons[] = $this->createPageButton($this->prevPageLabel, $page, $this->prevPageCssClass, $currentPage <= 0, false); - } - - // internal pages - list($beginPage, $endPage) = $this->getPageRange(); - for ($i = $beginPage; $i <= $endPage; ++$i) { - $buttons[] = $this->createPageButton($i + 1, $i, null, false, $i == $currentPage); - } - - // next page - if ($this->nextPageLabel !== null) { - if (($page = $currentPage + 1) >= $pageCount - 1) { - $page = $pageCount - 1; - } - $buttons[] = $this->createPageButton($this->nextPageLabel, $page, $this->nextPageCssClass, $currentPage >= $pageCount - 1, false); - } - - // last page - if ($this->lastPageLabel !== null) { - $buttons[] = $this->createPageButton($this->lastPageLabel, $pageCount - 1, $this->lastPageCssClass, $currentPage >= $pageCount - 1, false); - } - - return $buttons; - } - - /** - * Creates a page button. - * You may override this method to customize the generation of page buttons. - * @param string $label the text label for the button - * @param integer $page the page number - * @param string $class the CSS class for the page button. - * @param boolean $disabled whether this page button is disabled - * @param boolean $active whether this page button is active - * @return string the generated button - */ - protected function createPageButton($label, $page, $class, $disabled, $active) - { - if ($active) { - $class .= ' ' . $this->activePageCssClass; - } - if ($disabled) { - $class .= ' ' . $this->disabledPageCssClass; - } - $class = trim($class); - $options = array('class' => $class === '' ? null : $class); - return Html::tag('li', Html::a($label, $this->pagination->createUrl($page)), $options); - } - - /** - * @return array the begin and end pages that need to be displayed. - */ - protected function getPageRange() - { - $currentPage = $this->pagination->getPage(); - $pageCount = $this->pagination->getPageCount(); - - $beginPage = max(0, $currentPage - (int)($this->maxButtonCount / 2)); - if (($endPage = $beginPage + $this->maxButtonCount - 1) >= $pageCount) { - $endPage = $pageCount - 1; - $beginPage = max(0, $endPage - $this->maxButtonCount + 1); - } - return array($beginPage, $endPage); - } -} \ No newline at end of file diff --git a/yii/widgets/ListPager.php b/yii/widgets/ListPager.php deleted file mode 100644 index 1bc2b21..0000000 --- a/yii/widgets/ListPager.php +++ /dev/null @@ -1,95 +0,0 @@ -<?php -/** - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\widgets; - -use yii\base\InvalidConfigException; -use yii\helpers\Html; -use yii\base\Widget; -use yii\web\Pagination; - -/** - * ListPager displays a drop-down list that contains options leading to different pages. - * - * ListPager works with a [[Pagination]] object which specifies the totally number - * of pages and the current page number. - * - * Note that ListPager requires JavaScript to work. You should consider using [[LinkPager]] - * if you want to make your page work without JavaScript. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class ListPager extends Widget -{ - /** - * @var Pagination the pagination object that this pager is associated with. - * You must set this property in order to make ListPager work. - */ - public $pagination; - /** - * @var array HTML attributes for the drop-down list tag. The following options are specially handled: - * - * - prompt: string, a prompt text to be displayed as the first option. - * - * 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. - */ - public $options = array(); - /** - * @var string the template used to render the label for each list option. - * The token "{page}" will be replaced with the actual page number (1-based). - */ - public $template = '{page}'; - - - /** - * Initializes the pager. - */ - public function init() - { - if ($this->pagination === null) { - throw new InvalidConfigException('The "pagination" property must be set.'); - } - } - - /** - * Executes the widget. - * This overrides the parent implementation by displaying the generated page buttons. - */ - public function run() - { - $pageCount = $this->pagination->pageCount; - $currentPage = $this->pagination->getPage(); - - $pages = array(); - for ($i = 0; $i < $pageCount; ++$i) { - $pages[$this->pagination->createUrl($i)] = $this->generatePageText($i); - } - $selection = $this->pagination->createUrl($currentPage); - - if (!isset($this->options['onchange'])) { - $this->options['onchange'] = "if(this.value!='') {window.location=this.value;};"; - } - - echo Html::dropDownList(null, $selection, $pages, $this->options); - } - - /** - * Generates the label of the list option for the specified page number. - * You may override this method to customize the option display. - * @param integer $page zero-based page number - * @return string the list option for the page number - */ - protected function generatePageText($page) - { - return strtr($this->template, array( - '{page}' => $page + 1, - )); - } - -} \ No newline at end of file