diff --git a/apps/advanced/backend/views/layouts/main.php b/apps/advanced/backend/views/layouts/main.php index 1857e62..d1ede45 100644 --- a/apps/advanced/backend/views/layouts/main.php +++ b/apps/advanced/backend/views/layouts/main.php @@ -16,6 +16,7 @@ AppAsset::register($this); + <?= Html::encode($this->title) ?> head() ?> diff --git a/apps/advanced/backend/web/css/site.css b/apps/advanced/backend/web/css/site.css index 2d27436..bf9d2b2 100644 --- a/apps/advanced/backend/web/css/site.css +++ b/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.desc:after { content: "\e156"; } + +.error-summary { + color: #b94a48; + background: #fdf7f7; + border-left: 3px solid #eed3d7; + padding: 10px 20px; + margin: 0 0 15px 0; +} diff --git a/apps/advanced/frontend/views/layouts/main.php b/apps/advanced/frontend/views/layouts/main.php index 7bc03d2..2bfa9b0 100644 --- a/apps/advanced/frontend/views/layouts/main.php +++ b/apps/advanced/frontend/views/layouts/main.php @@ -17,6 +17,7 @@ AppAsset::register($this); + <?= Html::encode($this->title) ?> head() ?> diff --git a/apps/advanced/frontend/web/css/site.css b/apps/advanced/frontend/web/css/site.css index 2d27436..bf9d2b2 100644 --- a/apps/advanced/frontend/web/css/site.css +++ b/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.desc:after { content: "\e156"; } + +.error-summary { + color: #b94a48; + background: #fdf7f7; + border-left: 3px solid #eed3d7; + padding: 10px 20px; + margin: 0 0 15px 0; +} diff --git a/apps/basic/config/codeception/acceptance.php b/apps/basic/config/codeception/acceptance.php deleted file mode 100644 index 6036f88..0000000 --- a/apps/basic/config/codeception/acceptance.php +++ /dev/null @@ -1,11 +0,0 @@ - [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2basic_acceptance', - ], - ], -]; diff --git a/apps/basic/config/codeception/functional.php b/apps/basic/config/codeception/functional.php deleted file mode 100644 index 210b97d..0000000 --- a/apps/basic/config/codeception/functional.php +++ /dev/null @@ -1,17 +0,0 @@ - [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2basic_functional', - ], - 'request' => [ - 'enableCsrfValidation' => false, - ], - 'urlManager' => [ - 'baseUrl' => '/web/index.php', - ], - ], -]; diff --git a/apps/basic/config/codeception/unit.php b/apps/basic/config/codeception/unit.php deleted file mode 100644 index fd66970..0000000 --- a/apps/basic/config/codeception/unit.php +++ /dev/null @@ -1,15 +0,0 @@ - [ - 'fixture' => [ - 'class' => 'yii\test\DbFixtureManager', - 'basePath' => '@tests/unit/fixtures', - ], - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2basic_unit', - ], - ], -]; diff --git a/apps/basic/mails/layouts/html.php b/apps/basic/mails/layouts/html.php new file mode 100644 index 0000000..2e6b615 --- /dev/null +++ b/apps/basic/mails/layouts/html.php @@ -0,0 +1,24 @@ + +beginPage() ?> + + + + + <?= Html::encode($this->title) ?> + head() ?> + + + beginBody() ?> + + endBody() ?> + + +endPage() ?> \ No newline at end of file diff --git a/apps/basic/tests/README.md b/apps/basic/tests/README.md index 5ff2669..83b0511 100644 --- a/apps/basic/tests/README.md +++ b/apps/basic/tests/README.md @@ -1,35 +1,28 @@ This folder contains various tests for the basic application. 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 - application base directory (not in this `tests` directory!). -2. Adjust the test configuration files based on your environment: - - 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. +1. In the file `_bootstrap.php`, modify the definition of the constant `TEST_ENTRY_URL` so + that it points to the correct entry script URL. +2. Go to the application base directory and build the test suites: -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 - ``` -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 + vendor/bin/codecept build ``` +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 -more details about writing acceptance, functional and unit tests. +more details about writing and running acceptance, functional and unit tests. diff --git a/apps/basic/tests/_bootstrap.php b/apps/basic/tests/_bootstrap.php index ec9390e..5a40d64 100644 --- a/apps/basic/tests/_bootstrap.php +++ b/apps/basic/tests/_bootstrap.php @@ -1,9 +1,22 @@ guy->fillField($this->name, $contactData['name']); - $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']); + $data = []; + foreach ($contactData as $name => $value) { + $data["ContactForm[$name]"] = $value; } - $this->guy->click($this->button); + $this->guy->submitForm('#contact-form', $data); } } diff --git a/apps/basic/tests/_pages/LoginPage.php b/apps/basic/tests/_pages/LoginPage.php index 8493d51..aae5e0f 100644 --- a/apps/basic/tests/_pages/LoginPage.php +++ b/apps/basic/tests/_pages/LoginPage.php @@ -9,30 +9,14 @@ class LoginPage extends BasePage 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 $password */ public function login($username, $password) { - $this->guy->fillField($this->username, $username); - $this->guy->fillField($this->password, $password); - $this->guy->click($this->button); + $this->guy->submitForm('#login-form', [ + 'LoginForm[username]' => $username, + 'LoginForm[password]' => $password, + ]); } } diff --git a/apps/basic/tests/acceptance.suite.yml b/apps/basic/tests/acceptance.suite.yml index 2f37e16..f4d36f5 100644 --- a/apps/basic/tests/acceptance.suite.yml +++ b/apps/basic/tests/acceptance.suite.yml @@ -18,7 +18,7 @@ modules: # - WebDriver config: PhpBrowser: - url: 'http://localhost/basic-app/web/index-test-acceptance.php' + url: 'http://localhost' # WebDriver: -# url: 'http://localhost/basic-app/web/index-test-acceptance.php' +# url: 'http://localhost' # browser: firefox diff --git a/apps/basic/tests/acceptance/HomeCept.php b/apps/basic/tests/acceptance/HomeCept.php index eb28c39..62456f9 100644 --- a/apps/basic/tests/acceptance/HomeCept.php +++ b/apps/basic/tests/acceptance/HomeCept.php @@ -2,7 +2,7 @@ $I = new WebGuy($scenario); $I->wantTo('ensure that home page works'); -$I->amOnPage(''); +$I->amOnPage(Yii::$app->homeUrl); $I->see('My Company'); $I->seeLink('About'); $I->click('About'); diff --git a/apps/basic/tests/acceptance/_bootstrap.php b/apps/basic/tests/acceptance/_bootstrap.php index 32bac62..6ce3d17 100644 --- a/apps/basic/tests/acceptance/_bootstrap.php +++ b/apps/basic/tests/acceptance/_bootstrap.php @@ -1,8 +1,3 @@ [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_acceptance', + ], + 'urlManager' => [ + 'showScriptName' => true, + ], + ], +]); diff --git a/apps/basic/tests/functional.suite.yml b/apps/basic/tests/functional.suite.yml index b7d86de..0d400af 100644 --- a/apps/basic/tests/functional.suite.yml +++ b/apps/basic/tests/functional.suite.yml @@ -14,5 +14,4 @@ modules: - Yii2 config: Yii2: - entryScript: 'web/index-test-functional.php' - url: 'http://localhost/' + configFile: 'tests/functional/_config.php' diff --git a/apps/basic/tests/functional/ContactCept.php b/apps/basic/tests/functional/ContactCept.php index ddc7ca6..14e6197 100644 --- a/apps/basic/tests/functional/ContactCept.php +++ b/apps/basic/tests/functional/ContactCept.php @@ -1,6 +1,6 @@ wantTo('ensure that contact works'); diff --git a/apps/basic/tests/functional/HomeCept.php b/apps/basic/tests/functional/HomeCept.php index 0658ba2..3258ba3 100644 --- a/apps/basic/tests/functional/HomeCept.php +++ b/apps/basic/tests/functional/HomeCept.php @@ -2,7 +2,7 @@ $I = new TestGuy($scenario); $I->wantTo('ensure that home page works'); -$I->amOnPage(''); +$I->amOnPage(Yii::$app->homeUrl); $I->see('My Company'); $I->seeLink('About'); $I->click('About'); diff --git a/apps/basic/tests/functional/LoginCept.php b/apps/basic/tests/functional/LoginCept.php index 770b823..e5285cd 100644 --- a/apps/basic/tests/functional/LoginCept.php +++ b/apps/basic/tests/functional/LoginCept.php @@ -1,6 +1,6 @@ wantTo('ensure that login works'); diff --git a/apps/basic/tests/functional/_bootstrap.php b/apps/basic/tests/functional/_bootstrap.php index 6692104..6ce3d17 100644 --- a/apps/basic/tests/functional/_bootstrap.php +++ b/apps/basic/tests/functional/_bootstrap.php @@ -1,4 +1,3 @@ [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_functional', + ], + 'urlManager' => [ + 'showScriptName' => true, + ], + ], +]); diff --git a/apps/basic/tests/functional/_pages/ContactPage.php b/apps/basic/tests/functional/_pages/ContactPage.php deleted file mode 100644 index 0d0eba4..0000000 --- a/apps/basic/tests/functional/_pages/ContactPage.php +++ /dev/null @@ -1,51 +0,0 @@ -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'], - ]); - } - } -} diff --git a/apps/basic/tests/functional/_pages/LoginPage.php b/apps/basic/tests/functional/_pages/LoginPage.php deleted file mode 100644 index 9a8d0ef..0000000 --- a/apps/basic/tests/functional/_pages/LoginPage.php +++ /dev/null @@ -1,30 +0,0 @@ -guy->submitForm('#login-form', [ - $this->username => $username, - $this->password => $password, - ]); - } -} diff --git a/apps/basic/views/layouts/main.php b/apps/basic/views/layouts/main.php index e99b3c5..d1af295 100644 --- a/apps/basic/views/layouts/main.php +++ b/apps/basic/views/layouts/main.php @@ -16,6 +16,7 @@ AppAsset::register($this); + <?= Html::encode($this->title) ?> head() ?> diff --git a/apps/basic/web/css/site.css b/apps/basic/web/css/site.css index d44558d..7a1fb92 100644 --- a/apps/basic/web/css/site.css +++ b/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.desc:after { content: "\e156"; } + +.error-summary { + color: #b94a48; + background: #fdf7f7; + border-left: 3px solid #eed3d7; + padding: 10px 20px; + margin: 0 0 15px 0; +} diff --git a/apps/basic/web/index-test-functional.php b/apps/basic/web/index-test-functional.php deleted file mode 100644 index 65fd3fa..0000000 --- a/apps/basic/web/index-test-functional.php +++ /dev/null @@ -1,11 +0,0 @@ -run(); diff --git a/docs/guide/controller.md b/docs/guide/controller.md index de6cec5..193fe37 100644 --- a/docs/guide/controller.md +++ b/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`. -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 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 ------ @@ -208,6 +247,29 @@ Catching all incoming requests 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 -------- diff --git a/docs/guide/query-builder.md b/docs/guide/query-builder.md index f775c76..2bc215d 100644 --- a/docs/guide/query-builder.md +++ b/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. - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing the values that the column or DB expression should be like. - For example, `['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 - 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%'`. - 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` predicates when operand 2 is an array. - `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]); if (!empty($search)) { - $query->addWhere('like', 'title', $search); + $query->addWhere(['like', 'title', $search]); } ``` diff --git a/docs/internals/git-workflow.md b/docs/internals/git-workflow.md index fc8b275..1f98ebb 100644 --- a/docs/internals/git-workflow.md +++ b/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 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). ### 1. [Fork](http://help.github.com/fork-a-repo/) the Yii repository on github and clone your fork to your development diff --git a/extensions/yii/authclient/README.md b/extensions/yii/authclient/README.md index ea7f217..5a7b396 100644 --- a/extensions/yii/authclient/README.md +++ b/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: ``` - ['site/auth'] ]) ?> ``` diff --git a/extensions/yii/debug/LogTarget.php b/extensions/yii/debug/LogTarget.php index 4b75cfa..1359b36 100644 --- a/extensions/yii/debug/LogTarget.php +++ b/extensions/yii/debug/LogTarget.php @@ -52,6 +52,7 @@ class LogTarget extends Target $manifest = unserialize(file_get_contents($indexFile)); } $request = Yii::$app->getRequest(); + $response = Yii::$app->getResponse(); $manifest[$this->tag] = $summary = [ 'tag' => $this->tag, 'url' => $request->getAbsoluteUrl(), @@ -59,6 +60,8 @@ class LogTarget extends Target 'method' => $request->getMethod(), 'ip' => $request->getUserIP(), 'time' => time(), + 'statusCode' => $response->statusCode, + 'sqlCount' => $this->getSqlTotalCount(), ]; $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; + } + } diff --git a/extensions/yii/debug/components/search/Filter.php b/extensions/yii/debug/components/search/Filter.php new file mode 100644 index 0000000..557e12c --- /dev/null +++ b/extensions/yii/debug/components/search/Filter.php @@ -0,0 +1,72 @@ + [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; + } + +} diff --git a/extensions/yii/debug/components/search/matches/Base.php b/extensions/yii/debug/components/search/matches/Base.php new file mode 100644 index 0000000..6d8250d --- /dev/null +++ b/extensions/yii/debug/components/search/matches/Base.php @@ -0,0 +1,26 @@ + + * @since 2.0 + */ +abstract class Base extends Component implements MatcherInterface +{ + + /** + * @var mixed current value to check for the matcher + */ + public $value; + +} diff --git a/extensions/yii/debug/components/search/matches/Exact.php b/extensions/yii/debug/components/search/matches/Exact.php new file mode 100644 index 0000000..34bb0cd --- /dev/null +++ b/extensions/yii/debug/components/search/matches/Exact.php @@ -0,0 +1,36 @@ + + * @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); + } + } + +} diff --git a/extensions/yii/debug/components/search/matches/Greater.php b/extensions/yii/debug/components/search/matches/Greater.php new file mode 100644 index 0000000..e475dc7 --- /dev/null +++ b/extensions/yii/debug/components/search/matches/Greater.php @@ -0,0 +1,27 @@ + + * @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); + } + +} diff --git a/extensions/yii/debug/components/search/matches/Lower.php b/extensions/yii/debug/components/search/matches/Lower.php new file mode 100644 index 0000000..9222fd7 --- /dev/null +++ b/extensions/yii/debug/components/search/matches/Lower.php @@ -0,0 +1,27 @@ + + * @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); + } + +} diff --git a/extensions/yii/debug/components/search/matches/MatcherInterface.php b/extensions/yii/debug/components/search/matches/MatcherInterface.php new file mode 100644 index 0000000..16c0705 --- /dev/null +++ b/extensions/yii/debug/components/search/matches/MatcherInterface.php @@ -0,0 +1,25 @@ + + * @since 2.0 + */ +interface MatcherInterface +{ + + /** + * Check if the value is correct according current matcher. + * @param mixed $value + */ + public function check($value); + +} diff --git a/extensions/yii/debug/controllers/DefaultController.php b/extensions/yii/debug/controllers/DefaultController.php index 4d525f7..6109a39 100644 --- a/extensions/yii/debug/controllers/DefaultController.php +++ b/extensions/yii/debug/controllers/DefaultController.php @@ -10,6 +10,7 @@ namespace yii\debug\controllers; use Yii; use yii\web\Controller; use yii\web\NotFoundHttpException; +use yii\debug\models\search\Debug; /** * @author Qiang Xue @@ -38,7 +39,13 @@ class DefaultController extends Controller 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) diff --git a/extensions/yii/debug/models/search/Debug.php b/extensions/yii/debug/models/search/Debug.php new file mode 100644 index 0000000..f89990f --- /dev/null +++ b/extensions/yii/debug/models/search/Debug.php @@ -0,0 +1,149 @@ + '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])); + } + + } + +} diff --git a/extensions/yii/debug/panels/DbPanel.php b/extensions/yii/debug/panels/DbPanel.php index a487dd4..bc79072 100644 --- a/extensions/yii/debug/panels/DbPanel.php +++ b/extensions/yii/debug/panels/DbPanel.php @@ -117,7 +117,7 @@ EOD; public function save() { $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]; } } diff --git a/extensions/yii/debug/views/default/index.php b/extensions/yii/debug/views/default/index.php index 93737e2..73f5237 100644 --- a/extensions/yii/debug/views/default/index.php +++ b/extensions/yii/debug/views/default/index.php @@ -1,10 +1,14 @@ title = 'Yii Debugger'; @@ -19,28 +23,63 @@ $this->title = 'Yii Debugger';

Available Debug Data

- - - - - - - - - - - - $data): ?> - - - - - - - - - -
TagTimeIPMethodURL
$tag]) ?>
+ + 'yii\i18n\Formatter']) : Yii::$app->formatter; + +echo GridView::widget([ + 'dataProvider' => $dataProvider, + 'filterModel' => $searchModel, + 'rowOptions' => function ($model, $key, $index, $grid) use ($searchModel) { + if ($searchModel->isCodeCritical($model['statusCode'])) { + return ['class'=>'danger']; + } else { + return []; + } + }, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + [ + 'attribute' => 'tag', + 'value' => function ($data) + { + 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' + ], + ], +]); ?>
diff --git a/extensions/yii/debug/views/layouts/main.php b/extensions/yii/debug/views/layouts/main.php index 97ef08c..0f9a02c 100644 --- a/extensions/yii/debug/views/layouts/main.php +++ b/extensions/yii/debug/views/layouts/main.php @@ -11,6 +11,8 @@ yii\debug\DebugAsset::register($this); beginPage() ?> + + <?= Html::encode($this->title) ?> head() ?> diff --git a/extensions/yii/gii/generators/crud/templates/search.php b/extensions/yii/gii/generators/crud/templates/search.php index 1411896..bc55c60 100644 --- a/extensions/yii/gii/generators/crud/templates/search.php +++ b/extensions/yii/gii/generators/crud/templates/search.php @@ -74,7 +74,6 @@ class extends Model return; } if ($partialMatch) { - $value = '%' . strtr($value, ['%'=>'\%', '_'=>'\_', '\\'=>'\\\\']) . '%'; $query->andWhere(['like', $attribute, $value]); } else { $query->andWhere([$attribute => $value]); diff --git a/extensions/yii/gii/views/layouts/main.php b/extensions/yii/gii/views/layouts/main.php index 6950668..f324e41 100644 --- a/extensions/yii/gii/views/layouts/main.php +++ b/extensions/yii/gii/views/layouts/main.php @@ -14,6 +14,7 @@ $asset = yii\gii\GiiAsset::register($this); + <?= Html::encode($this->title) ?> head() ?> diff --git a/extensions/yii/sphinx/Query.php b/extensions/yii/sphinx/Query.php index dd13363..2a51996 100644 --- a/extensions/yii/sphinx/Query.php +++ b/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 * `name LIKE '%test%' AND name LIKE '%sample%'`. * 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` * 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) ->queryColumn(); } -} \ No newline at end of file +} diff --git a/extensions/yii/sphinx/QueryBuilder.php b/extensions/yii/sphinx/QueryBuilder.php index 586b3a5..6bd6d0c 100644 --- a/extensions/yii/sphinx/QueryBuilder.php +++ b/extensions/yii/sphinx/QueryBuilder.php @@ -754,11 +754,19 @@ class QueryBuilder extends Object * Creates an SQL expressions with the `LIKE` operator. * @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 array $operands the first operand is the column name. - * The second operand is a single value or an array of values that column value - * 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`. + * @param array $operands an array of two or three operands + * + * - The first operand is the column name. + * - The second operand is a single value or an array of values that column value + * 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 * @return string the generated SQL expression * @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."); } + $escape = isset($operands[2]) ? $operands[2] : ['%'=>'\%', '_'=>'\_', '\\'=>'\\\\']; + unset($operands[2]); + list($column, $values) = $operands; $values = (array)$values; @@ -791,7 +802,7 @@ class QueryBuilder extends Object $parts = []; foreach ($values as $value) { $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = $value; + $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%'); $parts[] = "$column $operator $phName"; } @@ -902,4 +913,4 @@ class QueryBuilder extends Object return $phName; } } -} \ No newline at end of file +} diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index b7699ac..9126e71 100644 --- a/framework/CHANGELOG.md +++ b/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 #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 #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 incorrect event name for `yii\jui\Spinner` (samdark) - 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 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 #1293: Replaced Console::showProgress() with a better approach. See Console::startProgress() for details (cebe) - 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 #1646: Added postgresql `QueryBuilder::checkIntegrity` and `QueryBuilder::resetSequence` (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 `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) - Enh: Support for file aliases in console command 'message' (omnilight) - 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 #1643: Added default value for `Captcha::options` (qiangxue) - Chg: Renamed `yii\jui\Widget::clientEventsMap` to `clientEventMap` (qiangxue) diff --git a/framework/yii/assets/yii.activeForm.js b/framework/yii/assets/yii.activeForm.js index e898efc..d4fc877 100644 --- a/framework/yii/assets/yii.activeForm.js +++ b/framework/yii/assets/yii.activeForm.js @@ -365,7 +365,7 @@ var updateSummary = function ($form, messages) { var data = $form.data('yiiActiveForm'), $summary = $form.find(data.settings.errorSummary), - $ul = $summary.find('ul'); + $ul = $summary.find('ul').html(''); if ($summary.length && messages) { $.each(data.attributes, function () { diff --git a/framework/yii/db/Command.php b/framework/yii/db/Command.php index 76b4269..f7eb227 100644 --- a/framework/yii/db/Command.php +++ b/framework/yii/db/Command.php @@ -368,7 +368,7 @@ class Command extends \yii\base\Component $db = $this->db; $rawSql = $this->getRawSql(); - Yii::info($rawSql, __METHOD__); + Yii::info($rawSql, 'yii\db\Command::query'); /** @var \yii\caching\Cache $cache */ if ($db->enableQueryCache && $method !== '') { @@ -378,19 +378,20 @@ class Command extends \yii\base\Component if (isset($cache) && $cache instanceof Cache) { $cacheKey = [ __CLASS__, + $method, $db->dsn, $db->username, $rawSql, ]; 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; } } $token = $rawSql; try { - Yii::beginProfile($token, __METHOD__); + Yii::beginProfile($token, 'yii\db\Command::query'); $this->prepare(); $this->pdoStatement->execute(); @@ -405,16 +406,16 @@ class Command extends \yii\base\Component $this->pdoStatement->closeCursor(); } - Yii::endProfile($token, __METHOD__); + Yii::endProfile($token, 'yii\db\Command::query'); if (isset($cache, $cacheKey) && $cache instanceof Cache) { $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; } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); + Yii::endProfile($token, 'yii\db\Command::query'); if ($e instanceof Exception) { throw $e; } else { diff --git a/framework/yii/db/Query.php b/framework/yii/db/Query.php index ee24c2f..9da4f14 100644 --- a/framework/yii/db/Query.php +++ b/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 * 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 - * 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%'`. - * 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` * predicates when operand 2 is an array. diff --git a/framework/yii/db/QueryBuilder.php b/framework/yii/db/QueryBuilder.php index a76f211..f819362 100644 --- a/framework/yii/db/QueryBuilder.php +++ b/framework/yii/db/QueryBuilder.php @@ -1017,11 +1017,19 @@ class QueryBuilder extends \yii\base\Object /** * 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 array $operands the first operand is the column name. - * The second operand is a single value or an array of values that column value - * 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`. + * @param array $operands an array of two or three operands + * + * - The first operand is the column name. + * - The second operand is a single value or an array of values that column value + * 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 * @return string the generated SQL expression * @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."); } + $escape = isset($operands[2]) ? $operands[2] : ['%'=>'\%', '_'=>'\_', '\\'=>'\\\\']; + unset($operands[2]); + list($column, $values) = $operands; $values = (array)$values; @@ -1054,7 +1065,7 @@ class QueryBuilder extends \yii\base\Object $parts = []; foreach ($values as $value) { $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = $value; + $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%'); $parts[] = "$column $operator $phName"; } diff --git a/framework/yii/db/QueryInterface.php b/framework/yii/db/QueryInterface.php index f3cc312..4cf802d 100644 --- a/framework/yii/db/QueryInterface.php +++ b/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 * 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 - * 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%'`. - * 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` * predicates when operand 2 is an array. @@ -203,4 +205,4 @@ interface QueryInterface * @return static the query object itself */ public function offset($offset); -} \ No newline at end of file +} diff --git a/framework/yii/db/mysql/Schema.php b/framework/yii/db/mysql/Schema.php index d5258c1..a649d8a 100644 --- a/framework/yii/db/mysql/Schema.php +++ b/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. * @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'; if ($schema !== '') { diff --git a/framework/yii/db/pgsql/QueryBuilder.php b/framework/yii/db/pgsql/QueryBuilder.php index f1ac032..998e746 100644 --- a/framework/yii/db/pgsql/QueryBuilder.php +++ b/framework/yii/db/pgsql/QueryBuilder.php @@ -109,7 +109,7 @@ class QueryBuilder extends \yii\db\QueryBuilder { $enable = $check ? 'ENABLE' : 'DISABLE'; $schema = $schema ? $schema : $this->db->schema->defaultSchema; - $tableNames = $table ? [$table] : $this->db->schema->findTableNames($schema); + $tableNames = $table ? [$table] : $this->db->schema->getTableNames($schema); $command = ''; foreach ($tableNames as $tableName) { diff --git a/framework/yii/db/pgsql/Schema.php b/framework/yii/db/pgsql/Schema.php index 59340c9..96889ab 100644 --- a/framework/yii/db/pgsql/Schema.php +++ b/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. * @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 === '') { $schema = $this->defaultSchema; diff --git a/framework/yii/db/sqlite/Schema.php b/framework/yii/db/sqlite/Schema.php index 6548999..9f410b4 100644 --- a/framework/yii/db/sqlite/Schema.php +++ b/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. * @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'"; return $this->db->createCommand($sql)->queryColumn(); diff --git a/framework/yii/helpers/BaseInflector.php b/framework/yii/helpers/BaseInflector.php index 2f4f01b..cb59986 100644 --- a/framework/yii/helpers/BaseInflector.php +++ b/framework/yii/helpers/BaseInflector.php @@ -215,60 +215,67 @@ class BaseInflector 'wildebeest' => 'wildebeest', 'Yengeese' => 'Yengeese', ]; + /** * @var array map of special chars and its translation. This is used by [[slug()]]. */ public static $transliteration = [ - '/ä|æ|ǽ/' => 'ae', - '/ö|œ/' => 'oe', - '/ü/' => 'ue', - '/Ä/' => 'Ae', - '/Ü/' => 'Ue', - '/Ö/' => 'Oe', - '/À|Á|Â|Ã|Å|Ǻ|Ā|Ă|Ą|Ǎ/' => 'A', - '/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª/' => 'a', - '/Ç|Ć|Ĉ|Ċ|Č/' => 'C', - '/ç|ć|ĉ|ċ|č/' => 'c', - '/Ð|Ď|Đ/' => 'D', - '/ð|ď|đ/' => 'd', - '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě/' => 'E', - '/è|é|ê|ë|ē|ĕ|ė|ę|ě/' => 'e', - '/Ĝ|Ğ|Ġ|Ģ/' => 'G', - '/ĝ|ğ|ġ|ģ/' => 'g', - '/Ĥ|Ħ/' => 'H', - '/ĥ|ħ/' => 'h', - '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ/' => 'I', - '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı/' => 'i', - '/Ĵ/' => 'J', - '/ĵ/' => 'j', - '/Ķ/' => 'K', - '/ķ/' => 'k', - '/Ĺ|Ļ|Ľ|Ŀ|Ł/' => 'L', - '/ĺ|ļ|ľ|ŀ|ł/' => 'l', - '/Ñ|Ń|Ņ|Ň/' => 'N', - '/ñ|ń|ņ|ň|ʼn/' => 'n', - '/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ/' => 'O', - '/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º/' => 'o', - '/Ŕ|Ŗ|Ř/' => 'R', - '/ŕ|ŗ|ř/' => 'r', - '/Ś|Ŝ|Ş|Ș|Š/' => 'S', - '/ś|ŝ|ş|ș|š|ſ/' => 's', - '/Ţ|Ț|Ť|Ŧ/' => 'T', - '/ţ|ț|ť|ŧ/' => 't', - '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ/' => 'U', - '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ/' => 'u', - '/Ý|Ÿ|Ŷ/' => 'Y', - '/ý|ÿ|ŷ/' => 'y', - '/Ŵ/' => 'W', - '/ŵ/' => 'w', - '/Ź|Ż|Ž/' => 'Z', - '/ź|ż|ž/' => 'z', - '/Æ|Ǽ/' => 'AE', - '/ß/' => 'ss', - '/IJ/' => 'IJ', - '/ij/' => 'ij', - '/Œ/' => 'OE', - '/ƒ/' => 'f' + // Latin + 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'Æ' => 'AE', 'Ç' => 'C', + 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', + 'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ő' => 'O', + 'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ű' => 'U', 'Ý' => 'Y', 'Þ' => 'TH', + 'ß' => 'ss', + 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'ae', 'ç' => 'c', + 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', + 'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ő' => 'o', + 'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ű' => 'u', 'ý' => 'y', 'þ' => 'th', + 'ÿ' => 'y', + // Latin symbols + '©' => '(c)', + // Greek + 'Α' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E', 'Ζ' => 'Z', 'Η' => 'H', 'Θ' => '8', + 'Ι' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => '3', 'Ο' => 'O', 'Π' => 'P', + 'Ρ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'PS', 'Ω' => 'W', + 'Ά' => 'A', 'Έ' => 'E', 'Ί' => 'I', 'Ό' => 'O', 'Ύ' => 'Y', 'Ή' => 'H', 'Ώ' => 'W', 'Ϊ' => 'I', + 'Ϋ' => 'Y', + 'α' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 'ζ' => 'z', 'η' => 'h', 'θ' => '8', + 'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => '3', 'ο' => 'o', 'π' => 'p', + 'ρ' => 'r', 'σ' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'ps', 'ω' => 'w', + 'ά' => 'a', 'έ' => 'e', 'ί' => 'i', 'ό' => 'o', 'ύ' => 'y', 'ή' => 'h', 'ώ' => 'w', 'ς' => 's', + 'ϊ' => 'i', 'ΰ' => 'y', 'ϋ' => 'y', 'ΐ' => 'i', + // Turkish + 'Ş' => 'S', 'İ' => 'I', 'Ç' => 'C', 'Ü' => 'U', 'Ö' => 'O', 'Ğ' => 'G', + 'ş' => 's', 'ı' => 'i', 'ç' => 'c', 'ü' => 'u', 'ö' => 'o', 'ğ' => 'g', + // Russian + 'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'Yo', 'Ж' => 'Zh', + 'З' => 'Z', 'И' => 'I', 'Й' => 'J', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O', + 'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C', + 'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sh', 'Ъ' => '', 'Ы' => 'Y', 'Ь' => '', 'Э' => 'E', 'Ю' => 'Yu', + 'Я' => 'Ya', + 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh', + 'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o', + 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c', + 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sh', 'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu', + 'я' => 'ya', + // Ukrainian + 'Є' => 'Ye', 'І' => 'I', 'Ї' => 'Yi', 'Ґ' => 'G', + 'є' => 'ye', 'і' => 'i', 'ї' => 'yi', 'ґ' => 'g', + // Czech + 'Č' => 'C', 'Ď' => 'D', 'Ě' => 'E', 'Ň' => 'N', 'Ř' => 'R', 'Š' => 'S', 'Ť' => 'T', 'Ů' => 'U', + 'Ž' => 'Z', + 'č' => 'c', 'ď' => 'd', 'ě' => 'e', 'ň' => 'n', 'ř' => 'r', 'š' => 's', 'ť' => 't', 'ů' => 'u', + 'ž' => 'z', + // Polish + 'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'e', 'Ł' => 'L', 'Ń' => 'N', 'Ó' => 'o', 'Ś' => 'S', 'Ź' => 'Z', + 'Ż' => 'Z', + 'ą' => '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 * non word characters removed. Maps special characters to ASCII using - * `Inflector::$transliteration` + * [[$transliteration]] array. * @param string $string An arbitrary string to convert * @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. */ - public static function slug($string, $replacement = '-') + public static function slug($string, $replacement = '-', $lowercase = true) { - $map = static::$transliteration + [ - '/[^\w\s]/' => ' ', - '/\\s+/' => $replacement, - '/(?<=[a-z])([A-Z])/' => $replacement . '\\1', - str_replace(':rep', preg_quote($replacement, '/'), '/^[:rep]+|[:rep]+$/') => '' - ]; - return preg_replace(array_keys($map), array_values($map), $string); + if (extension_loaded('intl') === true) { + $options = 'Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove;'; + $string = transliterator_transliterate($options, $string); + $string = preg_replace('/[-\s]+/', $replacement, $string); + } else { + $string = str_replace(array_keys(static::$transliteration), static::$transliteration, $string); + $string = preg_replace('/[^\p{L}\p{Nd}]+/u', $replacement, $string); + } + $string = trim($string, $replacement); + return $lowercase ? strtolower($string) : $string; } /** diff --git a/framework/yii/rbac/Manager.php b/framework/yii/rbac/Manager.php index a1bf47a..2f392c5 100644 --- a/framework/yii/rbac/Manager.php +++ b/framework/yii/rbac/Manager.php @@ -21,7 +21,7 @@ use yii\base\InvalidParamException; * Access Control (RBAC). * * 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 * 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. diff --git a/framework/yii/web/Request.php b/framework/yii/web/Request.php index ee232f4..717f403 100644 --- a/framework/yii/web/Request.php +++ b/framework/yii/web/Request.php @@ -577,7 +577,7 @@ class Request extends \yii\base\Request $pathInfo = substr($pathInfo, strlen($scriptUrl)); } elseif ($baseUrl === '' || strpos($pathInfo, $baseUrl) === 0) { $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)); } else { 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) { - $token = str_replace('.', '+', base64_decode($token)); + $token = base64_decode(str_replace('.', '+', $token)); $n = StringHelper::byteLength($token); if ($n <= self::CSRF_MASK_LENGTH) { return false; diff --git a/framework/yii/web/Response.php b/framework/yii/web/Response.php index 43e8f73..79d94dd 100644 --- a/framework/yii/web/Response.php +++ b/framework/yii/web/Response.php @@ -799,7 +799,7 @@ class Response extends \yii\base\Response $this->content = $this->data; break; 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); break; case self::FORMAT_JSONP: diff --git a/framework/yii/widgets/ActiveField.php b/framework/yii/widgets/ActiveField.php index bd26237..e188034 100644 --- a/framework/yii/widgets/ActiveField.php +++ b/framework/yii/widgets/ActiveField.php @@ -280,6 +280,7 @@ class ActiveField extends Component public function input($type, $options = []) { $options = array_merge($this->inputOptions, $options); + $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeInput($type, $this->model, $this->attribute, $options); return $this; } @@ -295,6 +296,7 @@ class ActiveField extends Component public function textInput($options = []) { $options = array_merge($this->inputOptions, $options); + $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeTextInput($this->model, $this->attribute, $options); return $this; } @@ -310,6 +312,7 @@ class ActiveField extends Component public function passwordInput($options = []) { $options = array_merge($this->inputOptions, $options); + $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activePasswordInput($this->model, $this->attribute, $options); return $this; } @@ -328,6 +331,7 @@ class ActiveField extends Component if ($this->inputOptions !== ['class' => 'form-control']) { $options = array_merge($this->inputOptions, $options); } + $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeFileInput($this->model, $this->attribute, $options); return $this; } @@ -342,6 +346,7 @@ class ActiveField extends Component public function textarea($options = []) { $options = array_merge($this->inputOptions, $options); + $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeTextarea($this->model, $this->attribute, $options); return $this; } @@ -379,6 +384,7 @@ class ActiveField extends Component } else { $this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options); } + $this->adjustLabelFor($options); return $this; } @@ -415,6 +421,7 @@ class ActiveField extends Component } else { $this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options); } + $this->adjustLabelFor($options); return $this; } @@ -453,6 +460,7 @@ class ActiveField extends Component public function dropDownList($items, $options = []) { $options = array_merge($this->inputOptions, $options); + $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeDropDownList($this->model, $this->attribute, $items, $options); return $this; } @@ -495,6 +503,7 @@ class ActiveField extends Component public function listBox($items, $options = []) { $options = array_merge($this->inputOptions, $options); + $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeListBox($this->model, $this->attribute, $items, $options); return $this; } @@ -526,6 +535,7 @@ class ActiveField extends Component */ public function checkboxList($items, $options = []) { + $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeCheckboxList($this->model, $this->attribute, $items, $options); return $this; } @@ -556,6 +566,7 @@ class ActiveField extends Component */ public function radioList($items, $options = []) { + $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeRadioList($this->model, $this->attribute, $items, $options); 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. * @return array the JS options */ diff --git a/tests/unit/framework/db/SchemaTest.php b/tests/unit/framework/db/SchemaTest.php index 3727737..05a0ff2 100644 --- a/tests/unit/framework/db/SchemaTest.php +++ b/tests/unit/framework/db/SchemaTest.php @@ -11,7 +11,7 @@ use yii\db\Schema; */ class SchemaTest extends DatabaseTestCase { - public function testFindTableNames() + public function testGetTableNames() { /** @var Schema $schema */ $schema = $this->getConnection()->schema; diff --git a/tests/unit/framework/helpers/InflectorTest.php b/tests/unit/framework/helpers/InflectorTest.php index 2cd3c9f..e303ebd 100644 --- a/tests/unit/framework/helpers/InflectorTest.php +++ b/tests/unit/framework/helpers/InflectorTest.php @@ -123,6 +123,8 @@ class InflectorTest extends TestCase 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')); }