Browse Source

Merge branch 'upstream' into 46-image-helper

* upstream: (35 commits)
  Fixes #1691: added “viewport” meta tag to layout views.
  Fixed the issue that query cache returns the same data for the same SQL but different query methods
  moved section
  subsection added
  typo [skip ci]
  docs about response
  Fixes #1586: `QueryBuilder::buildLikeCondition()` will now escape special characters and use percentage characters by default
  docs improved
  csrf docs added
  Fixes #1685: UrlManager::showScriptName should be set true for tests.
  Fixes #1688: ActiveForm is creating duplicated messages in error summary
  change back the visibility of findTableNames to protected.
  Typo fix.
  Fixes #1681: Added support for automatically adjusting the "for" attribute of label generated by `ActiveField::label()`
  Simplified tests.
  Fixes #1631: Charset is now explicitly set to UTF-8 when serving JSON
  Fixed typo
  added html layout for mail component in basic app
  CS fixes.
  Merge branch 'debug_module_improvements' of github.com:Ragazzo/yii2 into Ragazzo-debug_module_improvements
  ...
tags/2.0.0-beta
Antonio Ramirez 11 years ago
parent
commit
9c4689d8fb
  1. 1
      apps/advanced/backend/views/layouts/main.php
  2. 8
      apps/advanced/backend/web/css/site.css
  3. 1
      apps/advanced/frontend/views/layouts/main.php
  4. 8
      apps/advanced/frontend/web/css/site.css
  5. 11
      apps/basic/config/codeception/acceptance.php
  6. 17
      apps/basic/config/codeception/functional.php
  7. 15
      apps/basic/config/codeception/unit.php
  8. 24
      apps/basic/mails/layouts/html.php
  9. 45
      apps/basic/tests/README.md
  10. 13
      apps/basic/tests/_bootstrap.php
  11. 44
      apps/basic/tests/_pages/ContactPage.php
  12. 24
      apps/basic/tests/_pages/LoginPage.php
  13. 4
      apps/basic/tests/acceptance.suite.yml
  14. 2
      apps/basic/tests/acceptance/HomeCept.php
  15. 7
      apps/basic/tests/acceptance/_bootstrap.php
  16. 16
      apps/basic/tests/acceptance/_config.php
  17. 3
      apps/basic/tests/functional.suite.yml
  18. 2
      apps/basic/tests/functional/ContactCept.php
  19. 2
      apps/basic/tests/functional/HomeCept.php
  20. 2
      apps/basic/tests/functional/LoginCept.php
  21. 3
      apps/basic/tests/functional/_bootstrap.php
  22. 20
      apps/basic/tests/functional/_config.php
  23. 51
      apps/basic/tests/functional/_pages/ContactPage.php
  24. 30
      apps/basic/tests/functional/_pages/LoginPage.php
  25. 1
      apps/basic/views/layouts/main.php
  26. 8
      apps/basic/web/css/site.css
  27. 11
      apps/basic/web/index-test-functional.php
  28. 8
      apps/basic/web/index-test.php
  29. 64
      docs/guide/controller.md
  30. 13
      docs/guide/query-builder.md
  31. 2
      docs/internals/git-workflow.md
  32. 2
      extensions/yii/authclient/README.md
  33. 20
      extensions/yii/debug/LogTarget.php
  34. 72
      extensions/yii/debug/components/search/Filter.php
  35. 26
      extensions/yii/debug/components/search/matches/Base.php
  36. 36
      extensions/yii/debug/components/search/matches/Exact.php
  37. 27
      extensions/yii/debug/components/search/matches/Greater.php
  38. 27
      extensions/yii/debug/components/search/matches/Lower.php
  39. 25
      extensions/yii/debug/components/search/matches/MatcherInterface.php
  40. 9
      extensions/yii/debug/controllers/DefaultController.php
  41. 149
      extensions/yii/debug/models/search/Debug.php
  42. 2
      extensions/yii/debug/panels/DbPanel.php
  43. 83
      extensions/yii/debug/views/default/index.php
  44. 2
      extensions/yii/debug/views/layouts/main.php
  45. 1
      extensions/yii/gii/generators/crud/templates/search.php
  46. 1
      extensions/yii/gii/views/layouts/main.php
  47. 4
      extensions/yii/sphinx/Query.php
  48. 25
      extensions/yii/sphinx/QueryBuilder.php
  49. 6
      framework/CHANGELOG.md
  50. 2
      framework/yii/assets/yii.activeForm.js
  51. 13
      framework/yii/db/Command.php
  52. 8
      framework/yii/db/Query.php
  53. 23
      framework/yii/db/QueryBuilder.php
  54. 10
      framework/yii/db/QueryInterface.php
  55. 2
      framework/yii/db/mysql/Schema.php
  56. 2
      framework/yii/db/pgsql/QueryBuilder.php
  57. 2
      framework/yii/db/pgsql/Schema.php
  58. 2
      framework/yii/db/sqlite/Schema.php
  59. 129
      framework/yii/helpers/BaseInflector.php
  60. 2
      framework/yii/rbac/Manager.php
  61. 4
      framework/yii/web/Request.php
  62. 2
      framework/yii/web/Response.php
  63. 22
      framework/yii/widgets/ActiveField.php
  64. 2
      tests/unit/framework/db/SchemaTest.php
  65. 2
      tests/unit/framework/helpers/InflectorTest.php

1
apps/advanced/backend/views/layouts/main.php

@ -16,6 +16,7 @@ AppAsset::register($this);
<html lang="<?= Yii::$app->language ?>"> <html lang="<?= Yii::$app->language ?>">
<head> <head>
<meta charset="<?= Yii::$app->charset ?>"/> <meta charset="<?= Yii::$app->charset ?>"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= Html::encode($this->title) ?></title> <title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?> <?php $this->head() ?>
</head> </head>

8
apps/advanced/backend/web/css/site.css

@ -43,3 +43,11 @@ a.desc:after { content: /*"\e114"*/"\e152"; }
.sort-ordinal a.asc:after { content: "\e155"; } .sort-ordinal a.asc:after { content: "\e155"; }
.sort-ordinal a.desc:after { content: "\e156"; } .sort-ordinal a.desc:after { content: "\e156"; }
.error-summary {
color: #b94a48;
background: #fdf7f7;
border-left: 3px solid #eed3d7;
padding: 10px 20px;
margin: 0 0 15px 0;
}

1
apps/advanced/frontend/views/layouts/main.php

@ -17,6 +17,7 @@ AppAsset::register($this);
<html lang="<?= Yii::$app->language ?>"> <html lang="<?= Yii::$app->language ?>">
<head> <head>
<meta charset="<?= Yii::$app->charset ?>"/> <meta charset="<?= Yii::$app->charset ?>"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= Html::encode($this->title) ?></title> <title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?> <?php $this->head() ?>
</head> </head>

8
apps/advanced/frontend/web/css/site.css

@ -43,3 +43,11 @@ a.desc:after { content: /*"\e114"*/"\e152"; }
.sort-ordinal a.asc:after { content: "\e155"; } .sort-ordinal a.asc:after { content: "\e155"; }
.sort-ordinal a.desc:after { content: "\e156"; } .sort-ordinal a.desc:after { content: "\e156"; }
.error-summary {
color: #b94a48;
background: #fdf7f7;
border-left: 3px solid #eed3d7;
padding: 10px 20px;
margin: 0 0 15px 0;
}

11
apps/basic/config/codeception/acceptance.php

@ -1,11 +0,0 @@
<?php
// configuration adjustments for codeception acceptance tests. Will be merged with web.php config.
return [
'components' => [
'db' => [
'dsn' => 'mysql:host=localhost;dbname=yii2basic_acceptance',
],
],
];

17
apps/basic/config/codeception/functional.php

@ -1,17 +0,0 @@
<?php
// configuration adjustments for codeception functional tests. Will be merged with web.php config.
return [
'components' => [
'db' => [
'dsn' => 'mysql:host=localhost;dbname=yii2basic_functional',
],
'request' => [
'enableCsrfValidation' => false,
],
'urlManager' => [
'baseUrl' => '/web/index.php',
],
],
];

15
apps/basic/config/codeception/unit.php

@ -1,15 +0,0 @@
<?php
// configuration adjustments for codeception unit tests. Will be merged with web.php config.
return [
'components' => [
'fixture' => [
'class' => 'yii\test\DbFixtureManager',
'basePath' => '@tests/unit/fixtures',
],
'db' => [
'dsn' => 'mysql:host=localhost;dbname=yii2basic_unit',
],
],
];

24
apps/basic/mails/layouts/html.php

@ -0,0 +1,24 @@
<?php
use yii\helpers\Html;
use yii\mail\BaseMessage;
/**
* @var \yii\web\View $this
* @var BaseMessage $content
*/
?>
<?php $this->beginPage() ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=<?= Yii::$app->charset ?>" />
<title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
<?= $content ?>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

45
apps/basic/tests/README.md

@ -1,35 +1,28 @@
This folder contains various tests for the basic application. This folder contains various tests for the basic application.
These tests are developed with [Codeception PHP Testing Framework](http://codeception.com/). These tests are developed with [Codeception PHP Testing Framework](http://codeception.com/).
To run the tests, follow these steps: After creating the basic application, follow these steps to prepare for the tests:
1. Download Codeception([Quickstart step 1](http://codeception.com/quickstart)) and put the codeception.phar in the 1. In the file `_bootstrap.php`, modify the definition of the constant `TEST_ENTRY_URL` so
application base directory (not in this `tests` directory!). that it points to the correct entry script URL.
2. Adjust the test configuration files based on your environment: 2. Go to the application base directory and build the test suites:
- Configure the URL for [acceptance tests](http://codeception.com/docs/04-AcceptanceTests) in `acceptance.suite.yml`.
The URL should point to the `index-test-acceptance.php` file that is located under the `web` directory of the application.
- `functional.suite.yml` for [functional testing](http://codeception.com/docs/05-FunctionalTests) and
`unit.suite.yml` for [unit testing](http://codeception.com/docs/06-UnitTests) should already work out of the box
and should not need to be adjusted.
- If you want to run acceptance tests, you need to download [selenium standalone](http://www.seleniumhq.org/download/)
and start it with command `java -jar {selenium-standalone-name}.jar`.
After that you can use `WebDriver` codeception module that will connect to selenium and launch browser.
This also allows you to use [Xvfb](https://en.wikipedia.org/wiki/Xvfb) in your tests which allows you to run tests
without showing the running browser on the screen. There is codeception [blog post](http://codeception.com/05-24-2013/jenkins-ci-practice.html)
that explains how it works.
3. Go to the application base directory and build the test suites:
``` ```
php codecept.phar build // rebuild test scripts, only need to be run once vendor/bin/codecept build
```
4. Run the tests:
```
php codecept.phar run // run all available tests
// you can also run a test suite alone:
php codecept.phar run acceptance
php codecept.phar run functional
php codecept.phar run unit
``` ```
Now you can run the tests with the following commands:
```
# run all available tests
vendor/bin/codecept run
# run acceptance tests
vendor/bin/codecept run acceptance
# run functional tests
vendor/bin/codecept run functional
# run unit tests
vendor/bin/codecept run unit
```
Please refer to [Codeception tutorial](http://codeception.com/docs/01-Introduction) for Please refer to [Codeception tutorial](http://codeception.com/docs/01-Introduction) for
more details about writing acceptance, functional and unit tests. more details about writing and running acceptance, functional and unit tests.

13
apps/basic/tests/_bootstrap.php

@ -1,9 +1,22 @@
<?php <?php
// the entry script URL (without host info) for functional and acceptance tests
// PLEASE ADJUST IT TO THE ACTUAL ENTRY SCRIPT URL
defined('TEST_ENTRY_URL') or define('TEST_ENTRY_URL', '/yii2-basic/web/index-test.php');
// the entry script file path for functional and acceptance tests
defined('TEST_ENTRY_FILE') or define('TEST_ENTRY_FILE', dirname(__DIR__) . '/web/index-test.php');
defined('YII_DEBUG') or define('YII_DEBUG', true); defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'test'); defined('YII_ENV') or define('YII_ENV', 'test');
require_once(__DIR__ . '/../vendor/autoload.php'); require_once(__DIR__ . '/../vendor/autoload.php');
require_once(__DIR__ . '/../vendor/yiisoft/yii2/yii/Yii.php'); require_once(__DIR__ . '/../vendor/yiisoft/yii2/yii/Yii.php');
// set correct script paths
$_SERVER['SCRIPT_FILENAME'] = TEST_ENTRY_FILE;
$_SERVER['SCRIPT_NAME'] = TEST_ENTRY_URL;
Yii::setAlias('@tests', __DIR__); Yii::setAlias('@tests', __DIR__);

44
apps/basic/tests/_pages/ContactPage.php

@ -9,50 +9,14 @@ class ContactPage extends BasePage
public $route = 'site/contact'; public $route = 'site/contact';
/** /**
* contact form name text field locator
* @var string
*/
public $name = 'input[name="ContactForm[name]"]';
/**
* contact form email text field locator
* @var string
*/
public $email = 'input[name="ContactForm[email]"]';
/**
* contact form subject text field locator
* @var string
*/
public $subject = 'input[name="ContactForm[subject]"]';
/**
* contact form body textarea locator
* @var string
*/
public $body = 'textarea[name="ContactForm[body]"]';
/**
* contact form verification code text field locator
* @var string
*/
public $verifyCode = 'input[name="ContactForm[verifyCode]"]';
/**
* contact form submit button
* @var string
*/
public $button = 'button[type=submit]';
/**
*
* @param array $contactData * @param array $contactData
*/ */
public function submit(array $contactData) public function submit(array $contactData)
{ {
if (!empty($contactData)) $data = [];
{ foreach ($contactData as $name => $value) {
$this->guy->fillField($this->name, $contactData['name']); $data["ContactForm[$name]"] = $value;
$this->guy->fillField($this->email, $contactData['email']);
$this->guy->fillField($this->subject, $contactData['subject']);
$this->guy->fillField($this->body, $contactData['body']);
$this->guy->fillField($this->verifyCode, $contactData['verifyCode']);
} }
$this->guy->click($this->button); $this->guy->submitForm('#contact-form', $data);
} }
} }

24
apps/basic/tests/_pages/LoginPage.php

@ -9,30 +9,14 @@ class LoginPage extends BasePage
public $route = 'site/login'; public $route = 'site/login';
/** /**
* login form username text field locator
* @var string
*/
public $username = 'input[name="LoginForm[username]"]';
/**
* login form password text field locator
* @var string
*/
public $password = 'input[name="LoginForm[password]"]';
/**
* login form submit button locator
* @var string
*/
public $button = 'button[type=submit]';
/**
*
* @param string $username * @param string $username
* @param string $password * @param string $password
*/ */
public function login($username, $password) public function login($username, $password)
{ {
$this->guy->fillField($this->username, $username); $this->guy->submitForm('#login-form', [
$this->guy->fillField($this->password, $password); 'LoginForm[username]' => $username,
$this->guy->click($this->button); 'LoginForm[password]' => $password,
]);
} }
} }

4
apps/basic/tests/acceptance.suite.yml

@ -18,7 +18,7 @@ modules:
# - WebDriver # - WebDriver
config: config:
PhpBrowser: PhpBrowser:
url: 'http://localhost/basic-app/web/index-test-acceptance.php' url: 'http://localhost'
# WebDriver: # WebDriver:
# url: 'http://localhost/basic-app/web/index-test-acceptance.php' # url: 'http://localhost'
# browser: firefox # browser: firefox

2
apps/basic/tests/acceptance/HomeCept.php

@ -2,7 +2,7 @@
$I = new WebGuy($scenario); $I = new WebGuy($scenario);
$I->wantTo('ensure that home page works'); $I->wantTo('ensure that home page works');
$I->amOnPage(''); $I->amOnPage(Yii::$app->homeUrl);
$I->see('My Company'); $I->see('My Company');
$I->seeLink('About'); $I->seeLink('About');
$I->click('About'); $I->click('About');

7
apps/basic/tests/acceptance/_bootstrap.php

@ -1,8 +1,3 @@
<?php <?php
$config = yii\helpers\ArrayHelper::merge( new yii\web\Application(require(__DIR__ . '/_config.php'));
require(__DIR__ . '/../../config/web.php'),
require(__DIR__ . '/../../config/codeception/acceptance.php')
);
$application = new yii\web\Application($config);

16
apps/basic/tests/acceptance/_config.php

@ -0,0 +1,16 @@
<?php
use yii\helpers\ArrayHelper;
$config = require(__DIR__ . '/../../config/web.php');
return ArrayHelper::merge($config, [
'components' => [
'db' => [
'dsn' => 'mysql:host=localhost;dbname=yii2_basic_acceptance',
],
'urlManager' => [
'showScriptName' => true,
],
],
]);

3
apps/basic/tests/functional.suite.yml

@ -14,5 +14,4 @@ modules:
- Yii2 - Yii2
config: config:
Yii2: Yii2:
entryScript: 'web/index-test-functional.php' configFile: 'tests/functional/_config.php'
url: 'http://localhost/'

2
apps/basic/tests/functional/ContactCept.php

@ -1,6 +1,6 @@
<?php <?php
use tests\functional\_pages\ContactPage; use tests\_pages\ContactPage;
$I = new TestGuy($scenario); $I = new TestGuy($scenario);
$I->wantTo('ensure that contact works'); $I->wantTo('ensure that contact works');

2
apps/basic/tests/functional/HomeCept.php

@ -2,7 +2,7 @@
$I = new TestGuy($scenario); $I = new TestGuy($scenario);
$I->wantTo('ensure that home page works'); $I->wantTo('ensure that home page works');
$I->amOnPage(''); $I->amOnPage(Yii::$app->homeUrl);
$I->see('My Company'); $I->see('My Company');
$I->seeLink('About'); $I->seeLink('About');
$I->click('About'); $I->click('About');

2
apps/basic/tests/functional/LoginCept.php

@ -1,6 +1,6 @@
<?php <?php
use tests\functional\_pages\LoginPage; use tests\_pages\LoginPage;
$I = new TestGuy($scenario); $I = new TestGuy($scenario);
$I->wantTo('ensure that login works'); $I->wantTo('ensure that login works');

3
apps/basic/tests/functional/_bootstrap.php

@ -1,4 +1,3 @@
<?php <?php
// create an application instance to support URL creation before running any test new yii\web\Application(require(__DIR__ . '/_config.php'));
Yii::createObject(require(__DIR__ . '/../../web/index-test-functional.php'));

20
apps/basic/tests/functional/_config.php

@ -0,0 +1,20 @@
<?php
use yii\helpers\ArrayHelper;
// set correct script paths
$_SERVER['SCRIPT_FILENAME'] = TEST_ENTRY_FILE;
$_SERVER['SCRIPT_NAME'] = TEST_ENTRY_URL;
$config = require(__DIR__ . '/../../config/web.php');
return ArrayHelper::merge($config, [
'components' => [
'db' => [
'dsn' => 'mysql:host=localhost;dbname=yii2_basic_functional',
],
'urlManager' => [
'showScriptName' => true,
],
],
]);

51
apps/basic/tests/functional/_pages/ContactPage.php

@ -1,51 +0,0 @@
<?php
namespace tests\functional\_pages;
class ContactPage extends \tests\_pages\ContactPage
{
/**
* contact form name text field locator
* @var string
*/
public $name = 'ContactForm[name]';
/**
* contact form email text field locator
* @var string
*/
public $email = 'ContactForm[email]';
/**
* contact form subject text field locator
* @var string
*/
public $subject = 'ContactForm[subject]';
/**
* contact form body textarea locator
* @var string
*/
public $body = 'ContactForm[body]';
/**
* contact form verification code text field locator
* @var string
*/
public $verifyCode = 'ContactForm[verifyCode]';
/**
*
* @param array $contactData
*/
public function submit(array $contactData)
{
if (empty($contactData)) {
$this->guy->submitForm('#contact-form', []);
} else {
$this->guy->submitForm('#contact-form', [
$this->name => $contactData['name'],
$this->email => $contactData['email'],
$this->subject => $contactData['subject'],
$this->body => $contactData['body'],
$this->verifyCode => $contactData['verifyCode'],
]);
}
}
}

30
apps/basic/tests/functional/_pages/LoginPage.php

@ -1,30 +0,0 @@
<?php
namespace tests\functional\_pages;
class LoginPage extends \tests\_pages\LoginPage
{
/**
* login form username text field locator
* @var string
*/
public $username = 'LoginForm[username]';
/**
* login form password text field locator
* @var string
*/
public $password = 'LoginForm[password]';
/**
*
* @param string $username
* @param string $password
*/
public function login($username, $password)
{
$this->guy->submitForm('#login-form', [
$this->username => $username,
$this->password => $password,
]);
}
}

1
apps/basic/views/layouts/main.php

@ -16,6 +16,7 @@ AppAsset::register($this);
<html lang="<?= Yii::$app->language ?>"> <html lang="<?= Yii::$app->language ?>">
<head> <head>
<meta charset="<?= Yii::$app->charset ?>"/> <meta charset="<?= Yii::$app->charset ?>"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= Html::encode($this->title) ?></title> <title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?> <?php $this->head() ?>
</head> </head>

8
apps/basic/web/css/site.css

@ -44,3 +44,11 @@ a.desc:after { content: /*"\e114"*/"\e152"; }
.sort-ordinal a.asc:after { content: "\e155"; } .sort-ordinal a.asc:after { content: "\e155"; }
.sort-ordinal a.desc:after { content: "\e156"; } .sort-ordinal a.desc:after { content: "\e156"; }
.error-summary {
color: #b94a48;
background: #fdf7f7;
border-left: 3px solid #eed3d7;
padding: 10px 20px;
margin: 0 0 15px 0;
}

11
apps/basic/web/index-test-functional.php

@ -1,11 +0,0 @@
<?php
// this file is used as the entry script for codeception functional testing
$config = yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/../config/web.php'),
require(__DIR__ . '/../config/codeception/functional.php')
);
$config['class'] = 'yii\web\Application';
return $config;

8
apps/basic/web/index-test-acceptance.php → apps/basic/web/index-test.php

@ -1,6 +1,9 @@
<?php <?php
// NOTE: Make sure this file is not accessible when deployed to production // NOTE: Make sure this file is not accessible when deployed to production
if (!in_array(@$_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) {
die('You are not allowed to access this file.');
}
defined('YII_DEBUG') or define('YII_DEBUG', true); defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'test'); defined('YII_ENV') or define('YII_ENV', 'test');
@ -8,9 +11,6 @@ defined('YII_ENV') or define('YII_ENV', 'test');
require(__DIR__ . '/../vendor/autoload.php'); require(__DIR__ . '/../vendor/autoload.php');
require(__DIR__ . '/../vendor/yiisoft/yii2/yii/Yii.php'); require(__DIR__ . '/../vendor/yiisoft/yii2/yii/Yii.php');
$config = yii\helpers\ArrayHelper::merge( $config = require(__DIR__ . '/../tests/acceptance/_config.php');
require(__DIR__ . '/../config/web.php'),
require(__DIR__ . '/../config/codeception/acceptance.php')
);
(new yii\web\Application($config))->run(); (new yii\web\Application($config))->run();

64
docs/guide/controller.md

@ -35,10 +35,49 @@ class SiteController extends Controller
``` ```
As you can see, typical controller contains actions that are public class methods named as `actionSomething`. As you can see, typical controller contains actions that are public class methods named as `actionSomething`.
The output of an action is what the method returns. The return value will be handled by the `response` application The output of an action is what the method returns, it could be rendered result or it can be instance of ```yii\web\Response```, for [example](#custom-response-class).
The return value will be handled by the `response` application
component which can convert the output to differnet formats such as JSON for example. The default behavior component which can convert the output to differnet formats such as JSON for example. The default behavior
is to output the value unchanged though. is to output the value unchanged though.
You also can disable CSRF validation per controller and/or action, by setting its property:
```php
namespace app\controllers;
use yii\web\Controller;
class SiteController extends Controller
{
public $enableCsrfValidation = false;
public function actionIndex()
{
#CSRF validation will no be applied on this and other actions
}
}
```
To disable CSRF validation per custom actions you can do:
```php
namespace app\controllers;
use yii\web\Controller;
class SiteController extends Controller
{
public function beforeAction($action)
{
// ...set `$this->enableCsrfValidation` here based on some conditions...
// call parent method that will check CSRF if such property is true.
return parent::beforeAction($action);
}
}
```
Routes Routes
------ ------
@ -208,6 +247,29 @@ Catching all incoming requests
TBD TBD
Custom response class
---------------------
```php
namespace app\controllers;
use yii\web\Controller;
use app\components\web\MyCustomResponse; #extended from yii\web\Response
class SiteController extends Controller
{
public function actionCustom()
{
/*
* do your things here
* since Response in extended from yii\base\Object, you can initialize its values by passing in
* __constructor() simple array.
*/
return new MyCustomResponse(['data' => $myCustomData]);
}
}
```
See also See also
-------- --------

13
docs/guide/query-builder.md

@ -142,11 +142,16 @@ Operator can be one of the following:
- `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition.
- `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing
the values that the column or DB expression should be like. the values that the column or DB expression should be like.
For example, `['like', 'name', '%tester%']` will generate `name LIKE '%tester%'`. For example, `['like', 'name', 'tester']` will generate `name LIKE '%tester%'`.
When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated
using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate using `AND`. For example, `['like', 'name', ['test', 'sample']]` will generate
`name LIKE '%test%' AND name LIKE '%sample%'`. `name LIKE '%test%' AND name LIKE '%sample%'`.
The method will properly quote the column name and escape values in the range. You may also provide an optional third operand to specify how to escape special characters in the values.
The operand should be an array of mappings from the special characters to their
escaped counterparts. If this operand is not provided, a default escape mapping will be used.
You may use `false` or an empty array to indicate the values are already escaped and no escape
should be applied. Note that when using an escape mapping (or the third operand is not provided),
the values will be automatically enclosed within a pair of percentage characters.
- `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE`
predicates when operand 2 is an array. predicates when operand 2 is an array.
- `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE`
@ -162,7 +167,7 @@ $search = 'yii';
$query->where(['status' => $status]); $query->where(['status' => $status]);
if (!empty($search)) { if (!empty($search)) {
$query->addWhere('like', 'title', $search); $query->addWhere(['like', 'title', $search]);
} }
``` ```

2
docs/internals/git-workflow.md

@ -3,7 +3,7 @@ Git workflow for Yii 2 contributors
So you want to contribute to Yii? Great! But to increase the chances of your changes being accepted quickly, please So you want to contribute to Yii? Great! But to increase the chances of your changes being accepted quickly, please
follow the following steps (the first 2 steps only need to be done the first time you contribute). If you are new to git follow the following steps (the first 2 steps only need to be done the first time you contribute). If you are new to git
and github, you might want to first check out [github help](http://help.github.com/), [learn git](http://gitref.org/) and github, you might want to first check out [github help](http://help.github.com/), [try git](https://try.github.com)
or learn something about [git internal data model](http://nfarina.com/post/9868516270/git-is-simpler). or learn something about [git internal data model](http://nfarina.com/post/9868516270/git-is-simpler).
### 1. [Fork](http://help.github.com/fork-a-repo/) the Yii repository on github and clone your fork to your development ### 1. [Fork](http://help.github.com/fork-a-repo/) the Yii repository on github and clone your fork to your development

2
extensions/yii/authclient/README.md

@ -77,7 +77,7 @@ class SiteController extends Controller
You may use [[yii\authclient\widgets\Choice]] to compose auth client selection: You may use [[yii\authclient\widgets\Choice]] to compose auth client selection:
``` ```
<?= yii\authclient\Choice::widget([ <?= yii\authclient\widgets\Choice::widget([
'baseAuthUrl' => ['site/auth'] 'baseAuthUrl' => ['site/auth']
]) ?> ]) ?>
``` ```

20
extensions/yii/debug/LogTarget.php

@ -52,6 +52,7 @@ class LogTarget extends Target
$manifest = unserialize(file_get_contents($indexFile)); $manifest = unserialize(file_get_contents($indexFile));
} }
$request = Yii::$app->getRequest(); $request = Yii::$app->getRequest();
$response = Yii::$app->getResponse();
$manifest[$this->tag] = $summary = [ $manifest[$this->tag] = $summary = [
'tag' => $this->tag, 'tag' => $this->tag,
'url' => $request->getAbsoluteUrl(), 'url' => $request->getAbsoluteUrl(),
@ -59,6 +60,8 @@ class LogTarget extends Target
'method' => $request->getMethod(), 'method' => $request->getMethod(),
'ip' => $request->getUserIP(), 'ip' => $request->getUserIP(),
'time' => time(), 'time' => time(),
'statusCode' => $response->statusCode,
'sqlCount' => $this->getSqlTotalCount(),
]; ];
$this->gc($manifest); $this->gc($manifest);
@ -102,4 +105,21 @@ class LogTarget extends Target
} }
} }
} }
/**
* Returns total sql count executed in current request. If database panel is not configured
* returns 0.
* @return integer
*/
protected function getSqlTotalCount()
{
if (!isset($this->module->panels['db'])) {
return 0;
}
$profileLogs = $this->module->panels['db']->save();
# / 2 because messages are in couple (begin/end)
return count($profileLogs['messages']) / 2;
}
} }

72
extensions/yii/debug/components/search/Filter.php

@ -0,0 +1,72 @@
<?php
namespace yii\debug\components\search;
use yii\base\Component;
class Filter extends Component
{
/**
* @var array rules for matching filters in the way: [:fieldName => [rule1, rule2,..]]
*/
protected $rules = [];
/**
* Adds rules for filtering data. Match can be partial or exactly.
* @param string $name attribute name
* @param \yii\debug\components\search\matches\Base $rule
*/
public function addMatch($name, $rule)
{
if (empty($rule->value) && $rule->value !== 0) {
return;
}
$this->rules[$name][] = $rule;
}
/**
* Applies filter on given array and returns filtered data.
* @param array $data data to filter
* @return array filtered data
*/
public function filter(array $data)
{
$filtered = [];
foreach ($data as $row) {
if ($this->checkFilter($row)) {
$filtered[] = $row;
}
}
return $filtered;
}
/**
* Check if the given data satisfies filters.
* @param array $row
*/
public function checkFilter(array $row)
{
$matched = true;
foreach ($row as $name => $value) {
if (isset($this->rules[$name])) {
#check all rules for given attribute
foreach ($this->rules[$name] as $rule) {
if (!$rule->check($value)) {
$matched = false;
}
}
}
}
return $matched;
}
}

26
extensions/yii/debug/components/search/matches/Base.php

@ -0,0 +1,26 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\debug\components\search\matches;
use yii\base\Component;
/**
* Base mathcer class for all matchers that will be used with filter.
*
* @author Mark Jebri <mark.github@yandex.ru>
* @since 2.0
*/
abstract class Base extends Component implements MatcherInterface
{
/**
* @var mixed current value to check for the matcher
*/
public $value;
}

36
extensions/yii/debug/components/search/matches/Exact.php

@ -0,0 +1,36 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\debug\components\search\matches;
/**
*
* @author Mark Jebri <mark.github@yandex.ru>
* @since 2.0
*/
class Exact extends Base
{
/**
* @var boolean if current matcher should consider partial mathc of given value.
*/
public $partial = false;
/**
* Checks if the given value is the same as base one or has partial match with base one.
* @param mixed $value
*/
public function check($value)
{
if (!$this->partial) {
return (mb_strtolower($this->value, 'utf8') == mb_strtolower($value, 'utf8'));
} else {
return (mb_strpos($value, $this->value) !== false);
}
}
}

27
extensions/yii/debug/components/search/matches/Greater.php

@ -0,0 +1,27 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\debug\components\search\matches;
/**
*
* @author Mark Jebri <mark.github@yandex.ru>
* @since 2.0
*/
class Greater extends Base
{
/**
* Checks if the given value is the same as base one or has partial match with base one.
* @param mixed $value
*/
public function check($value)
{
return ($value > $this->value);
}
}

27
extensions/yii/debug/components/search/matches/Lower.php

@ -0,0 +1,27 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\debug\components\search\matches;
/**
*
* @author Mark Jebri <mark.github@yandex.ru>
* @since 2.0
*/
class Lower extends Base
{
/**
* Checks if the given value is the same as base one or has partial match with base one.
* @param mixed $value
*/
public function check($value)
{
return ($value < $this->value);
}
}

25
extensions/yii/debug/components/search/matches/MatcherInterface.php

@ -0,0 +1,25 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\debug\components\search\matches;
/**
* MatcherInterface is the interface that should be implemented by all matchers that will be used in filter.
*
* @author Mark Jebri <mark.github@yandex.ru>
* @since 2.0
*/
interface MatcherInterface
{
/**
* Check if the value is correct according current matcher.
* @param mixed $value
*/
public function check($value);
}

9
extensions/yii/debug/controllers/DefaultController.php

@ -10,6 +10,7 @@ namespace yii\debug\controllers;
use Yii; use Yii;
use yii\web\Controller; use yii\web\Controller;
use yii\web\NotFoundHttpException; use yii\web\NotFoundHttpException;
use yii\debug\models\search\Debug;
/** /**
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
@ -38,7 +39,13 @@ class DefaultController extends Controller
public function actionIndex() public function actionIndex()
{ {
return $this->render('index', ['manifest' => $this->getManifest()]); $searchModel = new Debug();
$dataProvider = $searchModel->search($_GET, $this->getManifest());
return $this->render('index', [
'dataProvider' => $dataProvider,
'searchModel' => $searchModel,
]);
} }
public function actionView($tag = null, $panel = null) public function actionView($tag = null, $panel = null)

149
extensions/yii/debug/models/search/Debug.php

@ -0,0 +1,149 @@
<?php
namespace yii\debug\models\search;
use yii\base\Model;
use yii\data\ArrayDataProvider;
use yii\debug\components\search\Filter;
use yii\debug\components\search\matches;
/**
* Debug represents the model behind the search form about requests manifest data.
*/
class Debug extends Model
{
/**
* @var string tag attribute input search value
*/
public $tag;
/**
* @var string ip attribute input search value
*/
public $ip;
/**
* @var string method attribute input search value
*/
public $method;
/**
* @var integer ajax attribute input search value
*/
public $ajax;
/**
* @var string url attribute input search value
*/
public $url;
/**
* @var string status code attribute input search value
*/
public $statusCode;
/**
*
* @var integer sql count attribute input search value
*/
public $sqlCount;
/**
* @var array critical codes, used to determine grid row options.
*/
public $criticalCodes = [400, 404, 500];
public function rules()
{
return [
[['tag', 'ip', 'method', 'ajax', 'url', 'statusCode', 'sqlCount'], 'safe'],
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'tag' => 'Tag',
'ip' => 'Ip',
'method' => 'Method',
'ajax' => 'Ajax',
'url' => 'url',
'statusCode' => 'Status code',
'sqlCount' => 'Total queries count',
];
}
/**
* Returns data provider with filled models. Filter applied if needed.
* @param array $params
* @param array $models
* @return \yii\data\ArrayDataProvider
*/
public function search($params, $models)
{
$dataProvider = new ArrayDataProvider([
'allModels' => $models,
'sort' => [
'attributes' => ['method', 'ip', 'tag', 'time', 'statusCode', 'sqlCount'],
],
'pagination' => [
'pageSize' => 10,
],
]);
if (!($this->load($params) && $this->validate())) {
return $dataProvider;
}
$filter = new Filter();
$this->addCondition($filter, 'tag', true);
$this->addCondition($filter, 'ip', true);
$this->addCondition($filter, 'method');
$this->addCondition($filter, 'ajax');
$this->addCondition($filter, 'url', true);
$this->addCondition($filter, 'statusCode');
$this->addCondition($filter, 'sqlCount');
$dataProvider->allModels = $filter->filter($models);
return $dataProvider;
}
/**
* Checks if the code is critical: 400 or greater, 500 or greater.
* @param integer $code
* @return bool
*/
public function isCodeCritical($code)
{
return in_array($code, $this->criticalCodes);
}
/**
* @param Filter $filter
* @param string $attribute
* @param boolean $partial
*/
public function addCondition($filter, $attribute, $partial = false)
{
$value = $this->$attribute;
if (mb_strpos($value, '>') !== false) {
$value = intval(str_replace('>', '', $value));
$filter->addMatch($attribute, new matches\Greater(['value' => $value]));
} elseif (mb_strpos($value, '<') !== false) {
$value = intval(str_replace('<', '', $value));
$filter->addMatch($attribute, new matches\Lower(['value' => $value]));
} else {
$filter->addMatch($attribute, new matches\Exact(['value' => $value, 'partial' => $partial]));
}
}
}

2
extensions/yii/debug/panels/DbPanel.php

@ -117,7 +117,7 @@ EOD;
public function save() public function save()
{ {
$target = $this->module->logTarget; $target = $this->module->logTarget;
$messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, ['yii\db\Command::queryInternal']); $messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, ['yii\db\Command::query', 'yii\db\Command::execute']);
return ['messages' => $messages]; return ['messages' => $messages];
} }
} }

83
extensions/yii/debug/views/default/index.php

@ -1,10 +1,14 @@
<?php <?php
use yii\helpers\Html; use yii\helpers\Html;
use yii\grid\GridView;
use yii\data\ArrayDataProvider;
/** /**
* @var \yii\web\View $this * @var \yii\web\View $this
* @var array $manifest * @var array $manifest
* @var \yii\debug\models\search\Debug $searchModel
* @var ArrayDataProvider $dataProvider
*/ */
$this->title = 'Yii Debugger'; $this->title = 'Yii Debugger';
@ -19,28 +23,63 @@ $this->title = 'Yii Debugger';
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<h1>Available Debug Data</h1> <h1>Available Debug Data</h1>
<table class="table table-condensed table-bordered table-striped table-hover" style="table-layout: fixed;">
<thead> <?php
<tr>
<th style="width: 120px;">Tag</th> $timeFormatter = extension_loaded('intl') ? Yii::createObject(['class' => 'yii\i18n\Formatter']) : Yii::$app->formatter;
<th style="width: 170px;">Time</th>
<th style="width: 120px;">IP</th> echo GridView::widget([
<th style="width: 70px;">Method</th> 'dataProvider' => $dataProvider,
<th>URL</th> 'filterModel' => $searchModel,
</tr> 'rowOptions' => function ($model, $key, $index, $grid) use ($searchModel) {
</thead> if ($searchModel->isCodeCritical($model['statusCode'])) {
<tbody> return ['class'=>'danger'];
<?php foreach ($manifest as $tag => $data): ?> } else {
<tr> return [];
<td><?= Html::a($tag, ['view', 'tag' => $tag]) ?></td> }
<td><?= date('Y-m-d h:i:sa', $data['time']) ?></td> },
<td><?= $data['ip'] ?></td> 'columns' => [
<td><?= $data['method'] ?></td> ['class' => 'yii\grid\SerialColumn'],
<td><?= $data['url'] ?></td> [
</tr> 'attribute' => 'tag',
<?php endforeach; ?> 'value' => function ($data)
</tbody> {
</table> return Html::a($data['tag'], ['view', 'tag' => $data['tag']]);
},
'format' => 'html',
],
[
'attribute' => 'time',
'value' => function ($data) use ($timeFormatter)
{
return $timeFormatter->asDateTime($data['time'], 'long');
},
],
'ip',
[
'attribute' => 'sqlCount',
'label' => 'Total queries count'
],
[
'attribute' => 'method',
'filter' => ['get' => 'GET', 'post' => 'POST', 'delete' => 'DELETE', 'put' => 'PUT', 'head' => 'HEAD']
],
[
'attribute'=>'ajax',
'value' => function ($data)
{
return $data['ajax'] ? 'Yes' : 'No';
},
'filter' => ['No', 'Yes'],
],
'url',
[
'attribute' => 'statusCode',
'filter' => [200 => 200, 404 => 404, 403 => 403, 500 => 500],
'label' => 'Status code'
],
],
]); ?>
</div> </div>
</div> </div>
</div> </div>

2
extensions/yii/debug/views/layouts/main.php

@ -11,6 +11,8 @@ yii\debug\DebugAsset::register($this);
<html> <html>
<?php $this->beginPage() ?> <?php $this->beginPage() ?>
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= Html::encode($this->title) ?></title> <title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?> <?php $this->head() ?>
</head> </head>

1
extensions/yii/gii/generators/crud/templates/search.php

@ -74,7 +74,6 @@ class <?= $searchModelClass ?> extends Model
return; return;
} }
if ($partialMatch) { if ($partialMatch) {
$value = '%' . strtr($value, ['%'=>'\%', '_'=>'\_', '\\'=>'\\\\']) . '%';
$query->andWhere(['like', $attribute, $value]); $query->andWhere(['like', $attribute, $value]);
} else { } else {
$query->andWhere([$attribute => $value]); $query->andWhere([$attribute => $value]);

1
extensions/yii/gii/views/layouts/main.php

@ -14,6 +14,7 @@ $asset = yii\gii\GiiAsset::register($this);
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= Html::encode($this->title) ?></title> <title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?> <?php $this->head() ?>
</head> </head>

4
extensions/yii/sphinx/Query.php

@ -429,6 +429,8 @@ class Query extends Component implements QueryInterface
* using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate * using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate
* `name LIKE '%test%' AND name LIKE '%sample%'`. * `name LIKE '%test%' AND name LIKE '%sample%'`.
* The method will properly quote the column name and escape values in the range. * The method will properly quote the column name and escape values in the range.
* Sometimes, you may want to add the percentage characters to the matching value by yourself, you may supply
* a third operand `false` to do so. For example, `['like', 'name', '%tester', false]` will generate `name LIKE '%tester'`.
* *
* - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE`
* predicates when operand 2 is an array. * predicates when operand 2 is an array.
@ -700,4 +702,4 @@ class Query extends Component implements QueryInterface
->callSnippets($this->from[0], $source, $match, $this->snippetOptions) ->callSnippets($this->from[0], $source, $match, $this->snippetOptions)
->queryColumn(); ->queryColumn();
} }
} }

25
extensions/yii/sphinx/QueryBuilder.php

@ -754,11 +754,19 @@ class QueryBuilder extends Object
* Creates an SQL expressions with the `LIKE` operator. * Creates an SQL expressions with the `LIKE` operator.
* @param IndexSchema[] $indexes list of indexes, which affected by query * @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`) * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
* @param array $operands the first operand is the column name. * @param array $operands an array of two or three operands
* The second operand is a single value or an array of values that column value *
* should be compared with. * - The first operand is the column name.
* If it is an empty array the generated expression will be a `false` value if * - The second operand is a single value or an array of values that column value
* operator is `LIKE` or `OR LIKE` and empty if operator is `NOT LIKE` or `OR NOT LIKE`. * should be compared with. If it is an empty array the generated expression will
* be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator
* is `NOT LIKE` or `OR NOT LIKE`.
* - An optional third operand can also be provided to specify how to escape special characters
* in the value(s). The operand should be an array of mappings from the special characters to their
* escaped counterparts. If this operand is not provided, a default escape mapping will be used.
* You may use `false` or an empty array to indicate the values are already escaped and no escape
* should be applied. Note that when using an escape mapping (or the third operand is not provided),
* the values will be automatically enclosed within a pair of percentage characters.
* @param array $params the binding parameters to be populated * @param array $params the binding parameters to be populated
* @return string the generated SQL expression * @return string the generated SQL expression
* @throws InvalidParamException if wrong number of operands have been given. * @throws InvalidParamException if wrong number of operands have been given.
@ -769,6 +777,9 @@ class QueryBuilder extends Object
throw new InvalidParamException("Operator '$operator' requires two operands."); throw new InvalidParamException("Operator '$operator' requires two operands.");
} }
$escape = isset($operands[2]) ? $operands[2] : ['%'=>'\%', '_'=>'\_', '\\'=>'\\\\'];
unset($operands[2]);
list($column, $values) = $operands; list($column, $values) = $operands;
$values = (array)$values; $values = (array)$values;
@ -791,7 +802,7 @@ class QueryBuilder extends Object
$parts = []; $parts = [];
foreach ($values as $value) { foreach ($values as $value) {
$phName = self::PARAM_PREFIX . count($params); $phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value; $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%');
$parts[] = "$column $operator $phName"; $parts[] = "$column $operator $phName";
} }
@ -902,4 +913,4 @@ class QueryBuilder extends Object
return $phName; return $phName;
} }
} }
} }

6
framework/CHANGELOG.md

@ -15,10 +15,14 @@ Yii Framework 2 Change Log
- Bug #1582: Error messages shown via client-side validation should not be double encoded (qiangxue) - Bug #1582: Error messages shown via client-side validation should not be double encoded (qiangxue)
- Bug #1591: StringValidator is accessing undefined property (qiangxue) - Bug #1591: StringValidator is accessing undefined property (qiangxue)
- Bug #1597: Added `enableAutoLogin` to basic and advanced application templates so "remember me" now works properly (samdark) - Bug #1597: Added `enableAutoLogin` to basic and advanced application templates so "remember me" now works properly (samdark)
- Bug #1631: Charset is now explicitly set to UTF-8 when serving JSON (samdark)
- Bug #1686: ActiveForm is creating duplicated messages in error summary (qiangxue)
- Bug: Fixed `Call to a member function registerAssetFiles() on a non-object` in case of wrong `sourcePath` for an asset bundle (samdark) - Bug: Fixed `Call to a member function registerAssetFiles() on a non-object` in case of wrong `sourcePath` for an asset bundle (samdark)
- Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark) - Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark)
- Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe) - Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe)
- Bug: Fixed issue with tabular input on ActiveField::radio() and ActiveField::checkbox() (jom) - Bug: Fixed issue with tabular input on ActiveField::radio() and ActiveField::checkbox() (jom)
- Bug: Fixed the issue that query cache returns the same data for the same SQL but different query methods (qiangxue)
- Enh #364: Improve Inflector::slug with `intl` transliteration. Improved transliteration char map. (tonydspaniard)
- Enh #797: Added support for validating multiple columns by `UniqueValidator` and `ExistValidator` (qiangxue) - Enh #797: Added support for validating multiple columns by `UniqueValidator` and `ExistValidator` (qiangxue)
- Enh #1293: Replaced Console::showProgress() with a better approach. See Console::startProgress() for details (cebe) - Enh #1293: Replaced Console::showProgress() with a better approach. See Console::startProgress() for details (cebe)
- Enh #1406: DB Schema support for Oracle Database (p0larbeer, qiangxue) - Enh #1406: DB Schema support for Oracle Database (p0larbeer, qiangxue)
@ -36,10 +40,12 @@ Yii Framework 2 Change Log
- Enh #1641: Added `BaseActiveRecord::updateAttributes()` (qiangxue) - Enh #1641: Added `BaseActiveRecord::updateAttributes()` (qiangxue)
- Enh #1646: Added postgresql `QueryBuilder::checkIntegrity` and `QueryBuilder::resetSequence` (Ragazzo) - Enh #1646: Added postgresql `QueryBuilder::checkIntegrity` and `QueryBuilder::resetSequence` (Ragazzo)
- Enh #1645: Added `Connection::$pdoClass` property (Ragazzo) - Enh #1645: Added `Connection::$pdoClass` property (Ragazzo)
- Enh #1681: Added support for automatically adjusting the "for" attribute of label generated by `ActiveField::label()` (qiangxue)
- Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark) - Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark)
- Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue)
- Enh: Support for file aliases in console command 'message' (omnilight) - Enh: Support for file aliases in console command 'message' (omnilight)
- Enh: Sort and Pagination can now create absolute URLs (cebe) - Enh: Sort and Pagination can now create absolute URLs (cebe)
- Chg #1586: `QueryBuilder::buildLikeCondition()` will now escape special characters and use percentage characters by default (qiangxue)
- Chg #1610: `Html::activeCheckboxList()` and `Html::activeRadioList()` will submit an empty string if no checkbox/radio is selected (qiangxue) - Chg #1610: `Html::activeCheckboxList()` and `Html::activeRadioList()` will submit an empty string if no checkbox/radio is selected (qiangxue)
- Chg #1643: Added default value for `Captcha::options` (qiangxue) - Chg #1643: Added default value for `Captcha::options` (qiangxue)
- Chg: Renamed `yii\jui\Widget::clientEventsMap` to `clientEventMap` (qiangxue) - Chg: Renamed `yii\jui\Widget::clientEventsMap` to `clientEventMap` (qiangxue)

2
framework/yii/assets/yii.activeForm.js

@ -365,7 +365,7 @@
var updateSummary = function ($form, messages) { var updateSummary = function ($form, messages) {
var data = $form.data('yiiActiveForm'), var data = $form.data('yiiActiveForm'),
$summary = $form.find(data.settings.errorSummary), $summary = $form.find(data.settings.errorSummary),
$ul = $summary.find('ul'); $ul = $summary.find('ul').html('');
if ($summary.length && messages) { if ($summary.length && messages) {
$.each(data.attributes, function () { $.each(data.attributes, function () {

13
framework/yii/db/Command.php

@ -368,7 +368,7 @@ class Command extends \yii\base\Component
$db = $this->db; $db = $this->db;
$rawSql = $this->getRawSql(); $rawSql = $this->getRawSql();
Yii::info($rawSql, __METHOD__); Yii::info($rawSql, 'yii\db\Command::query');
/** @var \yii\caching\Cache $cache */ /** @var \yii\caching\Cache $cache */
if ($db->enableQueryCache && $method !== '') { if ($db->enableQueryCache && $method !== '') {
@ -378,19 +378,20 @@ class Command extends \yii\base\Component
if (isset($cache) && $cache instanceof Cache) { if (isset($cache) && $cache instanceof Cache) {
$cacheKey = [ $cacheKey = [
__CLASS__, __CLASS__,
$method,
$db->dsn, $db->dsn,
$db->username, $db->username,
$rawSql, $rawSql,
]; ];
if (($result = $cache->get($cacheKey)) !== false) { if (($result = $cache->get($cacheKey)) !== false) {
Yii::trace('Query result served from cache', __METHOD__); Yii::trace('Query result served from cache', 'yii\db\Command::query');
return $result; return $result;
} }
} }
$token = $rawSql; $token = $rawSql;
try { try {
Yii::beginProfile($token, __METHOD__); Yii::beginProfile($token, 'yii\db\Command::query');
$this->prepare(); $this->prepare();
$this->pdoStatement->execute(); $this->pdoStatement->execute();
@ -405,16 +406,16 @@ class Command extends \yii\base\Component
$this->pdoStatement->closeCursor(); $this->pdoStatement->closeCursor();
} }
Yii::endProfile($token, __METHOD__); Yii::endProfile($token, 'yii\db\Command::query');
if (isset($cache, $cacheKey) && $cache instanceof Cache) { if (isset($cache, $cacheKey) && $cache instanceof Cache) {
$cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency); $cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency);
Yii::trace('Saved query result in cache', __METHOD__); Yii::trace('Saved query result in cache', 'yii\db\Command::query');
} }
return $result; return $result;
} catch (\Exception $e) { } catch (\Exception $e) {
Yii::endProfile($token, __METHOD__); Yii::endProfile($token, 'yii\db\Command::query');
if ($e instanceof Exception) { if ($e instanceof Exception) {
throw $e; throw $e;
} else { } else {

8
framework/yii/db/Query.php

@ -387,11 +387,13 @@ class Query extends Component implements QueryInterface
* *
* - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing
* the values that the column or DB expression should be like. * the values that the column or DB expression should be like.
* For example, `['like', 'name', '%tester%']` will generate `name LIKE '%tester%'`. * For example, `['like', 'name', 'tester']` will generate `name LIKE '%tester%'`.
* When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated
* using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate * using `AND`. For example, `['like', 'name', ['test', 'sample']]` will generate
* `name LIKE '%test%' AND name LIKE '%sample%'`. * `name LIKE '%test%' AND name LIKE '%sample%'`.
* The method will properly quote the column name and escape values in the range. * The method will properly quote the column name and escape special characters in the values.
* Sometimes, you may want to add the percentage characters to the matching value by yourself, you may supply
* a third operand `false` to do so. For example, `['like', 'name', '%tester', false]` will generate `name LIKE '%tester'`.
* *
* - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE`
* predicates when operand 2 is an array. * predicates when operand 2 is an array.

23
framework/yii/db/QueryBuilder.php

@ -1017,11 +1017,19 @@ class QueryBuilder extends \yii\base\Object
/** /**
* Creates an SQL expressions with the `LIKE` operator. * Creates an SQL expressions with the `LIKE` operator.
* @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`) * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
* @param array $operands the first operand is the column name. * @param array $operands an array of two or three operands
* The second operand is a single value or an array of values that column value *
* should be compared with. * - The first operand is the column name.
* If it is an empty array the generated expression will be a `false` value if * - The second operand is a single value or an array of values that column value
* operator is `LIKE` or `OR LIKE` and empty if operator is `NOT LIKE` or `OR NOT LIKE`. * should be compared with. If it is an empty array the generated expression will
* be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator
* is `NOT LIKE` or `OR NOT LIKE`.
* - An optional third operand can also be provided to specify how to escape special characters
* in the value(s). The operand should be an array of mappings from the special characters to their
* escaped counterparts. If this operand is not provided, a default escape mapping will be used.
* You may use `false` or an empty array to indicate the values are already escaped and no escape
* should be applied. Note that when using an escape mapping (or the third operand is not provided),
* the values will be automatically enclosed within a pair of percentage characters.
* @param array $params the binding parameters to be populated * @param array $params the binding parameters to be populated
* @return string the generated SQL expression * @return string the generated SQL expression
* @throws InvalidParamException if wrong number of operands have been given. * @throws InvalidParamException if wrong number of operands have been given.
@ -1032,6 +1040,9 @@ class QueryBuilder extends \yii\base\Object
throw new InvalidParamException("Operator '$operator' requires two operands."); throw new InvalidParamException("Operator '$operator' requires two operands.");
} }
$escape = isset($operands[2]) ? $operands[2] : ['%'=>'\%', '_'=>'\_', '\\'=>'\\\\'];
unset($operands[2]);
list($column, $values) = $operands; list($column, $values) = $operands;
$values = (array)$values; $values = (array)$values;
@ -1054,7 +1065,7 @@ class QueryBuilder extends \yii\base\Object
$parts = []; $parts = [];
foreach ($values as $value) { foreach ($values as $value) {
$phName = self::PARAM_PREFIX . count($params); $phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value; $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%');
$parts[] = "$column $operator $phName"; $parts[] = "$column $operator $phName";
} }

10
framework/yii/db/QueryInterface.php

@ -122,11 +122,13 @@ interface QueryInterface
* *
* - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing
* the values that the column or DB expression should be like. * the values that the column or DB expression should be like.
* For example, `['like', 'name', '%tester%']` will generate `name LIKE '%tester%'`. * For example, `['like', 'name', 'tester']` will generate `name LIKE '%tester%'`.
* When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated
* using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate * using `AND`. For example, `['like', 'name', ['test', 'sample']]` will generate
* `name LIKE '%test%' AND name LIKE '%sample%'`. * `name LIKE '%test%' AND name LIKE '%sample%'`.
* The method will properly quote the column name and escape values in the range. * The method will properly quote the column name and escape special characters in the values.
* Sometimes, you may want to add the percentage characters to the matching value by yourself, you may supply
* a third operand `false` to do so. For example, `['like', 'name', '%tester', false]` will generate `name LIKE '%tester'`.
* *
* - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE`
* predicates when operand 2 is an array. * predicates when operand 2 is an array.
@ -203,4 +205,4 @@ interface QueryInterface
* @return static the query object itself * @return static the query object itself
*/ */
public function offset($offset); public function offset($offset);
} }

2
framework/yii/db/mysql/Schema.php

@ -280,7 +280,7 @@ class Schema extends \yii\db\Schema
* @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
* @return array all table names in the database. The names have NO schema name prefix. * @return array all table names in the database. The names have NO schema name prefix.
*/ */
public function findTableNames($schema = '') protected function findTableNames($schema = '')
{ {
$sql = 'SHOW TABLES'; $sql = 'SHOW TABLES';
if ($schema !== '') { if ($schema !== '') {

2
framework/yii/db/pgsql/QueryBuilder.php

@ -109,7 +109,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
{ {
$enable = $check ? 'ENABLE' : 'DISABLE'; $enable = $check ? 'ENABLE' : 'DISABLE';
$schema = $schema ? $schema : $this->db->schema->defaultSchema; $schema = $schema ? $schema : $this->db->schema->defaultSchema;
$tableNames = $table ? [$table] : $this->db->schema->findTableNames($schema); $tableNames = $table ? [$table] : $this->db->schema->getTableNames($schema);
$command = ''; $command = '';
foreach ($tableNames as $tableName) { foreach ($tableNames as $tableName) {

2
framework/yii/db/pgsql/Schema.php

@ -158,7 +158,7 @@ class Schema extends \yii\db\Schema
* @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
* @return array all table names in the database. The names have NO schema name prefix. * @return array all table names in the database. The names have NO schema name prefix.
*/ */
public function findTableNames($schema = '') protected function findTableNames($schema = '')
{ {
if ($schema === '') { if ($schema === '') {
$schema = $this->defaultSchema; $schema = $this->defaultSchema;

2
framework/yii/db/sqlite/Schema.php

@ -87,7 +87,7 @@ class Schema extends \yii\db\Schema
* If not empty, the returned table names will be prefixed with the schema name. * If not empty, the returned table names will be prefixed with the schema name.
* @return array all table names in the database. * @return array all table names in the database.
*/ */
public function findTableNames($schema = '') protected function findTableNames($schema = '')
{ {
$sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence'"; $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence'";
return $this->db->createCommand($sql)->queryColumn(); return $this->db->createCommand($sql)->queryColumn();

129
framework/yii/helpers/BaseInflector.php

@ -215,60 +215,67 @@ class BaseInflector
'wildebeest' => 'wildebeest', 'wildebeest' => 'wildebeest',
'Yengeese' => 'Yengeese', 'Yengeese' => 'Yengeese',
]; ];
/** /**
* @var array map of special chars and its translation. This is used by [[slug()]]. * @var array map of special chars and its translation. This is used by [[slug()]].
*/ */
public static $transliteration = [ public static $transliteration = [
'/ä|æ|ǽ/' => 'ae', // Latin
'/ö|œ/' => 'oe', 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'Æ' => 'AE', 'Ç' => 'C',
'/ü/' => 'ue', 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',
'/Ä/' => 'Ae', 'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ő' => 'O',
'/Ü/' => 'Ue', 'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ű' => 'U', 'Ý' => 'Y', 'Þ' => 'TH',
'/Ö/' => 'Oe', 'ß' => 'ss',
'/À|Á|Â|Ã|Å|Ǻ|Ā|Ă|Ą|Ǎ/' => 'A', 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'ae', 'ç' => 'c',
'/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª/' => 'a', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
'/Ç|Ć|Ĉ|Ċ|Č/' => 'C', 'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ő' => 'o',
'/ç|ć|ĉ|ċ|č/' => 'c', 'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ű' => 'u', 'ý' => 'y', 'þ' => 'th',
'/Ð|Ď|Đ/' => 'D', 'ÿ' => 'y',
'/ð|ď|đ/' => 'd', // Latin symbols
'/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě/' => 'E', '©' => '(c)',
'/è|é|ê|ë|ē|ĕ|ė|ę|ě/' => 'e', // Greek
'/Ĝ|Ğ|Ġ|Ģ/' => 'G', 'Α' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E', 'Ζ' => 'Z', 'Η' => 'H', 'Θ' => '8',
'/ĝ|ğ|ġ|ģ/' => 'g', 'Ι' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => '3', 'Ο' => 'O', 'Π' => 'P',
'/Ĥ|Ħ/' => 'H', 'Ρ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'PS', 'Ω' => 'W',
'/ĥ|ħ/' => 'h', 'Ά' => 'A', 'Έ' => 'E', 'Ί' => 'I', 'Ό' => 'O', 'Ύ' => 'Y', 'Ή' => 'H', 'Ώ' => 'W', 'Ϊ' => 'I',
'/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ/' => 'I', 'Ϋ' => 'Y',
'/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı/' => 'i', 'α' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 'ζ' => 'z', 'η' => 'h', 'θ' => '8',
'/Ĵ/' => 'J', 'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => '3', 'ο' => 'o', 'π' => 'p',
'/ĵ/' => 'j', 'ρ' => 'r', 'σ' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'ps', 'ω' => 'w',
'/Ķ/' => 'K', 'ά' => 'a', 'έ' => 'e', 'ί' => 'i', 'ό' => 'o', 'ύ' => 'y', 'ή' => 'h', 'ώ' => 'w', 'ς' => 's',
'/ķ/' => 'k', 'ϊ' => 'i', 'ΰ' => 'y', 'ϋ' => 'y', 'ΐ' => 'i',
'/Ĺ|Ļ|Ľ|Ŀ|Ł/' => 'L', // Turkish
'/ĺ|ļ|ľ|ŀ|ł/' => 'l', 'Ş' => 'S', 'İ' => 'I', 'Ç' => 'C', 'Ü' => 'U', 'Ö' => 'O', 'Ğ' => 'G',
'/Ñ|Ń|Ņ|Ň/' => 'N', 'ş' => 's', 'ı' => 'i', 'ç' => 'c', 'ü' => 'u', 'ö' => 'o', 'ğ' => 'g',
'/ñ|ń|ņ|ň|ʼn/' => 'n', // Russian
'/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ/' => 'O', 'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'Yo', 'Ж' => 'Zh',
'/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º/' => 'o', 'З' => 'Z', 'И' => 'I', 'Й' => 'J', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O',
'/Ŕ|Ŗ|Ř/' => 'R', 'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C',
'/ŕ|ŗ|ř/' => 'r', 'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sh', 'Ъ' => '', 'Ы' => 'Y', 'Ь' => '', 'Э' => 'E', 'Ю' => 'Yu',
'/Ś|Ŝ|Ş|Ș|Š/' => 'S', 'Я' => 'Ya',
'/ś|ŝ|ş|ș|š|ſ/' => 's', 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh',
'/Ţ|Ț|Ť|Ŧ/' => 'T', 'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o',
'/ţ|ț|ť|ŧ/' => 't', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c',
'/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ/' => 'U', 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sh', 'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu',
'/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ/' => 'u', 'я' => 'ya',
'/Ý|Ÿ|Ŷ/' => 'Y', // Ukrainian
'/ý|ÿ|ŷ/' => 'y', 'Є' => 'Ye', 'І' => 'I', 'Ї' => 'Yi', 'Ґ' => 'G',
'/Ŵ/' => 'W', 'є' => 'ye', 'і' => 'i', 'ї' => 'yi', 'ґ' => 'g',
'/ŵ/' => 'w', // Czech
'/Ź|Ż|Ž/' => 'Z', 'Č' => 'C', 'Ď' => 'D', 'Ě' => 'E', 'Ň' => 'N', 'Ř' => 'R', 'Š' => 'S', 'Ť' => 'T', 'Ů' => 'U',
'/ź|ż|ž/' => 'z', 'Ž' => 'Z',
'/Æ|Ǽ/' => 'AE', 'č' => 'c', 'ď' => 'd', 'ě' => 'e', 'ň' => 'n', 'ř' => 'r', 'š' => 's', 'ť' => 't', 'ů' => 'u',
'/ß/' => 'ss', 'ž' => 'z',
'/IJ/' => 'IJ', // Polish
'/ij/' => 'ij', 'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'e', 'Ł' => 'L', 'Ń' => 'N', 'Ó' => 'o', 'Ś' => 'S', 'Ź' => 'Z',
'/Œ/' => 'OE', 'Ż' => 'Z',
'/ƒ/' => 'f' 'ą' => 'a', 'ć' => 'c', 'ę' => 'e', 'ł' => 'l', 'ń' => 'n', 'ó' => 'o', 'ś' => 's', 'ź' => 'z',
'ż' => 'z',
// Latvian
'Ā' => 'A', 'Č' => 'C', 'Ē' => 'E', 'Ģ' => 'G', 'Ī' => 'i', 'Ķ' => 'k', 'Ļ' => 'L', 'Ņ' => 'N',
'Š' => 'S', 'Ū' => 'u', 'Ž' => 'Z',
'ā' => 'a', 'č' => 'c', 'ē' => 'e', 'ģ' => 'g', 'ī' => 'i', 'ķ' => 'k', 'ļ' => 'l', 'ņ' => 'n',
'š' => 's', 'ū' => 'u', 'ž' => 'z'
]; ];
/** /**
@ -434,20 +441,24 @@ class BaseInflector
/** /**
* Returns a string with all spaces converted to given replacement and * Returns a string with all spaces converted to given replacement and
* non word characters removed. Maps special characters to ASCII using * non word characters removed. Maps special characters to ASCII using
* `Inflector::$transliteration` * [[$transliteration]] array.
* @param string $string An arbitrary string to convert * @param string $string An arbitrary string to convert
* @param string $replacement The replacement to use for spaces * @param string $replacement The replacement to use for spaces
* @param bool $lowercase whether to return the string in lowercase or not. Defaults to `true`.
* @return string The converted string. * @return string The converted string.
*/ */
public static function slug($string, $replacement = '-') public static function slug($string, $replacement = '-', $lowercase = true)
{ {
$map = static::$transliteration + [ if (extension_loaded('intl') === true) {
'/[^\w\s]/' => ' ', $options = 'Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove;';
'/\\s+/' => $replacement, $string = transliterator_transliterate($options, $string);
'/(?<=[a-z])([A-Z])/' => $replacement . '\\1', $string = preg_replace('/[-\s]+/', $replacement, $string);
str_replace(':rep', preg_quote($replacement, '/'), '/^[:rep]+|[:rep]+$/') => '' } else {
]; $string = str_replace(array_keys(static::$transliteration), static::$transliteration, $string);
return preg_replace(array_keys($map), array_values($map), $string); $string = preg_replace('/[^\p{L}\p{Nd}]+/u', $replacement, $string);
}
$string = trim($string, $replacement);
return $lowercase ? strtolower($string) : $string;
} }
/** /**

2
framework/yii/rbac/Manager.php

@ -21,7 +21,7 @@ use yii\base\InvalidParamException;
* Access Control (RBAC). * Access Control (RBAC).
* *
* The main idea is that permissions are organized as a hierarchy of * The main idea is that permissions are organized as a hierarchy of
* [[Item]] authorization items. Items on higer level inherit the permissions * [[Item]] authorization items. Items on higher level inherit the permissions
* represented by items on lower level. And roles are simply top-level authorization items * represented by items on lower level. And roles are simply top-level authorization items
* that may be assigned to individual users. A user is said to have a permission * that may be assigned to individual users. A user is said to have a permission
* to do something if the corresponding authorization item is inherited by one of his roles. * to do something if the corresponding authorization item is inherited by one of his roles.

4
framework/yii/web/Request.php

@ -577,7 +577,7 @@ class Request extends \yii\base\Request
$pathInfo = substr($pathInfo, strlen($scriptUrl)); $pathInfo = substr($pathInfo, strlen($scriptUrl));
} elseif ($baseUrl === '' || strpos($pathInfo, $baseUrl) === 0) { } elseif ($baseUrl === '' || strpos($pathInfo, $baseUrl) === 0) {
$pathInfo = substr($pathInfo, strlen($baseUrl)); $pathInfo = substr($pathInfo, strlen($baseUrl));
} elseif (strpos($_SERVER['PHP_SELF'], $scriptUrl) === 0) { } elseif (isset($_SERVER['PHP_SELF']) && strpos($_SERVER['PHP_SELF'], $scriptUrl) === 0) {
$pathInfo = substr($_SERVER['PHP_SELF'], strlen($scriptUrl)); $pathInfo = substr($_SERVER['PHP_SELF'], strlen($scriptUrl));
} else { } else {
throw new InvalidConfigException('Unable to determine the path info of the current request.'); throw new InvalidConfigException('Unable to determine the path info of the current request.');
@ -1121,7 +1121,7 @@ class Request extends \yii\base\Request
private function validateCsrfTokenInternal($token, $trueToken) private function validateCsrfTokenInternal($token, $trueToken)
{ {
$token = str_replace('.', '+', base64_decode($token)); $token = base64_decode(str_replace('.', '+', $token));
$n = StringHelper::byteLength($token); $n = StringHelper::byteLength($token);
if ($n <= self::CSRF_MASK_LENGTH) { if ($n <= self::CSRF_MASK_LENGTH) {
return false; return false;

2
framework/yii/web/Response.php

@ -799,7 +799,7 @@ class Response extends \yii\base\Response
$this->content = $this->data; $this->content = $this->data;
break; break;
case self::FORMAT_JSON: case self::FORMAT_JSON:
$this->getHeaders()->set('Content-Type', 'application/json'); $this->getHeaders()->set('Content-Type', 'application/json; charset=UTF-8');
$this->content = Json::encode($this->data); $this->content = Json::encode($this->data);
break; break;
case self::FORMAT_JSONP: case self::FORMAT_JSONP:

22
framework/yii/widgets/ActiveField.php

@ -280,6 +280,7 @@ class ActiveField extends Component
public function input($type, $options = []) public function input($type, $options = [])
{ {
$options = array_merge($this->inputOptions, $options); $options = array_merge($this->inputOptions, $options);
$this->adjustLabelFor($options);
$this->parts['{input}'] = Html::activeInput($type, $this->model, $this->attribute, $options); $this->parts['{input}'] = Html::activeInput($type, $this->model, $this->attribute, $options);
return $this; return $this;
} }
@ -295,6 +296,7 @@ class ActiveField extends Component
public function textInput($options = []) public function textInput($options = [])
{ {
$options = array_merge($this->inputOptions, $options); $options = array_merge($this->inputOptions, $options);
$this->adjustLabelFor($options);
$this->parts['{input}'] = Html::activeTextInput($this->model, $this->attribute, $options); $this->parts['{input}'] = Html::activeTextInput($this->model, $this->attribute, $options);
return $this; return $this;
} }
@ -310,6 +312,7 @@ class ActiveField extends Component
public function passwordInput($options = []) public function passwordInput($options = [])
{ {
$options = array_merge($this->inputOptions, $options); $options = array_merge($this->inputOptions, $options);
$this->adjustLabelFor($options);
$this->parts['{input}'] = Html::activePasswordInput($this->model, $this->attribute, $options); $this->parts['{input}'] = Html::activePasswordInput($this->model, $this->attribute, $options);
return $this; return $this;
} }
@ -328,6 +331,7 @@ class ActiveField extends Component
if ($this->inputOptions !== ['class' => 'form-control']) { if ($this->inputOptions !== ['class' => 'form-control']) {
$options = array_merge($this->inputOptions, $options); $options = array_merge($this->inputOptions, $options);
} }
$this->adjustLabelFor($options);
$this->parts['{input}'] = Html::activeFileInput($this->model, $this->attribute, $options); $this->parts['{input}'] = Html::activeFileInput($this->model, $this->attribute, $options);
return $this; return $this;
} }
@ -342,6 +346,7 @@ class ActiveField extends Component
public function textarea($options = []) public function textarea($options = [])
{ {
$options = array_merge($this->inputOptions, $options); $options = array_merge($this->inputOptions, $options);
$this->adjustLabelFor($options);
$this->parts['{input}'] = Html::activeTextarea($this->model, $this->attribute, $options); $this->parts['{input}'] = Html::activeTextarea($this->model, $this->attribute, $options);
return $this; return $this;
} }
@ -379,6 +384,7 @@ class ActiveField extends Component
} else { } else {
$this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options); $this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options);
} }
$this->adjustLabelFor($options);
return $this; return $this;
} }
@ -415,6 +421,7 @@ class ActiveField extends Component
} else { } else {
$this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options); $this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options);
} }
$this->adjustLabelFor($options);
return $this; return $this;
} }
@ -453,6 +460,7 @@ class ActiveField extends Component
public function dropDownList($items, $options = []) public function dropDownList($items, $options = [])
{ {
$options = array_merge($this->inputOptions, $options); $options = array_merge($this->inputOptions, $options);
$this->adjustLabelFor($options);
$this->parts['{input}'] = Html::activeDropDownList($this->model, $this->attribute, $items, $options); $this->parts['{input}'] = Html::activeDropDownList($this->model, $this->attribute, $items, $options);
return $this; return $this;
} }
@ -495,6 +503,7 @@ class ActiveField extends Component
public function listBox($items, $options = []) public function listBox($items, $options = [])
{ {
$options = array_merge($this->inputOptions, $options); $options = array_merge($this->inputOptions, $options);
$this->adjustLabelFor($options);
$this->parts['{input}'] = Html::activeListBox($this->model, $this->attribute, $items, $options); $this->parts['{input}'] = Html::activeListBox($this->model, $this->attribute, $items, $options);
return $this; return $this;
} }
@ -526,6 +535,7 @@ class ActiveField extends Component
*/ */
public function checkboxList($items, $options = []) public function checkboxList($items, $options = [])
{ {
$this->adjustLabelFor($options);
$this->parts['{input}'] = Html::activeCheckboxList($this->model, $this->attribute, $items, $options); $this->parts['{input}'] = Html::activeCheckboxList($this->model, $this->attribute, $items, $options);
return $this; return $this;
} }
@ -556,6 +566,7 @@ class ActiveField extends Component
*/ */
public function radioList($items, $options = []) public function radioList($items, $options = [])
{ {
$this->adjustLabelFor($options);
$this->parts['{input}'] = Html::activeRadioList($this->model, $this->attribute, $items, $options); $this->parts['{input}'] = Html::activeRadioList($this->model, $this->attribute, $items, $options);
return $this; return $this;
} }
@ -584,6 +595,17 @@ class ActiveField extends Component
} }
/** /**
* Adjusts the "for" attribute for the label based on the input options.
* @param array $options the input options
*/
protected function adjustLabelFor($options)
{
if (isset($options['id']) && !isset($this->labelOptions['for'])) {
$this->labelOptions['for'] = $options['id'];
}
}
/**
* Returns the JS options for the field. * Returns the JS options for the field.
* @return array the JS options * @return array the JS options
*/ */

2
tests/unit/framework/db/SchemaTest.php

@ -11,7 +11,7 @@ use yii\db\Schema;
*/ */
class SchemaTest extends DatabaseTestCase class SchemaTest extends DatabaseTestCase
{ {
public function testFindTableNames() public function testGetTableNames()
{ {
/** @var Schema $schema */ /** @var Schema $schema */
$schema = $this->getConnection()->schema; $schema = $this->getConnection()->schema;

2
tests/unit/framework/helpers/InflectorTest.php

@ -123,6 +123,8 @@ class InflectorTest extends TestCase
public function testSlug() public function testSlug()
{ {
$this->assertEquals("privet-hello-jii-framework-kak-dela-how-it-goes", Inflector::slug('Привет Hello Йии-- Framework !--- Как дела ? How it goes ?'));
$this->assertEquals("this-is-a-title", Inflector::slug('this is a title')); $this->assertEquals("this-is-a-title", Inflector::slug('this is a title'));
} }

Loading…
Cancel
Save