diff --git a/.gitignore b/.gitignore index 89fc2a8..832a890 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ nbproject .settings # windows thumbnail cache -Thumbs.db \ No newline at end of file +Thumbs.db + +# composer vendor dir +/yii/vendor \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e4b8278 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + +env: + - DB=mysql + +before_script: + - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS yiitest;'; fi" + +script: phpunit \ No newline at end of file diff --git a/app/index.php b/app/index.php deleted file mode 100644 index 8f98090..0000000 --- a/app/index.php +++ /dev/null @@ -1,9 +0,0 @@ -run(); diff --git a/app/assets/.gitignore b/apps/bootstrap/assets/.gitignore similarity index 100% rename from app/assets/.gitignore rename to apps/bootstrap/assets/.gitignore diff --git a/app/css/bootstrap-responsive.css b/apps/bootstrap/css/bootstrap-responsive.css similarity index 100% rename from app/css/bootstrap-responsive.css rename to apps/bootstrap/css/bootstrap-responsive.css diff --git a/app/css/bootstrap-responsive.min.css b/apps/bootstrap/css/bootstrap-responsive.min.css similarity index 100% rename from app/css/bootstrap-responsive.min.css rename to apps/bootstrap/css/bootstrap-responsive.min.css diff --git a/app/css/bootstrap.css b/apps/bootstrap/css/bootstrap.css similarity index 100% rename from app/css/bootstrap.css rename to apps/bootstrap/css/bootstrap.css diff --git a/app/css/bootstrap.min.css b/apps/bootstrap/css/bootstrap.min.css similarity index 100% rename from app/css/bootstrap.min.css rename to apps/bootstrap/css/bootstrap.min.css diff --git a/app/css/site.css b/apps/bootstrap/css/site.css similarity index 100% rename from app/css/site.css rename to apps/bootstrap/css/site.css diff --git a/app/img/glyphicons-halflings-white.png b/apps/bootstrap/img/glyphicons-halflings-white.png similarity index 100% rename from app/img/glyphicons-halflings-white.png rename to apps/bootstrap/img/glyphicons-halflings-white.png diff --git a/app/img/glyphicons-halflings.png b/apps/bootstrap/img/glyphicons-halflings.png similarity index 100% rename from app/img/glyphicons-halflings.png rename to apps/bootstrap/img/glyphicons-halflings.png diff --git a/apps/bootstrap/index.php b/apps/bootstrap/index.php new file mode 100644 index 0000000..a0488ca --- /dev/null +++ b/apps/bootstrap/index.php @@ -0,0 +1,14 @@ +run(); diff --git a/app/js/bootstrap.js b/apps/bootstrap/js/bootstrap.js similarity index 100% rename from app/js/bootstrap.js rename to apps/bootstrap/js/bootstrap.js diff --git a/app/js/bootstrap.min.js b/apps/bootstrap/js/bootstrap.min.js similarity index 100% rename from app/js/bootstrap.min.js rename to apps/bootstrap/js/bootstrap.min.js diff --git a/app/protected/.htaccess b/apps/bootstrap/protected/.htaccess similarity index 100% rename from app/protected/.htaccess rename to apps/bootstrap/protected/.htaccess diff --git a/app/protected/config/assets.php b/apps/bootstrap/protected/config/assets.php similarity index 98% rename from app/protected/config/assets.php rename to apps/bootstrap/protected/config/assets.php index 6602a6e..a3ba847 100644 --- a/app/protected/config/assets.php +++ b/apps/bootstrap/protected/config/assets.php @@ -16,4 +16,4 @@ return array( 'yii', ), ), -); \ No newline at end of file +); diff --git a/app/protected/config/main.php b/apps/bootstrap/protected/config/main.php similarity index 56% rename from app/protected/config/main.php rename to apps/bootstrap/protected/config/main.php index b982506..f19dead 100644 --- a/app/protected/config/main.php +++ b/apps/bootstrap/protected/config/main.php @@ -3,6 +3,12 @@ return array( 'id' => 'hello', 'basePath' => dirname(__DIR__), + 'preload' => array('log'), + 'modules' => array( + 'debug' => array( + 'class' => 'yii\debug\Module', + ) + ), 'components' => array( 'cache' => array( 'class' => 'yii\caching\FileCache', @@ -14,8 +20,17 @@ return array( 'assetManager' => array( 'bundles' => require(__DIR__ . '/assets.php'), ), + 'log' => array( + 'class' => 'yii\logging\Router', + 'targets' => array( + 'file' => array( + 'class' => 'yii\logging\FileTarget', + 'levels' => array('error', 'warning'), + ), + ), + ), ), 'params' => array( 'adminEmail' => 'admin@example.com', ), -); \ No newline at end of file +); diff --git a/app/protected/controllers/SiteController.php b/apps/bootstrap/protected/controllers/SiteController.php similarity index 88% rename from app/protected/controllers/SiteController.php rename to apps/bootstrap/protected/controllers/SiteController.php index 7e6bb15..b06ed06 100644 --- a/app/protected/controllers/SiteController.php +++ b/apps/bootstrap/protected/controllers/SiteController.php @@ -6,6 +6,15 @@ use app\models\ContactForm; class SiteController extends Controller { + public function actions() + { + return array( + 'captcha' => array( + 'class' => 'yii\web\CaptchaAction', + ), + ); + } + public function actionIndex() { echo $this->render('index'); @@ -46,4 +55,4 @@ class SiteController extends Controller { echo $this->render('about'); } -} \ No newline at end of file +} diff --git a/app/protected/models/ContactForm.php b/apps/bootstrap/protected/models/ContactForm.php similarity index 94% rename from app/protected/models/ContactForm.php rename to apps/bootstrap/protected/models/ContactForm.php index 8e8f831..7b713a1 100644 --- a/app/protected/models/ContactForm.php +++ b/apps/bootstrap/protected/models/ContactForm.php @@ -26,7 +26,7 @@ class ContactForm extends Model // email has to be a valid email address array('email', 'email'), // verifyCode needs to be entered correctly - //array('verifyCode', 'captcha', 'allowEmpty' => !Captcha::checkRequirements()), + array('verifyCode', 'captcha'), ); } @@ -60,4 +60,4 @@ class ContactForm extends Model return false; } } -} \ No newline at end of file +} diff --git a/app/protected/models/LoginForm.php b/apps/bootstrap/protected/models/LoginForm.php similarity index 99% rename from app/protected/models/LoginForm.php rename to apps/bootstrap/protected/models/LoginForm.php index 23e8c92..5ba1dc6 100644 --- a/app/protected/models/LoginForm.php +++ b/apps/bootstrap/protected/models/LoginForm.php @@ -55,4 +55,4 @@ class LoginForm extends Model return false; } } -} \ No newline at end of file +} diff --git a/app/protected/models/User.php b/apps/bootstrap/protected/models/User.php similarity index 99% rename from app/protected/models/User.php rename to apps/bootstrap/protected/models/User.php index fcbf14a..afbf9f8 100644 --- a/app/protected/models/User.php +++ b/apps/bootstrap/protected/models/User.php @@ -58,4 +58,4 @@ class User extends \yii\base\Object implements \yii\web\Identity { return $this->password === $password; } -} \ No newline at end of file +} diff --git a/app/protected/runtime/.gitignore b/apps/bootstrap/protected/runtime/.gitignore similarity index 100% rename from app/protected/runtime/.gitignore rename to apps/bootstrap/protected/runtime/.gitignore diff --git a/app/protected/views/layouts/main.php b/apps/bootstrap/protected/views/layouts/main.php similarity index 52% rename from app/protected/views/layouts/main.php rename to apps/bootstrap/protected/views/layouts/main.php index a455f20..a81f983 100644 --- a/app/protected/views/layouts/main.php +++ b/apps/bootstrap/protected/views/layouts/main.php @@ -1,9 +1,11 @@ registerAssetBundle('app'); ?> beginPage(); ?> @@ -23,22 +25,26 @@ $this->registerAssetBundle('app'); + widget('yii\widgets\Breadcrumbs', array( + 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : array(), + )); ?>
@@ -52,6 +58,7 @@ $this->registerAssetBundle('app'); endBody(); ?> +widget('yii\debug\Toolbar'); ?> -endPage(); ?> \ No newline at end of file +endPage(); ?> diff --git a/app/protected/views/site/about.php b/apps/bootstrap/protected/views/site/about.php similarity index 85% rename from app/protected/views/site/about.php rename to apps/bootstrap/protected/views/site/about.php index 7ebc5d5..86e19e1 100644 --- a/app/protected/views/site/about.php +++ b/apps/bootstrap/protected/views/site/about.php @@ -4,6 +4,7 @@ use yii\helpers\Html; * @var yii\base\View $this */ $this->title = 'About'; +$this->params['breadcrumbs'][] = $this->title; ?>

title); ?>

diff --git a/app/protected/views/site/contact.php b/apps/bootstrap/protected/views/site/contact.php similarity index 69% rename from app/protected/views/site/contact.php rename to apps/bootstrap/protected/views/site/contact.php index 246570d..bee1ede 100644 --- a/app/protected/views/site/contact.php +++ b/apps/bootstrap/protected/views/site/contact.php @@ -1,11 +1,15 @@ title = 'Contact'; +$this->params['breadcrumbs'][] = $this->title; ?>

title); ?>

@@ -19,7 +23,7 @@ $this->title = 'Contact'; If you have business inquiries or other questions, please fill out the following form to contact us. Thank you.

-beginWidget('yii\widgets\ActiveForm', array( +beginWidget(ActiveForm::className(), array( 'options' => array('class' => 'form-horizontal'), 'fieldConfig' => array('inputOptions' => array('class' => 'input-xlarge')), )); ?> @@ -27,7 +31,16 @@ $this->title = 'Contact'; field($model, 'email')->textInput(); ?> field($model, 'subject')->textInput(); ?> field($model, 'body')->textArea(array('rows' => 6)); ?> + 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(); + ?>
'btn btn-primary')); ?>
-endWidget(); ?> \ No newline at end of file +endWidget(); ?> diff --git a/app/protected/views/site/index.php b/apps/bootstrap/protected/views/site/index.php similarity index 100% rename from app/protected/views/site/index.php rename to apps/bootstrap/protected/views/site/index.php diff --git a/app/protected/views/site/login.php b/apps/bootstrap/protected/views/site/login.php similarity index 72% rename from app/protected/views/site/login.php rename to apps/bootstrap/protected/views/site/login.php index 5608ffb..65dc7d1 100644 --- a/app/protected/views/site/login.php +++ b/apps/bootstrap/protected/views/site/login.php @@ -1,21 +1,24 @@ title = 'Login'; +$this->params['breadcrumbs'][] = $this->title; ?>

title); ?>

Please fill out the following fields to login:

-beginWidget('yii\widgets\ActiveForm', array('options' => array('class' => 'form-horizontal'))); ?> +beginWidget(ActiveForm::className(), array('options' => array('class' => 'form-horizontal'))); ?> field($model, 'username')->textInput(); ?> field($model, 'password')->passwordInput(); ?> field($model, 'rememberMe')->checkbox(); ?>
'btn btn-primary')); ?>
-endWidget(); ?> \ No newline at end of file +endWidget(); ?> diff --git a/build/build b/build/build index fff4282..95b51e4 100755 --- a/build/build +++ b/build/build @@ -11,10 +11,10 @@ // 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.php'); $id = 'yiic-build'; $basePath = __DIR__; -$application = new yii\console\Application($id, $basePath); +$application = new yii\console\Application(array('id' => $id, 'basePath' => $basePath)); $application->run(); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e9c3927 --- /dev/null +++ b/composer.json @@ -0,0 +1,79 @@ +{ + "name": "yiisoft/yii2", + "description": "Yii2 Web Programming Framework", + "keywords": ["yii", "framework"], + "homepage": "http://www.yiiframework.com/", + "type": "library", + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com", + "homepage": "http://www.yiiframework.com/", + "role": "Founder and project lead" + }, + { + "name": "Alexander Makarov", + "email": "sam@rmcreative.ru", + "homepage": "http://rmcreative.ru/", + "role": "Core framework development" + }, + { + "name": "Maurizio Domba", + "homepage": "http://mdomba.info/", + "role": "Core framework development" + }, + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "http://cebe.cc/", + "role": "Core framework development" + }, + { + "name": "Timur Ruziev", + "email": "resurtm@gmail.com", + "homepage": "http://resurtm.com/", + "role": "Core framework development" + }, + { + "name": "Paul Klimov", + "email": "klimov.paul@gmail.com", + "role": "Core framework development" + }, + { + "name": "Wei Zhuo", + "email": "weizhuo@gmail.com", + "role": "Project site maintenance and development" + }, + { + "name": "Sebastián Thierer", + "email": "sebas@artfos.com", + "role": "Component development" + }, + { + "name": "Jeffrey Winesett", + "email": "jefftulsa@gmail.com", + "role": "Documentation and marketing" + } + ], + "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": "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" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..1cae3d4 --- /dev/null +++ b/composer.lock @@ -0,0 +1,212 @@ +{ + "_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 822c548..4e82793 100644 --- a/docs/api/db/ActiveRecord.md +++ b/docs/api/db/ActiveRecord.md @@ -412,7 +412,7 @@ class Customer extends \yii\db\ActiveRecord /** * @param ActiveQuery $query */ - public function active($query) + public static function active($query) { $query->andWhere('status = 1'); } @@ -435,7 +435,7 @@ class Customer extends \yii\db\ActiveRecord * @param ActiveQuery $query * @param integer $age */ - public function olderThan($query, $age = 30) + public static function olderThan($query, $age = 30) { $query->andWhere('age > :age', array(':age' => $age)); } diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/authentication.md b/docs/guide/authentication.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/authorization.md b/docs/guide/authorization.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/bootstrap.md b/docs/guide/bootstrap.md index 5f00a75..1bc3fe6 100644 --- a/docs/guide/bootstrap.md +++ b/docs/guide/bootstrap.md @@ -7,7 +7,7 @@ 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/app/index.php +http://localhost/yii/apps/bootstrap/index.php ~~~ diff --git a/docs/guide/caching.md b/docs/guide/caching.md new file mode 100644 index 0000000..cd945e7 --- /dev/null +++ b/docs/guide/caching.md @@ -0,0 +1,3 @@ +Caching +======= + diff --git a/docs/guide/console.md b/docs/guide/console.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/dao.md b/docs/guide/dao.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/error.md b/docs/guide/error.md new file mode 100644 index 0000000..c97fada --- /dev/null +++ b/docs/guide/error.md @@ -0,0 +1,3 @@ +Error Handling +============== + diff --git a/docs/guide/extension.md b/docs/guide/extension.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/form.md b/docs/guide/form.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/gii.md b/docs/guide/gii.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/index.md b/docs/guide/index.md index 6ad733c..dd72ca3 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -27,4 +27,4 @@ * [Performance Tuning](performance.md) * [Testing](testing.md) * [Automatic Code Generation](gii.md) -* [Upgrading from 1.1 to 2.0](upgrade.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 6cc5ef8..3f9a803 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -25,8 +25,8 @@ 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 -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, +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. @@ -34,7 +34,7 @@ Recommended Apache Configuration -------------------------------- Yii is ready to work with a default Apache web server configuration. -The `.htaccess` files in Yii framework and application folders restrict +The `.htaccess` files in Yii framework and application folders deny access to the restricted resources. To hide the bootstrap file (usually `index.php`) in your URLs you can add `mod_rewrite` instructions to the `.htaccess` file in your document root or to the virtual host configuration: @@ -63,7 +63,7 @@ server { access_log /www/mysite/log/access.log main; server_name mysite; - root $host_path/htdocs; + root $host_path/htdocs; set $yii_bootstrap "index.php"; charset utf-8; @@ -108,4 +108,5 @@ server { } ~~~ -Using this configuration you can set `cgi.fix_pathinfo=0` in php.ini to avoid many unnecessary system stat() calls. +Using this configuration you can set `cgi.fix_pathinfo=0` in php.ini to avoid +many unnecessary system `stat()` calls. diff --git a/docs/guide/logging.md b/docs/guide/logging.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/migration.md b/docs/guide/migration.md new file mode 100644 index 0000000..bafd293 --- /dev/null +++ b/docs/guide/migration.md @@ -0,0 +1,319 @@ +Database Migration +================== + +Like source code, the structure of a database is evolving as we develop and maintain +a database-driven application. For example, during development, we may want to +add a new table; or after the application is put into production, we may realize +the need of adding an index on a column. It is important to keep track of these +structural database changes (called **migration**) like we do with our source +code. If the source code and the database are out of sync, it is very likely +the whole system may break. For this reason, Yii provides a database migration +tool that can keep track of database migration history, apply new migrations, +or revert existing ones. + +The following steps show how we can use database migration during development: + +1. Tim creates a new migration (e.g. create a new table) +2. Tim commits the new migration into source control system (e.g. GIT, Mercurial) +3. Doug updates from source control system and receives the new migration +4. Doug applies the migration to his local development database + + +Yii supports database migration via the `yiic migrate` command line tool. This +tool supports creating new migrations, applying/reverting/redoing migrations, and +showing migration history and new migrations. + +Creating Migrations +------------------- + +To create a new migration (e.g. create a news table), we run the following command: + +~~~ +yiic migrate/create +~~~ + +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 +~~~ + +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] +class m101129_185401_create_news_table extends \yii\db\Migration +{ + public function up() + { + } + + public function down() + { + echo "m101129_185401_create_news_table cannot be reverted.\n"; + return false; + } +} +~~~ + +Notice that the class name is the same as the file name which is of the pattern +`m_`, where `` refers to the UTC timestamp (in the +format of `yymmdd_hhmmss`) when the migration is created, and `` is taken +from the command's `name` parameter. + +The `up()` method should contain the code implementing the actual database +migration, while the `down()` method may contain the code reverting what is +done in `up()`. + +Sometimes, it is impossible to implement `down()`. For example, if we delete +table rows in `up()`, we will not be able to recover them in `down()`. In this +case, the migration is called irreversible, meaning we cannot roll back to +a previous state of the database. In the above generated code, the `down()` +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] +class m101129_185401_create_news_table extends \yii\db\Migration +{ + public function up() + { + $this->db->createCommand()->createTable('tbl_news, array( + 'id' => 'pk', + 'title' => 'string NOT NULL', + 'content' => 'text', + ))->execute(); + } + + public function down() + { + $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. + +Transactional Migrations +------------------------ + +While performing complex DB migrations, we usually want to make sure that each +migration succeed or fail as a whole so that the database maintains the +consistency and integrity. In order to achieve this goal, we can exploit +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] +class m101129_185401_create_news_table extends \yii\db\Migration +{ + public function up() + { + $transaction=$this->getDbConnection()->beginTransaction(); + try + { + $this->db->createCommand()->createTable('tbl_news, array( + 'id' => 'pk', + 'title' => 'string NOT NULL', + 'content' => 'text', + ))->execute(); + $transaction->commit(); + } + catch(Exception $e) + { + echo "Exception: ".$e->getMessage()."\n"; + $transaction->rollback(); + return false; + } + } + + // ...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 +> `down()`, instead. And for MySQL, some SQL statements may cause +> [implicit commit](http://dev.mysql.com/doc/refman/5.1/en/implicit-commit.html). + + +Applying Migrations +------------------- + +To apply all available new migrations (i.e., make the local database up-to-date), +run the following command: + +~~~ +yiic 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 +after another, in the order of the timestamp value in the class name. + +After applying a migration, the migration tool will keep a record in a database +table named `tbl_migration`. This allows the tool to identify which migrations +have been applied and which are not. If the `tbl_migration` table does not exist, +the tool will automatically create it in the database specified by the `db` +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 +~~~ + +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 +~~~ + +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 +the last applied migration and the specified migration, all these migrations +will be applied. If the specified migration has been applied before, then all +migrations applied after it will be reverted (to be described in the next section). + + +Reverting Migrations +-------------------- + +To revert the last one or several applied migrations, we can use the following +command: + +~~~ +yiic 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. + +As we described before, not all migrations can be reverted. Trying to revert +such migrations will throw an exception and stop the whole reverting process. + + +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] +~~~ + +where the optional `step` parameter specifies how many migrations to be redone. +It defaults to 1, meaning redoing the last migration. + + +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] +~~~ + +where the optional parameter `limit` specifies the number of migrations to be +displayed. If `limit` is not specified, all available migrations will be displayed. + +The first command shows the migrations that have been applied, while the second +command shows the migrations that have not been applied. + + +Modifying Migration History +--------------------------- + +Sometimes, we may want to modify the migration history to a specific migration +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 +~~~ + +This command is very similar to `yiic migrate/to` command, except that it only +modifies the migration history table to the specified version without applying +or reverting the migrations. + + +Customizing Migration Command +----------------------------- + +There are several ways to customize the migration command. + +### Use Command Line Options + +The migration command comes with four options that can be specified in command +line: + +* `interactive`: boolean, specifies whether to perform migrations in an + interactive mode. Defaults to true, meaning the user will be prompted when + performing a specific migration. You may set this to false should the + migrations be done in a background process. + +* `migrationPath`: string, specifies the directory storing all migration class + files. This must be specified in terms of a path alias, and the corresponding + directory must exist. If not specified, it will use the `migrations` + sub-directory under the application base path. + +* `migrationTable`: string, specifies the name of the database table for storing + migration history information. It defaults to `tbl_migration`. The table + structure is `version varchar(255) primary key, apply_time integer`. + +* `connectionID`: string, specifies the ID of the database application component. + Defaults to 'db'. + +* `templateFile`: string, specifies the path of the file to be served as the code + template for generating the migration classes. This must be specified in terms + of a path alias (e.g. `application.migrations.template`). If not set, an + internal template will be used. Inside the template, the token `{ClassName}` + will be replaced with the actual migration class name. + +To specify these options, execute the migrate command using the following format + +~~~ +yiic 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 +~~~ + + +### Configure Command Globally + +While command line options allow us to configure the migration command +on-the-fly, sometimes we may want to configure the command once for all. +For example, we may want to use a different table to store the migration history, +or we may want to use a customized migration template. We can do so by modifying +the console application's configuration file like the following, + +```php +TBD +``` + +Now if we run the `migrate` command, the above configurations will take effect +without requiring us to enter the command line options every time. diff --git a/docs/guide/mvc.md b/docs/guide/mvc.md index 79140ce..a99d043 100644 --- a/docs/guide/mvc.md +++ b/docs/guide/mvc.md @@ -11,7 +11,7 @@ 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 -of a request. Application collects some information about a user request and +of a request. Application collects information about a user request and then dispatches it to an appropriate controller for further handling. The following diagram shows the static structure of a Yii application: @@ -21,6 +21,7 @@ The following diagram shows the static structure of a Yii application: A Typical Workflow ------------------ + The following diagram shows a typical workflow of a Yii application when it is handling a user request: diff --git a/docs/guide/performance.md b/docs/guide/performance.md new file mode 100644 index 0000000..9a871dc --- /dev/null +++ b/docs/guide/performance.md @@ -0,0 +1,181 @@ +Performance Tuning +================== + +Application performance consists of two parts. First is the framework performance +and the second is the application itself. Yii has a pretty low performance impact +on your application out of the box and can be fine-tuned further for production +environment. As for the application, we'll provide some of the best practices +along with examples on how to apply them to Yii. + +Preparing framework for production +---------------------------------- + +### Disabling Debug Mode + +First thing you should do before deploying your application to production environment +is to disable debug mode. A Yii application runs in debug mode if the constant +`YII_DEBUG` is defined as `true` in `index.php` so to disable debug the following +should be in your `index.php`: + +```php +defined('YII_DEBUG') or define('YII_DEBUG', false); +``` + +Debug mode is very useful during development stage, but it would impact performance +because some components cause extra burden in debug mode. For example, the message +logger may record additional debug information for every message being logged. + +### Enabling PHP opcode cache + +Enabling the PHP opcode cache improves any PHP application performance and lowers +memory usage significantly. Yii is no exception. It was tested with +[APC PHP extension](http://php.net/manual/en/book.apc.php) that caches +and optimizes PHP intermediate code and avoids the time spent in parsing PHP +scripts for every incoming request. + +### Turning on ActiveRecord database schema caching + +If the application is using Active Record, we should turn on the schema caching +to save the time of parsing database schema. This can be done by setting the +`Connection::enableSchemaCache` property to be `true` via application configuration +`protected/config/main.php`: + +```php +return array( + // ... + 'components' => array( + // ... + 'db' => array( + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=mydatabase', + 'username' => 'root', + 'password' => '', + 'enableSchemaCache' => true, + + // Duration of schema cache. + // 'schemaCacheDuration' => 3600, + + // Name of the cache component used. Default is 'cache'. + //'schemaCache' => 'cache', + ), + 'cache' => array( + 'class' => 'yii\caching\FileCache', + ), + ), +); +``` + +Note that `cache` application component should be configured. + +### Combining and Minimizing Assets + +TBD + +### Using better storage for sessions + +By default PHP uses files to handle sessions. It is OK for development and +small projects but when it comes to handling concurrent requests it's better to +switch to another storage such as database. You can do so by configuring your +application via `protected/config/main.php`: + +```php +return array( + // ... + 'components' => array( + 'session' => array( + 'class' => 'yii\web\DbSession', + + // Set the following if want to use DB component other than + // default 'db'. + // 'db' => 'mydb', + + // To override default session table set the following + // 'sessionTable' => 'my_session', + ), + ), +); +``` + +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 +be lost leading to unexpected logouts. + +Improving application +--------------------- + +### Using Serverside Caching Techniques + +As described in the Caching section, Yii provides several caching solutions that +may improve the performance of a Web application significantly. If the generation +of some data takes long time, we can use the data caching approach to reduce the +data generation frequency; If a portion of page remains relatively static, we +can use the fragment caching approach to reduce its rendering frequency; +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 + +TBD + +### Database Optimization + +Fetching data from database is often the main performance bottleneck in +a Web application. Although using caching may alleviate the performance hit, +it does not fully solve the problem. When the database contains enormous data +and the cached data is invalid, fetching the latest data could be prohibitively +expensive without proper database and query design. + +Design index wisely in a database. Indexing can make SELECT queries much faster, +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 +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 +a better choice. + +Last but not least, use LIMIT in your SELECT queries. This avoids fetching +overwhelming data from database and exhausting the memory allocated to PHP. + +### Using asArray + +A good way to save memory and processing time on read-only pages is to use +ActiveRecord's `asArray` method. + +```php +class PostController extends Controller +{ + public function actionIndex() + { + $posts = Post::find()->orderBy('id DESC')->limit(100)->asArray()->all(); + echo $this->render('index', array( + 'posts' => $posts, + )); + } +} +``` + +In the view you should access fields of each invidual record from `$posts` as array: + +```php +foreach($posts as $post) { + echo $post['title']."
"; +} +``` + +Note that you can use array notation even if `asArray` wasn't specified and you're +working with AR objects. + +### Processing data in background + +In order to respond to user requests faster you can process heavy parts of the +request later if there's no need for immediate response. + +- Cron jobs + console. +- queues + handlers. + +TBD \ No newline at end of file diff --git a/docs/guide/query-builder.md b/docs/guide/query-builder.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/security.md b/docs/guide/security.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/template.md b/docs/guide/template.md new file mode 100644 index 0000000..dc83d15 --- /dev/null +++ b/docs/guide/template.md @@ -0,0 +1,3 @@ +Template +======== + diff --git a/docs/guide/testing.md b/docs/guide/testing.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/theming.md b/docs/guide/theming.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md new file mode 100644 index 0000000..d35e6e0 --- /dev/null +++ b/docs/guide/upgrade-from-v1.md @@ -0,0 +1,459 @@ +Upgrading from Yii 1.1 +====================== + +In this chapter, we list the major changes introduced in Yii 2.0 since version 1.1. +We hope this list will make it easier for you to upgrade from Yii 1.1 and quickly +master Yii 2.0 based on your existing Yii knowledge. + + +Namespace +--------- + +The most obvious change in Yii 2.0 is the use of namespaces. Almost every core class +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 +class loader. + + +Component and Object +-------------------- + +Yii 2.0 breaks the `CComponent` class in 1.1 into two classes: `Object` and `Component`. +The `Object` class is a lightweight base class that allows defining class properties +via getters and setters. The `Component` class extends from `Object` and supports +the event feature and the behavior feature. + +If your class does not need the event or behavior feature, you should consider using +`Object` as the base class. This is usually the case for classes that represent basic +data structures. + + +Object Configuration +-------------------- + +The `Object` class introduces a uniform way of configuring objects. Any descendant class +of `Object` should declare its constructor (if needed) in the following way so that +it can be properly configured: + +```php +class MyClass extends \yii\Object +{ + public function __construct($param1, $param2, $config = array()) + { + // ... initialization before configuration is applied + + parent::__construct($config); + } + + public function init() + { + parent::init(); + + // ... initialization after configuration is applied + } +} +``` + +In the above, the last parameter of the constructor must take a configuration array +which contains name-value pairs for initializing the properties at the end of the constructor. +You can override the `init()` method to do initialization work that should be done after +the configuration is applied. + +By following this convention, you will be able to create and configure a new object +using a configuration array like the following: + +```php +$object = Yii::createObject(array( + 'class' => 'MyClass', + 'property1' => 'abc', + 'property2' => 'cde', +), $param1, $param2); +``` + + + +Events +------ + +There is no longer the need to define an `on`-method in order to define an event in Yii 2.0. +Instead, you can use whatever event names. To attach a handler to an event, you should +use the `on` method now: + +```php +$component->on($eventName, $handler); +// To detach the handler, use: +// $component->off($eventName, $handler); +``` + + +When you attach a handler, you can now associate it with some parameters which can be later +accessed via the event parameter by the handler: + +```php +$component->on($eventName, $handler, $params); +``` + + +Because of this change, you can now use "global" events. Simply trigger and attach handlers to +an event of the application instance: + +```php +Yii::$app->on($eventName, $handler); +.... +// this will trigger the event and cause $handler to be invoked. +Yii::$app->trigger($eventName); +``` + + +Path Alias +---------- + +Yii 2.0 expands the usage of path aliases to both file/directory paths and URLs. An alias +must start with a `@` character so that it can be differentiated from file/directory paths and URLs. +For example, the alias `@yii` refers to the Yii installation directory. Path aliases are +supported in most places in the Yii core code. For example, `FileCache::cachePath` can take +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 +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. + + +View +---- + +Yii 2.0 introduces a `View` class to represent the view part in 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.** +It refers to the view object that is used to render the view file. To access the controller +or the widget object, you have to use `$this->context` now. + +Because you can access the view object through the "view" application component, +you can now render a view file like the following anywhere in your code, not necessarily +in controllers or widgets: + +```php +$content = Yii::$app->view->renderFile($viewFile, $params); +// You can also explicitly create a new View instance to do the rendering +// $view = new View; +// $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 +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. + + +Models +------ + +A model is now associated with a form name returned 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. + + +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 +a list of scenarios and the corresponding attributes that need to be validated when +`validate()` is called. For example, + +```php +public function scenarios() +{ + return array( + 'backend' => array('email', 'role'), + 'frontend' => array('email', '!name'), + ); +} +``` + + +This method also determines which attributes are safe and which are not. In particular, +given a scenario, if an attribute appears in the corresponding attribute list in `scenarios()` +and the name is not prefixed with `!`, it is considered *safe*. + +Because of the above change, Yii 2.0 no longer has "safe" and "unsafe" validators. + +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. + + +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, + +```php +$model = new Post; +if ($this->populate($_POST, $model)) {...} +// which is equivalent to: +if (isset($_POST['Post'])) { + $model->attributes = $_POST['Post']; +} +``` + + + +Themes +------ + +Theme works completely different in 2.0. It is 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`. + +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. + +There is no more `CThemeManager`. Instead, `theme` is a configurable property of the "view" +application component. + + +Console Applications +-------------------- + +Console applications are now composed by controllers, too, 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 ` command to execute a console command, where `` +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()`. + +Yii 2.0 supports automatic generation of command help information from comment blocks. + + +I18N +---- + +Yii 2.0 removes date formatter and number formatter in favor of the PECL intl PHP module. + +Message translation is still supported, but managed via the "i18n" application component. +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 +-------------- + +Action filters are implemented via behaviors now. You should extend from `ActionFilter` to +define a new filter. To use a filter, you should attach the filter class to the controller +as a behavior. For example, to use the `AccessControl` filter, you should have the following +code in a controller: + +```php +public function behaviors() +{ + return array( + 'access' => array( + 'class' => 'yii\web\AccessControl', + 'rules' => array( + array('allow' => true, 'actions' => array('admin'), 'roles' => array('@')), + array('allow' => false), + ), + ), + ); +} +``` + + + +Assets +------ + +Yii 2.0 introduces a new concept called *asset bundle*. It is a bit 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. + + + +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 +introduces the class map (via `Yii::$classMap`) to overcome this difficulty. + + +`ActiveForm` +------------ + +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: + +```php +beginWidget('yii\widgets\ActiveForm'); ?> + field($model, 'username')->textInput(); ?> + field($model, 'password')->passwordInput(); ?> +
+ +
+endWidget(); ?> +``` + + + +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, + +```php +$query = new \yii\db\Query; +$query->select('id, name') + ->from('tbl_user') + ->limit(10); + +$command = $query->createCommand(); +$sql = $command->sql; +$rows = $command->queryAll(); +``` + + +Best of all, such query building methods can be used together with `ActiveRecord`, +as explained in the next sub-section. + + +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 +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: + +```php +class Customer extends \yii\db\ActiveRecord +{ + public function getOrders() + { + return $this->hasMany('Order', array('customer_id' => 'id')); + } +} +``` + + +You can use `$customer->orders` to access the customer's orders. You can also +use `$customer->getOrders()->andWhere('status=1')->all()` to perform on-the-fly +relational query with customized query conditions. + +When loading relational records in an eager way, Yii 2.0 does it differently from 1.1. +In particular, in 1.1 a JOIN query would be used to bring both the primary and the relational +records; while in 2.0, two SQL statements are executed without using JOIN: the first +statement brings back the primary records and the second brings back the relational records +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: + +```php +// to retrieve all *active* customers and order them by their ID: +$customers = Customer::find() + ->where(array('status' => $active)) + ->orderBy('id') + ->all(); +// return the customer whose PK is 1 +$customer = Customer::find(1); +``` + + +The `find()` method returns an instance of `ActiveQuery` which is a subclass of `Query`. +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, + +```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, +unless you explicitly list the attributes to save. + + +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, + +```php +$command = $connection->createCommand('SELECT [[id]] FROM {{posts}}'); +echo $command->sql; // MySQL: SELECT `id` FROM `posts` +``` + +This feature is especially useful if you are developing an application that supports +different DBMS. + + +User and Identity +----------------- + +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 +is much more straightforward to implement. The bootstrap application provides such an example. + + +URL Management +-------------- + +URL management is similar to 1.1. A major enhancement is that it now supports optional +parameters. For example, if you have rule declared as follows, then it will match +both `post/popular` and `post/1/popular`. In 1.1, you would have to use two rules to achieve +the same goal. + +```php +array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1), +) +``` + + + +Response +-------- + +Extensions +---------- + +Integration with Composer +------------------------- + +TBD + diff --git a/docs/guide/upgrade.md b/docs/guide/upgrade.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/url.md b/docs/guide/url.md new file mode 100644 index 0000000..46bb177 --- /dev/null +++ b/docs/guide/url.md @@ -0,0 +1,3 @@ +URL Management +============== + diff --git a/docs/guide/validation.md b/docs/guide/validation.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/internals/exception_hierarchy.png b/docs/internals/exception_hierarchy.png new file mode 100644 index 0000000..2c43e03 Binary files /dev/null and b/docs/internals/exception_hierarchy.png differ diff --git a/docs/internals/exception_hierarchy.vsd b/docs/internals/exception_hierarchy.vsd new file mode 100644 index 0000000..d044ea9 Binary files /dev/null and b/docs/internals/exception_hierarchy.vsd differ diff --git a/docs/view_renderers.md b/docs/view_renderers.md index 1d88d21..e26fe83 100644 --- a/docs/view_renderers.md +++ b/docs/view_renderers.md @@ -27,6 +27,9 @@ array( ) ``` +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 ---- diff --git a/framework/console/webapp/config.php b/framework/console/webapp/config.php deleted file mode 100644 index 112fb18..0000000 --- a/framework/console/webapp/config.php +++ /dev/null @@ -1,17 +0,0 @@ - array( - 'index.php' => array( - 'handler' => function($source) use ($controller) { - return $controller->replaceRelativePath($source, realpath(YII_PATH.'/yii.php'), 'yii'); - }, - 'permissions' => 0777, - ), - 'protected/runtime' => array( - 'permissions' => 0755, - ), - ), -); \ No newline at end of file diff --git a/framework/console/webapp/default/index.php b/framework/console/webapp/default/index.php deleted file mode 100644 index b84e257..0000000 --- a/framework/console/webapp/default/index.php +++ /dev/null @@ -1,10 +0,0 @@ -run(); \ No newline at end of file diff --git a/framework/console/webapp/default/protected/config/main.php b/framework/console/webapp/default/protected/config/main.php deleted file mode 100644 index 795811e..0000000 --- a/framework/console/webapp/default/protected/config/main.php +++ /dev/null @@ -1,20 +0,0 @@ - 'webapp', - 'name' => 'My Web Application', - - 'components' => array( - // uncomment the following to use a MySQL database - /* - 'db' => array( - 'class' => 'yii\db\Connection', - 'dsn' => 'mysql:host=localhost;dbname=testdrive', - 'username' => 'root', - 'password' => '', - ), - */ - 'cache' => array( - 'class' => 'yii\caching\DummyCache', - ), - ), -); \ No newline at end of file diff --git a/framework/console/webapp/default/protected/controllers/SiteController.php b/framework/console/webapp/default/protected/controllers/SiteController.php deleted file mode 100644 index b47b93c..0000000 --- a/framework/console/webapp/default/protected/controllers/SiteController.php +++ /dev/null @@ -1,15 +0,0 @@ -render('index', array( - 'name' => 'Qiang', - )); - } -} \ No newline at end of file diff --git a/framework/console/webapp/default/protected/views/layouts/main.php b/framework/console/webapp/default/protected/views/layouts/main.php deleted file mode 100644 index 5c883e6..0000000 --- a/framework/console/webapp/default/protected/views/layouts/main.php +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - <?php echo Html::encode($this->title)?> - - -

title)?>

-
- -
- - - \ No newline at end of file diff --git a/framework/console/webapp/default/protected/views/site/index.php b/framework/console/webapp/default/protected/views/site/index.php deleted file mode 100644 index 0fb8784..0000000 --- a/framework/console/webapp/default/protected/views/site/index.php +++ /dev/null @@ -1 +0,0 @@ -Hello, ! \ No newline at end of file diff --git a/framework/helpers/base/ConsoleColor.php b/framework/helpers/base/ConsoleColor.php deleted file mode 100644 index 5e7f577..0000000 --- a/framework/helpers/base/ConsoleColor.php +++ /dev/null @@ -1,470 +0,0 @@ - - * @since 2.0 - */ -class ConsoleColor -{ - const FG_BLACK = 30; - const FG_RED = 31; - const FG_GREEN = 32; - const FG_YELLOW = 33; - const FG_BLUE = 34; - const FG_PURPLE = 35; - const FG_CYAN = 36; - const FG_GREY = 37; - - const BG_BLACK = 40; - const BG_RED = 41; - const BG_GREEN = 42; - const BG_YELLOW = 43; - const BG_BLUE = 44; - const BG_PURPLE = 45; - const BG_CYAN = 46; - const BG_GREY = 47; - - const BOLD = 1; - const ITALIC = 3; - const UNDERLINE = 4; - const BLINK = 5; - const NEGATIVE = 7; - const CONCEALED = 8; - const CROSSED_OUT = 9; - const FRAMED = 51; - const ENCIRCLED = 52; - const OVERLINED = 53; - - /** - * Moves the terminal cursor up by sending ANSI control code CUU to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $rows number of rows the cursor should be moved up - */ - public static function moveCursorUp($rows=1) - { - echo "\033[" . (int) $rows . 'A'; - } - - /** - * Moves the terminal cursor down by sending ANSI control code CUD to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $rows number of rows the cursor should be moved down - */ - public static function moveCursorDown($rows=1) - { - echo "\033[" . (int) $rows . 'B'; - } - - /** - * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $steps number of steps the cursor should be moved forward - */ - public static function moveCursorForward($steps=1) - { - echo "\033[" . (int) $steps . 'C'; - } - - /** - * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $steps number of steps the cursor should be moved backward - */ - public static function moveCursorBackward($steps=1) - { - echo "\033[" . (int) $steps . 'D'; - } - - /** - * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal. - * @param integer $lines number of lines the cursor should be moved down - */ - public static function moveCursorNextLine($lines=1) - { - echo "\033[" . (int) $lines . 'E'; - } - - /** - * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal. - * @param integer $lines number of lines the cursor should be moved up - */ - public static function moveCursorPrevLine($lines=1) - { - echo "\033[" . (int) $lines . 'F'; - } - - /** - * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal. - * @param integer $column 1-based column number, 1 is the left edge of the screen. - * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. - */ - public static function moveCursorTo($column, $row=null) - { - if ($row === null) { - echo "\033[" . (int) $column . 'G'; - } else { - echo "\033[" . (int) $row . ';' . (int) $column . 'H'; - } - } - - /** - * Scrolls whole page up by sending ANSI control code SU to the terminal. - * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows. - * @param int $lines number of lines to scroll up - */ - public static function scrollUp($lines=1) - { - echo "\033[".(int)$lines."S"; - } - - /** - * Scrolls whole page down by sending ANSI control code SD to the terminal. - * New lines are added at the top. This is not supported by ANSI.SYS used in windows. - * @param int $lines number of lines to scroll down - */ - public static function scrollDown($lines=1) - { - echo "\033[".(int)$lines."T"; - } - - /** - * Saves the current cursor position by sending ANSI control code SCP to the terminal. - * Position can then be restored with {@link restoreCursorPosition}. - */ - public static function saveCursorPosition() - { - echo "\033[s"; - } - - /** - * Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal. - */ - public static function restoreCursorPosition() - { - echo "\033[u"; - } - - /** - * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal. - * Use {@link showCursor} to bring it back. - * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit. - */ - public static function hideCursor() - { - echo "\033[?25l"; - } - - /** - * Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal. - */ - public static function showCursor() - { - echo "\033[?25h"; - } - - /** - * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal. - * Cursor position will not be changed. - * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen. - */ - public static function clearScreen() - { - echo "\033[2J"; - } - - /** - * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal. - * Cursor position will not be changed. - */ - public static function clearScreenBeforeCursor() - { - echo "\033[1J"; - } - - /** - * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal. - * Cursor position will not be changed. - */ - public static function clearScreenAfterCursor() - { - echo "\033[0J"; - } - - /** - * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLine() - { - echo "\033[2K"; - } - - /** - * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLineBeforeCursor() - { - echo "\033[1K"; - } - - /** - * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLineAfterCursor() - { - echo "\033[0K"; - } - - /** - * Will send ANSI format for following output - * - * You can pass any of the FG_*, BG_* and TEXT_* constants and also xterm256ColorBg - * TODO: documentation - */ - public static function ansiStyle() - { - echo "\033[" . implode(';', func_get_args()) . 'm'; - } - - /** - * Will return a string formatted with the given ANSI style - * - * See {@link ansiStyle} for possible arguments. - * @param string $string the string to be formatted - * @return string - */ - public static function ansiStyleString($string) - { - $args = func_get_args(); - array_shift($args); - $code = implode(';', $args); - return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string."\033[0m"; - } - - //const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors - public static function xterm256ColorFg($i) // TODO naming! - { - return '38;5;'.$i; - } - - public static function xterm256ColorBg($i) // TODO naming! - { - return '48;5;'.$i; - } - - /** - * Usage: list($w, $h) = ConsoleHelper::getScreenSize(); - * - * @return array - */ - public static function getScreenSize() - { - // TODO implement - return array(150,50); - } - - /** - * resets any ansi style set by previous method {@link ansiStyle} - * Any output after this is will have default text style. - */ - public static function reset() - { - echo "\033[0m"; - } - - /** - * Strips ANSI control codes from a string - * - * @param string $string String to strip - * @return string - */ - public static function strip($string) - { - return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color - } - - // TODO refactor and review - public static function ansiToHtml($string) - { - $tags = 0; - return preg_replace_callback('/\033\[[\d;]+m/', function($ansi) use (&$tags) { - $styleA = array(); - foreach(explode(';', $ansi) as $controlCode) - { - switch($controlCode) - { - case static::FG_BLACK: $style = array('color' => '#000000'); break; - case static::FG_BLUE: $style = array('color' => '#000078'); break; - case static::FG_CYAN: $style = array('color' => '#007878'); break; - case static::FG_GREEN: $style = array('color' => '#007800'); break; - case static::FG_GREY: $style = array('color' => '#787878'); break; - case static::FG_PURPLE: $style = array('color' => '#780078'); break; - case static::FG_RED: $style = array('color' => '#780000'); break; - case static::FG_YELLOW: $style = array('color' => '#787800'); break; - case static::BG_BLACK: $style = array('background-color' => '#000000'); break; - case static::BG_BLUE: $style = array('background-color' => '#000078'); break; - case static::BG_CYAN: $style = array('background-color' => '#007878'); break; - case static::BG_GREEN: $style = array('background-color' => '#007800'); break; - case static::BG_GREY: $style = array('background-color' => '#787878'); break; - case static::BG_PURPLE: $style = array('background-color' => '#780078'); break; - case static::BG_RED: $style = array('background-color' => '#780000'); break; - case static::BG_YELLOW: $style = array('background-color' => '#787800'); break; - case static::BOLD: $style = array('font-weight' => 'bold'); break; - case static::ITALIC: $style = array('font-style' => 'italic'); break; - case static::UNDERLINE: $style = array('text-decoration' => array('underline')); break; - case static::OVERLINED: $style = array('text-decoration' => array('overline')); break; - case static::CROSSED_OUT:$style = array('text-decoration' => array('line-through')); break; - case static::BLINK: $style = array('text-decoration' => array('blink')); break; - case static::NEGATIVE: // ??? - case static::CONCEALED: - case static::ENCIRCLED: - case static::FRAMED: - // TODO allow resetting codes - break; - case 0: // ansi reset - $return = ''; - for($n=$tags; $tags>0; $tags--) { - $return .= ''; - } - return $return; - } - - $styleA = ArrayHelper::merge($styleA, $style); - } - $styleString[] = array(); - foreach($styleA as $name => $content) { - if ($name === 'text-decoration') { - $content = implode(' ', $content); - } - $styleString[] = $name.':'.$content; - } - $tags++; - return ' + + + + ./tests/unit + + + \ No newline at end of file diff --git a/readme.md b/readme.md index dfb9c45..178acd4 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,8 @@ without prior notices. **Yii 2.0 is not ready for production use yet.** DIRECTORY STRUCTURE ------------------- - app/ a ready-to-use application built on Yii 2 + apps/ ready-to-use Web apps built on Yii 2 + bootstrap/ a simple app supporting user login and contact page build/ internally used build tools docs/ documentation framework/ framework source files @@ -26,14 +27,25 @@ REQUIREMENTS The minimum requirement by Yii is that your Web server supports PHP 5.3.?. +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. + + HOW TO PARTICIPATE ------------------ -You are welcome to participate in Yii 2 development in the following ways: +**Your participation to Yii 2 development is very welcome!** + +You may participate in the following ways: * [Report issues](https://github.com/yiisoft/yii2/issues) * [Give us feedback or start a design discussion](http://www.yiiframework.com/forum/index.php/forum/42-design-discussions-for-yii-20/) * Fix issues, develop features, write/polish documentation - - Before you start, please adopt an existing issue or start a new one to avoid duplicated efforts. + - Before you start, please adopt an existing issue (labelled with "ready for adoption") or start a new one to avoid duplicated efforts. - Please submit a merge request after you finish development. diff --git a/tests/unit/DatabaseTestCase.php b/tests/unit/DatabaseTestCase.php new file mode 100644 index 0000000..b39c2e5 --- /dev/null +++ b/tests/unit/DatabaseTestCase.php @@ -0,0 +1,50 @@ +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.'); + } + } + + /** + * @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; + } +} diff --git a/tests/unit/MysqlTestCase.php b/tests/unit/MysqlTestCase.php index e1a1f7e..c7ef970 100644 --- a/tests/unit/MysqlTestCase.php +++ b/tests/unit/MysqlTestCase.php @@ -33,4 +33,4 @@ class MysqlTestCase extends TestCase } return $db; } -} \ No newline at end of file +} diff --git a/tests/unit/TestCase.php b/tests/unit/TestCase.php index 458c6f3..dccd3af 100644 --- a/tests/unit/TestCase.php +++ b/tests/unit/TestCase.php @@ -13,4 +13,4 @@ class TestCase extends \yii\test\TestCase } return isset(self::$params[$name]) ? self::$params[$name] : null; } -} \ No newline at end of file +} diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 8290bbe..285b55b 100644 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -5,7 +5,7 @@ define('YII_DEBUG', true); $_SERVER['SCRIPT_NAME'] = '/' . __DIR__; $_SERVER['SCRIPT_FILENAME'] = __FILE__; -require_once(__DIR__ . '/../../framework/yii.php'); +require_once(__DIR__ . '/../../yii/Yii.php'); Yii::setAlias('@yiiunit', __DIR__); diff --git a/tests/unit/data/ar/ActiveRecord.php b/tests/unit/data/ar/ActiveRecord.php index 95346de..f1194ea 100644 --- a/tests/unit/data/ar/ActiveRecord.php +++ b/tests/unit/data/ar/ActiveRecord.php @@ -23,4 +23,4 @@ class ActiveRecord extends \yii\db\ActiveRecord { return self::$db; } -} \ No newline at end of file +} diff --git a/tests/unit/data/ar/Customer.php b/tests/unit/data/ar/Customer.php index a090b7f..b26b51b 100644 --- a/tests/unit/data/ar/Customer.php +++ b/tests/unit/data/ar/Customer.php @@ -22,6 +22,6 @@ class Customer extends ActiveRecord public static function active($query) { - return $query->andWhere('status=1'); + $query->andWhere('status=1'); } -} \ No newline at end of file +} diff --git a/tests/unit/data/ar/Item.php b/tests/unit/data/ar/Item.php index 279893f..5d23378 100644 --- a/tests/unit/data/ar/Item.php +++ b/tests/unit/data/ar/Item.php @@ -8,4 +8,4 @@ class Item extends ActiveRecord { return 'tbl_item'; } -} \ No newline at end of file +} diff --git a/tests/unit/data/ar/Order.php b/tests/unit/data/ar/Order.php index 38257d6..f9dd715 100644 --- a/tests/unit/data/ar/Order.php +++ b/tests/unit/data/ar/Order.php @@ -43,4 +43,4 @@ class Order extends ActiveRecord return false; } } -} \ No newline at end of file +} diff --git a/tests/unit/data/ar/OrderItem.php b/tests/unit/data/ar/OrderItem.php index f879749..607133e 100644 --- a/tests/unit/data/ar/OrderItem.php +++ b/tests/unit/data/ar/OrderItem.php @@ -18,4 +18,4 @@ class OrderItem extends ActiveRecord { return $this->hasOne('Item', array('id' => 'item_id')); } -} \ No newline at end of file +} diff --git a/tests/unit/data/base/Singer.php b/tests/unit/data/base/Singer.php index 3305b98..f1b91e1 100644 --- a/tests/unit/data/base/Singer.php +++ b/tests/unit/data/base/Singer.php @@ -18,4 +18,4 @@ class Singer extends Model array('underscore_style', 'yii\validators\CaptchaValidator'), ); } -} \ No newline at end of file +} diff --git a/tests/unit/data/config.php b/tests/unit/data/config.php index fc15690..c980c57 100644 --- a/tests/unit/data/config.php +++ b/tests/unit/data/config.php @@ -1,10 +1,16 @@ array( - 'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest', - 'username' => 'root', - 'password' => '', - 'fixture' => __DIR__ . '/mysql.sql', - ), + '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', + ), + ) ); diff --git a/tests/unit/data/i18n/test.mo b/tests/unit/data/i18n/test.mo new file mode 100644 index 0000000..d5f94f1 Binary files /dev/null and b/tests/unit/data/i18n/test.mo differ diff --git a/tests/unit/data/i18n/test.po b/tests/unit/data/i18n/test.po new file mode 100644 index 0000000..fed95c9 --- /dev/null +++ b/tests/unit/data/i18n/test.po @@ -0,0 +1,64 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: resurtm \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +msgctxt "context1" +msgid "" +"Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\n" +"aliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel " +"malesuada.\n" +"Nunc vel sapien nunc, a pretium nulla." +msgstr "" +"Олицетворение однократно. Представленный лексико-семантический анализ " +"является\n" +"психолингвистическим в своей основе, но механизм сочленений полидисперсен. " +"Впечатление\n" +"однократно. Различное расположение выбирает сюжетный механизм сочленений." + +msgctxt "context1" +msgid "String number two." +msgstr "Строка номер два." + +msgctxt "context2" +msgid "" +"The other\n" +"\n" +"context.\n" +msgstr "" +"Другой\n" +"\n" +"контекст.\n" + +msgctxt "context1" +msgid "" +"Missing\n" +"\r\t\"translation." +msgstr "" + +msgctxt "context1" +msgid "" +"Nunc vel sapien nunc, a pretium nulla.\n" +"Pellentesque habitant morbi tristique senectus et netus et malesuada fames " +"ac turpis egestas." +msgstr "Короткий перевод." + +msgid "contextless" +msgstr "" + +msgctxt "context2" +msgid "" +"test1\\ntest2\n" +"\\\n" +"test3" +msgstr "" +"тест1\\nтест2\n" +"\\\n" +"тест3" diff --git a/tests/unit/data/mysql.sql b/tests/unit/data/mysql.sql index 8223f59..1bb5558 100644 --- a/tests/unit/data/mysql.sql +++ b/tests/unit/data/mysql.sql @@ -1,10 +1,6 @@ /** * This is the database schema for testing MySQL support of Yii DAO and Active Record. - * The following database setup is required to perform then relevant tests: - * Database name: yiitest - * username: test - * password: test - * charset: utf8 + * The database setup in config.php is required to perform then relevant tests: */ DROP TABLE IF EXISTS tbl_order_item CASCADE; diff --git a/tests/unit/data/sqlite.sql b/tests/unit/data/sqlite.sql index affeca9..f75bfa6 100644 --- a/tests/unit/data/sqlite.sql +++ b/tests/unit/data/sqlite.sql @@ -1,262 +1,88 @@ -CREATE TABLE users -( - id INTEGER NOT NULL PRIMARY KEY, - username VARCHAR(128) NOT NULL, - password VARCHAR(128) NOT NULL, - email VARCHAR(128) NOT NULL -); - -INSERT INTO users(id,username,password,email) VALUES (1,'user1','pass1','email1'); -INSERT INTO users(id,username,password,email) VALUES (2,'user2','pass2','email2'); -INSERT INTO users(id,username,password,email) VALUES (3,'user3','pass3','email3'); -INSERT INTO users(id,username,password,email) VALUES (4,'user4','pass4','email4'); - -CREATE TABLE groups -( - id INTEGER NOT NULL PRIMARY KEY, - name VARCHAR(128) NOT NULL -); - -INSERT INTO groups(id,name) VALUES (1,'group1'); -INSERT INTO groups(id,name) VALUES (2,'group2'); -INSERT INTO groups(id,name) VALUES (3,'group3'); -INSERT INTO groups(id,name) VALUES (4,'group4'); -INSERT INTO groups(id,name) VALUES (5,'group5'); -INSERT INTO groups(id,name) VALUES (6,'group6'); - -CREATE TABLE groups_descriptions -( - group_id INTEGER NOT NULL PRIMARY KEY, - name VARCHAR(128) NOT NULL -); - -INSERT INTO groups_descriptions(group_id,name) VALUES (1,'room1'); -INSERT INTO groups_descriptions(group_id,name) VALUES (2,'room2'); -INSERT INTO groups_descriptions(group_id,name) VALUES (3,'room3'); -INSERT INTO groups_descriptions(group_id,name) VALUES (4,'room4'); - -CREATE TABLE roles -( - user_id INTEGER NOT NULL, - group_id INTEGER NOT NULL, - name VARCHAR(128) NOT NULL, - PRIMARY KEY(user_id,group_id) -); - -INSERT INTO roles(user_id,group_id,name) VALUES (1,1,'dev'); -INSERT INTO roles(user_id,group_id,name) VALUES (1,2,'user'); -INSERT INTO roles(user_id,group_id,name) VALUES (2,1,'dev'); -INSERT INTO roles(user_id,group_id,name) VALUES (2,3,'user'); - -CREATE TABLE mentorships -( - teacher_id INTEGER NOT NULL, - student_id INTEGER NOT NULL, - progress VARCHAR(128) NOT NULL, - PRIMARY KEY(teacher_id,student_id) -); - -INSERT INTO mentorships(teacher_id,student_id,progress) VALUES (1,3,'good'); -INSERT INTO mentorships(teacher_id,student_id,progress) VALUES (2,4,'average'); - -CREATE TABLE profiles -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - 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 users (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO profiles (first_name, last_name, user_id) VALUES ('first 1','last 1',1); -INSERT INTO profiles (first_name, last_name, user_id) VALUES ('first 2','last 2',2); - -CREATE TABLE posts -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - 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 users (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 1',100000,1,'content 1'); -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 2',100001,2,'content 2'); -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 3',100002,2,'content 3'); -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 4',100003,2,'content 4'); -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 5',100004,3,'content 5'); - - -CREATE TABLE posts_nofk -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - title VARCHAR(128) NOT NULL, - create_time TIMESTAMP NOT NULL, - author_id INTEGER NOT NULL, - content TEXT -); - -INSERT INTO posts_nofk (title, create_time, author_id, content) VALUES ('post 1',100000,1,'content 1'); -INSERT INTO posts_nofk (title, create_time, author_id, content) VALUES ('post 2',100001,2,'content 2'); -INSERT INTO posts_nofk (title, create_time, author_id, content) VALUES ('post 3',100002,2,'content 3'); -INSERT INTO posts_nofk (title, create_time, author_id, content) VALUES ('post 4',100003,2,'content 4'); -INSERT INTO posts_nofk (title, create_time, author_id, content) VALUES ('post 5',100004,3,'content 5'); - - -CREATE TABLE comments -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - content TEXT NOT NULL, - post_id INTEGER NOT NULL, - author_id INTEGER NOT NULL, - CONSTRAINT FK_post_comment FOREIGN KEY (post_id) - REFERENCES posts (id) ON DELETE CASCADE ON UPDATE RESTRICT, - CONSTRAINT FK_user_comment FOREIGN KEY (author_id) - REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 1',1, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 2',1, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 3',1, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 4',2, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 5',2, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 6',3, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 7',3, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 8',3, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 9',3, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 10',5, 3); - -CREATE TABLE categories -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - name VARCHAR(128) NOT NULL, - parent_id INTEGER, - CONSTRAINT FK_category_category FOREIGN KEY (parent_id) - REFERENCES categories (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO categories (name, parent_id) VALUES ('cat 1',NULL); -INSERT INTO categories (name, parent_id) VALUES ('cat 2',NULL); -INSERT INTO categories (name, parent_id) VALUES ('cat 3',NULL); -INSERT INTO categories (name, parent_id) VALUES ('cat 4',1); -INSERT INTO categories (name, parent_id) VALUES ('cat 5',1); -INSERT INTO categories (name, parent_id) VALUES ('cat 6',5); -INSERT INTO categories (name, parent_id) VALUES ('cat 7',5); - -CREATE TABLE 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 posts (id) ON DELETE CASCADE ON UPDATE RESTRICT, - CONSTRAINT FK_post_category_category FOREIGN KEY (category_id) - REFERENCES categories (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO post_category (category_id, post_id) VALUES (1,1); -INSERT INTO post_category (category_id, post_id) VALUES (2,1); -INSERT INTO post_category (category_id, post_id) VALUES (3,1); -INSERT INTO post_category (category_id, post_id) VALUES (4,2); -INSERT INTO post_category (category_id, post_id) VALUES (1,2); -INSERT INTO post_category (category_id, post_id) VALUES (1,3); - -CREATE TABLE orders -( - key1 INTEGER NOT NULL, - key2 INTEGER NOT NULL, - name VARCHAR(128), - PRIMARY KEY (key1, key2) -); - -INSERT INTO orders (key1,key2,name) VALUES (1,2,'order 12'); -INSERT INTO orders (key1,key2,name) VALUES (1,3,'order 13'); -INSERT INTO orders (key1,key2,name) VALUES (2,1,'order 21'); -INSERT INTO orders (key1,key2,name) VALUES (2,2,'order 22'); - -CREATE TABLE items -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - name VARCHAR(128), - col1 INTEGER NOT NULL, - col2 INTEGER NOT NULL, - CONSTRAINT FK_order_item FOREIGN KEY (col1,col2) - REFERENCES orders (key1,key2) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO items (name,col1,col2) VALUES ('item 1',1,2); -INSERT INTO items (name,col1,col2) VALUES ('item 2',1,2); -INSERT INTO items (name,col1,col2) VALUES ('item 3',1,3); -INSERT INTO items (name,col1,col2) VALUES ('item 4',2,2); -INSERT INTO items (name,col1,col2) VALUES ('item 5',2,2); - -CREATE TABLE 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, - float_col REAL(4,3) NOT NULL, - float_col2 DOUBLE DEFAULT 1.23, - blob_col BLOB, - numeric_col NUMERIC(5,2) DEFAULT 33.22, - time TIMESTAMP DEFAULT 123, - bool_col BOOL NOT NULL, - bool_col2 BOOLEAN DEFAULT 1, - null_col INTEGER DEFAULT NULL -); - -CREATE TABLE Content -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - class VARCHAR(128), - parentID INTEGER NOT NULL, - ownerID INTEGER NOT NULL, - title VARCHAR(100), - CONSTRAINT FK_content_user FOREIGN KEY (ownerID) - REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT - CONSTRAINT FK_content_parent FOREIGN KEY (parentID) - REFERENCES Content (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO Content (class,parentID,ownerID,title) VALUES ('Article',-1,1,'article 1'); -INSERT INTO Content (class,parentID,ownerID,title) VALUES ('Article',-1,2,'article 2'); -INSERT INTO Content (class,parentID,ownerID,title) VALUES ('Comment',1,1,'comment 1'); -INSERT INTO Content (class,parentID,ownerID,title) VALUES ('Article',-1,2,'article 3'); -INSERT INTO Content (class,parentID,ownerID,title) VALUES ('Comment',4,2,'comment 2'); -INSERT INTO Content (class,parentID,ownerID,title) VALUES ('Comment',4,1,'comment 3'); - -CREATE TABLE Article -( - id INTEGER NOT NULL PRIMARY KEY, - authorID INTEGER NOT NULL, - body TEXT, - CONSTRAINT FK_article_content FOREIGN KEY (id) - REFERENCES Content (id) ON DELETE CASCADE ON UPDATE RESTRICT - CONSTRAINT FK_article_author FOREIGN KEY (authorID) - REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO Article (id,authorID,body) VALUES (1,1,'content for article 1'); -INSERT INTO Article (id,authorID,body) VALUES (2,2,'content for article 2'); -INSERT INTO Article (id,authorID,body) VALUES (4,1,'content for article 3'); - -CREATE TABLE Comment -( - id INTEGER NOT NULL PRIMARY KEY, - authorID INTEGER NOT NULL, - body TEXT, - CONSTRAINT FK_comment_content FOREIGN KEY (id) - REFERENCES Content (id) ON DELETE CASCADE ON UPDATE RESTRICT - CONSTRAINT FK_article_author FOREIGN KEY (authorID) - REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO Comment (id,authorID,body) VALUES (3,1,'content for comment 1'); -INSERT INTO Comment (id,authorID,body) VALUES (5,1,'content for comment 2'); -INSERT INTO Comment (id,authorID,body) VALUES (6,1,'content for comment 3'); - +/** + * This is the database schema for testing Sqlite 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_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; + +CREATE TABLE tbl_customer ( + id INTEGER NOT NULL, + email varchar(128) NOT NULL, + name varchar(128) NOT NULL, + address text, + status INTEGER DEFAULT 0, + PRIMARY KEY (id) +); + +CREATE TABLE tbl_category ( + id INTEGER NOT NULL, + name varchar(128) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE tbl_item ( + id INTEGER NOT NULL, + name varchar(128) NOT NULL, + category_id INTEGER NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE tbl_order ( + id INTEGER NOT NULL, + customer_id INTEGER NOT NULL, + create_time INTEGER NOT NULL, + total decimal(10,0) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE tbl_order_item ( + order_id INTEGER NOT NULL, + item_id INTEGER NOT NULL, + quantity INTEGER NOT NULL, + subtotal decimal(10,0) NOT NULL, + PRIMARY KEY (order_id, item_id) +); + +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(4,3) 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', + bool_col tinyint(1) NOT NULL, + bool_col2 tinyint(1) DEFAULT '1' +); + +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); \ No newline at end of file diff --git a/tests/unit/framework/YiiBaseTest.php b/tests/unit/framework/YiiBaseTest.php index 47474f2..4ebf45c 100644 --- a/tests/unit/framework/YiiBaseTest.php +++ b/tests/unit/framework/YiiBaseTest.php @@ -47,7 +47,6 @@ class YiiBaseTest extends TestCase public function testGetVersion() { - echo Yii::getVersion(); $this->assertTrue((boolean)preg_match('~\d+\.\d+(?:\.\d+)?(?:-\w+)?~', \Yii::getVersion())); } diff --git a/tests/unit/framework/base/ComponentTest.php b/tests/unit/framework/base/ComponentTest.php index 74b6e9a..7c860e3 100644 --- a/tests/unit/framework/base/ComponentTest.php +++ b/tests/unit/framework/base/ComponentTest.php @@ -387,4 +387,4 @@ class NewComponent2 extends Component $this->b = $b; $this->c = $c; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/base/ObjectTest.php b/tests/unit/framework/base/ObjectTest.php index b47b178..14856e2 100644 --- a/tests/unit/framework/base/ObjectTest.php +++ b/tests/unit/framework/base/ObjectTest.php @@ -181,4 +181,4 @@ class NewObject extends Object { return $this->_items; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/ApcCacheTest.php b/tests/unit/framework/caching/ApcCacheTest.php index 74ede2a..6018ce7 100644 --- a/tests/unit/framework/caching/ApcCacheTest.php +++ b/tests/unit/framework/caching/ApcCacheTest.php @@ -15,13 +15,15 @@ class ApcCacheTest extends CacheTest */ protected function getCacheInstance() { - if(!extension_loaded("apc")) { + if (!extension_loaded("apc")) { $this->markTestSkipped("APC not installed. Skipping."); + } elseif ('cli' === PHP_SAPI && !ini_get('apc.enable_cli')) { + $this->markTestSkipped("APC cli is not enabled. Skipping."); } - if($this->_cacheInstance === null) { + if ($this->_cacheInstance === null) { $this->_cacheInstance = new ApcCache(); } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/DbCacheTest.php b/tests/unit/framework/caching/DbCacheTest.php index 594e946..253240d 100644 --- a/tests/unit/framework/caching/DbCacheTest.php +++ b/tests/unit/framework/caching/DbCacheTest.php @@ -35,7 +35,8 @@ class DbCacheTest extends CacheTest function getConnection($reset = true) { if($this->_connection === null) { - $params = $this->getParam('mysql'); + $databases = $this->getParam('databases'); + $params = $databases['mysql']; $db = new \yii\db\Connection; $db->dsn = $params['dsn']; $db->username = $params['username']; @@ -67,4 +68,4 @@ class DbCacheTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/FileCacheTest.php b/tests/unit/framework/caching/FileCacheTest.php index 1f6debd..37d3222 100644 --- a/tests/unit/framework/caching/FileCacheTest.php +++ b/tests/unit/framework/caching/FileCacheTest.php @@ -22,4 +22,4 @@ class FileCacheTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/MemCacheTest.php b/tests/unit/framework/caching/MemCacheTest.php index e4804d9..40dba12 100644 --- a/tests/unit/framework/caching/MemCacheTest.php +++ b/tests/unit/framework/caching/MemCacheTest.php @@ -24,4 +24,4 @@ class MemCacheTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/MemCachedTest.php b/tests/unit/framework/caching/MemCachedTest.php index 59396df..c9e437c 100644 --- a/tests/unit/framework/caching/MemCachedTest.php +++ b/tests/unit/framework/caching/MemCachedTest.php @@ -26,4 +26,4 @@ class MemCachedTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/WinCacheTest.php b/tests/unit/framework/caching/WinCacheTest.php index b78d57b..c9470bd 100644 --- a/tests/unit/framework/caching/WinCacheTest.php +++ b/tests/unit/framework/caching/WinCacheTest.php @@ -28,4 +28,4 @@ class WinCacheTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/XCacheTest.php b/tests/unit/framework/caching/XCacheTest.php index e1ed844..b5e41a6 100644 --- a/tests/unit/framework/caching/XCacheTest.php +++ b/tests/unit/framework/caching/XCacheTest.php @@ -24,4 +24,4 @@ class XCacheTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/ZendDataCacheTest.php b/tests/unit/framework/caching/ZendDataCacheTest.php index 91dfbb5..86c06c8 100644 --- a/tests/unit/framework/caching/ZendDataCacheTest.php +++ b/tests/unit/framework/caching/ZendDataCacheTest.php @@ -24,4 +24,4 @@ class ZendDataCacheTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index 3b8c2b5..e337a5d 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -10,10 +10,11 @@ use yiiunit\data\ar\OrderItem; use yiiunit\data\ar\Order; use yiiunit\data\ar\Item; -class ActiveRecordTest extends \yiiunit\MysqlTestCase +class ActiveRecordTest extends \yiiunit\DatabaseTestCase { public function setUp() { + parent::setUp(); ActiveRecord::$db = $this->getConnection(); } @@ -356,4 +357,4 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $customers = Customer::find()->all(); $this->assertEquals(0, count($customers)); } -} \ No newline at end of file +} diff --git a/tests/unit/framework/db/CommandTest.php b/tests/unit/framework/db/CommandTest.php index d505f6d..0d9652b 100644 --- a/tests/unit/framework/db/CommandTest.php +++ b/tests/unit/framework/db/CommandTest.php @@ -7,7 +7,7 @@ use yii\db\Command; use yii\db\Query; use yii\db\DataReader; -class CommandTest extends \yiiunit\MysqlTestCase +class CommandTest extends \yiiunit\DatabaseTestCase { function testConstruct() { @@ -289,4 +289,4 @@ class CommandTest extends \yiiunit\MysqlTestCase { } -} \ No newline at end of file +} diff --git a/tests/unit/framework/db/ConnectionTest.php b/tests/unit/framework/db/ConnectionTest.php index 256c5a9..c837b95 100644 --- a/tests/unit/framework/db/ConnectionTest.php +++ b/tests/unit/framework/db/ConnectionTest.php @@ -4,12 +4,12 @@ namespace yiiunit\framework\db; use yii\db\Connection; -class ConnectionTest extends \yiiunit\MysqlTestCase +class ConnectionTest extends \yiiunit\DatabaseTestCase { function testConstruct() { $connection = $this->getConnection(false); - $params = $this->getParam('mysql'); + $params = $this->database; $this->assertEquals($params['dsn'], $connection->dsn); $this->assertEquals($params['username'], $connection->username); @@ -18,7 +18,7 @@ class ConnectionTest extends \yiiunit\MysqlTestCase function testOpenClose() { - $connection = $this->getConnection(false); + $connection = $this->getConnection(false, false); $this->assertFalse($connection->isActive); $this->assertEquals(null, $connection->pdo); @@ -39,9 +39,8 @@ class ConnectionTest extends \yiiunit\MysqlTestCase function testGetDriverName() { - $connection = $this->getConnection(false); - $this->assertEquals('mysql', $connection->driverName); - $this->assertFalse($connection->isActive); + $connection = $this->getConnection(false, false); + $this->assertEquals($this->driverName, $connection->driverName); } function testQuoteValue() diff --git a/tests/unit/framework/db/QueryTest.php b/tests/unit/framework/db/QueryTest.php index 2c4359f..398d0d9 100644 --- a/tests/unit/framework/db/QueryTest.php +++ b/tests/unit/framework/db/QueryTest.php @@ -7,7 +7,7 @@ use yii\db\Command; use yii\db\Query; use yii\db\DataReader; -class QueryTest extends \yiiunit\MysqlTestCase +class QueryTest extends \yiiunit\DatabaseTestCase { function testSelect() { @@ -113,4 +113,4 @@ class QueryTest extends \yiiunit\MysqlTestCase { } -} \ No newline at end of file +} diff --git a/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php new file mode 100644 index 0000000..539e879 --- /dev/null +++ b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php @@ -0,0 +1,12 @@ +driverName = 'sqlite'; + parent::setUp(); + } +} diff --git a/tests/unit/framework/db/sqlite/SqliteCommandTest.php b/tests/unit/framework/db/sqlite/SqliteCommandTest.php new file mode 100644 index 0000000..4110cd3 --- /dev/null +++ b/tests/unit/framework/db/sqlite/SqliteCommandTest.php @@ -0,0 +1,21 @@ +driverName = 'sqlite'; + parent::setUp(); + } + + 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/sqlite/SqliteConnectionTest.php b/tests/unit/framework/db/sqlite/SqliteConnectionTest.php new file mode 100644 index 0000000..eeb5aae --- /dev/null +++ b/tests/unit/framework/db/sqlite/SqliteConnectionTest.php @@ -0,0 +1,47 @@ +driverName = 'sqlite'; + parent::setUp(); + } + + function testConstruct() + { + $connection = $this->getConnection(false); + $params = $this->database; + + $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")); + } + + 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)')); + } +} diff --git a/tests/unit/framework/db/sqlite/SqliteQueryTest.php b/tests/unit/framework/db/sqlite/SqliteQueryTest.php new file mode 100644 index 0000000..f00e59e --- /dev/null +++ b/tests/unit/framework/db/sqlite/SqliteQueryTest.php @@ -0,0 +1,20 @@ +driverName = 'sqlite'; + parent::setUp(); + } +} diff --git a/tests/unit/framework/helpers/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php index 187217f..8c83278 100644 --- a/tests/unit/framework/helpers/ArrayHelperTest.php +++ b/tests/unit/framework/helpers/ArrayHelperTest.php @@ -12,6 +12,16 @@ class ArrayHelperTest extends \yii\test\TestCase } + public function testRemove() + { + $array = array('name' => 'b', 'age' => 3); + $name = ArrayHelper::remove($array, 'name'); + + $this->assertEquals($name, 'b'); + $this->assertEquals($array, array('age' => 3)); + } + + public function testMultisort() { // single key @@ -40,11 +50,20 @@ class ArrayHelperTest extends \yii\test\TestCase $array = array( array('name' => 'a', 'age' => 3), array('name' => 'b', 'age' => 2), + array('name' => 'B', 'age' => 4), array('name' => 'A', 'age' => 1), ); - ArrayHelper::multisort($array, array('name', 'age'), SORT_ASC, array(SORT_STRING|SORT_FLAG_CASE, SORT_REGULAR)); + + ArrayHelper::multisort($array, array('name', 'age'), SORT_ASC, 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); $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]); } } diff --git a/tests/unit/framework/helpers/JsonTest.php b/tests/unit/framework/helpers/JsonTest.php index 0e06eb3..1795ce6 100644 --- a/tests/unit/framework/helpers/JsonTest.php +++ b/tests/unit/framework/helpers/JsonTest.php @@ -4,7 +4,7 @@ namespace yiiunit\framework\helpers; use yii\helpers\Json; -use yii\helpers\JsExpression; +use yii\web\JsExpression; class JsonTest extends \yii\test\TestCase { @@ -57,4 +57,4 @@ class JsonTest extends \yii\test\TestCase $this->setExpectedException('yii\base\InvalidParamException'); Json::decode($json); } -} \ No newline at end of file +} diff --git a/tests/unit/framework/helpers/StringHelperTest.php b/tests/unit/framework/helpers/StringHelperTest.php index 4e1266f..64811d1 100644 --- a/tests/unit/framework/helpers/StringHelperTest.php +++ b/tests/unit/framework/helpers/StringHelperTest.php @@ -70,4 +70,48 @@ class StringHelperTest extends \yii\test\TestCase $this->assertEquals('PostTag', StringHelper::id2camel('post-tag')); $this->assertEquals('PostTag', StringHelper::id2camel('post_tag', '_')); } -} \ No newline at end of file + + public function testBasename() + { + $this->assertEquals('', StringHelper::basename('')); + + $this->assertEquals('file', StringHelper::basename('file')); + $this->assertEquals('file.test', StringHelper::basename('file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('file.test', '.test')); + + $this->assertEquals('file', StringHelper::basename('/file')); + $this->assertEquals('file.test', StringHelper::basename('/file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('/file.test', '.test')); + + $this->assertEquals('file', StringHelper::basename('/path/to/file')); + $this->assertEquals('file.test', StringHelper::basename('/path/to/file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('/path/to/file.test', '.test')); + + $this->assertEquals('file', StringHelper::basename('\file')); + $this->assertEquals('file.test', StringHelper::basename('\file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('\file.test', '.test')); + + $this->assertEquals('file', StringHelper::basename('C:\file')); + $this->assertEquals('file.test', StringHelper::basename('C:\file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('C:\file.test', '.test')); + + $this->assertEquals('file', StringHelper::basename('C:\path\to\file')); + $this->assertEquals('file.test', StringHelper::basename('C:\path\to\file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('C:\path\to\file.test', '.test')); + + // mixed paths + $this->assertEquals('file.test', StringHelper::basename('/path\to/file.test')); + $this->assertEquals('file.test', StringHelper::basename('/path/to\file.test')); + $this->assertEquals('file.test', StringHelper::basename('\path/to\file.test')); + + // \ and / in suffix + $this->assertEquals('file', StringHelper::basename('/path/to/filete/st', 'te/st')); + $this->assertEquals('st', StringHelper::basename('/path/to/filete/st', 'te\st')); + $this->assertEquals('file', StringHelper::basename('/path/to/filete\st', 'te\st')); + $this->assertEquals('st', StringHelper::basename('/path/to/filete\st', 'te/st')); + + // http://www.php.net/manual/en/function.basename.php#72254 + $this->assertEquals('foo', StringHelper::basename('/bar/foo/')); + $this->assertEquals('foo', StringHelper::basename('\\bar\\foo\\')); + } +} diff --git a/tests/unit/framework/helpers/VarDumperTest.php b/tests/unit/framework/helpers/VarDumperTest.php new file mode 100644 index 0000000..a797121 --- /dev/null +++ b/tests/unit/framework/helpers/VarDumperTest.php @@ -0,0 +1,12 @@ +markTestSkipped(); + } +} diff --git a/tests/unit/framework/i18n/GettextMoFileTest.php b/tests/unit/framework/i18n/GettextMoFileTest.php new file mode 100644 index 0000000..0aa22da --- /dev/null +++ b/tests/unit/framework/i18n/GettextMoFileTest.php @@ -0,0 +1,95 @@ +load($moFilePath, 'context1'); + $context2 = $moFile->load($moFilePath, 'context2'); + + // item count + $this->assertCount(3, $context1); + $this->assertCount(2, $context2); + + // original messages + $this->assertArrayNotHasKey("Missing\n\r\t\"translation.", $context1); + $this->assertArrayHasKey("Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\naliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel malesuada.\nNunc vel sapien nunc, a pretium nulla.", $context1); + $this->assertArrayHasKey("String number two.", $context1); + $this->assertArrayHasKey("Nunc vel sapien nunc, a pretium nulla.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", $context1); + + $this->assertArrayHasKey("The other\n\ncontext.\n", $context2); + $this->assertArrayHasKey("test1\\ntest2\n\\\ntest3", $context2); + + // translated messages + $this->assertFalse(in_array("", $context1)); + $this->assertTrue(in_array("Олицетворение однократно. Представленный лексико-семантический анализ является\nпсихолингвистическим в своей основе, но механизм сочленений полидисперсен. Впечатление\nоднократно. Различное расположение выбирает сюжетный механизм сочленений.", $context1)); + $this->assertTrue(in_array('Строка номер два.', $context1)); + $this->assertTrue(in_array('Короткий перевод.', $context1)); + + $this->assertTrue(in_array("Другой\n\nконтекст.\n", $context2)); + $this->assertTrue(in_array("тест1\\nтест2\n\\\nтест3", $context2)); + } + + public function testSave() + { + // initial data + $s = chr(4); + $messages = array( + 'Hello!' => 'Привет!', + "context1{$s}Hello?" => 'Привет?', + 'Hello!?' => '', + "context1{$s}Hello!?!" => '', + "context2{$s}\"Quotes\"" => '"Кавычки"', + "context2{$s}\nNew lines\n" => "\nПереносы строк\n", + "context2{$s}\tTabs\t" => "\tТабы\t", + "context2{$s}\rCarriage returns\r" => "\rВозвраты кареток\r", + ); + + // create temporary directory and dump messages + $poFileDirectory = __DIR__ . '/../../runtime/i18n'; + if (!is_dir($poFileDirectory)) { + mkdir($poFileDirectory); + } + if (is_file($poFileDirectory . '/test.mo')) { + unlink($poFileDirectory . '/test.mo'); + } + + $moFile = new GettextMoFile(); + $moFile->save($poFileDirectory . '/test.mo', $messages); + + // load messages + $context1 = $moFile->load($poFileDirectory . '/test.mo', 'context1'); + $context2 = $moFile->load($poFileDirectory . '/test.mo', 'context2'); + + // context1 + $this->assertCount(2, $context1); + + $this->assertArrayHasKey('Hello?', $context1); + $this->assertTrue(in_array('Привет?', $context1)); + + $this->assertArrayHasKey('Hello!?!', $context1); + $this->assertTrue(in_array('', $context1)); + + // context2 + $this->assertCount(4, $context2); + + $this->assertArrayHasKey("\"Quotes\"", $context2); + $this->assertTrue(in_array('"Кавычки"', $context2)); + + $this->assertArrayHasKey("\nNew lines\n", $context2); + $this->assertTrue(in_array("\nПереносы строк\n", $context2)); + + $this->assertArrayHasKey("\tTabs\t", $context2); + $this->assertTrue(in_array("\tТабы\t", $context2)); + + $this->assertArrayHasKey("\rCarriage returns\r", $context2); + $this->assertTrue(in_array("\rВозвраты кареток\r", $context2)); + } +} diff --git a/tests/unit/framework/i18n/GettextPoFileTest.php b/tests/unit/framework/i18n/GettextPoFileTest.php new file mode 100644 index 0000000..8dddb40 --- /dev/null +++ b/tests/unit/framework/i18n/GettextPoFileTest.php @@ -0,0 +1,95 @@ +load($poFilePath, 'context1'); + $context2 = $poFile->load($poFilePath, 'context2'); + + // item count + $this->assertCount(4, $context1); + $this->assertCount(2, $context2); + + // original messages + $this->assertArrayHasKey("Missing\n\r\t\"translation.", $context1); + $this->assertArrayHasKey("Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\naliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel malesuada.\nNunc vel sapien nunc, a pretium nulla.", $context1); + $this->assertArrayHasKey("String number two.", $context1); + $this->assertArrayHasKey("Nunc vel sapien nunc, a pretium nulla.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", $context1); + + $this->assertArrayHasKey("The other\n\ncontext.\n", $context2); + $this->assertArrayHasKey("test1\\\ntest2\n\\\\\ntest3", $context2); + + // translated messages + $this->assertTrue(in_array("", $context1)); + $this->assertTrue(in_array("Олицетворение однократно. Представленный лексико-семантический анализ является\nпсихолингвистическим в своей основе, но механизм сочленений полидисперсен. Впечатление\nоднократно. Различное расположение выбирает сюжетный механизм сочленений.", $context1)); + $this->assertTrue(in_array('Строка номер два.', $context1)); + $this->assertTrue(in_array('Короткий перевод.', $context1)); + + $this->assertTrue(in_array("Другой\n\nконтекст.\n", $context2)); + $this->assertTrue(in_array("тест1\\\nтест2\n\\\\\nтест3", $context2)); + } + + public function testSave() + { + // initial data + $s = chr(4); + $messages = array( + 'Hello!' => 'Привет!', + "context1{$s}Hello?" => 'Привет?', + 'Hello!?' => '', + "context1{$s}Hello!?!" => '', + "context2{$s}\"Quotes\"" => '"Кавычки"', + "context2{$s}\nNew lines\n" => "\nПереносы строк\n", + "context2{$s}\tTabs\t" => "\tТабы\t", + "context2{$s}\rCarriage returns\r" => "\rВозвраты кареток\r", + ); + + // create temporary directory and dump messages + $poFileDirectory = __DIR__ . '/../../runtime/i18n'; + if (!is_dir($poFileDirectory)) { + mkdir($poFileDirectory); + } + if (is_file($poFileDirectory . '/test.po')) { + unlink($poFileDirectory . '/test.po'); + } + + $poFile = new GettextPoFile(); + $poFile->save($poFileDirectory . '/test.po', $messages); + + // load messages + $context1 = $poFile->load($poFileDirectory . '/test.po', 'context1'); + $context2 = $poFile->load($poFileDirectory . '/test.po', 'context2'); + + // context1 + $this->assertCount(2, $context1); + + $this->assertArrayHasKey('Hello?', $context1); + $this->assertTrue(in_array('Привет?', $context1)); + + $this->assertArrayHasKey('Hello!?!', $context1); + $this->assertTrue(in_array('', $context1)); + + // context2 + $this->assertCount(4, $context2); + + $this->assertArrayHasKey("\"Quotes\"", $context2); + $this->assertTrue(in_array('"Кавычки"', $context2)); + + $this->assertArrayHasKey("\nNew lines\n", $context2); + $this->assertTrue(in_array("\nПереносы строк\n", $context2)); + + $this->assertArrayHasKey("\tTabs\t", $context2); + $this->assertTrue(in_array("\tТабы\t", $context2)); + + $this->assertArrayHasKey("\rCarriage returns\r", $context2); + $this->assertTrue(in_array("\rВозвраты кареток\r", $context2)); + } +} diff --git a/tests/unit/framework/rbac/ManagerTestBase.php b/tests/unit/framework/rbac/ManagerTestBase.php new file mode 100644 index 0000000..03d5354 --- /dev/null +++ b/tests/unit/framework/rbac/ManagerTestBase.php @@ -0,0 +1,248 @@ +auth->createItem($name, $type, $description, $bizRule, $data); + $this->assertTrue($item instanceof Item); + $this->assertEquals($item->type, $type); + $this->assertEquals($item->name, $name); + $this->assertEquals($item->description, $description); + $this->assertEquals($item->bizRule, $bizRule); + $this->assertEquals($item->data, $data); + + // test shortcut + $name2 = 'createUser'; + $item2 = $this->auth->createRole($name2, $description, $bizRule, $data); + $this->assertEquals($item2->type, Item::TYPE_ROLE); + + // test adding an item with the same name + $this->setExpectedException('Exception'); + $this->auth->createItem($name, $type, $description, $bizRule, $data); + } + + public function testGetItem() + { + $this->assertTrue($this->auth->getItem('readPost') instanceof Item); + $this->assertTrue($this->auth->getItem('reader') instanceof Item); + $this->assertNull($this->auth->getItem('unknown')); + } + + public function testRemoveAuthItem() + { + $this->assertTrue($this->auth->getItem('updatePost') instanceof Item); + $this->assertTrue($this->auth->removeItem('updatePost')); + $this->assertNull($this->auth->getItem('updatePost')); + $this->assertFalse($this->auth->removeItem('updatePost')); + } + + public function testChangeItemName() + { + $item = $this->auth->getItem('readPost'); + $this->assertTrue($item instanceof Item); + $this->assertTrue($this->auth->hasItemChild('reader', 'readPost')); + $item->name = 'readPost2'; + $this->assertNull($this->auth->getItem('readPost')); + $this->assertEquals($this->auth->getItem('readPost2'), $item); + $this->assertFalse($this->auth->hasItemChild('reader', 'readPost')); + $this->assertTrue($this->auth->hasItemChild('reader', 'readPost2')); + } + + public function testAddItemChild() + { + $this->auth->addItemChild('createPost', 'updatePost'); + + // test adding upper level item to lower one + $this->setExpectedException('Exception'); + $this->auth->addItemChild('readPost', 'reader'); + } + + public function testAddItemChild2() + { + // test adding inexistent items + $this->setExpectedException('Exception'); + $this->assertFalse($this->auth->addItemChild('createPost2', 'updatePost')); + } + + public function testRemoveItemChild() + { + $this->assertTrue($this->auth->hasItemChild('reader', 'readPost')); + $this->assertTrue($this->auth->removeItemChild('reader', 'readPost')); + $this->assertFalse($this->auth->hasItemChild('reader', 'readPost')); + $this->assertFalse($this->auth->removeItemChild('reader', 'readPost')); + } + + public function testGetItemChildren() + { + $this->assertEquals(array(), $this->auth->getItemChildren('readPost')); + $children = $this->auth->getItemChildren('author'); + $this->assertEquals(3, count($children)); + $this->assertTrue(reset($children) instanceof Item); + } + + public function testAssign() + { + $auth = $this->auth->assign('new user', 'createPost', 'rule', 'data'); + $this->assertTrue($auth instanceof Assignment); + $this->assertEquals($auth->userId, 'new user'); + $this->assertEquals($auth->itemName, 'createPost'); + $this->assertEquals($auth->bizRule, 'rule'); + $this->assertEquals($auth->data, 'data'); + + $this->setExpectedException('Exception'); + $this->auth->assign('new user', 'createPost2', 'rule', 'data'); + } + + public function testRevoke() + { + $this->assertTrue($this->auth->isAssigned('author B', 'author')); + $auth = $this->auth->getAssignment('author B', 'author'); + $this->assertTrue($auth instanceof Assignment); + $this->assertTrue($this->auth->revoke('author B', 'author')); + $this->assertFalse($this->auth->isAssigned('author B', 'author')); + $this->assertFalse($this->auth->revoke('author B', 'author')); + } + + public function testGetAssignments() + { + $this->auth->assign('author B', 'deletePost'); + $auths = $this->auth->getAssignments('author B'); + $this->assertEquals(2, count($auths)); + $this->assertTrue(reset($auths) instanceof Assignment); + } + + public function testGetItems() + { + $this->assertEquals(count($this->auth->getRoles()), 4); + $this->assertEquals(count($this->auth->getOperations()), 4); + $this->assertEquals(count($this->auth->getTasks()), 1); + $this->assertEquals(count($this->auth->getItems()), 9); + + $this->assertEquals(count($this->auth->getItems('author B', null)), 1); + $this->assertEquals(count($this->auth->getItems('author C', null)), 0); + $this->assertEquals(count($this->auth->getItems('author B', Item::TYPE_ROLE)), 1); + $this->assertEquals(count($this->auth->getItems('author B', Item::TYPE_OPERATION)), 0); + } + + public function testClearAll() + { + $this->auth->clearAll(); + $this->assertEquals(count($this->auth->getRoles()), 0); + $this->assertEquals(count($this->auth->getOperations()), 0); + $this->assertEquals(count($this->auth->getTasks()), 0); + $this->assertEquals(count($this->auth->getItems()), 0); + $this->assertEquals(count($this->auth->getAssignments('author B')), 0); + } + + public function testClearAssignments() + { + $this->auth->clearAssignments(); + $this->assertEquals(count($this->auth->getAssignments('author B')), 0); + } + + public function testDetectLoop() + { + $this->setExpectedException('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)); + } + + public function testCheckAccess() + { + $results = array( + 'reader A' => array( + 'createPost' => false, + 'readPost' => true, + 'updatePost' => false, + 'updateOwnPost' => false, + 'deletePost' => false, + ), + 'author B' => array( + 'createPost' => true, + 'readPost' => true, + 'updatePost' => true, + 'updateOwnPost' => true, + 'deletePost' => false, + ), + 'editor C' => array( + 'createPost' => false, + 'readPost' => true, + 'updatePost' => true, + 'updateOwnPost' => false, + 'deletePost' => false, + ), + 'admin D' => array( + 'createPost' => true, + 'readPost' => true, + 'updatePost' => true, + 'updateOwnPost' => false, + 'deletePost' => true, + ), + ); + + $params = array('authorID' => 'author B'); + + foreach (array('reader A', 'author B', 'editor C', 'admin D') as $user) { + $params['userID'] = $user; + foreach (array('createPost', 'readPost', 'updatePost', 'updateOwnPost', 'deletePost') as $operation) { + $result = $this->auth->checkAccess($user, $operation, $params); + $this->assertEquals($results[$user][$operation], $result); + } + } + } + + protected function prepareData() + { + $this->auth->createOperation('createPost', 'create a post'); + $this->auth->createOperation('readPost', 'read a post'); + $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->addChild('updatePost'); + + $role = $this->auth->createRole('reader'); + $role->addChild('readPost'); + + $role = $this->auth->createRole('author'); + $role->addChild('reader'); + $role->addChild('createPost'); + $role->addChild('updateOwnPost'); + + $role = $this->auth->createRole('editor'); + $role->addChild('reader'); + $role->addChild('updatePost'); + + $role = $this->auth->createRole('admin'); + $role->addChild('editor'); + $role->addChild('author'); + $role->addChild('deletePost'); + + $this->auth->assign('reader A', 'reader'); + $this->auth->assign('author B', 'author'); + $this->auth->assign('editor C', 'editor'); + $this->auth->assign('admin D', 'admin'); + } +} diff --git a/tests/unit/framework/rbac/PhpManagerTest.php b/tests/unit/framework/rbac/PhpManagerTest.php new file mode 100644 index 0000000..69fdd41 --- /dev/null +++ b/tests/unit/framework/rbac/PhpManagerTest.php @@ -0,0 +1,34 @@ +getRuntimePath() . '/rbac.php'; + @unlink($authFile); + $this->auth = new PhpManager; + $this->auth->authFile = $authFile; + $this->auth->init(); + $this->prepareData(); + } + + public function tearDown() + { + @unlink($this->auth->authFile); + } + + public function testSaveLoad() + { + $this->auth->save(); + $this->auth->clearAll(); + $this->auth->load(); + $this->testCheckAccess(); + } +} diff --git a/tests/unit/framework/validators/EmailValidatorTest.php b/tests/unit/framework/validators/EmailValidatorTest.php index fbc2f53..512eb5c 100644 --- a/tests/unit/framework/validators/EmailValidatorTest.php +++ b/tests/unit/framework/validators/EmailValidatorTest.php @@ -25,4 +25,4 @@ class EmailValidatorTest extends TestCase $this->assertTrue($validator->validateValue('sam@rmcreative.ru')); $this->assertFalse($validator->validateValue('test@example.com')); } -} \ No newline at end of file +} diff --git a/tests/unit/phpunit.xml b/tests/unit/phpunit.xml deleted file mode 100644 index 17db94e..0000000 --- a/tests/unit/phpunit.xml +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/tests/web/app/index.php b/tests/web/app/index.php index 4cfa1ab..7096665 100644 --- a/tests/web/app/index.php +++ b/tests/web/app/index.php @@ -1,6 +1,6 @@ run(); diff --git a/tests/web/app/protected/config/main.php b/tests/web/app/protected/config/main.php index eed6d54..d5be5de 100644 --- a/tests/web/app/protected/config/main.php +++ b/tests/web/app/protected/config/main.php @@ -1,3 +1,3 @@ * @since 2.0 */ -class Yii extends YiiBase +class Yii extends \yii\YiiBase { } diff --git a/framework/YiiBase.php b/yii/YiiBase.php similarity index 96% rename from framework/YiiBase.php rename to yii/YiiBase.php index 9d501b1..1a3f50c 100644 --- a/framework/YiiBase.php +++ b/yii/YiiBase.php @@ -4,6 +4,8 @@ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ +namespace yii; + use yii\base\Exception; use yii\base\InvalidConfigException; use yii\base\InvalidParamException; @@ -60,7 +62,7 @@ class YiiBase */ public static $enableIncludePath = true; /** - * @var yii\console\Application|yii\web\Application the application instance + * @var \yii\console\Application|\yii\web\Application the application instance */ public static $app; /** @@ -156,8 +158,8 @@ class YiiBase { foreach ($namespaces as $name => $path) { if ($name !== '') { - $name = '@' . str_replace('\\', '/', $name); - static::setAlias($name, $path); + $name = trim(strtr($name, array('\\' => '/', '_' => '/')), '/'); + static::setAlias('@' . $name, rtrim($path, '/\\') . '/' . $name); } } } @@ -368,7 +370,8 @@ class YiiBase include($classFile); - if (class_exists($className, false) || interface_exists($className, false)) { + if (class_exists($className, false) || interface_exists($className, false) || + function_exists('trait_exists') && trait_exists($className, false)) { return true; } else { throw new UnknownClassException("Unable to find '$className' in file: $classFile"); @@ -449,12 +452,12 @@ class YiiBase } $args = func_get_args(); array_shift($args); // remove $config - if ($config !== array()) { + if (!empty($config)) { $args[] = $config; } return $reflection->newInstanceArgs($args); } else { - return $config === array() ? new $class : new $class($config); + return empty($config) ? new $class : new $class($config); } } @@ -600,6 +603,13 @@ class YiiBase */ public static function t($message, $params = array(), $language = null) { - return self::$app->getI18N()->translate($message, $params, $language); + if (self::$app !== null) { + return self::$app->getI18N()->translate($message, $params, $language); + } else { + if (strpos($message, '|') !== false && preg_match('/^([\w\-\\/\.\\\\]+)\|(.*)/', $message, $matches)) { + $message = $matches[2]; + } + return is_array($params) ? strtr($message, $params) : $message; + } } } diff --git a/framework/assets.php b/yii/assets.php similarity index 66% rename from framework/assets.php rename to yii/assets.php index 5cea992..7ee177d 100644 --- a/framework/assets.php +++ b/yii/assets.php @@ -28,4 +28,18 @@ return array( ), 'depends' => array('yii', 'yii/validation'), ), -); \ No newline at end of file + '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/framework/assets/jquery.min.js b/yii/assets/jquery.min.js similarity index 100% rename from framework/assets/jquery.min.js rename to yii/assets/jquery.min.js diff --git a/framework/assets/yii.activeForm.js b/yii/assets/yii.activeForm.js similarity index 99% rename from framework/assets/yii.activeForm.js rename to yii/assets/yii.activeForm.js index 158ea74..d987879 100644 --- a/framework/assets/yii.activeForm.js +++ b/yii/assets/yii.activeForm.js @@ -116,8 +116,8 @@ }); }, - options: function() { - return this.data('yiiActiveForm').settings; + data: function() { + return this.data('yiiActiveForm'); }, submitForm: function () { @@ -384,4 +384,4 @@ } }; -})(window.jQuery); \ No newline at end of file +})(window.jQuery); diff --git a/yii/assets/yii.captcha.js b/yii/assets/yii.captcha.js new file mode 100644 index 0000000..9211edb --- /dev/null +++ b/yii/assets/yii.captcha.js @@ -0,0 +1,72 @@ +/** + * Yii Captcha widget. + * + * This is the JavaScript widget used by the yii\widgets\Captcha widget. + * + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @author Qiang Xue + * @since 2.0 + */ +(function ($) { + $.fn.yiiCaptcha = 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.yiiCaptcha'); + return false; + } + }; + + var defaults = { + refreshUrl: undefined, + hashKey: undefined + }; + + var methods = { + init: function (options) { + return this.each(function () { + var $e = $(this); + var settings = $.extend({}, defaults, options || {}); + $e.data('yiiCaptcha', { + settings: settings + }); + + $e.on('click.yiiCaptcha', function() { + methods.refresh.apply($e); + return false; + }); + + }); + }, + + refresh: function () { + var $e = this, + settings = this.data('yiiCaptcha').settings; + $.ajax({ + url: $e.data('yiiCaptcha').settings.refreshUrl, + dataType: 'json', + cache: false, + success: function(data) { + $e.attr('src', data['url']); + $('body').data(settings.hashKey, [data['hash1'], data['hash2']]); + } + }); + }, + + destroy: function () { + return this.each(function () { + $(window).unbind('.yiiCaptcha'); + $(this).removeData('yiiCaptcha'); + }); + }, + + data: function() { + return this.data('yiiCaptcha'); + } + }; +})(window.jQuery); + diff --git a/yii/assets/yii.debug.js b/yii/assets/yii.debug.js new file mode 100644 index 0000000..4e32d89 --- /dev/null +++ b/yii/assets/yii.debug.js @@ -0,0 +1,26 @@ +/** + * 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 + * @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/framework/assets/yii.js b/yii/assets/yii.js similarity index 100% rename from framework/assets/yii.js rename to yii/assets/yii.js diff --git a/framework/assets/yii.validation.js b/yii/assets/yii.validation.js similarity index 99% rename from framework/assets/yii.validation.js rename to yii/assets/yii.validation.js index fd098be..5fa8492 100644 --- a/framework/assets/yii.validation.js +++ b/yii/assets/yii.validation.js @@ -1,7 +1,7 @@ /** * Yii validation module. * - * This JavaScript module provides the validation methods for the built-in validaotrs. + * This JavaScript module provides the validation methods for the built-in validators. * * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC diff --git a/framework/base/Action.php b/yii/base/Action.php similarity index 100% rename from framework/base/Action.php rename to yii/base/Action.php diff --git a/framework/base/ActionEvent.php b/yii/base/ActionEvent.php similarity index 100% rename from framework/base/ActionEvent.php rename to yii/base/ActionEvent.php diff --git a/framework/base/ActionFilter.php b/yii/base/ActionFilter.php similarity index 99% rename from framework/base/ActionFilter.php rename to yii/base/ActionFilter.php index 1f82e5d..d69c0fe 100644 --- a/framework/base/ActionFilter.php +++ b/yii/base/ActionFilter.php @@ -87,4 +87,4 @@ class ActionFilter extends Behavior { return !in_array($action->id, $this->except, true) && (empty($this->only) || in_array($action->id, $this->only, true)); } -} \ No newline at end of file +} diff --git a/framework/base/Application.php b/yii/base/Application.php similarity index 95% rename from framework/base/Application.php rename to yii/base/Application.php index 6dca5cf..fb9a6c1 100644 --- a/framework/base/Application.php +++ b/yii/base/Application.php @@ -8,7 +8,6 @@ namespace yii\base; use Yii; -use yii\helpers\FileHelper; /** * Application is the base class for all application classes. @@ -85,6 +84,13 @@ class Application extends Module } else { throw new InvalidConfigException('The "basePath" configuration is required.'); } + + if (isset($config['timeZone'])) { + $this->setTimeZone($config['timeZone']); + unset($config['timeZone']); + } elseif (!ini_get('date.timezone')) { + $this->setTimeZone('UTC'); + } $this->registerErrorHandlers(); $this->registerCoreComponents(); @@ -223,6 +229,8 @@ class Application extends Module /** * Returns the time zone used by this application. * This is a simple wrapper of PHP function date_default_timezone_get(). + * If time zone is not configured in php.ini or application config, + * it will be set to UTC by default. * @return string the time zone used by this application. * @see http://php.net/manual/en/function.date-default-timezone-get.php */ @@ -306,6 +314,15 @@ class Application extends Module } /** + * Returns the auth manager for this application. + * @return \yii\rbac\Manager the auth manager for this application. + */ + public function getAuthManager() + { + return $this->getComponent('authManager'); + } + + /** * Registers the core application components. * @see setComponents */ diff --git a/framework/base/Behavior.php b/yii/base/Behavior.php similarity index 100% rename from framework/base/Behavior.php rename to yii/base/Behavior.php diff --git a/framework/base/Component.php b/yii/base/Component.php similarity index 99% rename from framework/base/Component.php rename to yii/base/Component.php index 80259e7..8e75835 100644 --- a/framework/base/Component.php +++ b/yii/base/Component.php @@ -90,6 +90,7 @@ class Component extends Object // as behavior: attach behavior $name = trim(substr($name, 3)); $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); + return; } else { // behavior property $this->ensureBehaviors(); @@ -496,6 +497,7 @@ class Component extends Object */ public function detachBehavior($name) { + $this->ensureBehaviors(); if (isset($this->_behaviors[$name])) { $behavior = $this->_behaviors[$name]; unset($this->_behaviors[$name]); @@ -511,6 +513,7 @@ class Component extends Object */ public function detachBehaviors() { + $this->ensureBehaviors(); if ($this->_behaviors !== null) { foreach ($this->_behaviors as $name => $behavior) { $this->detachBehavior($name); diff --git a/framework/base/Controller.php b/yii/base/Controller.php similarity index 99% rename from framework/base/Controller.php rename to yii/base/Controller.php index 583de60..3b16a93 100644 --- a/framework/base/Controller.php +++ b/yii/base/Controller.php @@ -8,7 +8,6 @@ namespace yii\base; use Yii; -use yii\helpers\FileHelper; use yii\helpers\StringHelper; /** @@ -183,7 +182,7 @@ class Controller extends Component } } - if ($missing !== array()) { + if (!empty($missing)) { throw new InvalidRequestException(Yii::t('yii|Missing required parameters: {params}', array( '{params}' => implode(', ', $missing), ))); @@ -204,7 +203,7 @@ class Controller extends Component public function forward($route, $params = array()) { $status = $this->run($route, $params); - exit($status); + Yii::$app->end($status); } /** diff --git a/framework/base/Dictionary.php b/yii/base/Dictionary.php similarity index 99% rename from framework/base/Dictionary.php rename to yii/base/Dictionary.php index 52262cb..5807d93 100644 --- a/framework/base/Dictionary.php +++ b/yii/base/Dictionary.php @@ -51,7 +51,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co */ public function __construct($data = array(), $config = array()) { - if ($data !== array()) { + if (!empty($data)) { $this->copyFrom($data); } parent::__construct($config); @@ -187,7 +187,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co public function copyFrom($data) { if (is_array($data) || $data instanceof \Traversable) { - if ($this->_d !== array()) { + if (!empty($this->_d)) { $this->removeAll(); } if ($data instanceof self) { diff --git a/framework/base/DictionaryIterator.php b/yii/base/DictionaryIterator.php similarity index 100% rename from framework/base/DictionaryIterator.php rename to yii/base/DictionaryIterator.php diff --git a/framework/base/ErrorException.php b/yii/base/ErrorException.php similarity index 100% rename from framework/base/ErrorException.php rename to yii/base/ErrorException.php diff --git a/framework/base/ErrorHandler.php b/yii/base/ErrorHandler.php similarity index 100% rename from framework/base/ErrorHandler.php rename to yii/base/ErrorHandler.php diff --git a/framework/base/Event.php b/yii/base/Event.php similarity index 100% rename from framework/base/Event.php rename to yii/base/Event.php diff --git a/framework/base/Exception.php b/yii/base/Exception.php similarity index 99% rename from framework/base/Exception.php rename to yii/base/Exception.php index 9ee698b..7d26bd0 100644 --- a/framework/base/Exception.php +++ b/yii/base/Exception.php @@ -22,4 +22,4 @@ class Exception extends \Exception { return \Yii::t('yii|Exception'); } -} \ No newline at end of file +} diff --git a/framework/base/HttpException.php b/yii/base/HttpException.php similarity index 100% rename from framework/base/HttpException.php rename to yii/base/HttpException.php diff --git a/framework/base/InlineAction.php b/yii/base/InlineAction.php similarity index 100% rename from framework/base/InlineAction.php rename to yii/base/InlineAction.php diff --git a/framework/base/InvalidCallException.php b/yii/base/InvalidCallException.php similarity index 100% rename from framework/base/InvalidCallException.php rename to yii/base/InvalidCallException.php diff --git a/framework/base/InvalidConfigException.php b/yii/base/InvalidConfigException.php similarity index 100% rename from framework/base/InvalidConfigException.php rename to yii/base/InvalidConfigException.php diff --git a/framework/base/InvalidParamException.php b/yii/base/InvalidParamException.php similarity index 100% rename from framework/base/InvalidParamException.php rename to yii/base/InvalidParamException.php diff --git a/framework/base/InvalidRequestException.php b/yii/base/InvalidRequestException.php similarity index 100% rename from framework/base/InvalidRequestException.php rename to yii/base/InvalidRequestException.php diff --git a/framework/base/InvalidRouteException.php b/yii/base/InvalidRouteException.php similarity index 100% rename from framework/base/InvalidRouteException.php rename to yii/base/InvalidRouteException.php diff --git a/framework/base/Model.php b/yii/base/Model.php similarity index 100% rename from framework/base/Model.php rename to yii/base/Model.php diff --git a/framework/base/ModelEvent.php b/yii/base/ModelEvent.php similarity index 100% rename from framework/base/ModelEvent.php rename to yii/base/ModelEvent.php diff --git a/framework/base/Module.php b/yii/base/Module.php similarity index 100% rename from framework/base/Module.php rename to yii/base/Module.php diff --git a/framework/base/NotSupportedException.php b/yii/base/NotSupportedException.php similarity index 100% rename from framework/base/NotSupportedException.php rename to yii/base/NotSupportedException.php diff --git a/framework/base/Object.php b/yii/base/Object.php similarity index 98% rename from framework/base/Object.php rename to yii/base/Object.php index a547990..a90a231 100644 --- a/framework/base/Object.php +++ b/yii/base/Object.php @@ -15,6 +15,14 @@ namespace yii\base; class Object { /** + * @return string the fully qualified name of this class. + */ + public static function className() + { + return get_called_class(); + } + + /** * Constructor. * The default implementation does two things: * diff --git a/framework/base/Request.php b/yii/base/Request.php similarity index 100% rename from framework/base/Request.php rename to yii/base/Request.php diff --git a/framework/base/Response.php b/yii/base/Response.php similarity index 100% rename from framework/base/Response.php rename to yii/base/Response.php diff --git a/framework/base/Theme.php b/yii/base/Theme.php similarity index 100% rename from framework/base/Theme.php rename to yii/base/Theme.php diff --git a/framework/base/UnknownClassException.php b/yii/base/UnknownClassException.php similarity index 100% rename from framework/base/UnknownClassException.php rename to yii/base/UnknownClassException.php diff --git a/framework/base/UnknownMethodException.php b/yii/base/UnknownMethodException.php similarity index 100% rename from framework/base/UnknownMethodException.php rename to yii/base/UnknownMethodException.php diff --git a/framework/base/UnknownPropertyException.php b/yii/base/UnknownPropertyException.php similarity index 100% rename from framework/base/UnknownPropertyException.php rename to yii/base/UnknownPropertyException.php diff --git a/framework/base/UserException.php b/yii/base/UserException.php similarity index 100% rename from framework/base/UserException.php rename to yii/base/UserException.php diff --git a/framework/base/Vector.php b/yii/base/Vector.php similarity index 99% rename from framework/base/Vector.php rename to yii/base/Vector.php index 7d43fdb..6418077 100644 --- a/framework/base/Vector.php +++ b/yii/base/Vector.php @@ -58,7 +58,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta */ public function __construct($data = array(), $config = array()) { - if ($data !== array()) { + if (!empty($data)) { $this->copyFrom($data); } parent::__construct($config); diff --git a/framework/base/VectorIterator.php b/yii/base/VectorIterator.php similarity index 100% rename from framework/base/VectorIterator.php rename to yii/base/VectorIterator.php diff --git a/framework/base/View.php b/yii/base/View.php similarity index 96% rename from framework/base/View.php rename to yii/base/View.php index a72982e..af65c49 100644 --- a/framework/base/View.php +++ b/yii/base/View.php @@ -23,6 +23,14 @@ use yii\helpers\Html; class View extends Component { /** + * @event ViewEvent an event that is triggered by [[beginPage()]]. + */ + const EVENT_BEGIN_PAGE = 'beginPage'; + /** + * @event ViewEvent an event that is triggered by [[endPage()]]. + */ + const EVENT_END_PAGE = 'endPage'; + /** * @event ViewEvent an event that is triggered by [[renderFile()]] right before it renders a view file. */ const EVENT_BEFORE_RENDER = 'beforeRender'; @@ -72,23 +80,21 @@ class View extends Component /** * @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. - * For example, - * - * ~~~ - * array( - * 'tpl' => array( - * 'class' => 'yii\renderers\SmartyRenderer', - * ), - * 'twig' => array( - * 'class' => 'yii\renderers\TwigRenderer', - * ), - * ) - * ~~~ + * 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. * * 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(); + public $renderers = array( + 'tpl' => array( + 'class' => 'yii\renderers\SmartyRenderer', + ), + 'twig' => array( + 'class' => 'yii\renderers\TwigRenderer', + ), + ); /** * @var Theme|array the theme object or the configuration array for creating the theme object. * If not set, it means theming is not enabled. @@ -557,6 +563,8 @@ class View extends Component { ob_start(); ob_implicit_flush(false); + + $this->trigger(self::EVENT_BEGIN_PAGE); } /** @@ -564,6 +572,8 @@ class View extends Component */ public function endPage() { + $this->trigger(self::EVENT_END_PAGE); + $content = ob_get_clean(); echo strtr($content, array( self::PL_HEAD => $this->renderHeadHtml(), @@ -744,10 +754,10 @@ class View extends Component { $lines = array(); if (!empty($this->metaTags)) { - $lines[] = implode("\n", $this->cssFiles); + $lines[] = implode("\n", $this->metaTags); } if (!empty($this->linkTags)) { - $lines[] = implode("\n", $this->cssFiles); + $lines[] = implode("\n", $this->linkTags); } if (!empty($this->cssFiles)) { $lines[] = implode("\n", $this->cssFiles); @@ -797,4 +807,4 @@ class View extends Component } return implode("\n", $lines); } -} \ No newline at end of file +} diff --git a/framework/base/ViewEvent.php b/yii/base/ViewEvent.php similarity index 99% rename from framework/base/ViewEvent.php rename to yii/base/ViewEvent.php index cac7be4..f1ee7b9 100644 --- a/framework/base/ViewEvent.php +++ b/yii/base/ViewEvent.php @@ -41,4 +41,4 @@ class ViewEvent extends Event $this->viewFile = $viewFile; parent::__construct($config); } -} \ No newline at end of file +} diff --git a/framework/base/ViewRenderer.php b/yii/base/ViewRenderer.php similarity index 100% rename from framework/base/ViewRenderer.php rename to yii/base/ViewRenderer.php diff --git a/framework/base/Widget.php b/yii/base/Widget.php similarity index 97% rename from framework/base/Widget.php rename to yii/base/Widget.php index 13e6d30..c0c524f 100644 --- a/framework/base/Widget.php +++ b/yii/base/Widget.php @@ -83,7 +83,8 @@ class Widget extends Component */ public function render($view, $params = array()) { - return $this->view->render($view, $params, $this); + $viewFile = $this->findViewFile($view); + return $this->view->renderFile($viewFile, $params, $this); } /** @@ -133,4 +134,4 @@ class Widget extends Component return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file; } -} \ No newline at end of file +} diff --git a/framework/caching/ApcCache.php b/yii/caching/ApcCache.php similarity index 100% rename from framework/caching/ApcCache.php rename to yii/caching/ApcCache.php diff --git a/framework/caching/Cache.php b/yii/caching/Cache.php similarity index 99% rename from framework/caching/Cache.php rename to yii/caching/Cache.php index fb56d5e..78f2854 100644 --- a/framework/caching/Cache.php +++ b/yii/caching/Cache.php @@ -349,4 +349,4 @@ abstract class Cache extends Component implements \ArrayAccess { $this->delete($key); } -} \ No newline at end of file +} diff --git a/framework/caching/ChainedDependency.php b/yii/caching/ChainedDependency.php similarity index 100% rename from framework/caching/ChainedDependency.php rename to yii/caching/ChainedDependency.php diff --git a/framework/caching/DbCache.php b/yii/caching/DbCache.php similarity index 100% rename from framework/caching/DbCache.php rename to yii/caching/DbCache.php diff --git a/framework/caching/DbDependency.php b/yii/caching/DbDependency.php similarity index 100% rename from framework/caching/DbDependency.php rename to yii/caching/DbDependency.php diff --git a/framework/caching/Dependency.php b/yii/caching/Dependency.php similarity index 99% rename from framework/caching/Dependency.php rename to yii/caching/Dependency.php index feb8c07..d1428fc 100644 --- a/framework/caching/Dependency.php +++ b/yii/caching/Dependency.php @@ -49,4 +49,4 @@ abstract class Dependency extends \yii\base\Object * @return mixed the data needed to determine if dependency has been changed. */ abstract protected function generateDependencyData(); -} \ No newline at end of file +} diff --git a/framework/caching/DummyCache.php b/yii/caching/DummyCache.php similarity index 100% rename from framework/caching/DummyCache.php rename to yii/caching/DummyCache.php diff --git a/framework/caching/ExpressionDependency.php b/yii/caching/ExpressionDependency.php similarity index 100% rename from framework/caching/ExpressionDependency.php rename to yii/caching/ExpressionDependency.php diff --git a/framework/caching/FileCache.php b/yii/caching/FileCache.php similarity index 100% rename from framework/caching/FileCache.php rename to yii/caching/FileCache.php diff --git a/framework/caching/FileDependency.php b/yii/caching/FileDependency.php similarity index 100% rename from framework/caching/FileDependency.php rename to yii/caching/FileDependency.php diff --git a/framework/caching/MemCache.php b/yii/caching/MemCache.php similarity index 100% rename from framework/caching/MemCache.php rename to yii/caching/MemCache.php diff --git a/framework/caching/MemCacheServer.php b/yii/caching/MemCacheServer.php similarity index 99% rename from framework/caching/MemCacheServer.php rename to yii/caching/MemCacheServer.php index 105137e..dc0de08 100644 --- a/framework/caching/MemCacheServer.php +++ b/yii/caching/MemCacheServer.php @@ -46,4 +46,4 @@ 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; -} \ No newline at end of file +} diff --git a/framework/caching/WinCache.php b/yii/caching/WinCache.php similarity index 99% rename from framework/caching/WinCache.php rename to yii/caching/WinCache.php index ee6b4a9..4e07c7f 100644 --- a/framework/caching/WinCache.php +++ b/yii/caching/WinCache.php @@ -89,4 +89,4 @@ class WinCache extends Cache { return wincache_ucache_clear(); } -} \ No newline at end of file +} diff --git a/framework/caching/XCache.php b/yii/caching/XCache.php similarity index 100% rename from framework/caching/XCache.php rename to yii/caching/XCache.php diff --git a/framework/caching/ZendDataCache.php b/yii/caching/ZendDataCache.php similarity index 100% rename from framework/caching/ZendDataCache.php rename to yii/caching/ZendDataCache.php diff --git a/framework/console/Application.php b/yii/console/Application.php similarity index 100% rename from framework/console/Application.php rename to yii/console/Application.php diff --git a/framework/console/Controller.php b/yii/console/Controller.php similarity index 53% rename from framework/console/Controller.php rename to yii/console/Controller.php index c7c5642..c07d92d 100644 --- a/framework/console/Controller.php +++ b/yii/console/Controller.php @@ -11,6 +11,7 @@ use Yii; use yii\base\Action; use yii\base\InlineAction; use yii\base\InvalidRouteException; +use yii\helpers\Console; /** * Controller is the base class of console command classes. @@ -35,6 +36,14 @@ class Controller extends \yii\base\Controller 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()]]. + */ + public $colors; + + /** * Runs an action 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. @@ -45,7 +54,7 @@ class Controller extends \yii\base\Controller */ public function runAction($id, $params = array()) { - if ($params !== array()) { + if (!empty($params)) { $options = $this->globalOptions(); foreach ($params as $name => $value) { if (in_array($name, $options, true)) { @@ -69,7 +78,7 @@ class Controller extends \yii\base\Controller */ public function bindActionParams($action, $params) { - if ($params !== array()) { + if (!empty($params)) { $options = $this->globalOptions(); foreach ($params as $name => $value) { if (in_array($name, $options, true)) { @@ -81,7 +90,7 @@ class Controller extends \yii\base\Controller $args = isset($params[Request::ANONYMOUS_PARAMS]) ? $params[Request::ANONYMOUS_PARAMS] : array(); unset($params[Request::ANONYMOUS_PARAMS]); - if ($params !== array()) { + if (!empty($params)) { throw new Exception(Yii::t('yii|Unknown options: {params}', array( '{params}' => implode(', ', array_keys($params)), ))); @@ -105,7 +114,7 @@ class Controller extends \yii\base\Controller } } - if ($missing !== array()) { + if (!empty($missing)) { throw new Exception(Yii::t('yii|Missing required arguments: {params}', array( '{params}' => implode(', ', $missing), ))); @@ -115,6 +124,99 @@ 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]]. + * + * Example: + * ~~~ + * $this->ansiFormat('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); + * ~~~ + * + * @param string $string the string to be formatted + * @return string + */ + public function ansiFormat($string) + { + if ($this->ansi === true || $this->ansi === null && Console::streamSupportsAnsiColors(STDOUT)) { + $args = func_get_args(); + array_shift($args); + $string = Console::ansiFormat($string, $args); + } + return $string; + } + + /** + * 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]]. + * + * Example: + * ~~~ + * $this->stdout('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); + * ~~~ + * + * @param string $string the string to print + * @return int|boolean Number of bytes printed or false on error + */ + public function stdout($string) + { + if ($this->ansi === true || $this->ansi === null && Console::streamSupportsAnsiColors(STDOUT)) { + $args = func_get_args(); + array_shift($args); + $string = Console::ansiFormat($string, $args); + } + return Console::stdout($string); + } + + /** + * 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]]. + * + * Example: + * ~~~ + * $this->stderr('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); + * ~~~ + * + * @param string $string the string to print + * @return int|boolean Number of bytes printed or false on error + */ + public function stderr($string) + { + if ($this->ansi === true || $this->ansi === null && Console::streamSupportsAnsiColors(STDERR)) { + $args = func_get_args(); + array_shift($args); + $string = Console::ansiFormat($string, $args); + } + return fwrite(STDERR, $string); + } + + /** + * Prompts the user for input and validates it + * + * @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 + * - validator: a callable function to validate input. The function must accept two parameters: + * - $input: the user input to validate + * - $error: the error value passed by reference if validation failed. + * @return string the user input + */ + public function prompt($text, $options = array()) + { + if ($this->interactive) { + return Console::prompt($text, $options); + } else { + return isset($options['default']) ? $options['default'] : ''; + } + } + + /** * Asks user to confirm by typing y or n. * * @param string $message to echo out before waiting for user input @@ -124,15 +226,27 @@ class Controller extends \yii\base\Controller public function confirm($message, $default = false) { if ($this->interactive) { - echo $message . ' (yes|no) [' . ($default ? 'yes' : 'no') . ']:'; - $input = trim(fgets(STDIN)); - return empty($input) ? $default : !strncasecmp($input, 'y', 1); + return Console::confirm($message, $default); } else { return true; } } /** + * Gives the user an option to choose from. Giving '?' as an input will show + * a list of options to choose from and their explanations. + * + * @param string $prompt the prompt message + * @param array $options Key-value array of options to choose from + * + * @return string An option character the user chose + */ + public function select($prompt, $options = array()) + { + return Console::select($prompt, $options); + } + + /** * Returns the names of the global options for this command. * A global option requires the existence of a public member variable whose * name is the option name. @@ -147,4 +261,4 @@ class Controller extends \yii\base\Controller { return array(); } -} \ No newline at end of file +} diff --git a/framework/console/Exception.php b/yii/console/Exception.php similarity index 100% rename from framework/console/Exception.php rename to yii/console/Exception.php diff --git a/framework/console/Request.php b/yii/console/Request.php similarity index 100% rename from framework/console/Request.php rename to yii/console/Request.php diff --git a/framework/console/controllers/AppController.php b/yii/console/controllers/AppController.php similarity index 99% rename from framework/console/controllers/AppController.php rename to yii/console/controllers/AppController.php index a47acfe..17f7420 100644 --- a/framework/console/controllers/AppController.php +++ b/yii/console/controllers/AppController.php @@ -321,4 +321,4 @@ class AppController extends Controller closedir($handle); return $list; } -} \ No newline at end of file +} diff --git a/framework/console/controllers/AssetController.php b/yii/console/controllers/AssetController.php similarity index 99% rename from framework/console/controllers/AssetController.php rename to yii/console/controllers/AssetController.php index 71a2cae..aab489b 100644 --- a/framework/console/controllers/AssetController.php +++ b/yii/console/controllers/AssetController.php @@ -350,4 +350,4 @@ return array( EOD; file_put_contents($configFile, $template); } -} \ No newline at end of file +} diff --git a/framework/console/controllers/CacheController.php b/yii/console/controllers/CacheController.php similarity index 100% rename from framework/console/controllers/CacheController.php rename to yii/console/controllers/CacheController.php diff --git a/framework/console/controllers/HelpController.php b/yii/console/controllers/HelpController.php similarity index 99% rename from framework/console/controllers/HelpController.php rename to yii/console/controllers/HelpController.php index 74c354b..6a66fd0 100644 --- a/framework/console/controllers/HelpController.php +++ b/yii/console/controllers/HelpController.php @@ -142,7 +142,7 @@ class HelpController extends Controller protected function getHelp() { $commands = $this->getCommands(); - if ($commands !== array()) { + if (!empty($commands)) { echo "The following commands are available:\n\n"; foreach ($commands as $command) { echo "* $command\n"; @@ -172,7 +172,7 @@ class HelpController extends Controller } $actions = $this->getActions($controller); - if ($actions !== array()) { + if (!empty($actions)) { echo "\nSUB-COMMANDS\n\n"; $prefix = $controller->getUniqueId(); foreach ($actions as $action) { @@ -280,7 +280,7 @@ class HelpController extends Controller } $options = $this->getOptionHelps($controller); - if ($options !== array()) { + if (!empty($options)) { echo "\nOPTIONS\n\n"; echo implode("\n\n", $options) . "\n\n"; } @@ -418,4 +418,4 @@ class HelpController extends Controller $name = $required ? "$name (required)" : $name; return $doc === '' ? $name : "$name: $doc"; } -} \ No newline at end of file +} diff --git a/framework/console/controllers/MessageController.php b/yii/console/controllers/MessageController.php similarity index 100% rename from framework/console/controllers/MessageController.php rename to yii/console/controllers/MessageController.php diff --git a/framework/console/controllers/MigrateController.php b/yii/console/controllers/MigrateController.php similarity index 95% rename from framework/console/controllers/MigrateController.php rename to yii/console/controllers/MigrateController.php index 3f816f1..fb06c66 100644 --- a/framework/console/controllers/MigrateController.php +++ b/yii/console/controllers/MigrateController.php @@ -144,7 +144,8 @@ class MigrateController extends Controller */ public function actionUp($limit = 0) { - if (($migrations = $this->getNewMigrations()) === array()) { + $migrations = $this->getNewMigrations(); + if (empty($migrations)) { echo "No new migration found. Your system is up-to-date.\n"; Yii::$app->end(); } @@ -198,7 +199,8 @@ class MigrateController extends Controller throw new Exception("The step argument must be greater than 0."); } - if (($migrations = $this->getMigrationHistory($limit)) === array()) { + $migrations = $this->getMigrationHistory($limit); + if (empty($migrations)) { echo "No migration has been done before.\n"; return; } @@ -244,7 +246,8 @@ class MigrateController extends Controller throw new Exception("The step argument must be greater than 0."); } - if (($migrations = $this->getMigrationHistory($limit)) === array()) { + $migrations = $this->getMigrationHistory($limit); + if (empty($migrations)) { echo "No migration has been done before.\n"; return; } @@ -407,7 +410,7 @@ class MigrateController extends Controller { $limit = (int)$limit; $migrations = $this->getMigrationHistory($limit); - if ($migrations === array()) { + if (empty($migrations)) { echo "No migration has been done before.\n"; } else { $n = count($migrations); @@ -441,7 +444,7 @@ class MigrateController extends Controller { $limit = (int)$limit; $migrations = $this->getNewMigrations(); - if ($migrations === array()) { + if (empty($migrations)) { echo "No new migrations found. Your system is up-to-date.\n"; } else { $n = count($migrations); diff --git a/framework/console/runtime/.gitignore b/yii/console/runtime/.gitignore similarity index 100% rename from framework/console/runtime/.gitignore rename to yii/console/runtime/.gitignore diff --git a/framework/db/ActiveQuery.php b/yii/db/ActiveQuery.php similarity index 99% rename from framework/db/ActiveQuery.php rename to yii/db/ActiveQuery.php index 43c3059..dac94c8 100644 --- a/framework/db/ActiveQuery.php +++ b/yii/db/ActiveQuery.php @@ -88,7 +88,8 @@ class ActiveQuery extends Query { if (method_exists($this->modelClass, $name)) { array_unshift($params, $this); - return call_user_func_array(array($this->modelClass, $name), $params); + call_user_func_array(array($this->modelClass, $name), $params); + return $this; } else { return parent::__call($name, $params); } @@ -102,7 +103,7 @@ class ActiveQuery extends Query { $command = $this->createCommand(); $rows = $command->queryAll(); - if ($rows !== array()) { + if (!empty($rows)) { $models = $this->createModels($rows); if (!empty($this->with)) { $this->populateRelations($models, $this->with); diff --git a/framework/db/ActiveRecord.php b/yii/db/ActiveRecord.php similarity index 95% rename from framework/db/ActiveRecord.php rename to yii/db/ActiveRecord.php index 45c53fb..356819d 100644 --- a/framework/db/ActiveRecord.php +++ b/yii/db/ActiveRecord.php @@ -8,6 +8,7 @@ namespace yii\db; +use yii\base\InvalidConfigException; use yii\base\Model; use yii\base\InvalidParamException; use yii\base\ModelEvent; @@ -27,7 +28,7 @@ use yii\helpers\StringHelper; * @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 $isPrimaryKey whether the record is new and should be inserted when calling [[save()]]. + * @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. * @@ -112,6 +113,7 @@ class ActiveRecord extends Model * @return ActiveQuery|ActiveRecord|null When `$q` is null, a new [[ActiveQuery]] instance * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be * returned (null will be returned if there is no matching). + * @throws InvalidConfigException if the AR class does not have a primary key * @see createQuery() */ public static function find($q = null) @@ -122,7 +124,11 @@ class ActiveRecord extends Model } elseif ($q !== null) { // query by primary key $primaryKey = static::primaryKey(); - return $query->where(array($primaryKey[0] => $q))->one(); + if (isset($primaryKey[0])) { + return $query->where(array($primaryKey[0] => $q))->one(); + } else { + throw new InvalidConfigException(get_called_class() . ' must have a primary key.'); + } } return $query; } @@ -246,7 +252,7 @@ class ActiveRecord extends Model */ public static function tableName() { - return 'tbl_' . StringHelper::camel2id(basename(get_called_class()), '_'); + return 'tbl_' . StringHelper::camel2id(StringHelper::basename(get_called_class()), '_'); } /** @@ -661,36 +667,33 @@ class ActiveRecord extends Model */ public function insert($runValidation = true, $attributes = null) { - if ($runValidation && !$this->validate($attributes)) { + if ($runValidation && !$this->validate($attributes) || !$this->beforeSave(true)) { return false; } - if ($this->beforeSave(true)) { - $values = $this->getDirtyAttributes($attributes); - if ($values === array()) { - foreach ($this->primaryKey() as $key) { - $values[$key] = isset($this->_attributes[$key]) ? $this->_attributes[$key] : null; - } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + foreach ($this->primaryKey() as $key) { + $values[$key] = isset($this->_attributes[$key]) ? $this->_attributes[$key] : null; } - $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; - } + } + $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; } } - 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; } - return false; } /** @@ -744,39 +747,35 @@ class ActiveRecord extends Model */ public function update($runValidation = true, $attributes = null) { - if ($runValidation && !$this->validate($attributes)) { + if ($runValidation && !$this->validate($attributes) || !$this->beforeSave(false)) { return false; } - if ($this->beforeSave(false)) { - $values = $this->getDirtyAttributes($attributes); - if ($values !== array()) { - $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); - - if ($lock !== null && !$rows) { - throw new StaleObjectException('The object being updated is outdated.'); + $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; } + $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; } else { - return false; + return 0; } } @@ -1125,8 +1124,8 @@ 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 . '".'); } /** diff --git a/framework/db/ActiveRelation.php b/yii/db/ActiveRelation.php similarity index 98% rename from framework/db/ActiveRelation.php rename to yii/db/ActiveRelation.php index f1b198b..42ae0e7 100644 --- a/framework/db/ActiveRelation.php +++ b/yii/db/ActiveRelation.php @@ -131,7 +131,7 @@ class ActiveRelation extends ActiveQuery /** * Finds the related records and populates them into the primary models. - * This method is internally by [[ActiveQuery]]. Do not call it directly. + * This method is internally used by [[ActiveQuery]]. Do not call it directly. * @param string $name the relation name * @param array $primaryModels primary models * @return array the related models @@ -266,7 +266,7 @@ class ActiveRelation extends ActiveQuery { $attributes = array_keys($this->link); $values = array(); - if (count($attributes) ===1) { + if (count($attributes) === 1) { // single key $attribute = reset($this->link); foreach ($models as $model) { diff --git a/framework/db/ColumnSchema.php b/yii/db/ColumnSchema.php similarity index 100% rename from framework/db/ColumnSchema.php rename to yii/db/ColumnSchema.php diff --git a/framework/db/Command.php b/yii/db/Command.php similarity index 99% rename from framework/db/Command.php rename to yii/db/Command.php index dc6c972..a117685 100644 --- a/framework/db/Command.php +++ b/yii/db/Command.php @@ -106,7 +106,7 @@ class Command extends \yii\base\Component */ public function getRawSql() { - if ($this->_params === array()) { + if (empty($this->_params)) { return $this->_sql; } else { $params = array(); diff --git a/framework/db/Connection.php b/yii/db/Connection.php similarity index 98% rename from framework/db/Connection.php rename to yii/db/Connection.php index c4bf4b9..571ec49 100644 --- a/framework/db/Connection.php +++ b/yii/db/Connection.php @@ -66,7 +66,7 @@ use yii\caching\Cache; * // ... executing other SQL statements ... * $transaction->commit(); * } catch(Exception $e) { - * $transaction->rollBack(); + * $transaction->rollback(); * } * ~~~ * @@ -351,9 +351,7 @@ class Connection extends Component $pdoClass = '\PDO'; if (($pos = strpos($this->dsn, ':')) !== false) { $driver = strtolower(substr($this->dsn, 0, $pos)); - if ($driver === 'sqlsrv') { - $pdoClass = 'yii\db\mssql\SqlsrvPDO'; - } elseif ($driver === 'mssql' || $driver === 'dblib') { + if ($driver === 'mssql' || $driver === 'dblib' || $driver === 'sqlsrv') { $pdoClass = 'yii\db\mssql\PDO'; } } @@ -519,7 +517,7 @@ class Connection extends Component public function quoteSql($sql) { $db = $this; - return preg_replace_callback('/(\\{\\{([\w\-\. ]+)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/', + return preg_replace_callback('/(\\{\\{([%\w\-\. ]+)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/', function($matches) use($db) { if (isset($matches[3])) { return $db->quoteColumnName($matches[3]); diff --git a/framework/db/DataReader.php b/yii/db/DataReader.php similarity index 100% rename from framework/db/DataReader.php rename to yii/db/DataReader.php diff --git a/framework/db/Exception.php b/yii/db/Exception.php similarity index 99% rename from framework/db/Exception.php rename to yii/db/Exception.php index ad97b5a..b7a60b4 100644 --- a/framework/db/Exception.php +++ b/yii/db/Exception.php @@ -41,4 +41,4 @@ class Exception extends \yii\base\Exception { return \Yii::t('yii|Database Exception'); } -} \ No newline at end of file +} diff --git a/framework/db/Expression.php b/yii/db/Expression.php similarity index 99% rename from framework/db/Expression.php rename to yii/db/Expression.php index 4ebcd5f..77e9f60 100644 --- a/framework/db/Expression.php +++ b/yii/db/Expression.php @@ -57,4 +57,4 @@ class Expression extends \yii\base\Object { return $this->expression; } -} \ No newline at end of file +} diff --git a/framework/db/Migration.php b/yii/db/Migration.php similarity index 99% rename from framework/db/Migration.php rename to yii/db/Migration.php index ce2cf97..f51e597 100644 --- a/framework/db/Migration.php +++ b/yii/db/Migration.php @@ -64,14 +64,14 @@ class Migration extends \yii\base\Component $transaction = $this->db->beginTransaction(); try { if ($this->safeUp() === false) { - $transaction->rollBack(); + $transaction->rollback(); return false; } $transaction->commit(); } catch (\Exception $e) { echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n"; echo $e->getTraceAsString() . "\n"; - $transaction->rollBack(); + $transaction->rollback(); return false; } return null; @@ -89,14 +89,14 @@ class Migration extends \yii\base\Component $transaction = $this->db->beginTransaction(); try { if ($this->safeDown() === false) { - $transaction->rollBack(); + $transaction->rollback(); return false; } $transaction->commit(); } catch (\Exception $e) { echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n"; echo $e->getTraceAsString() . "\n"; - $transaction->rollBack(); + $transaction->rollback(); return false; } return null; @@ -368,4 +368,4 @@ class Migration extends \yii\base\Component $this->db->createCommand()->dropIndex($name, $table)->execute(); echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; } -} \ No newline at end of file +} diff --git a/framework/db/Query.php b/yii/db/Query.php similarity index 99% rename from framework/db/Query.php rename to yii/db/Query.php index 2239f5d..b1fc718 100644 --- a/framework/db/Query.php +++ b/yii/db/Query.php @@ -483,7 +483,7 @@ class Query extends \yii\base\Component * Sets the ORDER BY part of the query. * @param string|array $columns the columns (and the directions) to be ordered by. * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `array('id' => Query::SORT_ASC ASC, 'name' => Query::SORT_DESC)`). + * (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 @@ -499,7 +499,7 @@ class Query extends \yii\base\Component * Adds additional ORDER BY columns to the query. * @param string|array $columns the columns (and the directions) to be ordered by. * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `array('id' => Query::SORT_ASC ASC, 'name' => Query::SORT_DESC)`). + * (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 @@ -589,7 +589,7 @@ class Query extends \yii\base\Component */ public function addParams($params) { - if ($params !== array()) { + if (!empty($params)) { if ($this->params === null) { $this->params = $params; } else { diff --git a/framework/db/QueryBuilder.php b/yii/db/QueryBuilder.php similarity index 99% rename from framework/db/QueryBuilder.php rename to yii/db/QueryBuilder.php index 441d287..a4e30ea 100644 --- a/framework/db/QueryBuilder.php +++ b/yii/db/QueryBuilder.php @@ -472,8 +472,8 @@ class QueryBuilder extends \yii\base\Object if (isset($this->typeMap[$type])) { return $this->typeMap[$type]; } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) { - if (isset($this->typeMap[$matches[0]])) { - return preg_replace('/^\w+/', $this->typeMap[$matches[0]], $type); + if (isset($this->typeMap[$matches[1]])) { + return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type); } } return $type; @@ -722,7 +722,7 @@ class QueryBuilder extends \yii\base\Object if (!is_array($condition)) { return (string)$condition; - } elseif ($condition === array()) { + } elseif (empty($condition)) { return ''; } if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... @@ -777,7 +777,7 @@ class QueryBuilder extends \yii\base\Object $parts[] = $operand; } } - if ($parts !== array()) { + if (!empty($parts)) { return '(' . implode(") $operator (", $parts) . ')'; } else { return ''; @@ -813,7 +813,7 @@ class QueryBuilder extends \yii\base\Object $values = (array)$values; - if ($values === array() || $column === array()) { + if (empty($values) || $column === array()) { return $operator === 'IN' ? '0=1' : ''; } @@ -885,7 +885,7 @@ class QueryBuilder extends \yii\base\Object $values = (array)$values; - if ($values === array()) { + if (empty($values)) { return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; } diff --git a/framework/db/Schema.php b/yii/db/Schema.php similarity index 100% rename from framework/db/Schema.php rename to yii/db/Schema.php diff --git a/framework/db/StaleObjectException.php b/yii/db/StaleObjectException.php similarity index 99% rename from framework/db/StaleObjectException.php rename to yii/db/StaleObjectException.php index 860c9fc..0a04bd3 100644 --- a/framework/db/StaleObjectException.php +++ b/yii/db/StaleObjectException.php @@ -20,4 +20,4 @@ class StaleObjectException extends Exception { return \Yii::t('yii|Stale Object Exception'); } -} \ No newline at end of file +} diff --git a/framework/db/TableSchema.php b/yii/db/TableSchema.php similarity index 100% rename from framework/db/TableSchema.php rename to yii/db/TableSchema.php diff --git a/framework/db/Transaction.php b/yii/db/Transaction.php similarity index 95% rename from framework/db/Transaction.php rename to yii/db/Transaction.php index d66c38e..195a8c8 100644 --- a/framework/db/Transaction.php +++ b/yii/db/Transaction.php @@ -25,7 +25,7 @@ use yii\base\InvalidConfigException; * //.... other SQL executions * $transaction->commit(); * } catch(Exception $e) { - * $transaction->rollBack(); + * $transaction->rollback(); * } * ~~~ * @@ -42,14 +42,14 @@ class Transaction extends \yii\base\Object 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. + * 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()]]. + * can [[commit()]] or [[rollback()]]. */ public function getIsActive() { diff --git a/framework/db/mysql/QueryBuilder.php b/yii/db/mysql/QueryBuilder.php similarity index 100% rename from framework/db/mysql/QueryBuilder.php rename to yii/db/mysql/QueryBuilder.php diff --git a/framework/db/mysql/Schema.php b/yii/db/mysql/Schema.php similarity index 100% rename from framework/db/mysql/Schema.php rename to yii/db/mysql/Schema.php diff --git a/framework/db/sqlite/QueryBuilder.php b/yii/db/sqlite/QueryBuilder.php similarity index 100% rename from framework/db/sqlite/QueryBuilder.php rename to yii/db/sqlite/QueryBuilder.php diff --git a/framework/db/sqlite/Schema.php b/yii/db/sqlite/Schema.php similarity index 98% rename from framework/db/sqlite/Schema.php rename to yii/db/sqlite/Schema.php index 45f8392..d4fb245 100644 --- a/framework/db/sqlite/Schema.php +++ b/yii/db/sqlite/Schema.php @@ -169,7 +169,7 @@ class Schema extends \yii\db\Schema } } } - $column->phpType = $this->getColumnPhpType($this->type); + $column->phpType = $this->getColumnPhpType($column); $value = $info['dflt_value']; if ($column->type === 'string') { diff --git a/yii/debug/Module.php b/yii/debug/Module.php new file mode 100644 index 0000000..3421d95 --- /dev/null +++ b/yii/debug/Module.php @@ -0,0 +1,17 @@ + + * @since 2.0 + */ +class Module extends \yii\base\Module +{ + public $controllerNamespace = 'yii\debug\controllers'; +} \ No newline at end of file diff --git a/yii/debug/Toolbar.php b/yii/debug/Toolbar.php new file mode 100644 index 0000000..84b55c8 --- /dev/null +++ b/yii/debug/Toolbar.php @@ -0,0 +1,38 @@ + + * @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/debug/controllers/DefaultController.php b/yii/debug/controllers/DefaultController.php new file mode 100644 index 0000000..ca90920 --- /dev/null +++ b/yii/debug/controllers/DefaultController.php @@ -0,0 +1,22 @@ + + * @since 2.0 + */ +class DefaultController extends Controller +{ + public function actionIndex($tag) + { + echo $tag; + } +} \ No newline at end of file diff --git a/framework/helpers/ArrayHelper.php b/yii/helpers/ArrayHelper.php similarity index 99% rename from framework/helpers/ArrayHelper.php rename to yii/helpers/ArrayHelper.php index 3061717..d58341c 100644 --- a/framework/helpers/ArrayHelper.php +++ b/yii/helpers/ArrayHelper.php @@ -16,4 +16,4 @@ namespace yii\helpers; */ class ArrayHelper extends base\ArrayHelper { -} \ No newline at end of file +} diff --git a/framework/helpers/ConsoleColor.php b/yii/helpers/Console.php similarity index 77% rename from framework/helpers/ConsoleColor.php rename to yii/helpers/Console.php index 794b9c8..0055107 100644 --- a/framework/helpers/ConsoleColor.php +++ b/yii/helpers/Console.php @@ -7,9 +7,8 @@ namespace yii\helpers; -// todo test this on all kinds of terminals, especially windows (check out lib ncurses) - /** + * 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 @@ -18,6 +17,6 @@ namespace yii\helpers; * @author Carsten Brandt * @since 2.0 */ -class ConsoleColor extends base\ConsoleColor +class Console extends base\Console { } diff --git a/framework/helpers/FileHelper.php b/yii/helpers/FileHelper.php similarity index 99% rename from framework/helpers/FileHelper.php rename to yii/helpers/FileHelper.php index 3fb24e1..04ce4e1 100644 --- a/framework/helpers/FileHelper.php +++ b/yii/helpers/FileHelper.php @@ -18,4 +18,4 @@ namespace yii\helpers; */ class FileHelper extends base\FileHelper { -} \ No newline at end of file +} diff --git a/framework/helpers/Html.php b/yii/helpers/Html.php similarity index 100% rename from framework/helpers/Html.php rename to yii/helpers/Html.php diff --git a/framework/helpers/Json.php b/yii/helpers/Json.php similarity index 99% rename from framework/helpers/Json.php rename to yii/helpers/Json.php index 2a20f3c..5e77c3f 100644 --- a/framework/helpers/Json.php +++ b/yii/helpers/Json.php @@ -15,4 +15,4 @@ namespace yii\helpers; class Json extends base\Json { -} \ No newline at end of file +} diff --git a/yii/helpers/Markdown.php b/yii/helpers/Markdown.php new file mode 100644 index 0000000..3f6c98e --- /dev/null +++ b/yii/helpers/Markdown.php @@ -0,0 +1,33 @@ + 'footnote_', + * )); + * ``` + * + * For more details please refer to [PHP Markdown library documentation](http://michelf.ca/projects/php-markdown/). + * @author Alexander Makarov + * @since 2.0 + */ +class Markdown extends base\Markdown +{ +} diff --git a/yii/helpers/Purifier.php b/yii/helpers/Purifier.php new file mode 100644 index 0000000..b659531 --- /dev/null +++ b/yii/helpers/Purifier.php @@ -0,0 +1,34 @@ + true, + * )); + * ``` + * + * For more details please refer to HTMLPurifier documentation](http://htmlpurifier.org/). + * + * @author Alexander Makarov + * @since 2.0 + */ +class Purifier extends base\Purifier +{ +} diff --git a/framework/helpers/SecurityHelper.php b/yii/helpers/SecurityHelper.php similarity index 99% rename from framework/helpers/SecurityHelper.php rename to yii/helpers/SecurityHelper.php index d3cb2ad..d16e7e6 100644 --- a/framework/helpers/SecurityHelper.php +++ b/yii/helpers/SecurityHelper.php @@ -26,4 +26,4 @@ namespace yii\helpers; */ class SecurityHelper extends base\SecurityHelper { -} \ No newline at end of file +} diff --git a/framework/helpers/StringHelper.php b/yii/helpers/StringHelper.php similarity index 100% rename from framework/helpers/StringHelper.php rename to yii/helpers/StringHelper.php diff --git a/framework/helpers/VarDumper.php b/yii/helpers/VarDumper.php similarity index 99% rename from framework/helpers/VarDumper.php rename to yii/helpers/VarDumper.php index 2659188..59a1718 100644 --- a/framework/helpers/VarDumper.php +++ b/yii/helpers/VarDumper.php @@ -25,4 +25,4 @@ namespace yii\helpers; */ class VarDumper extends base\VarDumper { -} \ No newline at end of file +} diff --git a/framework/helpers/base/ArrayHelper.php b/yii/helpers/base/ArrayHelper.php similarity index 82% rename from framework/helpers/base/ArrayHelper.php rename to yii/helpers/base/ArrayHelper.php index 9870542..e482883 100644 --- a/framework/helpers/base/ArrayHelper.php +++ b/yii/helpers/base/ArrayHelper.php @@ -36,7 +36,7 @@ class ArrayHelper { $args = func_get_args(); $res = array_shift($args); - while ($args !== array()) { + while (!empty($args)) { $next = array_shift($args); foreach ($next as $k => $v) { if (is_integer($k)) { @@ -87,6 +87,35 @@ class ArrayHelper } /** + * Removes an item from an array and returns the value. If the key does not exist in the array, the default value + * will be returned instead. + * + * Usage examples, + * + * ~~~ + * // $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)); + * ~~~ + * + * @param array $array the array to extract value from + * @param string $key key name of the array element + * @param mixed $default the default value to be returned if the specified key does not exist + * @return mixed|null the value of the element if found, default value otherwise + */ + public static function remove(&$array, $key, $default = null) + { + if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) { + $value = $array[$key]; + unset($array[$key]); + return $value; + } + return $default; + } + + /** * Indexes an array according to a specified key. * The input array should be multidimensional or an array of objects. * @@ -236,15 +265,17 @@ class ArrayHelper * To sort by multiple keys, provide an array of keys here. * @param boolean|array $ascending whether to sort in ascending or descending order. When * sorting by multiple keys with different ascending orders, use an array of ascending flags. - * @param integer|array $sortFlag the PHP sort flag. Valid values include: - * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, and `SORT_STRING | SORT_FLAG_CASE`. The last - * value is for sorting strings in case-insensitive manner. Please refer to - * See [PHP manual](http://php.net/manual/en/function.sort.php) for more details. - * When sorting by multiple keys with different sort flags, use an array of sort flags. + * @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) + * for more details. When sorting by multiple keys with different sort flags, use an array of sort flags. + * @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 * correct number of elements as that of $key. */ - public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR) + public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR, $caseSensitive = true) { $keys = is_array($key) ? $key : array($key); if (empty($keys) || empty($array)) { @@ -259,18 +290,28 @@ class ArrayHelper if (is_scalar($sortFlag)) { $sortFlag = array_fill(0, $n, $sortFlag); } elseif (count($sortFlag) !== $n) { - throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); + throw new InvalidParamException('The length of $sortFlag parameter must be the same as that of $keys.'); + } + if (is_scalar($caseSensitive)) { + $caseSensitive = array_fill(0, $n, $caseSensitive); + } elseif (count($caseSensitive) !== $n) { + throw new InvalidParamException('The length of $caseSensitive parameter must be the same as that of $keys.'); } $args = array(); foreach ($keys as $i => $key) { $flag = $sortFlag[$i]; - if ($flag == (SORT_STRING | SORT_FLAG_CASE)) { - $flag = SORT_STRING; - $column = array(); - foreach (static::getColumn($array, $key) as $k => $value) { - $column[$k] = strtolower($value); + $cs = $caseSensitive[$i]; + if (!$cs && ($flag === SORT_STRING)) { + if (defined('SORT_FLAG_CASE')) { + $flag = $flag | SORT_FLAG_CASE; + $args[] = static::getColumn($array, $key); + } else { + $column = array(); + foreach (static::getColumn($array, $key) as $k => $value) { + $column[$k] = mb_strtolower($value); + } + $args[] = $column; } - $args[] = $column; } else { $args[] = static::getColumn($array, $key); } @@ -337,4 +378,4 @@ class ArrayHelper } return $d; } -} \ No newline at end of file +} diff --git a/yii/helpers/base/Console.php b/yii/helpers/base/Console.php new file mode 100644 index 0000000..b611919 --- /dev/null +++ b/yii/helpers/base/Console.php @@ -0,0 +1,781 @@ + + * @since 2.0 + */ +class Console +{ + const FG_BLACK = 30; + const FG_RED = 31; + const FG_GREEN = 32; + const FG_YELLOW = 33; + const FG_BLUE = 34; + const FG_PURPLE = 35; + const FG_CYAN = 36; + const FG_GREY = 37; + + const BG_BLACK = 40; + const BG_RED = 41; + const BG_GREEN = 42; + const BG_YELLOW = 43; + const BG_BLUE = 44; + const BG_PURPLE = 45; + const BG_CYAN = 46; + const BG_GREY = 47; + + const NORMAL = 0; + const BOLD = 1; + const ITALIC = 3; + const UNDERLINE = 4; + const BLINK = 5; + const NEGATIVE = 7; + const CONCEALED = 8; + const CROSSED_OUT = 9; + const FRAMED = 51; + const ENCIRCLED = 52; + const OVERLINED = 53; + + /** + * Moves the terminal cursor up by sending ANSI control code CUU to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved up + */ + public static function moveCursorUp($rows = 1) + { + echo "\033[" . (int)$rows . 'A'; + } + + /** + * Moves the terminal cursor down by sending ANSI control code CUD to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved down + */ + public static function moveCursorDown($rows = 1) + { + echo "\033[" . (int)$rows . 'B'; + } + + /** + * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved forward + */ + public static function moveCursorForward($steps = 1) + { + echo "\033[" . (int)$steps . 'C'; + } + + /** + * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved backward + */ + public static function moveCursorBackward($steps = 1) + { + echo "\033[" . (int)$steps . 'D'; + } + + /** + * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal. + * @param integer $lines number of lines the cursor should be moved down + */ + public static function moveCursorNextLine($lines = 1) + { + echo "\033[" . (int)$lines . 'E'; + } + + /** + * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal. + * @param integer $lines number of lines the cursor should be moved up + */ + public static function moveCursorPrevLine($lines = 1) + { + echo "\033[" . (int)$lines . 'F'; + } + + /** + * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal. + * @param integer $column 1-based column number, 1 is the left edge of the screen. + * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. + */ + public static function moveCursorTo($column, $row = null) + { + if ($row === null) { + echo "\033[" . (int)$column . 'G'; + } else { + echo "\033[" . (int)$row . ';' . (int)$column . 'H'; + } + } + + /** + * Scrolls whole page up by sending ANSI control code SU to the terminal. + * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows. + * @param int $lines number of lines to scroll up + */ + public static function scrollUp($lines = 1) + { + echo "\033[" . (int)$lines . "S"; + } + + /** + * Scrolls whole page down by sending ANSI control code SD to the terminal. + * New lines are added at the top. This is not supported by ANSI.SYS used in windows. + * @param int $lines number of lines to scroll down + */ + public static function scrollDown($lines = 1) + { + echo "\033[" . (int)$lines . "T"; + } + + /** + * Saves the current cursor position by sending ANSI control code SCP to the terminal. + * Position can then be restored with {@link restoreCursorPosition}. + */ + public static function saveCursorPosition() + { + echo "\033[s"; + } + + /** + * Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal. + */ + public static function restoreCursorPosition() + { + echo "\033[u"; + } + + /** + * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal. + * Use {@link showCursor} to bring it back. + * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit. + */ + public static function hideCursor() + { + echo "\033[?25l"; + } + + /** + * Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal. + */ + public static function showCursor() + { + echo "\033[?25h"; + } + + /** + * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal. + * Cursor position will not be changed. + * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen. + */ + public static function clearScreen() + { + echo "\033[2J"; + } + + /** + * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal. + * Cursor position will not be changed. + */ + public static function clearScreenBeforeCursor() + { + echo "\033[1J"; + } + + /** + * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal. + * Cursor position will not be changed. + */ + public static function clearScreenAfterCursor() + { + echo "\033[0J"; + } + + /** + * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLine() + { + echo "\033[2K"; + } + + /** + * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLineBeforeCursor() + { + echo "\033[1K"; + } + + /** + * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLineAfterCursor() + { + echo "\033[0K"; + } + + /** + * Sets the ANSI format for any text that is printed afterwards. + * + * You can pass any of the FG_*, BG_* and TEXT_* constants and also [[xterm256ColorFg]] and [[xterm256ColorBg]]. + * TODO: documentation + */ + public static function ansiFormatBegin() + { + echo "\033[" . implode(';', func_get_args()) . 'm'; + } + + /** + * Resets any ANSI format set by previous method [[ansiFormatBegin()]] + * Any output after this is will have default text style. + */ + public static function ansiFormatReset() + { + echo "\033[0m"; + } + + /** + * Returns the ANSI format code. + * + * You can pass any of the FG_*, BG_* and TEXT_* constants and also [[xterm256ColorFg]] and [[xterm256ColorBg]]. + * TODO: documentation + */ + public static function ansiFormatCode($format) + { + return "\033[" . implode(';', $format) . 'm'; + } + + /** + * 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]]. + * @return string + */ + 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! + { + return '38;5;' . $i; + } + + public static function xterm256ColorBg($i) // TODO naming! + { + return '48;5;' . $i; + } + + /** + * Strips ANSI control codes from a string + * + * @param string $string String to strip + * @return string + */ + public static function stripAnsiFormat($string) + { + return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color + } + + // TODO refactor and review + public static function ansiToHtml($string) + { + $tags = 0; + return preg_replace_callback( + '/\033\[[\d;]+m/', + function ($ansi) use (&$tags) { + $styleA = array(); + foreach (explode(';', $ansi) as $controlCode) { + switch ($controlCode) { + case self::FG_BLACK: + $style = array('color' => '#000000'); + break; + case self::FG_BLUE: + $style = array('color' => '#000078'); + break; + case self::FG_CYAN: + $style = array('color' => '#007878'); + break; + case self::FG_GREEN: + $style = array('color' => '#007800'); + break; + case self::FG_GREY: + $style = array('color' => '#787878'); + break; + case self::FG_PURPLE: + $style = array('color' => '#780078'); + break; + case self::FG_RED: + $style = array('color' => '#780000'); + break; + case self::FG_YELLOW: + $style = array('color' => '#787800'); + break; + case self::BG_BLACK: + $style = array('background-color' => '#000000'); + break; + case self::BG_BLUE: + $style = array('background-color' => '#000078'); + break; + case self::BG_CYAN: + $style = array('background-color' => '#007878'); + break; + case self::BG_GREEN: + $style = array('background-color' => '#007800'); + break; + case self::BG_GREY: + $style = array('background-color' => '#787878'); + break; + case self::BG_PURPLE: + $style = array('background-color' => '#780078'); + break; + case self::BG_RED: + $style = array('background-color' => '#780000'); + break; + case self::BG_YELLOW: + $style = array('background-color' => '#787800'); + break; + case self::BOLD: + $style = array('font-weight' => 'bold'); + break; + case self::ITALIC: + $style = array('font-style' => 'italic'); + break; + case self::UNDERLINE: + $style = array('text-decoration' => array('underline')); + break; + case self::OVERLINED: + $style = array('text-decoration' => array('overline')); + break; + case self::CROSSED_OUT: + $style = array('text-decoration' => array('line-through')); + break; + case self::BLINK: + $style = array('text-decoration' => array('blink')); + break; + case self::NEGATIVE: // ??? + case self::CONCEALED: + case self::ENCIRCLED: + case self::FRAMED: + // TODO allow resetting codes + break; + case 0: // ansi reset + $return = ''; + for ($n = $tags; $tags > 0; $tags--) { + $return .= ''; + } + return $return; + } + + $styleA = ArrayHelper::merge($styleA, $style); + } + $styleString[] = array(); + foreach ($styleA as $name => $content) { + if ($name === 'text-decoration') { + $content = implode(' ', $content); + } + $styleString[] = $name . ':' . $content; + } + $tags++; + return ' $value) { + echo " $key - $value\n"; + } + echo " ? - Show help\n"; + goto top; + } elseif (!in_array($input, array_keys($options))) { + goto top; + } + return $input; + } + + /** + * Displays and updates a simple progress bar on screen. + * + * @param integer $done the number of items that are completed + * @param integer $total the total value of items that are to be done + * @param integer $size the size of the status bar (optional) + * @see http://snipplr.com/view/29548/ + */ + public static function showProgress($done, $total, $size = 30) + { + static $start; + + // if we go over our bound, just ignore it + if ($done > $total) { + return; + } + + if (empty($start)) { + $start = time(); + } + + $now = time(); + + $percent = (double)($done / $total); + $bar = floor($percent * $size); + + $status = "\r["; + $status .= str_repeat("=", $bar); + if ($bar < $size) { + $status .= ">"; + $status .= str_repeat(" ", $size - $bar); + } else { + $status .= "="; + } + + $display = number_format($percent * 100, 0); + + $status .= "] $display% $done/$total"; + + $rate = ($now - $start) / $done; + $left = $total - $done; + $eta = round($rate * $left, 2); + + $elapsed = $now - $start; + + $status .= " remaining: " . number_format($eta) . " sec. elapsed: " . number_format($elapsed) . " sec."; + + static::stdout("$status "); + + flush(); + + // when done, send a newline + if ($done == $total) { + echo "\n"; + } + } +} diff --git a/framework/helpers/base/FileHelper.php b/yii/helpers/base/FileHelper.php similarity index 99% rename from framework/helpers/base/FileHelper.php rename to yii/helpers/base/FileHelper.php index 2f62f43..954c86e 100644 --- a/framework/helpers/base/FileHelper.php +++ b/yii/helpers/base/FileHelper.php @@ -169,4 +169,4 @@ class FileHelper } closedir($handle); } -} \ No newline at end of file +} diff --git a/framework/helpers/base/Html.php b/yii/helpers/base/Html.php similarity index 99% rename from framework/helpers/base/Html.php rename to yii/helpers/base/Html.php index 5b8e7db..f601772 100644 --- a/framework/helpers/base/Html.php +++ b/yii/helpers/base/Html.php @@ -95,9 +95,9 @@ class Html public static $attributeOrder = array( 'type', 'id', + 'class', 'name', 'value', - 'class', 'href', 'src', @@ -127,13 +127,15 @@ class Html * Encodes special characters into HTML entities. * The [[yii\base\Application::charset|application charset]] will be used for encoding. * @param string $content the content to be encoded + * @param boolean $doubleEncode whether to encode HTML entities in `$content`. If false, + * HTML entities in `$content` will not be further encoded. * @return string the encoded content * @see decode * @see http://www.php.net/manual/en/function.htmlspecialchars.php */ - public static function encode($content) + public static function encode($content, $doubleEncode = true) { - return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); + return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset, $doubleEncode); } /** @@ -322,7 +324,7 @@ class Html $options['action'] = $action; $options['method'] = $method; $form = static::beginTag('form', $options); - if ($hiddenInputs !== array()) { + if (!empty($hiddenInputs)) { $form .= "\n" . implode("\n", $hiddenInputs); } @@ -375,7 +377,8 @@ class Html */ public static function mailto($text, $email = null, $options = array()) { - return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options); + $options['href'] = 'mailto:' . ($email === null ? $text : $email); + return static::tag('a', $text, $options); } /** @@ -615,7 +618,7 @@ class Html * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, * the value of this attribute will still be submitted to the server via the hidden input. * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. * * @return string the generated radio button tag @@ -644,7 +647,7 @@ class Html * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, * the value of this attribute will still be submitted to the server via the hidden input. * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. * * @return string the generated checkbox tag @@ -691,9 +694,9 @@ class Html * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', * except that the array keys represent the optgroup labels specified in $items. * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * + * * @return string the generated drop-down list tag */ public static function dropDownList($name, $selection = null, $items = array(), $options = array()) @@ -734,9 +737,9 @@ class Html * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple * mode, we can still obtain the posted unselect value. * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * + * * @return string the generated list box tag */ public static function listBox($name, $selection = null, $items = array(), $options = array()) diff --git a/framework/helpers/base/Json.php b/yii/helpers/base/Json.php similarity index 97% rename from framework/helpers/base/Json.php rename to yii/helpers/base/Json.php index 1deb5c2..8de55f9 100644 --- a/framework/helpers/base/Json.php +++ b/yii/helpers/base/Json.php @@ -8,7 +8,7 @@ namespace yii\helpers\base; use yii\base\InvalidParamException; -use yii\helpers\JsExpression; +use yii\web\JsExpression; /** * Json is a helper class providing JSON data encoding and decoding. @@ -34,7 +34,7 @@ class Json $expressions = array(); $value = static::processData($value, $expressions); $json = json_encode($value, $options); - return $expressions === array() ? $json : strtr($json, $expressions); + return empty($expressions) ? $json : strtr($json, $expressions); } /** @@ -104,4 +104,4 @@ class Json return $data; } } -} \ No newline at end of file +} diff --git a/yii/helpers/base/Markdown.php b/yii/helpers/base/Markdown.php new file mode 100644 index 0000000..1efdccb --- /dev/null +++ b/yii/helpers/base/Markdown.php @@ -0,0 +1,50 @@ + 'footnote_', + * )); + * ``` + * + * For more details please refer to [PHP Markdown library documentation](http://michelf.ca/projects/php-markdown/). + * @author Alexander Makarov + * @since 2.0 + */ +class Markdown +{ + /** + * @var MarkdownExtra + */ + protected static $markdown; + + public static function process($content, $config = array()) + { + if (static::$markdown===null) { + static::$markdown = new MarkdownExtra(); + } + foreach ($config as $name => $value) { + static::$markdown->{$name} = $value; + } + return static::$markdown->transform($content); + } +} diff --git a/yii/helpers/base/Purifier.php b/yii/helpers/base/Purifier.php new file mode 100644 index 0000000..2c5d334 --- /dev/null +++ b/yii/helpers/base/Purifier.php @@ -0,0 +1,39 @@ + true, + * )); + * ``` + * + * For more details please refer to HTMLPurifier documentation](http://htmlpurifier.org/). + * + * @author Alexander Makarov + * @since 2.0 + */ +class Purifier +{ + public static function process($content, $config = null) + { + $purifier=\HTMLPurifier::instance($config); + $purifier->config->set('Cache.SerializerPath', \Yii::$app->getRuntimePath()); + return $purifier->purify($content); + } +} diff --git a/framework/helpers/base/SecurityHelper.php b/yii/helpers/base/SecurityHelper.php similarity index 95% rename from framework/helpers/base/SecurityHelper.php rename to yii/helpers/base/SecurityHelper.php index 6ba48ba..3f69fee 100644 --- a/framework/helpers/base/SecurityHelper.php +++ b/yii/helpers/base/SecurityHelper.php @@ -42,7 +42,8 @@ class SecurityHelper public static function encrypt($data, $key) { $module = static::openCryptModule(); - $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + // 192-bit (24 bytes) key size + $key = StringHelper::substr($key, 0, 24); srand(); $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); mcrypt_generic_init($module, $key, $iv); @@ -63,7 +64,8 @@ class SecurityHelper public static function decrypt($data, $key) { $module = static::openCryptModule(); - $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + // 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); mcrypt_generic_init($module, $key, $iv); @@ -148,7 +150,8 @@ class SecurityHelper if (!extension_loaded('mcrypt')) { throw new InvalidConfigException('The mcrypt PHP extension is not installed.'); } - $module = @mcrypt_module_open('rijndael-256', '', MCRYPT_MODE_CBC, ''); + // AES uses a 128-bit block size + $module = @mcrypt_module_open('rijndael-128', '', 'cbc', ''); if ($module === false) { throw new Exception('Failed to initialize the mcrypt module.'); } @@ -164,11 +167,11 @@ class SecurityHelper * * ~~~ * // generates the hash (usually done during user registration or when the password is changed) - * $hash = SecurityHelper::hashPassword($password); + * $hash = SecurityHelper::generatePasswordHash($password); * // ...save $hash in database... * * // during login, validate if the password entered is correct using $hash fetched from database - * if (PasswordHelper::verifyPassword($password, $hash) { + * if (SecurityHelper::verifyPassword($password, $hash) { * // password is good * } else { * // password is bad @@ -214,7 +217,7 @@ class SecurityHelper throw new InvalidParamException('Password must be a string and cannot be empty.'); } - if (!preg_match('/^\$2[axy]\$(\d\d)\$[\./0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) { + if (!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) { throw new InvalidParamException('Hash is invalid.'); } @@ -269,4 +272,4 @@ class SecurityHelper $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); return $salt; } -} \ No newline at end of file +} diff --git a/framework/helpers/base/StringHelper.php b/yii/helpers/base/StringHelper.php similarity index 81% rename from framework/helpers/base/StringHelper.php rename to yii/helpers/base/StringHelper.php index cb4b09b..646bcbb 100644 --- a/framework/helpers/base/StringHelper.php +++ b/yii/helpers/base/StringHelper.php @@ -44,6 +44,29 @@ class StringHelper } /** + * 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'. diff --git a/framework/helpers/base/VarDumper.php b/yii/helpers/base/VarDumper.php similarity index 97% rename from framework/helpers/base/VarDumper.php rename to yii/helpers/base/VarDumper.php index fe15d98..730aafe 100644 --- a/framework/helpers/base/VarDumper.php +++ b/yii/helpers/base/VarDumper.php @@ -39,7 +39,7 @@ class VarDumper */ public static function dump($var, $depth = 10, $highlight = false) { - echo self::dumpAsString($var, $depth, $highlight); + echo static::dumpAsString($var, $depth, $highlight); } /** @@ -116,7 +116,7 @@ class VarDumper } elseif (self::$_depth <= $level) { self::$_output .= get_class($var) . '(...)'; } else { - $id = self::$_objects[] = $var; + $id = array_push(self::$_objects, $var); $className = get_class($var); $members = (array)$var; $spaces = str_repeat(' ', $level * 4); @@ -131,4 +131,4 @@ class VarDumper break; } } -} \ No newline at end of file +} diff --git a/framework/helpers/base/mimeTypes.php b/yii/helpers/base/mimeTypes.php similarity index 100% rename from framework/helpers/base/mimeTypes.php rename to yii/helpers/base/mimeTypes.php diff --git a/yii/i18n/GettextFile.php b/yii/i18n/GettextFile.php new file mode 100644 index 0000000..03eecca --- /dev/null +++ b/yii/i18n/GettextFile.php @@ -0,0 +1,37 @@ + + * @since 2.0 + */ +abstract class GettextFile extends Component +{ + /** + * Loads messages from a file. + * @param string $filePath file path + * @param string $context message context + * @return array message translations. Array keys are source messages and array values are translated messages: + * source message => translated message. + */ + abstract public function load($filePath, $context); + + /** + * Saves messages to a file. + * @param string $filePath file path + * @param array $messages message translations. Array keys are source messages and array values are + * translated messages: source message => translated message. Note if the message has a context, + * the message ID must be prefixed with the context with chr(4) as the separator. + */ + abstract public function save($filePath, $messages); +} diff --git a/yii/i18n/GettextMessageSource.php b/yii/i18n/GettextMessageSource.php new file mode 100644 index 0000000..0eb7cb3 --- /dev/null +++ b/yii/i18n/GettextMessageSource.php @@ -0,0 +1,59 @@ +basePath) . '/' . $language . '/' . $this->catalog; + if ($this->useMoFile) { + $messageFile .= static::MO_FILE_EXT; + } else { + $messageFile .= static::PO_FILE_EXT; + } + + if (is_file($messageFile)) { + if ($this->useMoFile) { + $gettextFile = new GettextMoFile(array('useBigEndian' => $this->useBigEndian)); + } else { + $gettextFile = new GettextPoFile(); + } + $messages = $gettextFile->load($messageFile, $category); + if (!is_array($messages)) { + $messages = array(); + } + return $messages; + } else { + Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); + return array(); + } + } +} diff --git a/yii/i18n/GettextMoFile.php b/yii/i18n/GettextMoFile.php new file mode 100644 index 0000000..bacba52 --- /dev/null +++ b/yii/i18n/GettextMoFile.php @@ -0,0 +1,267 @@ +. + * 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. + * + * 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. + * + * @author Qiang Xue + * @since 2.0 + */ +class GettextMoFile extends GettextFile +{ + /** + * @var boolean whether to use big-endian when reading and writing an integer. + */ + public $useBigEndian = false; + + /** + * Loads messages from an MO file. + * @param string $filePath file path + * @param string $context message context + * @return array message translations. Array keys are source messages and array values are translated messages: + * source message => translated message. + */ + public function load($filePath, $context) + { + if (false === ($fileHandle = @fopen($filePath, 'rb'))) { + throw new Exception('Unable to read file "' . $filePath . '".'); + } + if (false === @flock($fileHandle, LOCK_SH)) { + throw new Exception('Unable to lock file "' . $filePath . '" for reading.'); + } + + // magic + $array = unpack('c', $this->readBytes($fileHandle, 4)); + $magic = current($array); + if ($magic == -34) { + $this->useBigEndian = false; + } elseif ($magic == -107) { + $this->useBigEndian = true; + } else { + throw new Exception('Invalid MO file: ' . $filePath . ' (magic: ' . $magic . ').'); + } + + // revision + $revision = $this->readInteger($fileHandle); + if ($revision != 0) { + throw new Exception('Invalid MO file revision: ' . $revision . '.'); + } + + $count = $this->readInteger($fileHandle); + $sourceOffset = $this->readInteger($fileHandle); + $targetOffset = $this->readInteger($fileHandle); + + $sourceLengths = array(); + $sourceOffsets = array(); + fseek($fileHandle, $sourceOffset); + for ($i = 0; $i < $count; ++$i) { + $sourceLengths[] = $this->readInteger($fileHandle); + $sourceOffsets[] = $this->readInteger($fileHandle); + } + + $targetLengths = array(); + $targetOffsets = array(); + fseek($fileHandle, $targetOffset); + for ($i = 0; $i < $count; ++$i) { + $targetLengths[] = $this->readInteger($fileHandle); + $targetOffsets[] = $this->readInteger($fileHandle); + } + + $messages = array(); + for ($i = 0; $i < $count; ++$i) { + $id = $this->readString($fileHandle, $sourceLengths[$i], $sourceOffsets[$i]); + $separatorPosition = strpos($id, chr(4)); + + if (($context && $separatorPosition !== false && substr($id, 0, $separatorPosition) === $context) || + (!$context && $separatorPosition === false)) { + if ($separatorPosition !== false) { + $id = substr($id,$separatorPosition+1); + } + + $message = $this->readString($fileHandle, $targetLengths[$i], $targetOffsets[$i]); + $messages[$id] = $message; + } + } + + @flock($fileHandle, LOCK_UN); + @fclose($fileHandle); + return $messages; + } + + /** + * Saves messages to an MO file. + * @param string $filePath file path + * @param array $messages message translations. Array keys are source messages and array values are + * translated messages: source message => translated message. Note if the message has a context, + * the message ID must be prefixed with the context with chr(4) as the separator. + */ + public function save($filePath, $messages) + { + if (false === ($fileHandle = @fopen($filePath, 'wb'))) { + throw new Exception('Unable to write file "' . $filePath . '".'); + } + if (false === @flock($fileHandle, LOCK_EX)) { + throw new Exception('Unable to lock file "' . $filePath . '" for reading.'); + } + + // magic + if ($this->useBigEndian) { + $this->writeBytes($fileHandle, pack('c*', 0x95, 0x04, 0x12, 0xde)); // -107 + } else { + $this->writeBytes($fileHandle, pack('c*', 0xde, 0x12, 0x04, 0x95)); // -34 + } + + // revision + $this->writeInteger($fileHandle, 0); + + // message count + $messageCount = count($messages); + $this->writeInteger($fileHandle, $messageCount); + + // offset of source message table + $offset = 28; + $this->writeInteger($fileHandle, $offset); + $offset += $messageCount * 8; + $this->writeInteger($fileHandle, $offset); + + // hashtable size, omitted + $this->writeInteger($fileHandle, 0); + $offset += $messageCount * 8; + $this->writeInteger($fileHandle, $offset); + + // length and offsets for source messages + foreach (array_keys($messages) as $id) { + $length = strlen($id); + $this->writeInteger($fileHandle, $length); + $this->writeInteger($fileHandle, $offset); + $offset += $length + 1; + } + + // length and offsets for target messages + foreach ($messages as $message) { + $length = strlen($message); + $this->writeInteger($fileHandle, $length); + $this->writeInteger($fileHandle, $offset); + $offset += $length + 1; + } + + // source messages + foreach (array_keys($messages) as $id) { + $this->writeString($fileHandle, $id); + } + + // target messages + foreach ($messages as $message) { + $this->writeString($fileHandle, $message); + } + + @flock($fileHandle, LOCK_UN); + @fclose($fileHandle); + } + + /** + * Reads one or several bytes. + * @param resource $fileHandle to read from + * @param integer $byteCount to be read + * @return string bytes + */ + protected function readBytes($fileHandle, $byteCount = 1) + { + if ($byteCount > 0) { + return fread($fileHandle, $byteCount); + } + } + + /** + * Write bytes. + * @param resource $fileHandle to write to + * @param string $bytes to be written + * @return integer how many bytes are written + */ + protected function writeBytes($fileHandle, $bytes) + { + return fwrite($fileHandle, $bytes); + } + + /** + * Reads a 4-byte integer. + * @param resource $fileHandle to read from + * @return integer the result + */ + protected function readInteger($fileHandle) + { + $array = unpack($this->useBigEndian ? 'N' : 'V', $this->readBytes($fileHandle, 4)); + return current($array); + } + + /** + * Writes a 4-byte integer. + * @param resource $fileHandle to write to + * @param integer $integer to be written + * @return integer how many bytes are written + */ + protected function writeInteger($fileHandle, $integer) + { + return $this->writeBytes($fileHandle, pack($this->useBigEndian ? 'N' : 'V', (int)$integer)); + } + + /** + * Reads a string. + * @param resource $fileHandle file handle + * @param integer $length of the string + * @param integer $offset of the string in the file. If null, it reads from the current position. + * @return string the result + */ + protected function readString($fileHandle, $length, $offset = null) + { + if ($offset !== null) { + fseek($fileHandle, $offset); + } + return $this->readBytes($fileHandle, $length); + } + + /** + * Writes a string. + * @param resource $fileHandle to write to + * @param string $string to be written + * @return integer how many bytes are written + */ + protected function writeString($fileHandle, $string) + { + return $this->writeBytes($fileHandle, $string. "\0"); + } +} diff --git a/yii/i18n/GettextPoFile.php b/yii/i18n/GettextPoFile.php new file mode 100644 index 0000000..cac075b --- /dev/null +++ b/yii/i18n/GettextPoFile.php @@ -0,0 +1,97 @@ + + * @since 2.0 + */ +class GettextPoFile extends GettextFile +{ + /** + * Loads messages from a PO file. + * @param string $filePath file path + * @param string $context message context + * @return array message translations. Array keys are source messages and array values are translated messages: + * source message => translated message. + */ + public function load($filePath, $context) + { + $pattern = '/(msgctxt\s+"(.*?(?decode($matches[3][$i]); + $message = $this->decode($matches[4][$i]); + $messages[$id] = $message; + } + } + return $messages; + } + + /** + * Saves messages to a PO file. + * @param string $filePath file path + * @param array $messages message translations. Array keys are source messages and array values are + * translated messages: source message => translated message. Note if the message has a context, + * the message ID must be prefixed with the context with chr(4) as the separator. + */ + public function save($filePath, $messages) + { + $content = ''; + foreach ($messages as $id => $message) { + $separatorPosition = strpos($id, chr(4)); + if ($separatorPosition !== false) { + $content .= 'msgctxt "' . substr($id, 0, $separatorPosition) . "\"\n"; + $id = substr($id, $separatorPosition + 1); + } + $content .= 'msgid "' . $this->encode($id) . "\"\n"; + $content .= 'msgstr "' . $this->encode($message) . "\"\n\n"; + } + file_put_contents($filePath, $content); + } + + /** + * Encodes special characters in a message. + * @param string $string message to be encoded + * @return string the encoded message + */ + protected function encode($string) + { + return str_replace( + array('"', "\n", "\t", "\r"), + array('\\"', '\\n', '\\t', '\\r'), + $string + ); + } + + /** + * Decodes special characters in a message. + * @param string $string message to be decoded + * @return string the decoded message + */ + protected function decode($string) + { + $string = preg_replace( + array('/"\s+"/', '/\\\\n/', '/\\\\r/', '/\\\\t/', '/\\\\"/'), + array('', "\n", "\r", "\t", '"'), + $string + ); + return substr(rtrim($string), 1, -1); + } +} diff --git a/framework/i18n/I18N.php b/yii/i18n/I18N.php similarity index 99% rename from framework/i18n/I18N.php rename to yii/i18n/I18N.php index 8667abc..7fae5b0 100644 --- a/framework/i18n/I18N.php +++ b/yii/i18n/I18N.php @@ -109,7 +109,7 @@ class I18N extends Component unset($params[0]); } - return $params === array() ? $message : strtr($message, $params); + return empty($params) ? $message : strtr($message, $params); } /** diff --git a/framework/i18n/MessageSource.php b/yii/i18n/MessageSource.php similarity index 100% rename from framework/i18n/MessageSource.php rename to yii/i18n/MessageSource.php diff --git a/framework/i18n/MissingTranslationEvent.php b/yii/i18n/MissingTranslationEvent.php similarity index 100% rename from framework/i18n/MissingTranslationEvent.php rename to yii/i18n/MissingTranslationEvent.php diff --git a/framework/i18n/PhpMessageSource.php b/yii/i18n/PhpMessageSource.php similarity index 99% rename from framework/i18n/PhpMessageSource.php rename to yii/i18n/PhpMessageSource.php index 1ada44a..f62939f 100644 --- a/framework/i18n/PhpMessageSource.php +++ b/yii/i18n/PhpMessageSource.php @@ -76,4 +76,4 @@ class PhpMessageSource extends MessageSource return array(); } } -} \ No newline at end of file +} diff --git a/framework/i18n/data/plurals.php b/yii/i18n/data/plurals.php similarity index 99% rename from framework/i18n/data/plurals.php rename to yii/i18n/data/plurals.php index 52c733b..468f7e2 100644 --- a/framework/i18n/data/plurals.php +++ b/yii/i18n/data/plurals.php @@ -624,4 +624,4 @@ return array ( array ( 0 => 'in_array(fmod($n,10),array(1,2))||fmod($n,20)==0', ), -); \ No newline at end of file +); diff --git a/framework/i18n/data/plurals.xml b/yii/i18n/data/plurals.xml similarity index 100% rename from framework/i18n/data/plurals.xml rename to yii/i18n/data/plurals.xml diff --git a/framework/logging/DbTarget.php b/yii/logging/DbTarget.php similarity index 100% rename from framework/logging/DbTarget.php rename to yii/logging/DbTarget.php diff --git a/framework/logging/EmailTarget.php b/yii/logging/EmailTarget.php similarity index 99% rename from framework/logging/EmailTarget.php rename to yii/logging/EmailTarget.php index 4c84739..bb02e34 100644 --- a/framework/logging/EmailTarget.php +++ b/yii/logging/EmailTarget.php @@ -69,4 +69,4 @@ class EmailTarget extends Target } mail($sentTo, $subject, $body, implode("\r\n", $headers)); } -} \ No newline at end of file +} diff --git a/framework/logging/FileTarget.php b/yii/logging/FileTarget.php similarity index 100% rename from framework/logging/FileTarget.php rename to yii/logging/FileTarget.php diff --git a/framework/logging/Logger.php b/yii/logging/Logger.php similarity index 95% rename from framework/logging/Logger.php rename to yii/logging/Logger.php index 607c388..b557c5e 100644 --- a/framework/logging/Logger.php +++ b/yii/logging/Logger.php @@ -6,7 +6,9 @@ */ namespace yii\logging; -use yii\base\InvalidConfigException; + +use \yii\base\Component; +use \yii\base\InvalidConfigException; /** * Logger records logged messages in memory. @@ -17,7 +19,7 @@ use yii\base\InvalidConfigException; * @author Qiang Xue * @since 2.0 */ -class Logger extends \yii\base\Component +class Logger extends Component { /** * Error message level. An error message is one that indicates the abnormal termination of the @@ -80,6 +82,11 @@ class Logger extends \yii\base\Component * @var Router the log target router registered with this logger. */ public $router; + /** + * @var string a tag that uniquely identifies the current request. This can be used + * to differentiate the log messages for different requests. + */ + public $tag; /** * Initializes the logger by registering [[flush()]] as a shutdown function. @@ -87,6 +94,7 @@ class Logger extends \yii\base\Component public function init() { parent::init(); + $this->tag = date('Ymd-His', microtime(true)); register_shutdown_function(array($this, 'flush'), true); } diff --git a/framework/logging/ProfileTarget.php b/yii/logging/ProfileTarget.php similarity index 99% rename from framework/logging/ProfileTarget.php rename to yii/logging/ProfileTarget.php index 2b6ffe6..335e172 100644 --- a/framework/logging/ProfileTarget.php +++ b/yii/logging/ProfileTarget.php @@ -189,4 +189,4 @@ class CProfileLogRoute extends CWebLogRoute $total += $delta; return array($token, $calls, $min, $max, $total); } -} \ No newline at end of file +} diff --git a/framework/logging/Router.php b/yii/logging/Router.php similarity index 88% rename from framework/logging/Router.php rename to yii/logging/Router.php index 2f399fe..f544b72 100644 --- a/framework/logging/Router.php +++ b/yii/logging/Router.php @@ -28,16 +28,16 @@ use yii\base\Application; * 'preload' => array('log'), * 'components' => array( * 'log' => array( - * 'class' => '\yii\logging\Router', + * 'class' => 'yii\logging\Router', * 'targets' => array( * 'file' => array( - * 'class' => '\yii\logging\FileTarget', - * 'levels' => 'trace, info', - * 'categories' => 'yii\*', + * 'class' => 'yii\logging\FileTarget', + * 'levels' => array('trace', 'info'), + * 'categories' => array('yii\*'), * ), * 'email' => array( - * 'class' => '\yii\logging\EmailTarget', - * 'levels' => 'error, warning', + * 'class' => 'yii\logging\EmailTarget', + * 'levels' => array('error', 'warning'), * 'emails' => array('admin@example.com'), * ), * ), @@ -73,7 +73,6 @@ class Router extends Component public function init() { parent::init(); - foreach ($this->targets as $name => $target) { if (!$target instanceof Target) { $this->targets[$name] = Yii::createObject($target); diff --git a/framework/logging/Target.php b/yii/logging/Target.php similarity index 98% rename from framework/logging/Target.php rename to yii/logging/Target.php index e76e8ac..311334d 100644 --- a/framework/logging/Target.php +++ b/yii/logging/Target.php @@ -89,7 +89,7 @@ abstract class Target extends \yii\base\Component */ public function collect($messages, $final) { - $this->_messages = array($this->_messages, $this->filterMessages($messages)); + $this->_messages = array_merge($this->_messages, $this->filterMessages($messages)); $count = count($this->_messages); if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) { if (($context = $this->getContextMessage()) !== '') { diff --git a/framework/logging/WebTarget.php b/yii/logging/WebTarget.php similarity index 99% rename from framework/logging/WebTarget.php rename to yii/logging/WebTarget.php index b71e1a2..c98fd9f 100644 --- a/framework/logging/WebTarget.php +++ b/yii/logging/WebTarget.php @@ -58,4 +58,4 @@ class CWebLogRoute extends CLogRoute $viewFile = YII_PATH . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $view . '.php'; include($app->findLocalizedFile($viewFile, 'en')); } -} \ No newline at end of file +} diff --git a/yii/rbac/Assignment.php b/yii/rbac/Assignment.php new file mode 100644 index 0000000..5b6a607 --- /dev/null +++ b/yii/rbac/Assignment.php @@ -0,0 +1,106 @@ + + * @author Alexander Kochetov + * @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/rbac/DbManager.php b/yii/rbac/DbManager.php new file mode 100644 index 0000000..f3658b2 --- /dev/null +++ b/yii/rbac/DbManager.php @@ -0,0 +1,573 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class DbManager extends Manager +{ + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbManager object is created, if you want to change this property, you should only assign it + * with a DB connection object. + */ + public $db = 'db'; + /** + * @var string the name of the table storing authorization items. Defaults to 'tbl_auth_item'. + */ + public $itemTable = 'tbl_auth_item'; + /** + * @var string the name of the table storing authorization item hierarchy. Defaults to 'tbl_auth_item_child'. + */ + public $itemChildTable = 'tbl_auth_item_child'; + /** + * @var string the name of the table storing authorization item assignments. Defaults to 'tbl_auth_assignment'. + */ + public $assignmentTable = 'tbl_auth_assignment'; + + private $_usingSqlite; + + /** + * Initializes the application component. + * This method overrides the parent implementation by establishing the database connection. + */ + public function init() + { + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbManager::db must be either a DB connection instance or the application component ID of a DB connection."); + } + $this->_usingSqlite = !strncmp($this->db->getDriverName(), 'sqlite', 6); + parent::init(); + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This should can be either an integer or a string representing + * the unique identifier of a user. See [[User::id]]. + * @param string $itemName the name of the operation that need access check + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, + * which holds the value of `$userId`. + * @return boolean whether the operations can be performed by the user. + */ + public function checkAccess($userId, $itemName, $params = array()) + { + $assignments = $this->getAssignments($userId); + return $this->checkAccessRecursive($userId, $itemName, $params, $assignments); + } + + /** + * Performs access check for the specified user. + * This method is internally called by [[checkAccess()]]. + * @param mixed $userId the user ID. This should can be either an integer or a string representing + * the unique identifier of a user. See [[User::id]]. + * @param string $itemName the name of the operation that need access check + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, + * which holds the value of `$userId`. + * @param Assignment[] $assignments the assignments to the specified user + * @return boolean whether the operations can be performed by the user. + */ + protected function checkAccessRecursive($userId, $itemName, $params, $assignments) + { + if (($item = $this->getItem($itemName)) === null) { + return false; + } + Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); + if (!isset($params['userId'])) { + $params['userId'] = $userId; + } + if ($this->executeBizRule($item->getBizRule(), $params, $item->getData())) { + if (in_array($itemName, $this->defaultRoles)) { + return true; + } + if (isset($assignments[$itemName])) { + $assignment = $assignments[$itemName]; + if ($this->executeBizRule($assignment->getBizRule(), $params, $assignment->getData())) { + return true; + } + } + $query = new Query; + $parents = $query->select(array('parent')) + ->from($this->itemChildTable) + ->where(array('child' => $itemName)) + ->createCommand($this->db) + ->queryColumn(); + foreach ($parents as $parent) { + if ($this->checkAccessRecursive($userId, $parent, $params, $assignments)) { + return true; + } + } + } + return false; + } + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the item is added successfully + * @throws Exception if either parent or child doesn't exist. + * @throws InvalidCallException if a loop has been detected. + */ + public function addItemChild($itemName, $childName) + { + if ($itemName === $childName) { + throw new Exception("Cannot add '$itemName' as a child of itself."); + } + $query = new Query; + $rows = $query->from($this->itemTable) + ->where(array('or', 'name=:name1', 'name=:name2'), array( + ':name1' => $itemName, + ':name2' => $childName + )) + ->createCommand($this->db) + ->queryAll(); + if (count($rows) == 2) { + if ($rows[0]['name'] === $itemName) { + $parentType = $rows[0]['type']; + $childType = $rows[1]['type']; + } else { + $childType = $rows[0]['type']; + $parentType = $rows[1]['type']; + } + $this->checkItemChildType($parentType, $childType); + if ($this->detectLoop($itemName, $childName)) { + 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, + )); + return true; + } else { + throw new Exception("Either '$itemName' or '$childName' does not exist."); + } + } + + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + public function removeItemChild($itemName, $childName) + { + return $this->db->createCommand() + ->delete($this->itemChildTable, array( + 'parent' => $itemName, + 'child' => $childName + )) > 0; + } + + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + public function hasItemChild($itemName, $childName) + { + $query = new Query; + return $query->select(array('parent')) + ->from($this->itemChildTable) + ->where(array( + 'parent' => $itemName, + 'child' => $childName + )) + ->createCommand($this->db) + ->queryScalar() !== false; + } + + /** + * Returns the children of the specified item. + * @param mixed $names the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + public function getItemChildren($names) + { + $query = new Query; + $rows = $query->select(array('name', 'type', 'description', 'bizrule', 'data')) + ->from(array( + $this->itemTable, + $this->itemChildTable + )) + ->where(array('parent' => $names, 'name' => new Expression('child'))) + ->createCommand($this->db) + ->queryAll(); + $children = array(); + foreach ($rows as $row) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + $children[$row['name']] = new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + } + return $children; + } + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws InvalidParamException if the item does not exist or if the item has already been assigned to the user + */ + public function assign($userId, $itemName, $bizRule = null, $data = null) + { + if ($this->usingSqlite() && $this->getItem($itemName) === null) { + throw new InvalidParamException("The item '$itemName' does not exist."); + } + $this->db->createCommand() + ->insert($this->assignmentTable, array( + 'user_id' => $userId, + 'item_name' => $itemName, + 'bizrule' => $bizRule, + 'data' => serialize($data) + )); + return new Assignment($this, $userId, $itemName, $bizRule, $data); + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + public function revoke($userId, $itemName) + { + return $this->db->createCommand() + ->delete($this->assignmentTable, array( + 'user_id' => $userId, + 'item_name' => $itemName + )) > 0; + } + + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + public function isAssigned($itemName, $userId) + { + $query = new Query; + return $query->select(array('item_name')) + ->from($this->assignmentTable) + ->where(array( + 'user_id' => $userId, + 'item_name' => $itemName + )) + ->createCommand($this->db) + ->queryScalar() !== false; + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + public function getAssignment($userId, $itemName) + { + $query = new Query; + $row = $query->from($this->assignmentTable) + ->where(array( + 'user_id' => $userId, + 'item_name' => $itemName + )) + ->createCommand($this->db) + ->queryRow(); + if ($row !== false) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + return new Assignment($this, $row['user_id'], $row['item_name'], $row['bizrule'], $data); + } else { + return null; + } + } + + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Assignment[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + public function getAssignments($userId) + { + $query = new Query; + $rows = $query->from($this->assignmentTable) + ->where(array('user_id' => $userId)) + ->createCommand($this->db) + ->queryAll(); + $assignments = array(); + foreach ($rows as $row) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + $assignments[$row['item_name']] = new Assignment($this, $row['user_id'], $row['item_name'], $row['bizrule'], $data); + } + return $assignments; + } + + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + public function saveAssignment($assignment) + { + $this->db->createCommand() + ->update($this->assignmentTable, array( + 'bizrule' => $assignment->getBizRule(), + 'data' => serialize($assignment->getData()), + ), array( + 'user_id' => $assignment->getUserId(), + 'item_name' => $assignment->getItemName() + )); + } + + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + public function getItems($userId = null, $type = null) + { + $query = new Query; + if ($userId === null && $type === null) { + $command = $query->from($this->itemTable) + ->createCommand($this->db); + } elseif ($userId === null) { + $command = $query->from($this->itemTable) + ->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' + )) + ->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'), + )) + ->createCommand($this->db); + } + $items = array(); + foreach ($command->queryAll() as $row) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + $items[$row['name']] = new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + } + return $items; + } + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @return Item the authorization item + * @throws Exception if an item with the same name already exists + */ + public function createItem($name, $type, $description = '', $bizRule = null, $data = null) + { + $this->db->createCommand() + ->insert($this->itemTable, array( + 'name' => $name, + 'type' => $type, + 'description' => $description, + 'bizrule' => $bizRule, + 'data' => serialize($data) + )); + return new Item($this, $name, $type, $description, $bizRule, $data); + } + + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + public function removeItem($name) + { + 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)); + } + return $this->db->createCommand()->delete($this->itemTable, array('name' => $name)) > 0; + } + + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + public function getItem($name) + { + $query = new Query; + $row = $query->from($this->itemTable) + ->where(array('name' => $name)) + ->createCommand($this->db) + ->queryRow(); + + 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 null; + } + + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + */ + public function saveItem($item, $oldName = null) + { + if ($this->usingSqlite() && $oldName !== null && $item->getName() !== $oldName) { + $this->db->createCommand() + ->update($this->itemChildTable, array( + 'parent' => $item->getName(), + ), array( + 'parent' => $oldName, + )); + $this->db->createCommand() + ->update($this->itemChildTable, array( + 'child' => $item->getName(), + ), array( + 'child' => $oldName, + )); + $this->db->createCommand() + ->update($this->assignmentTable, array( + 'item_name' => $item->getName(), + ), array( + 'item_name' => $oldName, + )); + } + + $this->db->createCommand() + ->update($this->itemTable, array( + 'name' => $item->getName(), + 'type' => $item->getType(), + 'description' => $item->getDescription(), + 'bizrule' => $item->getBizRule(), + 'data' => serialize($item->getData()), + ), array( + 'name' => $oldName === null ? $item->getName() : $oldName, + )); + } + + /** + * Saves the authorization data to persistent storage. + */ + public function save() + { + } + + /** + * Removes all authorization data. + */ + public function clearAll() + { + $this->clearAssignments(); + $this->db->createCommand()->delete($this->itemChildTable); + $this->db->createCommand()->delete($this->itemTable); + } + + /** + * Removes all authorization assignments. + */ + public function clearAssignments() + { + $this->db->createCommand()->delete($this->assignmentTable); + } + + /** + * Checks whether there is a loop in the authorization item hierarchy. + * @param string $itemName parent item name + * @param string $childName the name of the child item that is to be added to the hierarchy + * @return boolean whether a loop exists + */ + protected function detectLoop($itemName, $childName) + { + if ($childName === $itemName) { + return true; + } + foreach ($this->getItemChildren($childName) as $child) { + if ($this->detectLoop($itemName, $child->getName())) { + return true; + } + } + return false; + } + + /** + * @return boolean whether the database is a SQLite database + */ + protected function usingSqlite() + { + return $this->_usingSqlite; + } +} diff --git a/yii/rbac/Item.php b/yii/rbac/Item.php new file mode 100644 index 0000000..ef56a53 --- /dev/null +++ b/yii/rbac/Item.php @@ -0,0 +1,275 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class Item extends Object +{ + const TYPE_OPERATION = 0; + 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 + */ + 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; + } + + /** + * Checks to see if the specified item is within the hierarchy starting from this item. + * This method is expected to be internally used by the actual implementations + * of the [[Manager::checkAccess()]]. + * @param string $itemName the name of the item to be checked + * @param array $params the parameters to be passed to business rule evaluation + * @return boolean whether the specified item is within the hierarchy starting from this item. + */ + public function checkAccess($itemName, $params = array()) + { + Yii::trace('Checking permission: ' . $this->_name, __METHOD__); + if ($this->_auth->executeBizRule($this->_bizRule, $params, $this->_data)) { + if ($this->_name == $itemName) { + return true; + } + foreach ($this->_auth->getItemChildren($this->_name) as $item) { + if ($item->checkAccess($itemName, $params)) { + return true; + } + } + } + return false; + } + + /** + * @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() + { + return $this->_name; + } + + /** + * @param string $value the item name + */ + public function setName($value) + { + if ($this->_name !== $value) { + $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); + } + } + + /** + * Adds a child item. + * @param string $name the name of the child item + * @return boolean whether the item is added successfully + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + * @see Manager::addItemChild + */ + public function addChild($name) + { + return $this->_auth->addItemChild($this->_name, $name); + } + + /** + * Removes a child item. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $name the child item name + * @return boolean whether the removal is successful + * @see Manager::removeItemChild + */ + public function removeChild($name) + { + return $this->_auth->removeItemChild($this->_name, $name); + } + + /** + * Returns a value indicating whether a child exists + * @param string $name the child item name + * @return boolean whether the child exists + * @see Manager::hasItemChild + */ + public function hasChild($name) + { + return $this->_auth->hasItemChild($this->_name, $name); + } + + /** + * Returns the children of this item. + * @return Item[] all child items of this item. + * @see Manager::getItemChildren + */ + public function getChildren() + { + return $this->_auth->getItemChildren($this->_name); + } + + /** + * Assigns this item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws \yii\base\Exception if the item has already been assigned to the user + * @see Manager::assign + */ + public function assign($userId, $bizRule = null, $data = null) + { + return $this->_auth->assign($userId, $this->_name, $bizRule, $data); + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return boolean whether removal is successful + * @see Manager::revoke + */ + public function revoke($userId) + { + return $this->_auth->revoke($userId, $this->_name); + } + + /** + * Returns a value indicating whether this item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return boolean whether the item has been assigned to the user. + * @see Manager::isAssigned + */ + public function isAssigned($userId) + { + return $this->_auth->isAssigned($userId, $this->_name); + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Assignment the item assignment information. Null is returned if + * this item is not assigned to the user. + * @see Manager::getAssignment + */ + public function getAssignment($userId) + { + return $this->_auth->getAssignment($userId, $this->_name); + } +} diff --git a/yii/rbac/Manager.php b/yii/rbac/Manager.php new file mode 100644 index 0000000..5c3c4f6 --- /dev/null +++ b/yii/rbac/Manager.php @@ -0,0 +1,312 @@ +Item). + * @property array $tasks Tasks (name=>Item). + * @property array $operations Operations (name=>Item). + * + * @author Qiang Xue + * @author Alexander Kochetov + * @since 2.0 + */ +abstract class Manager extends Component +{ + /** + * @var boolean Enable error reporting for bizRules. + */ + public $showErrors = false; + + /** + * @var array list of role names that are assigned to all users implicitly. + * These roles do not need to be explicitly assigned to any user. + * When calling [[checkAccess()]], these roles will be checked first. + * For performance reason, you should minimize the number of such roles. + * A typical usage of such roles is to define an 'authenticated' role and associate + * it with a biz rule which checks if the current user is authenticated. + * And then declare 'authenticated' in this property so that it can be applied to + * every authenticated user. + */ + public $defaultRoles = array(); + + /** + * Creates a role. + * This is a shortcut method to [[Manager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createRole($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_ROLE, $description, $bizRule, $data); + } + + /** + * Creates a task. + * This is a shortcut method to [[Manager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createTask($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_TASK, $description, $bizRule, $data); + } + + /** + * Creates an operation. + * This is a shortcut method to [[Manager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createOperation($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_OPERATION, $description, $bizRule, $data); + } + + /** + * Returns roles. + * 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) + */ + public function getRoles($userId = null) + { + return $this->getItems($userId, Item::TYPE_ROLE); + } + + /** + * Returns tasks. + * 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) + */ + public function getTasks($userId = null) + { + return $this->getItems($userId, Item::TYPE_TASK); + } + + /** + * Returns operations. + * 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) + */ + public function getOperations($userId = null) + { + return $this->getItems($userId, Item::TYPE_OPERATION); + } + + /** + * Executes the specified business rule. + * @param string $bizRule the business rule to be executed. + * @param array $params parameters passed to [[Manager::checkAccess()]]. + * @param mixed $data additional data associated with the authorization item or assignment. + * @return boolean whether the business rule returns true. + * If the business rule is empty, it will still return true. + */ + public function executeBizRule($bizRule, $params, $data) + { + return $bizRule === '' || $bizRule === null || ($this->showErrors ? eval($bizRule) != 0 : @eval($bizRule) != 0); + } + + /** + * Checks the item types to make sure a child can be added to a parent. + * @param integer $parentType parent item type + * @param integer $childType child item type + * @throws InvalidParamException if the item cannot be added as a child due to its incompatible type. + */ + protected function checkItemChildType($parentType, $childType) + { + 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]'."); + } + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This should be either an integer or a string representing + * the unique identifier of a user. See [[User::id]]. + * @param string $itemName the name of the operation that we are checking access to + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. + * @return boolean whether the operations can be performed by the user. + */ + abstract public function checkAccess($userId, $itemName, $params = array()); + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @throws \yii\base\Exception if an item with the same name already exists + * @return Item the authorization item + */ + abstract public function createItem($name, $type, $description = '', $bizRule = null, $data = null); + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + abstract public function removeItem($name); + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + abstract public function getItems($userId = null, $type = null); + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + abstract public function getItem($name); + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + */ + abstract public function saveItem($item, $oldName = null); + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + */ + abstract public function addItemChild($itemName, $childName); + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + abstract public function removeItemChild($itemName, $childName); + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + abstract public function hasItemChild($itemName, $childName); + /** + * Returns the children of the specified item. + * @param mixed $itemName the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + abstract public function getItemChildren($itemName); + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws \yii\base\Exception if the item does not exist or if the item has already been assigned to the user + */ + abstract public function assign($userId, $itemName, $bizRule = null, $data = null); + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + abstract public function revoke($userId, $itemName); + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + abstract public function isAssigned($userId, $itemName); + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + abstract public function getAssignment($userId, $itemName); + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Item[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + abstract public function getAssignments($userId); + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + abstract public function saveAssignment($assignment); + /** + * Removes all authorization data. + */ + abstract public function clearAll(); + /** + * Removes all authorization assignments. + */ + abstract public function clearAssignments(); + /** + * Saves authorization data into persistent storage. + * If any change is made to the authorization data, please make + * sure you call this method to save the changed data into persistent storage. + */ + abstract public function save(); +} diff --git a/yii/rbac/PhpManager.php b/yii/rbac/PhpManager.php new file mode 100644 index 0000000..8a4dbec --- /dev/null +++ b/yii/rbac/PhpManager.php @@ -0,0 +1,516 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class PhpManager extends Manager +{ + /** + * @var string the path of the PHP script that contains the authorization data. + * If not set, it will be using 'protected/data/rbac.php' as the data file. + * Make sure this file is writable by the Web server process if the authorization + * needs to be changed. + * @see loadFromFile + * @see saveToFile + */ + public $authFile; + + private $_items = array(); // itemName => item + private $_children = array(); // itemName, childName => child + private $_assignments = array(); // userId, itemName => assignment + + /** + * Initializes the application component. + * This method overrides parent implementation by loading the authorization data + * from PHP script. + */ + public function init() + { + parent::init(); + if ($this->authFile === null) { + $this->authFile = Yii::getAlias('@app/data/rbac') . '.php'; + } + $this->load(); + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This can be either an integer or a string representing + * @param string $itemName the name of the operation that need access check + * the unique identifier of a user. See [[User::id]]. + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to + * this array, which holds the value of `$userId`. + * @return boolean whether the operations can be performed by the user. + */ + public function checkAccess($userId, $itemName, $params = array()) + { + if (!isset($this->_items[$itemName])) { + return false; + } + /** @var $item Item */ + $item = $this->_items[$itemName]; + Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); + if (!isset($params['userId'])) { + $params['userId'] = $userId; + } + if ($this->executeBizRule($item->getBizRule(), $params, $item->getData())) { + 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())) { + return true; + } + } + foreach ($this->_children as $parentName => $children) { + if (isset($children[$itemName]) && $this->checkAccess($userId, $parentName, $params)) { + return true; + } + } + } + return false; + } + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the item is added successfully + * @throws Exception if either parent or child doesn't exist. + * @throws InvalidCallException if item already has a child with $itemName or if a loop has been detected. + */ + public function addItemChild($itemName, $childName) + { + if (!isset($this->_items[$childName], $this->_items[$itemName])) { + throw new Exception("Either '$itemName' or '$childName' does not exist."); + } + /** @var $child Item */ + $child = $this->_items[$childName]; + /** @var $item Item */ + $item = $this->_items[$itemName]; + $this->checkItemChildType($item->getType(), $child->getType()); + if ($this->detectLoop($itemName, $childName)) { + throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); + } + if (isset($this->_children[$itemName][$childName])) { + throw new InvalidCallException("The item '$itemName' already has a child '$childName'."); + } + $this->_children[$itemName][$childName] = $this->_items[$childName]; + return true; + } + + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + public function removeItemChild($itemName, $childName) + { + if (isset($this->_children[$itemName][$childName])) { + unset($this->_children[$itemName][$childName]); + return true; + } else { + return false; + } + } + + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + public function hasItemChild($itemName, $childName) + { + return isset($this->_children[$itemName][$childName]); + } + + /** + * Returns the children of the specified item. + * @param mixed $names the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + public function getItemChildren($names) + { + if (is_string($names)) { + return isset($this->_children[$names]) ? $this->_children[$names] : array(); + } + + $children = array(); + foreach ($names as $name) { + if (isset($this->_children[$name])) { + $children = array_merge($children, $this->_children[$name]); + } + } + return $children; + } + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws InvalidParamException if the item does not exist or if the item has already been assigned to the user + */ + public function assign($userId, $itemName, $bizRule = null, $data = null) + { + if (!isset($this->_items[$itemName])) { + throw new InvalidParamException("Unknown authorization item '$itemName'."); + } 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); + } + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + public function revoke($userId, $itemName) + { + if (isset($this->_assignments[$userId][$itemName])) { + unset($this->_assignments[$userId][$itemName]); + return true; + } else { + return false; + } + } + + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + public function isAssigned($userId, $itemName) + { + return isset($this->_assignments[$userId][$itemName]); + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + public function getAssignment($userId, $itemName) + { + return isset($this->_assignments[$userId][$itemName]) ? $this->_assignments[$userId][$itemName] : null; + } + + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Assignment[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + public function getAssignments($userId) + { + return isset($this->_assignments[$userId]) ? $this->_assignments[$userId] : array(); + } + + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + public function getItems($userId = null, $type = null) + { + if ($userId === null && $type === null) { + return $this->_items; + } + $items = array(); + if ($userId === null) { + foreach ($this->_items as $name => $item) { + /** @var $item Item */ + if ($item->getType() == $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)) { + $items[$name] = $this->_items[$name]; + } + } + } + return $items; + } + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @return Item the authorization item + * @throws Exception if an item with the same name already exists + */ + public function createItem($name, $type, $description = '', $bizRule = null, $data = null) + { + 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); + } + + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + public function removeItem($name) + { + if (isset($this->_items[$name])) { + foreach ($this->_children as &$children) { + unset($children[$name]); + } + foreach ($this->_assignments as &$assignments) { + unset($assignments[$name]); + } + unset($this->_items[$name]); + return true; + } else { + return false; + } + } + + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + public function getItem($name) + { + return isset($this->_items[$name]) ? $this->_items[$name] : null; + } + + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + * @throws InvalidParamException if an item with the same name already taken + */ + public function saveItem($item, $oldName = null) + { + 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."); + } + if (isset($this->_items[$oldName]) && $this->_items[$oldName] === $item) { + unset($this->_items[$oldName]); + $this->_items[$newName] = $item; + if (isset($this->_children[$oldName])) { + $this->_children[$newName] = $this->_children[$oldName]; + unset($this->_children[$oldName]); + } + foreach ($this->_children as &$children) { + if (isset($children[$oldName])) { + $children[$newName] = $children[$oldName]; + unset($children[$oldName]); + } + } + foreach ($this->_assignments as &$assignments) { + if (isset($assignments[$oldName])) { + $assignments[$newName] = $assignments[$oldName]; + unset($assignments[$oldName]); + } + } + } + } + } + + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + public function saveAssignment($assignment) + { + } + + /** + * Saves authorization data into persistent storage. + * If any change is made to the authorization data, please make + * sure you call this method to save the changed data into persistent storage. + */ + public function save() + { + $items = array(); + foreach ($this->_items as $name => $item) { + /** @var $item Item */ + $items[$name] = array( + 'type' => $item->getType(), + 'description' => $item->getDescription(), + 'bizRule' => $item->getBizRule(), + 'data' => $item->getData(), + ); + if (isset($this->_children[$name])) { + foreach ($this->_children[$name] as $child) { + /** @var $child Item */ + $items[$name]['children'][] = $child->getName(); + } + } + } + + foreach ($this->_assignments as $userId => $assignments) { + foreach ($assignments as $name => $assignment) { + /** @var $assignment Assignment */ + if (isset($items[$name])) { + $items[$name]['assignments'][$userId] = array( + 'bizRule' => $assignment->getBizRule(), + 'data' => $assignment->getData(), + ); + } + } + } + + $this->saveToFile($items, $this->authFile); + } + + /** + * Loads authorization data. + */ + public function load() + { + $this->clearAll(); + + $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']); + } + + foreach ($items as $name => $item) { + if (isset($item['children'])) { + foreach ($item['children'] as $childName) { + if (isset($this->_items[$childName])) { + $this->_children[$name][$childName] = $this->_items[$childName]; + } + } + } + if (isset($item['assignments'])) { + foreach ($item['assignments'] as $userId => $assignment) { + $this->_assignments[$userId][$name] = new Assignment($this, $name, $userId, $assignment['bizRule'], $assignment['data']); + } + } + } + } + + /** + * Removes all authorization data. + */ + public function clearAll() + { + $this->clearAssignments(); + $this->_children = array(); + $this->_items = array(); + } + + /** + * Removes all authorization assignments. + */ + public function clearAssignments() + { + $this->_assignments = array(); + } + + /** + * Checks whether there is a loop in the authorization item hierarchy. + * @param string $itemName parent item name + * @param string $childName the name of the child item that is to be added to the hierarchy + * @return boolean whether a loop exists + */ + protected function detectLoop($itemName, $childName) + { + if ($childName === $itemName) { + return true; + } + if (!isset($this->_children[$childName], $this->_items[$itemName])) { + return false; + } + foreach ($this->_children[$childName] as $child) { + /** @var $child Item */ + if ($this->detectLoop($itemName, $child->getName())) { + return true; + } + } + return false; + } + + /** + * Loads the authorization data from a PHP script file. + * @param string $file the file path. + * @return array the authorization data + * @see saveToFile + */ + protected function loadFromFile($file) + { + if (is_file($file)) { + return require($file); + } else { + return array(); + } + } + + /** + * Saves the authorization data to a PHP script file. + * @param array $data the authorization data + * @param string $file the file path. + * @see loadFromFile + */ + protected function saveToFile($data, $file) + { + file_put_contents($file, " + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists [tbl_auth_assignment]; +drop table if exists [tbl_auth_item_child]; +drop table if exists [tbl_auth_item]; + +create table [tbl_auth_item] +( + [name] varchar(64) not null, + [type] integer not null, + [description] text, + [bizrule] text, + [data] text, + primary key ([name]) +); + +create table [tbl_auth_item_child] +( + [parent] varchar(64) not null, + [child] varchar(64) not null, + primary key ([parent],[child]), + foreign key ([parent]) references [tbl_auth_item] ([name]) on delete cascade on update cascade, + foreign key ([child]) references [tbl_auth_item] ([name]) on delete cascade on update cascade +); + +create table [tbl_auth_assignment] +( + [item_name] varchar(64) not null, + [user_id] varchar(64) not null, + [bizrule] 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/yii/rbac/schema-mysql.sql new file mode 100644 index 0000000..aa8015b --- /dev/null +++ b/yii/rbac/schema-mysql.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists `tbl_auth_assignment`; +drop table if exists `tbl_auth_item_child`; +drop table if exists `tbl_auth_item`; + +create table `tbl_auth_item` +( + `name` varchar(64) not null, + `type` integer not null, + `description` text, + `bizrule` text, + `data` text, + primary key (`name`) +) engine InnoDB; + +create table `tbl_auth_item_child` +( + `parent` varchar(64) not null, + `child` varchar(64) not null, + primary key (`parent`,`child`), + foreign key (`parent`) references `tbl_auth_item` (`name`) on delete cascade on update cascade, + foreign key (`child`) references `tbl_auth_item` (`name`) on delete cascade on update cascade +) engine InnoDB; + +create table `tbl_auth_assignment` +( + `item_name` varchar(64) not null, + `user_id` varchar(64) not null, + `bizrule` text, + `data` text, + primary key (`item_name`,`user_id`), + foreign key (`item_name`) references `tbl_auth_item` (`name`) on delete cascade on update cascade +) engine InnoDB; diff --git a/yii/rbac/schema-oci.sql b/yii/rbac/schema-oci.sql new file mode 100644 index 0000000..16b7964 --- /dev/null +++ b/yii/rbac/schema-oci.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists "tbl_auth_assignment"; +drop table if exists "tbl_auth_item_child"; +drop table if exists "tbl_auth_item"; + +create table "tbl_auth_item" +( + "name" varchar(64) not null, + "type" integer not null, + "description" text, + "bizrule" text, + "data" text, + primary key ("name") +); + +create table "tbl_auth_item_child" +( + "parent" varchar(64) not null, + "child" varchar(64) not null, + primary key ("parent","child"), + foreign key ("parent") references "tbl_auth_item" ("name") on delete cascade on update cascade, + foreign key ("child") references "tbl_auth_item" ("name") on delete cascade on update cascade +); + +create table "tbl_auth_assignment" +( + "item_name" varchar(64) not null, + "user_id" varchar(64) not null, + "bizrule" 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/yii/rbac/schema-pgsql.sql new file mode 100644 index 0000000..16b7964 --- /dev/null +++ b/yii/rbac/schema-pgsql.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists "tbl_auth_assignment"; +drop table if exists "tbl_auth_item_child"; +drop table if exists "tbl_auth_item"; + +create table "tbl_auth_item" +( + "name" varchar(64) not null, + "type" integer not null, + "description" text, + "bizrule" text, + "data" text, + primary key ("name") +); + +create table "tbl_auth_item_child" +( + "parent" varchar(64) not null, + "child" varchar(64) not null, + primary key ("parent","child"), + foreign key ("parent") references "tbl_auth_item" ("name") on delete cascade on update cascade, + foreign key ("child") references "tbl_auth_item" ("name") on delete cascade on update cascade +); + +create table "tbl_auth_assignment" +( + "item_name" varchar(64) not null, + "user_id" varchar(64) not null, + "bizrule" 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/yii/rbac/schema-sqlite.sql new file mode 100644 index 0000000..668196e --- /dev/null +++ b/yii/rbac/schema-sqlite.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists 'tbl_auth_assignment'; +drop table if exists 'tbl_auth_item_child'; +drop table if exists 'tbl_auth_item'; + +create table 'tbl_auth_item' +( + "name" varchar(64) not null, + "type" integer not null, + "description" text, + "bizrule" text, + "data" text, + primary key ("name") +); + +create table 'tbl_auth_item_child' +( + "parent" varchar(64) not null, + "child" varchar(64) not null, + primary key ("parent","child"), + foreign key ("parent") references 'tbl_auth_item' ("name") on delete cascade on update cascade, + foreign key ("child") references 'tbl_auth_item' ("name") on delete cascade on update cascade +); + +create table 'tbl_auth_assignment' +( + "item_name" varchar(64) not null, + "user_id" varchar(64) not null, + "bizrule" 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/renderers/SmartyViewRenderer.php b/yii/renderers/SmartyViewRenderer.php similarity index 92% rename from framework/renderers/SmartyViewRenderer.php rename to yii/renderers/SmartyViewRenderer.php index 920f3c6..c6147c0 100644 --- a/framework/renderers/SmartyViewRenderer.php +++ b/yii/renderers/SmartyViewRenderer.php @@ -24,11 +24,6 @@ use yii\helpers\Html; class SmartyViewRenderer extends ViewRenderer { /** - * @var string the directory or path alias pointing to where Smarty code is located. - */ - public $smartyPath = '@app/vendors/Smarty'; - - /** * @var string the directory or path alias pointing to where Smarty cache will be stored. */ public $cachePath = '@app/runtime/Smarty/cache'; @@ -45,7 +40,6 @@ class SmartyViewRenderer extends ViewRenderer public function init() { - require_once(Yii::getAlias($this->smartyPath) . '/Smarty.class.php'); $this->smarty = new Smarty(); $this->smarty->setCompileDir(Yii::getAlias($this->compilePath)); $this->smarty->setCacheDir(Yii::getAlias($this->cachePath)); @@ -102,4 +96,4 @@ class SmartyViewRenderer extends ViewRenderer return $template->fetch(); } -} \ No newline at end of file +} diff --git a/framework/renderers/TwigViewRenderer.php b/yii/renderers/TwigViewRenderer.php similarity index 89% rename from framework/renderers/TwigViewRenderer.php rename to yii/renderers/TwigViewRenderer.php index 79faa9f..de561d3 100644 --- a/framework/renderers/TwigViewRenderer.php +++ b/yii/renderers/TwigViewRenderer.php @@ -23,11 +23,6 @@ use yii\helpers\Html; class TwigViewRenderer extends ViewRenderer { /** - * @var string the directory or path alias pointing to where Twig code is located. - */ - public $twigPath = '@Twig'; - - /** * @var string the directory or path alias pointing to where Twig cache will be stored. */ public $cachePath = '@app/runtime/Twig/cache'; @@ -45,10 +40,6 @@ class TwigViewRenderer extends ViewRenderer public function init() { - if (!isset(Yii::$aliases['@Twig'])) { - Yii::setAlias('@Twig', $this->twigPath); - } - $loader = new \Twig_Loader_String(); $this->twig = new \Twig_Environment($loader, array_merge(array( diff --git a/framework/test/TestCase.php b/yii/test/TestCase.php similarity index 100% rename from framework/test/TestCase.php rename to yii/test/TestCase.php diff --git a/framework/test/WebTestCase.php b/yii/test/WebTestCase.php similarity index 100% rename from framework/test/WebTestCase.php rename to yii/test/WebTestCase.php diff --git a/framework/validators/BooleanValidator.php b/yii/validators/BooleanValidator.php similarity index 100% rename from framework/validators/BooleanValidator.php rename to yii/validators/BooleanValidator.php diff --git a/framework/validators/CaptchaValidator.php b/yii/validators/CaptchaValidator.php similarity index 97% rename from framework/validators/CaptchaValidator.php rename to yii/validators/CaptchaValidator.php index 4eba9df..2e58cf2 100644 --- a/framework/validators/CaptchaValidator.php +++ b/yii/validators/CaptchaValidator.php @@ -21,6 +21,7 @@ use yii\helpers\Html; */ class CaptchaValidator extends Validator { + public $skipOnEmpty = false; /** * @var boolean whether the comparison is case sensitive. Defaults to false. */ @@ -70,7 +71,7 @@ class CaptchaValidator extends Validator /** * Returns the CAPTCHA action object. * @throws InvalidConfigException - * @return CaptchaAction the action object + * @return \yii\web\CaptchaAction the action object */ public function getCaptchaAction() { diff --git a/framework/validators/CompareValidator.php b/yii/validators/CompareValidator.php similarity index 100% rename from framework/validators/CompareValidator.php rename to yii/validators/CompareValidator.php diff --git a/framework/validators/DateValidator.php b/yii/validators/DateValidator.php similarity index 100% rename from framework/validators/DateValidator.php rename to yii/validators/DateValidator.php diff --git a/framework/validators/DefaultValueValidator.php b/yii/validators/DefaultValueValidator.php similarity index 100% rename from framework/validators/DefaultValueValidator.php rename to yii/validators/DefaultValueValidator.php diff --git a/framework/validators/EmailValidator.php b/yii/validators/EmailValidator.php similarity index 99% rename from framework/validators/EmailValidator.php rename to yii/validators/EmailValidator.php index ad74dd6..949b3f9 100644 --- a/framework/validators/EmailValidator.php +++ b/yii/validators/EmailValidator.php @@ -9,7 +9,7 @@ namespace yii\validators; use Yii; use yii\helpers\Html; -use yii\helpers\JsExpression; +use yii\web\JsExpression; use yii\helpers\Json; /** diff --git a/framework/validators/ExistValidator.php b/yii/validators/ExistValidator.php similarity index 100% rename from framework/validators/ExistValidator.php rename to yii/validators/ExistValidator.php diff --git a/framework/validators/FileValidator.php b/yii/validators/FileValidator.php similarity index 97% rename from framework/validators/FileValidator.php rename to yii/validators/FileValidator.php index 0fddcf5..cc36d07 100644 --- a/framework/validators/FileValidator.php +++ b/yii/validators/FileValidator.php @@ -116,7 +116,7 @@ class FileValidator extends Validator } if (!is_array($this->types)) { $this->types = preg_split('/[\s,]+/', strtolower($this->types), -1, PREG_SPLIT_NO_EMPTY); - } + } } /** @@ -138,8 +138,8 @@ class FileValidator extends Validator } } $object->$attribute = array_values($files); - if ($files === array()) { - $this->addError($object, $attribute, $this->uploadRequired); + if (empty($files)) { + $this->addError($object, $attribute, $this->uploadRequired); } if (count($files) > $this->maxFiles) { $this->addError($object, $attribute, $this->tooMany, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles)); @@ -153,7 +153,7 @@ class FileValidator extends Validator if ($file instanceof UploadedFile && $file->getError() != UPLOAD_ERR_NO_FILE) { $this->validateFile($object, $attribute, $file); } else { - $this->addError($object, $attribute, $this->uploadRequired); + $this->addError($object, $attribute, $this->uploadRequired); } } } @@ -192,11 +192,11 @@ class FileValidator extends Validator 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->getName(), __METHOD__); break; case UPLOAD_ERR_EXTENSION: $this->addError($object, $attribute, $this->message); - Yii::warning('File upload was stopped by some PHP extension: ', $file->getName(), __METHOD__); + Yii::warning('File upload was stopped by some PHP extension: ' . $file->getName(), __METHOD__); break; default: break; @@ -248,4 +248,4 @@ class FileValidator extends Validator return (int)$sizeStr; } } -} \ No newline at end of file +} diff --git a/framework/validators/FilterValidator.php b/yii/validators/FilterValidator.php similarity index 100% rename from framework/validators/FilterValidator.php rename to yii/validators/FilterValidator.php diff --git a/framework/validators/InlineValidator.php b/yii/validators/InlineValidator.php similarity index 99% rename from framework/validators/InlineValidator.php rename to yii/validators/InlineValidator.php index 3689a2f..8af5bbc 100644 --- a/framework/validators/InlineValidator.php +++ b/yii/validators/InlineValidator.php @@ -96,4 +96,4 @@ class InlineValidator extends Validator return null; } } -} \ No newline at end of file +} diff --git a/framework/validators/NumberValidator.php b/yii/validators/NumberValidator.php similarity index 99% rename from framework/validators/NumberValidator.php rename to yii/validators/NumberValidator.php index d0a4002..10f0e52 100644 --- a/framework/validators/NumberValidator.php +++ b/yii/validators/NumberValidator.php @@ -9,7 +9,7 @@ namespace yii\validators; use Yii; use yii\helpers\Html; -use yii\helpers\JsExpression; +use yii\web\JsExpression; use yii\helpers\Json; /** @@ -151,4 +151,4 @@ class NumberValidator extends Validator return 'yii.validation.number(value, messages, ' . Json::encode($options) . ');'; } -} \ No newline at end of file +} diff --git a/framework/validators/RangeValidator.php b/yii/validators/RangeValidator.php similarity index 100% rename from framework/validators/RangeValidator.php rename to yii/validators/RangeValidator.php diff --git a/framework/validators/RegularExpressionValidator.php b/yii/validators/RegularExpressionValidator.php similarity index 99% rename from framework/validators/RegularExpressionValidator.php rename to yii/validators/RegularExpressionValidator.php index 79a1a3c..23419b9 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/yii/validators/RegularExpressionValidator.php @@ -10,7 +10,7 @@ namespace yii\validators; use Yii; use yii\base\InvalidConfigException; use yii\helpers\Html; -use yii\helpers\JsExpression; +use yii\web\JsExpression; use yii\helpers\Json; /** diff --git a/framework/validators/RequiredValidator.php b/yii/validators/RequiredValidator.php similarity index 100% rename from framework/validators/RequiredValidator.php rename to yii/validators/RequiredValidator.php diff --git a/framework/validators/StringValidator.php b/yii/validators/StringValidator.php similarity index 100% rename from framework/validators/StringValidator.php rename to yii/validators/StringValidator.php diff --git a/framework/validators/UniqueValidator.php b/yii/validators/UniqueValidator.php similarity index 99% rename from framework/validators/UniqueValidator.php rename to yii/validators/UniqueValidator.php index 2240e0a..7072ff4 100644 --- a/framework/validators/UniqueValidator.php +++ b/yii/validators/UniqueValidator.php @@ -97,4 +97,4 @@ class UniqueValidator extends Validator $this->addError($object, $attribute, $this->message); } } -} \ No newline at end of file +} diff --git a/framework/validators/UrlValidator.php b/yii/validators/UrlValidator.php similarity index 99% rename from framework/validators/UrlValidator.php rename to yii/validators/UrlValidator.php index 0ed59bd..c418353 100644 --- a/framework/validators/UrlValidator.php +++ b/yii/validators/UrlValidator.php @@ -9,7 +9,7 @@ namespace yii\validators; use Yii; use yii\helpers\Html; -use yii\helpers\JsExpression; +use yii\web\JsExpression; use yii\helpers\Json; /** diff --git a/framework/validators/Validator.php b/yii/validators/Validator.php similarity index 100% rename from framework/validators/Validator.php rename to yii/validators/Validator.php diff --git a/framework/views/error.php b/yii/views/error.php similarity index 99% rename from framework/views/error.php rename to yii/views/error.php index 548d04b..009050a 100644 --- a/framework/views/error.php +++ b/yii/views/error.php @@ -64,4 +64,4 @@ $title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $excep versionInfo : ''?> - \ No newline at end of file + diff --git a/framework/views/exception.php b/yii/views/exception.php similarity index 100% rename from framework/views/exception.php rename to yii/views/exception.php diff --git a/framework/views/migration.php b/yii/views/migration.php similarity index 100% rename from framework/views/migration.php rename to yii/views/migration.php diff --git a/framework/web/AccessControl.php b/yii/web/AccessControl.php similarity index 99% rename from framework/web/AccessControl.php rename to yii/web/AccessControl.php index f5983ae..e890510 100644 --- a/framework/web/AccessControl.php +++ b/yii/web/AccessControl.php @@ -103,4 +103,4 @@ class AccessControl extends ActionFilter throw new HttpException(403, Yii::t('yii|You are not allowed to perform this action.')); } } -} \ No newline at end of file +} diff --git a/framework/web/AccessRule.php b/yii/web/AccessRule.php similarity index 98% rename from framework/web/AccessRule.php rename to yii/web/AccessRule.php index 3f8c057..e565e18 100644 --- a/framework/web/AccessRule.php +++ b/yii/web/AccessRule.php @@ -9,9 +9,6 @@ namespace yii\web; use yii\base\Component; use yii\base\Action; -use yii\base\Controller; -use yii\web\User; -use yii\web\Request; /** * @@ -144,7 +141,7 @@ class AccessRule extends Component return true; } elseif ($role === '@' && !$user->getIsGuest()) { return true; - } elseif ($user->hasAccess($role)) { + } elseif ($user->checkAccess($role)) { return true; } } @@ -185,4 +182,4 @@ class AccessRule extends Component { return empty($this->matchCallback) || call_user_func($this->matchCallback, $this, $action); } -} \ No newline at end of file +} diff --git a/framework/web/Application.php b/yii/web/Application.php similarity index 100% rename from framework/web/Application.php rename to yii/web/Application.php diff --git a/framework/web/AssetBundle.php b/yii/web/AssetBundle.php similarity index 99% rename from framework/web/AssetBundle.php rename to yii/web/AssetBundle.php index 4e1eb59..37577dd 100644 --- a/framework/web/AssetBundle.php +++ b/yii/web/AssetBundle.php @@ -173,4 +173,4 @@ class AssetBundle extends Object } } } -} \ No newline at end of file +} diff --git a/framework/web/AssetConverter.php b/yii/web/AssetConverter.php similarity index 98% rename from framework/web/AssetConverter.php rename to yii/web/AssetConverter.php index 8340be5..4fde1fc 100644 --- a/framework/web/AssetConverter.php +++ b/yii/web/AssetConverter.php @@ -53,10 +53,10 @@ class AssetConverter extends Component implements IAssetConverter )); exec($command, $output); Yii::info("Converted $asset into $result: " . implode("\n", $output), __METHOD__); - return "$baseUrl/$result"; } + return "$baseUrl/$result"; } } return "$baseUrl/$asset"; } -} \ No newline at end of file +} diff --git a/framework/web/AssetManager.php b/yii/web/AssetManager.php similarity index 100% rename from framework/web/AssetManager.php rename to yii/web/AssetManager.php diff --git a/framework/web/CacheSession.php b/yii/web/CacheSession.php similarity index 100% rename from framework/web/CacheSession.php rename to yii/web/CacheSession.php diff --git a/yii/web/CaptchaAction.php b/yii/web/CaptchaAction.php new file mode 100644 index 0000000..e3d6eaa --- /dev/null +++ b/yii/web/CaptchaAction.php @@ -0,0 +1,338 @@ + + * @since 2.0 + */ +class CaptchaAction extends Action +{ + /** + * The name of the GET parameter indicating whether the CAPTCHA image should be regenerated. + */ + const REFRESH_GET_VAR = 'refresh'; + /** + * @var integer how many times should the same CAPTCHA be displayed. Defaults to 3. + * A value less than or equal to 0 means the test is unlimited (available since version 1.1.2). + */ + public $testLimit = 3; + /** + * @var integer the width of the generated CAPTCHA image. Defaults to 120. + */ + public $width = 120; + /** + * @var integer the height of the generated CAPTCHA image. Defaults to 50. + */ + public $height = 50; + /** + * @var integer padding around the text. Defaults to 2. + */ + public $padding = 2; + /** + * @var integer the background color. For example, 0x55FF00. + * Defaults to 0xFFFFFF, meaning white color. + */ + public $backColor = 0xFFFFFF; + /** + * @var integer the font color. For example, 0x55FF00. Defaults to 0x2040A0 (blue color). + */ + public $foreColor = 0x2040A0; + /** + * @var boolean whether to use transparent background. Defaults to false. + */ + public $transparent = false; + /** + * @var integer the minimum length for randomly generated word. Defaults to 6. + */ + public $minLength = 6; + /** + * @var integer the maximum length for randomly generated word. Defaults to 7. + */ + public $maxLength = 7; + /** + * @var integer the offset between characters. Defaults to -2. You can adjust this property + * in order to decrease or increase the readability of the captcha. + **/ + public $offset = -2; + /** + * @var string the TrueType font file. This can be either a file path or path alias. + */ + public $fontFile = '@yii/web/SpicyRice.ttf'; + /** + * @var string the fixed verification code. When this is 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. + * If not set, it means the verification code will be randomly generated. + */ + public $fixedVerifyCode; + + + /** + * Initializes the action. + * @throws InvalidConfigException if the font file does not exist. + */ + public function init() + { + $this->fontFile = Yii::getAlias($this->fontFile); + if (!is_file($this->fontFile)) { + throw new InvalidConfigException("The font file does not exist: {$this->fontFile}"); + } + } + + /** + * Runs the action. + */ + public function run() + { + if (isset($_GET[self::REFRESH_GET_VAR])) { + // AJAX request for regenerating code + $code = $this->getVerifyCode(true); + echo 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())), + )); + } else { + $this->renderImage($this->getVerifyCode()); + } + Yii::$app->end(); + } + + /** + * Generates a hash code that can be used for client side validation. + * @param string $code the CAPTCHA code + * @return string a hash code generated from the CAPTCHA code + */ + public function generateValidationHash($code) + { + for ($h = 0, $i = strlen($code) - 1; $i >= 0; --$i) { + $h += ord($code[$i]); + } + return $h; + } + + /** + * Gets the verification code. + * @param boolean $regenerate whether the verification code should be regenerated. + * @return string the verification code. + */ + public function getVerifyCode($regenerate = false) + { + if ($this->fixedVerifyCode !== null) { + return $this->fixedVerifyCode; + } + + $session = Yii::$app->session; + $session->open(); + $name = $this->getSessionKey(); + if ($session[$name] === null || $regenerate) { + $session[$name] = $this->generateVerifyCode(); + $session[$name . 'count'] = 1; + } + return $session[$name]; + } + + /** + * Validates the input to see if it matches the generated code. + * @param string $input user input + * @param boolean $caseSensitive whether the comparison should be case-sensitive + * @return boolean whether the input is valid + */ + public function validate($input, $caseSensitive) + { + $code = $this->getVerifyCode(); + $valid = $caseSensitive ? ($input === $code) : strcasecmp($input, $code) === 0; + $session = Yii::$app->session; + $session->open(); + $name = $this->getSessionKey() . 'count'; + $session[$name] = $session[$name] + 1; + if ($session[$name] > $this->testLimit && $this->testLimit > 0) { + $this->getVerifyCode(true); + } + return $valid; + } + + /** + * Generates a new verification code. + * @return string the generated verification code + */ + protected function generateVerifyCode() + { + 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'; + $vowels = 'aeiou'; + $code = ''; + for ($i = 0; $i < $length; ++$i) { + if ($i % 2 && mt_rand(0, 10) > 2 || !($i % 2) && mt_rand(0, 10) > 9) { + $code .= $vowels[mt_rand(0, 4)]; + } else { + $code .= $letters[mt_rand(0, 20)]; + } + } + + return $code; + } + + /** + * Returns the session variable name used to store verification code. + * @return string the session variable name + */ + protected function getSessionKey() + { + return '__captcha/' . $this->getUniqueId(); + } + + /** + * Renders the CAPTCHA image. + * @param string $code the verification code + */ + protected function renderImage($code) + { + if (Captcha::checkRequirements() === 'gd') { + $this->renderImageByGD($code); + } else { + $this->renderImageByImagick($code); + } + } + + /** + * Renders the CAPTCHA image based on the code using GD library. + * @param string $code the verification code + */ + protected function renderImageByGD($code) + { + $image = imagecreatetruecolor($this->width, $this->height); + + $backColor = imagecolorallocate($image, + (int)($this->backColor % 0x1000000 / 0x10000), + (int)($this->backColor % 0x10000 / 0x100), + $this->backColor % 0x100); + imagefilledrectangle($image, 0, 0, $this->width, $this->height, $backColor); + imagecolordeallocate($image, $backColor); + + if ($this->transparent) { + imagecolortransparent($image, $backColor); + } + + $foreColor = imagecolorallocate($image, + (int)($this->foreColor % 0x1000000 / 0x10000), + (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); + $h = $box[1] - $box[5]; + $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h); + $x = 10; + $y = round($this->height * 27 / 40); + for ($i = 0; $i < $length; ++$i) { + $fontSize = (int)(rand(26, 32) * $scale * 0.8); + $angle = rand(-10, 10); + $letter = $code[$i]; + $box = imagettftext($image, $fontSize, $angle, $x, $y, $foreColor, $this->fontFile, $letter); + $x = $box[2] + $this->offset; + } + + 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"); + imagepng($image); + imagedestroy($image); + } + + /** + * Renders the CAPTCHA image based on the code using ImageMagick library. + * @param string $code the verification code + */ + protected function renderImageByImagick($code) + { + $backColor = $this->transparent ? new \ImagickPixel('transparent') : new \ImagickPixel('#' . dechex($this->backColor)); + $foreColor = new \ImagickPixel('#' . dechex($this->foreColor)); + + $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); + $fontMetrics = $image->queryFontMetrics($draw, $code); + + $length = strlen($code); + $w = (int)($fontMetrics['textWidth']) - 8 + $this->offset * ($length - 1); + $h = (int)($fontMetrics['textHeight']) - 8; + $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h); + $x = 10; + $y = round($this->height * 27 / 40); + for ($i = 0; $i < $length; ++$i) { + $draw = new \ImagickDraw(); + $draw->setFont($this->fontFile); + $draw->setFontSize((int)(rand(26, 32) * $scale * 0.8)); + $draw->setFillColor($foreColor); + $image->annotateImage($draw, $x, $y, rand(-10, 10), $code[$i]); + $fontMetrics = $image->queryFontMetrics($draw, $code[$i]); + $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; + } +} diff --git a/framework/web/Controller.php b/yii/web/Controller.php similarity index 99% rename from framework/web/Controller.php rename to yii/web/Controller.php index 099bf96..517f4b4 100644 --- a/framework/web/Controller.php +++ b/yii/web/Controller.php @@ -40,4 +40,4 @@ class Controller extends \yii\base\Controller } return Yii::$app->getUrlManager()->createUrl($route, $params); } -} \ No newline at end of file +} diff --git a/framework/web/Cookie.php b/yii/web/Cookie.php similarity index 100% rename from framework/web/Cookie.php rename to yii/web/Cookie.php diff --git a/framework/web/CookieCollection.php b/yii/web/CookieCollection.php similarity index 100% rename from framework/web/CookieCollection.php rename to yii/web/CookieCollection.php diff --git a/framework/web/DbSession.php b/yii/web/DbSession.php similarity index 99% rename from framework/web/DbSession.php rename to yii/web/DbSession.php index 2910b40..e81aa7f 100644 --- a/framework/web/DbSession.php +++ b/yii/web/DbSession.php @@ -71,13 +71,13 @@ class DbSession extends Session */ 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("DbSession::db must be either a DB connection instance or the application component ID of a DB connection."); - } + } + parent::init(); } /** diff --git a/framework/web/HttpCache.php b/yii/web/HttpCache.php similarity index 99% rename from framework/web/HttpCache.php rename to yii/web/HttpCache.php index f64b37f..0a3bb86 100644 --- a/framework/web/HttpCache.php +++ b/yii/web/HttpCache.php @@ -128,4 +128,4 @@ class HttpCache extends ActionFilter { return '"' . base64_encode(sha1($seed, true)) . '"'; } -} \ No newline at end of file +} diff --git a/framework/web/IAssetConverter.php b/yii/web/IAssetConverter.php similarity index 99% rename from framework/web/IAssetConverter.php rename to yii/web/IAssetConverter.php index 4334d3e..d1d1da0 100644 --- a/framework/web/IAssetConverter.php +++ b/yii/web/IAssetConverter.php @@ -24,4 +24,4 @@ interface IAssetConverter * need conversion, "$baseUrl/$asset" should be returned. */ public function convert($asset, $basePath, $baseUrl); -} \ No newline at end of file +} diff --git a/framework/web/Identity.php b/yii/web/Identity.php similarity index 99% rename from framework/web/Identity.php rename to yii/web/Identity.php index 6d67bc0..101ecdb 100644 --- a/framework/web/Identity.php +++ b/yii/web/Identity.php @@ -78,4 +78,4 @@ interface Identity * @see getAuthKey() */ public function validateAuthKey($authKey); -} \ No newline at end of file +} diff --git a/framework/helpers/JsExpression.php b/yii/web/JsExpression.php similarity index 97% rename from framework/helpers/JsExpression.php rename to yii/web/JsExpression.php index 0054b64..027c065 100644 --- a/framework/helpers/JsExpression.php +++ b/yii/web/JsExpression.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\helpers; +namespace yii\web; use yii\base\Object; @@ -42,4 +42,4 @@ class JsExpression extends Object { return $this->expression; } -} \ No newline at end of file +} diff --git a/framework/web/PageCache.php b/yii/web/PageCache.php similarity index 96% rename from framework/web/PageCache.php rename to yii/web/PageCache.php index 5a50825..2fe36b3 100644 --- a/framework/web/PageCache.php +++ b/yii/web/PageCache.php @@ -101,4 +101,4 @@ class PageCache extends ActionFilter { $this->view->endCache(); } -} \ No newline at end of file +} diff --git a/framework/web/Pagination.php b/yii/web/Pagination.php similarity index 99% rename from framework/web/Pagination.php rename to yii/web/Pagination.php index 1d41c0c..3d4e242 100644 --- a/framework/web/Pagination.php +++ b/yii/web/Pagination.php @@ -73,7 +73,7 @@ class Pagination extends \yii\base\Object * @var boolean whether to always have the page parameter in the URL created by [[createUrl()]]. * If false and [[page]] is 0, the page parameter will not be put in the URL. */ - public $forcePageVar = false; + public $forcePageVar = true; /** * @var string the route of the controller action for displaying the paged contents. * If not set, it means using the currently requested route. @@ -205,4 +205,4 @@ class Pagination extends \yii\base\Object { return $this->pageSize < 1 ? -1 : $this->pageSize; } -} \ No newline at end of file +} diff --git a/framework/web/Request.php b/yii/web/Request.php similarity index 99% rename from framework/web/Request.php rename to yii/web/Request.php index ac19d5a..d3f419b 100644 --- a/framework/web/Request.php +++ b/yii/web/Request.php @@ -69,8 +69,8 @@ class Request extends \yii\base\Request $result = Yii::$app->getUrlManager()->parseRequest($this); if ($result !== false) { list ($route, $params) = $result; - $params = array_merge($_GET, $params); - return array($route, $params); + $_GET = array_merge($_GET, $params); + return array($route, $_GET); } else { throw new HttpException(404, Yii::t('yii|Page not found.')); } @@ -96,7 +96,7 @@ class Request extends \yii\base\Request */ public function getIsPostRequest() { - return isset($_SERVER['REQUEST_METHOD']) && !strcasecmp($_SERVER['REQUEST_METHOD'], 'POST'); + return $this->getRequestMethod() === 'POST'; } /** diff --git a/framework/web/Response.php b/yii/web/Response.php similarity index 100% rename from framework/web/Response.php rename to yii/web/Response.php diff --git a/framework/web/Session.php b/yii/web/Session.php similarity index 100% rename from framework/web/Session.php rename to yii/web/Session.php diff --git a/framework/web/SessionIterator.php b/yii/web/SessionIterator.php similarity index 100% rename from framework/web/SessionIterator.php rename to yii/web/SessionIterator.php diff --git a/framework/web/Sort.php b/yii/web/Sort.php similarity index 99% rename from framework/web/Sort.php rename to yii/web/Sort.php index e5c2451..324e733 100644 --- a/framework/web/Sort.php +++ b/yii/web/Sort.php @@ -251,7 +251,7 @@ class Sort extends \yii\base\Object } } } - if ($this->_attributeOrders === array() && is_array($this->defaults)) { + if (empty($this->_attributeOrders) && is_array($this->defaults)) { $this->_attributeOrders = $this->defaults; } } @@ -333,4 +333,4 @@ class Sort extends \yii\base\Object return false; } } -} \ No newline at end of file +} diff --git a/yii/web/SpicyRice.md b/yii/web/SpicyRice.md new file mode 100644 index 0000000..d99f3dc --- /dev/null +++ b/yii/web/SpicyRice.md @@ -0,0 +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) diff --git a/yii/web/SpicyRice.ttf b/yii/web/SpicyRice.ttf new file mode 100644 index 0000000..638436c Binary files /dev/null and b/yii/web/SpicyRice.ttf differ diff --git a/framework/web/UploadedFile.php b/yii/web/UploadedFile.php similarity index 98% rename from framework/web/UploadedFile.php rename to yii/web/UploadedFile.php index c67281c..6e685a3 100644 --- a/framework/web/UploadedFile.php +++ b/yii/web/UploadedFile.php @@ -7,7 +7,7 @@ namespace yii\web; -use yii\widgets\ActiveForm; +use yii\widgets\Html; /** * @author Qiang Xue @@ -66,7 +66,7 @@ class UploadedFile extends \yii\base\Object */ public static function getInstance($model, $attribute) { - $name = ActiveForm::getInputName($model, $attribute); + $name = Html::getInputName($model, $attribute); return static::getInstanceByName($name); } @@ -80,7 +80,7 @@ class UploadedFile extends \yii\base\Object */ public static function getInstances($model, $attribute) { - $name = ActiveForm::getInputName($model, $attribute); + $name = Html::getInputName($model, $attribute); return static::getInstancesByName($name); } diff --git a/framework/web/UrlManager.php b/yii/web/UrlManager.php similarity index 97% rename from framework/web/UrlManager.php rename to yii/web/UrlManager.php index 318f329..aab7979 100644 --- a/framework/web/UrlManager.php +++ b/yii/web/UrlManager.php @@ -51,7 +51,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 = true; + public $showScriptName = false; /** * @var string the GET variable name for route. This property is used only if [[enablePrettyUrl]] is false. */ @@ -90,7 +90,7 @@ class UrlManager extends Component */ protected function compileRules() { - if (!$this->enablePrettyUrl || $this->rules === array()) { + if (!$this->enablePrettyUrl || empty($this->rules)) { return; } if (is_string($this->cache)) { @@ -174,7 +174,7 @@ class UrlManager extends Component public function createUrl($route, $params = array()) { $anchor = isset($params['#']) ? '#' . $params['#'] : ''; - unset($params['#']); + unset($params['#'], $params[$this->routeVar]); $route = trim($route, '/'); $baseUrl = $this->getBaseUrl(); @@ -190,13 +190,13 @@ class UrlManager extends Component if ($this->suffix !== null) { $route .= $this->suffix; } - if ($params !== array()) { + if (!empty($params)) { $route .= '?' . http_build_query($params); } return rtrim($baseUrl, '/') . '/' . $route . $anchor; } else { $url = $baseUrl . '?' . $this->routeVar . '=' . $route; - if ($params !== array()) { + if (!empty($params)) { $url .= '&' . http_build_query($params); } return $url; diff --git a/framework/web/UrlRule.php b/yii/web/UrlRule.php similarity index 97% rename from framework/web/UrlRule.php rename to yii/web/UrlRule.php index d9cb4fd..96cb994 100644 --- a/framework/web/UrlRule.php +++ b/yii/web/UrlRule.php @@ -125,7 +125,7 @@ class UrlRule extends Object if (isset($this->defaults[$name])) { $length = strlen($match[0][0]); $offset = $match[0][1]; - if ($this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') { + if ($offset > 1 && $this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') { $tr["/<$name>"] = "(/(?P<$name>$pattern))?"; } else { $tr["<$name>"] = "(?P<$name>$pattern)?"; @@ -144,7 +144,7 @@ class UrlRule extends Object $this->_template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u'; - if ($this->_routeParams !== array()) { + if (!empty($this->_routeParams)) { $this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; } } @@ -275,7 +275,7 @@ class UrlRule extends Object $url .= ($this->suffix === null ? $manager->suffix : $this->suffix); } - if ($params !== array()) { + if (!empty($params)) { $url .= '?' . http_build_query($params); } return $url; diff --git a/framework/web/User.php b/yii/web/User.php similarity index 90% rename from framework/web/User.php rename to yii/web/User.php index b8bf7cd..7d8e300 100644 --- a/framework/web/User.php +++ b/yii/web/User.php @@ -40,11 +40,11 @@ class User extends Component */ public $enableAutoLogin = false; /** - * @var string|array the URL for login when [[loginRequired()]] is called. + * @var string|array the URL for login when [[loginRequired()]] is called. * 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 to the login action, and the rest of + * The first element of the array should be the route to the login action, and the rest of * the name-value pairs are GET parameters used to construct the login URL. For example, - * + * * ~~~ * array('site/login', 'ref' => 1) * ~~~ @@ -86,6 +86,8 @@ class User extends Component */ public $returnUrlVar = '__returnUrl'; + private $_access = array(); + /** * Initializes the application component. @@ -447,4 +449,35 @@ class User extends Component } } } + + /** + * Performs access check for this user. + * @param string $operation the name of the operation that need access check. + * @param array $params name-value pairs that would be passed to business rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to + * this array, which holds the value of [[id]] when [[DbAuthManager]] or + * [[PhpAuthManager]] is used. + * @param boolean $allowCaching whether to allow caching the result of access check. + * When this parameter is true (default), if the access check of an operation was performed + * before, its result will be directly returned when calling this method to check the same + * operation. If this parameter is false, this method will always call + * [[AuthManager::checkAccess()]] to obtain the up-to-date access result. Note that this + * caching is effective only within the same request and only works when `$params = array()`. + * @return boolean whether the operations can be performed by this user. + */ + public function checkAccess($operation, $params = array(), $allowCaching = true) + { + $auth = Yii::$app->getAuthManager(); + if ($auth === null) { + return false; + } + if ($allowCaching && empty($params) && isset($this->_access[$operation])) { + return $this->_access[$operation]; + } + $access = $auth->checkAccess($this->getId(), $operation, $params); + if ($allowCaching && empty($params)) { + $this->_access[$operation] = $access; + } + return $access; + } } diff --git a/framework/web/UserEvent.php b/yii/web/UserEvent.php similarity index 99% rename from framework/web/UserEvent.php rename to yii/web/UserEvent.php index 7a5d23d..4e39380 100644 --- a/framework/web/UserEvent.php +++ b/yii/web/UserEvent.php @@ -31,4 +31,4 @@ class UserEvent extends Event * This property is only meaningful for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_BEFORE_LOGOUT]] events. */ public $isValid = true; -} \ No newline at end of file +} diff --git a/framework/widgets/ActiveField.php b/yii/widgets/ActiveField.php similarity index 99% rename from framework/widgets/ActiveField.php rename to yii/widgets/ActiveField.php index da17012..9f3f201 100644 --- a/framework/widgets/ActiveField.php +++ b/yii/widgets/ActiveField.php @@ -10,7 +10,7 @@ use yii\base\Component; use yii\db\ActiveRecord; use yii\helpers\Html; use yii\base\Model; -use yii\helpers\JsExpression; +use yii\web\JsExpression; /** * @author Qiang Xue @@ -104,7 +104,7 @@ class ActiveField extends Component public function begin() { $options = $this->getClientOptions(); - if ($options !== array()) { + if (!empty($options)) { $this->form->attributes[$this->attribute] = $options; } @@ -123,7 +123,7 @@ class ActiveField extends Component return Html::beginTag($this->tag, $options); } - + public function end() { return Html::endTag($this->tag); @@ -143,7 +143,7 @@ class ActiveField extends Component $validators[] = $js; } } - if ($validators !== array()) { + if (!empty($validators)) { $options['validate'] = new JsExpression("function(attribute,value,messages){" . implode('', $validators) . '}'); } } @@ -152,7 +152,7 @@ class ActiveField extends Component $options['enableAjaxValidation'] = 1; } - if ($enableClientValidation || $enableAjaxValidation) { + if ($enableClientValidation && !empty($options['validate']) || $enableAjaxValidation) { $inputID = Html::getInputId($this->model, $this->attribute); $options['name'] = $inputID; $names = array( @@ -541,4 +541,4 @@ class ActiveField extends Component . '' ); } -} \ No newline at end of file +} diff --git a/framework/widgets/ActiveForm.php b/yii/widgets/ActiveForm.php similarity index 98% rename from framework/widgets/ActiveForm.php rename to yii/widgets/ActiveForm.php index c11bceb..24451b9 100644 --- a/framework/widgets/ActiveForm.php +++ b/yii/widgets/ActiveForm.php @@ -12,7 +12,6 @@ use yii\base\Widget; use yii\base\Model; use yii\helpers\Html; use yii\helpers\Json; -use yii\helpers\JsExpression; /** * ActiveForm ... @@ -131,7 +130,7 @@ class ActiveForm extends Widget */ public function run() { - if ($this->attributes !== array()) { + if (!empty($this->attributes)) { $id = $this->options['id']; $options = Json::encode($this->getClientOptions()); $attributes = Json::encode($this->attributes); @@ -198,7 +197,7 @@ class ActiveForm extends Widget $options['class'] .= ' ' . $this->errorSummaryCssClass; } - if ($lines !== array()) { + if (!empty($lines)) { $content = "
  • " . implode("
  • \n
  • ", $lines) . "
    • "; return Html::tag('div', $header . $content . $footer, $options); } else { diff --git a/framework/widgets/Block.php b/yii/widgets/Block.php similarity index 99% rename from framework/widgets/Block.php rename to yii/widgets/Block.php index d6f7317..fdd210f 100644 --- a/framework/widgets/Block.php +++ b/yii/widgets/Block.php @@ -46,4 +46,4 @@ class Block extends Widget } $this->view->blocks[$this->id] = $block; } -} \ No newline at end of file +} diff --git a/yii/widgets/Breadcrumbs.php b/yii/widgets/Breadcrumbs.php new file mode 100644 index 0000000..22d09b3 --- /dev/null +++ b/yii/widgets/Breadcrumbs.php @@ -0,0 +1,139 @@ +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 + * @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 = "
    • {link} /
    • \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 = "
    • {link}
    • \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 new file mode 100644 index 0000000..918e30c --- /dev/null +++ b/yii/widgets/Captcha.php @@ -0,0 +1,102 @@ + + * @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/framework/widgets/ContentDecorator.php b/yii/widgets/ContentDecorator.php similarity index 100% rename from framework/widgets/ContentDecorator.php rename to yii/widgets/ContentDecorator.php diff --git a/framework/widgets/FragmentCache.php b/yii/widgets/FragmentCache.php similarity index 94% rename from framework/widgets/FragmentCache.php rename to yii/widgets/FragmentCache.php index 637d115..aa24acd 100644 --- a/framework/widgets/FragmentCache.php +++ b/yii/widgets/FragmentCache.php @@ -107,7 +107,7 @@ class FragmentCache extends Widget $data = array($content, $this->dynamicPlaceholders); $this->cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); - if ($this->view->cacheStack === array() && !empty($this->dynamicPlaceholders)) { + if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) { $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); } echo $content; @@ -133,7 +133,7 @@ class FragmentCache extends Widget if (is_array($data) && count($data) === 2) { list ($content, $placeholders) = $data; if (is_array($placeholders) && count($placeholders) > 0) { - if ($this->view->cacheStack === array()) { + if (empty($this->view->cacheStack)) { // outermost cache: replace placeholder with dynamic content $content = $this->updateDynamicContent($content, $placeholders); } @@ -171,4 +171,4 @@ class FragmentCache extends Widget } return $this->cache->buildKey($factors); } -} \ No newline at end of file +} diff --git a/yii/widgets/LinkPager.php b/yii/widgets/LinkPager.php new file mode 100644 index 0000000..1651246 --- /dev/null +++ b/yii/widgets/LinkPager.php @@ -0,0 +1,201 @@ + + * @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 new file mode 100644 index 0000000..1bc2b21 --- /dev/null +++ b/yii/widgets/ListPager.php @@ -0,0 +1,95 @@ + + * @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 diff --git a/yii/widgets/Menu.php b/yii/widgets/Menu.php new file mode 100644 index 0000000..b8f69e1 --- /dev/null +++ b/yii/widgets/Menu.php @@ -0,0 +1,287 @@ +widget('yii\widgets\Menu', array( + * 'items' => array( + * // Important: you need to specify url as 'controller/action', + * // not just as 'controller' even if default acion 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), + * ), + * )); + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class Menu extends Widget +{ + /** + * @var array list of menu items. Each menu item should be an array of the following structure: + * + * - label: string, optional, specifies the menu item label. When [[encodeLabels]] is true, the label + * will be HTML-encoded. If the label is not specified, an empty string will be used. + * - url: string or array, optional, specifies the URL of the menu item. It will be processed by [[Html::url]]. + * When this is set, the actual menu item content will be generated using [[linkTemplate]]; + * otherwise, [[labelTemplate]] will be used. + * - visible: boolean, optional, whether this menu item is visible. Defaults to true. + * - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items. + * - active: boolean, optional, whether this menu item is in active state (currently selected). + * If a menu item is active, its CSS class will be appended with [[activeCssClass]]. + * If this option is not set, the menu item will be set active automatically when the current request + * is triggered by [[url]]. For more details, please refer to [[isItemActive()]]. + * - 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. + */ + public $items = 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. + * This property will be overridden by the `template` option set in individual menu items via [[items]]. + */ + public $linkTemplate = '{label}'; + /** + * @var string the template used to render the body of a menu which is NOT a link. + * In this template, the token `{label}` will be replaced with the label of the menu item. + * This property will be overridden by the `template` option set in individual menu items via [[items]]. + */ + public $labelTemplate = '{label}'; + /** + * @var string the template used to render a list of sub-menus. + * In this template, the token `{items}` will be replaced with the renderer sub-menu items. + */ + public $submenuTemplate = "\n
        \n{items}\n
      \n"; + /** + * @var boolean whether the labels for menu items should be HTML-encoded. + */ + public $encodeLabels = true; + /** + * @var string the CSS class to be appended to the active menu item. + */ + public $activeCssClass = 'active'; + /** + * @var boolean whether to automatically activate items according to whether their route setting + * matches the currently requested route. + * @see isItemActive + */ + public $activateItems = true; + /** + * @var boolean whether to activate parent menu items when one of the corresponding child menu items is active. + * The activated parent menu items will also have its CSS classes appended with [[activeCssClass]]. + */ + public $activateParents = false; + /** + * @var boolean whether to hide empty menu items. An empty menu item is one whose `url` option is not + * set and which has no visible child menu items. + */ + public $hideEmptyItems = true; + /** + * @var array the HTML attributes for the menu's container tag. + */ + public $options = array(); + /** + * @var string the CSS class that will be assigned to the first item in the main menu or each submenu. + * Defaults to null, meaning no such CSS class will be assigned. + */ + public $firstItemCssClass; + /** + * @var string the CSS class that will be assigned to the last item in the main menu or each submenu. + * Defaults to null, meaning no such CSS class will be assigned. + */ + 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. + * @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; + + + /** + * Renders the menu. + */ + public function run() + { + if ($this->route === null && Yii::$app->controller !== null) { + $this->route = Yii::$app->controller->getRoute(); + } + if ($this->params === null) { + $this->params = $_GET; + } + $items = $this->normalizeItems($this->items, $hasActiveChild); + echo Html::tag('ul', $this->renderItems($items), $this->options); + } + + /** + * Recursively renders the menu items (without the container tag). + * @param array $items the menu items to be rendered recursively + * @return string the rendering result + */ + protected function renderItems($items) + { + $n = count($items); + $lines = array(); + foreach ($items as $i => $item) { + $options = isset($item['itemOptions']) ? $item['itemOptions'] : array(); + $class = array(); + if ($item['active']) { + $class[] = $this->activeCssClass; + } + if ($i === 0 && $this->firstItemCssClass !== null) { + $class[] = $this->firstItemCssClass; + } + if ($i === $n - 1 && $this->lastItemCssClass !== null) { + $class[] = $this->lastItemCssClass; + } + if (!empty($class)) { + if (empty($options['class'])) { + $options['class'] = implode(' ', $class); + } else { + $options['class'] .= ' ' . implode(' ', $class); + } + } + + $menu = $this->renderItem($item); + if (!empty($item['items'])) { + $menu .= strtr($this->submenuTemplate, array( + '{items}' => $this->renderItems($item['items']), + )); + } + $lines[] = Html::tag('li', $menu, $options); + } + return implode("\n", $lines); + } + + /** + * Renders the content of a menu item. + * Note that the container and the sub-menus are not rendered here. + * @param array $item the menu item to be rendered. Please refer to [[items]] to see what data might be in the item. + * @return string the rendering result + */ + protected function renderItem($item) + { + if (isset($item['url'])) { + $template = isset($item['template']) ? $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; + return strtr($template, array( + '{label}' => $item['label'], + )); + } + } + + /** + * Normalizes the [[items]] property to remove invisible items and activate certain items. + * @param array $items the items to be normalized. + * @param boolean $active whether there is an active child menu item. + * @return array the normalized menu items + */ + protected function normalizeItems($items, &$active) + { + foreach ($items as $i => $item) { + if (isset($item['visible']) && !$item['visible']) { + unset($items[$i]); + continue; + } + if (!isset($item['label'])) { + $item['label'] = ''; + } + if ($this->encodeLabels) { + $items[$i]['label'] = Html::encode($item['label']); + } + $hasActiveChild = false; + if (isset($item['items'])) { + $items[$i]['items'] = $this->normalizeItems($item['items'], $route, $hasActiveChild); + if (empty($items[$i]['items']) && $this->hideEmptyItems) { + unset($items[$i]['items']); + if (!isset($item['url'])) { + unset($items[$i]); + continue; + } + } + } + if (!isset($item['active'])) { + if ($this->activateParents && $hasActiveChild || $this->activateItems && $this->isItemActive($item)) { + $active = $items[$i]['active'] = true; + } else { + $items[$i]['active'] = false; + } + } elseif ($item['active']) { + $active = true; + } + } + return array_values($items); + } + + /** + * 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']) && trim($item['url'][0], '/') === $this->route) { + 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/yiic b/yii/yiic similarity index 100% rename from framework/yiic rename to yii/yiic diff --git a/framework/yiic.bat b/yii/yiic.bat similarity index 100% rename from framework/yiic.bat rename to yii/yiic.bat diff --git a/framework/yiic.php b/yii/yiic.php similarity index 94% rename from framework/yiic.php rename to yii/yiic.php index 3872e2f..7cd0c40 100644 --- a/framework/yiic.php +++ b/yii/yiic.php @@ -12,7 +12,7 @@ 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__ . '/yii.php'); +require(__DIR__ . '/Yii.php'); $application = new yii\console\Application(array( 'id' => 'yiic',