From 7669d60df65264c800f95593d66c261fab7254d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Gannaz?= Date: Thu, 13 Oct 2016 18:32:26 +0200 Subject: [PATCH 001/105] Document and test ListView.showOnEmpty This addresses #11627 by completing the documentation without changing the behavior. Some basic unit tests are added on ListView to ensure this won't change unintentionnally. That means a ListView widget with an empty provider and `showOnEmpty=true` will still display an empty element (by default, an empty "div.list-view"). --- framework/widgets/BaseListView.php | 4 +- tests/framework/widgets/ListViewTest.php | 56 ++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/framework/widgets/ListViewTest.php diff --git a/framework/widgets/BaseListView.php b/framework/widgets/BaseListView.php index 7b2b541c87..75a4cadf8a 100644 --- a/framework/widgets/BaseListView.php +++ b/framework/widgets/BaseListView.php @@ -69,7 +69,9 @@ abstract class BaseListView extends Widget */ public $summaryOptions = ['class' => 'summary']; /** - * @var boolean whether to show the list view if [[dataProvider]] returns no data. + * @var boolean whether to show an empty list view if [[dataProvider]] returns no data. + * The default value is false which displays an element according to the `emptyText` + * and `emptyTextOptions` properties. */ public $showOnEmpty = false; /** diff --git a/tests/framework/widgets/ListViewTest.php b/tests/framework/widgets/ListViewTest.php new file mode 100644 index 0000000000..15db395604 --- /dev/null +++ b/tests/framework/widgets/ListViewTest.php @@ -0,0 +1,56 @@ +mockApplication(); + } + + public function testEmptyListShown() + { + $dataProvider = new ArrayDataProvider([ + 'allModels' => [], + ]); + + ob_start(); + echo ListView::widget([ + 'dataProvider' => $dataProvider, + 'showOnEmpty' => false, + 'emptyText' => "Nothing at all", + ]); + $actualHtml = ob_get_clean(); + + $this->assertTrue(strpos($actualHtml, "Nothing at all") !== false, "displays the empty message"); + $this->assertTrue(strpos($actualHtml, '
') !== false, "adds the 'empty' class"); + $this->assertTrue(strpos($actualHtml, '
') === false, "does not display the summary"); + } + + public function testEmptyListNotShown() + { + $dataProvider = new ArrayDataProvider([ + 'allModels' => [], + ]); + + ob_start(); + echo ListView::widget([ + 'dataProvider' => $dataProvider, + 'showOnEmpty' => true, + 'emptyText' => "Nothing at all", + ]); + $actualHtml = ob_get_clean(); + + $this->assertTrue(strpos($actualHtml, '
') === false, "does not add the 'empty' class"); + $this->assertTrue(strpos($actualHtml, '
') === false, "does not display the summary"); + $this->assertEmpty(trim(\strip_tags($actualHtml)), "contains no text"); + } +} From 8ea1f510afef71706a31cd717680d8422560e1f3 Mon Sep 17 00:00:00 2001 From: Alexey Rogachev Date: Thu, 1 Dec 2016 11:25:18 +0100 Subject: [PATCH 002/105] Implemented Javascript test suite fixes #12840 Used @nkovacs draft as starting point(https://github.com/yiisoft/yii2/issues/11037#issuecomment-193356450). I also fixed some bugs in `yii.validation.js` and did some refactoring. Implemented tests: - yii.validation.js Running tests: - Install Node.js. I used the latest version available at the moment - `v7.1.0`. Note that used `jsdom` dependent library version requires `Node.js >= 4`. - Run `npm install` to install dependencies. - Execute `npm test` for running tests. --- .gitignore | 3 + .travis.yml | 7 + framework/assets/yii.validation.js | 156 ++- package.json | 36 + tests/js/tests/yii.validation.test.js | 1656 +++++++++++++++++++++++++ 5 files changed, 1778 insertions(+), 80 deletions(-) create mode 100644 package.json create mode 100644 tests/js/tests/yii.validation.test.js diff --git a/.gitignore b/.gitignore index 6b729e664c..18e2a7877f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ phpunit.phar # ignore sub directory for dev installed apps and extensions /apps /extensions + +# NPM packages +/node_modules diff --git a/.travis.yml b/.travis.yml index 087851bb6e..2114d1b618 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,6 +66,7 @@ cache: directories: - vendor - $HOME/.composer/cache + - $HOME/.npm # try running against postgres 9.3 addons: @@ -85,6 +86,8 @@ install: - tests/data/travis/apc-setup.sh - tests/data/travis/memcache-setup.sh # - tests/data/travis/cubrid-setup.sh + # NPM packages for JS testing + - travis_retry npm install before_script: # show some versions and env information @@ -92,6 +95,8 @@ before_script: - php -r "echo INTL_ICU_DATA_VERSION . \"\n\";" - mysql --version - psql --version + - node --version + - npm --version # initialize databases - mysql -e 'CREATE DATABASE `yiitest`;'; @@ -112,6 +117,8 @@ script: - cd framework && composer validate --no-check-lock && cd .. # run PHPUnit - vendor/bin/phpunit --verbose $PHPUNIT_FLAGS --exclude-group mssql,oci,wincache,xcache,zenddata,cubrid + # run Mocha for JS testing + - npm test after_script: - | diff --git a/framework/assets/yii.validation.js b/framework/assets/yii.validation.js index c49be404db..38e749fc16 100644 --- a/framework/assets/yii.validation.js +++ b/framework/assets/yii.validation.js @@ -13,7 +13,7 @@ yii.validation = (function ($) { var pub = { isEmpty: function (value) { - return value === null || value === undefined || value == [] || value === ''; + return value === null || value === undefined || ($.isArray(value) && value.length === 0) || value === ''; }, addMessage: function (messages, message, value) { @@ -36,6 +36,7 @@ yii.validation = (function ($) { } }, + // "boolean" is a reserved keyword in older versions of ES so it's quoted for IE < 9 support 'boolean': function (value, messages, options) { if (options.skipOnEmpty && pub.isEmpty(value)) { return; @@ -58,15 +59,16 @@ yii.validation = (function ($) { return; } + if (options.is !== undefined && value.length != options.is) { + pub.addMessage(messages, options.notEqual, value); + return; + } if (options.min !== undefined && value.length < options.min) { pub.addMessage(messages, options.tooShort, value); } if (options.max !== undefined && value.length > options.max) { pub.addMessage(messages, options.tooLong, value); } - if (options.is !== undefined && value.length != options.is) { - pub.addMessage(messages, options.notEqual, value); - } }, file: function (attribute, messages, options) { @@ -76,9 +78,8 @@ yii.validation = (function ($) { }); }, - image: function (attribute, messages, options, deferred) { + image: function (attribute, messages, options, deferredList) { var files = getUploadedFiles(attribute, messages, options); - $.each(files, function (i, file) { validateFile(file, messages, options); @@ -87,48 +88,33 @@ yii.validation = (function ($) { return; } - var def = $.Deferred(), - fr = new FileReader(), - img = new Image(); - - img.onload = function () { - if (options.minWidth && this.width < options.minWidth) { - messages.push(options.underWidth.replace(/\{file\}/g, file.name)); - } - - if (options.maxWidth && this.width > options.maxWidth) { - messages.push(options.overWidth.replace(/\{file\}/g, file.name)); - } - - if (options.minHeight && this.height < options.minHeight) { - messages.push(options.underHeight.replace(/\{file\}/g, file.name)); - } - - if (options.maxHeight && this.height > options.maxHeight) { - messages.push(options.overHeight.replace(/\{file\}/g, file.name)); - } - def.resolve(); - }; - - img.onerror = function () { - messages.push(options.notImage.replace(/\{file\}/g, file.name)); - def.resolve(); - }; - - fr.onload = function () { - img.src = fr.result; - }; - - // Resolve deferred if there was error while reading data - fr.onerror = function () { - def.resolve(); - }; - - fr.readAsDataURL(file); - - deferred.push(def); + var deferred = $.Deferred(); + pub.validateImage(file, messages, options, deferred, new FileReader(), new Image()); + deferredList.push(deferred); }); + }, + validateImage: function (file, messages, options, deferred, fileReader, image) { + image.onload = function() { + validateImageSize(file, image, messages, options); + deferred.resolve(); + }; + + image.onerror = function () { + messages.push(options.notImage.replace(/\{file\}/g, file.name)); + deferred.resolve(); + }; + + fileReader.onload = function () { + image.src = this.result; + }; + + // Resolve deferred if there was error while reading data + fileReader.onerror = function () { + deferred.resolve(); + }; + + fileReader.readAsDataURL(file); }, number: function (value, messages, options) { @@ -170,6 +156,10 @@ yii.validation = (function ($) { } }); + if (options.not === undefined) { + options.not = false; + } + if (options.not === inArray) { pub.addMessage(messages, options.message, value); } @@ -190,25 +180,26 @@ yii.validation = (function ($) { return; } - var valid = true; - - - var regexp = /^((?:"?([^"]*)"?\s)?)(?:\s+)?(?:(]+))(>?))$/, + var valid = true, + regexp = /^((?:"?([^"]*)"?\s)?)(?:\s+)?(?:(]+))(>?))$/, matches = regexp.exec(value); if (matches === null) { - valid = false + valid = false; } else { - if (options.enableIDN) { - matches[5] = punycode.toASCII(matches[5]); - matches[6] = punycode.toASCII(matches[6]); + var localPart = matches[5], + domain = matches[6]; - value = matches[1] + matches[3] + matches[5] + '@' + matches[6] + matches[7]; + if (options.enableIDN) { + localPart = punycode.toASCII(localPart); + domain = punycode.toASCII(domain); + + value = matches[1] + matches[3] + localPart + '@' + domain + matches[7]; } - if (matches[5].length > 64) { + if (localPart.length > 64) { valid = false; - } else if ((matches[5] + '@' + matches[6]).length > 254) { + } else if ((localPart + '@' + domain).length > 254) { valid = false; } else { valid = options.pattern.test(value) || (options.allowName && options.fullPattern.test(value)); @@ -262,11 +253,7 @@ yii.validation = (function ($) { // CAPTCHA may be updated via AJAX and the updated hash is stored in body data var hash = $('body').data(options.hashKey); - if (hash == null) { - hash = options.hash; - } else { - hash = hash[options.caseSensitive ? 0 : 1]; - } + hash = hash == null ? options.hash : hash[options.caseSensitive ? 0 : 1]; var v = options.caseSensitive ? value : value.toLowerCase(); for (var i = v.length - 1, h = 0; i >= 0; --i) { h += v.charCodeAt(i); @@ -281,7 +268,8 @@ yii.validation = (function ($) { return; } - var compareValue, valid = true; + var compareValue, + valid = true; if (options.compareAttribute === undefined) { compareValue = options.compareValue; } else { @@ -328,17 +316,13 @@ yii.validation = (function ($) { }, ip: function (value, messages, options) { - var getIpVersion = function (value) { - return value.indexOf(':') === -1 ? 4 : 6; - }; - - var negation = null, cidr = null; - if (options.skipOnEmpty && pub.isEmpty(value)) { return; } - var matches = new RegExp(options.ipParsePattern).exec(value); + var negation = null, + cidr = null, + matches = new RegExp(options.ipParsePattern).exec(value); if (matches) { negation = matches[1] || null; value = matches[2]; @@ -358,7 +342,8 @@ yii.validation = (function ($) { return; } - if (getIpVersion(value) == 6) { + var ipVersion = value.indexOf(':') === -1 ? 4 : 6; + if (ipVersion == 6) { if (!options.ipv6) { pub.addMessage(messages, options.messages.ipv6NotAllowed, value); } @@ -405,15 +390,8 @@ yii.validation = (function ($) { function validateFile(file, messages, options) { if (options.extensions && options.extensions.length > 0) { - var index, ext; - - index = file.name.lastIndexOf('.'); - - if (!~index) { - ext = ''; - } else { - ext = file.name.substr(index + 1, file.name.length).toLowerCase(); - } + var index = file.name.lastIndexOf('.'); + var ext = !~index ? '' : file.name.substr(index + 1, file.name.length).toLowerCase(); if (!~options.extensions.indexOf(ext)) { messages.push(options.wrongExtension.replace(/\{file\}/g, file.name)); @@ -445,5 +423,23 @@ yii.validation = (function ($) { return false; } + function validateImageSize(file, image, messages, options) { + if (options.minWidth && image.width < options.minWidth) { + messages.push(options.underWidth.replace(/\{file\}/g, file.name)); + } + + if (options.maxWidth && image.width > options.maxWidth) { + messages.push(options.overWidth.replace(/\{file\}/g, file.name)); + } + + if (options.minHeight && image.height < options.minHeight) { + messages.push(options.underHeight.replace(/\{file\}/g, file.name)); + } + + if (options.maxHeight && image.height > options.maxHeight) { + messages.push(options.overHeight.replace(/\{file\}/g, file.name)); + } + } + return pub; })(jQuery); diff --git a/package.json b/package.json new file mode 100644 index 0000000000..96e8e5036b --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "yii2", + "version": "2.0.10", + "description": "a modern PHP framework designed for professional Web development", + "main": "index.js", + "directories": { + "doc": "docs", + "test": "tests/js/tests" + }, + "dependencies": {}, + "devDependencies": { + "chai": "^3.5.0", + "jsdom": "^9.8.3", + "leche": "^2.1.2", + "mocha": "^3.1.2", + "mocha-jsdom": "^1.1.0", + "sinon": "^1.17.6" + }, + "scripts": { + "test": "./node_modules/mocha/bin/mocha tests/js/tests/*.test.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/yiisoft/yii2.git" + }, + "keywords": [ + "php", + "framework" + ], + "author": "", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/yiisoft/yii2/issues" + }, + "homepage": "https://github.com/yiisoft/yii2" +} diff --git a/tests/js/tests/yii.validation.test.js b/tests/js/tests/yii.validation.test.js new file mode 100644 index 0000000000..084dc58ff8 --- /dev/null +++ b/tests/js/tests/yii.validation.test.js @@ -0,0 +1,1656 @@ +var assert = require('chai').assert; + +assert.isDeferred = function (object) { + if (typeof object.resolve !== 'function') { + return false; + } + + return String(object.resolve) === String($.Deferred().resolve); +}; + +var sinon = require('sinon'); +var withData = require('leche').withData; + +var StringUtils = { + repeatString: function (value, times) { + return (new Array(times + 1)).join(value); + } +}; + +var jsdom = require('mocha-jsdom'); +var punycode = require('../../../vendor/bower/punycode/punycode'); + +var fs = require('fs'); +var vm = require('vm'); +var yii; + +describe('yii.validation', function () { + this.timeout(15000); + + var VALIDATOR_SUCCESS_MESSAGE = 'should leave messages as is'; + var VALIDATOR_ERROR_MESSAGE = 'should add appropriate errors(s) to messages'; + + function getValidatorMessage(expectedResult) { + var isTrueBoolean = typeof expectedResult === 'boolean' && expectedResult === true; + var isEmptyArray = Array.isArray(expectedResult) && expectedResult.length === 0; + + return isTrueBoolean || isEmptyArray ? VALIDATOR_SUCCESS_MESSAGE : VALIDATOR_ERROR_MESSAGE; + } + + var $; + var code; + var script; + + function FileReader() { + this.readAsDataURL = function() { + }; + } + + function Image() { + } + + function registerTestableCode(customSandbox) { + if (customSandbox === undefined) { + customSandbox = { + File: {}, + FileReader: FileReader, + Image: Image, + punycode: punycode + }; + } + + var path = 'framework/assets/yii.validation.js'; + + if (code === undefined) { + code = fs.readFileSync(path); + } + + if (script === undefined) { + script = new vm.Script(code); + } + + var defaultSandbox = {yii: {}, jQuery: $}; + var sandbox = $.extend({}, defaultSandbox, customSandbox); + var context = new vm.createContext(sandbox); + + script.runInContext(context); + yii = sandbox.yii; + } + + jsdom({src: fs.readFileSync('vendor/bower/jquery/dist/jquery.js', 'utf-8')}); + + before(function () { + $ = window.$; + registerTestableCode(); + }); + + it('should exist', function () { + assert.isObject(yii.validation); + }); + + describe('isEmpty method', function () { + withData({ + 'undefined': [undefined, true], + 'null': [null, true], + 'empty array': [[], true], + 'empty string': ['', true], + 'string containing whitespace': [' ', false], + 'empty object': [{}, false], + 'non-zero integer': [1, false], + 'non-empty string': ['a', false], + 'non-empty array': [[1], false] + }, function (value, expectedValue) { + var message = expectedValue ? 'should return "true"' : 'should return "false"'; + it(message, function () { + assert.strictEqual(yii.validation.isEmpty(value), expectedValue); + }); + }); + }); + + describe('addMessage method', function () { + withData({ + 'empty messages': [[], 'Message', 1, ['Message']], + 'non-empty messages': [['Message 1'], 'Message 2', 1, ['Message 1', 'Message 2']], + 'message as template': [[], 'Message with value {value}', 1, ['Message with value 1']] + }, function (messages, message, value, expectedMessages) { + it('should extend messages and replace value in template', function () { + yii.validation.addMessage(messages, message, value); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('required validator', function () { + withData({ + 'empty string': ['', {}, false], + 'empty string, strict mode': ['', {strict: true}, true], + 'string containing whitespace': [' ', {}, false], + 'string containing whitespace, strict mode': [' ', {strict: true}, true], + 'non-empty string': ['a', {}, true], + 'undefined': [undefined, {}, false], + 'undefined, strict mode': [undefined, {strict: true}, false], + // requiredValue + 'integer and required value set to different integer': [1, {requiredValue: 2}, false], + 'string and required value set to integer with the same value': ['1', {requiredValue: 1}, true], + 'string and required value set to integer with the same value, strict mode': [ + '1', + {requiredValue: 1, strict: true}, + false + ], + 'integer and required value set to same integer, strict mode': [ + 1, + {requiredValue: 1, strict: true}, + true + ] + }, function (value, options, expectValid) { + it(getValidatorMessage(expectValid), function () { + options.message = 'This field is required.'; + var messages = []; + var expectedMessages = expectValid ? [] : ['This field is required.']; + + yii.validation.required(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('boolean validator', function () { + var defaultOptions = { + message: 'The value must have a boolean type.', + trueValue: '1', + falseValue: '0' + }; + + withData({ + 'empty string': ['', {}, false], + 'empty string, skip on empty': ['', {skipOnEmpty: true}, true], + 'non-empty string, does not equal neither trueValue no falseValue': ['a', {}, false], + 'integer, value equals falseValue': [0, {}, true], + 'integer, value equals trueValue': [1, {}, true], + 'string equals falseValue': ['0', {}, true], + 'string equals trueValue': ['1', {}, true], + 'integer, value equals falseValue, strict mode': [0, {strict: true}, false], + 'integer, value equals trueValue, strict mode': [1, {strict: true}, false], + // trueValue, falseValue + 'string equals custom trueValue, custom trueValue is set': ['yes', {trueValue: 'yes'}, true], + 'string does not equal neither trueValue no falseValue, custom trueValue is set': [ + 'no', + {trueValue: 'yes'}, + false + ], + 'string equals custom falseValue, custom falseValue is set': ['no', {falseValue: 'no'}, true], + 'string does not equal neither trueValue no falseValue, custom falseValue is set': [ + 'yes', + {falseValue: 'no'}, + false + ], + 'string equals custom trueValue, custom trueValue and falseValue are set': [ + 'yes', + {trueValue: 'yes', falseValue: 'no'}, + true + ], + 'string equals custom falseValue, custom trueValue and falseValue are set': [ + 'no', + {trueValue: 'yes', falseValue: 'no'}, + true + ], + 'string does not equal neither custom trueValue no falseValue, custom trueValue and falseValue are set': [ + 'a', + {trueValue: 'yes', falseValue: 'no'}, + false + ] + }, function (value, customOptions, expectValid) { + it(getValidatorMessage(expectValid), function () { + var options = $.extend({}, defaultOptions, customOptions); + var messages = []; + var expectedMessages = expectValid ? [] : ['The value must have a boolean type.']; + + yii.validation.boolean(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('string validator', function () { + var defaultOptions = { + message: 'Invalid type.', + tooShort: 'Too short.', + tooLong: 'Too long.', + notEqual: 'Not equal.' + }; + + withData({ + 'empty string': ['', {}, []], + 'empty string, skip on empty': ['', {skipOnEmpty: true}, []], + 'non-empty string': ['a', {}, []], + 'integer': [1, {}, ['Invalid type.']], + // min + 'string less than min': ['Word', {min: 5}, ['Too short.']], + 'string more than min': ['Some string', {min: 5}, []], + 'string equals min': ['Equal', {min: 5}, []], + // max + 'string less than max': ['Word', {max: 5}, []], + 'string more than max': ['Some string', {max: 5}, ['Too long.']], + 'string equals max': ['Equal', {max: 5}, []], + // is + 'string equals exact length': ['Equal', {is: 5}, []], + 'string does not equal exact length': ['Does not equal', {is: 5}, ['Not equal.']], + 'string does not equal exact length and less than min': ['Word', {is: 5, min: 5}, ['Not equal.']], + // min and max + 'string less than min, both min and max are set': ['Word', {min: 5, max: 10}, ['Too short.']], + 'string in between of min and max, both min and max are set': ['Between', {min: 5, max: 10}, []], + 'string more than max, both min and max are set': ['Some string', {min: 5, max: 10}, ['Too long.']] + }, function (value, customOptions, expectedMessages) { + it(getValidatorMessage(expectedMessages), function () { + var options = $.extend({}, defaultOptions, customOptions); + var messages = []; + + yii.validation.string(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('file validator', function () { + var defaultOptions = { + message: 'Unable to upload a file.', + uploadRequired: 'Upload is required.', + tooMany: 'Too many files.', + wrongExtension: 'File {file} has wrong extension.', + wrongMimeType: 'File {file} has wrong mime type.', + tooSmall: 'File {file} is too small.', + tooBig: 'File {file} is too big.' + }; + var attribute = { + input: '#input-id', + $form: 'jQuery form object' + }; + var files; + var filesService = { + getFiles: function () { + return files; + } + }; + var $input = { + get: function (value) { + return value === 0 ? {files: filesService.getFiles()} : undefined; + } + }; + var jQueryInitStub; + var inputGetSpy; + var filesServiceSpy; + + beforeEach(function () { + jQueryInitStub = sinon.stub($.fn, 'init'); + jQueryInitStub.withArgs(attribute.input, attribute.$form).returns($input); + inputGetSpy = sinon.spy($input, 'get'); + filesServiceSpy = sinon.spy(filesService, 'getFiles'); + }); + + afterEach(function () { + jQueryInitStub.restore(); + inputGetSpy.restore(); + filesServiceSpy.restore(); + }); + + describe('with File API is not available', function () { + beforeEach(function () { + registerTestableCode({File: undefined}); + }); + + afterEach(function () { + registerTestableCode(); + }); + + it(VALIDATOR_SUCCESS_MESSAGE, function () { + var messages = []; + + yii.validation.file(attribute, messages, defaultOptions); + assert.deepEqual(messages, []); + + assert.isFalse(jQueryInitStub.called); + assert.isFalse(inputGetSpy.called); + assert.isFalse(filesServiceSpy.called); + }); + }); + + describe('with File API is available', function () { + withData({ + 'files are not available': [undefined, {}, ['Unable to upload a file.']], + 'no files': [[], {}, ['Upload is required.']], + 'no files, skip on empty': [[], {skipOnEmpty: true}, []], + // maxFiles + 'number of files less than maximum': [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {maxFiles: 2}, + [] + ], + 'number of files equals maximum': [ + [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {name: 'file.png', type: 'image/png', size: 150 * 1024} + ], + {maxFiles: 2}, + [] + ], + 'number of files more than maximum': [ + [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {name: 'file.png', type: 'image/png', size: 150 * 1024}, + {name: 'file.bmp', type: 'image/bmp', size: 200 * 1024} + ], + {maxFiles: 2}, + ['Too many files.'] + ], + // extensions + 'files in extensions list': [ + [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {name: 'file.png', type: 'image/png', size: 150 * 1024} + ], + {extensions: ['jpg', 'png']}, + [] + ], + 'file not in extensions list': [ + [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {name: 'file.bmp', type: 'image/bmp', size: 150 * 1024} + ], + {extensions: ['jpg', 'png']}, + ['File file.bmp has wrong extension.'] + ], + // mimeTypes + 'mime type in mime types list': [ + [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {name: 'file.png', type: 'image/png', size: 150 * 1024} + ], + {mimeTypes: ['image/jpeg', 'image/png']}, + [] + ], + 'mime type not in mime types list': [ + [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {name: 'file.bmp', type: 'image/bmp', size: 150 * 1024} + ], + {mimeTypes: ['image/jpeg', 'image/png']}, + ['File file.bmp has wrong mime type.'] + ], + // maxSize + 'size less than maximum size': [ + [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {name: 'file.png', type: 'image/png', size: 150 * 1024} + ], + {maxSize: 200 * 1024}, + [] + ], + 'size equals maximum size': [ + [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {name: 'file.png', type: 'image/png', size: 100 * 1024} + ], + {maxSize: 100 * 1024}, + [] + ], + 'size more than maximum size': [ + [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {name: 'file.png', type: 'image/png', size: 150 * 1024} + ], + {maxSize: 50 * 1024}, + ['File file.jpg is too big.', 'File file.png is too big.'] + ], + // minSize + 'size less than minimum size': [ + [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {name: 'file.png', type: 'image/png', size: 150 * 1024} + ], + {minSize: 120 * 1024}, + ['File file.jpg is too small.'] + ], + 'size equals minimum size': [ + [ + {name: 'file.jpg', type: 'image/bmp', size: 100 * 1024}, + {name: 'file.png', type: 'image/png', size: 100 * 1024} + ], + {maxSize: 100 * 1024}, + [] + ], + 'size more than minimum size': [ + [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {name: 'file.bmp', type: 'image/bmp', size: 150 * 1024} + ], + {minSize: 80 * 1024}, + [] + ], + 'one file is less than minimum size, one file is more than maximum size': [ + [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}, + {name: 'file.png', type: 'image/png', size: 250 * 1024} + ], + {minSize: 150 * 1024, maxSize: 200 * 1024}, + ['File file.jpg is too small.', 'File file.png is too big.'] + ] + }, function (uploadedFiles, customOptions, expectedMessages) { + it(getValidatorMessage(expectedMessages), function () { + files = uploadedFiles; + var options = $.extend({}, defaultOptions, customOptions); + var messages = []; + + yii.validation.file(attribute, messages, options); + assert.deepEqual(messages, expectedMessages); + + assert.isTrue(jQueryInitStub.calledOnce); + assert.deepEqual(jQueryInitStub.getCall(0).args, [attribute.input, attribute.$form]); + assert.isTrue(inputGetSpy.calledOnce); + assert.deepEqual(inputGetSpy.getCall(0).args, [0]); + assert.isTrue(filesServiceSpy.calledOnce); + }); + }); + }); + }); + + describe('image validator', function () { + var attribute = { + input: '#input-id', + $form: 'jQuery form object' + }; + var files; + var filesService = { + getFiles: function () { + return files; + } + }; + var $input = { + get: function (value) { + return value === 0 ? {files: filesService.getFiles()} : undefined; + } + }; + var deferred; + var jQueryInitStub; + var inputGetSpy; + var filesServiceSpy; + var validateImageStub; + var deferredStub; + + beforeEach(function () { + jQueryInitStub = sinon.stub($.fn, 'init'); + jQueryInitStub.withArgs(attribute.input, attribute.$form).returns($input); + inputGetSpy = sinon.spy($input, 'get'); + filesServiceSpy = sinon.spy(filesService, 'getFiles'); + validateImageStub = sinon.stub(yii.validation, 'validateImage'); + deferred = $.Deferred(); + deferredStub = sinon.stub(deferred, 'resolve'); + }); + + afterEach(function () { + jQueryInitStub.restore(); + inputGetSpy.restore(); + filesServiceSpy.restore(); + validateImageStub.restore(); + deferredStub.restore(); + }); + + describe('with FileReader API is not available', function () { + beforeEach(function () { + registerTestableCode({FileReader: undefined}); + }); + + afterEach(function () { + registerTestableCode(); + }); + + it(VALIDATOR_SUCCESS_MESSAGE, function () { + files = [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024, width: 100, height: 100}, + {name: 'file.png', type: 'image/png', size: 150 * 1024, width: 250, height: 250} + ]; + var messages = []; + var deferredList = []; + + yii.validation.image(attribute, messages, {}, deferredList); + assert.deepEqual(messages, []); + + assert.isFalse(validateImageStub.called); + assert.isFalse(deferredStub.called); + assert.deepEqual(deferredList, []); + }); + }); + + describe('with FileReader API is available', function () { + it(VALIDATOR_ERROR_MESSAGE, function () { + files = [ + {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024, width: 100, height: 100}, + {name: 'file.bmp', type: 'image/bmp', size: 150 * 1024, width: 250, height: 250} + ]; + var options = { + extensions: ['jpg', 'png'], + wrongExtension: 'File {file} has wrong extension.', + minWidth: 200, + underWidth: 'File {file} has small width.' + }; + var messages = []; + var deferredList = []; + + yii.validation.image(attribute, messages, options, deferredList); + assert.deepEqual(messages, ['File file.bmp has wrong extension.']); + + assert.equal(validateImageStub.callCount, files.length); + + for (var i = 0; i < validateImageStub.callCount; i++) { + assert.equal(validateImageStub.getCall(i).args.length, 6); + assert.deepEqual(validateImageStub.getCall(i).args[0], files[i]); + assert.deepEqual(validateImageStub.getCall(i).args[1], ['File file.bmp has wrong extension.']); + assert.deepEqual(validateImageStub.getCall(i).args[2], options); + assert.isDeferred(validateImageStub.getCall(i).args[3]); + assert.instanceOf(validateImageStub.getCall(i).args[4], FileReader); + assert.instanceOf(validateImageStub.getCall(i).args[5], Image); + } + + assert.equal(deferredList.length, files.length); + + for (i = 0; i < deferredList.length; i++) { + assert.isDeferred(deferredList[i]); + } + }); + }); + }); + + describe('validateImage method', function () { + var file = {name: 'file.jpg', type: 'image/jpeg', size: 100 * 1024}; + var image = new Image(); + var deferred; + var fileReader = new FileReader(); + var deferredStub; + var fileReaderStub; + + beforeEach(function () { + deferred = $.Deferred(); + deferredStub = sinon.stub(deferred, 'resolve'); + }); + + afterEach(function () { + deferredStub.restore(); + fileReaderStub.restore(); + }); + + function verifyStubs() { + assert.isTrue(fileReaderStub.calledOnce); + assert.isTrue(deferredStub.calledOnce); + } + + describe('with error while reading data', function () { + beforeEach(function () { + fileReaderStub = sinon.stub(fileReader, 'readAsDataURL', function () { + this.onerror(); + }); + }); + + it(VALIDATOR_SUCCESS_MESSAGE, function () { + var messages = []; + + yii.validation.validateImage(file, messages, {}, deferred, fileReader, image); + assert.deepEqual(messages, []); + + verifyStubs(); + }); + }); + + describe('with error while reading image', function () { + beforeEach(function () { + fileReaderStub = sinon.stub(fileReader, 'readAsDataURL', function () { + this.onload = function () { + image.onerror(); + }; + + this.onload(); + }); + }); + + it(VALIDATOR_ERROR_MESSAGE, function () { + var messages = []; + var options = {notImage: 'File {file} is not an image.'}; + + yii.validation.validateImage(file, messages, options, deferred, fileReader, image); + assert.deepEqual(messages, ['File file.jpg is not an image.']); + + verifyStubs(); + }); + }); + + describe('with successfully read image', function () { + var defaultOptions = { + underWidth: 'File {file} has small width.', + overWidth: 'File {file} has big width.', + underHeight: 'File {file} has small height.', + overHeight: 'File {file} has big height.' + }; + + beforeEach(function () { + fileReaderStub = sinon.stub(fileReader, 'readAsDataURL', function () { + this.onload = function () { + image.onload(); + }; + + this.onload(); + }); + }); + + withData({ + // minWidth + 'width less than minimum width': [ + {width: 100, height: 100}, + {minWidth: 200}, + ['File file.jpg has small width.'] + ], + 'width equals minimum width': [{width: 100, height: 100}, {minWidth: 100}, []], + 'width more than minimum width': [{width: 200, height: 200}, {minWidth: 100}, []], + // maxWidth + 'width less than maximum width': [{width: 100, height: 100}, {maxWidth: 200}, []], + 'width equals maximum width': [{width: 100, height: 100}, {maxWidth: 100}, []], + 'width more than maximum width': [ + {width: 200, height: 200}, + {maxWidth: 100}, + ['File file.jpg has big width.'] + ], + // minHeight + 'height less than minimum height': [ + {width: 100, height: 100}, + {minHeight: 200}, + ['File file.jpg has small height.'] + ], + 'height equals minimum height': [{width: 100, height: 100}, {minHeight: 100}, []], + 'height more than minimum height': [{width: 200, height: 200}, {minHeight: 100}, []], + // maxHeight + 'height less than maximum height': [{width: 100, height: 100}, {maxHeight: 200}, []], + 'height equals maximum height': [{width: 100, height: 100}, {maxHeight: 100}, []], + 'height more than maximum height': [ + {width: 200, height: 200}, + {maxHeight: 100}, + ['File file.jpg has big height.'] + ], + // minWidth and minHeight + 'width less than minimum width and height less than minimum height': [ + {width: 100, height: 100}, + {minWidth: 200, minHeight: 200}, + ['File file.jpg has small width.', 'File file.jpg has small height.'] + ] + }, function (imageSize, customOptions, expectedMessages) { + it(getValidatorMessage(expectedMessages), function () { + image.width = imageSize.width; + image.height = imageSize.height; + var options = $.extend({}, defaultOptions, customOptions); + var messages = []; + + yii.validation.validateImage(file, messages, options, deferred, fileReader, image); + assert.deepEqual(messages, expectedMessages); + + verifyStubs(); + }); + }); + }); + }); + + describe('number validator', function () { + var integerPattern = /^\s*[+-]?\d+\s*$/; + var numberPattern = /^\s*[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\s*$/; + var defaultOptions = { + message: 'Not a number.', + tooSmall: 'Number is too small.', + tooBig: 'Number is too big.' + }; + + describe('with integer pattern', function () { + withData({ + 'empty string': ['', false], + 'non-empty string': ['a', false], + 'zero': ['0', true], + 'positive integer, no sign': ['2', true], + 'positive integer with sign': ['+2', true], + 'negative integer': ['-2', true], + 'decimal fraction with dot': ['2.5', false], + 'decimal fraction with comma': ['2,5', false] + }, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + var options = $.extend({}, defaultOptions, {pattern: integerPattern}); + var messages = []; + var expectedMessages = expectValid ? [] : ['Not a number.']; + + yii.validation.number(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('with number pattern', function () { + withData({ + 'empty string': ['', false], + 'non-empty string': ['a', false], + 'zero': ['0', true], + 'positive integer, no sign': ['2', true], + 'positive integer with sign': ['+2', true], + 'negative integer': ['-2', true], + 'decimal fraction with dot, no sign': ['2.5', true], + 'positive decimal fraction with dot and sign': ['+2.5', true], + 'negative decimal fraction with dot': ['-2.5', true], + 'decimal fraction with comma': ['2,5', false], + 'floating number with exponential part': ['-1.23e-10', true] + }, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + var options = $.extend({}, defaultOptions, {pattern: numberPattern}); + var messages = []; + var expectedMessages = expectValid ? [] : ['Not a number.']; + + yii.validation.number(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('with different options, integer pattern', function () { + withData({ + 'empty string, skip on empty': ['', {skipOnEmpty: true}, []], + // Not a string + 'undefined': [undefined, {}, []], + 'integer, fits pattern': [2, {}, []], + 'integer, does not fit pattern': [2.5, {}, []], + // min + 'less than minimum': ['1', {min: 2}, ['Number is too small.']], + 'equals minimum': ['2', {min: 2}, []], + 'more than minimum': ['3', {min: 2}, []], + 'wrong integer and less than min': ['1.5', {min: 2}, ['Not a number.']], + // max + 'less than maximum': ['1', {max: 2}, []], + 'equals maximum': ['2', {max: 2}, []], + 'more than maximum': ['3', {max: 2}, ['Number is too big.']] + }, function (value, customOptions, expectedMessages) { + it(getValidatorMessage(expectedMessages), function () { + customOptions.pattern = integerPattern; + var options = $.extend({}, defaultOptions, customOptions); + var messages = []; + + yii.validation.number(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + }); + + describe('range validator', function () { + withData({ + 'empty string, skip on empty': ['', {skipOnEmpty: true}, []], + 'array and arrays are not allowed': [['a', 'b'], {}, ['Invalid value.']], + 'string in array': ['a', {range: ['a', 'b', 'c']}, []], + 'string not in array': ['d', {range: ['a', 'b', 'c']}, ['Invalid value.']], + 'array in array': [['a', 'b'], {range: ['a', 'b', 'c'], allowArray: true}, []], + 'array not in array': [['a', 'd'], {range: ['a', 'b', 'c'], allowArray: true}, ['Invalid value.']], + 'string in array and inverted logic': ['a', {range: ['a', 'b', 'c'], not: true}, ['Invalid value.']], + 'string not in array and inverted logic': ['d', {range: ['a', 'b', 'c'], not: true}, []], + 'array in array and inverted logic': [ + ['a', 'b'], + {range: ['a', 'b', 'c'], allowArray: true, not: true}, + ['Invalid value.'] + ], + 'array not in array and inverted logic': [ + ['a', 'd'], + {range: ['a', 'b', 'c'], allowArray: true, not: true}, + [] + ] + }, function (value, options, expectedMessages) { + it(getValidatorMessage(expectedMessages), function () { + options.message = 'Invalid value.'; + var messages = []; + + yii.validation.range(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('regular expression validator', function () { + var integerPattern = /^\s*[+-]?\d+\s*$/; + + describe('with integer pattern', function () { + withData({ + 'empty string, skip on empty': ['', {skipOnEmpty: true}, []], + 'regular integer': ['2', {}, []], + 'non-integer': ['2.5', {}, ['Invalid value.']], + 'regular integer, inverted logic': ['2', {not: true}, ['Invalid value.']], + 'integer pattern, non-integer, inverted logic': ['2.5', {pattern: integerPattern, not: true}, []] + }, function (value, options, expectedMessages) { + it(getValidatorMessage(expectedMessages), function () { + options.message = 'Invalid value.'; + options.pattern = integerPattern; + var messages = []; + + yii.validation.regularExpression(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + }); + + describe('email validator', function () { + var pattern = "^[a-zA-Z0-9!#$%&'*+\\/=?^_`{|}~-]+(?:\\.[a-zA-Z0-9!#$%&'*+\\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9]" + + "(?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$"; + pattern = new RegExp(pattern); + var fullPattern = "^[^@]*<[a-zA-Z0-9!#$%&'*+\\/=?^_`{|}~-]+(?:\\.[a-zA-Z0-9!#$%&'*+\\/=?^_`{|}~-]+)*@" + + "(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?>$"; + fullPattern = new RegExp(fullPattern); + var defaultOptions = { + pattern: pattern, + fullPattern: fullPattern, + message: 'Invalid value.' + }; + + describe('with empty string, skip on empty', function () { + it(VALIDATOR_SUCCESS_MESSAGE, function () { + var messages = []; + var options = $.extend({}, defaultOptions, {skipOnEmpty: true}); + + yii.validation.email('', messages, options); + assert.deepEqual(messages, []); + }); + }); + + describe('with basic configuration', function () { + withData({ + 'letters only': ['sam@rmcreative.ru', true], + 'numbers in local-part': ['5011@gmail.com', true], + 'uppercase and lowercase letters, dot and numbers in local-part': ['Abc.123@example.com', true], + 'user mailbox': ['user+mailbox/department=shipping@example.com', true], + 'special symbols in local-part': ['!#$%&\'*+-/=?^_`.{|}~@example.com', true], + 'domain only': ['rmcreative.ru', false], + 'unicode in domain': ['example@äüößìà.de', false], + 'unicode (russian characters) in domain': ['sam@рмкреатиф.ru', false], + 'ASCII in domain': ['example@xn--zcack7ayc9a.de', true], + 'angle brackets, name': ['Carsten Brandt ', false], + 'angle brackets, quoted name': ['"Carsten Brandt" ', false], + 'angle brackets, no name': ['', false], + 'angle brackets, name, dot in local-part': ['John Smith ', false], + 'angle brackets, name, domain only': ['John Smith ', false], + 'no angle brackets, name': ['Information info@oertliches.de', false], + 'no angle brackets, name, unicode in domain': ['Information info@örtliches.de', false], + 'angle brackets, long quoted name': [ + '"' + StringUtils.repeatString('a', 300) + '" ', + false + ], + 'angle brackets, name, local part more than 64 characters': [ + 'Short Name <' + StringUtils.repeatString('a', 65) + '@example.com>', + false + ], + 'angle brackets, name, domain more than 254 characters': [ + 'Short Name <' + StringUtils.repeatString('a', 255) + '.com>', + false + ], + 'angle brackets, name, unicode in domain': ['Information ', false], + 'angle brackets, name, unicode, local-part length is close to 64 characters': [ + // 21 * 3 = 63 + 'Короткое имя <' + StringUtils.repeatString('бла', 21) + '@пример.com>', + false + ], + 'angle brackets, name, unicode, domain length is close to 254 characters': [ + // 83 * 3 + 4 = 253 + 'Короткое имя <тест@' + StringUtils.repeatString('бла', 83) + '.com>', + false + ] + }, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + + yii.validation.email(value, messages, defaultOptions); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('with allowed name', function () { + withData({ + 'letters only': ['sam@rmcreative.ru', true], + 'numbers in local-part': ['5011@gmail.com', true], + 'uppercase and lowercase letters, dot and numbers in local-part': ['Abc.123@example.com', true], + 'user mailbox': ['user+mailbox/department=shipping@example.com', true], + 'special symbols in local-part': ['!#$%&\'*+-/=?^_`.{|}~@example.com', true], + 'domain only': ['rmcreative.ru', false], + 'unicode in domain': ['example@äüößìà.de', false], + 'unicode (russian characters) in domain': ['sam@рмкреатиф.ru', false], + 'ASCII in domain': ['example@xn--zcack7ayc9a.de', true], + 'angle brackets, name': ['Carsten Brandt ', true], + 'angle brackets, quoted name': ['"Carsten Brandt" ', true], + 'angle brackets, no name': ['', true], + 'angle brackets, name, dot in local-part': ['John Smith ', true], + 'angle brackets, name, domain only': ['John Smith ', false], + 'no angle brackets, name': ['Information info@oertliches.de', false], + 'no angle brackets, name, unicode in domain': ['Information info@örtliches.de', false], + 'angle brackets, long quoted name': [ + '"' + StringUtils.repeatString('a', 300) + '" ', + true + ], + 'angle brackets, name, local part more than 64 characters': [ + 'Short Name <' + StringUtils.repeatString('a', 65) + '@example.com>', + false + ], + 'angle brackets, name, domain more than 254 characters': [ + 'Short Name <' + StringUtils.repeatString('a', 255) + '.com>', + false + ], + 'angle brackets, name, unicode in domain': ['Information ', false], + 'angle brackets, name, unicode, local-part length is close to 64 characters': [ + // 21 * 3 = 63 + 'Короткое имя <' + StringUtils.repeatString('бла', 21) + '@пример.com>', + false + ], + 'angle brackets, name, unicode, domain length is close to 254 characters': [ + // 83 * 3 + 4 = 253 + 'Короткое имя <тест@' + StringUtils.repeatString('бла', 83) + '.com>', + false + ] + }, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + var options = $.extend({}, defaultOptions, {allowName: true}); + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + + yii.validation.email(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('with enabled IDN', function () { + withData({ + 'letters only': ['sam@rmcreative.ru', true], + 'numbers in local-part': ['5011@gmail.com', true], + 'uppercase and lowercase letters, dot and numbers in local-part': ['Abc.123@example.com', true], + 'user mailbox': ['user+mailbox/department=shipping@example.com', true], + 'special symbols in local-part': ['!#$%&\'*+-/=?^_`.{|}~@example.com', true], + 'domain only': ['rmcreative.ru', false], + 'unicode in domain': ['example@äüößìà.de', true], + 'unicode (russian characters) in domain': ['sam@рмкреатиф.ru', true], + 'ASCII in domain': ['example@xn--zcack7ayc9a.de', true], + 'angle brackets, name': ['Carsten Brandt ', false], + 'angle brackets, quoted name': ['"Carsten Brandt" ', false], + 'angle brackets, no name': ['', false], + 'angle brackets, name, dot in local-part': ['John Smith ', false], + 'angle brackets, name, domain only': ['John Smith ', false], + 'no angle brackets, name': ['Information info@oertliches.de', false], + 'no angle brackets, name, unicode in domain': ['Information info@örtliches.de', false], + 'angle brackets, long quoted name': [ + '"' + StringUtils.repeatString('a', 300) + '" ', + false + ], + 'angle brackets, name, local part more than 64 characters': [ + 'Short Name <' + StringUtils.repeatString('a', 65) + '@example.com>', + false + ], + 'angle brackets, name, domain more than 254 characters': [ + 'Short Name <' + StringUtils.repeatString('a', 255) + '.com>', + false + ], + 'angle brackets, name, unicode in domain': ['Information ', false], + 'angle brackets, name, unicode, local-part length is close to 64 characters': [ + // 21 * 3 = 63 + 'Короткое имя <' + StringUtils.repeatString('бла', 21) + '@пример.com>', + false + ], + 'angle brackets, name, unicode, domain length is close to 254 characters': [ + // 83 * 3 + 4 = 253 + 'Короткое имя <тест@' + StringUtils.repeatString('бла', 83) + '.com>', + false + ] + }, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + var options = $.extend({}, defaultOptions, {enableIDN: true}); + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + + yii.validation.email(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('with allowed name and enabled IDN', function () { + withData({ + 'letters only': ['sam@rmcreative.ru', true], + 'numbers in local-part': ['5011@gmail.com', true], + 'uppercase and lowercase letters, dot and numbers in local-part': ['Abc.123@example.com', true], + 'user mailbox': ['user+mailbox/department=shipping@example.com', true], + 'special symbols in local-part': ['!#$%&\'*+-/=?^_`.{|}~@example.com', true], + 'domain only': ['rmcreative.ru', false], + 'unicode in domain': ['example@äüößìà.de', true], + 'unicode (russian characters) in domain': ['sam@рмкреатиф.ru', true], + 'ASCII in domain': ['example@xn--zcack7ayc9a.de', true], + 'angle brackets, name': ['Carsten Brandt ', true], + 'angle brackets, quoted name': ['"Carsten Brandt" ', true], + 'angle brackets, no name': ['', true], + 'angle brackets, name, dot in local-part': ['John Smith ', true], + 'angle brackets, name, domain only': ['John Smith ', false], + 'no angle brackets, name': ['Information info@oertliches.de', false], + 'no angle brackets, name, unicode in domain': ['Information info@örtliches.de', false], + 'angle brackets, long quoted name': [ + '"' + StringUtils.repeatString('a', 300) + '" ', + true + ], + 'angle brackets, name, local part more than 64 characters': [ + 'Short Name <' + StringUtils.repeatString('a', 65) + '@example.com>', + false + ], + 'angle brackets, name, domain more than 254 characters': [ + 'Short Name <' + StringUtils.repeatString('a', 255) + '.com>', + false + ], + 'angle brackets, name, unicode in domain': ['Information ', true], + 'angle brackets, name, unicode, local-part length is close to 64 characters': [ + // 21 * 3 = 63 + 'Короткое имя <' + StringUtils.repeatString('бла', 21) + '@пример.com>', + false + ], + 'angle brackets, name, unicode, domain length is close to 254 characters': [ + // 83 * 3 + 4 = 253 + 'Короткое имя <тест@' + StringUtils.repeatString('бла', 83) + '.com>', + false + ] + }, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + var options = $.extend({}, defaultOptions, {allowName: true, enableIDN: true}); + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + + yii.validation.email(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + }); + + describe('url validator', function () { + function getPattern(validSchemes) { + if (validSchemes === undefined) { + validSchemes = ['http', 'https']; + } + + var pattern = '^{schemes}://(([A-Z0-9][A-Z0-9_-]*)(\\.[A-Z0-9][A-Z0-9_-]*)+)(?::\\d{1,5})?(?:$|[?\\/#])'; + pattern = pattern.replace('{schemes}', '(' + validSchemes.join('|') + ')'); + + return new RegExp(pattern, 'i'); + } + + var defaultOptions = { + pattern: getPattern(), + message: 'Invalid value.' + }; + + describe('with empty string, skip on empty', function () { + it(VALIDATOR_SUCCESS_MESSAGE, function () { + var messages = []; + var options = $.extend({}, defaultOptions, {skipOnEmpty: true}); + + yii.validation.url('', messages, options); + assert.deepEqual(messages, []); + }); + }); + + describe('with basic configuration', function () { + withData({ + 'domain only': ['google.de', false], + 'http': ['http://google.de', true], + 'https': ['https://google.de', true], + 'scheme with typo': ['htp://yiiframework.com', false], + 'https, action with get parameters': [ + 'https://www.google.de/search?q=yii+framework&ie=utf-8&oe=utf-8&rls=org.mozilla:de:official' + + '&client=firefox-a&gws_rd=cr', + true + ], + 'scheme not in valid schemes': ['ftp://ftp.ruhr-uni-bochum.de/', false], + 'invalid domain': ['http://invalid,domain', false], + 'not allowed symbol (comma) after domain': ['http://example.com,', false], + 'not allowed symbol (star) after domain': ['http://example.com*12', false], + 'symbols after slash': ['http://example.com/*12', true], + 'get parameter without value': ['http://example.com/?test', true], + 'anchor': ['http://example.com/#test', true], + 'port, anchor': ['http://example.com:80/#test', true], + 'port (length equals limit), anchor': ['http://example.com:65535/#test', true], + 'port, get parameter without value': ['http://example.com:81/?good', true], + 'get parameter without value and slash': ['http://example.com?test', true], + 'anchor without slash': ['http://example.com#test', true], + 'port and anchor without slash': ['http://example.com:81#test', true], + 'port and get parameter without value and slash': ['http://example.com:81?good', true], + 'not allowed symbol after domain followed by get parameter without value': [ + 'http://example.com,?test', + false + ], + 'skipped port and get parameter without value': ['http://example.com:?test', false], + 'skipped port and action': ['http://example.com:test', false], + 'port (length more than limit) and action': ['http://example.com:123456/test', false], + 'unicode, special symbols': ['http://äüö?=!"§$%&/()=}][{³²€.edu', false] + }, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + + yii.validation.url(value, messages, defaultOptions); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('with default scheme', function () { + withData({ + 'no scheme': ['yiiframework.com', true], + 'http': ['http://yiiframework.com', true] + }, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + var options = $.extend({}, defaultOptions, {defaultScheme: 'https'}); + + yii.validation.url(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('without scheme', function () { + it(VALIDATOR_SUCCESS_MESSAGE, function () { + var messages = []; + var options = $.extend({}, defaultOptions, { + pattern: /(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)/i + }); + + yii.validation.url('yiiframework.com', messages, options); + assert.deepEqual(messages, []); + }); + }); + + describe('with default scheme and custom schemes', function () { + withData({ + 'ftp': ['ftp://ftp.ruhr-uni-bochum.de/', true], + 'no scheme': ['google.de', true], + 'http': ['http://google.de', true], + 'https': ['https://google.de', true], + 'scheme with typo': ['htp://yiiframework.com', false], + 'relative url': ['//yiiframework.com', false] + }, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + var options = $.extend({}, defaultOptions, { + pattern: getPattern(['http', 'https', 'ftp', 'ftps']), + defaultScheme: 'http' + }); + + yii.validation.url(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + + describe('with enabled IDN', function () { + withData({ + 'unicode in domain': ['http://äüößìà.de', true], + // converted via http://mct.verisign-grs.com/convertServlet + 'ASCII in domain': ['http://xn--zcack7ayc9a.de', true] + }, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + var options = $.extend({}, defaultOptions, {enableIDN: true}); + + yii.validation.url(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }); + }); + }); + }); + + describe('trim filter', function () { + var attribute = {input: '#input-id'}; + var getInputVal; + var $input = { + val: function () { + return getInputVal(); + } + }; + var $form = { + find: function () { + return $input; + } + }; + + var formSpy; + var inputSpy; + + beforeEach(function () { + formSpy = sinon.spy($form, 'find'); + inputSpy = sinon.spy($input, 'val'); + }); + + afterEach(function () { + formSpy.restore(); + inputSpy.restore(); + }); + + describe('with empty string, skip on empty', function () { + it('should leave value and element value as is and return not changed value', function () { + getInputVal = function () { + return ''; + }; + + assert.strictEqual(yii.validation.trim($form, attribute, {skipOnEmpty: true}), ''); + + assert.isTrue(formSpy.calledOnce); + assert.equal(formSpy.getCall(0).args[0], attribute.input); + + assert.isTrue(inputSpy.calledOnce); + assert.strictEqual(inputSpy.getCall(0).args[0], undefined); + }); + }); + + withData({ + 'nothing to trim': ['value', 'value'], + 'spaces at the beginning and end': [' value ', 'value'], + 'newlines at the beginning and end': ['\nvalue\n', 'value'], + 'spaces and newlines at the beginning and end': ['\n value \n', 'value'] + }, function (value, expectedValue) { + it('should return trimmed value and set it as value of element', function () { + getInputVal = function (val) { + return val === undefined ? value : undefined; + }; + + assert.equal(yii.validation.trim($form, attribute, {}), expectedValue); + + assert.isTrue(formSpy.calledOnce); + assert.equal(formSpy.getCall(0).args[0], attribute.input); + + assert.equal(inputSpy.callCount, 2); + assert.strictEqual(inputSpy.getCall(0).args[0], undefined); + assert.equal(inputSpy.getCall(1).args[0], expectedValue); + }); + }); + }); + + describe('captcha validator', function () { + // Converted using yii\captcha\CaptchaAction generateValidationHash() method + var hashes = {'Code': 379, 'code': 411}; + var caseInSensitiveData = { + 'valid code in lowercase': ['code', true], + 'valid code in uppercase': ['CODE', true], + 'valid code as is': ['Code', true], + 'invalid code': ['invalid code', false] + }; + var caseSensitiveData = { + 'valid code in lowercase': ['code', false], + 'valid code in uppercase': ['CODE', false], + 'valid code as is': ['Code', true], + 'invalid code': ['invalid code', false] + }; + var defaultOptions = { + message: 'Invalid value.', + hashKey: 'hashKey' + }; + var hashesData = [hashes['Code'], hashes['code']]; + var jQueryDataStub; + + beforeEach(function () { + jQueryDataStub = sinon.stub($.prototype, 'data', function () { + return hashesData; + }); + }); + + afterEach(function () { + jQueryDataStub.restore(); + }); + + function verifyJQueryDataStub() { + assert.isTrue(jQueryDataStub.calledOnce); + assert.equal(jQueryDataStub.getCall(0).args[0], defaultOptions.hashKey); + } + + describe('with empty string, skip on empty', function () { + it(VALIDATOR_SUCCESS_MESSAGE, function () { + var messages = []; + var options = $.extend({}, defaultOptions, {skipOnEmpty: true}); + + yii.validation.captcha('', messages, options); + assert.deepEqual(messages, []); + + assert.isFalse(jQueryDataStub.called); + }); + }); + + describe('with ajax, case insensitive', function () { + withData(caseInSensitiveData, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + + yii.validation.captcha(value, messages, defaultOptions); + assert.deepEqual(messages, expectedMessages); + + verifyJQueryDataStub(); + }); + }); + }); + + describe('with ajax, case sensitive', function () { + withData(caseSensitiveData, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + var options = $.extend({}, defaultOptions, {caseSensitive: true}); + + yii.validation.captcha(value, messages, options); + assert.deepEqual(messages, expectedMessages); + + verifyJQueryDataStub(); + }); + }); + }); + + describe('with hash, case insensitive', function () { + withData(caseInSensitiveData, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + hashesData = undefined; + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + var options = $.extend({}, defaultOptions, {hash: hashes['code']}); + + yii.validation.captcha(value, messages, options); + assert.deepEqual(messages, expectedMessages); + + verifyJQueryDataStub(); + }); + }); + }); + + describe('with hash, case sensitive', function () { + withData(caseSensitiveData, function (value, expectValid) { + it(getValidatorMessage(expectValid), function () { + hashesData = undefined; + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + var options = $.extend({}, defaultOptions, {hash: hashes['Code'], caseSensitive: true}); + + yii.validation.captcha(value, messages, options); + assert.deepEqual(messages, expectedMessages); + + verifyJQueryDataStub(); + }); + }); + }); + }); + + describe('compare validator', function () { + var $input = { + val: function () { + return 'b'; + } + }; + var jQueryInitStub; + var inputSpy; + + beforeEach(function () { + jQueryInitStub = sinon.stub($.fn, 'init', function () { + return $input; + }); + inputSpy = sinon.spy($input, 'val'); + }); + + afterEach(function () { + jQueryInitStub.restore(); + inputSpy.restore(); + }); + + withData({ + 'empty string, skip on empty': ['', {skipOnEmpty: true}, true], + // == + '"==" operator, 2 identical integers': [2, {operator: '==', compareValue: 2}, true], + '"==" operator, 2 different integers': [2, {operator: '==', compareValue: 3}, false], + '"==" operator, 2 identical decimal fractions': [2.5, {operator: '==', compareValue: 2.5}, true], + '"==" operator, integer and string with the same values': [2, {operator: '==', compareValue: '2'}, true], + '"==" operator, integer and string with the different values': [ + 2, + {operator: '==', compareValue: '3'}, + false + ], + '"==" operator, 2 identical strings': ['b', {operator: '==', compareValue: 'b'}, true], + // === + '"===" operator, 2 identical integers': [2, {operator: '===', compareValue: 2}, true], + '"===" operator, 2 different integers': [2, {operator: '===', compareValue: 3}, false], + '"===" operator, 2 identical decimal fractions': [2.5, {operator: '===', compareValue: 2.5}, true], + '"===" operator, integer and string with the same value': [2, {operator: '===', compareValue: '2'}, false], + '"===" operator, integer and string with the different values': [ + 2, + {operator: '===', compareValue: '3'}, + false + ], + '"===" operator, 2 identical strings': ['b', {operator: '===', compareValue: 'b'}, true], + // != + '"!=" operator, 2 identical integers': [2, {operator: '!=', compareValue: 2}, false], + '"!=" operator, 2 different integers': [2, {operator: '!=', compareValue: 3}, true], + '"!=" operator, 2 identical decimal fractions': [2.5, {operator: '!=', compareValue: 2.5}, false], + '"!=" operator, integer and string with the same value': [2, {operator: '!=', compareValue: '2'}, false], + '"!=" operator, integer and string with the different values': [ + 2, + {operator: '!=', compareValue: '3'}, + true + ], + '"!=" operator, 2 identical strings': ['b', {operator: '!=', compareValue: 'b'}, false], + // !== + '"!==" operator, 2 identical integers': [2, {operator: '!==', compareValue: 2}, false], + '"!==" operator, 2 different integers': [2, {operator: '!==', compareValue: 3}, true], + '"!==" operator, 2 identical decimal fractions': [2.5, {operator: '!==', compareValue: 2.5}, false], + '"!==" operator, integer and string with the same value': [2, {operator: '!==', compareValue: '2'}, true], + '"!==" operator, integer and string with the different values': [ + 2, + {operator: '!==', compareValue: '3'}, + true + ], + '"!==" operator, 2 identical strings': ['b', {operator: '!==', compareValue: 'b'}, false], + // > + '">" operator, 2 identical integers': [2, {operator: '>', compareValue: 2}, false], + '">" operator, 2 integers, 2nd is greater': [2, {operator: '>', compareValue: 3}, false], + '">" operator, 2 integers, 2nd is lower': [2, {operator: '>', compareValue: 1}, true], + '">" operator, 2 identical strings': ['b', {operator: '>', compareValue: 'b'}, false], + '">" operator, 2 strings, 2nd is greater': ['a', {operator: '>', compareValue: 'b'}, false], + '">" operator, 2 strings, 2nd is lower': ['b', {operator: '>', compareValue: 'a'}, true], + // >= + '">=" operator, 2 identical integers': [2, {operator: '>=', compareValue: 2}, true], + '">=" operator, 2 integers, 2nd is greater': [2, {operator: '>=', compareValue: 3}, false], + '">=" operator, 2 integers, 2nd is lower': [2, {operator: '>=', compareValue: 1}, true], + '">=" operator, 2 identical strings': ['b', {operator: '>=', compareValue: 'b'}, true], + '">=" operator, 2 strings, 2nd is greater': ['a', {operator: '>=', compareValue: 'b'}, false], + '">=" operator, 2 strings, 2nd is lower': ['b', {operator: '>=', compareValue: 'a'}, true], + // < + '"<" operator, 2 identical integers': [2, {operator: '<', compareValue: 2}, false], + '"<" operator, 2 integers, 2nd is greater': [2, {operator: '<', compareValue: 3}, true], + '"<" operator, 2 integers, 2nd is lower': [2, {operator: '<', compareValue: 1}, false], + '"<" operator, 2 identical strings': ['b', {operator: '<', compareValue: 'b'}, false], + '"<" operator, 2 strings, 2nd is greater': ['a', {operator: '<', compareValue: 'b'}, true], + '"<" operator, 2 strings, 2nd is lower': ['b', {operator: '<', compareValue: 'a'}, false], + '"<" operator, strings "10" and "2"': ['10', {operator: '<', compareValue: '2'}, true], + // <= + '"<=" operator, 2 identical integers': [2, {operator: '<=', compareValue: 2}, true], + '"<=" operator, 2 integers, 2nd is greater': [2, {operator: '<=', compareValue: 3}, true], + '"<=" operator, 2 integers, 2nd is lower': [2, {operator: '<=', compareValue: 1}, false], + '"<=" operator, 2 identical strings': ['b', {operator: '<=', compareValue: 'b'}, true], + '"<=" operator, 2 strings, 2nd is greater': ['a', {operator: '<=', compareValue: 'b'}, true], + '"<=" operator, 2 strings, 2nd is lower': ['b', {operator: '<=', compareValue: 'a'}, false], + // type + 'number type, "<" operator, strings "10" and "2"': [ + '10', + {operator: '<', compareValue: '2', type: 'number'}, + false + ], + // default compare value + 'default compare value, "===" operator, against undefined': [undefined, {operator: '==='}, true] + }, function (value, options, expectValid) { + it(getValidatorMessage(expectValid), function () { + options.message = 'Invalid value.'; + var messages = []; + var expectedMessages = expectValid ? [] : ['Invalid value.']; + + yii.validation.compare(value, messages, options); + assert.deepEqual(messages, expectedMessages); + + assert.isFalse(jQueryInitStub.called); + assert.isFalse(inputSpy.called); + }) + }); + + describe('with compareAttribute, "==" operator and 2 identical strings', function () { + it(VALIDATOR_SUCCESS_MESSAGE, function () { + var messages = []; + + yii.validation.compare('b', messages, {operator: '==', compareAttribute: 'input-id'}); + assert.deepEqual(messages, []); + + assert.isTrue(jQueryInitStub.calledOnce); + assert.equal(jQueryInitStub.getCall(0).args[0], '#input-id'); + + assert.isTrue(inputSpy.calledOnce); + assert.strictEqual(inputSpy.getCall(0).args[0], undefined); + }); + }); + }); + + describe('ip validator', function () { + var ipParsePattern = '^(\\!?)(.+?)(\/(\\d+))?$'; + var ipv4Pattern = '^(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?' + + '[0-9]?[0-9]))$'; + var ipv6Pattern = '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:)' + + '{1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}' + + '(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}' + + '(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|' + + 'fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}' + + '[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|' + + '(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$'; + var defaultOptions = { + messages: { + message: 'Invalid value.', + noSubnet: 'No subnet.', + hasSubnet: 'Has subnet.', + ipv4NotAllowed: 'IPv4 is not allowed.', + ipv6NotAllowed: 'IPv6 is not allowed.' + }, + 'ipParsePattern': ipParsePattern, + 'ipv4Pattern': ipv4Pattern, + 'ipv6Pattern': ipv6Pattern, + ipv4: true, + ipv6: true + }; + + withData({ + 'empty string, skip on empty': ['', {skipOnEmpty: true}, []], + 'not IP': ['not IP', {}, ['Invalid value.']], + // subnet, IPv4 + 'IPv4, subnet option is not defined': ['192.168.10.0', {}, []], + 'IPv4, subnet option is set to "false"': ['192.168.10.0', {subnet: false}, []], + 'IPv4, subnet option is set to "true"': ['192.168.10.0', {subnet: true}, ['No subnet.']], + 'IPv4 with CIDR subnet, subnet option is not defined': ['192.168.10.0/24', {}, []], + 'IPv4 with CIDR subnet, subnet option is set to "false"': [ + '192.168.10.0/24', + {subnet: false}, + ['Has subnet.'] + ], + 'IPv4 with CIDR subnet, subnet option is set to "true"': ['192.168.10.0/24', {subnet: true}, []], + // subnet, IPv6 + 'IPv6, subnet option is not defined': ['2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', {}, []], + 'IPv6, subnet option is set to "false"': ['2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', {subnet: false}, []], + 'IPv6, subnet option is set to "true"': [ + '2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', + {subnet: true}, + ['No subnet.'] + ], + 'IPv6 with CIDR subnet, subnet option is not defined': [ + '2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d/24', + {}, + [] + ], + 'IPv6 with CIDR subnet, subnet option is set to "false"': [ + '2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d/24', + {subnet: false}, + ['Has subnet.'] + ], + 'IPv6 with CIDR subnet, subnet option is set to "true"': [ + '2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d/24', + {subnet: true}, + [] + ], + // negation, IPv4 + 'IPv4, negation option is not defined': ['192.168.10.0', {}, []], + 'IPv4, negation option is set to "false"': ['192.168.10.0', {negation: false}, []], + 'IPv4, negation option is set to "true"': ['192.168.10.0', {negation: true}, []], + 'IPv4 with negation, negation option is not defined': ['!192.168.10.0', {}, []], + 'IPv4 with negation, negation option is set to "false"': [ + '!192.168.10.0', + {negation: false}, + ['Invalid value.'] + ], + 'IPv4 with negation, negation option is set to "true"': ['!192.168.10.0', {negation: true}, []], + // negation, IPv6 + 'IPv6, negation option is not defined': ['2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', {}, []], + 'IPv6, negation option is set to "false"': [ + '2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', + {negation: false}, + [] + ], + 'IPv6, negation option is set to "true"': ['2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', {negation: true}, []], + 'IPv6 with negation, negation option is not defined': ['!2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', {}, []], + 'IPv6 with negation, negation option is set to "false"': [ + '!2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', + {negation: false}, + ['Invalid value.'] + ], + 'IPv6 with negation, negation option is set to "true"': [ + '!2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', + {negation: true}, + [] + ], + // ipv4, ipv6 + 'IPv4, IPv4 option is set to "false"': ['192.168.10.0', {ipv4: false}, ['IPv4 is not allowed.']], + 'IPv6, IPv6 option is set to "false"': [ + '2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', + {ipv6: false}, + ['IPv6 is not allowed.'] + ], + 'IPv6, short variation (4 groups)': ['2001:db8::ae21:ad12', {}, []], + 'IPv6, short variation (2 groups)': ['::ae21:ad12', {}, []], + 'IPv4, IPv4 and IPv6 options are set to "false"': [ + '192.168.10.0', + {ipv4: false, ipv6: false}, + ['IPv4 is not allowed.'] + ], + 'IPv6, IPv4 and IPv6 options are set to "false"': [ + '2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', + {ipv4: false, ipv6: false}, + ['IPv6 is not allowed.'] + ], + 'invalid IPv4': ['192,168.10.0', {}, ['Invalid value.']], + 'invalid IPv6': ['2001,0db8:11a3:09d7:1f34:8a2e:07a0:765d', {}, ['Invalid value.']], + 'invalid IPv4, IPv4 option is set to "false"': [ + '192,168.10.0', + {ipv4: false}, + ['IPv4 is not allowed.', 'Invalid value.'] + ], + 'invalid IPv6, IPv6 option is set to "false"': [ + '2001,0db8:11a3:09d7:1f34:8a2e:07a0:765d', + {ipv6: false}, + ['IPv6 is not allowed.', 'Invalid value.'] + ] + }, function (value, customOptions, expectedMessages) { + it(getValidatorMessage(expectedMessages), function () { + var messages = []; + var options = $.extend({}, defaultOptions, customOptions); + + yii.validation.ip(value, messages, options); + assert.deepEqual(messages, expectedMessages); + }) + }); + }); +}); From 7f537d684e88f60f5d2b6423f18c9dd19269c1ec Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Thu, 1 Dec 2016 14:27:20 +0100 Subject: [PATCH 003/105] Custom ID generation for Pjax widgets to solve unstable ajax calls (issue #12969) Implements the solution proposed in the following comment: https://github.com/yiisoft/yii2/pull/12977#issuecomment-260123078 --- framework/widgets/Pjax.php | 23 +++++++++++++++++- tests/framework/widgets/PjaxTest.php | 35 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/framework/widgets/PjaxTest.php diff --git a/framework/widgets/Pjax.php b/framework/widgets/Pjax.php index fb4002efa2..98919e0e3e 100644 --- a/framework/widgets/Pjax.php +++ b/framework/widgets/Pjax.php @@ -97,7 +97,11 @@ class Pjax extends Widget * [pjax project page](https://github.com/yiisoft/jquery-pjax) for available options. */ public $clientOptions; - + /** + * @var string the prefix to the automatically generated pjax widget IDs. + * @see getId() + */ + public static $pjaxAutoIdPrefix = 'w_pjax_'; /** * @inheritdoc @@ -132,6 +136,23 @@ class Pjax extends Widget } } + /** + * Custom ID generation for Pjax widgets + * This override is to attain a more reliable match between ID's of local and remote Pjax widgets (#12969) + * The implementation follows the solution proposed in the following comment: + * @link https://github.com/yiisoft/yii2/pull/12977#issuecomment-260123078 + * @since 2.0.11 + */ + private static $pjaxCounter = 0; + private $_pjaxId; + public function getId($autoGenerate = true) + { + if ($autoGenerate && $this->_pjaxId === null) { + $this->_pjaxId = static::$pjaxAutoIdPrefix . static::$pjaxCounter++; + } + return $this->_pjaxId; + } + /** * @inheritdoc */ diff --git a/tests/framework/widgets/PjaxTest.php b/tests/framework/widgets/PjaxTest.php new file mode 100644 index 0000000000..182b6e8b77 --- /dev/null +++ b/tests/framework/widgets/PjaxTest.php @@ -0,0 +1,35 @@ + new ArrayDataProvider()]); + ob_start(); + $pjax1 = new Pjax(); + ob_end_clean(); + $nonPjaxWidget2 = new ListView(['dataProvider' => new ArrayDataProvider()]); + ob_start(); + $pjax2 = new Pjax(); + ob_end_clean(); + + $this->assertEquals('w0', $nonPjaxWidget1->options['id']); + $this->assertEquals('w1', $nonPjaxWidget2->options['id']); + $this->assertEquals('w_pjax_0', $pjax1->options['id']); + $this->assertEquals('w_pjax_1', $pjax2->options['id']); + } + + protected function setUp() + { + parent::setUp(); + $this->mockWebApplication(); + } + +} From 02af14e62f832cb9e77868db5cea08c872a92456 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Thu, 1 Dec 2016 11:41:52 +0100 Subject: [PATCH 004/105] move JS tests into a separate branch in travis matrix this avoids running JS tests for each PHP version. --- .travis.yml | 29 +++++++++++++++++++++++------ package.json | 1 - 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2114d1b618..0f95252285 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,6 +54,29 @@ matrix: services: - mysql - postgresql + # have a separate branch for javascript tests + - language: node_js + node_js: 6 + dist: trusty + # overwrite php related settings + php: + services: + addons: + install: + - travis_retry npm install + # disable xdebug for performance in composer + - phpenv config-rm xdebug.ini || echo "xdebug is not installed" + - travis_retry composer self-update && composer --version + - travis_retry composer global require "fxp/composer-asset-plugin:^1.2.0" + - travis_retry composer update --prefer-dist --no-interaction + before_script: + - node --version + - npm --version + - php --version + - composer --version + script: npm test + after_script: + allow_failures: - php: nightly @@ -86,8 +109,6 @@ install: - tests/data/travis/apc-setup.sh - tests/data/travis/memcache-setup.sh # - tests/data/travis/cubrid-setup.sh - # NPM packages for JS testing - - travis_retry npm install before_script: # show some versions and env information @@ -95,8 +116,6 @@ before_script: - php -r "echo INTL_ICU_DATA_VERSION . \"\n\";" - mysql --version - psql --version - - node --version - - npm --version # initialize databases - mysql -e 'CREATE DATABASE `yiitest`;'; @@ -117,8 +136,6 @@ script: - cd framework && composer validate --no-check-lock && cd .. # run PHPUnit - vendor/bin/phpunit --verbose $PHPUNIT_FLAGS --exclude-group mssql,oci,wincache,xcache,zenddata,cubrid - # run Mocha for JS testing - - npm test after_script: - | diff --git a/package.json b/package.json index 96e8e5036b..804d697417 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "yii2", - "version": "2.0.10", "description": "a modern PHP framework designed for professional Web development", "main": "index.js", "directories": { From 9218adcec21fe2d42c1c6ddbd07ca609928041fa Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Thu, 1 Dec 2016 15:34:03 +0100 Subject: [PATCH 005/105] fixes after @samdark review --- framework/widgets/Pjax.php | 28 ++++++++-------------------- tests/framework/widgets/PjaxTest.php | 4 ++-- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/framework/widgets/Pjax.php b/framework/widgets/Pjax.php index 98919e0e3e..316d0e6ebd 100644 --- a/framework/widgets/Pjax.php +++ b/framework/widgets/Pjax.php @@ -98,10 +98,15 @@ class Pjax extends Widget */ public $clientOptions; /** - * @var string the prefix to the automatically generated pjax widget IDs. - * @see getId() + * @inheritdoc + * Note: This variable must be redeclared to force separate counting for Pjax widgets (issue #12969) */ - public static $pjaxAutoIdPrefix = 'w_pjax_'; + public static $counter = 0; + /** + * @inheritdoc + */ + public static $autoIdPrefix = 'p'; + /** * @inheritdoc @@ -136,23 +141,6 @@ class Pjax extends Widget } } - /** - * Custom ID generation for Pjax widgets - * This override is to attain a more reliable match between ID's of local and remote Pjax widgets (#12969) - * The implementation follows the solution proposed in the following comment: - * @link https://github.com/yiisoft/yii2/pull/12977#issuecomment-260123078 - * @since 2.0.11 - */ - private static $pjaxCounter = 0; - private $_pjaxId; - public function getId($autoGenerate = true) - { - if ($autoGenerate && $this->_pjaxId === null) { - $this->_pjaxId = static::$pjaxAutoIdPrefix . static::$pjaxCounter++; - } - return $this->_pjaxId; - } - /** * @inheritdoc */ diff --git a/tests/framework/widgets/PjaxTest.php b/tests/framework/widgets/PjaxTest.php index 182b6e8b77..60ccea5087 100644 --- a/tests/framework/widgets/PjaxTest.php +++ b/tests/framework/widgets/PjaxTest.php @@ -22,8 +22,8 @@ class PjaxTest extends TestCase $this->assertEquals('w0', $nonPjaxWidget1->options['id']); $this->assertEquals('w1', $nonPjaxWidget2->options['id']); - $this->assertEquals('w_pjax_0', $pjax1->options['id']); - $this->assertEquals('w_pjax_1', $pjax2->options['id']); + $this->assertEquals('p0', $pjax1->options['id']); + $this->assertEquals('p1', $pjax2->options['id']); } protected function setUp() From 781ac2e139ade2397ec2d8141b7fc5b80b3b22b0 Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Thu, 1 Dec 2016 16:00:06 +0100 Subject: [PATCH 006/105] add changelog --- framework/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 5f8206d1be..d1cba34dd1 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -54,6 +54,7 @@ Yii Framework 2 Change Log - Enh #13074: Improved `\yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks) - Enh #13050: Added `yii\filters\HostControl` allowing protection against 'host header' attacks (klimov-paul) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) +- Bug: #12969: Improved unique ID generation for `yii\widgets\Pjax` widgets (dynasource,samdark,rob006) 2.0.10 October 20, 2016 ----------------------- From 5e12a5cc3a354ea232bd9538b9c2d824b108a036 Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Thu, 1 Dec 2016 16:00:31 +0100 Subject: [PATCH 007/105] added spaces --- framework/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index d1cba34dd1..c38e612eb4 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -54,7 +54,7 @@ Yii Framework 2 Change Log - Enh #13074: Improved `\yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks) - Enh #13050: Added `yii\filters\HostControl` allowing protection against 'host header' attacks (klimov-paul) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) -- Bug: #12969: Improved unique ID generation for `yii\widgets\Pjax` widgets (dynasource,samdark,rob006) +- Bug: #12969: Improved unique ID generation for `yii\widgets\Pjax` widgets (dynasource, samdark, rob006) 2.0.10 October 20, 2016 ----------------------- From 51786cb5957798b0d3fa93bb97fddcf90ef6f94f Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Thu, 1 Dec 2016 16:28:00 +0100 Subject: [PATCH 008/105] improved composer stability for the asset plugin --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0f95252285..730f2881e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,7 +67,7 @@ matrix: # disable xdebug for performance in composer - phpenv config-rm xdebug.ini || echo "xdebug is not installed" - travis_retry composer self-update && composer --version - - travis_retry composer global require "fxp/composer-asset-plugin:^1.2.0" + - travis_retry composer global require "fxp/composer-asset-plugin:^1.2.0" --no-plugins - travis_retry composer update --prefer-dist --no-interaction before_script: - node --version @@ -102,7 +102,7 @@ install: phpenv config-rm xdebug.ini || echo "xdebug is not installed" fi - travis_retry composer self-update && composer --version - - travis_retry composer global require "fxp/composer-asset-plugin:^1.2.0" + - travis_retry composer global require "fxp/composer-asset-plugin:^1.2.0" --no-plugins - export PATH="$HOME/.composer/vendor/bin:$PATH" # core framework: - travis_retry composer update --prefer-dist --no-interaction From d295812695b15f1d6c47370f3a9fd725ac29c4cc Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Thu, 1 Dec 2016 17:24:41 +0100 Subject: [PATCH 009/105] upgrade file updated --- framework/UPGRADE.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index d91e00d9d1..f111c83d61 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -51,6 +51,13 @@ version B between A and C, you need to follow the instructions for both A and B. +Upgrade from Yii 2.0.10 +---------------------- + +* PJAX: The value of the `$autoIdPrefix` property of `yii\widgets\Pjax` has been changed to `p`. If you have any PHP or Javascript +code dependant on this property (like i.e. tests or events), you should update these to match this new value. + + Upgrade from Yii 2.0.9 ---------------------- From 7b3f4095c2184a8b0d9d24fb0c143dec59bd8bdf Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Thu, 1 Dec 2016 17:57:11 +0100 Subject: [PATCH 010/105] added @since --- framework/widgets/Pjax.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/widgets/Pjax.php b/framework/widgets/Pjax.php index 316d0e6ebd..bc702b3bcc 100644 --- a/framework/widgets/Pjax.php +++ b/framework/widgets/Pjax.php @@ -99,11 +99,13 @@ class Pjax extends Widget public $clientOptions; /** * @inheritdoc - * Note: This variable must be redeclared to force separate counting for Pjax widgets (issue #12969) + * Note: This variable is redeclared to force separate counting for Pjax widgets (issue #12969) + * @since 2.0.11 */ public static $counter = 0; /** * @inheritdoc + * @since 2.0.11 */ public static $autoIdPrefix = 'p'; From 5784dd1dbf87dd01a68345996c5acc6b9679a887 Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Thu, 1 Dec 2016 22:48:40 +0100 Subject: [PATCH 011/105] fix travis --- framework/widgets/Pjax.php | 6 +++--- tests/framework/widgets/PjaxTest.php | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/framework/widgets/Pjax.php b/framework/widgets/Pjax.php index bc702b3bcc..10b6f1fa77 100644 --- a/framework/widgets/Pjax.php +++ b/framework/widgets/Pjax.php @@ -99,13 +99,13 @@ class Pjax extends Widget public $clientOptions; /** * @inheritdoc - * Note: This variable is redeclared to force separate counting for Pjax widgets (issue #12969) - * @since 2.0.11 + * @internal + * @since 2.0.11 (#12969) */ public static $counter = 0; /** * @inheritdoc - * @since 2.0.11 + * @since 2.0.11 (#12969) */ public static $autoIdPrefix = 'p'; diff --git a/tests/framework/widgets/PjaxTest.php b/tests/framework/widgets/PjaxTest.php index 60ccea5087..2018e9df96 100644 --- a/tests/framework/widgets/PjaxTest.php +++ b/tests/framework/widgets/PjaxTest.php @@ -11,6 +11,8 @@ class PjaxTest extends TestCase { public function testGeneratedIdByPjaxWidget() { + ListView::$counter = 0; + Pjax::$counter = 0; $nonPjaxWidget1 = new ListView(['dataProvider' => new ArrayDataProvider()]); ob_start(); $pjax1 = new Pjax(); From deea3cf798de7151c8e55c71a31865fdfab0f7c3 Mon Sep 17 00:00:00 2001 From: zalatov Date: Mon, 28 Nov 2016 10:06:54 +1000 Subject: [PATCH 012/105] Fixes #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions --- framework/CHANGELOG.md | 1 + .../web/RangeNotSatisfiableHttpException.php | 35 +++++++++++++++++++ framework/web/Response.php | 8 ++--- tests/framework/web/ResponseTest.php | 2 +- 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 framework/web/RangeNotSatisfiableHttpException.php diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 5f8206d1be..a47f24092f 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -54,6 +54,7 @@ Yii Framework 2 Change Log - Enh #13074: Improved `\yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks) - Enh #13050: Added `yii\filters\HostControl` allowing protection against 'host header' attacks (klimov-paul) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) +- Enh #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions (zalatov) 2.0.10 October 20, 2016 ----------------------- diff --git a/framework/web/RangeNotSatisfiableHttpException.php b/framework/web/RangeNotSatisfiableHttpException.php new file mode 100644 index 0000000000..c1ccbca572 --- /dev/null +++ b/framework/web/RangeNotSatisfiableHttpException.php @@ -0,0 +1,35 @@ + + * + * @since 2.0.11 + */ +class RangeNotSatisfiableHttpException extends HttpException +{ + /** + * Constructor. + * @param string $message error message + * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message = null, $code = 0, \Exception $previous = null) + { + parent::__construct(416, $message, $code, $previous); + } +} diff --git a/framework/web/Response.php b/framework/web/Response.php index 0d6b78465f..2ec4d6b0f4 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -482,7 +482,7 @@ class Response extends \yii\base\Response * meaning a download dialog will pop up. * * @return $this the response object itself - * @throws HttpException if the requested range is not satisfiable + * @throws RangeNotSatisfiableHttpException if the requested range is not satisfiable * @see sendFile() for an example implementation. */ public function sendContentAsFile($content, $attachmentName, $options = []) @@ -494,7 +494,7 @@ class Response extends \yii\base\Response if ($range === false) { $headers->set('Content-Range', "bytes */$contentLength"); - throw new HttpException(416, 'Requested range not satisfiable'); + throw new RangeNotSatisfiableHttpException(); } list($begin, $end) = $range; @@ -533,7 +533,7 @@ class Response extends \yii\base\Response * This option is available since version 2.0.4. * * @return $this the response object itself - * @throws HttpException if the requested range cannot be satisfied. + * @throws RangeNotSatisfiableHttpException if the requested range is not satisfiable * @see sendFile() for an example implementation. */ public function sendStreamAsFile($handle, $attachmentName, $options = []) @@ -549,7 +549,7 @@ class Response extends \yii\base\Response $range = $this->getHttpRange($fileSize); if ($range === false) { $headers->set('Content-Range', "bytes */$fileSize"); - throw new HttpException(416, 'Requested range not satisfiable'); + throw new RangeNotSatisfiableHttpException(); } list($begin, $end) = $range; diff --git a/tests/framework/web/ResponseTest.php b/tests/framework/web/ResponseTest.php index 8a9c22d988..26b0221cc2 100644 --- a/tests/framework/web/ResponseTest.php +++ b/tests/framework/web/ResponseTest.php @@ -71,7 +71,7 @@ class ResponseTest extends \yiiunit\TestCase */ public function testSendFileWrongRanges($rangeHeader) { - $this->setExpectedException('yii\web\HttpException'); + $this->setExpectedException('yii\web\RangeNotSatisfiableHttpException'); $dataFile = \Yii::getAlias('@yiiunit/data/web/data.txt'); $_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader; From 1141fc81a7f466590618eed40f843e9166556f9b Mon Sep 17 00:00:00 2001 From: SG5 Date: Fri, 25 Nov 2016 16:13:28 +0300 Subject: [PATCH 013/105] Fixes #12735: Fixed `yii\console\controllers\MigrateController` creating multiple primary keys for field `bigPrimaryKey:unsigned` --- framework/CHANGELOG.md | 1 + .../console/controllers/MigrateController.php | 2 +- .../migrate_create/create_unsigned_big_pk.php | 32 +++++++++++++++++++ .../migrate_create/create_unsigned_pk.php | 32 +++++++++++++++++++ .../controllers/MigrateControllerTest.php | 8 +++++ 5 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/data/console/migrate_create/create_unsigned_big_pk.php create mode 100644 tests/data/console/migrate_create/create_unsigned_pk.php diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index a47f24092f..9030302d8a 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -10,6 +10,7 @@ Yii Framework 2 Change Log - Bug #9796: Initialization of not existing `yii\grid\ActionColumn` default buttons (arogachev) - Bug #12681: Changed `data` column type from `text` to `blob` to handle null-byte (`\0`) in serialized RBAC rule properly (silverfire) - Bug #12714: Fixed `yii\validation\EmailValidator` to prevent false-positives checks when property `checkDns` is set to `true` (silverfire) +- Bug #12735: Fixed `yii\console\controllers\MigrateController` creating multiple primary keys for field `bigPrimaryKey:unsigned` (SG5) - Bug #12791: Fixed `yii\behaviors\AttributeTypecastBehavior` unable to automatically detect `attributeTypes`, triggering PHP Fatal Error (klimov-paul) - Bug #12803, #12921: Fixed BC break in `yii.activeForm.js` introduced in #11999. Reverted commit 3ba72da (silverfire) - Bug #12810: Fixed `yii\rbac\DbManager::getChildRoles()` and `yii\rbac\PhpManager::getChildRoles()` throws an exception when role has no child roles (mysterydragon) diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index a91c6220ef..2b9866afbf 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -462,7 +462,7 @@ class MigrateController extends BaseMigrateController protected function addDefaultPrimaryKey(&$fields) { foreach ($fields as $field) { - if ($field['decorators'] === 'primaryKey()' || $field['decorators'] === 'bigPrimaryKey()') { + if (false !== strripos($field['decorators'], 'primarykey()')) { return; } } diff --git a/tests/data/console/migrate_create/create_unsigned_big_pk.php b/tests/data/console/migrate_create/create_unsigned_big_pk.php new file mode 100644 index 0000000000..54b357423d --- /dev/null +++ b/tests/data/console/migrate_create/create_unsigned_big_pk.php @@ -0,0 +1,32 @@ +createTable('{table}', [ + 'brand_id' => \$this->bigPrimaryKey()->unsigned(), + ]); + } + + /** + * @inheritdoc + */ + public function down() + { + \$this->dropTable('{table}'); + } +} + +CODE; diff --git a/tests/data/console/migrate_create/create_unsigned_pk.php b/tests/data/console/migrate_create/create_unsigned_pk.php new file mode 100644 index 0000000000..1c6984326b --- /dev/null +++ b/tests/data/console/migrate_create/create_unsigned_pk.php @@ -0,0 +1,32 @@ +createTable('{table}', [ + 'brand_id' => \$this->primaryKey()->unsigned(), + ]); + } + + /** + * @inheritdoc + */ + public function down() + { + \$this->dropTable('{table}'); + } +} + +CODE; diff --git a/tests/framework/console/controllers/MigrateControllerTest.php b/tests/framework/console/controllers/MigrateControllerTest.php index c22cd74733..c95d1cb253 100644 --- a/tests/framework/console/controllers/MigrateControllerTest.php +++ b/tests/framework/console/controllers/MigrateControllerTest.php @@ -95,6 +95,14 @@ class MigrateControllerTest extends TestCase 'fields' => 'title:primaryKey,body:text:notNull,price:money(11,2)', ]); + $this->assertCommandCreatedFile('create_unsigned_pk', $migrationName, $table, [ + 'fields' => 'brand_id:primaryKey:unsigned', + ]); + + $this->assertCommandCreatedFile('create_unsigned_big_pk', $migrationName, $table, [ + 'fields' => 'brand_id:bigPrimaryKey:unsigned', + ]); + $this->assertCommandCreatedFile('create_id_pk', $migrationName, $table, [ 'fields' => 'id:primaryKey, address:string, From fb779e3ec61bae96c3e8abdf2b6173408c51d826 Mon Sep 17 00:00:00 2001 From: "a.kompaniets" Date: Mon, 3 Oct 2016 00:14:15 +0300 Subject: [PATCH 014/105] Fixes #12399: Added `ActiveField::addAriaAttributes` property for `aria-required` and `aria-invalid` attributes rendering --- framework/CHANGELOG.md | 1 + framework/assets/yii.activeForm.js | 16 ++++- framework/widgets/ActiveField.php | 66 ++++++++++++++++++++- tests/framework/widgets/ActiveFieldTest.php | 53 +++++++++++++++++ 4 files changed, 133 insertions(+), 3 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 9030302d8a..95e6cb045b 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -61,6 +61,7 @@ Yii Framework 2 Change Log ----------------------- - Bug #7670: Added `yii\web\UrlNormalizer` for normalizing requests with and without trailing slashes (rob006, cronfy, klimov-paul) +- Enh #12399 Added `ActiveField::addAriaAttributes` property for `aria-required` and `aria-invalid` attributes render - Bug #7670: Added `UrlNormalizer` for normalizing requests with and without trailing slashes (rob006, cronfy, klimov-paul) - Bug #9027: Fixed descendant class of `yii\web\UploadedFile` returns parent instances in case invoked after it (andrewnester) - Bug #9277: Fixed `yii\console\controllers\AssetController` looses custom options of 'target' bundles (petrabarus, klimov-paul) diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index 795d18e53b..0fbb4a6f00 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -168,7 +168,9 @@ // whether the validation is cancelled by beforeValidateAttribute event handler cancelled: false, // the value of the input - value: undefined + value: undefined, + // whether to toggle aria-invalid attribute after validation + ariaInvalidToggle: false }; @@ -707,6 +709,7 @@ hasError = messages[attribute.id].length > 0; var $container = $form.find(attribute.container); var $error = $container.find(attribute.error); + toggleAriaInvalid($form, attribute, hasError); if (hasError) { if (attribute.encodeError) { $error.text(messages[attribute.id][0]); @@ -775,4 +778,15 @@ } }; + var toggleAriaInvalid = function($form, attribute, hasError) { + var $input; + if (attribute.ariaInvalidToggle) { + $input = $form.find(attribute.input); + if (hasError) { + $input.attr('aria-invalid', 'true'); + } else { + $input.attr('aria-invalid', 'false'); + } + } + } })(window.jQuery); diff --git a/framework/widgets/ActiveField.php b/framework/widgets/ActiveField.php index eb946f442e..8e14cf93f2 100644 --- a/framework/widgets/ActiveField.php +++ b/framework/widgets/ActiveField.php @@ -147,6 +147,10 @@ class ActiveField extends Component * it is maintained by various methods of this class. */ public $parts = []; + /** + * @var bool adds aria HTML attributes `aria-required` and `aria-invalid` for inputs + */ + public $addAriaAttributes = false; /** * @var string this property holds a custom input id if it was set using [[inputOptions]] or in one of the @@ -158,6 +162,11 @@ class ActiveField extends Component */ private $_skipLabelFor = false; + /** + * @var bool whether `aria-invalid` HTML attribute should be toggled by validation on client + */ + private $_skipClientAriaInvalid = true; + /** * PHP magic method that returns the string representation of this object. @@ -358,6 +367,7 @@ class ActiveField extends Component public function input($type, $options = []) { $options = array_merge($this->inputOptions, $options); + $this->addAriaAttributes($options); $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeInput($type, $this->model, $this->attribute, $options); @@ -384,6 +394,7 @@ class ActiveField extends Component public function textInput($options = []) { $options = array_merge($this->inputOptions, $options); + $this->addAriaAttributes($options); $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeTextInput($this->model, $this->attribute, $options); @@ -429,6 +440,7 @@ class ActiveField extends Component public function passwordInput($options = []) { $options = array_merge($this->inputOptions, $options); + $this->addAriaAttributes($options); $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activePasswordInput($this->model, $this->attribute, $options); @@ -456,6 +468,7 @@ class ActiveField extends Component if (!isset($this->form->options['enctype'])) { $this->form->options['enctype'] = 'multipart/form-data'; } + $this->addAriaAttributes($options); $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeFileInput($this->model, $this->attribute, $options); @@ -475,6 +488,7 @@ class ActiveField extends Component public function textarea($options = []) { $options = array_merge($this->inputOptions, $options); + $this->addAriaAttributes($options); $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeTextarea($this->model, $this->attribute, $options); @@ -522,6 +536,7 @@ class ActiveField extends Component $options['label'] = null; $this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options); } + $this->addAriaAttributes($options); $this->adjustLabelFor($options); return $this; @@ -568,6 +583,7 @@ class ActiveField extends Component $options['label'] = null; $this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options); } + $this->addAriaAttributes($options); $this->adjustLabelFor($options); return $this; @@ -595,6 +611,7 @@ class ActiveField extends Component public function dropDownList($items, $options = []) { $options = array_merge($this->inputOptions, $options); + $this->addAriaAttributes($options); $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeDropDownList($this->model, $this->attribute, $items, $options); @@ -623,6 +640,7 @@ class ActiveField extends Component public function listBox($items, $options = []) { $options = array_merge($this->inputOptions, $options); + $this->addAriaAttributes($options); $this->adjustLabelFor($options); $this->parts['{input}'] = Html::activeListBox($this->model, $this->attribute, $items, $options); @@ -642,6 +660,7 @@ class ActiveField extends Component */ public function checkboxList($items, $options = []) { + $this->addAriaAttributes($options); $this->adjustLabelFor($options); $this->_skipLabelFor = true; $this->parts['{input}'] = Html::activeCheckboxList($this->model, $this->attribute, $items, $options); @@ -661,6 +680,7 @@ class ActiveField extends Component */ public function radioList($items, $options = []) { + $this->addAriaAttributes($options); $this->adjustLabelFor($options); $this->_skipLabelFor = true; $this->parts['{input}'] = Html::activeRadioList($this->model, $this->attribute, $items, $options); @@ -699,6 +719,7 @@ class ActiveField extends Component $config['attribute'] = $this->attribute; $config['view'] = $this->form->getView(); if (isset($config['options']) && isset(class_parents($class)['yii\widgets\InputWidget'])) { + $this->addAriaAttributes($config['options']); $this->adjustLabelFor($config['options']); } $this->parts['{input}'] = $class::widget($config); @@ -732,8 +753,8 @@ class ActiveField extends Component return []; } - $enableClientValidation = $this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation; - $enableAjaxValidation = $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation; + $enableClientValidation = $this->getEnableClientValidation(); + $enableAjaxValidation = $this->getEnableAjaxValidation(); if ($enableClientValidation) { $validators = []; @@ -781,6 +802,10 @@ class ActiveField extends Component $options['validate'] = new JsExpression("function (attribute, value, messages, deferred, \$form) {" . implode('', $validators) . '}'); } + if ($this->addAriaAttributes && !$this->_skipClientAriaInvalid) { + $options['ariaInvalidToggle'] = true; + } + // only get the options that are different from the default ones (set in yii.activeForm.js) return array_diff_assoc($options, [ 'validateOnChange' => true, @@ -789,9 +814,29 @@ class ActiveField extends Component 'validationDelay' => 500, 'encodeError' => true, 'error' => '.help-block', + 'ariaInvalidToggle' => false, ]); } + /** + * Checks if client validation enabled for the field + * @return bool + */ + protected function getEnableClientValidation() + { + return $this->enableClientValidation + || $this->enableClientValidation === null && $this->form->enableClientValidation; + } + + /** + * Checks if ajax validation enabled for the field + * @return bool + */ + protected function getEnableAjaxValidation() + { + return $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation; + } + /** * Returns the HTML `id` of the input element of this form field. * @return string the input id. @@ -801,4 +846,21 @@ class ActiveField extends Component { return $this->_inputId ?: Html::getInputId($this->model, $this->attribute); } + + /** + * Adds aria attributes to the input options + * @param $options array input options + */ + protected function addAriaAttributes(&$options) + { + if ($this->addAriaAttributes) { + if (!isset($options['aria-required'])) { + $options['aria-required'] = $this->model->isAttributeRequired($this->attribute) ? 'true' : 'false'; + } + if (!isset($options['aria-invalid'])) { + $options['aria-invalid'] = $this->model->hasErrors($this->attribute) ? 'true' : 'false'; + $this->_skipClientAriaInvalid = false; + } + } + } } diff --git a/tests/framework/widgets/ActiveFieldTest.php b/tests/framework/widgets/ActiveFieldTest.php index b8199ae629..a7bd5d1966 100644 --- a/tests/framework/widgets/ActiveFieldTest.php +++ b/tests/framework/widgets/ActiveFieldTest.php @@ -444,6 +444,59 @@ EOD; ], $actualValue); } + public function testAriaAttributes() + { + $this->activeField->addAriaAttributes = true; + + $expectedValue = << + + +
Hint for attributeName attribute
+
+
+EOD; + + $actualValue = $this->activeField->render(); + $this->assertEqualsWithoutLE($expectedValue, $actualValue); + } + + public function testAriaRequiredAttribute() + { + $this->activeField->addAriaAttributes = true; + $this->helperModel->addRule([$this->attributeName], 'required'); + + $expectedValue = << + + +
Hint for attributeName attribute
+
+
+EOD; + + $actualValue = $this->activeField->render(); + $this->assertEqualsWithoutLE($expectedValue, $actualValue); + } + + public function testAriaInvalidAttribute() + { + $this->activeField->addAriaAttributes = true; + $this->helperModel->addError($this->attributeName, 'Some error'); + + $expectedValue = << + + +
Hint for attributeName attribute
+
Some error
+
+EOD; + + $actualValue = $this->activeField->render(); + $this->assertEqualsWithoutLE($expectedValue, $actualValue); + } + /** * Helper methods */ From ed8acc8e035b99955a3d34cdd3b9dc8ed227d2c2 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 2 Dec 2016 01:44:57 +0300 Subject: [PATCH 015/105] #12399 adjustments --- framework/CHANGELOG.md | 4 ++-- framework/assets/yii.activeForm.js | 10 ++-------- framework/widgets/ActiveField.php | 20 +++++++++++++------- tests/framework/widgets/ActiveFieldTest.php | 6 +++--- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 95e6cb045b..95bd824d95 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -36,6 +36,7 @@ Yii Framework 2 Change Log - Enh #11756: Added type mapping for `varbinary` data type in MySQL DBMS (silverfire) - Enh #11929: Changed `type` column type from `int` to `smallInt` in RBAC migrations (silverfire) - Enh #12015: Changed visibility `yii\db\ActiveQueryTrait::createModels()` from private to protected (ArekX, dynasource) +- Enh #12399: Added `ActiveField::addAriaAttributes` property for `aria-required` and `aria-invalid` attributes rendering (Oxyaction, samdark) - Enh #12619: Added catch `Throwable` in `yii\base\ErrorHandler::handleException()` (rob006) - Enh #12726: `yii\base\Application::$version` converted to `yii\base\Module::$version` virtual property, allowing to specify version as a PHP callback (klimov-paul) - Enh #12738: Added support for creating protocol-relative URLs in `UrlManager::createAbsoluteUrl()` and `Url` helper methods (rob006) @@ -60,8 +61,7 @@ Yii Framework 2 Change Log 2.0.10 October 20, 2016 ----------------------- -- Bug #7670: Added `yii\web\UrlNormalizer` for normalizing requests with and without trailing slashes (rob006, cronfy, klimov-paul) -- Enh #12399 Added `ActiveField::addAriaAttributes` property for `aria-required` and `aria-invalid` attributes render +- Bug #7670: Added `yii\web\UrlNormalizer` for normalizing requests with and without trailing slashes (rob006, cronfy, klimov-paul) - Bug #7670: Added `UrlNormalizer` for normalizing requests with and without trailing slashes (rob006, cronfy, klimov-paul) - Bug #9027: Fixed descendant class of `yii\web\UploadedFile` returns parent instances in case invoked after it (andrewnester) - Bug #9277: Fixed `yii\console\controllers\AssetController` looses custom options of 'target' bundles (petrabarus, klimov-paul) diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index 0fbb4a6f00..b8d86a8c20 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -170,7 +170,7 @@ // the value of the input value: undefined, // whether to toggle aria-invalid attribute after validation - ariaInvalidToggle: false + ariaInvalidToggle: true }; @@ -779,14 +779,8 @@ }; var toggleAriaInvalid = function($form, attribute, hasError) { - var $input; if (attribute.ariaInvalidToggle) { - $input = $form.find(attribute.input); - if (hasError) { - $input.attr('aria-invalid', 'true'); - } else { - $input.attr('aria-invalid', 'false'); - } + $form.find(attribute.input).attr('aria-invalid', hasError ? 'true' : 'false'); } } })(window.jQuery); diff --git a/framework/widgets/ActiveField.php b/framework/widgets/ActiveField.php index 8e14cf93f2..1035fb93de 100644 --- a/framework/widgets/ActiveField.php +++ b/framework/widgets/ActiveField.php @@ -149,8 +149,9 @@ class ActiveField extends Component public $parts = []; /** * @var bool adds aria HTML attributes `aria-required` and `aria-invalid` for inputs + * @since 2.0.11 */ - public $addAriaAttributes = false; + public $addAriaAttributes = true; /** * @var string this property holds a custom input id if it was set using [[inputOptions]] or in one of the @@ -802,8 +803,8 @@ class ActiveField extends Component $options['validate'] = new JsExpression("function (attribute, value, messages, deferred, \$form) {" . implode('', $validators) . '}'); } - if ($this->addAriaAttributes && !$this->_skipClientAriaInvalid) { - $options['ariaInvalidToggle'] = true; + if ($this->addAriaAttributes === false && !$this->_skipClientAriaInvalid) { + $options['ariaInvalidToggle'] = false; } // only get the options that are different from the default ones (set in yii.activeForm.js) @@ -814,13 +815,14 @@ class ActiveField extends Component 'validationDelay' => 500, 'encodeError' => true, 'error' => '.help-block', - 'ariaInvalidToggle' => false, + 'ariaInvalidToggle' => true, ]); } /** * Checks if client validation enabled for the field * @return bool + * @since 2.0.11 */ protected function getEnableClientValidation() { @@ -831,6 +833,7 @@ class ActiveField extends Component /** * Checks if ajax validation enabled for the field * @return bool + * @since 2.0.11 */ protected function getEnableAjaxValidation() { @@ -850,15 +853,18 @@ class ActiveField extends Component /** * Adds aria attributes to the input options * @param $options array input options + * @since 2.0.11 */ protected function addAriaAttributes(&$options) { if ($this->addAriaAttributes) { - if (!isset($options['aria-required'])) { - $options['aria-required'] = $this->model->isAttributeRequired($this->attribute) ? 'true' : 'false'; + if (!isset($options['aria-required']) && $this->model->isAttributeRequired($this->attribute)) { + $options['aria-required'] = 'true'; } if (!isset($options['aria-invalid'])) { - $options['aria-invalid'] = $this->model->hasErrors($this->attribute) ? 'true' : 'false'; + if ($this->model->hasErrors($this->attribute)) { + $options['aria-invalid'] = 'true'; + } $this->_skipClientAriaInvalid = false; } } diff --git a/tests/framework/widgets/ActiveFieldTest.php b/tests/framework/widgets/ActiveFieldTest.php index a7bd5d1966..07d750890d 100644 --- a/tests/framework/widgets/ActiveFieldTest.php +++ b/tests/framework/widgets/ActiveFieldTest.php @@ -451,7 +451,7 @@ EOD; $expectedValue = << - +
Hint for attributeName attribute
@@ -469,7 +469,7 @@ EOD; $expectedValue = << - +
Hint for attributeName attribute
@@ -487,7 +487,7 @@ EOD; $expectedValue = << - +
Hint for attributeName attribute
Some error
From 65c2ade8ed66b61967b08b36be918e1930dbd90d Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Fri, 2 Dec 2016 10:04:59 +0200 Subject: [PATCH 016/105] Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX Fixes #13118 --- framework/CHANGELOG.md | 1 + framework/assets/yii.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 9030302d8a..632335a2d2 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -27,6 +27,7 @@ Yii Framework 2 Change Log - Bug #13071: Help option for commands was not working in modules (arogachev, haimanman) - Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov) - Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) +- Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) - Enh #6809: Added `\yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) - Enh #7333: Improved error message for `yii\di\Instance::ensure()` when a component does not exist (cebe) - Enh #7420: Attributes for prompt generated with `renderSelectOptions` of `\yii\helpers\Html` helper (arogachev) diff --git a/framework/assets/yii.js b/framework/assets/yii.js index e2db617a38..a73c296787 100644 --- a/framework/assets/yii.js +++ b/framework/assets/yii.js @@ -164,7 +164,7 @@ window.yii = (function ($) { pjaxContainer, pjaxOptions = {}; - if (pjax !== undefined && $.support.pjax) { + if (pjax !== undefined && pjax !== 0 && $.support.pjax) { if ($e.data('pjax-container')) { pjaxContainer = $e.data('pjax-container'); } else { From fec8ae2e1eddccb5f468e830a35cf23eac5bbaf0 Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Fri, 2 Dec 2016 09:05:10 +0100 Subject: [PATCH 017/105] removed @since --- framework/widgets/Pjax.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/framework/widgets/Pjax.php b/framework/widgets/Pjax.php index 10b6f1fa77..ee109c5a88 100644 --- a/framework/widgets/Pjax.php +++ b/framework/widgets/Pjax.php @@ -100,12 +100,10 @@ class Pjax extends Widget /** * @inheritdoc * @internal - * @since 2.0.11 (#12969) */ public static $counter = 0; /** * @inheritdoc - * @since 2.0.11 (#12969) */ public static $autoIdPrefix = 'p'; From 9807b2e1a1ac1c671c860ab448eab8bf9154b2bf Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Fri, 2 Dec 2016 10:09:08 +0200 Subject: [PATCH 018/105] Follow-up to 65c2ade. Smarter fix of #13118 --- framework/assets/yii.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/framework/assets/yii.js b/framework/assets/yii.js index a73c296787..372ae45ae4 100644 --- a/framework/assets/yii.js +++ b/framework/assets/yii.js @@ -153,7 +153,7 @@ window.yii = (function ($) { method = !$e.data('method') && $form ? $form.attr('method') : $e.data('method'), action = $e.attr('href'), params = $e.data('params'), - pjax = $e.data('pjax'), + pjax = $e.data('pjax') || 0, pjaxPushState = !!$e.data('pjax-push-state'), pjaxReplaceState = !!$e.data('pjax-replace-state'), pjaxTimeout = $e.data('pjax-timeout'), @@ -164,7 +164,7 @@ window.yii = (function ($) { pjaxContainer, pjaxOptions = {}; - if (pjax !== undefined && pjax !== 0 && $.support.pjax) { + if (pjax !== 0 && $.support.pjax) { if ($e.data('pjax-container')) { pjaxContainer = $e.data('pjax-container'); } else { @@ -190,13 +190,13 @@ window.yii = (function ($) { if (method === undefined) { if (action && action != '#') { - if (pjax !== undefined && $.support.pjax) { + if (pjax !== 0 && $.support.pjax) { $.pjax.click(event, pjaxOptions); } else { window.location = action; } } else if ($e.is(':submit') && $form.length) { - if (pjax !== undefined && $.support.pjax) { + if (pjax !== 0 && $.support.pjax) { $form.on('submit',function(e){ $.pjax.submit(e, pjaxOptions); }) @@ -249,7 +249,7 @@ window.yii = (function ($) { oldAction = $form.attr('action'); $form.attr('action', action); } - if (pjax !== undefined && $.support.pjax) { + if (pjax !== 0 && $.support.pjax) { $form.on('submit',function(e){ $.pjax.submit(e, pjaxOptions); }) From b25ebaa782599687a559a957455cb7e5bba36fa7 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 2 Dec 2016 13:11:34 +0300 Subject: [PATCH 019/105] Fixed formatting --- framework/CHANGELOG.md | 2 +- framework/assets/yii.activeForm.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 95bd824d95..ad82444bfe 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -61,7 +61,7 @@ Yii Framework 2 Change Log 2.0.10 October 20, 2016 ----------------------- -- Bug #7670: Added `yii\web\UrlNormalizer` for normalizing requests with and without trailing slashes (rob006, cronfy, klimov-paul) +- Bug #7670: Added `yii\web\UrlNormalizer` for normalizing requests with and without trailing slashes (rob006, cronfy, klimov-paul) - Bug #7670: Added `UrlNormalizer` for normalizing requests with and without trailing slashes (rob006, cronfy, klimov-paul) - Bug #9027: Fixed descendant class of `yii\web\UploadedFile` returns parent instances in case invoked after it (andrewnester) - Bug #9277: Fixed `yii\console\controllers\AssetController` looses custom options of 'target' bundles (petrabarus, klimov-paul) diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index b8d86a8c20..d12270bd73 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -778,7 +778,7 @@ } }; - var toggleAriaInvalid = function($form, attribute, hasError) { + var toggleAriaInvalid = function ($form, attribute, hasError) { if (attribute.ariaInvalidToggle) { $form.find(attribute.input).attr('aria-invalid', hasError ? 'true' : 'false'); } From dc294ba56fb462fdf73ca2a99f27c9a22c51307c Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 2 Dec 2016 14:30:13 +0300 Subject: [PATCH 020/105] Renamed variables, changed logic of turning client updates on --- framework/assets/yii.activeForm.js | 4 ++-- framework/widgets/ActiveField.php | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index d12270bd73..a37e727872 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -170,7 +170,7 @@ // the value of the input value: undefined, // whether to toggle aria-invalid attribute after validation - ariaInvalidToggle: true + updateAriaInvalid: true }; @@ -779,7 +779,7 @@ }; var toggleAriaInvalid = function ($form, attribute, hasError) { - if (attribute.ariaInvalidToggle) { + if (attribute.updateAriaInvalid) { $form.find(attribute.input).attr('aria-invalid', hasError ? 'true' : 'false'); } } diff --git a/framework/widgets/ActiveField.php b/framework/widgets/ActiveField.php index 1035fb93de..7ce67ecbf3 100644 --- a/framework/widgets/ActiveField.php +++ b/framework/widgets/ActiveField.php @@ -164,9 +164,9 @@ class ActiveField extends Component private $_skipLabelFor = false; /** - * @var bool whether `aria-invalid` HTML attribute should be toggled by validation on client + * @var bool whether `aria-invalid` HTML attribute should be updated by client validation */ - private $_skipClientAriaInvalid = true; + private $_updateAriaInvalidOnClient = true; /** @@ -803,8 +803,8 @@ class ActiveField extends Component $options['validate'] = new JsExpression("function (attribute, value, messages, deferred, \$form) {" . implode('', $validators) . '}'); } - if ($this->addAriaAttributes === false && !$this->_skipClientAriaInvalid) { - $options['ariaInvalidToggle'] = false; + if ($this->addAriaAttributes === false || !$this->_updateAriaInvalidOnClient) { + $options['updateAriaInvalid'] = false; } // only get the options that are different from the default ones (set in yii.activeForm.js) @@ -815,7 +815,7 @@ class ActiveField extends Component 'validationDelay' => 500, 'encodeError' => true, 'error' => '.help-block', - 'ariaInvalidToggle' => true, + 'updateAriaInvalid' => true, ]); } @@ -865,7 +865,7 @@ class ActiveField extends Component if ($this->model->hasErrors($this->attribute)) { $options['aria-invalid'] = 'true'; } - $this->_skipClientAriaInvalid = false; + $this->_updateAriaInvalidOnClient = false; } } } From edfbab4a6b067a31c93fa5a6fcf512f15bed2462 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 2 Dec 2016 14:37:13 +0300 Subject: [PATCH 021/105] More renaming and formatting fix --- framework/widgets/ActiveField.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/framework/widgets/ActiveField.php b/framework/widgets/ActiveField.php index 7ce67ecbf3..16e3a084bd 100644 --- a/framework/widgets/ActiveField.php +++ b/framework/widgets/ActiveField.php @@ -754,10 +754,10 @@ class ActiveField extends Component return []; } - $enableClientValidation = $this->getEnableClientValidation(); - $enableAjaxValidation = $this->getEnableAjaxValidation(); + $clientValidation = $this->clientValidationEnabled(); + $ajaxValidation = $this->ajaxValidationEnabled(); - if ($enableClientValidation) { + if ($clientValidation) { $validators = []; foreach ($this->model->getActiveValidators($attribute) as $validator) { /* @var $validator \yii\validators\Validator */ @@ -771,7 +771,7 @@ class ActiveField extends Component } } - if (!$enableAjaxValidation && (!$enableClientValidation || empty($validators))) { + if (!$ajaxValidation && (!$clientValidation || empty($validators))) { return []; } @@ -792,7 +792,7 @@ class ActiveField extends Component } $options['encodeError'] = !isset($this->errorOptions['encode']) || $this->errorOptions['encode']; - if ($enableAjaxValidation) { + if ($ajaxValidation) { $options['enableAjaxValidation'] = true; } foreach (['validateOnChange', 'validateOnBlur', 'validateOnType', 'validationDelay'] as $name) { @@ -824,10 +824,9 @@ class ActiveField extends Component * @return bool * @since 2.0.11 */ - protected function getEnableClientValidation() + protected function clientValidationEnabled() { - return $this->enableClientValidation - || $this->enableClientValidation === null && $this->form->enableClientValidation; + return $this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation; } /** @@ -835,7 +834,7 @@ class ActiveField extends Component * @return bool * @since 2.0.11 */ - protected function getEnableAjaxValidation() + protected function ajaxValidationEnabled() { return $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation; } From de4f518b94875cb2b651bc3d07116cdd0d9aa4df Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 2 Dec 2016 14:38:44 +0300 Subject: [PATCH 022/105] More renaming --- framework/assets/yii.activeForm.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index a37e727872..789cb27954 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -169,7 +169,7 @@ cancelled: false, // the value of the input value: undefined, - // whether to toggle aria-invalid attribute after validation + // whether to update aria-invalid attribute after validation updateAriaInvalid: true }; @@ -709,7 +709,7 @@ hasError = messages[attribute.id].length > 0; var $container = $form.find(attribute.container); var $error = $container.find(attribute.error); - toggleAriaInvalid($form, attribute, hasError); + updateAriaInvalid($form, attribute, hasError); if (hasError) { if (attribute.encodeError) { $error.text(messages[attribute.id][0]); @@ -778,7 +778,7 @@ } }; - var toggleAriaInvalid = function ($form, attribute, hasError) { + var updateAriaInvalid = function ($form, attribute, hasError) { if (attribute.updateAriaInvalid) { $form.find(attribute.input).attr('aria-invalid', hasError ? 'true' : 'false'); } From c17766181feca90a7642ffa4fcd7c52655fc4490 Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Mon, 10 Oct 2016 15:35:07 +0300 Subject: [PATCH 023/105] Added `QueryInterface::emulateExecution()` Added `QueryInterface::emulateExecution()`, which allows preventing of the actual query execution. This allows to cancel `DataProvider` preventing search query execution in case search model is invalid: ``` php public function search($params) { $query = Item::find(); $dataProvider = new ActiveDataProvider([ 'query' => $query, ]); $this->load($params); if (!$this->validate()) { $query->where('0=1'); $query->emulateExecution(); // No SQL execution will be done return $dataProvider; } ``` This also fix unecessary query in case of `via()` usage. See #12390. fixes #12390 fixes #6373 close #12708 --- framework/CHANGELOG.md | 6 ++- framework/UPGRADE.md | 6 +++ framework/db/ActiveRelationTrait.php | 6 +++ framework/db/Query.php | 29 ++++++++++ framework/db/QueryInterface.php | 12 +++++ framework/db/QueryTrait.php | 22 ++++++++ tests/framework/db/ActiveRecordTest.php | 66 +++++++++++++++++++++++ tests/framework/db/QueryTest.php | 70 +++++++++++++++++++++++++ 8 files changed, 215 insertions(+), 2 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 632335a2d2..3c263771a5 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -28,7 +28,8 @@ Yii Framework 2 Change Log - Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov) - Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) -- Enh #6809: Added `\yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) +- Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) +- Enh #6809: Added `yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) - Enh #7333: Improved error message for `yii\di\Instance::ensure()` when a component does not exist (cebe) - Enh #7420: Attributes for prompt generated with `renderSelectOptions` of `\yii\helpers\Html` helper (arogachev) - Enh #9162: Added support of closures in `value` for attributes in `yii\widgets\DetailView` (arogachev) @@ -37,6 +38,7 @@ Yii Framework 2 Change Log - Enh #11756: Added type mapping for `varbinary` data type in MySQL DBMS (silverfire) - Enh #11929: Changed `type` column type from `int` to `smallInt` in RBAC migrations (silverfire) - Enh #12015: Changed visibility `yii\db\ActiveQueryTrait::createModels()` from private to protected (ArekX, dynasource) +- Enh #12390: Avoid creating queries with false where contdition (`0=1`) when fetching relational data (klimov-paul) - Enh #12619: Added catch `Throwable` in `yii\base\ErrorHandler::handleException()` (rob006) - Enh #12726: `yii\base\Application::$version` converted to `yii\base\Module::$version` virtual property, allowing to specify version as a PHP callback (klimov-paul) - Enh #12738: Added support for creating protocol-relative URLs in `UrlManager::createAbsoluteUrl()` and `Url` helper methods (rob006) @@ -53,7 +55,7 @@ Yii Framework 2 Change Log - Enh #13036: Added shortcut methods `asJson()` and `asXml()` for returning JSON and XML data in web controller actions (cebe) - Enh #13020: Added `disabledListItemSubTagOptions` attribute for `yii\widgets\LinkPager` in order to customize the disabled list item sub tag element (nadar) - Enh #12988: Changed `textarea` method within the `yii\helpers\BaseHtml` class to allow users to control whether html entities found within `$value` will be double-encoded or not (cyphix333) -- Enh #13074: Improved `\yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks) +- Enh #13074: Improved `yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks) - Enh #13050: Added `yii\filters\HostControl` allowing protection against 'host header' attacks (klimov-paul) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) - Enh #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions (zalatov) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index d91e00d9d1..7c67fd70a2 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -50,6 +50,12 @@ if you want to upgrade from version A to version C and there is version B between A and C, you need to follow the instructions for both A and B. +Upgrade from Yii 2.0.10 +----------------------- + +* A new method `public function emulateExecution($value = true);` has been added to the `yii\db\QueryInterace`. + This method is implemented in the `yii\db\QueryTrait`, so this only affects your code if you implement QueryInterface + in a class that does not use the trait. Upgrade from Yii 2.0.9 ---------------------- diff --git a/framework/db/ActiveRelationTrait.php b/framework/db/ActiveRelationTrait.php index b8c5241160..4d9caf3d4f 100644 --- a/framework/db/ActiveRelationTrait.php +++ b/framework/db/ActiveRelationTrait.php @@ -464,6 +464,9 @@ trait ActiveRelationTrait } } } + if (empty($values)) { + $this->emulateExecution(); + } } else { // composite keys @@ -478,6 +481,9 @@ trait ActiveRelationTrait $v[$attribute] = $model[$link]; } $values[] = $v; + if (empty($v)) { + $this->emulateExecution(); + } } } $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]); diff --git a/framework/db/Query.php b/framework/db/Query.php index 6fe9bfc9f9..1bb831da0d 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -207,6 +207,9 @@ class Query extends Component implements QueryInterface */ public function all($db = null) { + if ($this->emulateExecution) { + return []; + } $rows = $this->createCommand($db)->queryAll(); return $this->populate($rows); } @@ -244,6 +247,9 @@ class Query extends Component implements QueryInterface */ public function one($db = null) { + if ($this->emulateExecution) { + return false; + } return $this->createCommand($db)->queryOne(); } @@ -257,6 +263,9 @@ class Query extends Component implements QueryInterface */ public function scalar($db = null) { + if ($this->emulateExecution) { + return null; + } return $this->createCommand($db)->queryScalar(); } @@ -268,6 +277,10 @@ class Query extends Component implements QueryInterface */ public function column($db = null) { + if ($this->emulateExecution) { + return []; + } + if ($this->indexBy === null) { return $this->createCommand($db)->queryColumn(); } @@ -300,6 +313,9 @@ class Query extends Component implements QueryInterface */ public function count($q = '*', $db = null) { + if ($this->emulateExecution) { + return 0; + } return $this->queryScalar("COUNT($q)", $db); } @@ -313,6 +329,9 @@ class Query extends Component implements QueryInterface */ public function sum($q, $db = null) { + if ($this->emulateExecution) { + return 0; + } return $this->queryScalar("SUM($q)", $db); } @@ -326,6 +345,9 @@ class Query extends Component implements QueryInterface */ public function average($q, $db = null) { + if ($this->emulateExecution) { + return 0; + } return $this->queryScalar("AVG($q)", $db); } @@ -363,6 +385,9 @@ class Query extends Component implements QueryInterface */ public function exists($db = null) { + if ($this->emulateExecution) { + return false; + } $command = $this->createCommand($db); $params = $command->params; $command->setSql($command->db->getQueryBuilder()->selectExists($command->getSql())); @@ -379,6 +404,10 @@ class Query extends Component implements QueryInterface */ protected function queryScalar($selectExpression, $db) { + if ($this->emulateExecution) { + return null; + } + $select = $this->select; $limit = $this->limit; $offset = $this->offset; diff --git a/framework/db/QueryInterface.php b/framework/db/QueryInterface.php index 28d16c9e1c..7503fa3664 100644 --- a/framework/db/QueryInterface.php +++ b/framework/db/QueryInterface.php @@ -252,4 +252,16 @@ interface QueryInterface * @return $this the query object itself */ public function offset($offset); + + /** + * Sets whether to emulate query execution, preventing any interaction with data storage. + * After this mode is enabled, methods, returning query results like [[one()]], [[all()]], [[exists()]] + * and so on, will return empty or false values. + * You should use this method in case your program logic indicates query should not return any results, like + * in case you set false where condition like `0=1`. + * @param boolean $value whether to prevent query execution. + * @return $this the query object itself. + * @since 2.0.11 + */ + public function emulateExecution($value = true); } diff --git a/framework/db/QueryTrait.php b/framework/db/QueryTrait.php index 4a64903d0b..67cd2bf215 100644 --- a/framework/db/QueryTrait.php +++ b/framework/db/QueryTrait.php @@ -50,6 +50,12 @@ trait QueryTrait * row data. For more details, see [[indexBy()]]. This property is only used by [[QueryInterface::all()|all()]]. */ public $indexBy; + /** + * @var boolean whether to emulate the actual query execution, returning empty or false results. + * @see emulateExecution() + * @since 2.0.11 + */ + public $emulateExecution = false; /** @@ -388,4 +394,20 @@ trait QueryTrait $this->offset = $offset; return $this; } + + /** + * Sets whether to emulate query execution, preventing any interaction with data storage. + * After this mode is enabled, methods, returning query results like [[one()]], [[all()]], [[exists()]] + * and so on, will return empty or false values. + * You should use this method in case your program logic indicates query should not return any results, like + * in case you set false where condition like `0=1`. + * @param boolean $value whether to prevent query execution. + * @return $this the query object itself. + * @since 2.0.11 + */ + public function emulateExecution($value = true) + { + $this->emulateExecution = $value; + return $this; + } } diff --git a/tests/framework/db/ActiveRecordTest.php b/tests/framework/db/ActiveRecordTest.php index 709757bf7b..8137243752 100644 --- a/tests/framework/db/ActiveRecordTest.php +++ b/tests/framework/db/ActiveRecordTest.php @@ -1294,4 +1294,70 @@ abstract class ActiveRecordTest extends DatabaseTestCase $this->assertEquals($newTotal, $newOrder->total); } + public function testEmulateExecution() + { + $this->assertGreaterThan(0, Customer::find()->from('customer')->count()); + + $rows = Customer::find() + ->from('customer') + ->emulateExecution() + ->all(); + $this->assertSame([], $rows); + + $row = Customer::find() + ->from('customer') + ->emulateExecution() + ->one(); + $this->assertSame(null, $row); + + $exists = Customer::find() + ->from('customer') + ->emulateExecution() + ->exists(); + $this->assertSame(false, $exists); + + $count = Customer::find() + ->from('customer') + ->emulateExecution() + ->count(); + $this->assertSame(0, $count); + + $sum = Customer::find() + ->from('customer') + ->emulateExecution() + ->sum('id'); + $this->assertSame(0, $sum); + + $sum = Customer::find() + ->from('customer') + ->emulateExecution() + ->average('id'); + $this->assertSame(0, $sum); + + $max = Customer::find() + ->from('customer') + ->emulateExecution() + ->max('id'); + $this->assertSame(null, $max); + + $min = Customer::find() + ->from('customer') + ->emulateExecution() + ->min('id'); + $this->assertSame(null, $min); + + $scalar = Customer::find() + ->select(['id']) + ->from('customer') + ->emulateExecution() + ->scalar(); + $this->assertSame(null, $scalar); + + $column = Customer::find() + ->select(['id']) + ->from('customer') + ->emulateExecution() + ->column(); + $this->assertSame([], $column); + } } diff --git a/tests/framework/db/QueryTest.php b/tests/framework/db/QueryTest.php index a5a68f1420..005f9a0c85 100644 --- a/tests/framework/db/QueryTest.php +++ b/tests/framework/db/QueryTest.php @@ -2,6 +2,7 @@ namespace yiiunit\framework\db; +use yii\db\Connection; use yii\db\Expression; use yii\db\Query; @@ -317,4 +318,73 @@ abstract class QueryTest extends DatabaseTestCase $count = (new Query)->from('customer')->having(['status' => 2])->count('*', $db); $this->assertEquals(1, $count); } + + public function testEmulateExecution() + { + $db = $this->getConnection(); + + $this->assertGreaterThan(0, (new Query())->from('customer')->count('*', $db)); + + $rows = (new Query()) + ->from('customer') + ->emulateExecution() + ->all($db); + $this->assertSame([], $rows); + + $row = (new Query()) + ->from('customer') + ->emulateExecution() + ->one($db); + $this->assertSame(false, $row); + + $exists = (new Query()) + ->from('customer') + ->emulateExecution() + ->exists($db); + $this->assertSame(false, $exists); + + $count = (new Query()) + ->from('customer') + ->emulateExecution() + ->count('*', $db); + $this->assertSame(0, $count); + + $sum = (new Query()) + ->from('customer') + ->emulateExecution() + ->sum('id', $db); + $this->assertSame(0, $sum); + + $sum = (new Query()) + ->from('customer') + ->emulateExecution() + ->average('id', $db); + $this->assertSame(0, $sum); + + $max = (new Query()) + ->from('customer') + ->emulateExecution() + ->max('id', $db); + $this->assertSame(null, $max); + + $min = (new Query()) + ->from('customer') + ->emulateExecution() + ->min('id', $db); + $this->assertSame(null, $min); + + $scalar = (new Query()) + ->select(['id']) + ->from('customer') + ->emulateExecution() + ->scalar($db); + $this->assertSame(null, $scalar); + + $column = (new Query()) + ->select(['id']) + ->from('customer') + ->emulateExecution() + ->column($db); + $this->assertSame([], $column); + } } From 437825be70c0bd72b582244c243cc985c11f46d6 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Thu, 17 Nov 2016 12:32:10 +0200 Subject: [PATCH 024/105] Implemented Container::setDependencies(), Container::setDefinitions Closes #11758 Closes #13029 --- docs/guide/concept-configurations.md | 22 ++++ docs/guide/concept-di-container.md | 153 +++++++++++++++++++++-- framework/CHANGELOG.md | 1 + framework/base/Application.php | 17 +++ framework/di/Container.php | 80 ++++++++++++ tests/framework/base/ApplicationTest.php | 32 +++++ tests/framework/di/ContainerTest.php | 57 +++++++++ 7 files changed, 353 insertions(+), 9 deletions(-) create mode 100644 tests/framework/base/ApplicationTest.php diff --git a/docs/guide/concept-configurations.md b/docs/guide/concept-configurations.md index a5f7905061..0afefb2e04 100644 --- a/docs/guide/concept-configurations.md +++ b/docs/guide/concept-configurations.md @@ -135,6 +135,28 @@ an [entry script](structure-entry-scripts.md), where the class name is already g More details about configuring the `components` property of an application can be found in the [Applications](structure-applications.md) section and the [Service Locator](concept-service-locator.md) section. +Since version 2.0.11, the application configuration supports [Dependency Injection Container](concept-di-container.md) +configuration using `container` property. For example: + +```php +$config = [ + 'id' => 'basic', + 'basePath' => dirname(__DIR__), + 'extensions' => require(__DIR__ . '/../vendor/yiisoft/extensions.php'), + 'container' => [ + 'definitions' => [ + 'yii\widgets\LinkPager' => ['maxButtonCount' => 5] + ], + 'singletons' => [ + // Dependency Injection Container singletons configuration + ] + ] +]; +``` + +To know more about the possible values of `definitions` and `singletons` configuration arrays and real-life examples, +please read [Advanced Practical Usage](concept-di-container.md#advanced-practical-usage) subsection of the +[Dependency Injection Container](concept-di-container.md) article. ### Widget Configurations diff --git a/docs/guide/concept-di-container.md b/docs/guide/concept-di-container.md index 976730b2af..2b561e5756 100644 --- a/docs/guide/concept-di-container.md +++ b/docs/guide/concept-di-container.md @@ -224,11 +224,13 @@ and the container will automatically resolve dependencies by instantiating them them into the newly created objects. The dependency resolution is recursive, meaning that if a dependency has other dependencies, those dependencies will also be resolved automatically. -You can use [[yii\di\Container::get()]] to create new objects. The method takes a dependency name, -which can be a class name, an interface name or an alias name. The dependency name may or may -not be registered via `set()` or `setSingleton()`. You may optionally provide a list of class -constructor parameters and a [configuration](concept-configurations.md) to configure the newly created object. -For example, +You can use [[yii\di\Container::get()|get()]] to either create or get object instance. +The method takes a dependency name, which can be a class name, an interface name or an alias name. +The dependency name may be registered via [[yii\di\Container::set()|set()]] +or [[yii\di\Container::setSingleton()|setSingleton()]]. You may optionally provide a list of class +constructor parameters and a [configuration](concept-configurations.md) to configure the newly created object. + +For example: ```php // "db" is a previously registered alias name @@ -312,10 +314,10 @@ Yii creates a DI container when you include the `Yii.php` file in the [entry scr of your application. The DI container is accessible via [[Yii::$container]]. When you call [[Yii::createObject()]], the method will actually call the container's [[yii\di\Container::get()|get()]] method to create a new object. As aforementioned, the DI container will automatically resolve the dependencies (if any) and inject them -into the newly created object. Because Yii uses [[Yii::createObject()]] in most of its core code to create +into obtained object. Because Yii uses [[Yii::createObject()]] in most of its core code to create new objects, this means you can customize the objects globally by dealing with [[Yii::$container]]. -For example, you can customize globally the default number of pagination buttons of [[yii\widgets\LinkPager]]: +For example, let's customize globally the default number of pagination buttons of [[yii\widgets\LinkPager]]. ```php \Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]); @@ -368,6 +370,138 @@ cannot be instantiated. This is because you need to tell the DI container how to Now if you access the controller again, an instance of `app\components\BookingService` will be created and injected as the 3rd parameter to the controller's constructor. +Advanced Practical Usage +--------------- + +Say we work on API application and have: + - `app\components\Request` class that extends `yii\web\Request` and provides additional functionality + - `app\components\Response` class that extends `yii\web\Response` and should have `format` property + set to `json` on creation + - `app\storage\FileStorage` and `app\storage\DocumentsReader` classes the implement some logic on + working with documents that are located in some file storage: + ```php + class FileStorage + { + public function __contruct($root) { + // whatever + } + } + + class DocumentsReader + { + public function __contruct(FileStorage $fs) { + // whatever + } + } + ``` + +It is possible to configure multiple definitions at once, passing configuration array to +[[yii\di\Container::setDefinitions()|setDefinitions()]] or [[yii\di\Container::setSingletons()|setSingletons()]] method. +Iterating over the configuration array, the methods will call [[yii\di\Container::set()|set()]] +or [[yii\di\Container::setSingleton()|setSingleton()]] respectively for each item. + +The configuration array format is: + + - `key`: class name, interface name or alias name. The key will be passed to the + [[yii\di\Container::set()|set()]] method as a first argument `$class`. + - `value`: the definition associated with `$class`. Possible values are described in [[yii\di\Container::set()|set()]] + documentation for the `$definition` parameter. Will be passed to the [[set()]] method as + the second argument `$definition`. + +For example, let's configure our container to follow the aforementioned requirements: + +```php +$container->setDefinitions([ + 'yii\web\Request' => 'app\components\Request', + 'yii\web\Response' => [ + 'class' => 'app\components\Response', + 'format' => 'json' + ], + 'app\storage\DocumentsReader' => function () { + $fs = new app\storage\FileStorage('/var/tempfiles'); + return new app\storage\DocumentsReader($fs); + } +]); + +$reader = $container->get('app\storage\DocumentsReader); +// Will create DocumentReader object with its dependencies as described in the config +``` + +> Tip: Container may be configured in declarative style using application configuration since version 2.0.11. +Check out the [Application Configurations](concept-service-locator.md#application-configurations) subsection of +the [Configurations](concept-configurations.md) guide article. + +Everything works, but in case we need to create create `DocumentWriter` class, +we shall copy-paste the line that creates `FileStorage` object, that is not the smartest way, obviously. + +As described in the [Resolving Dependencies](#resolving-dependencies) subsection, [[yii\di\Container::set()|set()]] +and [[yii\di\Container::setSingleton()|setSingleton()]] can optionally take dependency's constructor parameters as +a third argument. To set the constructor parameters, you may use the following configuration array format: + + - `key`: class name, interface name or alias name. The key will be passed to the + [[yii\di\Container::set()|set()]] method as a first argument `$class`. + - `value`: array of two elements. The first element will be passed the [[yii\di\Container::set()|set()]] method as the + second argument `$definition`, the second one — as `$params`. + +Let's modify our example: + +```php +$container->setDefinitions([ + 'tempFileStorage' => [ // we've created an alias for convenience + ['class' => 'app\storage\FileStorage'], + ['/var/tempfiles'] // could be extracted from some config files + ], + 'app\storage\DocumentsReader' => [ + ['class' => 'app\storage\DocumentsReader'], + [Instance::of('tempFileStorage')] + ], + 'app\storage\DocumentsWriter' => [ + ['class' => 'app\storage\DocumentsWriter'], + [Instance::of('tempFileStorage')] + ] +]); + +$reader = $container->get('app\storage\DocumentsReader); +// Will behave exactly the same as in the previous example. +``` + +You might notice `Instance::of('tempFileStorage')` notation. It means, that the [[yii\di\Container|Container]] +will implicitly provide dependency, registered with `tempFileStorage` name and pass it as the first argument +of `app\storage\DocumentsWriter` constructor. + +> Note: [[yii\di\Container::setDefinitions()|setDefinitions()]] and [[yii\di\Container::setSingletons()|setSingletons()]] + methods are available since version 2.0.11. + +Another step on configuration optimization is to register some dependencies as singletons. +A dependency registered via [[yii\di\Container::set()|set()]] will be instantiated each time it is needed. +Some classes do not change the state during runtime, therefore they may be registered as singletons +in order to increase the application performance. + +A good example could be `app\storage\FileStorage` class, that executes some operations on file system with a simple +API (e.g. `$fs->read()`, `$fs->write()`). These operations do not change the internal class state, so we can +create its instance once and use it multiple times. + +```php +$container->setSingletons([ + 'tempFileStorage' => [ + ['class' => 'app\storage\FileStorage'], + ['/var/tempfiles'] + ], +]); + +$container->setDefinitions([ + 'app\storage\DocumentsReader' => [ + ['class' => 'app\storage\DocumentsReader'], + [Instance::of('tempFileStorage')] + ], + 'app\storage\DocumentsWriter' => [ + ['class' => 'app\storage\DocumentsWriter'], + [Instance::of('tempFileStorage')] + ] +]); + +$reader = $container->get('app\storage\DocumentsReader); +``` When to Register Dependencies ----------------------------- @@ -375,8 +509,9 @@ When to Register Dependencies Because dependencies are needed when new objects are being created, their registration should be done as early as possible. The following are the recommended practices: -* If you are the developer of an application, you can register dependencies in your - application's [entry script](structure-entry-scripts.md) or in a script that is included by the entry script. +* If you are the developer of an application, you can register your dependencies using application configuration. + Please, read the [Application Configurations](concept-service-locator.md#application-configurations) subsection of + the [Configurations](concept-configurations.md) guide article. * If you are the developer of a redistributable [extension](structure-extensions.md), you can register dependencies in the bootstrapping class of the extension. diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 3c263771a5..64bdccf15e 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -57,6 +57,7 @@ Yii Framework 2 Change Log - Enh #12988: Changed `textarea` method within the `yii\helpers\BaseHtml` class to allow users to control whether html entities found within `$value` will be double-encoded or not (cyphix333) - Enh #13074: Improved `yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks) - Enh #13050: Added `yii\filters\HostControl` allowing protection against 'host header' attacks (klimov-paul) +- Enh #11758: Implemented Dependency Injection Container configuration using Application configuration array (silverfire) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) - Enh #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions (zalatov) diff --git a/framework/base/Application.php b/framework/base/Application.php index f31bfd1d27..03688807d8 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -246,6 +246,12 @@ abstract class Application extends Module $this->setTimeZone('UTC'); } + if (isset($config['container'])) { + $this->setContainer($config['container']); + + unset($config['container']); + } + // merge core components with custom components foreach ($this->coreComponents() as $id => $component) { if (!isset($config['components'][$id])) { @@ -652,4 +658,15 @@ abstract class Application extends Module exit($status); } } + + /** + * Configures [[Yii::$container]] with the $config + * + * @param array $config values given in terms of name-value pairs + * @since 2.0.11 + */ + public function setContainer($config) + { + Yii::configure(Yii::$container, $config); + } } diff --git a/framework/di/Container.php b/framework/di/Container.php index 3649d6fe9c..5446126d43 100644 --- a/framework/di/Container.php +++ b/framework/di/Container.php @@ -566,4 +566,84 @@ class Container extends Component } return $args; } + + /** + * Registers class definitions within this container. + * + * @param array $definitions array of definitions. There are two allowed formats of array. + * The first format: + * - key: class name, interface name or alias name. The key will be passed to the [[set()]] method + * as a first argument `$class`. + * - value: the definition associated with `$class`. Possible values are described in + * [[set()]] documentation for the `$definition` parameter. Will be passed to the [[set()]] method + * as the second argument `$definition`. + * + * Example: + * ```php + * $container->setDefinitions([ + * 'yii\web\Request' => 'app\components\Request', + * 'yii\web\Response' => [ + * 'class' => 'app\components\Response', + * 'format' => 'json' + * ], + * 'foo\Bar' => function () { + * $qux = new Qux; + * $foo = new Foo($qux); + * return new Bar($foo); + * } + * ]); + * ``` + * + * The second format: + * - key: class name, interface name or alias name. The key will be passed to the [[set()]] method + * as a first argument `$class`. + * - value: array of two elements. The first element will be passed the [[set()]] method as the + * second argument `$definition`, the second one — as `$params`. + * + * Example: + * ```php + * $container->setDefinitions([ + * 'foo\Bar' => [ + * ['class' => 'app\Bar'], + * [Instance::of('baz')] + * ] + * ]); + * ``` + * + * @see set() to know more about possible values of definitions + * @since 2.0.11 + */ + public function setDefinitions(array $definitions) + { + foreach ($definitions as $class => $definition) { + if (count($definition) === 2 && array_values($definition) === $definition) { + $this->set($class, $definition[0], $definition[1]); + continue; + } + + $this->set($class, $definition); + } + } + + /** + * Registers class definitions as singletons within this container by calling [[setSingleton()]] + * + * @param array $singletons array of singleton definitions. See [[setDefinitions()]] + * for allowed formats of array. + * + * @see setDefinitions() for allowed formats of $singletons parameter + * @see setSingleton() to know more about possible values of definitions + * @since 2.0.11 + */ + public function setSingletons(array $singletons) + { + foreach ($singletons as $class => $definition) { + if (count($definition) === 2 && array_values($definition) === $definition) { + $this->setSingleton($class, $definition[0], $definition[1]); + continue; + } + + $this->setSingleton($class, $definition); + } + } } diff --git a/tests/framework/base/ApplicationTest.php b/tests/framework/base/ApplicationTest.php new file mode 100644 index 0000000000..2be0858d93 --- /dev/null +++ b/tests/framework/base/ApplicationTest.php @@ -0,0 +1,32 @@ +mockApplication([ + 'container' => [ + 'definitions' => [ + Dispatcher::className() => DispatcherMock::className() + ] + ], + 'bootstrap' => ['log'] + ]); + + $this->assertInstanceOf(DispatcherMock::className(), Yii::$app->log); + } +} + +class DispatcherMock extends Dispatcher +{ + +} diff --git a/tests/framework/di/ContainerTest.php b/tests/framework/di/ContainerTest.php index c6ac6db1ad..fe632a8b78 100644 --- a/tests/framework/di/ContainerTest.php +++ b/tests/framework/di/ContainerTest.php @@ -10,6 +10,9 @@ namespace yiiunit\framework\di; use Yii; use yii\di\Container; use yii\di\Instance; +use yiiunit\data\ar\Cat; +use yiiunit\data\ar\Order; +use yiiunit\data\ar\Type; use yiiunit\framework\di\stubs\Bar; use yiiunit\framework\di\stubs\Foo; use yiiunit\framework\di\stubs\Qux; @@ -223,4 +226,58 @@ class ContainerTest extends TestCase }; $this->assertNull($container->invoke($closure)); } + + public function testSetDependencies() + { + $container = new Container(); + $container->setDefinitions([ + 'model.order' => Order::className(), + Cat::className() => Type::className(), + 'test\TraversableInterface' => [ + ['class' => 'yiiunit\data\base\TraversableObject'], + [['item1', 'item2']] + ], + 'qux.using.closure' => function () { + return new Qux(); + } + ]); + $container->setDefinitions([]); + + $this->assertInstanceOf(Order::className(), $container->get('model.order')); + $this->assertInstanceOf(Type::className(), $container->get(Cat::className())); + + $traversable = $container->get('test\TraversableInterface'); + $this->assertInstanceOf('yiiunit\data\base\TraversableObject', $traversable); + $this->assertEquals('item1', $traversable->current()); + + $this->assertInstanceOf('yiiunit\framework\di\stubs\Qux', $container->get('qux.using.closure')); + } + + public function testContainerSingletons() + { + $container = new Container(); + $container->setSingletons([ + 'model.order' => Order::className(), + 'test\TraversableInterface' => [ + ['class' => 'yiiunit\data\base\TraversableObject'], + [['item1', 'item2']] + ], + 'qux.using.closure' => function () { + return new Qux(); + } + ]); + $container->setSingletons([]); + + $order = $container->get('model.order'); + $sameOrder = $container->get('model.order'); + $this->assertSame($order, $sameOrder); + + $traversable = $container->get('test\TraversableInterface'); + $sameTraversable = $container->get('test\TraversableInterface'); + $this->assertSame($traversable, $sameTraversable); + + $foo = $container->get('qux.using.closure'); + $sameFoo = $container->get('qux.using.closure'); + $this->assertSame($foo, $sameFoo); + } } From 0b9feb28e50e35fc9963a4cf125e2dd470a6eb08 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 2 Dec 2016 16:59:01 +0300 Subject: [PATCH 025/105] More renaming, removed unnecessary code --- framework/widgets/ActiveField.php | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/framework/widgets/ActiveField.php b/framework/widgets/ActiveField.php index 16e3a084bd..2aa2d29a56 100644 --- a/framework/widgets/ActiveField.php +++ b/framework/widgets/ActiveField.php @@ -163,12 +163,6 @@ class ActiveField extends Component */ private $_skipLabelFor = false; - /** - * @var bool whether `aria-invalid` HTML attribute should be updated by client validation - */ - private $_updateAriaInvalidOnClient = true; - - /** * PHP magic method that returns the string representation of this object. * @return string the string representation of this object. @@ -754,8 +748,8 @@ class ActiveField extends Component return []; } - $clientValidation = $this->clientValidationEnabled(); - $ajaxValidation = $this->ajaxValidationEnabled(); + $clientValidation = $this->isClientValidationEnabled(); + $ajaxValidation = $this->isAjaxValidationEnabled(); if ($clientValidation) { $validators = []; @@ -803,7 +797,7 @@ class ActiveField extends Component $options['validate'] = new JsExpression("function (attribute, value, messages, deferred, \$form) {" . implode('', $validators) . '}'); } - if ($this->addAriaAttributes === false || !$this->_updateAriaInvalidOnClient) { + if ($this->addAriaAttributes === false) { $options['updateAriaInvalid'] = false; } @@ -824,7 +818,7 @@ class ActiveField extends Component * @return bool * @since 2.0.11 */ - protected function clientValidationEnabled() + protected function isClientValidationEnabled() { return $this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation; } @@ -834,7 +828,7 @@ class ActiveField extends Component * @return bool * @since 2.0.11 */ - protected function ajaxValidationEnabled() + protected function isAjaxValidationEnabled() { return $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation; } @@ -864,7 +858,6 @@ class ActiveField extends Component if ($this->model->hasErrors($this->attribute)) { $options['aria-invalid'] = 'true'; } - $this->_updateAriaInvalidOnClient = false; } } } From fc0752f388d7dec6c82ac5ec431702a188bb8c81 Mon Sep 17 00:00:00 2001 From: Robert Korulczyk Date: Fri, 2 Dec 2016 15:56:49 +0100 Subject: [PATCH 026/105] Disable `Controversial/CamelCasePropertyName` codeclimate check (#13121) * Disable `Controversial/CamelCasePropertyName` codeclimate check * Fix indentation --- .codeclimate.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index 614dd1ae2f..bbabf2810a 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -13,8 +13,10 @@ engines: phpmd: enabled: true checks: - CleanCode/StaticAccess: - enabled: false + CleanCode/StaticAccess: + enabled: false + Controversial/CamelCasePropertyName: + enabled: false ratings: paths: - "**.js" From faea888652be0c71c7b02d8e4d1e94b8397650ee Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 2 Dec 2016 16:15:17 +0100 Subject: [PATCH 027/105] update codeclimate rules to match yii style follow up to #13121 --- .codeclimate.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index bbabf2810a..d8166d7a26 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -12,11 +12,30 @@ engines: enabled: true phpmd: enabled: true + # configure checks, see https://phpmd.org/rules/index.html for details checks: + # Static access on Yii::$app is normal in Yii CleanCode/StaticAccess: enabled: false + # Yii is a framework so if fulfills the job of encapsulating superglobals + Controversial/Superglobals: + enabled: false + # allow private properties to start with $_ Controversial/CamelCasePropertyName: - enabled: false + enabled: true + allow-underscore: true + # Short variable names are no problem in most cases, e.g. $n = count(...); + Naming/ShortVariable: + enabled: false + # Long variable names can help with better understanding so we increase the limit a bit + Naming/LongVariable: + enabled: true + maximum: 25 + # method names like up(), gc(), ... are okay. + Naming/ShortMethodName: + enabled: true + minimum: 2 + ratings: paths: - "**.js" From 9c0e94efea24c81445484329882a4a9c0d772970 Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Fri, 2 Dec 2016 21:53:08 +0100 Subject: [PATCH 028/105] Update CHANGELOG.md typo --- framework/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 4a7969652e..822efbc255 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -39,7 +39,7 @@ Yii Framework 2 Change Log - Enh #11929: Changed `type` column type from `int` to `smallInt` in RBAC migrations (silverfire) - Enh #12015: Changed visibility `yii\db\ActiveQueryTrait::createModels()` from private to protected (ArekX, dynasource) - Enh #12399: Added `ActiveField::addAriaAttributes` property for `aria-required` and `aria-invalid` attributes rendering (Oxyaction, samdark) -- Enh #12390: Avoid creating queries with false where contdition (`0=1`) when fetching relational data (klimov-paul) +- Enh #12390: Avoid creating queries with false where condition (`0=1`) when fetching relational data (klimov-paul) - Enh #12619: Added catch `Throwable` in `yii\base\ErrorHandler::handleException()` (rob006) - Enh #12726: `yii\base\Application::$version` converted to `yii\base\Module::$version` virtual property, allowing to specify version as a PHP callback (klimov-paul) - Enh #12738: Added support for creating protocol-relative URLs in `UrlManager::createAbsoluteUrl()` and `Url` helper methods (rob006) From 954c771fdbd867500bc5de8498a3a7453716a74a Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 3 Dec 2016 00:15:53 +0300 Subject: [PATCH 029/105] Code cleanup (#13113) * Simplified code branching, named variable better * More simple code cleanup * More consistent line breaks before return statemetns --- framework/BaseYii.php | 46 +++++++------- framework/base/Component.php | 112 ++++++++++++++++++---------------- framework/base/Controller.php | 6 +- framework/base/Event.php | 25 ++++---- framework/base/Model.php | 23 +++---- framework/base/Module.php | 15 ++--- framework/base/View.php | 6 +- 7 files changed, 113 insertions(+), 120 deletions(-) diff --git a/framework/BaseYii.php b/framework/BaseYii.php index 1f19990810..cc9acc9452 100644 --- a/framework/BaseYii.php +++ b/framework/BaseYii.php @@ -139,20 +139,20 @@ class BaseYii if (isset(static::$aliases[$root])) { if (is_string(static::$aliases[$root])) { return $pos === false ? static::$aliases[$root] : static::$aliases[$root] . substr($alias, $pos); - } else { - foreach (static::$aliases[$root] as $name => $path) { - if (strpos($alias . '/', $name . '/') === 0) { - return $path . substr($alias, strlen($name)); - } + } + + foreach (static::$aliases[$root] as $name => $path) { + if (strpos($alias . '/', $name . '/') === 0) { + return $path . substr($alias, strlen($name)); } } } if ($throwException) { throw new InvalidParamException("Invalid path alias: $alias"); - } else { - return false; } + + return false; } /** @@ -170,11 +170,11 @@ class BaseYii if (isset(static::$aliases[$root])) { if (is_string(static::$aliases[$root])) { return $root; - } else { - foreach (static::$aliases[$root] as $name => $path) { - if (strpos($alias . '/', $name . '/') === 0) { - return $name; - } + } + + foreach (static::$aliases[$root] as $name => $path) { + if (strpos($alias . '/', $name . '/') === 0) { + return $name; } } } @@ -346,9 +346,9 @@ class BaseYii return static::$container->invoke($type, $params); } elseif (is_array($type)) { throw new InvalidConfigException('Object configuration must be an array containing a "class" element.'); - } else { - throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type)); } + + throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type)); } private static $_logger; @@ -360,9 +360,9 @@ class BaseYii { if (self::$_logger !== null) { return self::$_logger; - } else { - return self::$_logger = static::createObject('yii\log\Logger'); } + + return self::$_logger = static::createObject('yii\log\Logger'); } /** @@ -499,14 +499,14 @@ class BaseYii { if (static::$app !== null) { return static::$app->getI18n()->translate($category, $message, $params, $language ?: static::$app->language); - } else { - $p = []; - foreach ((array) $params as $name => $value) { - $p['{' . $name . '}'] = $value; - } - - return ($p === []) ? $message : strtr($message, $p); } + + $placeholders = []; + foreach ((array) $params as $name => $value) { + $placeholders['{' . $name . '}'] = $value; + } + + return ($placeholders === []) ? $message : strtr($message, $placeholders); } /** diff --git a/framework/base/Component.php b/framework/base/Component.php index f116ce9b5c..83568f64f9 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -130,20 +130,21 @@ class Component extends Object if (method_exists($this, $getter)) { // read property, e.g. getName() return $this->$getter(); - } else { - // behavior property - $this->ensureBehaviors(); - foreach ($this->_behaviors as $behavior) { - if ($behavior->canGetProperty($name)) { - return $behavior->$name; - } + } + + // behavior property + $this->ensureBehaviors(); + foreach ($this->_behaviors as $behavior) { + if ($behavior->canGetProperty($name)) { + return $behavior->$name; } } + if (method_exists($this, 'set' . $name)) { throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name); - } else { - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); } + + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); } /** @@ -182,22 +183,22 @@ class Component extends Object $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); return; - } else { - // behavior property - $this->ensureBehaviors(); - foreach ($this->_behaviors as $behavior) { - if ($behavior->canSetProperty($name)) { - $behavior->$name = $value; + } - return; - } + // behavior property + $this->ensureBehaviors(); + foreach ($this->_behaviors as $behavior) { + if ($behavior->canSetProperty($name)) { + $behavior->$name = $value; + return; } } + if (method_exists($this, 'get' . $name)) { throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); - } else { - throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); } + + throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); } /** @@ -219,15 +220,16 @@ class Component extends Object $getter = 'get' . $name; if (method_exists($this, $getter)) { return $this->$getter() !== null; - } else { - // behavior property - $this->ensureBehaviors(); - foreach ($this->_behaviors as $behavior) { - if ($behavior->canGetProperty($name)) { - return $behavior->$name !== null; - } + } + + // behavior property + $this->ensureBehaviors(); + foreach ($this->_behaviors as $behavior) { + if ($behavior->canGetProperty($name)) { + return $behavior->$name !== null; } } + return false; } @@ -250,16 +252,17 @@ class Component extends Object if (method_exists($this, $setter)) { $this->$setter(null); return; - } else { - // behavior property - $this->ensureBehaviors(); - foreach ($this->_behaviors as $behavior) { - if ($behavior->canSetProperty($name)) { - $behavior->$name = null; - return; - } + } + + // behavior property + $this->ensureBehaviors(); + foreach ($this->_behaviors as $behavior) { + if ($behavior->canSetProperty($name)) { + $behavior->$name = null; + return; } } + throw new InvalidCallException('Unsetting an unknown or read-only property: ' . get_class($this) . '::' . $name); } @@ -503,19 +506,19 @@ class Component extends Object if ($handler === null) { unset($this->_events[$name]); return true; - } else { - $removed = false; - foreach ($this->_events[$name] as $i => $event) { - if ($event[0] === $handler) { - unset($this->_events[$name][$i]); - $removed = true; - } - } - if ($removed) { - $this->_events[$name] = array_values($this->_events[$name]); - } - return $removed; } + + $removed = false; + foreach ($this->_events[$name] as $i => $event) { + if ($event[0] === $handler) { + unset($this->_events[$name][$i]); + $removed = true; + } + } + if ($removed) { + $this->_events[$name] = array_values($this->_events[$name]); + } + return $removed; } /** @@ -621,9 +624,9 @@ class Component extends Object unset($this->_behaviors[$name]); $behavior->detach(); return $behavior; - } else { - return null; } + + return null; } /** @@ -666,13 +669,14 @@ class Component extends Object if (is_int($name)) { $behavior->attach($this); $this->_behaviors[] = $behavior; - } else { - if (isset($this->_behaviors[$name])) { - $this->_behaviors[$name]->detach(); - } - $behavior->attach($this); - $this->_behaviors[$name] = $behavior; } + + if (isset($this->_behaviors[$name])) { + $this->_behaviors[$name]->detach(); + } + $behavior->attach($this); + $this->_behaviors[$name] = $behavior; + return $behavior; } } diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 690c3cd368..93143ecd2a 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -186,9 +186,8 @@ class Controller extends Component implements ViewContextInterface return $this->runAction($route, $params); } elseif ($pos > 0) { return $this->module->runAction($route, $params); - } else { - return Yii::$app->runAction(ltrim($route, '/'), $params); } + return Yii::$app->runAction(ltrim($route, '/'), $params); } /** @@ -393,9 +392,8 @@ class Controller extends Component implements ViewContextInterface $layoutFile = $this->findLayoutFile($this->getView()); if ($layoutFile !== false) { return $this->getView()->renderFile($layoutFile, ['content' => $content], $this); - } else { - return $content; } + return $content; } /** diff --git a/framework/base/Event.php b/framework/base/Event.php index 498607abff..d5cca0cbec 100644 --- a/framework/base/Event.php +++ b/framework/base/Event.php @@ -116,20 +116,19 @@ class Event extends Object if ($handler === null) { unset(self::$_events[$name][$class]); return true; - } else { - $removed = false; - foreach (self::$_events[$name][$class] as $i => $event) { - if ($event[0] === $handler) { - unset(self::$_events[$name][$class][$i]); - $removed = true; - } - } - if ($removed) { - self::$_events[$name][$class] = array_values(self::$_events[$name][$class]); - } - - return $removed; } + + $removed = false; + foreach (self::$_events[$name][$class] as $i => $event) { + if ($event[0] === $handler) { + unset(self::$_events[$name][$class][$i]); + $removed = true; + } + } + if ($removed) { + self::$_events[$name][$class] = array_values(self::$_events[$name][$class]); + } + return $removed; } /** diff --git a/framework/base/Model.php b/framework/base/Model.php index 05fecdda7d..91b7d0f66e 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -562,9 +562,8 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab { if ($attribute === null) { return $this->_errors === null ? [] : $this->_errors; - } else { - return isset($this->_errors[$attribute]) ? $this->_errors[$attribute] : []; } + return isset($this->_errors[$attribute]) ? $this->_errors[$attribute] : []; } /** @@ -578,16 +577,15 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab { if (empty($this->_errors)) { return []; - } else { - $errors = []; - foreach ($this->_errors as $name => $es) { - if (!empty($es)) { - $errors[$name] = reset($es); - } - } - - return $errors; } + + $errors = []; + foreach ($this->_errors as $name => $es) { + if (!empty($es)) { + $errors[$name] = reset($es); + } + } + return $errors; } /** @@ -829,9 +827,8 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab $this->setAttributes($data[$scope]); return true; - } else { - return false; } + return false; } /** diff --git a/framework/base/Module.php b/framework/base/Module.php index bd347eaac2..670d157ece 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -391,9 +391,8 @@ class Module extends ServiceLocator $module = $this->getModule(substr($id, 0, $pos)); return $module === null ? false : $module->hasModule(substr($id, $pos + 1)); - } else { - return isset($this->_modules[$id]); } + return isset($this->_modules[$id]); } /** @@ -467,9 +466,8 @@ class Module extends ServiceLocator } return $modules; - } else { - return $this->_modules; } + return $this->_modules; } /** @@ -527,10 +525,10 @@ class Module extends ServiceLocator } return $result; - } else { - $id = $this->getUniqueId(); - throw new InvalidRouteException('Unable to resolve the request "' . ($id === '' ? $route : $id . '/' . $route) . '".'); } + + $id = $this->getUniqueId(); + throw new InvalidRouteException('Unable to resolve the request "' . ($id === '' ? $route : $id . '/' . $route) . '".'); } /** @@ -640,9 +638,8 @@ class Module extends ServiceLocator return get_class($controller) === $className ? $controller : null; } elseif (YII_DEBUG) { throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); - } else { - return null; } + return null; } /** diff --git a/framework/base/View.php b/framework/base/View.php index 4d3ff524fb..f08547542a 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -347,9 +347,8 @@ class View extends Component $this->addDynamicPlaceholder($placeholder, $statements); return $placeholder; - } else { - return $this->evaluateDynamicContent($statements); } + return $this->evaluateDynamicContent($statements); } /** @@ -465,9 +464,8 @@ class View extends Component $this->endCache(); return false; - } else { - return true; } + return true; } /** From 05b17ebb650e0aaac5649743eaa2b71011e22bbe Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 9 Nov 2016 22:30:33 +0100 Subject: [PATCH 030/105] Added bash completion file for ./yii commands This allows to auto-complete commands available by the `./yii` executable on the bash by expanding the first argument with the TAB key. E.g. Typing `./yii [TAB]` will list all commands. `./yii mig[TAB]` will auto-complete to `./yii migrate` and tab after that will list all migration sub-commands(actions). fixes #475 --- docs/guide/tutorial-console.md | 14 ++++++++ framework/CHANGELOG.md | 2 ++ .../console/controllers/HelpController.php | 26 +++++++++++++++ framework/yii.bash-completion | 32 +++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 framework/yii.bash-completion diff --git a/docs/guide/tutorial-console.md b/docs/guide/tutorial-console.md index 0ab694e3bf..4f390837c7 100644 --- a/docs/guide/tutorial-console.md +++ b/docs/guide/tutorial-console.md @@ -107,6 +107,20 @@ You can see an example of this in the advanced project template. > ``` +Bash completion +--------------- + +Auto-completion of command arguments is a useful thing when working with the shell. Since version 2.0.11, the `./yii` command provides auto completion +for the bash out of the box. This is provided by the `yii.bash-completion` file located in the framework base directory. +For temporary usage you can source that file by typing: + + source yii.bash-completion + +This will include the definitions given in the `yii.bash-completion` file in your current session. +For permanent usage you can copy the file to `/etc/bash_completion.d/` on your system or add the source line above to your `.bashrc` file. +Check the [Bash Manual](https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html) for more information. + + Creating your own console commands ---------------------------------- diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 822efbc255..f274c7df4d 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -30,6 +30,8 @@ Yii Framework 2 Change Log - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) - Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) - Enh #6809: Added `yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) +- Enh #475: Added basic bash completion support for the `./yii` command (cebe) +- Enh #6809: Added `\yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) - Enh #7333: Improved error message for `yii\di\Instance::ensure()` when a component does not exist (cebe) - Enh #7420: Attributes for prompt generated with `renderSelectOptions` of `\yii\helpers\Html` helper (arogachev) - Enh #9162: Added support of closures in `value` for attributes in `yii\widgets\DetailView` (arogachev) diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 27fbcd6394..a55b115ce1 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -68,6 +68,32 @@ class HelpController extends Controller } } + /** + * List all available controllers and actions in machine readable format. + * This is used for bash completion. + * @since 2.0.11 + */ + public function actionList() + { + $commands = $this->getCommandDescriptions(); + foreach ($commands as $command => $description) { + $result = Yii::$app->createController($command); + if ($result === false || !($result[0] instanceof Controller)) { + continue; + } + /** @var $controller Controller */ + list($controller, $actionID) = $result; + $actions = $this->getActions($controller); + if (!empty($actions)) { + $prefix = $controller->getUniqueId(); + $this->stdout("$prefix\n"); + foreach ($actions as $action) { + $this->stdout("$prefix/$action\n"); + } + } + } + } + /** * Returns all available command names. * @return array all available command names diff --git a/framework/yii.bash-completion b/framework/yii.bash-completion new file mode 100644 index 0000000000..5f6cdc8263 --- /dev/null +++ b/framework/yii.bash-completion @@ -0,0 +1,32 @@ +# This file implements bash completion for the ./yii command file. +# It completes the commands available by the ./yii command. +# See also: +# - https://debian-administration.org/article/317/An_introduction_to_bash_completion_part_2 on how this works. +# - https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html +# - http://www.yiiframework.com/doc-2.0/guide-tutorial-console.html#bash-completion +# +# Usage: +# Temporarily you can source this file in you bash by typing: source yii.bash-completion +# For permanent availability, copy or link this file to /etc/bash_completion.d/ +# + +_yii() +{ + local cur opts yii + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + yii="${COMP_WORDS[0]}" + + # only complete first argument + if [[ COMP_CWORD -eq 1 ]] ; then + # fetch available commands from ./yii help/list command + opts=$($yii help/list) + # generate completion suggestions + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi +} + +# register completion for the ./yii command +# you may adjust this line if your command file is named differently +complete -F _yii ./yii From beb140446bfed279399b9b853cc172212e41c6fb Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Sat, 26 Nov 2016 22:38:36 +0100 Subject: [PATCH 031/105] added test for helpcontroller list action --- .../controllers/HelpControllerTest.php | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/framework/console/controllers/HelpControllerTest.php b/tests/framework/console/controllers/HelpControllerTest.php index 14d25e12ea..06c008c264 100644 --- a/tests/framework/console/controllers/HelpControllerTest.php +++ b/tests/framework/console/controllers/HelpControllerTest.php @@ -46,6 +46,39 @@ class HelpControllerTest extends TestCase } public function testActionIndex() + { + $this->mockApplication([ + 'enableCoreCommands' => false, + 'controllerMap' => [ + 'migrate' => 'yii\console\controllers\MigrateController', + 'cache' => 'yii\console\controllers\CacheController', + ], + ]); + $result = Console::stripAnsiFormat($this->runControllerAction('list')); + $this->assertEquals(<<runControllerAction('index')); $this->assertContains('This is Yii version ', $result); @@ -56,7 +89,7 @@ class HelpControllerTest extends TestCase public function testActionIndexWithHelpCommand() { - $result = Console::stripAnsiFormat($this->runControllerAction('index', ['command' => 'help'])); + $result = Console::stripAnsiFormat($this->runControllerAction('index', ['command' => 'help/index'])); $this->assertContains('Displays available commands or the detailed information', $result); $this->assertContains('bootstrap.php help [command] [...options...]', $result); $this->assertContains('--appconfig: string', $result); From 4454d05410bb7bea29a5646ba49e750599b25434 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Mon, 28 Nov 2016 16:09:50 +0200 Subject: [PATCH 032/105] Added ZSH completion, moved scripts to a separate directory --- .../completion/bash/yii | 0 contrib/completion/zsh/_yii | 38 +++++++++ docs/guide/tutorial-console.md | 46 +++++++++-- .../console/controllers/HelpController.php | 80 ++++++++++++++++++- .../controllers/HelpControllerTest.php | 46 ++++++++++- 5 files changed, 198 insertions(+), 12 deletions(-) rename framework/yii.bash-completion => contrib/completion/bash/yii (100%) create mode 100644 contrib/completion/zsh/_yii diff --git a/framework/yii.bash-completion b/contrib/completion/bash/yii similarity index 100% rename from framework/yii.bash-completion rename to contrib/completion/bash/yii diff --git a/contrib/completion/zsh/_yii b/contrib/completion/zsh/_yii new file mode 100644 index 0000000000..e85ebc349e --- /dev/null +++ b/contrib/completion/zsh/_yii @@ -0,0 +1,38 @@ +#compdef yii + +_yii() { + local state command lastArgument commands options executive + lastArgument=${words[${#words[@]}]} + executive=$words[1] + + # lookup for command + for word in ${words[@]:1}; do + if [[ $word != -* ]]; then + command=$word + break + fi + done + + + [[ $lastArgument == $command ]] && state="command" + [[ $lastArgument != $command ]] && state="option" + + case $state in + command) + commands=("${(@f)$(${executive} help/list 2>/dev/null)}") + _describe 'command' commands + ;; + option) + options=("${(@f)$(${executive} help/usage ${command} 2>/dev/null)}") + _message -r "$options" + + suboptions=("${(@f)$(${executive} help/list-action-options ${command} 2>/dev/null)}") + _describe -V -o -t suboption 'action options' suboptions + ;; + *) + esac + +} + +compdef _yii yii + diff --git a/docs/guide/tutorial-console.md b/docs/guide/tutorial-console.md index 4f390837c7..db572eaaee 100644 --- a/docs/guide/tutorial-console.md +++ b/docs/guide/tutorial-console.md @@ -107,19 +107,49 @@ You can see an example of this in the advanced project template. > ``` -Bash completion +Console command completion --------------- -Auto-completion of command arguments is a useful thing when working with the shell. Since version 2.0.11, the `./yii` command provides auto completion -for the bash out of the box. This is provided by the `yii.bash-completion` file located in the framework base directory. -For temporary usage you can source that file by typing: +Auto-completion of command arguments is a useful thing when working with the shell. +Since version 2.0.11, the `./yii` command provides auto completion for the bash out of the box. - source yii.bash-completion +### Bash completion -This will include the definitions given in the `yii.bash-completion` file in your current session. -For permanent usage you can copy the file to `/etc/bash_completion.d/` on your system or add the source line above to your `.bashrc` file. -Check the [Bash Manual](https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html) for more information. +Make sure bash completion is installed. For most of installations it is available by default. +Place the completion script in `/etc/bash_completion.d/`: + + curl -L https://raw.githubusercontent.com/yiisoft/yii2/blob/master/contrib/completion/bash/yii > /etc/bash_completion.d/yii + +Check the [Bash Manual](https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html) for +other ways of including completion script to your environment. + +### ZSH completion + +Put the completion script in directory for completions, using e.g. `~/.zsh/completion/` + +``` +mkdir -p ~/.zsh/completion +curl -L https://raw.githubusercontent.com/yiisoft/yii2/blob/master/contrib/completion/zsh/_yii > ~/.zsh/completion/_yii +``` + +Include the directory in the `$fpath`, e.g. by adding it to `~/.zshrc` + +``` +fpath=(~/.zsh/completion $fpath) +``` + +Make sure `compinit` is loaded or do it by adding in `~/.zshrc` + +``` +autoload -Uz compinit && compinit -i +``` + +Then reload your shell + +``` +exec $SHELL -l +``` Creating your own console commands ---------------------------------- diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index a55b115ce1..6e8f6fb58d 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -70,7 +70,7 @@ class HelpController extends Controller /** * List all available controllers and actions in machine readable format. - * This is used for bash completion. + * This is used for shell completion. * @since 2.0.11 */ public function actionList() @@ -94,6 +94,84 @@ class HelpController extends Controller } } + /** + * List all available options for the $action in machine readable format. + * This is used for shell completion. + * + * @param string $action route to action + * @since 2.0.11 + */ + public function actionListActionOptions($action) + { + $result = Yii::$app->createController($action); + + if ($result === false || !($result[0] instanceof Controller)) { + return; + } + + /** @var Controller $controller */ + list($controller, $actionID) = $result; + $action = $controller->createAction($actionID); + if ($action === null) { + return; + } + + $arguments = $controller->getActionArgsHelp($action); + foreach ($arguments as $argument => $help) { + $description = str_replace("\n", '', addcslashes($help['comment'], ':')) ?: $argument; + $this->stdout($argument . ':' . $description . "\n"); + } + + $this->stdout("\n"); + $options = $controller->getActionOptionsHelp($action); + foreach ($options as $argument => $help) { + $description = str_replace("\n", '', addcslashes($help['comment'], ':')) ?: $argument; + $this->stdout('--' . $argument . ':' . $description . "\n"); + } + } + + /** + * Displays usage information for $action + * + * @param string $action route to action + * @since 2.0.11 + */ + public function actionUsage($action) + { + $result = Yii::$app->createController($action); + + if ($result === false || !($result[0] instanceof Controller)) { + return; + } + + /** @var Controller $controller */ + list($controller, $actionID) = $result; + $action = $controller->createAction($actionID); + if ($action === null) { + return; + } + + $scriptName = $this->getScriptName(); + if ($action->id === $controller->defaultAction) { + $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW)); + } else { + $this->stdout($scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW)); + } + + $args = $controller->getActionArgsHelp($action); + foreach ($args as $name => $arg) { + if ($arg['required']) { + $this->stdout(' <' . $name . '>', Console::FG_CYAN); + } else { + $this->stdout(' [' . $name . ']', Console::FG_CYAN); + } + } + + $this->stdout("\n"); + + return; + } + /** * Returns all available command names. * @return array all available command names diff --git a/tests/framework/console/controllers/HelpControllerTest.php b/tests/framework/console/controllers/HelpControllerTest.php index 06c008c264..e508b16662 100644 --- a/tests/framework/console/controllers/HelpControllerTest.php +++ b/tests/framework/console/controllers/HelpControllerTest.php @@ -45,7 +45,7 @@ class HelpControllerTest extends TestCase return $controller->flushStdOutBuffer(); } - public function testActionIndex() + public function testActionList() { $this->mockApplication([ 'enableCoreCommands' => false, @@ -64,6 +64,8 @@ cache/index help help/index help/list +help/list-action-options +help/usage migrate migrate/create migrate/down @@ -78,7 +80,45 @@ STRING , $result); } - public function testActionList() + public function testActionListActionOptions() + { + $this->mockApplication([ + 'enableCoreCommands' => false, + 'controllerMap' => [ + 'migrate' => 'yii\console\controllers\MigrateController', + 'cache' => 'yii\console\controllers\CacheController', + ], + ]); + $result = Console::stripAnsiFormat($this->runControllerAction('list-action-options', ['action' => 'help/list-action-options'])); + $this->assertEquals(<<mockApplication([ + 'enableCoreCommands' => false, + 'controllerMap' => [ + 'migrate' => 'yii\console\controllers\MigrateController', + 'cache' => 'yii\console\controllers\CacheController', + ], + ]); + $result = Console::stripAnsiFormat($this->runControllerAction('usage', ['action' => 'help/list-action-options'])); + $this->assertEquals(<< + +STRING + , $result); + } + + public function testActionIndex() { $result = Console::stripAnsiFormat($this->runControllerAction('index')); $this->assertContains('This is Yii version ', $result); @@ -121,4 +161,4 @@ STRING class BufferedHelpController extends HelpController { use StdOutBufferControllerTrait; -} \ No newline at end of file +} From 3e6074f32d78e50c2fc76c760bcc7e03ac5a4ad2 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Sat, 3 Dec 2016 00:03:14 +0100 Subject: [PATCH 033/105] bash completion for ./yii command options --- contrib/completion/bash/yii | 46 +++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/contrib/completion/bash/yii b/contrib/completion/bash/yii index 5f6cdc8263..8f5ea8c58e 100644 --- a/contrib/completion/bash/yii +++ b/contrib/completion/bash/yii @@ -12,21 +12,47 @@ _yii() { - local cur opts yii + local cur opts yii command COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" yii="${COMP_WORDS[0]}" - # only complete first argument - if [[ COMP_CWORD -eq 1 ]] ; then - # fetch available commands from ./yii help/list command - opts=$($yii help/list) - # generate completion suggestions - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - fi + # exit if ./yii does not exist + test -f $yii || return 0 + + # lookup for command + for word in ${COMP_WORDS[@]:1}; do + if [[ $word != -* ]]; then + command=$word + break + fi + done + + [[ $cur == $command ]] && state="command" + [[ $cur != $command ]] && state="option" + [[ $cur = *=* ]] && state="value" + + case $state in + command) + # complete command/route if not given + # fetch available commands from ./yii help/list command + opts=$($yii help/list 2> /dev/null) + ;; + option) + # fetch available options from ./yii help/list-action-options command + opts=$($yii help/list-action-options $command 2> /dev/null | grep -o '^--[a-zA-Z0-9]*') + ;; + value) + # TODO allow normal file completion after an option, e.g. --migrationPath=... + ;; + esac + + # generate completion suggestions + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + } # register completion for the ./yii command # you may adjust this line if your command file is named differently -complete -F _yii ./yii +complete -F _yii ./yii yii From 1454d3fa37672a5e771b8d4ab8b83d18a8ec3216 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Mon, 28 Nov 2016 16:34:35 +0200 Subject: [PATCH 034/105] Updated CHANGELOG fixed typo --- contrib/completion/bash/yii | 2 +- framework/CHANGELOG.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/contrib/completion/bash/yii b/contrib/completion/bash/yii index 8f5ea8c58e..084325ad20 100644 --- a/contrib/completion/bash/yii +++ b/contrib/completion/bash/yii @@ -6,7 +6,7 @@ # - http://www.yiiframework.com/doc-2.0/guide-tutorial-console.html#bash-completion # # Usage: -# Temporarily you can source this file in you bash by typing: source yii.bash-completion +# Temporarily you can source this file in you bash by typing: source yii # For permanent availability, copy or link this file to /etc/bash_completion.d/ # diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index f274c7df4d..4c9d63017c 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -28,10 +28,9 @@ Yii Framework 2 Change Log - Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov) - Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) +- Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire) - Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) - Enh #6809: Added `yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) -- Enh #475: Added basic bash completion support for the `./yii` command (cebe) -- Enh #6809: Added `\yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) - Enh #7333: Improved error message for `yii\di\Instance::ensure()` when a component does not exist (cebe) - Enh #7420: Attributes for prompt generated with `renderSelectOptions` of `\yii\helpers\Html` helper (arogachev) - Enh #9162: Added support of closures in `value` for attributes in `yii\widgets\DetailView` (arogachev) From 1d3495d84dac5cabbf59f8e331a75426c483393d Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 5 Oct 2016 11:50:50 +0200 Subject: [PATCH 035/105] Alternative implementation for Unknown command suggestions alternative implementation for #12661, proof of concept, does not include filtering by similarity yet. issue #12659 --- framework/console/Application.php | 2 +- framework/console/ErrorHandler.php | 11 ++- framework/console/UnknownCommandException.php | 81 +++++++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 framework/console/UnknownCommandException.php diff --git a/framework/console/Application.php b/framework/console/Application.php index e550bfc6f7..ab096f5c1c 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -180,7 +180,7 @@ class Application extends \yii\base\Application $res = parent::runAction($route, $params); return is_object($res) ? $res : (int)$res; } catch (InvalidRouteException $e) { - throw new Exception("Unknown command \"$route\".", 0, $e); + throw new UnknownCommandException($route, $this, 0, $e); } } diff --git a/framework/console/ErrorHandler.php b/framework/console/ErrorHandler.php index c2ef846e9a..adae784f45 100644 --- a/framework/console/ErrorHandler.php +++ b/framework/console/ErrorHandler.php @@ -29,7 +29,16 @@ class ErrorHandler extends \yii\base\ErrorHandler */ protected function renderException($exception) { - if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) { + if ($exception instanceof UnknownCommandException) { + // display message and suggest alternatives in case of unknown command + $message = $this->formatMessage($exception->getName() . ': ') . $exception->command; + $alternatives = $exception->suggestAlternatives(); + if (count($alternatives) == 1) { + $message .= "\n\nDid you mean " . reset($alternatives) . " ?"; + } elseif (count($alternatives) > 1) { + $message .= "\n\nDid you mean one of these?\n - " . implode("\n - ", $alternatives); + } + } elseif ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) { $message = $this->formatMessage($exception->getName() . ': ') . $exception->getMessage(); } elseif (YII_DEBUG) { if ($exception instanceof Exception) { diff --git a/framework/console/UnknownCommandException.php b/framework/console/UnknownCommandException.php new file mode 100644 index 0000000000..3d288e56b3 --- /dev/null +++ b/framework/console/UnknownCommandException.php @@ -0,0 +1,81 @@ + + * @since 2.0.11 + */ +class UnknownCommandException extends Exception +{ + public $command; + /** + * @var Application + */ + public $application; + + public function __construct($route, $application, $code = 0, \Exception $previous = null) + { + $this->command = $route; + $this->application = $application; + parent::__construct("Unknown command \"$route\".", $code, $previous); + } + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Unknown command'; + } + + public function suggestAlternatives() + { + $help = $this->application->createController('help'); + if ($help === false) { + return []; + } + /** @var $helpController HelpController */ + list($helpController, $actionID) = $help; + + $availableActions = []; + $commands = $helpController->getCommands(); + foreach ($commands as $command) { + $result = $this->application->createController($command); + if ($result === false) { + continue; + } + // add the command itself (default action) + $availableActions[] = $command; + + // add all actions of this controller + /** @var $controller Controller */ + list($controller, $actionID) = $result; + $actions = $helpController->getActions($controller); + if (!empty($actions)) { + $prefix = $controller->getUniqueId(); + foreach ($actions as $action) { + $availableActions[] = $prefix . '/' . $action; + } + } + } + $availableActions = $this->filterBySimilarity($availableActions); + + asort($availableActions); + return $availableActions; + } + + private function filterBySimilarity($actions) + { + // TODO + return $actions; + } +} From 187c44e43e452ba656fcb58eceb89a920a564dbd Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Sat, 26 Nov 2016 23:38:22 +0100 Subject: [PATCH 036/105] implement suggestion for unknown command in console application suggestion is based on two principles: - first suggest commands the begin with the unknown name, to suggest commands after accidentally hitting enter - second find similar commands by computing the levenshtein distance which is a measurement on how many changes need to be made to convert one string into another. This is perfect for finding typos. --- framework/CHANGELOG.md | 1 + framework/console/ErrorHandler.php | 6 +- framework/console/UnknownCommandException.php | 80 ++++++++++++++++--- .../console/UnkownCommandExceptionTest.php | 70 ++++++++++++++++ 4 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 tests/framework/console/UnkownCommandExceptionTest.php diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 822efbc255..f46d889bd5 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -41,6 +41,7 @@ Yii Framework 2 Change Log - Enh #12399: Added `ActiveField::addAriaAttributes` property for `aria-required` and `aria-invalid` attributes rendering (Oxyaction, samdark) - Enh #12390: Avoid creating queries with false where condition (`0=1`) when fetching relational data (klimov-paul) - Enh #12619: Added catch `Throwable` in `yii\base\ErrorHandler::handleException()` (rob006) +- Enh #12659: Suggest alternatives when console command was not found (mdmunir, cebe) - Enh #12726: `yii\base\Application::$version` converted to `yii\base\Module::$version` virtual property, allowing to specify version as a PHP callback (klimov-paul) - Enh #12738: Added support for creating protocol-relative URLs in `UrlManager::createAbsoluteUrl()` and `Url` helper methods (rob006) - Enh #12748: Added Migration tool automatic generation reference column for foreignKey (MKiselev) diff --git a/framework/console/ErrorHandler.php b/framework/console/ErrorHandler.php index adae784f45..cf62613a99 100644 --- a/framework/console/ErrorHandler.php +++ b/framework/console/ErrorHandler.php @@ -32,9 +32,9 @@ class ErrorHandler extends \yii\base\ErrorHandler if ($exception instanceof UnknownCommandException) { // display message and suggest alternatives in case of unknown command $message = $this->formatMessage($exception->getName() . ': ') . $exception->command; - $alternatives = $exception->suggestAlternatives(); - if (count($alternatives) == 1) { - $message .= "\n\nDid you mean " . reset($alternatives) . " ?"; + $alternatives = $exception->getSuggestedAlternatives(); + if (count($alternatives) === 1) { + $message .= "\n\nDid you mean \"" . reset($alternatives) . "\"?"; } elseif (count($alternatives) > 1) { $message .= "\n\nDid you mean one of these?\n - " . implode("\n - ", $alternatives); } diff --git a/framework/console/UnknownCommandException.php b/framework/console/UnknownCommandException.php index 3d288e56b3..51c48c1a82 100644 --- a/framework/console/UnknownCommandException.php +++ b/framework/console/UnknownCommandException.php @@ -6,22 +6,35 @@ */ namespace yii\console; + use yii\console\controllers\HelpController; /** - * Exception represents an exception caused by incorrect usage of a console command. + * UnknownCommandException represents an exception caused by incorrect usage of a console command. * * @author Carsten Brandt * @since 2.0.11 */ class UnknownCommandException extends Exception { + /** + * @var string the name of the command that could not be recognized. + */ public $command; /** * @var Application */ - public $application; + protected $application; + + /** + * Construct the exception. + * + * @param string $route the route of the command that could not be found. + * @param Application $application the console application instance involved. + * @param int $code the Exception code. + * @param \Exception $previous the previous exception used for the exception chaining. + */ public function __construct($route, $application, $code = 0, \Exception $previous = null) { $this->command = $route; @@ -37,7 +50,20 @@ class UnknownCommandException extends Exception return 'Unknown command'; } - public function suggestAlternatives() + /** + * Suggest alternative commands for [[$command]] based on string similarity. + * + * Alternatives are searched using the following steps: + * + * - suggest alternatives that begin with `$command` + * - find typos by calculating the Levenshtein distance between the unknown command and all + * available commands. The Levenshtein distance is defined as the minimal number of + * characters you have to replace, insert or delete to transform str1 into str2. + * + * @see http://php.net/manual/en/function.levenshtein.php + * @return array a list of suggested alternatives sorted by similarity. + */ + public function getSuggestedAlternatives() { $help = $this->application->createController('help'); if ($help === false) { @@ -67,15 +93,49 @@ class UnknownCommandException extends Exception } } } - $availableActions = $this->filterBySimilarity($availableActions); - - asort($availableActions); - return $availableActions; + return $this->filterBySimilarity($availableActions, $this->command); } - private function filterBySimilarity($actions) + /** + * Find suggest alternative commands based on string similarity. + * + * Alternatives are searched using the following steps: + * + * - suggest alternatives that begin with `$command` + * - find typos by calculating the Levenshtein distance between the unknown command and all + * available commands. The Levenshtein distance is defined as the minimal number of + * characters you have to replace, insert or delete to transform str1 into str2. + * + * @see http://php.net/manual/en/function.levenshtein.php + * @param array $actions available command names. + * @param string $command the command to compare to. + * @return array a list of suggested alternatives sorted by similarity. + */ + private function filterBySimilarity($actions, $command) { - // TODO - return $actions; + $alternatives = []; + + // suggest alternatives that begin with $command first + foreach ($actions as $action) { + if (strpos($action, $command) === 0) { + $alternatives[] = $action; + } + } + + // calculate the Levenshtein distance between the unknown command and all available commands. + $distances = array_map(function($action) use ($command) { + $action = strlen($action) > 255 ? substr($action, 0, 255) : $action; + $command = strlen($command) > 255 ? substr($command, 0, 255) : $command; + return levenshtein($action, $command); + }, array_combine($actions, $actions)); + + // we assume a typo if the levensthein distance is no more than 3, i.e. 3 replacements needed + $relevantTypos = array_filter($distances, function($distance) { + return $distance <= 3; + }); + asort($relevantTypos); + $alternatives = array_merge($alternatives, array_flip($relevantTypos)); + + return array_unique($alternatives); } } diff --git a/tests/framework/console/UnkownCommandExceptionTest.php b/tests/framework/console/UnkownCommandExceptionTest.php new file mode 100644 index 0000000000..88897ede55 --- /dev/null +++ b/tests/framework/console/UnkownCommandExceptionTest.php @@ -0,0 +1,70 @@ +mockApplication([ + 'enableCoreCommands' => false, + 'controllerMap' => [ + 'cache' => 'yii\console\controllers\CacheController', + 'migrate' => 'yii\console\controllers\MigrateController', + 'message' => 'yii\console\controllers\MessageController', + ], + ]); + } + + public function suggestedCommandsProvider() + { + return [ + ['migate', ['migrate']], + ['mihate/u', ['migrate/up']], + ['mirgte/u', ['migrate/up']], + ['mirgte/up', ['migrate/up']], + ['mirgte', ['migrate']], + ['hlp', ['help']], + ['ca', ['cache', 'cache/flush', 'cache/flush-all', 'cache/flush-schema', 'cache/index']], + ['cach', ['cache', 'cache/flush', 'cache/flush-all', 'cache/flush-schema', 'cache/index']], + ['cach/fush', ['cache/flush']], + ['cach/fushall', ['cache/flush-all']], + ['what?', []], + // test UTF 8 chars + ['ёлка', []], + // this crashes levenshtein because string is longer than 255 chars + [str_repeat('asdw1234', 31), []], + [str_repeat('asdw1234', 32), []], + [str_repeat('asdw1234', 33), []], + ]; + } + + /** + * @dataProvider suggestedCommandsProvider + */ + public function testSuggestCommand($command, $expectedSuggestion) + { + $exception = new UnknownCommandException($command, Yii::$app); + $this->assertEquals($expectedSuggestion, $exception->getSuggestedAlternatives()); + } + + public function testNameAndConstructor() + { + $exception = new UnknownCommandException('test', Yii::$app); + $this->assertEquals('Unknown command', $exception->getName()); + $this->assertEquals('test', $exception->command); + } +} From 35998449eba8e295f5ed420350765c1196bf13e3 Mon Sep 17 00:00:00 2001 From: Robert Korulczyk Date: Sat, 3 Dec 2016 07:25:39 +0100 Subject: [PATCH 037/105] Add `yii\filters\HostControl::$fallbackHostInfo` (#13117) --- docs/guide/security-best-practices.md | 1 + framework/CHANGELOG.md | 2 +- framework/filters/HostControl.php | 25 +++++++++++++++--- tests/framework/filters/HostControlTest.php | 29 ++++++++++++++++++++- 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/docs/guide/security-best-practices.md b/docs/guide/security-best-practices.md index 318aba9101..f6d3786748 100644 --- a/docs/guide/security-best-practices.md +++ b/docs/guide/security-best-practices.md @@ -256,6 +256,7 @@ return [ 'example.com', '*.example.com', ], + 'fallbackHostInfo' => 'https://example.com', ], // ... ]; diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 12cf914bd0..a64b1e5dbe 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -58,8 +58,8 @@ Yii Framework 2 Change Log - Enh #13036: Added shortcut methods `asJson()` and `asXml()` for returning JSON and XML data in web controller actions (cebe) - Enh #13020: Added `disabledListItemSubTagOptions` attribute for `yii\widgets\LinkPager` in order to customize the disabled list item sub tag element (nadar) - Enh #12988: Changed `textarea` method within the `yii\helpers\BaseHtml` class to allow users to control whether html entities found within `$value` will be double-encoded or not (cyphix333) +- Enh #13050: Added `yii\filters\HostControl` allowing protection against 'host header' attacks (klimov-paul, rob006) - Enh #13074: Improved `yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks) -- Enh #13050: Added `yii\filters\HostControl` allowing protection against 'host header' attacks (klimov-paul) - Enh #11758: Implemented Dependency Injection Container configuration using Application configuration array (silverfire) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) - Enh #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions (zalatov) diff --git a/framework/filters/HostControl.php b/framework/filters/HostControl.php index d89129d61b..bbc9402e73 100644 --- a/framework/filters/HostControl.php +++ b/framework/filters/HostControl.php @@ -105,6 +105,14 @@ class HostControl extends ActionFilter * host name, creation of absolute URL links, caching page parts and so on. */ public $denyCallback; + /** + * @var string|null fallback host info (e.g. `http://www.yiiframework.com`) used when [[\yii\web\Request::$hostInfo|Request::$hostInfo]] is invalid. + * This value will replace [[\yii\web\Request::$hostInfo|Request::$hostInfo]] before [[$denyCallback]] is called to make sure that + * an invalid host will not be used for further processing. You can set it to `null` to leave [[\yii\web\Request::$hostInfo|Request::$hostInfo]] untouched. + * Default value is empty string (this will result creating relative URLs instead of absolute). + * @see \yii\web\Request::getHostInfo() + */ + public $fallbackHostInfo = ''; /** @@ -132,6 +140,11 @@ class HostControl extends ActionFilter } } + // replace invalid host info to prevent using it in further processing + if ($this->fallbackHostInfo !== null) { + Yii::$app->getRequest()->setHostInfo($this->fallbackHostInfo); + } + if ($this->denyCallback !== null) { call_user_func($this->denyCallback, $action); } else { @@ -147,18 +160,24 @@ class HostControl extends ActionFilter * You may override this method, creating your own deny access handler. While doing so, make sure you * avoid usage of the current requested host name, creation of absolute URL links, caching page parts and so on. * @param \yii\base\Action $action the action to be executed. + * @throws NotFoundHttpException */ protected function denyAccess($action) { + $exception = new NotFoundHttpException(Yii::t('yii', 'Page not found.')); + + // use regular error handling if $this->fallbackHostInfo was set + if (!empty(Yii::$app->getRequest()->hostName)) { + throw $exception; + } + $response = Yii::$app->getResponse(); $errorHandler = Yii::$app->getErrorHandler(); - $exception = new NotFoundHttpException(Yii::t('yii', 'Page not found.')); - $response->setStatusCode($exception->statusCode, $exception->getMessage()); $response->data = $errorHandler->renderFile($errorHandler->errorView, ['exception' => $exception]); $response->send(); Yii::$app->end(); } -} \ No newline at end of file +} diff --git a/tests/framework/filters/HostControlTest.php b/tests/framework/filters/HostControlTest.php index 8097f0ca95..ed53d4d5a6 100644 --- a/tests/framework/filters/HostControlTest.php +++ b/tests/framework/filters/HostControlTest.php @@ -131,4 +131,31 @@ class HostControlTest extends TestCase $this->assertFalse($filter->beforeAction($action)); $this->assertTrue($this->denyCallBackCalled, 'denyCallback should have been called.'); } -} \ No newline at end of file + + public function testDefaultHost() + { + $filter = new HostControl(); + $filter->allowedHosts = ['example.com']; + $filter->fallbackHostInfo = 'http://yiiframework.com'; + $filter->denyCallback = function() {}; + + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $filter->beforeAction($action); + + $this->assertSame('yiiframework.com', Yii::$app->getRequest()->getHostName()); + } + + public function testErrorHandlerWithDefaultHost() + { + $this->setExpectedException('yii\web\NotFoundHttpException', 'Page not found.'); + + $filter = new HostControl(); + $filter->allowedHosts = ['example.com']; + $filter->fallbackHostInfo = 'http://yiiframework.com'; + + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $filter->beforeAction($action); + } +} From c5a88ae0b59366494e83298ae11e187b6110cfc5 Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Sat, 3 Dec 2016 10:31:29 +0100 Subject: [PATCH 038/105] Update security-authorization.md fix namespace to fix #13125 --- docs/guide/security-authorization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/security-authorization.md b/docs/guide/security-authorization.md index c37576df04..7cb6f176da 100644 --- a/docs/guide/security-authorization.md +++ b/docs/guide/security-authorization.md @@ -328,7 +328,7 @@ public function signup() $user->save(false); // the following three lines were added: - $auth = Yii::$app->authManager; + $auth = \Yii::$app->authManager; $authorRole = $auth->getRole('author'); $auth->assign($authorRole, $user->getId()); From ce9e6fd6f987bc147095bd061517574451234924 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Sat, 3 Dec 2016 19:42:27 +0100 Subject: [PATCH 039/105] disable codeclimate complaining about else statements encouraging early return is good, but this check complains about every single else statement in the code. see https://github.com/yiisoft/yii2/pull/13126#issuecomment-264653343 --- .codeclimate.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.codeclimate.yml b/.codeclimate.yml index d8166d7a26..2debb0dcb5 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -14,6 +14,10 @@ engines: enabled: true # configure checks, see https://phpmd.org/rules/index.html for details checks: + # else is not always bad. Disabling this as there is no reason to differentiate + # between early return and normal else cases. + CleanCode/ElseExpression: + enabled: false # Static access on Yii::$app is normal in Yii CleanCode/StaticAccess: enabled: false From 496310e7e30be8e981657a9cc31b30c834c6c8cd Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Sun, 4 Dec 2016 02:50:22 +0100 Subject: [PATCH 040/105] fixed typo in security guide about namespace backslash vs. slash fixes #13132 --- docs/guide-es/security-authorization.md | 2 +- docs/guide-fr/security-authorization.md | 2 +- docs/guide-ja/security-authorization.md | 2 +- docs/guide-ru/security-authorization.md | 2 +- docs/guide/security-authorization.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide-es/security-authorization.md b/docs/guide-es/security-authorization.md index 669912fc30..11d01a914a 100644 --- a/docs/guide-es/security-authorization.md +++ b/docs/guide-es/security-authorization.md @@ -304,7 +304,7 @@ class RbacController extends Controller ``` > Note: Si estas utilizando el template avanzado, necesitas poner tu `RbacController` dentro del directorio `console/controllers` - y cambiar el espacio de nombres a `console/controllers`. + y cambiar el espacio de nombres a `console\controllers`. Después de ejecutar el comando `yii rbac/init`, obtendremos la siguiente jerarquía: diff --git a/docs/guide-fr/security-authorization.md b/docs/guide-fr/security-authorization.md index 609ebe7183..a6a06c968a 100644 --- a/docs/guide-fr/security-authorization.md +++ b/docs/guide-fr/security-authorization.md @@ -258,7 +258,7 @@ class RbacController extends Controller } ``` -> Note: si vous utilisez le modèle avancé, vous devez mettre votre `RbacController` dans le dossier `console/controllers` et changer l'espace de noms en `console/controllers`. +> Note: si vous utilisez le modèle avancé, vous devez mettre votre `RbacController` dans le dossier `console/controllers` et changer l'espace de noms en `console\controllers`. Après avoir exécuté la commande `yii rbac/init` vous vous retrouverez avec la hiérarchie suivante : diff --git a/docs/guide-ja/security-authorization.md b/docs/guide-ja/security-authorization.md index 340e4536e3..e3849d53b9 100644 --- a/docs/guide-ja/security-authorization.md +++ b/docs/guide-ja/security-authorization.md @@ -306,7 +306,7 @@ class RbacController extends Controller ``` > Note: アドバンストテンプレートを使おうとするときは、`RbacController` を `console/controllers` -ディレクトリの中に置いて、名前空間を `console/controllers` に変更する必要があります。 +ディレクトリの中に置いて、名前空間を `console\controllers` に変更する必要があります。 `yii rbac/init` によってコマンドを実行した後には、次の権限階層が得られます。 diff --git a/docs/guide-ru/security-authorization.md b/docs/guide-ru/security-authorization.md index 51a2ae1338..15e8013ff1 100644 --- a/docs/guide-ru/security-authorization.md +++ b/docs/guide-ru/security-authorization.md @@ -298,7 +298,7 @@ class RbacController extends Controller ``` > Note: Если вы используете шаблон проекта advanced, `RbacController` необходимо создать в директории `console/controllers` - и сменить пространство имён на `console/controllers`. + и сменить пространство имён на `console\controllers`. После выполнения команды `yii rbac/init` мы получим следующую иерархию: diff --git a/docs/guide/security-authorization.md b/docs/guide/security-authorization.md index 7cb6f176da..2daf66effd 100644 --- a/docs/guide/security-authorization.md +++ b/docs/guide/security-authorization.md @@ -304,7 +304,7 @@ class RbacController extends Controller ``` > Note: If you are using advanced template, you need to put your `RbacController` inside `console/controllers` directory - and change namespace to `console/controllers`. + and change namespace to `console\controllers`. After executing the command with `yii rbac/init` we'll get the following hierarchy: From d7436dea83d85e0bc10142c98f00e22a3a43a131 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Sun, 4 Dec 2016 13:17:38 +0100 Subject: [PATCH 041/105] fixed urls in bash/zsh installation instructions --- docs/guide/tutorial-console.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/guide/tutorial-console.md b/docs/guide/tutorial-console.md index db572eaaee..0f2bf86e6b 100644 --- a/docs/guide/tutorial-console.md +++ b/docs/guide/tutorial-console.md @@ -119,7 +119,10 @@ Make sure bash completion is installed. For most of installations it is availabl Place the completion script in `/etc/bash_completion.d/`: - curl -L https://raw.githubusercontent.com/yiisoft/yii2/blob/master/contrib/completion/bash/yii > /etc/bash_completion.d/yii + curl -L https://raw.githubusercontent.com/yiisoft/yii2/master/contrib/completion/bash/yii -o /etc/bash_completion.d/yii + +For temporary usage you can put the file into the current directory and include it in the current session via `source yii`. +If globally installed you may need to restart the terminal or `source ~/.bashrc` to activate it. Check the [Bash Manual](https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html) for other ways of including completion script to your environment. @@ -130,7 +133,7 @@ Put the completion script in directory for completions, using e.g. `~/.zsh/compl ``` mkdir -p ~/.zsh/completion -curl -L https://raw.githubusercontent.com/yiisoft/yii2/blob/master/contrib/completion/zsh/_yii > ~/.zsh/completion/_yii +curl -L https://raw.githubusercontent.com/yiisoft/yii2/master/contrib/completion/zsh/_yii -o ~/.zsh/completion/_yii ``` Include the directory in the `$fpath`, e.g. by adding it to `~/.zshrc` From 72532fe910ba48340356d3339c5b4dd4665c2c42 Mon Sep 17 00:00:00 2001 From: Evgeniy Tkachenko Date: Mon, 5 Dec 2016 12:54:01 +0200 Subject: [PATCH 042/105] Fixed code formatting in concept-di-container.md [skip-ci] (#13136) --- docs/guide/concept-di-container.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/guide/concept-di-container.md b/docs/guide/concept-di-container.md index 2b561e5756..0def6287f6 100644 --- a/docs/guide/concept-di-container.md +++ b/docs/guide/concept-di-container.md @@ -379,21 +379,22 @@ Say we work on API application and have: set to `json` on creation - `app\storage\FileStorage` and `app\storage\DocumentsReader` classes the implement some logic on working with documents that are located in some file storage: - ```php - class FileStorage - { - public function __contruct($root) { - // whatever - } - } + +```php +class FileStorage +{ + public function __contruct($root) { + // whatever + } +} - class DocumentsReader - { - public function __contruct(FileStorage $fs) { - // whatever - } - } - ``` +class DocumentsReader +{ + public function __contruct(FileStorage $fs) { + // whatever + } +} +``` It is possible to configure multiple definitions at once, passing configuration array to [[yii\di\Container::setDefinitions()|setDefinitions()]] or [[yii\di\Container::setSingletons()|setSingletons()]] method. From 9c1f892ecfae0b90f731301b20e982b3ccbc0dc9 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 5 Dec 2016 12:57:33 +0100 Subject: [PATCH 043/105] Update concept-di-container.md put code block into the list --- docs/guide/concept-di-container.md | 43 +++++++++++++++--------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/docs/guide/concept-di-container.md b/docs/guide/concept-di-container.md index 0def6287f6..1f74678fee 100644 --- a/docs/guide/concept-di-container.md +++ b/docs/guide/concept-di-container.md @@ -374,27 +374,28 @@ Advanced Practical Usage --------------- Say we work on API application and have: - - `app\components\Request` class that extends `yii\web\Request` and provides additional functionality - - `app\components\Response` class that extends `yii\web\Response` and should have `format` property - set to `json` on creation - - `app\storage\FileStorage` and `app\storage\DocumentsReader` classes the implement some logic on - working with documents that are located in some file storage: - -```php -class FileStorage -{ - public function __contruct($root) { - // whatever - } -} - -class DocumentsReader -{ - public function __contruct(FileStorage $fs) { - // whatever - } -} -``` + +- `app\components\Request` class that extends `yii\web\Request` and provides additional functionality +- `app\components\Response` class that extends `yii\web\Response` and should have `format` property + set to `json` on creation +- `app\storage\FileStorage` and `app\storage\DocumentsReader` classes the implement some logic on + working with documents that are located in some file storage: + + ```php + class FileStorage + { + public function __contruct($root) { + // whatever + } + } + + class DocumentsReader + { + public function __contruct(FileStorage $fs) { + // whatever + } + } + ``` It is possible to configure multiple definitions at once, passing configuration array to [[yii\di\Container::setDefinitions()|setDefinitions()]] or [[yii\di\Container::setSingletons()|setSingletons()]] method. From eb6ea11931a9bc0413f5fed2e9720c9349e21350 Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Mon, 5 Dec 2016 13:57:07 +0100 Subject: [PATCH 044/105] Fix docs about table prefixes (#13127) * fixes #8354 to improve docs on table prefixes * accents * patched silverfires comments * Update db-active-record.md improved wording and added link about quoting. * fix link to DAO guide --- docs/guide/db-active-record.md | 20 ++++++++++++++++---- docs/guide/db-query-builder.md | 3 +++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/guide/db-active-record.md b/docs/guide/db-active-record.md index 8083a3fd99..e93a0f4b95 100644 --- a/docs/guide/db-active-record.md +++ b/docs/guide/db-active-record.md @@ -50,9 +50,20 @@ However, most content described here are also applicable to Active Record for No ## Declaring Active Record Classes -To get started, declare an Active Record class by extending [[yii\db\ActiveRecord]]. Because each Active Record -class is associated with a database table, in this class you should override the [[yii\db\ActiveRecord::tableName()|tableName()]] -method to specify which table the class is associated with. +To get started, declare an Active Record class by extending [[yii\db\ActiveRecord]]. + +### Setting a table name + +By default each Active Record class is associated with its database table. +The [[yii\db\ActiveRecord::tableName()|tableName()]] method returns the table name by converting the class name via [[yii\helpers\Inflector::camel2id()]]. +You may override this method if the table is not named after this convention. + +Also a default [[yii\db\Connection::$tablePrefix|tablePrefix]] can be applied. For example if + [[yii\db\Connection::$tablePrefix|tablePrefix]] is `tbl_`, `Customer` becomes `tbl_customer` and `OrderItem` becomes `tbl_order_item`. + +If a table name is given as `{{%TableName}}`, then the percentage character `%` will be replaced with the table prefix. +For example, `{{%post}}` becomes `{{tbl_post}}`. The brackets around the table name are used for +[quoting in an SQL query](db-dao.md#quoting-table-and-column-names). In the following example, we declare an Active Record class named `Customer` for the `customer` database table. @@ -71,11 +82,12 @@ class Customer extends ActiveRecord */ public static function tableName() { - return 'customer'; + return '{{customer}}'; } } ``` +### Active records are called "models" Active Record instances are considered as [models](structure-models.md). For this reason, we usually put Active Record classes under the `app\models` namespace (or other namespaces for keeping model classes). diff --git a/docs/guide/db-query-builder.md b/docs/guide/db-query-builder.md index c7c7de298f..d39d536b60 100644 --- a/docs/guide/db-query-builder.md +++ b/docs/guide/db-query-builder.md @@ -153,6 +153,9 @@ $subQuery = (new Query())->select('id')->from('user')->where('status=1'); $query->from(['u' => $subQuery]); ``` +#### Prefixes +Also a default [[yii\db\Connection::$tablePrefix|tablePrefix]] can be applied. Implementation instructions +are in the ["Quoting Tables" section of the "Database Access Objects" guide](guide-db-dao.html#quoting-table-and-column-names). ### [[yii\db\Query::where()|where()]] From 7dac3ed1ca70f2ed56f69e23bb1658067d223c31 Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Mon, 5 Dec 2016 13:58:11 +0100 Subject: [PATCH 045/105] Kirkhansen 9053 add radio button column (#13126) * Added the RadioButtonColumn class and updated the changelog. * - several enhancements: - unit tests added - changelog fixed * Update CHANGELOG.md * small markdown fixes * fix travis: custom ID --- framework/CHANGELOG.md | 1 + framework/grid/RadioButtonColumn.php | 92 +++++++++++++++ .../framework/grid/RadiobuttonColumnTest.php | 105 ++++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 framework/grid/RadioButtonColumn.php create mode 100644 tests/framework/grid/RadiobuttonColumnTest.php diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index a64b1e5dbe..c50936e459 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -33,6 +33,7 @@ Yii Framework 2 Change Log - Enh #6809: Added `yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) - Enh #7333: Improved error message for `yii\di\Instance::ensure()` when a component does not exist (cebe) - Enh #7420: Attributes for prompt generated with `renderSelectOptions` of `\yii\helpers\Html` helper (arogachev) +- Enh #9053: Added`yii\grid\RadioButtonColumn` (darwinisgod) - Enh #9162: Added support of closures in `value` for attributes in `yii\widgets\DetailView` (arogachev) - Enh #10896: Select only primary key when counting records in UniqueValidator (developeruz) - Enh #11037: `yii.js` and `yii.validation.js` use `Regexp.test()` instead of `String.match()` (arogachev, nkovacs) diff --git a/framework/grid/RadioButtonColumn.php b/framework/grid/RadioButtonColumn.php new file mode 100644 index 0000000000..e49ac3a3ae --- /dev/null +++ b/framework/grid/RadioButtonColumn.php @@ -0,0 +1,92 @@ + [ + * // ... + * [ + * 'class' => 'yii\grid\RadioButtonColumn', + * 'radioOptions' => function ($model) { + * return [ + * 'value' => $model['value'], + * 'checked' => $model['value'] == 2 + * ]; + * } + * ], + * ] + * ``` + * + * @author Kirk Hansen + * @since 2.0.11 + */ +class RadioButtonColumn extends Column +{ + /** + * @var string the name of the input radio button input fields. + */ + public $name = 'radioButtonSelection'; + /** + * @var array|\Closure the HTML attributes for the radio buttons. This can either be an array of + * attributes or an anonymous function ([[Closure]]) returning such an array. + * + * The signature of the function should be as follows: `function ($model, $key, $index, $column)` + * where `$model`, `$key`, and `$index` refer to the model, key and index of the row currently being rendered + * and `$column` is a reference to the [[RadioButtonColumn]] object. + * + * A function may be used to assign different attributes to different rows based on the data in that row. + * Specifically if you want to set a different value for the radio button you can use this option + * in the following way (in this example using the `name` attribute of the model): + * ```php + * 'radioOptions' => function ($model, $key, $index, $column) { + * return ['value' => $model->attribute]; + * } + * ``` + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $radioOptions = []; + + + /** + * @inheritdoc + * @throws \yii\base\InvalidConfigException if [[name]] is not set. + */ + public function init() + { + parent::init(); + if (empty($this->name)) { + throw new InvalidConfigException('The "name" property must be set.'); + } + } + + /** + * @inheritdoc + */ + protected function renderDataCellContent($model, $key, $index) + { + if ($this->radioOptions instanceof Closure) { + $options = call_user_func($this->radioOptions, $model, $key, $index, $this); + } else { + $options = $this->radioOptions; + if (!isset($options['value'])) { + $options['value'] = is_array($key) ? json_encode($key, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : $key; + } + } + $checked = isset($options['checked']) ? $options['checked'] : false; + return Html::radio($this->name, $checked, $options); + } +} diff --git a/tests/framework/grid/RadiobuttonColumnTest.php b/tests/framework/grid/RadiobuttonColumnTest.php new file mode 100644 index 0000000000..c247bf43e9 --- /dev/null +++ b/tests/framework/grid/RadiobuttonColumnTest.php @@ -0,0 +1,105 @@ + null + ]); + } + + public function testOptionsByArray() + { + $column = new RadioButtonColumn([ + 'radioOptions' => [ + 'value' => 42 + ] + ]); + $this->assertEquals('', $column->renderDataCell([], 1, 0)); + } + + public function testOptionsByCallback() + { + $model = [ + 'label' => 'label', + 'value' => 123 + ]; + $column = new RadioButtonColumn([ + 'radioOptions' => function ($model) { + return [ + 'value' => $model['value'] + ]; + } + ]); + $actual = $column->renderDataCell($model, 1, 0); + $this->assertEquals('', $actual); + } + + public function testMultipleInGrid() + { + $this->mockApplication(); + Yii::setAlias('@webroot', '@yiiunit/runtime'); + Yii::setAlias('@web', 'http://localhost/'); + Yii::$app->assetManager->bundles['yii\web\JqueryAsset'] = false; + Yii::$app->set('request', new Request(['url' => '/abc'])); + + $models = [ + ['label' => 'label1', 'value' => 1], + ['label' => 'label2', 'value' => 2, 'checked' => true] + ]; + $grid = new GridView([ + 'dataProvider' => new ArrayDataProvider(['allModels' => $models]), + 'options' => ['id' => 'radio-gridview'], + 'columns' => [ + [ + 'class' => RadioButtonColumn::className(), + 'radioOptions' => function ($model) { + return [ + 'value' => $model['value'], + 'checked' => $model['value'] == 2 + ]; + } + ] + ] + ]); + ob_start(); + $grid->run(); + $actual = ob_get_clean(); + $this->assertEqualsWithoutLE(<<
Showing 1-2 of 2 items.
+ + + + + + +
 
+ +HTML + , $actual); + } +} From 0261a5498f1fbbe9e84dc181c6fc8cada94f25bd Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Mon, 5 Dec 2016 22:27:38 +0200 Subject: [PATCH 046/105] Simplified ListViewTest --- tests/framework/widgets/ListViewTest.php | 67 ++++++++---------------- 1 file changed, 23 insertions(+), 44 deletions(-) diff --git a/tests/framework/widgets/ListViewTest.php b/tests/framework/widgets/ListViewTest.php index 7c6f991a26..5cbd727f18 100644 --- a/tests/framework/widgets/ListViewTest.php +++ b/tests/framework/widgets/ListViewTest.php @@ -20,42 +20,27 @@ class ListViewTest extends \yiiunit\TestCase public function testEmptyListShown() { - $dataProvider = new ArrayDataProvider([ - 'allModels' => [], - ]); - - ob_start(); - echo ListView::widget([ - 'dataProvider' => $dataProvider, - 'showOnEmpty' => false, + $this->getListView([ + 'dataProvider' => new ArrayDataProvider(['allModels' => []]), 'emptyText' => "Nothing at all", - ]); - $actualHtml = ob_get_clean(); + ])->run(); - $this->assertTrue(strpos($actualHtml, "Nothing at all") !== false, "displays the empty message"); - $this->assertTrue(strpos($actualHtml, '
') !== false, "adds the 'empty' class"); - $this->assertTrue(strpos($actualHtml, '
') === false, "does not display the summary"); + $this->expectOutputString('
Nothing at all
'); } public function testEmptyListNotShown() { - $dataProvider = new ArrayDataProvider([ - 'allModels' => [], - ]); - - ob_start(); - echo ListView::widget([ - 'dataProvider' => $dataProvider, + $this->getListView([ + 'dataProvider' => new ArrayDataProvider(['allModels' => []]), 'showOnEmpty' => true, - 'emptyText' => "Nothing at all", - ]); - $actualHtml = ob_get_clean(); + ])->run(); - $this->assertTrue(strpos($actualHtml, '
') === false, "does not add the 'empty' class"); - $this->assertTrue(strpos($actualHtml, '
') === false, "does not display the summary"); - $this->assertEmpty(trim(\strip_tags($actualHtml)), "contains no text"); + $this->expectOutputString(<< - $this->mockWebApplication(); +
+HTML + ); } private function getListView($options = []) @@ -79,7 +64,7 @@ class ListViewTest extends \yiiunit\TestCase public function testSimplyListView() { - $listView = $this->getListView(); + $this->getListView()->run(); $this->expectOutputString(<<
Showing 1-3 of 3 items.
@@ -89,13 +74,11 @@ class ListViewTest extends \yiiunit\TestCase
HTML ); - - $listView->run(); } public function testWidgetOptions() { - $listView = $this->getListView(['options' => ['class' => 'test-passed'], 'separator' => '']); + $this->getListView(['options' => ['class' => 'test-passed'], 'separator' => ''])->run(); $this->expectOutputString(<<
Showing 1-3 of 3 items.
@@ -103,8 +86,6 @@ HTML
HTML ); - - $listView->run(); } public function itemViewOptions() @@ -126,7 +107,7 @@ HTML
Item #0: silverfire - Widget: yii\widgets\ListView
Item #1: samdark - Widget: yii\widgets\ListView
Item #2: cebe - Widget: yii\widgets\ListView
-
' +', ], [ '@yiiunit/data/views/widgets/ListView/item', @@ -134,8 +115,8 @@ HTML
Item #0: silverfire - Widget: yii\widgets\ListView
Item #1: samdark - Widget: yii\widgets\ListView
Item #2: cebe - Widget: yii\widgets\ListView
-' - ] +', + ], ]; } @@ -144,9 +125,8 @@ HTML */ public function testItemViewOptions($itemView, $expected) { - $listView = $this->getListView(['itemView' => $itemView]); + $this->getListView(['itemView' => $itemView])->run(); $this->expectOutputString($expected); - $listView->run(); } public function itemOptions() @@ -168,8 +148,8 @@ HTML 'test' => 'passed', 'key' => $key, 'index' => $index, - 'id' => $model['id'] - ] + 'id' => $model['id'], + ], ]; }, @@ -177,8 +157,8 @@ HTML 0 1 2 -' - ] +', + ], ]; } @@ -187,8 +167,7 @@ HTML */ public function testItemOptions($itemOptions, $expected) { - $listView = $this->getListView(['itemOptions' => $itemOptions]); + $this->getListView(['itemOptions' => $itemOptions])->run(); $this->expectOutputString($expected); - $listView->run(); } } From 76e253e140ed4511af12a0d5b861b06bd6ba4821 Mon Sep 17 00:00:00 2001 From: Alexey Rogachev Date: Wed, 16 Nov 2016 13:42:17 +0600 Subject: [PATCH 047/105] `yii\validators\Validator` - added separate methods for client-side validation options Closes #11163 --- docs/guide/input-validation.md | 16 ++++++++++++++ framework/CHANGELOG.md | 1 + framework/captcha/CaptchaValidator.php | 15 ++++++++++--- framework/validators/BooleanValidator.php | 15 ++++++++++--- framework/validators/CompareValidator.php | 15 ++++++++++--- framework/validators/EmailValidator.php | 21 +++++++++++++------ framework/validators/FileValidator.php | 5 +---- framework/validators/FilterValidator.php | 15 ++++++++++--- framework/validators/IpValidator.php | 15 ++++++++++--- framework/validators/NumberValidator.php | 15 ++++++++++--- framework/validators/RangeValidator.php | 15 ++++++++++--- .../validators/RegularExpressionValidator.php | 15 ++++++++++--- framework/validators/RequiredValidator.php | 15 ++++++++++--- framework/validators/StringValidator.php | 12 ++++++++--- framework/validators/UrlValidator.php | 21 +++++++++++++------ framework/validators/Validator.php | 17 +++++++++++++++ 16 files changed, 182 insertions(+), 46 deletions(-) diff --git a/docs/guide/input-validation.md b/docs/guide/input-validation.md index 9af27d6218..7cdbdd9927 100644 --- a/docs/guide/input-validation.md +++ b/docs/guide/input-validation.md @@ -889,6 +889,22 @@ validation of individual input fields by configuring their [[yii\widgets\ActiveF property to be false. When `enableClientValidation` is configured at both the input field level and the form level, the former will take precedence. +> Info: Since version 2.0.11 all validators extending from [[yii\validators\Validator]] receive client-side options +> from separate method - [[yii\validators\Validator::getClientOptions()]]. You can use it: +> +> - if you want to implement your own custom client-side validation but leave the synchronization with server-side +> validator options; +> - to extend or customize to fit your specific needs: +> +> ```php +> protected function getClientOptions($model, $attribute) +> { +> $options = parent::getClientOptions($model, $attribute); +> // Modify $options here +> +> return $options; +> } +> ``` ### Implementing Client-Side Validation diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index c50936e459..b9e7d97b45 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -37,6 +37,7 @@ Yii Framework 2 Change Log - Enh #9162: Added support of closures in `value` for attributes in `yii\widgets\DetailView` (arogachev) - Enh #10896: Select only primary key when counting records in UniqueValidator (developeruz) - Enh #11037: `yii.js` and `yii.validation.js` use `Regexp.test()` instead of `String.match()` (arogachev, nkovacs) +- Enh #11163: Added separate method for client-side validation options in `yii\validators\Validator` (arogachev) - Enh #11756: Added type mapping for `varbinary` data type in MySQL DBMS (silverfire) - Enh #11929: Changed `type` column type from `int` to `smallInt` in RBAC migrations (silverfire) - Enh #12015: Changed visibility `yii\db\ActiveQueryTrait::createModels()` from private to protected (ArekX, dynasource) diff --git a/framework/captcha/CaptchaValidator.php b/framework/captcha/CaptchaValidator.php index 83eac474c2..a319740f57 100644 --- a/framework/captcha/CaptchaValidator.php +++ b/framework/captcha/CaptchaValidator.php @@ -85,6 +85,17 @@ class CaptchaValidator extends Validator * @inheritdoc */ public function clientValidateAttribute($model, $attribute, $view) + { + ValidationAsset::register($view); + $options = $this->getClientOptions($model, $attribute); + + return 'yii.validation.captcha(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + } + + /** + * @inheritdoc + */ + protected function getClientOptions($model, $attribute) { $captcha = $this->createCaptchaAction(); $code = $captcha->getVerifyCode(false); @@ -101,8 +112,6 @@ class CaptchaValidator extends Validator $options['skipOnEmpty'] = 1; } - ValidationAsset::register($view); - - return 'yii.validation.captcha(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + return $options; } } diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index 52c6dfcb9d..96beafe802 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -69,6 +69,17 @@ class BooleanValidator extends Validator * @inheritdoc */ public function clientValidateAttribute($model, $attribute, $view) + { + ValidationAsset::register($view); + $options = $this->getClientOptions($model, $attribute); + + return 'yii.validation.boolean(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + } + + /** + * @inheritdoc + */ + protected function getClientOptions($model, $attribute) { $options = [ 'trueValue' => $this->trueValue, @@ -86,8 +97,6 @@ class BooleanValidator extends Validator $options['strict'] = 1; } - ValidationAsset::register($view); - - return 'yii.validation.boolean(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + return $options; } } diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index b2ee65cce9..b243722ee1 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -224,6 +224,17 @@ class CompareValidator extends Validator * @inheritdoc */ public function clientValidateAttribute($model, $attribute, $view) + { + ValidationAsset::register($view); + $options = $this->getClientOptions($model, $attribute); + + return 'yii.validation.compare(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + } + + /** + * @inheritdoc + */ + protected function getClientOptions($model, $attribute) { $options = [ 'operator' => $this->operator, @@ -251,8 +262,6 @@ class CompareValidator extends Validator 'compareValueOrAttribute' => $compareValueOrAttribute, ], Yii::$app->language); - ValidationAsset::register($view); - - return 'yii.validation.compare(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + return $options; } } diff --git a/framework/validators/EmailValidator.php b/framework/validators/EmailValidator.php index 2bf573702b..1989f2cd76 100644 --- a/framework/validators/EmailValidator.php +++ b/framework/validators/EmailValidator.php @@ -108,6 +108,20 @@ class EmailValidator extends Validator * @inheritdoc */ public function clientValidateAttribute($model, $attribute, $view) + { + ValidationAsset::register($view); + if ($this->enableIDN) { + PunycodeAsset::register($view); + } + $options = $this->getClientOptions($model, $attribute); + + return 'yii.validation.email(value, messages, ' . Json::htmlEncode($options) . ');'; + } + + /** + * @inheritdoc + */ + protected function getClientOptions($model, $attribute) { $options = [ 'pattern' => new JsExpression($this->pattern), @@ -122,11 +136,6 @@ class EmailValidator extends Validator $options['skipOnEmpty'] = 1; } - ValidationAsset::register($view); - if ($this->enableIDN) { - PunycodeAsset::register($view); - } - - return 'yii.validation.email(value, messages, ' . Json::htmlEncode($options) . ');'; + return $options; } } diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index 51562430b2..c0ebf847bd 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -384,10 +384,7 @@ class FileValidator extends Validator } /** - * Returns the client-side validation options. - * @param \yii\base\Model $model the model being validated - * @param string $attribute the attribute name being validated - * @return array the client-side validation options + * @inheritdoc */ protected function getClientOptions($model, $attribute) { diff --git a/framework/validators/FilterValidator.php b/framework/validators/FilterValidator.php index 5b011029fd..5f214d712c 100644 --- a/framework/validators/FilterValidator.php +++ b/framework/validators/FilterValidator.php @@ -88,13 +88,22 @@ class FilterValidator extends Validator return null; } + ValidationAsset::register($view); + $options = $this->getClientOptions($model, $attribute); + + return 'value = yii.validation.trim($form, attribute, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + } + + /** + * @inheritdoc + */ + protected function getClientOptions($model, $attribute) + { $options = []; if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; } - ValidationAsset::register($view); - - return 'value = yii.validation.trim($form, attribute, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + return $options; } } diff --git a/framework/validators/IpValidator.php b/framework/validators/IpValidator.php index 3e9b97d25d..54018a1ce3 100644 --- a/framework/validators/IpValidator.php +++ b/framework/validators/IpValidator.php @@ -587,6 +587,17 @@ class IpValidator extends Validator * @inheritdoc */ public function clientValidateAttribute($model, $attribute, $view) + { + ValidationAsset::register($view); + $options = $this->getClientOptions($model, $attribute); + + return 'yii.validation.ip(value, messages, ' . Json::htmlEncode($options) . ');'; + } + + /** + * @inheritdoc + */ + protected function getClientOptions($model, $attribute) { $messages = [ 'ipv6NotAllowed' => $this->ipv6NotAllowed, @@ -615,8 +626,6 @@ class IpValidator extends Validator $options['skipOnEmpty'] = 1; } - ValidationAsset::register($view); - - return 'yii.validation.ip(value, messages, ' . Json::htmlEncode($options) . ');'; + return $options; } } diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index a3f27e8186..edf9dfc0ff 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -120,6 +120,17 @@ class NumberValidator extends Validator * @inheritdoc */ public function clientValidateAttribute($model, $attribute, $view) + { + ValidationAsset::register($view); + $options = $this->getClientOptions($model, $attribute); + + return 'yii.validation.number(value, messages, ' . Json::htmlEncode($options) . ');'; + } + + /** + * @inheritdoc + */ + protected function getClientOptions($model, $attribute) { $label = $model->getAttributeLabel($attribute); @@ -152,8 +163,6 @@ class NumberValidator extends Validator $options['skipOnEmpty'] = 1; } - ValidationAsset::register($view); - - return 'yii.validation.number(value, messages, ' . Json::htmlEncode($options) . ');'; + return $options; } } diff --git a/framework/validators/RangeValidator.php b/framework/validators/RangeValidator.php index 4e1c89aecf..66296a765c 100644 --- a/framework/validators/RangeValidator.php +++ b/framework/validators/RangeValidator.php @@ -108,6 +108,17 @@ class RangeValidator extends Validator $this->range = call_user_func($this->range, $model, $attribute); } + ValidationAsset::register($view); + $options = $this->getClientOptions($model, $attribute); + + return 'yii.validation.range(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + } + + /** + * @inheritdoc + */ + protected function getClientOptions($model, $attribute) + { $range = []; foreach ($this->range as $value) { $range[] = (string) $value; @@ -126,8 +137,6 @@ class RangeValidator extends Validator $options['allowArray'] = 1; } - ValidationAsset::register($view); - - return 'yii.validation.range(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + return $options; } } diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index 2180519689..b5a98e467b 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -64,6 +64,17 @@ class RegularExpressionValidator extends Validator * @inheritdoc */ public function clientValidateAttribute($model, $attribute, $view) + { + ValidationAsset::register($view); + $options = $this->getClientOptions($model, $attribute); + + return 'yii.validation.regularExpression(value, messages, ' . Json::htmlEncode($options) . ');'; + } + + /** + * @inheritdoc + */ + protected function getClientOptions($model, $attribute) { $pattern = Html::escapeJsRegularExpression($this->pattern); @@ -78,8 +89,6 @@ class RegularExpressionValidator extends Validator $options['skipOnEmpty'] = 1; } - ValidationAsset::register($view); - - return 'yii.validation.regularExpression(value, messages, ' . Json::htmlEncode($options) . ');'; + return $options; } } diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index dd99b99101..54e5ad8a2e 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -87,6 +87,17 @@ class RequiredValidator extends Validator * @inheritdoc */ public function clientValidateAttribute($model, $attribute, $view) + { + ValidationAsset::register($view); + $options = $this->getClientOptions($model, $attribute); + + return 'yii.validation.required(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + } + + /** + * @inheritdoc + */ + protected function getClientOptions($model, $attribute) { $options = []; if ($this->requiredValue !== null) { @@ -105,8 +116,6 @@ class RequiredValidator extends Validator 'attribute' => $model->getAttributeLabel($attribute), ], Yii::$app->language); - ValidationAsset::register($view); - - return 'yii.validation.required(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + return $options; } } diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index 3c93b57c54..2465dcf83f 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -152,6 +152,14 @@ class StringValidator extends Validator * @inheritdoc */ public function clientValidateAttribute($model, $attribute, $view) + { + ValidationAsset::register($view); + $options = $this->getClientOptions($model, $attribute); + + return 'yii.validation.string(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + } + + protected function getClientOptions($model, $attribute) { $label = $model->getAttributeLabel($attribute); @@ -186,8 +194,6 @@ class StringValidator extends Validator $options['skipOnEmpty'] = 1; } - ValidationAsset::register($view); - - return 'yii.validation.string(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; + return $options; } } diff --git a/framework/validators/UrlValidator.php b/framework/validators/UrlValidator.php index 413736d22f..b6501ad5f5 100644 --- a/framework/validators/UrlValidator.php +++ b/framework/validators/UrlValidator.php @@ -112,6 +112,20 @@ class UrlValidator extends Validator * @inheritdoc */ public function clientValidateAttribute($model, $attribute, $view) + { + ValidationAsset::register($view); + if ($this->enableIDN) { + PunycodeAsset::register($view); + } + $options = $this->getClientOptions($model, $attribute); + + return 'yii.validation.url(value, messages, ' . Json::htmlEncode($options) . ');'; + } + + /** + * @inheritdoc + */ + protected function getClientOptions($model, $attribute) { if (strpos($this->pattern, '{schemes}') !== false) { $pattern = str_replace('{schemes}', '(' . implode('|', $this->validSchemes) . ')', $this->pattern); @@ -133,11 +147,6 @@ class UrlValidator extends Validator $options['defaultScheme'] = $this->defaultScheme; } - ValidationAsset::register($view); - if ($this->enableIDN) { - PunycodeAsset::register($view); - } - - return 'yii.validation.url(value, messages, ' . Json::htmlEncode($options) . ');'; + return $options; } } diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index c162547c11..1eb6084dec 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -328,6 +328,8 @@ class Validator extends Component /** * Returns the JavaScript needed for performing client-side validation. * + * Calls [[getClientOptions()]] to generate options array for client-side validation. + * * You may override this method to return the JavaScript validation code if * the validator can support client-side validation. * @@ -353,6 +355,7 @@ class Validator extends Component * containing a model form with this validator applied. * @return string the client-side validation script. Null if the validator does not support * client-side validation. + * @see getClientOptions() * @see \yii\widgets\ActiveForm::enableClientValidation */ public function clientValidateAttribute($model, $attribute, $view) @@ -360,6 +363,20 @@ class Validator extends Component return null; } + /** + * Returns the client-side validation options. + * This method is usually called from [[clientValidateAttribute()]]. You may override this method to modify options + * that will be passed to the client-side validation. + * @param \yii\base\Model $model the model being validated + * @param string $attribute the attribute name being validated + * @return array the client-side validation options + * @since 2.0.11 + */ + protected function getClientOptions($model, $attribute) + { + return []; + } + /** * Returns a value indicating whether the validator is active for the given scenario and attribute. * From 5092c9666defdfb12f7519af21894a8cfc963ee7 Mon Sep 17 00:00:00 2001 From: Bizley Date: Mon, 5 Dec 2016 21:52:39 +0100 Subject: [PATCH 048/105] Code Climate PHPMD settings (#13139) * codeclimate phpmd rulesets * PHPMD ruleset * phpmd_ruleset moved * Delete phpmd_ruleset.xml * ruleset moved --- .codeclimate.yml | 31 ++------------------- tests/data/codeclimate/phpmd_ruleset.xml | 35 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 29 deletions(-) create mode 100644 tests/data/codeclimate/phpmd_ruleset.xml diff --git a/.codeclimate.yml b/.codeclimate.yml index 2debb0dcb5..08b73bbaad 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,4 +1,3 @@ ---- engines: duplication: enabled: true @@ -12,34 +11,8 @@ engines: enabled: true phpmd: enabled: true - # configure checks, see https://phpmd.org/rules/index.html for details - checks: - # else is not always bad. Disabling this as there is no reason to differentiate - # between early return and normal else cases. - CleanCode/ElseExpression: - enabled: false - # Static access on Yii::$app is normal in Yii - CleanCode/StaticAccess: - enabled: false - # Yii is a framework so if fulfills the job of encapsulating superglobals - Controversial/Superglobals: - enabled: false - # allow private properties to start with $_ - Controversial/CamelCasePropertyName: - enabled: true - allow-underscore: true - # Short variable names are no problem in most cases, e.g. $n = count(...); - Naming/ShortVariable: - enabled: false - # Long variable names can help with better understanding so we increase the limit a bit - Naming/LongVariable: - enabled: true - maximum: 25 - # method names like up(), gc(), ... are okay. - Naming/ShortMethodName: - enabled: true - minimum: 2 - + config: + rulesets: "codesize,design,unusedcode,tests/data/codeclimate/phpmd_ruleset.xml" ratings: paths: - "**.js" diff --git a/tests/data/codeclimate/phpmd_ruleset.xml b/tests/data/codeclimate/phpmd_ruleset.xml new file mode 100644 index 0000000000..6189b6b62c --- /dev/null +++ b/tests/data/codeclimate/phpmd_ruleset.xml @@ -0,0 +1,35 @@ + + + Custom PHPMD settings for naming, cleancode and controversial rulesets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 1d4eb072d34222bace64477b4f84928b1f7f897b Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 5 Dec 2016 22:01:05 +0100 Subject: [PATCH 049/105] readded comments to phpmd rules #13139 --- tests/data/codeclimate/phpmd_ruleset.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/data/codeclimate/phpmd_ruleset.xml b/tests/data/codeclimate/phpmd_ruleset.xml index 6189b6b62c..3182294457 100644 --- a/tests/data/codeclimate/phpmd_ruleset.xml +++ b/tests/data/codeclimate/phpmd_ruleset.xml @@ -7,11 +7,13 @@ + + @@ -19,7 +21,9 @@ + + @@ -27,6 +31,7 @@ + From 4735d4da6b42a6aa3dadb7fc874460e03c0acdb8 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 7 Dec 2016 00:08:08 +0100 Subject: [PATCH 050/105] Update DynamicModel.php added note about limitations, i.e. no scenarios -> no safe attributes -> no load(). --- framework/base/DynamicModel.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/framework/base/DynamicModel.php b/framework/base/DynamicModel.php index 69e271aa7f..3a6b2e4f93 100644 --- a/framework/base/DynamicModel.php +++ b/framework/base/DynamicModel.php @@ -50,6 +50,10 @@ use yii\validators\Validator; * "dynamic attributes". It basically allows an attribute to be defined dynamically through its constructor * or [[defineAttribute()]]. * + * Note that it does not support the usual validation flow as normal models, i.e. no scenarios are being defined + * and therefor none of the attributes is safe. This means that [[load()]] will not be able to perform massive + * assignment and return `false`. Attributes should be assigned via constructor instead. + * * @author Qiang Xue * @since 2.0 */ From 48c806fe1cac8ccc349bf01e629bcf6536fdf42d Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 7 Dec 2016 00:21:00 +0100 Subject: [PATCH 051/105] added unit test for DynamicModel::load() issue #12116 --- framework/base/DynamicModel.php | 4 ---- tests/framework/base/ApplicationTest.php | 3 +++ tests/framework/base/DynamicModelTest.php | 28 ++++++++++++++++++++--- tests/framework/base/EventTest.php | 3 +-- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/framework/base/DynamicModel.php b/framework/base/DynamicModel.php index 3a6b2e4f93..69e271aa7f 100644 --- a/framework/base/DynamicModel.php +++ b/framework/base/DynamicModel.php @@ -50,10 +50,6 @@ use yii\validators\Validator; * "dynamic attributes". It basically allows an attribute to be defined dynamically through its constructor * or [[defineAttribute()]]. * - * Note that it does not support the usual validation flow as normal models, i.e. no scenarios are being defined - * and therefor none of the attributes is safe. This means that [[load()]] will not be able to perform massive - * assignment and return `false`. Attributes should be assigned via constructor instead. - * * @author Qiang Xue * @since 2.0 */ diff --git a/tests/framework/base/ApplicationTest.php b/tests/framework/base/ApplicationTest.php index 2be0858d93..37fc67e175 100644 --- a/tests/framework/base/ApplicationTest.php +++ b/tests/framework/base/ApplicationTest.php @@ -9,6 +9,9 @@ use yiiunit\data\ar\Order; use yiiunit\data\ar\Type; use yiiunit\TestCase; +/** + * @group base + */ class ApplicationTest extends TestCase { public function testContainerSettingsAffectBootstrap() diff --git a/tests/framework/base/DynamicModelTest.php b/tests/framework/base/DynamicModelTest.php index 534f37215e..8388a7f6aa 100644 --- a/tests/framework/base/DynamicModelTest.php +++ b/tests/framework/base/DynamicModelTest.php @@ -11,9 +11,7 @@ use yii\base\DynamicModel; use yiiunit\TestCase; /** - * - * @author Qiang Xue - * @since 2.0 + * @group base */ class DynamicModelTest extends TestCase { @@ -77,4 +75,28 @@ class DynamicModelTest extends TestCase $this->setExpectedException('yii\base\UnknownPropertyException'); $age = $model->age; } + + public function testLoad() + { + $dynamic = new DynamicModel(); + //define two attributes + $dynamic->defineAttribute('name'); + $dynamic->defineAttribute('mobile'); + // define rule + $dynamic->addRule(['name','mobile'], 'required'); + // define your sample data + $data = [ + 'DynamicModel' => [ + 'name' => $name = 'your name 2', + 'mobile' => $mobile = 'my number mobile', + ] + ]; + // load data + $this->assertFalse($dynamic->load([])); + $this->assertTrue($dynamic->load($data)); + + $this->assertTrue($dynamic->validate()); + $this->assertEquals($name, $dynamic->name); + $this->assertEquals($mobile, $dynamic->mobile); + } } diff --git a/tests/framework/base/EventTest.php b/tests/framework/base/EventTest.php index c145711760..3d9abcf223 100644 --- a/tests/framework/base/EventTest.php +++ b/tests/framework/base/EventTest.php @@ -12,8 +12,7 @@ use yii\base\Event; use yiiunit\TestCase; /** - * @author Qiang Xue - * @since 2.0 + * @group base */ class EventTest extends TestCase { From 6d005092ac8c175dabbc5e72c598fef5f58e8adb Mon Sep 17 00:00:00 2001 From: sztyvny Date: Wed, 7 Dec 2016 13:06:09 +0100 Subject: [PATCH 052/105] Polish translations for field validation fix (#13153) [skip ci] --- framework/messages/pl/yii.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/messages/pl/yii.php b/framework/messages/pl/yii.php index 9ee4afa0b2..62b3f47c6b 100644 --- a/framework/messages/pl/yii.php +++ b/framework/messages/pl/yii.php @@ -89,9 +89,9 @@ return [ '{attribute} must not be an IPv4 address.' => '{attribute} nie może być adresem IPv4.', '{attribute} must not be an IPv6 address.' => '{attribute} nie może być adresem IPv6.', '{attribute} must not be equal to "{compareValueOrAttribute}".' => '{attribute} musi mieć wartość różną od "{compareValueOrAttribute}".', - '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} powinien zawierać co najmniej {min, number} {min, plural, one{znak} few{znaki} many{znaków} other{znaku}}.', - '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} powinien zawierać nie więcej niż {max, number} {max, plural, one{znak} few{znaki} many{znaków} other{znaku}}.', - '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} powinien zawierać dokładnie {length, number} {length, plural, one{znak} few{znaki} many{znaków} other{znaku}}.', + '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} musi zawierać co najmniej {min, number} {min, plural, one{znak} few{znaki} many{znaków} other{znaku}}.', + '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} musi zawierać nie więcej niż {max, number} {max, plural, one{znak} few{znaki} many{znaków} other{znaku}}.', + '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} musi zawierać dokładnie {length, number} {length, plural, one{znak} few{znaki} many{znaków} other{znaku}}.', '{delta, plural, =1{1 day} other{# days}}' => '{delta, plural, =1{1 dzień} other{# dni} other{# dnia}}', '{delta, plural, =1{1 hour} other{# hours}}' => '{delta, plural, =1{1 godzina} few{# godziny} many{# godzin} other{# godziny}}', '{delta, plural, =1{1 minute} other{# minutes}}' => '{delta, plural, =1{1 minuta} few{# minuty} many{# minut} other{# minuty}}', From 0e5efb91ebd57c0912a9fdb8945baa08a23090c0 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 7 Dec 2016 18:59:48 +0300 Subject: [PATCH 053/105] Replaced links to non-existing widget demos with links to API docs and guide [skip ci] --- docs/guide/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/guide/README.md b/docs/guide/README.md index 44389c6d98..f4204658b9 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -147,7 +147,7 @@ Development Tools * [Debug Toolbar and Debugger](https://github.com/yiisoft/yii2-debug/blob/master/docs/guide/README.md) * [Generating Code using Gii](https://github.com/yiisoft/yii2-gii/blob/master/docs/guide/README.md) -* **TBD** [Generating API Documentation](https://github.com/yiisoft/yii2-apidoc) +* [Generating API Documentation](https://github.com/yiisoft/yii2-apidoc) Testing @@ -179,14 +179,14 @@ Special Topics Widgets ------- -* GridView: **TBD** link to demo page -* ListView: **TBD** link to demo page -* DetailView: **TBD** link to demo page -* ActiveForm: **TBD** link to demo page -* Pjax: **TBD** link to demo page -* Menu: **TBD** link to demo page -* LinkPager: **TBD** link to demo page -* LinkSorter: **TBD** link to demo page +* [GridView](http://www.yiiframework.com/doc-2.0/yii-grid-gridview.html) +* [ListView](http://www.yiiframework.com/doc-2.0/yii-widgets-listview.html) +* [DetailView](http://www.yiiframework.com/doc-2.0/yii-widgets-detailview.html) +* [ActiveForm](http://www.yiiframework.com/doc-2.0/guide-input-forms.html#activerecord-based-forms-activeform) +* [Pjax](http://www.yiiframework.com/doc-2.0/yii-widgets-pjax.html) +* [Menu](http://www.yiiframework.com/doc-2.0/yii-widgets-menu.html) +* [LinkPager](http://www.yiiframework.com/doc-2.0/yii-widgets-linkpager.html) +* [LinkSorter](http://www.yiiframework.com/doc-2.0/yii-widgets-linksorter.html) * [Bootstrap Widgets](https://github.com/yiisoft/yii2-bootstrap/blob/master/docs/guide/README.md) * [jQuery UI Widgets](https://github.com/yiisoft/yii2-jui/blob/master/docs/guide/README.md) From f5beaf3edf459883ad1711d27c45aefc7d01e574 Mon Sep 17 00:00:00 2001 From: Nikola Kovacs Date: Tue, 6 Dec 2016 11:21:20 +0100 Subject: [PATCH 054/105] Make validator getClientOptions public This allows implementing custom client-side validation without extending every validator. Fixes #11163 close #13145 --- docs/guide/input-validation.md | 6 +++--- framework/CHANGELOG.md | 2 +- framework/UPGRADE.md | 7 +++++-- framework/captcha/CaptchaValidator.php | 2 +- framework/validators/BooleanValidator.php | 2 +- framework/validators/CompareValidator.php | 2 +- framework/validators/EmailValidator.php | 2 +- framework/validators/FileValidator.php | 2 +- framework/validators/FilterValidator.php | 2 +- framework/validators/ImageValidator.php | 2 +- framework/validators/IpValidator.php | 2 +- framework/validators/NumberValidator.php | 2 +- framework/validators/RangeValidator.php | 2 +- framework/validators/RegularExpressionValidator.php | 2 +- framework/validators/RequiredValidator.php | 2 +- framework/validators/StringValidator.php | 2 +- framework/validators/UrlValidator.php | 2 +- framework/validators/Validator.php | 2 +- 18 files changed, 24 insertions(+), 21 deletions(-) diff --git a/docs/guide/input-validation.md b/docs/guide/input-validation.md index 7cdbdd9927..2c209d1796 100644 --- a/docs/guide/input-validation.md +++ b/docs/guide/input-validation.md @@ -895,13 +895,13 @@ the former will take precedence. > - if you want to implement your own custom client-side validation but leave the synchronization with server-side > validator options; > - to extend or customize to fit your specific needs: -> +> > ```php -> protected function getClientOptions($model, $attribute) +> public function getClientOptions($model, $attribute) > { > $options = parent::getClientOptions($model, $attribute); > // Modify $options here -> +> > return $options; > } > ``` diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index b9e7d97b45..1fcf8dc7ef 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -37,7 +37,7 @@ Yii Framework 2 Change Log - Enh #9162: Added support of closures in `value` for attributes in `yii\widgets\DetailView` (arogachev) - Enh #10896: Select only primary key when counting records in UniqueValidator (developeruz) - Enh #11037: `yii.js` and `yii.validation.js` use `Regexp.test()` instead of `String.match()` (arogachev, nkovacs) -- Enh #11163: Added separate method for client-side validation options in `yii\validators\Validator` (arogachev) +- Enh #11163: Added separate method for client-side validation options `yii\validators\Validator::getClientOptions()` (arogachev) - Enh #11756: Added type mapping for `varbinary` data type in MySQL DBMS (silverfire) - Enh #11929: Changed `type` column type from `int` to `smallInt` in RBAC migrations (silverfire) - Enh #12015: Changed visibility `yii\db\ActiveQueryTrait::createModels()` from private to protected (ArekX, dynasource) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index 7c67fd70a2..ecccdb5ed7 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -29,14 +29,14 @@ The simple way to upgrade Yii, for example to version 2.0.10 (replace this with composer require "yiisoft/yii2:~2.0.10" This however may fail due to changes in the dependencies of yiisoft/yii2, which may change due to security updates -in other libraries or by adding support for newer versions. `composer require` will not update any other packages +in other libraries or by adding support for newer versions. `composer require` will not update any other packages as a safety feature. The better way to upgrade is to change the `composer.json` file to require the new Yii version and then run `composer update` by specifying all packages that are allowed to be updated. composer update yiisoft/yii2 yiisoft/yii2-composer bower-asset/jquery.inputmask - + The above command will only update the specified packages and leave the versions of all other dependencies intact. This helps to update packages step by step without causing a lot of package version changes that might break in some way. If you feel lucky you can of course update everything to the latest version by running `composer update` without @@ -57,6 +57,9 @@ Upgrade from Yii 2.0.10 This method is implemented in the `yii\db\QueryTrait`, so this only affects your code if you implement QueryInterface in a class that does not use the trait. +* `yii\validators\FileValidator::getClientOptions()` and `yii\validators\ImageValidator::getClientOptions()` are now public. + If you extend from these classes and override these methods, you must make them public as well. + Upgrade from Yii 2.0.9 ---------------------- diff --git a/framework/captcha/CaptchaValidator.php b/framework/captcha/CaptchaValidator.php index a319740f57..357a81bc70 100644 --- a/framework/captcha/CaptchaValidator.php +++ b/framework/captcha/CaptchaValidator.php @@ -95,7 +95,7 @@ class CaptchaValidator extends Validator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $captcha = $this->createCaptchaAction(); $code = $captcha->getVerifyCode(false); diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index 96beafe802..cff9fc9fef 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -79,7 +79,7 @@ class BooleanValidator extends Validator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $options = [ 'trueValue' => $this->trueValue, diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index b243722ee1..c9284f0f72 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -234,7 +234,7 @@ class CompareValidator extends Validator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $options = [ 'operator' => $this->operator, diff --git a/framework/validators/EmailValidator.php b/framework/validators/EmailValidator.php index 1989f2cd76..9262b56908 100644 --- a/framework/validators/EmailValidator.php +++ b/framework/validators/EmailValidator.php @@ -121,7 +121,7 @@ class EmailValidator extends Validator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $options = [ 'pattern' => new JsExpression($this->pattern), diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index c0ebf847bd..64774b362a 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -386,7 +386,7 @@ class FileValidator extends Validator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $label = $model->getAttributeLabel($attribute); diff --git a/framework/validators/FilterValidator.php b/framework/validators/FilterValidator.php index 5f214d712c..cedf241fd1 100644 --- a/framework/validators/FilterValidator.php +++ b/framework/validators/FilterValidator.php @@ -97,7 +97,7 @@ class FilterValidator extends Validator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $options = []; if ($this->skipOnEmpty) { diff --git a/framework/validators/ImageValidator.php b/framework/validators/ImageValidator.php index 769cad9398..6d72812acf 100644 --- a/framework/validators/ImageValidator.php +++ b/framework/validators/ImageValidator.php @@ -172,7 +172,7 @@ class ImageValidator extends FileValidator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $options = parent::getClientOptions($model, $attribute); diff --git a/framework/validators/IpValidator.php b/framework/validators/IpValidator.php index 54018a1ce3..9b72adfad0 100644 --- a/framework/validators/IpValidator.php +++ b/framework/validators/IpValidator.php @@ -597,7 +597,7 @@ class IpValidator extends Validator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $messages = [ 'ipv6NotAllowed' => $this->ipv6NotAllowed, diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index edf9dfc0ff..cf5b7c0cb8 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -130,7 +130,7 @@ class NumberValidator extends Validator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $label = $model->getAttributeLabel($attribute); diff --git a/framework/validators/RangeValidator.php b/framework/validators/RangeValidator.php index 66296a765c..63be6eacc6 100644 --- a/framework/validators/RangeValidator.php +++ b/framework/validators/RangeValidator.php @@ -117,7 +117,7 @@ class RangeValidator extends Validator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $range = []; foreach ($this->range as $value) { diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index b5a98e467b..60130e2685 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -74,7 +74,7 @@ class RegularExpressionValidator extends Validator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $pattern = Html::escapeJsRegularExpression($this->pattern); diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index 54e5ad8a2e..21966c5725 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -97,7 +97,7 @@ class RequiredValidator extends Validator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $options = []; if ($this->requiredValue !== null) { diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index 2465dcf83f..b41ae55f87 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -159,7 +159,7 @@ class StringValidator extends Validator return 'yii.validation.string(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; } - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { $label = $model->getAttributeLabel($attribute); diff --git a/framework/validators/UrlValidator.php b/framework/validators/UrlValidator.php index b6501ad5f5..8f6ef5bb19 100644 --- a/framework/validators/UrlValidator.php +++ b/framework/validators/UrlValidator.php @@ -125,7 +125,7 @@ class UrlValidator extends Validator /** * @inheritdoc */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { if (strpos($this->pattern, '{schemes}') !== false) { $pattern = str_replace('{schemes}', '(' . implode('|', $this->validSchemes) . ')', $this->pattern); diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index 1eb6084dec..e04d6f4994 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -372,7 +372,7 @@ class Validator extends Component * @return array the client-side validation options * @since 2.0.11 */ - protected function getClientOptions($model, $attribute) + public function getClientOptions($model, $attribute) { return []; } From f8787b447162d459043799bf7517e06b704a5ce8 Mon Sep 17 00:00:00 2001 From: Sergey Makinen Date: Wed, 7 Dec 2016 01:58:20 +0300 Subject: [PATCH 055/105] Adds tests for yii\filters\PageCache --- tests/data/views/pageCacheLayout.php | 15 + tests/framework/filters/PageCacheTest.php | 424 ++++++++++++++++++++++ 2 files changed, 439 insertions(+) create mode 100644 tests/data/views/pageCacheLayout.php create mode 100644 tests/framework/filters/PageCacheTest.php diff --git a/tests/data/views/pageCacheLayout.php b/tests/data/views/pageCacheLayout.php new file mode 100644 index 0000000000..41197793b9 --- /dev/null +++ b/tests/data/views/pageCacheLayout.php @@ -0,0 +1,15 @@ +beginPage(); +$this->head(); +$this->beginBody(); +?> +{ + "static": "", + "dynamic": "renderDynamic('return Yii::$app->params[\'dynamic\'];') ?>" +} +endBody(); +$this->endPage(); +?> diff --git a/tests/framework/filters/PageCacheTest.php b/tests/framework/filters/PageCacheTest.php new file mode 100644 index 0000000000..030af6d028 --- /dev/null +++ b/tests/framework/filters/PageCacheTest.php @@ -0,0 +1,424 @@ + 'disabled', + 'properties' => [ + 'enabled' => false + ], + 'cacheable' => false, + ], + [ + 'name' => 'simple', + ], + + // Cookies + [ + 'name' => 'allCookies', + 'properties' => [ + 'cacheCookies' => true + ], + 'cookies' => [ + 'test-cookie-1' => true, + 'test-cookie-2' => true, + ] + ], + [ + 'name' => 'someCookies', + 'properties' => [ + 'cacheCookies' => ['test-cookie-2'] + ], + 'cookies' => [ + 'test-cookie-1' => false, + 'test-cookie-2' => true, + ] + ], + [ + 'name' => 'noCookies', + 'properties' => [ + 'cacheCookies' => false + ], + 'cookies' => [ + 'test-cookie-1' => false, + 'test-cookie-2' => false, + ] + ], + + // Headers + [ + 'name' => 'allHeaders', + 'properties' => [ + 'cacheHeaders' => true + ], + 'headers' => [ + 'test-header-1' => true, + 'test-header-2' => true, + ] + ], + [ + 'name' => 'someHeaders', + 'properties' => [ + 'cacheHeaders' => ['test-header-2'] + ], + 'headers' => [ + 'test-header-1' => false, + 'test-header-2' => true, + ] + ], + [ + 'name' => 'noHeaders', + 'properties' => [ + 'cacheHeaders' => false + ], + 'headers' => [ + 'test-header-1' => false, + 'test-header-2' => false, + ] + ], + + // All together + [ + 'name' => 'someCookiesSomeHeaders', + 'properties' => [ + 'cacheCookies' => ['test-cookie-2'], + 'cacheHeaders' => ['test-header-2'] + ], + 'cookies' => [ + 'test-cookie-1' => false, + 'test-cookie-2' => true, + ], + 'headers' => [ + 'test-header-1' => false, + 'test-header-2' => true, + ] + ], + ]; + + foreach ($testCases as $testCase) { + $testCase = ArrayHelper::merge([ + 'properties' => [], + 'cacheable' => true, + ], $testCase); + if (isset(Yii::$app)) { + $this->destroyApplication(); + } + // Prepares the test response + $this->mockWebApplication(); + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $filter = new PageCache(array_merge([ + 'cache' => $cache = new ArrayCache(), + 'view' => new View() + ], $testCase['properties'])); + $this->assertTrue($filter->beforeAction($action), $testCase['name']); + // Cookies + $cookies = []; + if (isset($testCase['cookies'])) { + foreach (array_keys($testCase['cookies']) as $name) { + $value = Yii::$app->security->generateRandomString(); + Yii::$app->response->cookies->add(new Cookie([ + 'name' => $name, + 'value' => $value, + 'expire' => PHP_INT_MAX + ])); + $cookies[$name] = $value; + } + } + // Headers + $headers = []; + if (isset($testCase['headers'])) { + foreach (array_keys($testCase['headers']) as $name) { + $value = Yii::$app->security->generateRandomString(); + Yii::$app->response->headers->add($name, $value); + $headers[$name] = $value; + } + } + // Content + $static = Yii::$app->security->generateRandomString(); + Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); + $content = $filter->view->render('@yiiunit/data/views/pageCacheLayout.php', ['static' => $static]); + Yii::$app->response->content = $content; + ob_start(); + Yii::$app->response->send(); + ob_end_clean(); + // Metadata + $metadata = [ + 'format' => Yii::$app->response->format, + 'version' => Yii::$app->response->version, + 'statusCode' => Yii::$app->response->statusCode, + 'statusText' => Yii::$app->response->statusText, + ]; + if ($testCase['cacheable']) { + $this->assertNotEmpty($this->getInaccessibleProperty($filter->cache, '_cache'), $testCase['name']); + } else { + $this->assertEmpty($this->getInaccessibleProperty($filter->cache, '_cache'), $testCase['name']); + continue; + } + + // Verifies the cached response + $this->destroyApplication(); + $this->mockWebApplication(); + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $filter = new PageCache(array_merge([ + 'cache' => $cache, + 'view' => new View() + ]), $testCase['properties']); + Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); + $this->assertFalse($filter->beforeAction($action), $testCase['name']); + // Content + $json = Json::decode(Yii::$app->response->content); + $this->assertSame($static, $json['static'], $testCase['name']); + $this->assertSame($dynamic, $json['dynamic'], $testCase['name']); + // Metadata + $this->assertSame($metadata['format'], Yii::$app->response->format, $testCase['name']); + $this->assertSame($metadata['version'], Yii::$app->response->version, $testCase['name']); + $this->assertSame($metadata['statusCode'], Yii::$app->response->statusCode, $testCase['name']); + $this->assertSame($metadata['statusText'], Yii::$app->response->statusText, $testCase['name']); + // Cookies + if (isset($testCase['cookies'])) { + foreach ($testCase['cookies'] as $name => $expected) { + $this->assertSame($expected, Yii::$app->response->cookies->has($name), $testCase['name']); + if ($expected) { + $this->assertSame($cookies[$name], Yii::$app->response->cookies->getValue($name), $testCase['name']); + } + } + } + // Headers + if (isset($testCase['headers'])) { + foreach ($testCase['headers'] as $name => $expected) { + $this->assertSame($expected, Yii::$app->response->headers->has($name), $testCase['name']); + if ($expected) { + $this->assertSame($headers[$name], Yii::$app->response->headers->get($name), $testCase['name']); + } + } + } + } + } + + public function testExpired() + { + // Prepares the test response + $this->mockWebApplication(); + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $filter = new PageCache([ + 'cache' => $cache = new ArrayCache(), + 'view' => new View(), + 'duration' => 1 + ]); + $this->assertTrue($filter->beforeAction($action)); + $static = Yii::$app->security->generateRandomString(); + Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); + $content = $filter->view->render('@yiiunit/data/views/pageCacheLayout.php', ['static' => $static]); + Yii::$app->response->content = $content; + ob_start(); + Yii::$app->response->send(); + ob_end_clean(); + + $this->assertNotEmpty($this->getInaccessibleProperty($filter->cache, '_cache')); + + // Verifies the cached response + sleep(2); + $this->destroyApplication(); + $this->mockWebApplication(); + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $filter = new PageCache([ + 'cache' => $cache, + 'view' => new View() + ]); + Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); + $this->assertTrue($filter->beforeAction($action)); + ob_start(); + Yii::$app->response->send(); + ob_end_clean(); + } + + public function testVaryByRoute() + { + $testCases = [ + false, + true, + ]; + + foreach ($testCases as $enabled) { + if (isset(Yii::$app)) { + $this->destroyApplication(); + } + // Prepares the test response + $this->mockWebApplication(); + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $filter = new PageCache([ + 'cache' => $cache = new ArrayCache(), + 'view' => new View(), + 'varyByRoute' => $enabled + ]); + $this->assertTrue($filter->beforeAction($action)); + $static = Yii::$app->security->generateRandomString(); + Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); + $content = $filter->view->render('@yiiunit/data/views/pageCacheLayout.php', ['static' => $static]); + Yii::$app->response->content = $content; + ob_start(); + Yii::$app->response->send(); + ob_end_clean(); + + $this->assertNotEmpty($this->getInaccessibleProperty($filter->cache, '_cache')); + + // Verifies the cached response + $this->destroyApplication(); + $this->mockWebApplication(); + $controller = new Controller('test', Yii::$app); + $action = new Action('test2', $controller); + $filter = new PageCache([ + 'cache' => $cache, + 'view' => new View(), + 'varyByRoute' => $enabled + ]); + Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); + $this->assertSame($enabled, $filter->beforeAction($action), $enabled); + ob_start(); + Yii::$app->response->send(); + ob_end_clean(); + } + } + + public function testVariations() + { + $testCases = [ + [true, 'name' => 'value'], + [false, 'name' => 'value2'], + ]; + + foreach ($testCases as $testCase) { + if (isset(Yii::$app)) { + $this->destroyApplication(); + } + $expected = array_shift($testCase); + // Prepares the test response + $this->mockWebApplication(); + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $originalVariations = $testCases[0]; + array_shift($originalVariations); + $filter = new PageCache([ + 'cache' => $cache = new ArrayCache(), + 'view' => new View(), + 'variations' => $originalVariations + ]); + $this->assertTrue($filter->beforeAction($action)); + $static = Yii::$app->security->generateRandomString(); + Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); + $content = $filter->view->render('@yiiunit/data/views/pageCacheLayout.php', ['static' => $static]); + Yii::$app->response->content = $content; + ob_start(); + Yii::$app->response->send(); + ob_end_clean(); + + $this->assertNotEmpty($this->getInaccessibleProperty($filter->cache, '_cache')); + + // Verifies the cached response + $this->destroyApplication(); + $this->mockWebApplication(); + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $filter = new PageCache([ + 'cache' => $cache, + 'view' => new View(), + 'variations' => $testCase + ]); + Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); + $this->assertNotSame($expected, $filter->beforeAction($action), $expected); + ob_start(); + Yii::$app->response->send(); + ob_end_clean(); + } + } + + public function testDependency() + { + $testCases = [ + false, + true, + ]; + + foreach ($testCases as $changed) { + if (isset(Yii::$app)) { + $this->destroyApplication(); + } + // Prepares the test response + $this->mockWebApplication(); + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $filter = new PageCache([ + 'cache' => $cache = new ArrayCache(), + 'view' => new View(), + 'dependency' => [ + 'class' => ExpressionDependency::className(), + 'expression' => 'Yii::$app->params[\'dependency\']', + ] + ]); + $this->assertTrue($filter->beforeAction($action)); + $static = Yii::$app->security->generateRandomString(); + Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); + Yii::$app->params['dependency'] = $dependency = Yii::$app->security->generateRandomString(); + $content = $filter->view->render('@yiiunit/data/views/pageCacheLayout.php', ['static' => $static]); + Yii::$app->response->content = $content; + ob_start(); + Yii::$app->response->send(); + ob_end_clean(); + + $this->assertNotEmpty($this->getInaccessibleProperty($filter->cache, '_cache')); + + // Verifies the cached response + $this->destroyApplication(); + $this->mockWebApplication(); + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $filter = new PageCache([ + 'cache' => $cache, + 'view' => new View(), + ]); + Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); + if ($changed) { + Yii::$app->params['dependency'] = Yii::$app->security->generateRandomString(); + } else { + Yii::$app->params['dependency'] = $dependency; + } + $this->assertSame($changed, $filter->beforeAction($action), $changed); + ob_start(); + Yii::$app->response->send(); + ob_end_clean(); + } + } +} From 8913bee15a22721800069587bdf15a978b90b082 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Thu, 8 Dec 2016 15:07:33 +0100 Subject: [PATCH 056/105] refactored pagecache test - use dataprovider for different test cases instead of foreach - use time mocking to avoid sleep(2) --- tests/framework/filters/PageCacheTest.php | 247 ++++++++++++---------- 1 file changed, 132 insertions(+), 115 deletions(-) diff --git a/tests/framework/filters/PageCacheTest.php b/tests/framework/filters/PageCacheTest.php index 030af6d028..9d38e82c8b 100644 --- a/tests/framework/filters/PageCacheTest.php +++ b/tests/framework/filters/PageCacheTest.php @@ -12,6 +12,7 @@ use yii\helpers\Json; use yii\web\Controller; use yii\web\Cookie; use yii\web\View; +use yiiunit\framework\caching\CacheTestCase; use yiiunit\TestCase; /** @@ -26,23 +27,29 @@ class PageCacheTest extends TestCase $_SERVER['SCRIPT_NAME'] = "/index.php"; } - public function testCache() + protected function tearDown() { - $testCases = [ + CacheTestCase::$time = null; + CacheTestCase::$microtime = null; + } + + public function cacheTestCaseProvider() + { + return [ // Basic - [ + [[ 'name' => 'disabled', 'properties' => [ 'enabled' => false ], 'cacheable' => false, - ], - [ + ]], + [[ 'name' => 'simple', - ], + ]], // Cookies - [ + [[ 'name' => 'allCookies', 'properties' => [ 'cacheCookies' => true @@ -51,8 +58,8 @@ class PageCacheTest extends TestCase 'test-cookie-1' => true, 'test-cookie-2' => true, ] - ], - [ + ]], + [[ 'name' => 'someCookies', 'properties' => [ 'cacheCookies' => ['test-cookie-2'] @@ -61,8 +68,8 @@ class PageCacheTest extends TestCase 'test-cookie-1' => false, 'test-cookie-2' => true, ] - ], - [ + ]], + [[ 'name' => 'noCookies', 'properties' => [ 'cacheCookies' => false @@ -71,10 +78,10 @@ class PageCacheTest extends TestCase 'test-cookie-1' => false, 'test-cookie-2' => false, ] - ], + ]], // Headers - [ + [[ 'name' => 'allHeaders', 'properties' => [ 'cacheHeaders' => true @@ -83,8 +90,8 @@ class PageCacheTest extends TestCase 'test-header-1' => true, 'test-header-2' => true, ] - ], - [ + ]], + [[ 'name' => 'someHeaders', 'properties' => [ 'cacheHeaders' => ['test-header-2'] @@ -93,8 +100,8 @@ class PageCacheTest extends TestCase 'test-header-1' => false, 'test-header-2' => true, ] - ], - [ + ]], + [[ 'name' => 'noHeaders', 'properties' => [ 'cacheHeaders' => false @@ -103,10 +110,10 @@ class PageCacheTest extends TestCase 'test-header-1' => false, 'test-header-2' => false, ] - ], + ]], // All together - [ + [[ 'name' => 'someCookiesSomeHeaders', 'properties' => [ 'cacheCookies' => ['test-cookie-2'], @@ -120,106 +127,110 @@ class PageCacheTest extends TestCase 'test-header-1' => false, 'test-header-2' => true, ] - ], + ]], ]; + } - foreach ($testCases as $testCase) { - $testCase = ArrayHelper::merge([ - 'properties' => [], - 'cacheable' => true, - ], $testCase); - if (isset(Yii::$app)) { - $this->destroyApplication(); - } - // Prepares the test response - $this->mockWebApplication(); - $controller = new Controller('test', Yii::$app); - $action = new Action('test', $controller); - $filter = new PageCache(array_merge([ - 'cache' => $cache = new ArrayCache(), - 'view' => new View() - ], $testCase['properties'])); - $this->assertTrue($filter->beforeAction($action), $testCase['name']); - // Cookies - $cookies = []; - if (isset($testCase['cookies'])) { - foreach (array_keys($testCase['cookies']) as $name) { - $value = Yii::$app->security->generateRandomString(); - Yii::$app->response->cookies->add(new Cookie([ - 'name' => $name, - 'value' => $value, - 'expire' => PHP_INT_MAX - ])); - $cookies[$name] = $value; - } - } - // Headers - $headers = []; - if (isset($testCase['headers'])) { - foreach (array_keys($testCase['headers']) as $name) { - $value = Yii::$app->security->generateRandomString(); - Yii::$app->response->headers->add($name, $value); - $headers[$name] = $value; - } - } - // Content - $static = Yii::$app->security->generateRandomString(); - Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); - $content = $filter->view->render('@yiiunit/data/views/pageCacheLayout.php', ['static' => $static]); - Yii::$app->response->content = $content; - ob_start(); - Yii::$app->response->send(); - ob_end_clean(); - // Metadata - $metadata = [ - 'format' => Yii::$app->response->format, - 'version' => Yii::$app->response->version, - 'statusCode' => Yii::$app->response->statusCode, - 'statusText' => Yii::$app->response->statusText, - ]; - if ($testCase['cacheable']) { - $this->assertNotEmpty($this->getInaccessibleProperty($filter->cache, '_cache'), $testCase['name']); - } else { - $this->assertEmpty($this->getInaccessibleProperty($filter->cache, '_cache'), $testCase['name']); - continue; - } - - // Verifies the cached response + /** + * @dataProvider cacheTestCaseProvider + */ + public function testCache($testCase) + { + $testCase = ArrayHelper::merge([ + 'properties' => [], + 'cacheable' => true, + ], $testCase); + if (isset(Yii::$app)) { $this->destroyApplication(); - $this->mockWebApplication(); - $controller = new Controller('test', Yii::$app); - $action = new Action('test', $controller); - $filter = new PageCache(array_merge([ - 'cache' => $cache, - 'view' => new View() - ]), $testCase['properties']); - Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); - $this->assertFalse($filter->beforeAction($action), $testCase['name']); - // Content - $json = Json::decode(Yii::$app->response->content); - $this->assertSame($static, $json['static'], $testCase['name']); - $this->assertSame($dynamic, $json['dynamic'], $testCase['name']); - // Metadata - $this->assertSame($metadata['format'], Yii::$app->response->format, $testCase['name']); - $this->assertSame($metadata['version'], Yii::$app->response->version, $testCase['name']); - $this->assertSame($metadata['statusCode'], Yii::$app->response->statusCode, $testCase['name']); - $this->assertSame($metadata['statusText'], Yii::$app->response->statusText, $testCase['name']); - // Cookies - if (isset($testCase['cookies'])) { - foreach ($testCase['cookies'] as $name => $expected) { - $this->assertSame($expected, Yii::$app->response->cookies->has($name), $testCase['name']); - if ($expected) { - $this->assertSame($cookies[$name], Yii::$app->response->cookies->getValue($name), $testCase['name']); - } + } + // Prepares the test response + $this->mockWebApplication(); + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $filter = new PageCache(array_merge([ + 'cache' => $cache = new ArrayCache(), + 'view' => new View() + ], $testCase['properties'])); + $this->assertTrue($filter->beforeAction($action), $testCase['name']); + // Cookies + $cookies = []; + if (isset($testCase['cookies'])) { + foreach (array_keys($testCase['cookies']) as $name) { + $value = Yii::$app->security->generateRandomString(); + Yii::$app->response->cookies->add(new Cookie([ + 'name' => $name, + 'value' => $value, + 'expire' => PHP_INT_MAX + ])); + $cookies[$name] = $value; + } + } + // Headers + $headers = []; + if (isset($testCase['headers'])) { + foreach (array_keys($testCase['headers']) as $name) { + $value = Yii::$app->security->generateRandomString(); + Yii::$app->response->headers->add($name, $value); + $headers[$name] = $value; + } + } + // Content + $static = Yii::$app->security->generateRandomString(); + Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); + $content = $filter->view->render('@yiiunit/data/views/pageCacheLayout.php', ['static' => $static]); + Yii::$app->response->content = $content; + ob_start(); + Yii::$app->response->send(); + ob_end_clean(); + // Metadata + $metadata = [ + 'format' => Yii::$app->response->format, + 'version' => Yii::$app->response->version, + 'statusCode' => Yii::$app->response->statusCode, + 'statusText' => Yii::$app->response->statusText, + ]; + if ($testCase['cacheable']) { + $this->assertNotEmpty($this->getInaccessibleProperty($filter->cache, '_cache'), $testCase['name']); + } else { + $this->assertEmpty($this->getInaccessibleProperty($filter->cache, '_cache'), $testCase['name']); + return; + } + + // Verifies the cached response + $this->destroyApplication(); + $this->mockWebApplication(); + $controller = new Controller('test', Yii::$app); + $action = new Action('test', $controller); + $filter = new PageCache(array_merge([ + 'cache' => $cache, + 'view' => new View() + ]), $testCase['properties']); + Yii::$app->params['dynamic'] = $dynamic = Yii::$app->security->generateRandomString(); + $this->assertFalse($filter->beforeAction($action), $testCase['name']); + // Content + $json = Json::decode(Yii::$app->response->content); + $this->assertSame($static, $json['static'], $testCase['name']); + $this->assertSame($dynamic, $json['dynamic'], $testCase['name']); + // Metadata + $this->assertSame($metadata['format'], Yii::$app->response->format, $testCase['name']); + $this->assertSame($metadata['version'], Yii::$app->response->version, $testCase['name']); + $this->assertSame($metadata['statusCode'], Yii::$app->response->statusCode, $testCase['name']); + $this->assertSame($metadata['statusText'], Yii::$app->response->statusText, $testCase['name']); + // Cookies + if (isset($testCase['cookies'])) { + foreach ($testCase['cookies'] as $name => $expected) { + $this->assertSame($expected, Yii::$app->response->cookies->has($name), $testCase['name']); + if ($expected) { + $this->assertSame($cookies[$name], Yii::$app->response->cookies->getValue($name), $testCase['name']); } } - // Headers - if (isset($testCase['headers'])) { - foreach ($testCase['headers'] as $name => $expected) { - $this->assertSame($expected, Yii::$app->response->headers->has($name), $testCase['name']); - if ($expected) { - $this->assertSame($headers[$name], Yii::$app->response->headers->get($name), $testCase['name']); - } + } + // Headers + if (isset($testCase['headers'])) { + foreach ($testCase['headers'] as $name => $expected) { + $this->assertSame($expected, Yii::$app->response->headers->has($name), $testCase['name']); + if ($expected) { + $this->assertSame($headers[$name], Yii::$app->response->headers->get($name), $testCase['name']); } } } @@ -227,6 +238,9 @@ class PageCacheTest extends TestCase public function testExpired() { + CacheTestCase::$time = time(); + CacheTestCase::$microtime = microtime(true); + // Prepares the test response $this->mockWebApplication(); $controller = new Controller('test', Yii::$app); @@ -247,8 +261,11 @@ class PageCacheTest extends TestCase $this->assertNotEmpty($this->getInaccessibleProperty($filter->cache, '_cache')); + // mock sleep(2); + CacheTestCase::$time += 2; + CacheTestCase::$microtime += 2; + // Verifies the cached response - sleep(2); $this->destroyApplication(); $this->mockWebApplication(); $controller = new Controller('test', Yii::$app); From 74f72ac663759c81615e9cec66e74a9fc00627d3 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 8 Dec 2016 12:12:30 +0300 Subject: [PATCH 057/105] Updated caching guide Russian translation [skip ci] --- docs/guide-ru/caching-data.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/guide-ru/caching-data.md b/docs/guide-ru/caching-data.md index d89b9dce25..60ce0abcca 100644 --- a/docs/guide-ru/caching-data.md +++ b/docs/guide-ru/caching-data.md @@ -161,6 +161,10 @@ if ($data === false) { } ``` +Начиная с версии 2.0.11 вы можете изменить значение по умолчанию (бесконечность) для длительности кеширования задав +[[yii\caching\Cache::$defaultDuration|defaultDuration]] в конфигурации компонента кеша. Таким образом, можно будет +не передавать значение `duration` в [[yii\caching\Cache::set()|set()]] каждый раз. + ### Зависимости кэша В добавок к изменению срока действия ключа элемент может быть признан недействительным из-за *изменения зависимостей*. К примеру, [[yii\caching\FileDependency]] представляет собой зависимость от времени изменения файла. Когда это время изменяется, любые устаревшие данные, найденные в кэше, должны быть признаны недействительным, а [[yii\caching\Cache::get()|get()]] в этом случае должен вернуть `false`. From 447c68bef76bb7fc1041bf880d1eb0e8b87cc1bf Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 8 Dec 2016 12:13:02 +0300 Subject: [PATCH 058/105] Updated configuration and DI guides Russian translation [skip ci] --- docs/guide-ru/concept-configurations.md | 23 ++++ docs/guide-ru/concept-di-container.md | 175 +++++++++++++++++++++--- 2 files changed, 181 insertions(+), 17 deletions(-) diff --git a/docs/guide-ru/concept-configurations.md b/docs/guide-ru/concept-configurations.md index c0ac69b7c4..2a755380c7 100644 --- a/docs/guide-ru/concept-configurations.md +++ b/docs/guide-ru/concept-configurations.md @@ -135,6 +135,29 @@ $config = [ За более подробной документацией о настройках свойства `components` в конфигурации приложения обратитесь к главам [приложения](structure-applications.md) и [Service Locator](concept-service-locator.md). +Начиная с версии 2.0.11, можно настраивать [контейнер зависимостей](concept-di-container.md) через конфигурацию +приложения. Для этого используется свойство `container`: + +```php +$config = [ + 'id' => 'basic', + 'basePath' => dirname(__DIR__), + 'extensions' => require(__DIR__ . '/../vendor/yiisoft/extensions.php'), + 'container' => [ + 'definitions' => [ + 'yii\widgets\LinkPager' => ['maxButtonCount' => 5] + ], + 'singletons' => [ + // Конфигурация для единожды создающихся объектов + ] + ] +]; +``` + +Чтобы узнать о возможных значениях `definitions` и `singletons`, а также о реальных примерах использования, +прочитайте подраздел [более сложное практическое применение](concept-di-container.md#advanced-practical-usage) раздела +[Dependency Injection Container](concept-di-container.md). + ### Конфигурации виджетов diff --git a/docs/guide-ru/concept-di-container.md b/docs/guide-ru/concept-di-container.md index 0ede428088..c9864f23fc 100644 --- a/docs/guide-ru/concept-di-container.md +++ b/docs/guide-ru/concept-di-container.md @@ -104,6 +104,138 @@ $container->get('Foo', [], [ > Info: Метод [[yii\di\Container::get()]] третьим аргументом принимает массив конфигурации, которым инициализируется создаваемый объект. Если класс реализует интерфейс [[yii\base\Configurable]] (например, [[yii\base\Object]]), то массив конфигурации передается в последний параметр конструктора класса. Иначе конфигурация применяется уже *после* создания объекта. +Более сложное практическое применение +--------------- + +Допустим, мы работаем над API и у нас есть: + +- `app\components\Request`, наследуемый от `yii\web\Request` и реализующий дополнительные возможности. +- `app\components\Response`, наследуемый от `yii\web\Response` с свойством `format`, по умолчанию инициализируемом как `json`. +- `app\storage\FileStorage` и `app\storage\DocumentsReader`, где реализована некая логика для работы с документами в + неком файловом хранилище: + + ```php + class FileStorage + { + public function __contruct($root) { + // делаем что-то + } + } + + class DocumentsReader + { + public function __contruct(FileStorage $fs) { + // делаем что-то + } + } + ``` + +Возможно настроить несколько компонентов сразу передав массив конфигурации в метод +[[yii\di\Container::setDefinitions()|setDefinitions()]] или [[yii\di\Container::setSingletons()|setSingletons()]]. +Внутри метода фреймворк обойдёт массив конфигурации и вызовет для каждого элемента [[yii\di\Container::set()|set()]] или +[[yii\di\Container::setSingleton()|setSingleton()]] соответственно. + +Формат массива конфигурации следующий: + + - Ключ: имя класса, интерфейса или псевдонима. Ключ передаётся в первый аргумент `$class` метода + [[yii\di\Container::set()|set()]]. + - Значение: конфигурация для класса. Возможные значения описаны в документации параметра `$definition` метода + [[yii\di\Container::set()|set()]]. Значение передаётся в аргумент `$definition` метода [[set()]]. + +Для примера, давайте настроим наш контейнер: + +```php +$container->setDefinitions([ + 'yii\web\Request' => 'app\components\Request', + 'yii\web\Response' => [ + 'class' => 'app\components\Response', + 'format' => 'json' + ], + 'app\storage\DocumentsReader' => function () { + $fs = new app\storage\FileStorage('/var/tempfiles'); + return new app\storage\DocumentsReader($fs); + } +]); + +$reader = $container->get('app\storage\DocumentsReader); +// Создаст объект DocumentReader со всеми зависимостями +``` + +> Tip: Начиная с версии 2.0.11 контейнер может быть настроен в декларативном стиле через конфигурацию приложения. +Как это сделать ищите в подразделе [Конфигурация приложения](concept-service-locator.md#application-configurations) +раздела [Конфигурации](concept-configurations.md). + +Вроде всё работает, но если нам необходимо создать экземпляр класса `DocumentWriter`, придётся скопировать код, +создающий экземпляр`FileStorage`, что, очевидно, не является оптимальным. + +Как описано в подразделе [Разрешение зависимостей](#resolving-dependencies), [[yii\di\Container::set()|set()]] +и [[yii\di\Container::setSingleton()|setSingleton()]] могут опционально принимать третьим аргументов параметры +для конструктора. Формат таков: + + - Ключ: имя класса, интерфейса или псевдонима. Ключ передаётся в первый аргумент `$class` метода [[yii\di\Container::set()|set()]]. + - Значение: массив из двух элементов. Первый элемент передаётся в метод [[yii\di\Container::set()|set()]] вторым + аргументом `$definition`, второй элемент — аргументом `$params`. + +Исправим наш пример: + +```php +$container->setDefinitions([ + 'tempFileStorage' => [ // для удобства мы задали псевдоним + ['class' => 'app\storage\FileStorage'], + ['/var/tempfiles'] + ], + 'app\storage\DocumentsReader' => [ + ['class' => 'app\storage\DocumentsReader'], + [Instance::of('tempFileStorage')] + ], + 'app\storage\DocumentsWriter' => [ + ['class' => 'app\storage\DocumentsWriter'], + [Instance::of('tempFileStorage')] + ] +]); + +$reader = $container->get('app\storage\DocumentsReader); +// Код будет работать ровно так же, как и в предыдущем примере. +``` + +Вы могли заметить вызов `Instance::of('tempFileStorage')`. Он означает, что [[yii\di\Container|Container]] +наявно предоставит зависимость, зарегистрированную с именем `tempFileStorage` и передаст её первым аргументом +в конструктор `app\storage\DocumentsWriter`. + +> Note: Методы [[yii\di\Container::setDefinitions()|setDefinitions()]] и [[yii\di\Container::setSingletons()|setSingletons()]] + доступны с версии 2.0.11. + +Ещё один шаг по оптимизации конфигурации — регистрировать некоторые зависимости как синглтоны. Зависимость, регистрируемая +через метод [[yii\di\Container::set()|set()]] будет созаваться каждый раз при обращении к ней. Некоторые классы не меняют +своего состояния на протяжении всей работы приложения, поэтому могут быть зарегистрированы как синглтоны. Это увеличит +производительность приложения. + +Хорошим примером может быть класс `app\storage\FileStorage`, который выполняет некие операции над файловой системой +через простой API: `$fs->read()`, `$fs->write()`. Обе операции не меняют внутреннее состояние класса, поэтому мы можем +создать класс один раз и далее использовать его. + +```php +$container->setSingletons([ + 'tempFileStorage' => [ + ['class' => 'app\storage\FileStorage'], + ['/var/tempfiles'] + ], +]); + +$container->setDefinitions([ + 'app\storage\DocumentsReader' => [ + ['class' => 'app\storage\DocumentsReader'], + [Instance::of('tempFileStorage')] + ], + 'app\storage\DocumentsWriter' => [ + ['class' => 'app\storage\DocumentsWriter'], + [Instance::of('tempFileStorage')] + ] +]); + +$reader = $container->get('app\storage\DocumentsReader); +``` + ### Внедрение зависимости через PHP callback В данном случае, контейнер будет использовать зарегистрированный PHP callback для создания новых экземпляров класса. @@ -211,13 +343,16 @@ $container->setSingleton('yii\db\Connection', [ Разрешение зависимостей ---------------------- После регистрации зависимостей, вы можете использовать контейнер внедрения зависимостей для создания новых объектов, -и контейнер автоматически разрешит зависимости их экземпляра и их внедрений во вновь создаваемых объектах. Разрешение зависимостей рекурсивно, то есть -если зависимость имеет другие зависимости, эти зависимости также будут автоматически разрешены. +и контейнер автоматически разрешит зависимости их экземпляра и их внедрений во вновь создаваемых объектах. Разрешение +зависимостей рекурсивно, то есть если зависимость имеет другие зависимости, эти зависимости также будут автоматически +разрешены. -Вы можете использовать [[yii\di\Container::get()]] для создания новых объектов. Метод принимает имя зависимости, которым может быть имя класса, имя интерфейса или псевдоним. -Имя зависимости может быть или не может быть зарегистрировано через `set()` или `setSingleton()`. -Вы можете опционально предоставить список параметров конструктора класса и [конфигурацию](concept-configurations.md) для настройки созданного объекта. -Например, +Вы можете использовать [[yii\di\Container::get()]] для создания или получения объектов. Метод принимает имя зависимости, +которым может быть имя класса, имя интерфейса или псевдоним. Имя зависимости может быть зарегистрировано через +`set()` или `setSingleton()`. Вы можете опционально предоставить список параметров конструктора класса и +[конфигурацию](concept-configurations.md) для настройки созданного объекта. + +Например: ```php // "db" ранее зарегистрированный псевдоним @@ -228,11 +363,14 @@ $engine = $container->get('app\components\SearchEngine', [$apiKey], ['type' => 1 ``` За кулисами, контейнер внедрения зависимостей делает гораздо больше работы, чем просто создание нового объекта. -Прежде всего, контейнер, осмотрит конструктор класса, чтобы узнать имя зависимого класса или интерфейса, а затем автоматически разрешит эти зависимости рекурсивно. +Прежде всего, контейнер, осмотрит конструктор класса, чтобы узнать имя зависимого класса или интерфейса, а затем +автоматически разрешит эти зависимости рекурсивно. -Следующий код демонстрирует более сложный пример. Класс `UserLister` зависит от объекта, реализующего интерфейс `UserFinderInterface`; класс `UserFinder` реализует этот интерфейс и зависит от - объекта `Connection`. Все эти зависимости были объявлены через тип подсказки параметров конструктора класса. -При регистрации зависимости через свойство, контейнер внедрения зависимостей позволяет автоматически разрешить эти зависимости и создаёт новый экземпляр `UserLister` простым вызовом `get('userLister')`. +Следующий код демонстрирует более сложный пример. Класс `UserLister` зависит от объекта, реализующего интерфейс +`UserFinderInterface`; класс `UserFinder` реализует этот интерфейс и зависит от объекта `Connection`. Все эти зависимости +были объявлены через тип подсказки параметров конструктора класса. При регистрации зависимости через свойство, контейнер +внедрения зависимостей позволяет автоматически разрешить эти зависимости и создаёт новый экземпляр `UserLister` простым +вызовом `get('userLister')`. ```php namespace app\models; @@ -291,17 +429,17 @@ $lister = new UserLister($finder); ``` -Практическое использование +Практическое применение --------------- Yii создаёт контейнер внедрения зависимостей когда вы подключаете файл `Yii.php` во [входном скрипте](structure-entry-scripts.md) вашего приложения. Контейнер внедрения зависимостей доступен через [[Yii::$container]]. При вызове [[Yii::createObject()]], метод на самом деле вызовет метод контейнера [[yii\di\Container::get()|get()]], чтобы создать новый объект. -Как упомянуто выше, контейнер внедрения зависимостей автоматически разрешит зависимости (если таковые имеются) и внедрит их в только что созданный объект. -Поскольку Yii использует [[Yii::createObject()]] в большей части кода своего ядра для создания новых объектов, это означает, -что вы можете настроить глобальные объекты, имея дело с [[Yii::$container]]. +Как упомянуто выше, контейнер внедрения зависимостей автоматически разрешит зависимости (если таковые имеются) и внедрит их +получаемый объект. Поскольку Yii использует [[Yii::createObject()]] в большей части кода своего ядра для создания новых +объектов, это означает, что вы можете настроить глобальные объекты, имея дело с [[Yii::$container]]. -Например, вы можете настроить по умолчанию глобальное количество кнопок в пейджере [[yii\widgets\LinkPager]]: +Например, давайте настроим количество кнопок в пейджере [[yii\widgets\LinkPager]] по умолчанию глобально: ```php \Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]); @@ -356,8 +494,11 @@ class HotelController extends Controller Поскольку зависимости необходимы тогда, когда создаются новые объекты, то их регистрация должна быть сделана как можно раньше. Ниже приведены рекомендуемые практики: -* Если вы разработчик приложения, то вы можете зарегистрировать зависимости во [входном скрипте](structure-entry-scripts.md) вашего приложения или в скрипте, подключённого во входном скрипте. -* Если вы разработчик распространяемого [расширения](structure-extensions.md), то вы можете зарегистрировать зависимости в загрузочном классе расширения. +* Если вы разработчик приложения, то вы можете зарегистрировать зависимости в конфигурации вашего приложения. + Как это сделать описано в подразделе [Конфигурация приложения](concept-service-locator.md#application-configurations) + раздела [Конфигурации](concept-configurations.md). +* Если вы разработчик распространяемого [расширения](structure-extensions.md), то вы можете зарегистрировать зависимости + в загрузочном классе расширения. Итог From d815ca102ac98a4ed274896ad51e4bad41ef4009 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 8 Dec 2016 15:27:12 +0300 Subject: [PATCH 059/105] Updated Russian translation of AR guide [skip ci] --- docs/guide-ru/db-active-record.md | 52 ++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/docs/guide-ru/db-active-record.md b/docs/guide-ru/db-active-record.md index ef0418aec6..e023e37c5a 100644 --- a/docs/guide-ru/db-active-record.md +++ b/docs/guide-ru/db-active-record.md @@ -49,9 +49,20 @@ Yii поддерживает работу с Active Record для следующ ## Объявление классов Active Record -Для начала объявите свой собственный класс, унаследовав класс [[yii\db\ActiveRecord]]. Поскольку каждый класс -Active Record сопоставлен с таблицей в базе данных, в своём классе вы должны переопределить метод -[[yii\db\ActiveRecord::tableName()|tableName()]], чтобы указать с какой именно таблицей связан ваш класс. +Для начала объявите свой собственный класс, унаследовав класс [[yii\db\ActiveRecord]]. + +### Настройка имени таблицы + +По умолчанию каждый класс Active Record ассоциирован с таблицей в базе данных. Метод +[[yii\db\ActiveRecord::tableName()|tableName()]] получает имя таблицы из имени класса с помощью [[yii\helpers\Inflector::camel2id()]]. +Если таблица не названа соответственно, вы можете переопределить данный метод. + +Также может быть применён [[yii\db\Connection::$tablePrefix|tablePrefix]] по умолчанию. Например, если +[[yii\db\Connection::$tablePrefix|tablePrefix]] задан как `tbl_`, `Customer` преобразуется в `tbl_customer`, а +`OrderItem` в `tbl_order_item`. + +Если имя таблицы указано в формате `{{%TableName}}`, символ `%` заменяется префиксом. Например, , `{{%post}}` становится +`{{tbl_post}}`. Фигуриные скобки используются для [экранирования в SQL-запросах](db-dao.md#quoting-table-and-column-names). В нижеследующем примере мы объявляем класс Active Record с названием `Customer` для таблицы `customer`. @@ -70,11 +81,13 @@ class Customer extends ActiveRecord */ public static function tableName() { - return 'customer'; + return '{{customer}}'; } } ``` +### Классы Active record называются "моделями" + Объекты Active Record являются [моделями](structure-models.md). Именно поэтому мы обычно задаём классам Active Record пространство имён `app\models` (или другое пространство имён, предназначенное для моделей). @@ -1134,6 +1147,37 @@ $customers = Customer::find()->joinWith([ [[yii\db\ActiveQuery::onCondition()|onCondition()]], это условие будет размещено в конструкции `ON`, если запрос содержит оператор JOIN. Если же запрос не содержит оператор JOIN, такое условие будет автоматически размещено в конструкции `WHERE`. + +#### Псевдонимы связанных таблиц + +Как уже было отмечено, при использовании в запросе JOIN-ов, приходится явно решать конфликты имён. Поэтому часто таблицам +дают псевдонимы. Задать псевдоним для реляционного запроса можно следующим образом: + +```php +$query->joinWith([ + 'orders' => function ($q) { + $q->from(['o' => Order::tableName()]); + }, +]) +``` + +Выглядит это довольно сложно. Либо приходится задавать явно имена таблиц, либо вызывать `Order::tableName()`. +Начиная с версии 2.0.7 вы можете задать и использовать псевдоним для связанной таблицы следующим образом: + +```php +// join the orders relation and sort the result by orders.id +$query->joinWith(['orders o'])->orderBy('o.id'); +``` + +Этот синтаксис работает для простых связей. Если же необходимо использовать связующую таблицу, например +`$query->joinWith(['orders.product'])`, то вызовы joinWith вкладываются друг в друга: + +```php +$query->joinWith(['orders o' => function($q) { + $q->joinWith('product p'); + }]) + ->where('o.amount > 100'); +``` ### Обратные связи From 6e826dfade78af601813c675e82751377e15f349 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 8 Dec 2016 15:33:22 +0300 Subject: [PATCH 060/105] Assorted minor Russian guide updates [skip ci] --- docs/guide-ru/db-query-builder.md | 4 ++++ docs/guide-ru/helper-array.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/guide-ru/db-query-builder.md b/docs/guide-ru/db-query-builder.md index 6d70dccf71..b606f839bb 100644 --- a/docs/guide-ru/db-query-builder.md +++ b/docs/guide-ru/db-query-builder.md @@ -146,6 +146,10 @@ $subQuery = (new Query())->select('id')->from('user')->where('status=1'); $query->from(['u' => $subQuery]); ``` +#### Префиксы + +Также может применяться [[yii\db\Connection::$tablePrefix|tablePrefix]] по умолчанию. Подробное описание смотрите +в подразделе [«Экранирование имён таблиц и столбцов» раздела «Объекты доступа к данным (DAO)»](guide-db-dao.html#quoting-table-and-column-names). ### [[yii\db\Query::where()|where()]] diff --git a/docs/guide-ru/helper-array.md b/docs/guide-ru/helper-array.md index 3dc3bacd1f..2b4be9731a 100644 --- a/docs/guide-ru/helper-array.md +++ b/docs/guide-ru/helper-array.md @@ -81,7 +81,7 @@ if (!ArrayHelper::keyExists('username', $data1, false) || !ArrayHelper::keyExist Часто нужно извлечь столбец значений из многомерного массива или объекта. Например, список ID. ```php -$data = [ +$array = [ ['id' => '123', 'data' => 'abc'], ['id' => '345', 'data' => 'def'], ]; From 33c27bcf1eb053f7be17b4dfea0d32125f457a4f Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Thu, 8 Dec 2016 16:56:39 +0100 Subject: [PATCH 061/105] Update yii.php fixes #13169 --- framework/messages/bg/yii.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/messages/bg/yii.php b/framework/messages/bg/yii.php index 9206c9bff3..be63e9cd2e 100644 --- a/framework/messages/bg/yii.php +++ b/framework/messages/bg/yii.php @@ -71,7 +71,7 @@ return array ( '{attribute} is not a valid email address.' => 'Полето "{attribute}" съдържа невалиден email адрес.', '{attribute} must be "{requiredValue}".' => 'Полето "{attribute}" трябва да съдържа "{requiredValue}".', '{attribute} must be a number.' => 'Полето "{attribute}" съдържа невалиден номер.', - '{attribute} must be a string.' => 'Полето "{attribute}" трябва съдържа текст.', + '{attribute} must be a string.' => 'Полето "{attribute}" трябва да съдържа текст.', '{attribute} must be an integer.' => 'Полето "{attribute}" трябва да съдържа цяло число.', '{attribute} must be either "{true}" or "{false}".' => 'Полето "{attribute}" трябва да бъде "{true}" или "{false}".', '{attribute} must be greater than "{compareValue}".' => 'Полето "{attribute}" трябва да е по-голямо от "{compareValue}".', From 65e4a28800acaf6df18d67b6cbd1216126120c87 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Thu, 8 Dec 2016 18:00:05 +0100 Subject: [PATCH 062/105] consistent behavior for Yii::$app->controller and ->action (#13167) Fixed inconsistency, `Yii::$app->controller` is available after handling the request since 2.0.10, this is now also the case for `Yii::$app->controller->action`. fixes #12795 --- framework/CHANGELOG.md | 1 + framework/base/Controller.php | 4 +- tests/framework/base/ControllerTest.php | 57 +++++++++++++++++++++++++ tests/framework/base/ModuleTest.php | 47 ++++++++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 tests/framework/base/ControllerTest.php diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 1fcf8dc7ef..296b8fb8eb 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -12,6 +12,7 @@ Yii Framework 2 Change Log - Bug #12714: Fixed `yii\validation\EmailValidator` to prevent false-positives checks when property `checkDns` is set to `true` (silverfire) - Bug #12735: Fixed `yii\console\controllers\MigrateController` creating multiple primary keys for field `bigPrimaryKey:unsigned` (SG5) - Bug #12791: Fixed `yii\behaviors\AttributeTypecastBehavior` unable to automatically detect `attributeTypes`, triggering PHP Fatal Error (klimov-paul) +- Bug #12795: Fixed inconsistency, `Yii::$app->controller` is available after handling the request since 2.0.10, this is now also the case for `Yii::$app->controller->action` (cebe) - Bug #12803, #12921: Fixed BC break in `yii.activeForm.js` introduced in #11999. Reverted commit 3ba72da (silverfire) - Bug #12810: Fixed `yii\rbac\DbManager::getChildRoles()` and `yii\rbac\PhpManager::getChildRoles()` throws an exception when role has no child roles (mysterydragon) - Bug #12822: Fixed `yii\i18n\Formatter::asTimestamp()` to process timestamp with miliseconds correctly (h311ion) diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 93143ecd2a..41e98cb6d6 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -164,7 +164,9 @@ class Controller extends Component implements ViewContextInterface } } - $this->action = $oldAction; + if ($oldAction !== null) { + $this->action = $oldAction; + } return $result; } diff --git a/tests/framework/base/ControllerTest.php b/tests/framework/base/ControllerTest.php new file mode 100644 index 0000000000..fdf742ecaf --- /dev/null +++ b/tests/framework/base/ControllerTest.php @@ -0,0 +1,57 @@ +mockApplication(); + + static::$actionRuns = []; + $controller = new TestController('test-controller', Yii::$app); + $this->assertNull($controller->action); + $result = $controller->runAction('test1'); + $this->assertEquals('test1', $result); + $this->assertEquals([ + 'test-controller/test1', + ], static::$actionRuns); + $this->assertNotNull($controller->action); + $this->assertEquals('test1', $controller->action->id); + $this->assertEquals('test-controller/test1', $controller->action->uniqueId); + + $result = $controller->runAction('test2'); + $this->assertEquals('test2', $result); + $this->assertEquals([ + 'test-controller/test1', + 'test-controller/test2', + ], static::$actionRuns); + $this->assertNotNull($controller->action); + $this->assertEquals('test1', $controller->action->id); + $this->assertEquals('test-controller/test1', $controller->action->uniqueId); + } +} + + +class TestController extends Controller +{ + public function actionTest1() + { + ControllerTest::$actionRuns[] = $this->action->uniqueId; + return 'test1'; + } + public function actionTest2() + { + ControllerTest::$actionRuns[] = $this->action->uniqueId; + return 'test2'; + } +} \ No newline at end of file diff --git a/tests/framework/base/ModuleTest.php b/tests/framework/base/ModuleTest.php index 20071ceb2a..c2348c1450 100644 --- a/tests/framework/base/ModuleTest.php +++ b/tests/framework/base/ModuleTest.php @@ -2,6 +2,8 @@ namespace yiiunit\framework\base; +use Yii; +use yii\base\Controller; use yiiunit\TestCase; /** @@ -47,9 +49,54 @@ class ModuleTest extends TestCase $version = $module->getVersion(); $this->assertEquals('1.0', $version); } + + public static $actionRuns = []; + + public function testRunControllerAction() + { + $module = new TestModule('test'); + $this->assertNull(Yii::$app->controller); + static::$actionRuns = []; + + $module->runAction('test-controller1/test1'); + $this->assertEquals([ + 'test/test-controller1/test1', + ], static::$actionRuns); + $this->assertNotNull(Yii::$app->controller); + $this->assertEquals('test-controller1', Yii::$app->controller->id); + $this->assertEquals('test/test-controller1', Yii::$app->controller->uniqueId); + $this->assertNotNull(Yii::$app->controller->action); + $this->assertEquals('test/test-controller1/test1', Yii::$app->controller->action->uniqueId); + + $module->runAction('test-controller2/test2'); + $this->assertEquals([ + 'test/test-controller1/test1', + 'test/test-controller2/test2', + ], static::$actionRuns); + $this->assertNotNull(Yii::$app->controller); + $this->assertEquals('test-controller1', Yii::$app->controller->id); + $this->assertEquals('test/test-controller1', Yii::$app->controller->uniqueId); + $this->assertNotNull(Yii::$app->controller->action); + $this->assertEquals('test/test-controller1/test1', Yii::$app->controller->action->uniqueId); + } } class TestModule extends \yii\base\Module { + public $controllerMap = [ + 'test-controller1' => 'yiiunit\framework\base\ModuleTestController', + 'test-controller2' => 'yiiunit\framework\base\ModuleTestController', + ]; +} +class ModuleTestController extends Controller +{ + public function actionTest1() + { + ModuleTest::$actionRuns[] = $this->action->uniqueId; + } + public function actionTest2() + { + ModuleTest::$actionRuns[] = $this->action->uniqueId; + } } \ No newline at end of file From 92eee10ae194d6428cd0688c24aa4494185745ae Mon Sep 17 00:00:00 2001 From: Boudewijn Vahrmeijer Date: Thu, 8 Dec 2016 21:22:18 +0100 Subject: [PATCH 063/105] Change the name of method getQueryTableName and remove its $query argument (#12893) * refactores getQueryTableName: * replaces the $query argument with a $this implementation * exposes getQueryTableName to be public instead of private. Fixes #12878 * added unit tests for exposed method * updated changelog * - methodname changed to 'getTableNameAndAlias' - scope back to private - added @internal tag to emphasize that the method is used purely for the internal workings of this piece of software. - removed changelog (as the API has not changed) * update tests --- framework/db/ActiveQuery.php | 18 ++++++++--------- tests/framework/db/ActiveQueryTest.php | 28 +++++++++++++++++++++----- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/framework/db/ActiveQuery.php b/framework/db/ActiveQuery.php index 9ad09c1838..c25ea8d1d4 100644 --- a/framework/db/ActiveQuery.php +++ b/framework/db/ActiveQuery.php @@ -155,7 +155,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface } if (empty($this->select) && !empty($this->join)) { - list(, $alias) = $this->getQueryTableName($this); + list(, $alias) = $this->getTableNameAndAlias(); $this->select = ["$alias.*"]; } @@ -551,18 +551,18 @@ class ActiveQuery extends Query implements ActiveQueryInterface /** * Returns the table name and the table alias for [[modelClass]]. - * @param ActiveQuery $query * @return array the table name and the table alias. + * @internal */ - private function getQueryTableName($query) + private function getTableNameAndAlias() { - if (empty($query->from)) { + if (empty($this->from)) { /* @var $modelClass ActiveRecord */ - $modelClass = $query->modelClass; + $modelClass = $this->modelClass; $tableName = $modelClass::tableName(); } else { $tableName = ''; - foreach ($query->from as $alias => $tableName) { + foreach ($this->from as $alias => $tableName) { if (is_string($alias)) { return [$tableName, $alias]; } else { @@ -603,8 +603,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface return; } - list ($parentTable, $parentAlias) = $this->getQueryTableName($parent); - list ($childTable, $childAlias) = $this->getQueryTableName($child); + list ($parentTable, $parentAlias) = $parent->getTableNameAndAlias(); + list ($childTable, $childAlias) = $child->getTableNameAndAlias(); if (!empty($child->link)) { @@ -778,7 +778,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface public function alias($alias) { if (empty($this->from) || count($this->from) < 2) { - list($tableName, ) = $this->getQueryTableName($this); + list($tableName, ) = $this->getTableNameAndAlias(); $this->from = [$alias => $tableName]; } else { /* @var $modelClass ActiveRecord */ diff --git a/tests/framework/db/ActiveQueryTest.php b/tests/framework/db/ActiveQueryTest.php index 7e0f6f7175..6825aa7763 100644 --- a/tests/framework/db/ActiveQueryTest.php +++ b/tests/framework/db/ActiveQueryTest.php @@ -118,7 +118,7 @@ abstract class ActiveQueryTest extends DatabaseTestCase } /** - * @todo: tests for internal logic of joinWith() + * @todo: tests for internal logic of innerJoinWith() */ public function testInnerJoinWith() { @@ -129,6 +129,24 @@ abstract class ActiveQueryTest extends DatabaseTestCase ], $result->joinWith); } + /** + * @todo: tests for the regex inside getQueryTableName + */ + public function testGetQueryTableName_from_not_set() + { + $query = new ActiveQuery(Customer::className()); + $result = $this->invokeMethod($query,'getTableNameAndAlias'); + $this->assertEquals(['customer','customer'], $result); + } + + public function testGetQueryTableName_from_set() + { + $options = ['from' => ['alias'=>'customer']]; + $query = new ActiveQuery(Customer::className(),$options); + $result = $this->invokeMethod($query,'getTableNameAndAlias'); + $this->assertEquals(['customer','alias'], $result); + } + public function testOnCondition() { $query = new ActiveQuery(Customer::className()); @@ -139,7 +157,7 @@ abstract class ActiveQueryTest extends DatabaseTestCase $this->assertEquals($params, $result->params); } - public function testAndOnCondition_OnIsNull() + public function testAndOnCondition_on_not_set() { $query = new ActiveQuery(Customer::className()); $on = ['active' => true]; @@ -149,7 +167,7 @@ abstract class ActiveQueryTest extends DatabaseTestCase $this->assertEquals($params, $result->params); } - public function testAndOnCondition_OnIsNotNull() + public function testAndOnCondition_on_set() { $onOld = ['active' => true]; $query = new ActiveQuery(Customer::className()); @@ -162,7 +180,7 @@ abstract class ActiveQueryTest extends DatabaseTestCase $this->assertEquals($params, $result->params); } - public function testOrOnCondition_OnIsNull() + public function testOrOnCondition_on_not_set() { $query = new ActiveQuery(Customer::className()); $on = ['active' => true]; @@ -172,7 +190,7 @@ abstract class ActiveQueryTest extends DatabaseTestCase $this->assertEquals($params, $result->params); } - public function testOrOnCondition_OnIsNotNull() + public function testOrOnCondition_on_set() { $onOld = ['active' => true]; $query = new ActiveQuery(Customer::className()); From 3611aa409bc96c4e0e26baae541dfc05c0c926a9 Mon Sep 17 00:00:00 2001 From: Sergey Makinen Date: Fri, 9 Dec 2016 01:12:52 +0300 Subject: [PATCH 064/105] Fixes PageCacheTest: requestedRoute is not set up (#13173) --- tests/framework/filters/PageCacheTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/framework/filters/PageCacheTest.php b/tests/framework/filters/PageCacheTest.php index 9d38e82c8b..c23a797c7b 100644 --- a/tests/framework/filters/PageCacheTest.php +++ b/tests/framework/filters/PageCacheTest.php @@ -296,6 +296,7 @@ class PageCacheTest extends TestCase $this->mockWebApplication(); $controller = new Controller('test', Yii::$app); $action = new Action('test', $controller); + Yii::$app->requestedRoute = $action->uniqueId; $filter = new PageCache([ 'cache' => $cache = new ArrayCache(), 'view' => new View(), @@ -317,6 +318,7 @@ class PageCacheTest extends TestCase $this->mockWebApplication(); $controller = new Controller('test', Yii::$app); $action = new Action('test2', $controller); + Yii::$app->requestedRoute = $action->uniqueId; $filter = new PageCache([ 'cache' => $cache, 'view' => new View(), From bd85b7ced22afa81284a6a0a1c4898f5041897bc Mon Sep 17 00:00:00 2001 From: Alexey Rogachev Date: Fri, 9 Dec 2016 04:29:53 +0600 Subject: [PATCH 065/105] Closes #6242: Access to validator in inline validation (#13027) * Closes #6242: Access to validator in inline validation * Updated PHPDoc according to PR review [skip ci] * Imrpoved PHPDoc, updated guide info [skip ci] * Fixes related with PR review * Corrected CHANGELOG [skip ci] * Update input-validation.md added version info --- docs/guide/input-validation.md | 15 ++++++++--- framework/CHANGELOG.md | 1 + framework/validators/InlineValidator.php | 23 +++++++++------- .../models/FakedValidationModel.php | 20 +++++++++++++- tests/framework/validators/ValidatorTest.php | 27 +++++++++++++++++++ 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/docs/guide/input-validation.md b/docs/guide/input-validation.md index 2c209d1796..b02078b551 100644 --- a/docs/guide/input-validation.md +++ b/docs/guide/input-validation.md @@ -331,8 +331,9 @@ the method/function is: /** * @param string $attribute the attribute currently being validated * @param mixed $params the value of the "params" given in the rule + * @param \yii\validators\InlineValidator related InlineValidator instance */ -function ($attribute, $params) +function ($attribute, $params, $validator) ``` If an attribute fails the validation, the method/function should call [[yii\base\Model::addError()]] to save @@ -355,7 +356,7 @@ class MyForm extends Model ['country', 'validateCountry'], // an inline validator defined as an anonymous function - ['token', function ($attribute, $params) { + ['token', function ($attribute, $params, $validator) { if (!ctype_alnum($this->$attribute)) { $this->addError($attribute, 'The token must contain letters or digits.'); } @@ -363,7 +364,7 @@ class MyForm extends Model ]; } - public function validateCountry($attribute, $params) + public function validateCountry($attribute, $params, $validator) { if (!in_array($this->$attribute, ['USA', 'Web'])) { $this->addError($attribute, 'The country must be either "USA" or "Web".'); @@ -372,6 +373,14 @@ class MyForm extends Model } ``` +> Note: Since version 2.0.11 you can use [[yii\validators\InlineValidator::addError()]] for adding errors instead. That way the error +> message can be formatted using [[yii\i18n\I18N::format()]] right away. Use `{attribute}` and `{value}` in the error +> message to refer to an attribute label (no need to get it manually) and attribute value accordingly: +> +> ```php +> $validator->addError($this, $attribute, 'The value "{value}" is not acceptable for {attribute}.'); +> ``` + > Note: By default, inline validators will not be applied if their associated attributes receive empty inputs or if they have already failed some validation rules. If you want to make sure a rule is always applied, you may configure the [[yii\validators\Validator::skipOnEmpty|skipOnEmpty]] and/or [[yii\validators\Validator::skipOnError|skipOnError]] diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 296b8fb8eb..2a8f5e5972 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -30,6 +30,7 @@ Yii Framework 2 Change Log - Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) - Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire) +- Enh #6242: Access to validator in inline validation (arogachev) - Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) - Enh #6809: Added `yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) - Enh #7333: Improved error message for `yii\di\Instance::ensure()` when a component does not exist (cebe) diff --git a/framework/validators/InlineValidator.php b/framework/validators/InlineValidator.php index 8423e8bf28..7392dc7e8e 100644 --- a/framework/validators/InlineValidator.php +++ b/framework/validators/InlineValidator.php @@ -13,11 +13,12 @@ namespace yii\validators; * The validation method must have the following signature: * * ```php - * function foo($attribute, $params) + * function foo($attribute, $params, $validator) * ``` * - * where `$attribute` refers to the name of the attribute being validated, while `$params` - * is an array representing the additional parameters supplied in the validation rule. + * where `$attribute` refers to the name of the attribute being validated, while `$params` is an array representing the + * additional parameters supplied in the validation rule. Parameter `$validator` refers to the related + * [[InlineValidator]] object and is available since version 2.0.11. * * @author Qiang Xue * @since 2.0 @@ -26,13 +27,15 @@ class InlineValidator extends Validator { /** * @var string|\Closure an anonymous function or the name of a model class method that will be - * called to perform the actual validation. The signature of the method should be like the following, - * where `$attribute` is the name of the attribute to be validated, and `$params` contains the value - * of [[params]] that you specify when declaring the inline validation rule: + * called to perform the actual validation. The signature of the method should be like the following: * * ```php - * function foo($attribute, $params) + * function foo($attribute, $params, $validator) * ``` + * + * - `$attribute` is the name of the attribute to be validated; + * - `$params` contains the value of [[params]] that you specify when declaring the inline validation rule; + * - `$validator` is a reference to related [[InlineValidator]] object. */ public $method; /** @@ -44,7 +47,7 @@ class InlineValidator extends Validator * The signature of the method should be like the following: * * ```php - * function foo($attribute, $params) + * function foo($attribute, $params, $validator) * { * return "javascript"; * } @@ -66,7 +69,7 @@ class InlineValidator extends Validator if (is_string($method)) { $method = [$model, $method]; } - call_user_func($method, $attribute, $this->params); + call_user_func($method, $attribute, $this->params, $this); } /** @@ -80,7 +83,7 @@ class InlineValidator extends Validator $method = [$model, $method]; } - return call_user_func($method, $attribute, $this->params); + return call_user_func($method, $attribute, $this->params, $this); } else { return null; } diff --git a/tests/data/validators/models/FakedValidationModel.php b/tests/data/validators/models/FakedValidationModel.php index 6da3aa290e..fa08686a2f 100644 --- a/tests/data/validators/models/FakedValidationModel.php +++ b/tests/data/validators/models/FakedValidationModel.php @@ -11,6 +11,7 @@ class FakedValidationModel extends Model public $val_attr_c; public $val_attr_d; private $attr = []; + private $inlineValArgs; /** * @param array $attributes @@ -36,11 +37,18 @@ class FakedValidationModel extends Model ]; } - public function inlineVal($attribute, $params = []) + public function inlineVal($attribute, $params = [], $validator) { + $this->inlineValArgs = func_get_args(); + return true; } + public function clientInlineVal($attribute, $params = [], $validator) + { + return func_get_args(); + } + public function __get($name) { if (stripos($name, 'attr') === 0) { @@ -63,4 +71,14 @@ class FakedValidationModel extends Model { return $attr; } + + /** + * Returns the arguments of the inlineVal method in the last call. + * @return array|null an array of arguments in the last call or null if method never been called. + * @see inlineVal + */ + public function getInlineValArgs() + { + return $this->inlineValArgs; + } } diff --git a/tests/framework/validators/ValidatorTest.php b/tests/framework/validators/ValidatorTest.php index f2c1721550..2a7d9ff5da 100644 --- a/tests/framework/validators/ValidatorTest.php +++ b/tests/framework/validators/ValidatorTest.php @@ -176,12 +176,39 @@ class ValidatorTest extends TestCase $val->validate('abc'); } + public function testValidateAttribute() + { + // Access to validator in inline validation (https://github.com/yiisoft/yii2/issues/6242) + + $model = new FakedValidationModel(); + $val = TestValidator::createValidator('inlineVal', $model, ['val_attr_a'], ['params' => ['foo' => 'bar']]); + $val->validateAttribute($model, 'val_attr_a'); + $args = $model->getInlineValArgs(); + + $this->assertCount(3, $args); + $this->assertEquals('val_attr_a', $args[0]); + $this->assertEquals(['foo' => 'bar'], $args[1]); + $this->assertInstanceOf(InlineValidator::className(), $args[2]); + } + public function testClientValidateAttribute() { $val = new TestValidator(); $this->assertNull( $val->clientValidateAttribute($this->getTestModel(), 'attr_runMe1', []) ); //todo pass a view instead of array + + // Access to validator in inline validation (https://github.com/yiisoft/yii2/issues/6242) + + $model = new FakedValidationModel(); + $val = TestValidator::createValidator('inlineVal', $model, ['val_attr_a'], ['params' => ['foo' => 'bar']]); + $val->clientValidate = 'clientInlineVal'; + $args = $val->clientValidateAttribute($model, 'val_attr_a', null); + + $this->assertCount(3, $args); + $this->assertEquals('val_attr_a', $args[0]); + $this->assertEquals(['foo' => 'bar'], $args[1]); + $this->assertInstanceOf(InlineValidator::className(), $args[2]); } public function testIsActive() From d96363674c0bcbd860cc36cc4d2f5ca7baceafb5 Mon Sep 17 00:00:00 2001 From: Alexey Rogachev Date: Sat, 10 Dec 2016 18:42:28 +0600 Subject: [PATCH 066/105] Added JS tests for yii.captcha.js (#12840), fixes #13159 (#13160) * Added JS tests for yii.captcha.js (#12840), fixes #13159 * Sinon needs to be imported in similar fashion in other test files * Added according line to CHANGELOG [skip ci] * Update CHANGELOG.md * Try to run tests with default timeout in Travis * Added note about JS tests to the docs [skip ci] * Simplified tests for multiple elements (init, destroy methods) --- docs/internals/git-workflow.md | 16 ++- framework/CHANGELOG.md | 1 + framework/assets/yii.captcha.js | 11 +- tests/js/tests/yii.captcha.test.js | 151 ++++++++++++++++++++++++++ tests/js/tests/yii.validation.test.js | 5 +- 5 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 tests/js/tests/yii.captcha.test.js diff --git a/docs/internals/git-workflow.md b/docs/internals/git-workflow.md index ce9cbe8a39..1aa9e1f154 100644 --- a/docs/internals/git-workflow.md +++ b/docs/internals/git-workflow.md @@ -39,6 +39,14 @@ The following steps are not necessary if you want to work only on translations o > Note: If you see errors like `Problem 1 The requested package bower-asset/jquery could not be found in any version, there may be a typo in the package name.`, you will need to run `composer global require "fxp/composer-asset-plugin:^1.2.0"` +If you are going to work with JavaScript: + +- run `npm install` to install JavaScript testing tools and dependencies (assuming you have [Node.js and NPM installed] +(https://nodejs.org/en/download/package-manager/)). + +> Note: JavaScript tests depend on [jsdom](https://github.com/tmpvar/jsdom) library which requires Node.js 4 or newer. +Using of Node.js 6 or 7 is more preferable. + - run `php build/build dev/app basic` to clone the basic app and install composer dependencies for the basic app. This command will install foreign composer packages as normal but will link the yii2 repo to the currently checked out repo, so you have one instance of all the code installed. @@ -63,7 +71,13 @@ Some tests require additional databases to be set up and configured. You can cre settings that are configured in `tests/data/config.php`. You may limit the tests to a group of tests you are working on e.g. to run only tests for the validators and redis -`phpunit --group=validators,redis`. You get the list of available groups by running `phpunit --list-groups`. +`phpunit --group=validators,redis`. You get the list of available groups by running `phpunit --list-groups`. + +You can execute JavaScript unit tests by running `npm test` in the repo root directory. + +> Note: If you get timeout errors like `Error: timeout of 2000ms exceeded. Ensure the done() callback is being called +in this test.`, you can increase timeout: `npm test -- --timeout 30000` (don't miss `--`, it's needed for passing +additional arguments). ### Extensions diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 2a8f5e5972..627c9e28ec 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -27,6 +27,7 @@ Yii Framework 2 Change Log - Bug #12974: Fixed incorrect order of migrations history in case `yii\console\controllers\MigrateController::$migrationNamespaces` is in use (evgen-d, klimov-paul) - Bug #13071: Help option for commands was not working in modules (arogachev, haimanman) - Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov) +- Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev) - Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) - Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire) diff --git a/framework/assets/yii.captcha.js b/framework/assets/yii.captcha.js index b5c01c5753..7293183e76 100644 --- a/framework/assets/yii.captcha.js +++ b/framework/assets/yii.captcha.js @@ -16,7 +16,7 @@ } else if (typeof method === 'object' || !method) { return methods.init.apply(this, arguments); } else { - $.error('Method ' + method + ' does not exist on jQuery.yiiCaptcha'); + $.error('Method ' + method + ' does not exist in jQuery.yiiCaptcha'); return false; } }; @@ -39,7 +39,6 @@ methods.refresh.apply($e); return false; }); - }); }, @@ -58,10 +57,9 @@ }, destroy: function () { - return this.each(function () { - $(window).unbind('.yiiCaptcha'); - $(this).removeData('yiiCaptcha'); - }); + this.off('.yiiCaptcha'); + this.removeData('yiiCaptcha'); + return this; }, data: function () { @@ -69,4 +67,3 @@ } }; })(window.jQuery); - diff --git a/tests/js/tests/yii.captcha.test.js b/tests/js/tests/yii.captcha.test.js new file mode 100644 index 0000000000..0ac51c2321 --- /dev/null +++ b/tests/js/tests/yii.captcha.test.js @@ -0,0 +1,151 @@ +var assert = require('chai').assert; +var sinon; +var withData = require('leche').withData; +var jsdom = require('mocha-jsdom'); + +var fs = require('fs'); +var vm = require('vm'); + +describe('yii.captcha', function () { + var yiiCaptchaPath = 'framework/assets/yii.captcha.js'; + var jQueryPath = 'vendor/bower/jquery/dist/jquery.js'; + var $; + var $captcha; + var settings = { + refreshUrl: '/site/captcha?refresh=1', + hashKey: 'yiiCaptcha/site/captcha' + }; + + function registerTestableCode() { + var code = fs.readFileSync(yiiCaptchaPath); + var script = new vm.Script(code); + var context = new vm.createContext({window: window}); + + script.runInContext(context); + } + + var imgHtml = '' + + ''; + var html = '' + imgHtml + ''; + + jsdom({ + html: html, + src: fs.readFileSync(jQueryPath, 'utf-8') + }); + + before(function () { + $ = window.$; + registerTestableCode(); + sinon = require('sinon'); + }); + + afterEach(function () { + if ($captcha.length) { + $captcha.yiiCaptcha('destroy'); + } + }); + + describe('init', function () { + var customSettings = { + refreshUrl: '/posts/captcha?refresh=1', + hashKey: 'yiiCaptcha/posts/captcha' + }; + + withData({ + 'no method specified': [function () { + $captcha = $('.captcha').yiiCaptcha(settings); + }, settings], + 'no method specified, custom options': [function () { + $captcha = $('.captcha').yiiCaptcha(customSettings); + }, customSettings], + 'manual method call': [function () { + $captcha = $('.captcha').yiiCaptcha('init', settings); + }, settings] + }, function (initFunction, expectedSettings) { + it('should save settings for all elements', function () { + initFunction(); + assert.deepEqual($('#captcha').data('yiiCaptcha'), {settings: expectedSettings}); + assert.deepEqual($('#captcha-2').data('yiiCaptcha'), {settings: expectedSettings}); + }); + }); + }); + + describe('refresh', function () { + var server; + var response = {hash1: 747, hash2: 748, url: '/site/captcha?v=584696959e038'}; + + beforeEach(function () { + server = sinon.fakeServer.create(); + window.XMLHttpRequest = global.XMLHttpRequest; + }); + + afterEach(function () { + server.restore(); + }); + + withData({ + 'click on the captcha': [function () { + $captcha.trigger('click'); + }], + 'manual method call': [function () { + $captcha.yiiCaptcha('refresh'); + }] + }, function (refreshFunction) { + it('should send ajax request, update the image and data for client-side validation', function () { + $captcha = $('#captcha').yiiCaptcha(settings); + refreshFunction(); + server.requests[0].respond(200, {"Content-Type": "application/json"}, JSON.stringify(response)); + + assert.lengthOf(server.requests, 1); + assert.include(server.requests[0].url, settings.refreshUrl + '&_='); + assert.include(server.requests[0].requestHeaders.Accept, 'application/json'); + assert.equal($captcha.attr('src'), response.url); + assert.deepEqual($('body').data(settings.hashKey), [response.hash1, response.hash2]); + }); + }); + }); + + describe('destroy method', function () { + var ajaxStub; + + before(function () { + ajaxStub = sinon.stub($, 'ajax'); + }); + + after(function () { + ajaxStub.restore(); + }); + + var message = 'should remove event handlers with saved settings for destroyed element only and return ' + + 'initial jQuery object'; + it(message, function () { + $captcha = $('.captcha').yiiCaptcha(settings); + var $captcha1 = $('#captcha'); + var $captcha2 = $('#captcha-2'); + var destroyResult = $captcha1.yiiCaptcha('destroy'); + $captcha1.trigger('click'); + $captcha2.trigger('click'); + + assert.strictEqual(destroyResult, $captcha1); + assert.isTrue(ajaxStub.calledOnce); + assert.isUndefined($captcha1.data('yiiCaptcha')); + assert.deepEqual($captcha2.data('yiiCaptcha'), {settings: settings}); + }); + }); + + describe('data method', function () { + it('should return saved settings', function () { + $captcha = $('#captcha').yiiCaptcha(settings); + assert.deepEqual($captcha.yiiCaptcha('data'), {settings: settings}); + }); + }); + + describe('call of not existing method', function () { + it('should throw according error', function () { + $captcha = $('#captcha').yiiCaptcha(settings); + assert.throws(function () { + $captcha.yiiCaptcha('foobar'); + }, 'Method foobar does not exist in jQuery.yiiCaptcha'); + }); + }); +}); diff --git a/tests/js/tests/yii.validation.test.js b/tests/js/tests/yii.validation.test.js index 084dc58ff8..4105caf4a5 100644 --- a/tests/js/tests/yii.validation.test.js +++ b/tests/js/tests/yii.validation.test.js @@ -8,7 +8,7 @@ assert.isDeferred = function (object) { return String(object.resolve) === String($.Deferred().resolve); }; -var sinon = require('sinon'); +var sinon; var withData = require('leche').withData; var StringUtils = { @@ -25,8 +25,6 @@ var vm = require('vm'); var yii; describe('yii.validation', function () { - this.timeout(15000); - var VALIDATOR_SUCCESS_MESSAGE = 'should leave messages as is'; var VALIDATOR_ERROR_MESSAGE = 'should add appropriate errors(s) to messages'; @@ -82,6 +80,7 @@ describe('yii.validation', function () { before(function () { $ = window.$; registerTestableCode(); + sinon = require('sinon'); }); it('should exist', function () { From b8bf0d0c0cdf9d8efa460f6b16cebab9c80f7134 Mon Sep 17 00:00:00 2001 From: Zlakomanov Alexander Date: Thu, 8 Dec 2016 14:46:49 +0300 Subject: [PATCH 067/105] added hint into foreign keys query for oracle 11g optimisation update oci tests improved varible name Closes #13122 --- framework/CHANGELOG.md | 1 + framework/db/oci/Schema.php | 121 +++++++++++++--------- tests/data/config.php | 6 ++ tests/framework/db/QueryBuilderTest.php | 5 - tests/framework/db/oci/ConnectionTest.php | 58 +++++++++++ 5 files changed, 139 insertions(+), 52 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 627c9e28ec..ab56c124f9 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -68,6 +68,7 @@ Yii Framework 2 Change Log - Enh #11758: Implemented Dependency Injection Container configuration using Application configuration array (silverfire) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) - Enh #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions (zalatov) +- Enh #13122: Optimized query for information about foreign keys in `yii\db\oci` (zlakomanoff) 2.0.10 October 20, 2016 ----------------------- diff --git a/framework/db/oci/Schema.php b/framework/db/oci/Schema.php index 1f47207053..4231ba5518 100644 --- a/framework/db/oci/Schema.php +++ b/framework/db/oci/Schema.php @@ -121,17 +121,23 @@ class Schema extends \yii\db\Schema protected function findColumns($table) { $sql = <<db->createCommand($seq_name_sql, [':tableName' => $tableName])->queryScalar(); + $sequenceName = $this->db->createCommand($sequenceNameSql, [':tableName' => $tableName])->queryScalar(); return $sequenceName === false ? null : $sequenceName; } @@ -251,15 +259,23 @@ SQL; protected function findConstraints($table) { $sql = <<db->createCommand($sql, [ @@ -306,13 +322,12 @@ SQL; protected function findSchemaNames() { $sql = <<db->createCommand($sql)->queryColumn(); } @@ -324,20 +339,29 @@ SQL; { if ($schema === '') { $sql = <<db->createCommand($sql); } else { $sql = <<db->createCommand($sql, [':schema' => $schema]); } @@ -371,13 +395,16 @@ SQL; public function findUniqueIndexes($table) { $query = <<db->createCommand($query, [ diff --git a/tests/data/config.php b/tests/data/config.php index 8ba7b26ae0..107258621f 100644 --- a/tests/data/config.php +++ b/tests/data/config.php @@ -43,6 +43,12 @@ $config = [ 'password' => 'postgres', 'fixture' => __DIR__ . '/postgres.sql', ], + 'oci' => [ + 'dsn' => 'oci:dbname=LOCAL_XE;charset=AL32UTF8;', + 'username' => '', + 'password' => '', + 'fixture' => __DIR__ . '/oci.sql', + ], ], ]; diff --git a/tests/framework/db/QueryBuilderTest.php b/tests/framework/db/QueryBuilderTest.php index 719f3b15d8..a498ed35af 100644 --- a/tests/framework/db/QueryBuilderTest.php +++ b/tests/framework/db/QueryBuilderTest.php @@ -167,7 +167,6 @@ abstract class QueryBuilderTest extends DatabaseTestCase [ 'mysql' => 'char(1) CHECK (value LIKE "test%")', 'sqlite' => 'char(1) CHECK (value LIKE "test%")', - 'oci' => 'CHAR(1) CHECK (value LIKE "test%")', 'cubrid' => 'char(1) CHECK (value LIKE "test%")', ], ], @@ -188,7 +187,6 @@ abstract class QueryBuilderTest extends DatabaseTestCase [ 'mysql' => 'char(6) CHECK (value LIKE "test%")', 'sqlite' => 'char(6) CHECK (value LIKE "test%")', - 'oci' => 'CHAR(6) CHECK (value LIKE "test%")', 'cubrid' => 'char(6) CHECK (value LIKE "test%")', ], ], @@ -883,7 +881,6 @@ abstract class QueryBuilderTest extends DatabaseTestCase 'mysql' => 'timestamp NULL DEFAULT NULL', 'postgres' => 'timestamp(0) NULL DEFAULT NULL', 'sqlite' => 'timestamp NULL DEFAULT NULL', - 'oci' => 'TIMESTAMP NULL DEFAULT NULL', 'sqlsrv' => 'timestamp NULL DEFAULT NULL', 'cubrid' => 'timestamp NULL DEFAULT NULL', ], @@ -912,7 +909,6 @@ abstract class QueryBuilderTest extends DatabaseTestCase [ 'mysql' => "int(11) COMMENT 'test comment'", 'postgres' => 'integer', - 'oci' => "NUMBER(10)", 'sqlsrv' => 'int', 'cubrid' => "int COMMENT 'test comment'", ], @@ -923,7 +919,6 @@ abstract class QueryBuilderTest extends DatabaseTestCase [ 'mysql' => "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'test comment'", 'postgres' => 'serial NOT NULL PRIMARY KEY', - 'oci' => 'NUMBER(10) NOT NULL PRIMARY KEY', 'sqlsrv' => 'int IDENTITY PRIMARY KEY', 'cubrid' => "int NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'test comment'", ], diff --git a/tests/framework/db/oci/ConnectionTest.php b/tests/framework/db/oci/ConnectionTest.php index d65b000fb3..b78c6c1789 100644 --- a/tests/framework/db/oci/ConnectionTest.php +++ b/tests/framework/db/oci/ConnectionTest.php @@ -9,4 +9,62 @@ namespace yiiunit\framework\db\oci; class ConnectionTest extends \yiiunit\framework\db\ConnectionTest { protected $driverName = 'oci'; + + public function testSerialize() + { + $connection = $this->getConnection(false, false); + $connection->open(); + $serialized = serialize($connection); + $unserialized = unserialize($serialized); + $this->assertInstanceOf('yii\db\Connection', $unserialized); + + $this->assertEquals(123, $unserialized->createCommand("SELECT 123 FROM DUAL")->queryScalar()); + } + + public function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"table"', $connection->quoteTableName('table')); + $this->assertEquals('"table"', $connection->quoteTableName('"table"')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema.table')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema."table"')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('"schema"."table"')); + $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); + $this->assertEquals('(table)', $connection->quoteTableName('(table)')); + } + + public function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"column"', $connection->quoteColumnName('column')); + $this->assertEquals('"column"', $connection->quoteColumnName('"column"')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + + $this->assertEquals('"column"', $connection->quoteSql('[[column]]')); + $this->assertEquals('"column"', $connection->quoteSql('{{column}}')); + } + + public function testQuoteFullColumnName() + { + $connection = $this->getConnection(false, false); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('table.column')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('table."column"')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('"table".column')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('"table"."column"')); + + $this->assertEquals('[[table.column]]', $connection->quoteColumnName('[[table.column]]')); + $this->assertEquals('{{table}}."column"', $connection->quoteColumnName('{{table}}.column')); + $this->assertEquals('{{table}}."column"', $connection->quoteColumnName('{{table}}."column"')); + $this->assertEquals('{{table}}.[[column]]', $connection->quoteColumnName('{{table}}.[[column]]')); + $this->assertEquals('{{%table}}."column"', $connection->quoteColumnName('{{%table}}.column')); + $this->assertEquals('{{%table}}."column"', $connection->quoteColumnName('{{%table}}."column"')); + + $this->assertEquals('"table"."column"', $connection->quoteSql('[[table.column]]')); + $this->assertEquals('"table"."column"', $connection->quoteSql('{{table}}.[[column]]')); + $this->assertEquals('"table"."column"', $connection->quoteSql('{{table}}."column"')); + $this->assertEquals('"table"."column"', $connection->quoteSql('{{%table}}.[[column]]')); + $this->assertEquals('"table"."column"', $connection->quoteSql('{{%table}}."column"')); + } } From 8e0af242032b2cb7daa6b4373765ce36dc3a4e04 Mon Sep 17 00:00:00 2001 From: Robert Korulczyk Date: Sun, 11 Dec 2016 19:39:21 +0100 Subject: [PATCH 068/105] Fix protocol-relative URLs in `Url::to()` (#13156) (#13157) * Fix protocol-relative URLs in `Url::to()` * Use `isRelative()` in `Url::to()` --- framework/helpers/BaseUrl.php | 2 +- tests/framework/helpers/UrlTest.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/framework/helpers/BaseUrl.php b/framework/helpers/BaseUrl.php index 69b240456a..5623aec512 100644 --- a/framework/helpers/BaseUrl.php +++ b/framework/helpers/BaseUrl.php @@ -223,7 +223,7 @@ class BaseUrl return $url; } - if (($pos = strpos($url, ':')) === false || !ctype_alpha(substr($url, 0, $pos))) { + if (static::isRelative($url)) { // turn relative URL into absolute $url = static::getUrlManager()->getHostInfo() . '/' . ltrim($url, '/'); } diff --git a/tests/framework/helpers/UrlTest.php b/tests/framework/helpers/UrlTest.php index 391d894c73..1be2ffe43e 100644 --- a/tests/framework/helpers/UrlTest.php +++ b/tests/framework/helpers/UrlTest.php @@ -186,6 +186,13 @@ class UrlTest extends TestCase $this->assertEquals('https://example.com/#test', Url::to('@web5', 'https')); $this->assertEquals('//example.com/#test', Url::to('@web5', '')); + // @see https://github.com/yiisoft/yii2/issues/13156 + \Yii::setAlias('@cdn', '//cdn.example.com'); + $this->assertEquals('http://cdn.example.com/images/logo.gif', Url::to('@cdn/images/logo.gif', 'http')); + $this->assertEquals('//cdn.example.com/images/logo.gif', Url::to('@cdn/images/logo.gif', '')); + $this->assertEquals('https://cdn.example.com/images/logo.gif', Url::to('@cdn/images/logo.gif', 'https')); + \Yii::setAlias('@cdn', null); + //In case there is no controller, throw an exception $this->removeMockedAction(); From 7d494c1915f4c1d4be1d2487d504b53dc2c538b6 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Sun, 11 Dec 2016 21:04:12 +0200 Subject: [PATCH 069/105] Fixed unexpected form submit on `validate()` call Fixed `validate()` method in `yii.activeForm.js` to prevent unexpected form submit when `forceValidate` set to `true` Closes #13105 --- framework/CHANGELOG.md | 1 + framework/assets/yii.activeForm.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index ab56c124f9..ed7b3f0f39 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -30,6 +30,7 @@ Yii Framework 2 Change Log - Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev) - Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) +- Bug #13105: Fixed `validate()` method in `yii.activeForm.js` to prevent unexpected form submit when `forceValidate` set to `true` (silverfire) - Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire) - Enh #6242: Access to validator in inline validation (arogachev) - Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index 789cb27954..bb39f85a4c 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -306,9 +306,9 @@ needAjaxValidation = false, messages = {}, deferreds = deferredArray(), - submitting = data.submitting; + submitting = data.submitting && !forceValidate; - if (submitting) { + if (data.submitting) { var event = $.Event(events.beforeValidate); $form.trigger(event, [messages, deferreds]); From 75b98c6b7ebe712097207f1e3a4af38fc20909db Mon Sep 17 00:00:00 2001 From: Dmitry Naumenko Date: Sun, 11 Dec 2016 21:52:49 +0200 Subject: [PATCH 070/105] Updated PHPDocs for Cache --- framework/caching/Cache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/caching/Cache.php b/framework/caching/Cache.php index 2acdf1c140..4cc32d0ceb 100644 --- a/framework/caching/Cache.php +++ b/framework/caching/Cache.php @@ -209,7 +209,7 @@ abstract class Cache extends Component implements \ArrayAccess * a complex data structure consisting of factors representing the key. * @param mixed $value the value to be cached * @param int $duration default duration in seconds before the cache will expire. If not set, - * default [[ttl]] value is used. + * default [[defaultDuration]] value is used. * @param Dependency $dependency dependency of the cached item. If the dependency changes, * the corresponding value in the cache will be invalidated when it is fetched via [[get()]]. * This parameter is ignored if [[serializer]] is false. From ae83dd0524cab9841b9632c0c904aaac506a4a57 Mon Sep 17 00:00:00 2001 From: Chris Harris Date: Sun, 11 Dec 2016 12:36:35 -0800 Subject: [PATCH 071/105] Fixes #13128: Fixed incorrect position of {pos} string in ColumnSchemaBuilder `__toString` --- framework/CHANGELOG.md | 1 + framework/db/ColumnSchemaBuilder.php | 3 +- framework/db/cubrid/ColumnSchemaBuilder.php | 6 +-- framework/db/mysql/ColumnSchemaBuilder.php | 6 +-- framework/db/oci/ColumnSchemaBuilder.php | 24 ++------- tests/framework/db/QueryBuilderTest.php | 59 ++++++++++++++++++++- 6 files changed, 70 insertions(+), 29 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index ed7b3f0f39..ec9606bd8c 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -30,6 +30,7 @@ Yii Framework 2 Change Log - Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev) - Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) +- Bug #13128: Fixed incorrect position of {pos} string in ColumnSchemaBuilder `__toString` (df2) - Bug #13105: Fixed `validate()` method in `yii.activeForm.js` to prevent unexpected form submit when `forceValidate` set to `true` (silverfire) - Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire) - Enh #6242: Access to validator in inline validation (arogachev) diff --git a/framework/db/ColumnSchemaBuilder.php b/framework/db/ColumnSchemaBuilder.php index 024a9e829f..34aae2e23f 100644 --- a/framework/db/ColumnSchemaBuilder.php +++ b/framework/db/ColumnSchemaBuilder.php @@ -258,7 +258,8 @@ class ColumnSchemaBuilder extends Object } /** - * Specify additional SQL to be appended to schema string. + * Specify additional SQL to be appended to column definition. + * Position modifiers will be appended after column definition in databases that support them. * @param string $sql the SQL string to be appended. * @return $this * @since 2.0.9 diff --git a/framework/db/cubrid/ColumnSchemaBuilder.php b/framework/db/cubrid/ColumnSchemaBuilder.php index 5ca57458ef..39502ca5b0 100644 --- a/framework/db/cubrid/ColumnSchemaBuilder.php +++ b/framework/db/cubrid/ColumnSchemaBuilder.php @@ -58,13 +58,13 @@ class ColumnSchemaBuilder extends AbstractColumnSchemaBuilder { switch ($this->getTypeCategory()) { case self::CATEGORY_PK: - $format = '{type}{check}{pos}{comment}{append}'; + $format = '{type}{check}{comment}{append}{pos}'; break; case self::CATEGORY_NUMERIC: - $format = '{type}{length}{unsigned}{notnull}{unique}{default}{check}{comment}{pos}{append}'; + $format = '{type}{length}{unsigned}{notnull}{unique}{default}{check}{comment}{append}{pos}'; break; default: - $format = '{type}{length}{notnull}{unique}{default}{check}{comment}{pos}{append}'; + $format = '{type}{length}{notnull}{unique}{default}{check}{comment}{append}{pos}'; } return $this->buildCompleteString($format); } diff --git a/framework/db/mysql/ColumnSchemaBuilder.php b/framework/db/mysql/ColumnSchemaBuilder.php index bdd6a0a831..eca93e0791 100644 --- a/framework/db/mysql/ColumnSchemaBuilder.php +++ b/framework/db/mysql/ColumnSchemaBuilder.php @@ -58,13 +58,13 @@ class ColumnSchemaBuilder extends AbstractColumnSchemaBuilder { switch ($this->getTypeCategory()) { case self::CATEGORY_PK: - $format = '{type}{length}{check}{comment}{pos}{append}'; + $format = '{type}{length}{check}{comment}{append}{pos}'; break; case self::CATEGORY_NUMERIC: - $format = '{type}{length}{unsigned}{notnull}{unique}{default}{check}{comment}{pos}{append}'; + $format = '{type}{length}{unsigned}{notnull}{unique}{default}{check}{comment}{append}{pos}'; break; default: - $format = '{type}{length}{notnull}{unique}{default}{check}{comment}{pos}{append}'; + $format = '{type}{length}{notnull}{unique}{default}{check}{comment}{append}{pos}'; } return $this->buildCompleteString($format); } diff --git a/framework/db/oci/ColumnSchemaBuilder.php b/framework/db/oci/ColumnSchemaBuilder.php index 6893d4c0b7..8629de8138 100644 --- a/framework/db/oci/ColumnSchemaBuilder.php +++ b/framework/db/oci/ColumnSchemaBuilder.php @@ -26,24 +26,6 @@ class ColumnSchemaBuilder extends AbstractColumnSchemaBuilder return $this->isUnsigned ? ' UNSIGNED' : ''; } - /** - * @inheritdoc - */ - protected function buildAfterString() - { - return $this->after !== null ? - ' AFTER ' . $this->db->quoteColumnName($this->after) : - ''; - } - - /** - * @inheritdoc - */ - protected function buildFirstString() - { - return $this->isFirst ? ' FIRST' : ''; - } - /** * @inheritdoc */ @@ -51,13 +33,13 @@ class ColumnSchemaBuilder extends AbstractColumnSchemaBuilder { switch ($this->getTypeCategory()) { case self::CATEGORY_PK: - $format = '{type}{length}{check}{pos}{append}'; + $format = '{type}{length}{check}{append}'; break; case self::CATEGORY_NUMERIC: - $format = '{type}{length}{unsigned}{default}{notnull}{check}{pos}{append}'; + $format = '{type}{length}{unsigned}{default}{notnull}{check}{append}'; break; default: - $format = '{type}{length}{default}{notnull}{check}{pos}{append}'; + $format = '{type}{length}{default}{notnull}{check}{append}'; } return $this->buildCompleteString($format); } diff --git a/tests/framework/db/QueryBuilderTest.php b/tests/framework/db/QueryBuilderTest.php index a498ed35af..5d9d0643e0 100644 --- a/tests/framework/db/QueryBuilderTest.php +++ b/tests/framework/db/QueryBuilderTest.php @@ -923,6 +923,61 @@ abstract class QueryBuilderTest extends DatabaseTestCase 'cubrid' => "int NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'test comment'", ], ], + [ + Schema::TYPE_PK . " FIRST", + $this->primaryKey()->first(), + [ + 'mysql' => "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST", + 'postgres' => 'serial NOT NULL PRIMARY KEY', + 'oci' => 'NUMBER(10) NOT NULL PRIMARY KEY', + 'sqlsrv' => 'int IDENTITY PRIMARY KEY', + 'cubrid' => "int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST", + ], + ], + [ + Schema::TYPE_INTEGER . " FIRST", + $this->integer()->first(), + [ + 'mysql' => "int(11) FIRST", + 'postgres' => 'integer', + 'oci' => "NUMBER(10)", + 'sqlsrv' => 'int', + 'cubrid' => "int FIRST", + ], + ], + [ + Schema::TYPE_STRING . ' FIRST', + $this->string()->first(), + [ + 'mysql' => 'varchar(255) FIRST', + 'postgres' => 'varchar(255)', + 'oci' => 'VARCHAR2(255)', + 'sqlsrv' => 'varchar(255)', + 'cubrid' => 'varchar(255) FIRST', + ], + ], + [ + Schema::TYPE_INTEGER . " NOT NULL FIRST", + $this->integer()->append('NOT NULL')->first(), + [ + 'mysql' => "int(11) NOT NULL FIRST", + 'postgres' => 'integer NOT NULL', + 'oci' => "NUMBER(10) NOT NULL", + 'sqlsrv' => 'int NOT NULL', + 'cubrid' => "int NOT NULL FIRST", + ], + ], + [ + Schema::TYPE_STRING . ' NOT NULL FIRST', + $this->string()->append('NOT NULL')->first(), + [ + 'mysql' => 'varchar(255) NOT NULL FIRST', + 'postgres' => 'varchar(255) NOT NULL', + 'oci' => 'VARCHAR2(255) NOT NULL', + 'sqlsrv' => 'varchar(255) NOT NULL', + 'cubrid' => 'varchar(255) NOT NULL FIRST', + ], + ], ]; foreach ($items as $i => $item) { @@ -965,7 +1020,9 @@ abstract class QueryBuilderTest extends DatabaseTestCase if (!(strncmp($column, Schema::TYPE_PK, 2) === 0 || strncmp($column, Schema::TYPE_UPK, 3) === 0 || strncmp($column, Schema::TYPE_BIGPK, 5) === 0 || - strncmp($column, Schema::TYPE_UBIGPK, 6) === 0)) { + strncmp($column, Schema::TYPE_UBIGPK, 6) === 0 || + strncmp(substr($column, -5), 'FIRST', 5) === 0 + )) { $columns['col' . ++$i] = str_replace('CHECK (value', 'CHECK ([[col' . $i . ']]', $column); } } From 50b668eee81d0c9c187c1258745e946ed248548c Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Tue, 13 Dec 2016 16:54:51 +0200 Subject: [PATCH 072/105] redundant code removed --- tests/framework/db/ActiveRecordTest.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/framework/db/ActiveRecordTest.php b/tests/framework/db/ActiveRecordTest.php index 8137243752..6b01e834c8 100644 --- a/tests/framework/db/ActiveRecordTest.php +++ b/tests/framework/db/ActiveRecordTest.php @@ -1296,66 +1296,56 @@ abstract class ActiveRecordTest extends DatabaseTestCase public function testEmulateExecution() { - $this->assertGreaterThan(0, Customer::find()->from('customer')->count()); + $this->assertGreaterThan(0, Customer::find()->count()); $rows = Customer::find() - ->from('customer') ->emulateExecution() ->all(); $this->assertSame([], $rows); $row = Customer::find() - ->from('customer') ->emulateExecution() ->one(); $this->assertSame(null, $row); $exists = Customer::find() - ->from('customer') ->emulateExecution() ->exists(); $this->assertSame(false, $exists); $count = Customer::find() - ->from('customer') ->emulateExecution() ->count(); $this->assertSame(0, $count); $sum = Customer::find() - ->from('customer') ->emulateExecution() ->sum('id'); $this->assertSame(0, $sum); $sum = Customer::find() - ->from('customer') ->emulateExecution() ->average('id'); $this->assertSame(0, $sum); $max = Customer::find() - ->from('customer') ->emulateExecution() ->max('id'); $this->assertSame(null, $max); $min = Customer::find() - ->from('customer') ->emulateExecution() ->min('id'); $this->assertSame(null, $min); $scalar = Customer::find() ->select(['id']) - ->from('customer') ->emulateExecution() ->scalar(); $this->assertSame(null, $scalar); $column = Customer::find() ->select(['id']) - ->from('customer') ->emulateExecution() ->column(); $this->assertSame([], $column); From a132ee9a08079d726883068983d02e8aa0af7214 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Tue, 13 Dec 2016 18:13:59 +0200 Subject: [PATCH 073/105] Fixed order or checks in IpValidator Fixes #13198 --- framework/CHANGELOG.md | 1 + framework/assets/yii.validation.js | 12 +++---- framework/validators/IpValidator.php | 13 ++++--- .../framework/validators/IpValidatorTest.php | 35 +++++++++++++++++-- tests/js/tests/yii.validation.test.js | 6 ++-- 5 files changed, 50 insertions(+), 17 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index ec9606bd8c..cb443db12e 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -32,6 +32,7 @@ Yii Framework 2 Change Log - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) - Bug #13128: Fixed incorrect position of {pos} string in ColumnSchemaBuilder `__toString` (df2) - Bug #13105: Fixed `validate()` method in `yii.activeForm.js` to prevent unexpected form submit when `forceValidate` set to `true` (silverfire) +- Bug #13198: Fixed order of checks in `yii\validators\IpValidator` that sometimes caused wrong error message (silverfire) - Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire) - Enh #6242: Access to validator in inline validation (arogachev) - Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) diff --git a/framework/assets/yii.validation.js b/framework/assets/yii.validation.js index 38e749fc16..984690d8ee 100644 --- a/framework/assets/yii.validation.js +++ b/framework/assets/yii.validation.js @@ -344,19 +344,19 @@ yii.validation = (function ($) { var ipVersion = value.indexOf(':') === -1 ? 4 : 6; if (ipVersion == 6) { - if (!options.ipv6) { - pub.addMessage(messages, options.messages.ipv6NotAllowed, value); - } if (!(new RegExp(options.ipv6Pattern)).test(value)) { pub.addMessage(messages, options.messages.message, value); } - } else { - if (!options.ipv4) { - pub.addMessage(messages, options.messages.ipv4NotAllowed, value); + if (!options.ipv6) { + pub.addMessage(messages, options.messages.ipv6NotAllowed, value); } + } else { if (!(new RegExp(options.ipv4Pattern)).test(value)) { pub.addMessage(messages, options.messages.message, value); } + if (!options.ipv4) { + pub.addMessage(messages, options.messages.ipv4NotAllowed, value); + } } } }; diff --git a/framework/validators/IpValidator.php b/framework/validators/IpValidator.php index 9b72adfad0..55f762006c 100644 --- a/framework/validators/IpValidator.php +++ b/framework/validators/IpValidator.php @@ -369,12 +369,12 @@ class IpValidator extends Validator $cidr = static::IPV6_ADDRESS_LENGTH; } - if (!$this->ipv6) { - return [$this->ipv6NotAllowed, []]; - } if (!$this->validateIPv6($ip)) { return [$this->message, []]; } + if (!$this->ipv6) { + return [$this->ipv6NotAllowed, []]; + } if ($this->expandIPv6) { $ip = $this->expandIPv6($ip); @@ -388,13 +388,12 @@ class IpValidator extends Validator $isCidrDefault = true; $cidr = static::IPV4_ADDRESS_LENGTH; } - - if (!$this->ipv4) { - return [$this->ipv4NotAllowed, []]; - } if (!$this->validateIPv4($ip)) { return [$this->message, []]; } + if (!$this->ipv4) { + return [$this->ipv4NotAllowed, []]; + } } if (!$this->isAllowed($ip, $cidr)) { diff --git a/tests/framework/validators/IpValidatorTest.php b/tests/framework/validators/IpValidatorTest.php index 9471a336e1..04cd90e7ed 100644 --- a/tests/framework/validators/IpValidatorTest.php +++ b/tests/framework/validators/IpValidatorTest.php @@ -61,7 +61,7 @@ class IpValidatorTest extends TestCase } public function provideBadIps() { - return [['not.an.ip'], [['what an array', '??']], [123456], [true], [false]]; + return [['not.an.ip'], [['what an array', '??']], [123456], [true], [false], ['bad:forSure']]; } /** @@ -74,6 +74,37 @@ class IpValidatorTest extends TestCase $this->assertFalse($validator->validate($badIp)); } + /** + * @dataProvider provideBadIps + */ + public function testValidateModelAttributeNotAnIP($badIp) + { + $validator = new IpValidator(); + $model = new FakedValidationModel(); + + $model->attr_ip = $badIp; + $validator->validateAttribute($model, 'attr_ip'); + $this->assertEquals('attr_ip must be a valid IP address.', $model->getFirstError('attr_ip')); + $model->clearErrors(); + + + $validator->ipv4 = false; + + $model->attr_ip = $badIp; + $validator->validateAttribute($model, 'attr_ip'); + $this->assertEquals('attr_ip must be a valid IP address.', $model->getFirstError('attr_ip')); + $model->clearErrors(); + + + $validator->ipv4 = true; + $validator->ipv6 = false; + + $model->attr_ip = $badIp; + $validator->validateAttribute($model, 'attr_ip'); + $this->assertEquals('attr_ip must be a valid IP address.', $model->getFirstError('attr_ip')); + $model->clearErrors(); + } + public function testValidateValueIPv4() { $validator = new IpValidator(); @@ -359,4 +390,4 @@ class IpValidatorTest extends TestCase $this->assertEquals('fa01::2/614', $model->attr_ip); $this->assertEquals('attr_ip contains wrong subnet mask.', $model->getFirstError('attr_ip')); } -} \ No newline at end of file +} diff --git a/tests/js/tests/yii.validation.test.js b/tests/js/tests/yii.validation.test.js index 4105caf4a5..dba4e3d1d8 100644 --- a/tests/js/tests/yii.validation.test.js +++ b/tests/js/tests/yii.validation.test.js @@ -1547,6 +1547,8 @@ describe('yii.validation', function () { withData({ 'empty string, skip on empty': ['', {skipOnEmpty: true}, []], 'not IP': ['not IP', {}, ['Invalid value.']], + 'not IP, IPv4 is disabled': ['not:IP', {ipv4: false}, ['Invalid value.']], + 'not IP, IPv6 is disabled': ['not IP', {ipv6: false}, ['Invalid value.']], // subnet, IPv4 'IPv4, subnet option is not defined': ['192.168.10.0', {}, []], 'IPv4, subnet option is set to "false"': ['192.168.10.0', {subnet: false}, []], @@ -1635,12 +1637,12 @@ describe('yii.validation', function () { 'invalid IPv4, IPv4 option is set to "false"': [ '192,168.10.0', {ipv4: false}, - ['IPv4 is not allowed.', 'Invalid value.'] + ['Invalid value.', 'IPv4 is not allowed.'] ], 'invalid IPv6, IPv6 option is set to "false"': [ '2001,0db8:11a3:09d7:1f34:8a2e:07a0:765d', {ipv6: false}, - ['IPv6 is not allowed.', 'Invalid value.'] + ['Invalid value.', 'IPv6 is not allowed.'] ] }, function (value, customOptions, expectedMessages) { it(getValidatorMessage(expectedMessages), function () { From a1581329cf161d871251289c126aa7f3e5944bfd Mon Sep 17 00:00:00 2001 From: Alexey Rogachev Date: Wed, 14 Dec 2016 16:19:43 +0600 Subject: [PATCH 074/105] Fixes #13183: Docs for working with separated migrations (#13201) --- docs/guide/db-migrations.md | 59 ++++++++++++++++++++++++++++ docs/guide/security-authorization.md | 3 ++ 2 files changed, 62 insertions(+) diff --git a/docs/guide/db-migrations.md b/docs/guide/db-migrations.md index 7453d4f740..c32231c95a 100644 --- a/docs/guide/db-migrations.md +++ b/docs/guide/db-migrations.md @@ -891,6 +891,65 @@ will be used to record the migration history. You no longer need to specify it v command-line option. +### Separated migrations + +Sometimes you may need to use migrations from a different namespace. It can be some extension or module in your own +project. One of such examples is migrations for [RBAC component](security-authorization.md#configuring-rbac). +Since version 2.0.10 you can use [[yii\console\controllers\MigrateController::migrationNamespaces|migrationNamespaces]] +to solve this task: + +```php +return [ + 'controllerMap' => [ + 'migrate' => [ + 'class' => 'yii\console\controllers\MigrateController', + 'migrationNamespaces' => [ + 'app\migrations', // Common migrations for the whole application + 'module\migrations', // Migrations for the specific project's module +                'yii\rbac\migrations', // Migrations for the specific extension + ], + ], + ], +]; +``` + +If you want them to be applied and tracked down completely separated from each other, you can configure multiple +migration commands which will use different namespaces and migration history tables: + +```php +return [ + 'controllerMap' => [ + // Common migrations for the whole application + 'migrate-app' => [ + 'class' => 'yii\console\controllers\MigrateController', + 'migrationNamespaces' => ['app\migrations'], + 'migrationTable' => 'migration_app', + ], + // Migrations for the specific project's module + 'migrate-module' => [ + 'class' => 'yii\console\controllers\MigrateController', + 'migrationNamespaces' => ['module\migrations'], + 'migrationTable' => 'migration_module', + ], + // Migrations for the specific extension + 'migrate-rbac' => [ + 'class' => 'yii\console\controllers\MigrateController', +            'migrationNamespaces' => ['yii\rbac\migrations'], + 'migrationTable' => 'migration_rbac', + ], + ], +]; +``` + +Note that to synchronize database you now need to run multiple commands instead of one: + +``` +yii migrate-app +yii migrate-module +yii migrate-rbac +``` + + ## Migrating Multiple Databases By default, migrations are applied to the same database specified by the `db` [application component](structure-application-components.md). diff --git a/docs/guide/security-authorization.md b/docs/guide/security-authorization.md index 2daf66effd..1c40765a3e 100644 --- a/docs/guide/security-authorization.md +++ b/docs/guide/security-authorization.md @@ -242,6 +242,9 @@ Before you can go on you need to create those tables in the database. To do this `yii migrate --migrationPath=@yii/rbac/migrations` +Read more about working with migrations from different namespaces in +[Separated Migrations](db-migrations.md#separated-migrations) section. + The `authManager` can now be accessed via `\Yii::$app->authManager`. From 7be8ccbb4168e34155d16dcd3042be751c6716fb Mon Sep 17 00:00:00 2001 From: Alexey Rogachev Date: Wed, 14 Dec 2016 17:44:15 +0600 Subject: [PATCH 075/105] All words in h3 in docs must start with capital letter (#13207) --- docs/guide/db-migrations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/db-migrations.md b/docs/guide/db-migrations.md index c32231c95a..5e070c61d7 100644 --- a/docs/guide/db-migrations.md +++ b/docs/guide/db-migrations.md @@ -891,7 +891,7 @@ will be used to record the migration history. You no longer need to specify it v command-line option. -### Separated migrations +### Separated Migrations Sometimes you may need to use migrations from a different namespace. It can be some extension or module in your own project. One of such examples is migrations for [RBAC component](security-authorization.md#configuring-rbac). From da944aa9c0a5b1ea39ebeee1404bc4b1934b3b74 Mon Sep 17 00:00:00 2001 From: Sergey Makinen Date: Wed, 14 Dec 2016 14:59:00 +0300 Subject: [PATCH 076/105] Rebased "Revamps PageCache to store data in one entry (#10506)" (#12145) --- framework/CHANGELOG.md | 1 + framework/filters/PageCache.php | 195 +++++++++++++++++++++----------- 2 files changed, 129 insertions(+), 67 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index cb443db12e..2207de3643 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -47,6 +47,7 @@ Yii Framework 2 Change Log - Enh #11756: Added type mapping for `varbinary` data type in MySQL DBMS (silverfire) - Enh #11929: Changed `type` column type from `int` to `smallInt` in RBAC migrations (silverfire) - Enh #12015: Changed visibility `yii\db\ActiveQueryTrait::createModels()` from private to protected (ArekX, dynasource) +- Enh #12145: Added `beforeCacheResponse` and `afterRestoreResponse` to `yii\filters\PageCache` to be more easily extendable (sergeymakinen) - Enh #12399: Added `ActiveField::addAriaAttributes` property for `aria-required` and `aria-invalid` attributes rendering (Oxyaction, samdark) - Enh #12390: Avoid creating queries with false where condition (`0=1`) when fetching relational data (klimov-paul) - Enh #12619: Added catch `Throwable` in `yii\base\ErrorHandler::handleException()` (rob006) diff --git a/framework/filters/PageCache.php b/framework/filters/PageCache.php index 9b9495e285..8b02278118 100644 --- a/framework/filters/PageCache.php +++ b/framework/filters/PageCache.php @@ -8,8 +8,8 @@ namespace yii\filters; use Yii; -use yii\base\ActionFilter; use yii\base\Action; +use yii\base\ActionFilter; use yii\caching\Cache; use yii\caching\Dependency; use yii\di\Instance; @@ -46,13 +46,14 @@ use yii\web\Response; * ``` * * @author Qiang Xue + * @author Sergey Makinen * @since 2.0 */ class PageCache extends ActionFilter { /** * @var bool whether the content being cached should be differentiated according to the route. - * A route consists of the requested controller ID and action ID. Defaults to true. + * A route consists of the requested controller ID and action ID. Defaults to `true`. */ public $varyByRoute = true; /** @@ -64,7 +65,7 @@ class PageCache extends ActionFilter public $cache = 'cache'; /** * @var int number of seconds that the data can remain valid in cache. - * Use 0 to indicate that the cached data will never expire. + * Use `0` to indicate that the cached data will never expire. */ public $duration = 60; /** @@ -123,6 +124,13 @@ class PageCache extends ActionFilter * @since 2.0.4 */ public $cacheHeaders = true; + /** + * @var array a list of placeholders for embedding dynamic contents. This property + * is used internally to implement the content caching feature. Do not modify it. + * @internal + * @since 2.0.11 + */ + public $dynamicPlaceholders; /** @@ -154,57 +162,70 @@ class PageCache extends ActionFilter $this->dependency = Yii::createObject($this->dependency); } - $properties = []; - foreach (['cache', 'duration', 'dependency', 'variations'] as $name) { - $properties[$name] = $this->$name; - } - $id = $this->varyByRoute ? $action->getUniqueId() : __CLASS__; $response = Yii::$app->getResponse(); - ob_start(); - ob_implicit_flush(false); - if ($this->view->beginCache($id, $properties)) { + $data = $this->cache->get($this->calculateCacheKey()); + if (!is_array($data) || !isset($data['cacheVersion']) || $data['cacheVersion'] !== 1) { + $this->view->cacheStack[] = $this; + ob_start(); + ob_implicit_flush(false); $response->on(Response::EVENT_AFTER_SEND, [$this, 'cacheResponse']); Yii::trace('Valid page content is not found in the cache.', __METHOD__); return true; } else { - $data = $this->cache->get($this->calculateCacheKey()); - if (is_array($data)) { - $this->restoreResponse($response, $data); - } - $response->content = ob_get_clean(); + $this->restoreResponse($response, $data); Yii::trace('Valid page content is found in the cache.', __METHOD__); return false; } } /** - * Restores response properties from the given data - * @param Response $response the response to be restored - * @param array $data the response property data + * This method is invoked right before the response caching is to be started. + * You may override this method to cancel caching by returning `false` or store an additional data + * in a cache entry by returning an array instead of `true`. + * @return bool|array whether to cache or not, return an array instead of `true` to store an additional data. + * @since 2.0.11 + */ + public function beforeCacheResponse() + { + return true; + } + + /** + * This method is invoked right after the response restoring is finished (but before the response is sent). + * You may override this method to do last-minute preparation before the response is sent. + * @param array|null $data an array of an additional data stored in a cache entry or `null`. + * @since 2.0.11 + */ + public function afterRestoreResponse($data) + { + } + + /** + * Restores response properties from the given data. + * @param Response $response the response to be restored. + * @param array $data the response property data. * @since 2.0.3 */ protected function restoreResponse($response, $data) { - if (isset($data['format'])) { - $response->format = $data['format']; + foreach (['format', 'version', 'statusCode', 'statusText', 'content'] as $name) { + $response->{$name} = $data[$name]; } - if (isset($data['version'])) { - $response->version = $data['version']; + foreach (['headers', 'cookies'] as $name) { + if (isset($data[$name]) && is_array($data[$name])) { + $response->{$name}->fromArray(array_merge($data[$name], $response->{$name}->toArray())); + } } - if (isset($data['statusCode'])) { - $response->statusCode = $data['statusCode']; - } - if (isset($data['statusText'])) { - $response->statusText = $data['statusText']; - } - if (isset($data['headers']) && is_array($data['headers'])) { - $headers = $response->getHeaders()->toArray(); - $response->getHeaders()->fromArray(array_merge($data['headers'], $headers)); - } - if (isset($data['cookies']) && is_array($data['cookies'])) { - $cookies = $response->getCookies()->toArray(); - $response->getCookies()->fromArray(array_merge($data['cookies'], $cookies)); + if (!empty($data['dynamicPlaceholders']) && is_array($data['dynamicPlaceholders'])) { + if (empty($this->view->cacheStack)) { + // outermost cache: replace placeholder with dynamic content + $response->content = $this->updateDynamicContent($response->content, $data['dynamicPlaceholders']); + } + foreach ($data['dynamicPlaceholders'] as $name => $statements) { + $this->view->addDynamicPlaceholder($name, $statements); + } } + $this->afterRestoreResponse(isset($data['cacheData']) ? $data['cacheData'] : null); } /** @@ -213,43 +234,83 @@ class PageCache extends ActionFilter */ public function cacheResponse() { - $this->view->endCache(); + array_pop($this->view->cacheStack); + $beforeCacheResponseResult = $this->beforeCacheResponse(); + if ($beforeCacheResponseResult === false) { + $content = ob_get_clean(); + if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) { + $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); + } + echo $content; + return; + } + $response = Yii::$app->getResponse(); $data = [ - 'format' => $response->format, - 'version' => $response->version, - 'statusCode' => $response->statusCode, - 'statusText' => $response->statusText, + 'cacheVersion' => 1, + 'cacheData' => is_array($beforeCacheResponseResult) ? $beforeCacheResponseResult : null, + 'content' => ob_get_clean() ]; - if (!empty($this->cacheHeaders)) { - $headers = $response->getHeaders()->toArray(); - if (is_array($this->cacheHeaders)) { - $filtered = []; - foreach ($this->cacheHeaders as $name) { - $name = strtolower($name); - if (isset($headers[$name])) { - $filtered[$name] = $headers[$name]; - } - } - $headers = $filtered; - } - $data['headers'] = $headers; + if ($data['content'] === false || $data['content'] === '') { + return; } - if (!empty($this->cacheCookies)) { - $cookies = $response->getCookies()->toArray(); - if (is_array($this->cacheCookies)) { - $filtered = []; - foreach ($this->cacheCookies as $name) { - if (isset($cookies[$name])) { - $filtered[$name] = $cookies[$name]; - } - } - $cookies = $filtered; - } - $data['cookies'] = $cookies; + + $data['dynamicPlaceholders'] = $this->dynamicPlaceholders; + foreach (['format', 'version', 'statusCode', 'statusText'] as $name) { + $data[$name] = $response->{$name}; } + $this->insertResponseCollectionIntoData($response, 'headers', $data); + $this->insertResponseCollectionIntoData($response, 'cookies', $data); $this->cache->set($this->calculateCacheKey(), $data, $this->duration, $this->dependency); - echo ob_get_clean(); + if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) { + $data['content'] = $this->updateDynamicContent($data['content'], $this->dynamicPlaceholders); + } + echo $data['content']; + } + + /** + * Inserts (or filters/ignores according to config) response headers/cookies into a cache data array. + * @param Response $response the response. + * @param string $collectionName currently it's `headers` or `cookies`. + * @param array $data the cache data. + */ + private function insertResponseCollectionIntoData(Response $response, $collectionName, array &$data) + { + $property = 'cache' . ucfirst($collectionName); + if ($this->{$property} === false) { + return; + } + + $all = $response->{$collectionName}->toArray(); + if (is_array($this->{$property})) { + $filtered = []; + foreach ($this->{$property} as $name) { + if ($collectionName === 'headers') { + $name = strtolower($name); + } + if (isset($all[$name])) { + $filtered[$name] = $all[$name]; + } + } + $all = $filtered; + } + $data[$collectionName] = $all; + } + + /** + * Replaces placeholders in content by results of evaluated dynamic statements. + * @param string $content content to be parsed. + * @param array $placeholders placeholders and their values. + * @return string final content. + * @since 2.0.11 + */ + protected function updateDynamicContent($content, $placeholders) + { + foreach ($placeholders as $name => $statements) { + $placeholders[$name] = $this->view->evaluateDynamicContent($statements); + } + + return strtr($content, $placeholders); } /** From 88f2348ed512136c85c3019e990a9c2f315d3582 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Wed, 14 Dec 2016 22:50:53 +0200 Subject: [PATCH 077/105] Fixed `ActiveRecord::unlinkAll()` to respect `onCondition()` Closes #12213 --- framework/CHANGELOG.md | 1 + framework/db/BaseActiveRecord.php | 6 +++ tests/data/ar/Order.php | 7 +++ tests/framework/db/ActiveRecordTest.php | 67 +++++++++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 2207de3643..b28380cc59 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -33,6 +33,7 @@ Yii Framework 2 Change Log - Bug #13128: Fixed incorrect position of {pos} string in ColumnSchemaBuilder `__toString` (df2) - Bug #13105: Fixed `validate()` method in `yii.activeForm.js` to prevent unexpected form submit when `forceValidate` set to `true` (silverfire) - Bug #13198: Fixed order of checks in `yii\validators\IpValidator` that sometimes caused wrong error message (silverfire) +- Bug #12213: Fixed `yii\db\ActiveRecord::unlinkAll()` to respect `onCondition()` of the relational query (silverfire) - Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire) - Enh #6242: Access to validator in inline validation (arogachev) - Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php index 7c18f14475..b914cad55c 100644 --- a/framework/db/BaseActiveRecord.php +++ b/framework/db/BaseActiveRecord.php @@ -1443,6 +1443,9 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface if (!empty($viaRelation->where)) { $condition = ['and', $condition, $viaRelation->where]; } + if (!empty($viaRelation->on)) { + $condition = ['and', $condition, $viaRelation->on]; + } if (is_array($relation->via)) { /* @var $viaClass ActiveRecordInterface */ if ($delete) { @@ -1477,6 +1480,9 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface if (!empty($relation->where)) { $condition = ['and', $condition, $relation->where]; } + if (!empty($relation->on)) { + $condition = ['and', $condition, $relation->on]; + } if ($delete) { $relatedModel::deleteAll($condition); } else { diff --git a/tests/data/ar/Order.php b/tests/data/ar/Order.php index 12cc6e59fa..61e8b03d74 100644 --- a/tests/data/ar/Order.php +++ b/tests/data/ar/Order.php @@ -178,6 +178,13 @@ class Order extends ActiveRecord ->viaTable('order_item', ['order_id' => 'id']); } + public function getLimitedItems() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->onCondition(['item.id' => [3, 5]]) + ->via('orderItems'); + } + public function beforeSave($insert) { if (parent::beforeSave($insert)) { diff --git a/tests/framework/db/ActiveRecordTest.php b/tests/framework/db/ActiveRecordTest.php index 6b01e834c8..304bfb6737 100644 --- a/tests/framework/db/ActiveRecordTest.php +++ b/tests/framework/db/ActiveRecordTest.php @@ -3,6 +3,7 @@ namespace yiiunit\framework\db; use yii\db\ActiveQuery; +use yii\db\ActiveRecordInterface; use yiiunit\data\ar\ActiveRecord; use yiiunit\data\ar\BitValues; use yiiunit\data\ar\Category; @@ -52,6 +53,11 @@ abstract class ActiveRecordTest extends DatabaseTestCase return OrderItem::className(); } + public function getCategoryClass() + { + return Category::className(); + } + public function getOrderWithNullFKClass() { return OrderWithNullFK::className(); @@ -1350,4 +1356,65 @@ abstract class ActiveRecordTest extends DatabaseTestCase ->column(); $this->assertSame([], $column); } + + /** + * https://github.com/yiisoft/yii2/issues/12213 + */ + public function testUnlinkAllOnCondition() + { + /** @var Category $categoryClass */ + $categoryClass = $this->getCategoryClass(); + /** @var Item $itemClass */ + $itemClass = $this->getItemClass(); + + // Ensure there are three items with category_id = 2 in the Items table + $itemsCount = $itemClass::find()->where(['category_id' => 2])->count(); + $this->assertEquals(3, $itemsCount); + + $categoryQuery = $categoryClass::find()->with('limitedItems')->where(['id' => 2]); + // Ensure that limitedItems relation returns only one item + // (category_id = 2 and id in (1,2,3)) + $category = $categoryQuery->one(); + $this->assertCount(1, $category->limitedItems); + + // Unlink all items in the limitedItems relation + $category->unlinkAll('limitedItems', true); + + // Make sure that only one item was unlinked + $itemsCount = $itemClass::find()->where(['category_id' => 2])->count(); + $this->assertEquals(2, $itemsCount); + + // Call $categoryQuery again to ensure no items were found + $this->assertCount(0, $categoryQuery->one()->limitedItems); + } + + /** + * https://github.com/yiisoft/yii2/issues/12213 + */ + public function testUnlinkAllOnConditionViaTable() + { + /** @var Order $orderClass */ + $orderClass = $this->getOrderClass(); + /** @var Item $itemClass */ + $itemClass = $this->getItemClass(); + + // Ensure there are three items with category_id = 2 in the Items table + $itemsCount = $itemClass::find()->where(['category_id' => 2])->count(); + $this->assertEquals(3, $itemsCount); + + $orderQuery = $orderClass::find()->with('limitedItems')->where(['id' => 2]); + // Ensure that limitedItems relation returns only one item + // (category_id = 2 and id in (4, 5)) + $category = $orderQuery->one(); + $this->assertCount(2, $category->limitedItems); + + // Unlink all items in the limitedItems relation + $category->unlinkAll('limitedItems', true); + + // Call $orderQuery again to ensure that links are removed + $this->assertCount(0, $orderQuery->one()->limitedItems); + + // Make sure that only links were removed, the items were not removed + $this->assertEquals(3, $itemClass::find()->where(['category_id' => 2])->count()); + } } From e78a7fe20eb87cd81e6d52f2b34c886a5bcbca58 Mon Sep 17 00:00:00 2001 From: Konstantin Tsukur Date: Sun, 5 Jun 2016 19:58:39 +0300 Subject: [PATCH 078/105] Initial work on #11697 --- framework/db/Query.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/framework/db/Query.php b/framework/db/Query.php index 1bb831da0d..6683f6f3f2 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -851,6 +851,24 @@ class Query extends Component implements QueryInterface return $this; } + public function andFilterHaving(array $condition) + { + $condition = $this->filterCondition($condition); + if ($condition !== []) { + $this->andHaving($condition); + } + return $this; + } + + public function orFilterHaving(array $condition) + { + $condition = $this->filterCondition($condition); + if ($condition !== []) { + $this->orHaving($condition); + } + return $this; + } + /** * Adds an additional HAVING condition to the existing one. * The new condition and the existing one will be joined using the 'OR' operator. From 837b33767c20b592fc01f9775e1fb72d57c19995 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 14 Dec 2016 23:58:04 +0300 Subject: [PATCH 079/105] Fixes #11697: Added `filterHaving()`, `andFilterHaving()` and `orFilterHaving()` to `yii\db\Query` Signed-off-by: Alexander Makarov --- docs/guide/db-query-builder.md | 6 ++ framework/CHANGELOG.md | 1 + framework/db/Query.php | 103 +++++++++++++++++++++++++------ tests/framework/db/QueryTest.php | 54 ++++++++++++++++ 4 files changed, 146 insertions(+), 18 deletions(-) diff --git a/docs/guide/db-query-builder.md b/docs/guide/db-query-builder.md index d39d536b60..de863a2a0e 100644 --- a/docs/guide/db-query-builder.md +++ b/docs/guide/db-query-builder.md @@ -369,6 +369,12 @@ You can also specify operator explicitly: $query->andFilterCompare('name', 'Doe', 'like'); ``` +Since Yii 2.0.11 there are similar methods for `HAVING` condition: + +- [[yii\db\Query::filterHaving()|filterHaving()]] +- [[yii\db\Query::andFilterHaving()|andFilterHaving()]] +- [[yii\db\Query::orFilterHaving()|orFilterHaving()]] + ### [[yii\db\Query::orderBy()|orderBy()]] The [[yii\db\Query::orderBy()|orderBy()]] method specifies the `ORDER BY` fragment of a SQL query. For example, diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 2207de3643..3984a807dd 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -73,6 +73,7 @@ Yii Framework 2 Change Log - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) - Enh #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions (zalatov) - Enh #13122: Optimized query for information about foreign keys in `yii\db\oci` (zlakomanoff) +- Enh #11697: Added `filterHaving()`, `andFilterHaving()` and `orFilterHaving()` to `yii\db\Query` (nicdnepr, samdark) 2.0.10 October 20, 2016 ----------------------- diff --git a/framework/db/Query.php b/framework/db/Query.php index 6683f6f3f2..fb0862d284 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -851,24 +851,6 @@ class Query extends Component implements QueryInterface return $this; } - public function andFilterHaving(array $condition) - { - $condition = $this->filterCondition($condition); - if ($condition !== []) { - $this->andHaving($condition); - } - return $this; - } - - public function orFilterHaving(array $condition) - { - $condition = $this->filterCondition($condition); - if ($condition !== []) { - $this->orHaving($condition); - } - return $this; - } - /** * Adds an additional HAVING condition to the existing one. * The new condition and the existing one will be joined using the 'OR' operator. @@ -890,6 +872,91 @@ class Query extends Component implements QueryInterface return $this; } + /** + * Sets the HAVING part of the query but ignores [[isEmpty()|empty operands]]. + * + * This method is similar to [[having()]]. The main difference is that this method will + * remove [[isEmpty()|empty query operands]]. As a result, this method is best suited + * for building query conditions based on filter values entered by users. + * + * The following code shows the difference between this method and [[having()]]: + * + * ```php + * // HAVING `age`=:age + * $query->filterHaving(['name' => null, 'age' => 20]); + * // HAVING `age`=:age + * $query->having(['age' => 20]); + * // HAVING `name` IS NULL AND `age`=:age + * $query->having(['name' => null, 'age' => 20]); + * ``` + * + * Note that unlike [[having()]], you cannot pass binding parameters to this method. + * + * @param array $condition the conditions that should be put in the HAVING part. + * See [[having()]] on how to specify this parameter. + * @return $this the query object itself + * @see having() + * @see andFilterHaving() + * @see orFilterHaving() + * @since 2.0.11 + */ + public function filterHaving(array $condition) + { + $condition = $this->filterCondition($condition); + if ($condition !== []) { + $this->having($condition); + } + return $this; + } + + /** + * Adds an additional HAVING condition to the existing one but ignores [[isEmpty()|empty operands]]. + * The new condition and the existing one will be joined using the 'AND' operator. + * + * This method is similar to [[andHaving()]]. The main difference is that this method will + * remove [[isEmpty()|empty query operands]]. As a result, this method is best suited + * for building query conditions based on filter values entered by users. + * + * @param array $condition the new HAVING condition. Please refer to [[having()]] + * on how to specify this parameter. + * @return $this the query object itself + * @see filterHaving() + * @see orFilterHaving() + * @since 2.0.11 + */ + public function andFilterHaving(array $condition) + { + $condition = $this->filterCondition($condition); + if ($condition !== []) { + $this->andHaving($condition); + } + return $this; + } + + /** + * Adds an additional HAVING condition to the existing one but ignores [[isEmpty()|empty operands]]. + * The new condition and the existing one will be joined using the 'OR' operator. + * + * This method is similar to [[orHaving()]]. The main difference is that this method will + * remove [[isEmpty()|empty query operands]]. As a result, this method is best suited + * for building query conditions based on filter values entered by users. + * + * @param array $condition the new HAVING condition. Please refer to [[having()]] + * on how to specify this parameter. + * @return $this the query object itself + * @see filterHaving() + * @see andFilterHaving() + * @since 2.0.11 + */ + public function orFilterHaving(array $condition) + { + $condition = $this->filterCondition($condition); + if ($condition !== []) { + $this->orHaving($condition); + } + return $this; + } + /** * Appends a SQL statement using UNION operator. * @param string|Query $sql the SQL statement to be appended using UNION diff --git a/tests/framework/db/QueryTest.php b/tests/framework/db/QueryTest.php index 005f9a0c85..a7db8628f2 100644 --- a/tests/framework/db/QueryTest.php +++ b/tests/framework/db/QueryTest.php @@ -110,6 +110,60 @@ abstract class QueryTest extends DatabaseTestCase $this->assertEquals($condition, $query->where); } + public function testFilterHaving() + { + // should work with hash format + $query = new Query; + $query->filterHaving([ + 'id' => 0, + 'title' => ' ', + 'author_ids' => [], + ]); + $this->assertEquals(['id' => 0], $query->having); + + $query->andFilterHaving(['status' => null]); + $this->assertEquals(['id' => 0], $query->having); + + $query->orFilterHaving(['name' => '']); + $this->assertEquals(['id' => 0], $query->having); + + // should work with operator format + $query = new Query; + $condition = ['like', 'name', 'Alex']; + $query->filterHaving($condition); + $this->assertEquals($condition, $query->having); + + $query->andFilterHaving(['between', 'id', null, null]); + $this->assertEquals($condition, $query->having); + + $query->orFilterHaving(['not between', 'id', null, null]); + $this->assertEquals($condition, $query->having); + + $query->andFilterHaving(['in', 'id', []]); + $this->assertEquals($condition, $query->having); + + $query->andFilterHaving(['not in', 'id', []]); + $this->assertEquals($condition, $query->having); + + $query->andFilterHaving(['not in', 'id', []]); + $this->assertEquals($condition, $query->having); + + $query->andFilterHaving(['like', 'id', '']); + $this->assertEquals($condition, $query->having); + + $query->andFilterHaving(['or like', 'id', '']); + $this->assertEquals($condition, $query->having); + + $query->andFilterHaving(['not like', 'id', ' ']); + $this->assertEquals($condition, $query->having); + + $query->andFilterHaving(['or not like', 'id', null]); + $this->assertEquals($condition, $query->having); + + $query->andFilterHaving(['or', ['eq', 'id', null], ['eq', 'id', []]]); + $this->assertEquals($condition, $query->having); + } + public function testFilterRecursively() { $query = new Query(); From 518be6b8ec61df2c39b3de3e0f26d74626f4f304 Mon Sep 17 00:00:00 2001 From: Dmitry Naumenko Date: Wed, 14 Dec 2016 23:07:44 +0200 Subject: [PATCH 080/105] Enhanced PHPDocs markup --- framework/db/Query.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/framework/db/Query.php b/framework/db/Query.php index fb0862d284..9402a7921f 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -573,7 +573,7 @@ class Query extends Component implements QueryInterface /** * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'AND' operator. + * The new condition and the existing one will be joined using the `AND` operator. * @param string|array|Expression $condition the new WHERE condition. Please refer to [[where()]] * on how to specify this parameter. * @param array $params the parameters (name => value) to be bound to the query. @@ -594,7 +594,7 @@ class Query extends Component implements QueryInterface /** * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'OR' operator. + * The new condition and the existing one will be joined using the `OR` operator. * @param string|array|Expression $condition the new WHERE condition. Please refer to [[where()]] * on how to specify this parameter. * @param array $params the parameters (name => value) to be bound to the query. @@ -619,7 +619,7 @@ class Query extends Component implements QueryInterface * It adds an additional WHERE condition for the given field and determines the comparison operator * based on the first few characters of the given value. * The condition is added in the same way as in [[andFilterWhere]] so [[isEmpty()|empty values]] are ignored. - * The new condition and the existing one will be joined using the 'AND' operator. + * The new condition and the existing one will be joined using the `AND` operator. * * The comparison operator is intelligently determined based on the first few characters in the given value. * In particular, it recognizes the following operators if they appear as the leading characters in the given value: @@ -832,7 +832,7 @@ class Query extends Component implements QueryInterface /** * Adds an additional HAVING condition to the existing one. - * The new condition and the existing one will be joined using the 'AND' operator. + * The new condition and the existing one will be joined using the `AND` operator. * @param string|array|Expression $condition the new HAVING condition. Please refer to [[where()]] * on how to specify this parameter. * @param array $params the parameters (name => value) to be bound to the query. @@ -853,7 +853,7 @@ class Query extends Component implements QueryInterface /** * Adds an additional HAVING condition to the existing one. - * The new condition and the existing one will be joined using the 'OR' operator. + * The new condition and the existing one will be joined using the `OR` operator. * @param string|array|Expression $condition the new HAVING condition. Please refer to [[where()]] * on how to specify this parameter. * @param array $params the parameters (name => value) to be bound to the query. @@ -911,7 +911,7 @@ class Query extends Component implements QueryInterface /** * Adds an additional HAVING condition to the existing one but ignores [[isEmpty()|empty operands]]. - * The new condition and the existing one will be joined using the 'AND' operator. + * The new condition and the existing one will be joined using the `AND` operator. * * This method is similar to [[andHaving()]]. The main difference is that this method will * remove [[isEmpty()|empty query operands]]. As a result, this method is best suited @@ -935,7 +935,7 @@ class Query extends Component implements QueryInterface /** * Adds an additional HAVING condition to the existing one but ignores [[isEmpty()|empty operands]]. - * The new condition and the existing one will be joined using the 'OR' operator. + * The new condition and the existing one will be joined using the `OR` operator. * * This method is similar to [[orHaving()]]. The main difference is that this method will * remove [[isEmpty()|empty query operands]]. As a result, this method is best suited From 01997441ccd09e48f7e14859e12342598302083e Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Thu, 15 Dec 2016 01:18:02 +0100 Subject: [PATCH 081/105] added tests for rest\UrlRule::createUrl() verify issue #13200 --- tests/framework/rest/UrlRuleTest.php | 117 +++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/tests/framework/rest/UrlRuleTest.php b/tests/framework/rest/UrlRuleTest.php index f3f9e6c957..51565af5fe 100644 --- a/tests/framework/rest/UrlRuleTest.php +++ b/tests/framework/rest/UrlRuleTest.php @@ -2,6 +2,7 @@ namespace yiiunit\framework\rest; +use Yii; use yii\web\UrlManager; use yii\rest\UrlRule; use yii\web\Request; @@ -194,4 +195,120 @@ class UrlRuleTest extends TestCase ]; } + + /** + * Proviedes test cases for createUrl() method + * + * - first param are properties of the UrlRule + * - second param is the route to create + * - third param is the expected URL + */ + public function createUrlDataProvider() + { + return [ + // with pluralize + [ + [ // Rule properties + 'controller' => 'v1/channel', + 'pluralize' => true, + ], + ['v1/channel/index'], // route + 'v1/channels', // expected + ], + [ + [ // Rule properties + 'controller' => ['v1/channel'], + 'pluralize' => true, + ], + ['v1/channel/index'], // route + 'v1/channels', // expected + ], + [ + [ // Rule properties + 'controller' => ['v1/channel', 'v1/u' => 'v1/user'], + 'pluralize' => true, + ], + ['v1/channel/index'], // route + 'v1/channels', // expected + ], + [ + [ // Rule properties + 'controller' => ['v1/channel', 'v1/u' => 'v1/user'], + 'pluralize' => true, + ], + ['v1/user/index'], // route + 'v1/u', // expected + ], + [ + [ // Rule properties + 'controller' => 'v1/channel', + 'pluralize' => true, + ], + ['v1/channel/index', 'offset' => 1], // route + 'v1/channels?offset=1', // expected + ], + + + // without pluralize + [ + [ // Rule properties + 'controller' => 'v1/channel', + 'pluralize' => false, + ], + ['v1/channel/index'], // route + 'v1/channel', // expected + ], + [ + [ // Rule properties + 'controller' => ['v1/channel'], + 'pluralize' => false, + ], + ['v1/channel/index'], // route + 'v1/channel', // expected + ], + [ + [ // Rule properties + 'controller' => ['v1/channel', 'v1/u' => 'v1/user'], + 'pluralize' => false, + ], + ['v1/channel/index'], // route + 'v1/channel', // expected + ], + [ + [ // Rule properties + 'controller' => ['v1/channel', 'v1/u' => 'v1/user'], + 'pluralize' => false, + ], + ['v1/user/index'], // route + 'v1/u', // expected + ], + [ + [ // Rule properties + 'controller' => 'v1/channel', + 'pluralize' => false, + ], + ['v1/channel/index', 'offset' => 1], // route + 'v1/channel?offset=1', // expected + ], + + // --- + ]; + } + + /** + * @dataProvider createUrlDataProvider + */ + public function testCreateUrl($rule, $params, $expected) + { + $this->mockWebApplication(); + Yii::$app->set('request', new Request(['hostInfo' => 'http://api.example.com', 'scriptUrl' => '/index.php'])); + $route = array_shift($params); + + $manager = new UrlManager([ + 'cache' => null, + ]); + $rule = new UrlRule($rule); + $this->assertEquals($expected, $rule->createUrl($manager, $route, $params)); + } + } From bfba0aa711869f2eed3ecd5d1b672c55a72a4554 Mon Sep 17 00:00:00 2001 From: Elvira Sheina Date: Thu, 15 Dec 2016 16:23:16 +0500 Subject: [PATCH 082/105] Refactor validateAttribute method in UniqueValidator (#13202) * Refactor validateAttribute method in UniqueValidator Extract prepareParams and prepareQuery from validateAttribute, so they can be tested separately. * Added issue number to changelog * Eliminated unneeded variable * Renamed methods and parameters, update PHPDocs --- framework/CHANGELOG.md | 1 + framework/validators/UniqueValidator.php | 110 +++++++++++++----- .../validators/UniqueValidatorTest.php | 59 +++++++++- 3 files changed, 142 insertions(+), 28 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index b28380cc59..e321921b17 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -74,6 +74,7 @@ Yii Framework 2 Change Log - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) - Enh #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions (zalatov) - Enh #13122: Optimized query for information about foreign keys in `yii\db\oci` (zlakomanoff) +- Enh #13202: Refactor validateAttribute method in UniqueValidator (developeruz) 2.0.10 October 20, 2016 ----------------------- diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index 5fd5be1d3c..03c29c07d9 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -8,7 +8,11 @@ namespace yii\validators; use Yii; +use yii\base\Model; +use yii\db\ActiveQuery; +use yii\db\ActiveQueryInterface; use yii\db\ActiveRecordInterface; +use yii\db\Query; use yii\helpers\Inflector; /** @@ -44,7 +48,7 @@ class UniqueValidator extends Validator */ public $targetClass; /** - * @var string|array the name of the ActiveRecord attribute that should be used to + * @var string|array the name of the [[\yii\db\ActiveRecord|ActiveRecord]] attribute that should be used to * validate the uniqueness of the current attribute value. If not set, it will use the name * of the attribute currently being validated. You may use an array to validate the uniqueness * of multiple columns at the same time. The array values are the attributes that will be @@ -111,32 +115,38 @@ class UniqueValidator extends Validator /* @var $targetClass ActiveRecordInterface */ $targetClass = $this->targetClass === null ? get_class($model) : $this->targetClass; $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute; + $conditions = $this->prepareConditions($targetAttribute, $model, $attribute); - if (is_array($targetAttribute)) { - $params = []; - foreach ($targetAttribute as $k => $v) { - $params[$v] = is_int($k) ? $model->$v : $model->$k; - } - } else { - $params = [$targetAttribute => $model->$attribute]; - } - - foreach ($params as $value) { + foreach ($conditions as $value) { if (is_array($value)) { $this->addError($model, $attribute, Yii::t('yii', '{attribute} is invalid.')); - return; } } - $query = $targetClass::find(); - $query->andWhere($params); - - if ($this->filter instanceof \Closure) { - call_user_func($this->filter, $query); - } elseif ($this->filter !== null) { - $query->andWhere($this->filter); + if ($this->modelExists($targetClass, $conditions, $model)) { + if (count($targetAttribute) > 1) { + $this->addComboNotUniqueError($model, $attribute); + } else { + $this->addError($model, $attribute, $this->message); + } } + } + + /** + * Checks whether the $model exists in the database. + * + * @param string $targetClass the name of the ActiveRecord class that should be used to validate the uniqueness + * of the current attribute value. + * @param array $conditions conditions, compatible with [[\yii\db\Query::where()|Query::where()]] key-value format. + * @param Model $model the data model to be validated + * + * @return bool whether the model already exists + */ + private function modelExists($targetClass, $conditions, $model) + { + /** @var ActiveRecordInterface $targetClass $query */ + $query = $this->prepareQuery($targetClass, $conditions); if (!$model instanceof ActiveRecordInterface || $model->getIsNewRecord() || $model->className() !== $targetClass::className()) { // if current $model isn't in the database yet then it's OK just to call exists() @@ -144,11 +154,11 @@ class UniqueValidator extends Validator $exists = $query->exists(); } else { // if current $model is in the database already we can't use exists() - /* @var $models ActiveRecordInterface[] */ + /** @var $models ActiveRecordInterface[] */ $models = $query->select($targetClass::primaryKey())->limit(2)->all(); $n = count($models); if ($n === 1) { - $keys = array_keys($params); + $keys = array_keys($conditions); $pks = $targetClass::primaryKey(); sort($keys); sort($pks); @@ -164,13 +174,59 @@ class UniqueValidator extends Validator } } - if ($exists) { - if (count($targetAttribute) > 1) { - $this->addComboNotUniqueError($model, $attribute); - } else { - $this->addError($model, $attribute, $this->message); - } + return $exists; + } + + /** + * Prepares a query by applying filtering conditions defined in $conditions method property + * and [[filter]] class property. + * + * @param ActiveRecordInterface $targetClass the name of the ActiveRecord class that should be used to validate + * the uniqueness of the current attribute value. + * @param array $conditions conditions, compatible with [[\yii\db\Query::where()|Query::where()]] key-value format + * + * @return ActiveQueryInterface|ActiveQuery + */ + private function prepareQuery($targetClass, $conditions) + { + $query = $targetClass::find(); + $query->andWhere($conditions); + + if ($this->filter instanceof \Closure) { + call_user_func($this->filter, $query); + } elseif ($this->filter !== null) { + $query->andWhere($this->filter); } + + return $query; + } + + /** + * Processes attributes' relations described in $targetAttribute parameter into conditions, compatible with + * [[\yii\db\Query::where()|Query::where()]] key-value format. + * + * @param string|array $targetAttribute the name of the [[\yii\db\ActiveRecord|ActiveRecord]] attribute that + * should be used to validate the uniqueness of the current attribute value. You may use an array to validate + * the uniqueness of multiple columns at the same time. The array values are the attributes that will be + * used to validate the uniqueness, while the array keys are the attributes whose values are to be validated. + * If the key and the value are the same, you can just specify the value. + * @param Model $model the data model to be validated + * @param string $attribute the name of the attribute to be validated in the $model + + * @return array conditions, compatible with [[\yii\db\Query::where()|Query::where()]] key-value format. + */ + private function prepareConditions($targetAttribute, $model, $attribute) + { + if (is_array($targetAttribute)) { + $conditions = []; + foreach ($targetAttribute as $k => $v) { + $conditions[$v] = is_int($k) ? $model->$v : $model->$k; + } + } else { + $conditions = [$targetAttribute => $model->$attribute]; + } + + return $conditions; } /** diff --git a/tests/framework/validators/UniqueValidatorTest.php b/tests/framework/validators/UniqueValidatorTest.php index 9305118864..02a3940a72 100644 --- a/tests/framework/validators/UniqueValidatorTest.php +++ b/tests/framework/validators/UniqueValidatorTest.php @@ -303,4 +303,61 @@ abstract class UniqueValidatorTest extends DatabaseTestCase $val->validateAttribute($m, 'ref'); $this->assertTrue($m->hasErrors('ref')); } -} \ No newline at end of file + + public function testPrepareParams() + { + $model = new FakedValidationModel(); + $model->val_attr_a = 'test value a'; + $model->val_attr_b = 'test value b'; + $model->val_attr_c = 'test value c'; + $attribute = 'val_attr_a'; + + $targetAttribute = 'val_attr_b'; + $result = $this->invokeMethod(new UniqueValidator(), 'prepareConditions', [$targetAttribute, $model, $attribute]); + $expected = ['val_attr_b' => 'test value a']; + $this->assertEquals($expected, $result); + + $targetAttribute = ['val_attr_b', 'val_attr_c']; + $result = $this->invokeMethod(new UniqueValidator(), 'prepareConditions', [$targetAttribute, $model, $attribute]); + $expected = ['val_attr_b' => 'test value b', 'val_attr_c' => 'test value c']; + $this->assertEquals($expected, $result); + + $targetAttribute = ['val_attr_a' => 'val_attr_b']; + $result = $this->invokeMethod(new UniqueValidator(), 'prepareConditions', [$targetAttribute, $model, $attribute]); + $expected = ['val_attr_b' => 'test value a']; + $this->assertEquals($expected, $result); + + + $targetAttribute = ['val_attr_b', 'val_attr_a' => 'val_attr_c']; + $result = $this->invokeMethod(new UniqueValidator(), 'prepareConditions', [$targetAttribute, $model, $attribute]); + $expected = ['val_attr_b' => 'test value b', 'val_attr_c' => 'test value a']; + $this->assertEquals($expected, $result); + } + + public function testPrepareQuery() + { + $schema = $this->getConnection()->schema; + + $model = new ValidatorTestMainModel(); + $query = $this->invokeMethod(new UniqueValidator(), 'prepareQuery', [$model,['val_attr_b' => 'test value a']]); + $expected = "SELECT * FROM {$schema->quoteTableName('validator_main')} WHERE {$schema->quoteColumnName('val_attr_b')}=:qp0"; + $this->assertEquals($expected, $query->createCommand()->getSql()); + + $params = ['val_attr_b' => 'test value b', 'val_attr_c' => 'test value a']; + $query = $this->invokeMethod(new UniqueValidator(), 'prepareQuery', [$model, $params]); + $expected = "SELECT * FROM {$schema->quoteTableName('validator_main')} WHERE ({$schema->quoteColumnName('val_attr_b')}=:qp0) AND ({$schema->quoteColumnName('val_attr_c')}=:qp1)"; + $this->assertEquals($expected, $query->createCommand()->getSql()); + + $params = ['val_attr_b' => 'test value b']; + $query = $this->invokeMethod(new UniqueValidator(['filter' => 'val_attr_a > 0']), 'prepareQuery', [$model, $params]); + $expected = "SELECT * FROM {$schema->quoteTableName('validator_main')} WHERE ({$schema->quoteColumnName('val_attr_b')}=:qp0) AND (val_attr_a > 0)"; + $this->assertEquals($expected, $query->createCommand()->getSql()); + + $params = ['val_attr_b' => 'test value b']; + $query = $this->invokeMethod(new UniqueValidator(['filter' => function($query) { + $query->orWhere('val_attr_a > 0'); + }]), 'prepareQuery', [$model, $params]); + $expected = "SELECT * FROM {$schema->quoteTableName('validator_main')} WHERE ({$schema->quoteColumnName('val_attr_b')}=:qp0) OR (val_attr_a > 0)"; + $this->assertEquals($expected, $query->createCommand()->getSql()); + } +} From b78d6367ba454e71a6dffd752ae29683a3203206 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 15 Dec 2016 18:02:25 +0300 Subject: [PATCH 083/105] Removed duplicate assertion from tests, divided test method into two --- tests/framework/db/QueryTest.php | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/framework/db/QueryTest.php b/tests/framework/db/QueryTest.php index a7db8628f2..4ade380dd5 100644 --- a/tests/framework/db/QueryTest.php +++ b/tests/framework/db/QueryTest.php @@ -56,9 +56,8 @@ abstract class QueryTest extends DatabaseTestCase $this->assertEquals([':id' => 1, ':name' => 'something', ':age' => '30'], $query->params); } - public function testFilterWhere() + public function testFilterWhereWithHashFormat() { - // should work with hash format $query = new Query; $query->filterWhere([ 'id' => 0, @@ -72,8 +71,10 @@ abstract class QueryTest extends DatabaseTestCase $query->orFilterWhere(['name' => '']); $this->assertEquals(['id' => 0], $query->where); + } - // should work with operator format + public function testFilterWhereWithOperatorFormat() + { $query = new Query; $condition = ['like', 'name', 'Alex']; $query->filterWhere($condition); @@ -91,9 +92,6 @@ abstract class QueryTest extends DatabaseTestCase $query->andFilterWhere(['not in', 'id', []]); $this->assertEquals($condition, $query->where); - $query->andFilterWhere(['not in', 'id', []]); - $this->assertEquals($condition, $query->where); - $query->andFilterWhere(['like', 'id', '']); $this->assertEquals($condition, $query->where); @@ -110,9 +108,8 @@ abstract class QueryTest extends DatabaseTestCase $this->assertEquals($condition, $query->where); } - public function testFilterHaving() + public function testFilterHavingWithHashFormat() { - // should work with hash format $query = new Query; $query->filterHaving([ 'id' => 0, @@ -126,8 +123,10 @@ abstract class QueryTest extends DatabaseTestCase $query->orFilterHaving(['name' => '']); $this->assertEquals(['id' => 0], $query->having); + } - // should work with operator format + public function testFilterHavingWithOperatorFormat() + { $query = new Query; $condition = ['like', 'name', 'Alex']; $query->filterHaving($condition); @@ -145,9 +144,6 @@ abstract class QueryTest extends DatabaseTestCase $query->andFilterHaving(['not in', 'id', []]); $this->assertEquals($condition, $query->having); - $query->andFilterHaving(['not in', 'id', []]); - $this->assertEquals($condition, $query->having); - $query->andFilterHaving(['like', 'id', '']); $this->assertEquals($condition, $query->having); @@ -326,7 +322,8 @@ abstract class QueryTest extends DatabaseTestCase } /** - * @depends testFilterWhere + * @depends testFilterWhereWithHashFormat + * @depends testFilterWhereWithOperatorFormat */ public function testAndFilterCompare() { From 821ba54720ec54d278cbf11dcc46ca430be9cb8f Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Thu, 15 Dec 2016 22:41:02 +0200 Subject: [PATCH 084/105] Reordered CHANGELOG --- framework/CHANGELOG.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index ee86ed33af..1472388e14 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -5,9 +5,11 @@ Yii Framework 2 Change Log ------------------------ - Bug #4113: Error page stacktrace was generating links to private methods which are not part of the API docs (samdark) +- Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) - Bug #9305: Fixed MSSQL `Schema::TYPE_TIMESTAMP` to be 'datetime' instead of 'timestamp', which is just an incremental number (nkovacs) - Bug #9616: Fixed mysql\Schema::loadColumnSchema to set enumValues attribute correctly if enum definition contains commas (fphammerle) - Bug #9796: Initialization of not existing `yii\grid\ActionColumn` default buttons (arogachev) +- Bug #12213: Fixed `yii\db\ActiveRecord::unlinkAll()` to respect `onCondition()` of the relational query (silverfire) - Bug #12681: Changed `data` column type from `text` to `blob` to handle null-byte (`\0`) in serialized RBAC rule properly (silverfire) - Bug #12714: Fixed `yii\validation\EmailValidator` to prevent false-positives checks when property `checkDns` is set to `true` (silverfire) - Bug #12735: Fixed `yii\console\controllers\MigrateController` creating multiple primary keys for field `bigPrimaryKey:unsigned` (SG5) @@ -27,13 +29,11 @@ Yii Framework 2 Change Log - Bug #12974: Fixed incorrect order of migrations history in case `yii\console\controllers\MigrateController::$migrationNamespaces` is in use (evgen-d, klimov-paul) - Bug #13071: Help option for commands was not working in modules (arogachev, haimanman) - Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov) -- Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev) -- Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) +- Bug #13105: Fixed `validate()` method in `yii.activeForm.js` to prevent unexpected form submit when `forceValidate` set to `true` (silverfire) - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) - Bug #13128: Fixed incorrect position of {pos} string in ColumnSchemaBuilder `__toString` (df2) -- Bug #13105: Fixed `validate()` method in `yii.activeForm.js` to prevent unexpected form submit when `forceValidate` set to `true` (silverfire) +- Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev) - Bug #13198: Fixed order of checks in `yii\validators\IpValidator` that sometimes caused wrong error message (silverfire) -- Bug #12213: Fixed `yii\db\ActiveRecord::unlinkAll()` to respect `onCondition()` of the relational query (silverfire) - Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire) - Enh #6242: Access to validator in inline validation (arogachev) - Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) @@ -45,12 +45,14 @@ Yii Framework 2 Change Log - Enh #10896: Select only primary key when counting records in UniqueValidator (developeruz) - Enh #11037: `yii.js` and `yii.validation.js` use `Regexp.test()` instead of `String.match()` (arogachev, nkovacs) - Enh #11163: Added separate method for client-side validation options `yii\validators\Validator::getClientOptions()` (arogachev) +- Enh #11697: Added `filterHaving()`, `andFilterHaving()` and `orFilterHaving()` to `yii\db\Query` (nicdnepr, samdark) - Enh #11756: Added type mapping for `varbinary` data type in MySQL DBMS (silverfire) +- Enh #11758: Implemented Dependency Injection Container configuration using Application configuration array (silverfire) - Enh #11929: Changed `type` column type from `int` to `smallInt` in RBAC migrations (silverfire) - Enh #12015: Changed visibility `yii\db\ActiveQueryTrait::createModels()` from private to protected (ArekX, dynasource) - Enh #12145: Added `beforeCacheResponse` and `afterRestoreResponse` to `yii\filters\PageCache` to be more easily extendable (sergeymakinen) -- Enh #12399: Added `ActiveField::addAriaAttributes` property for `aria-required` and `aria-invalid` attributes rendering (Oxyaction, samdark) - Enh #12390: Avoid creating queries with false where condition (`0=1`) when fetching relational data (klimov-paul) +- Enh #12399: Added `ActiveField::addAriaAttributes` property for `aria-required` and `aria-invalid` attributes rendering (Oxyaction, samdark) - Enh #12619: Added catch `Throwable` in `yii\base\ErrorHandler::handleException()` (rob006) - Enh #12659: Suggest alternatives when console command was not found (mdmunir, cebe) - Enh #12726: `yii\base\Application::$version` converted to `yii\base\Module::$version` virtual property, allowing to specify version as a PHP callback (klimov-paul) @@ -62,20 +64,19 @@ Yii Framework 2 Change Log - Enh #12798: Changed `yii\cache\Dependency::getHasChanged()` (deprecated, to be removed in 2.1) to `yii\cache\Dependency::isChanged()` (dynasource) - Enh #12807: Added console controller checks for `yii\console\controllers\HelpController` (schmunk42) - Enh #12816: Added `columnSchemaClass` option for `yii\db\Schema` which adds ability to specify custom `yii\db\ColumnSchema` class (nanodesu88) +- Enh #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions (zalatov) - Enh #12881: Added `removeValue` method to `yii\helpers\BaseArrayHelper` (nilsburg) - Enh #12901: Added `getDefaultHelpHeader` method to the `yii\console\controllers\HelpController` class to be able to override default help header in a class heir (diezztsk) +- Enh #12988: Changed `textarea` method within the `yii\helpers\BaseHtml` class to allow users to control whether html entities found within `$value` will be double-encoded or not (cyphix333) +- Enh #13020: Added `disabledListItemSubTagOptions` attribute for `yii\widgets\LinkPager` in order to customize the disabled list item sub tag element (nadar) - Enh #13035: Use ArrayHelper::getValue() in SluggableBehavior::getValue() (thyseus) - Enh #13036: Added shortcut methods `asJson()` and `asXml()` for returning JSON and XML data in web controller actions (cebe) -- Enh #13020: Added `disabledListItemSubTagOptions` attribute for `yii\widgets\LinkPager` in order to customize the disabled list item sub tag element (nadar) -- Enh #12988: Changed `textarea` method within the `yii\helpers\BaseHtml` class to allow users to control whether html entities found within `$value` will be double-encoded or not (cyphix333) - Enh #13050: Added `yii\filters\HostControl` allowing protection against 'host header' attacks (klimov-paul, rob006) - Enh #13074: Improved `yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks) -- Enh #11758: Implemented Dependency Injection Container configuration using Application configuration array (silverfire) -- Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) -- Enh #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions (zalatov) - Enh #13122: Optimized query for information about foreign keys in `yii\db\oci` (zlakomanoff) -- Enh #11697: Added `filterHaving()`, `andFilterHaving()` and `orFilterHaving()` to `yii\db\Query` (nicdnepr, samdark) - Enh #13202: Refactor validateAttribute method in UniqueValidator (developeruz) +- Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) + 2.0.10 October 20, 2016 ----------------------- From 1d5a6d17224a7c79f508c8abdadf7aab3f503c88 Mon Sep 17 00:00:00 2001 From: zalatov Date: Fri, 28 Oct 2016 09:44:04 +1000 Subject: [PATCH 085/105] Add is_dir() validation for BaseFileHelper::findFiles(). Sometimes it throws exception when file/directory is deleted or moved while executing script. --- framework/CHANGELOG.md | 4 ++++ framework/helpers/BaseFileHelper.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 1472388e14..84daaf0824 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -77,6 +77,10 @@ Yii Framework 2 Change Log - Enh #13202: Refactor validateAttribute method in UniqueValidator (developeruz) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) +- Bug #12824: Enabled usage of `yii\mutex\FileMutex` on Windows systems (davidsonalencar) +- Enh #11037 yii.js and yii.validation.js should use Regexp.test instead of String.match (arogachev, nkovacs) +- Bug #9796: Initialization of not existing `yii\grid\ActionColumn` default buttons (arogachev) +- Enh: Added `is_dir()` validation to `BaseFileHelper::findFiles` method (zalatov) 2.0.10 October 20, 2016 ----------------------- diff --git a/framework/helpers/BaseFileHelper.php b/framework/helpers/BaseFileHelper.php index d4a373705f..583655ab45 100644 --- a/framework/helpers/BaseFileHelper.php +++ b/framework/helpers/BaseFileHelper.php @@ -411,7 +411,7 @@ class BaseFileHelper if (static::filterPath($path, $options)) { if (is_file($path)) { $list[] = $path; - } elseif (!isset($options['recursive']) || $options['recursive']) { + } elseif (is_dir($path) && (!isset($options['recursive']) || $options['recursive'])) { $list = array_merge($list, static::findFiles($path, $options)); } } From cfe5202e446fae5e2c780baaec3d7e7015a1f141 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Thu, 15 Dec 2016 22:56:30 +0200 Subject: [PATCH 086/105] Fixed CHANGELOG merge problems --- framework/CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 84daaf0824..736460db0a 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -56,6 +56,7 @@ Yii Framework 2 Change Log - Enh #12619: Added catch `Throwable` in `yii\base\ErrorHandler::handleException()` (rob006) - Enh #12659: Suggest alternatives when console command was not found (mdmunir, cebe) - Enh #12726: `yii\base\Application::$version` converted to `yii\base\Module::$version` virtual property, allowing to specify version as a PHP callback (klimov-paul) +- Enh #12732: Added `is_dir()` validation to `yii\helpers\BaseFileHelper::findFiles()` method (zalatov, silverfire) - Enh #12738: Added support for creating protocol-relative URLs in `UrlManager::createAbsoluteUrl()` and `Url` helper methods (rob006) - Enh #12748: Added Migration tool automatic generation reference column for foreignKey (MKiselev) - Enh #12748: Migration generator now tries to fetch reference column name for foreignKey from schema if it's not set explicitly (MKiselev) @@ -77,10 +78,6 @@ Yii Framework 2 Change Log - Enh #13202: Refactor validateAttribute method in UniqueValidator (developeruz) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) -- Bug #12824: Enabled usage of `yii\mutex\FileMutex` on Windows systems (davidsonalencar) -- Enh #11037 yii.js and yii.validation.js should use Regexp.test instead of String.match (arogachev, nkovacs) -- Bug #9796: Initialization of not existing `yii\grid\ActionColumn` default buttons (arogachev) -- Enh: Added `is_dir()` validation to `BaseFileHelper::findFiles` method (zalatov) 2.0.10 October 20, 2016 ----------------------- From 9a45b70adfc5b8284889e671cd1f00400c52917a Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Thu, 15 Dec 2016 23:20:52 +0200 Subject: [PATCH 087/105] Updated FileHelperTest --- tests/framework/helpers/FileHelperTest.php | 57 ++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/tests/framework/helpers/FileHelperTest.php b/tests/framework/helpers/FileHelperTest.php index 333994b5d1..178274f73a 100644 --- a/tests/framework/helpers/FileHelperTest.php +++ b/tests/framework/helpers/FileHelperTest.php @@ -61,10 +61,11 @@ class FileHelperTest extends TestCase if ($handle = opendir($dirName)) { while (false !== ($entry = readdir($handle))) { if ($entry != '.' && $entry != '..') { - if (is_dir($dirName . DIRECTORY_SEPARATOR . $entry) === true) { - $this->removeDir($dirName . DIRECTORY_SEPARATOR . $entry); + $item = $dirName . DIRECTORY_SEPARATOR . $entry; + if (is_dir($item) === true && !is_link($item)) { + $this->removeDir($item); } else { - unlink($dirName . DIRECTORY_SEPARATOR . $entry); + unlink($item); } } } @@ -512,6 +513,56 @@ class FileHelperTest extends TestCase $this->assertEquals([$dirName . DIRECTORY_SEPARATOR . $passedFileName], $foundFiles); } + /** + * @depends testFindFiles + */ + public function testFindFilesRecursiveWithSymLink() + { + $dirName = 'test_dir'; + $this->createFileStructure([ + $dirName => [ + 'theDir' => [ + 'file1' => 'abc', + 'file2' => 'def', + ], + 'symDir' => ['symlink', 'theDir'], + ], + ]); + $dirName = $this->testFilePath . DIRECTORY_SEPARATOR . $dirName; + + $expected = [ + $dirName . DIRECTORY_SEPARATOR . 'symDir' . DIRECTORY_SEPARATOR . 'file1', + $dirName . DIRECTORY_SEPARATOR . 'symDir' . DIRECTORY_SEPARATOR . 'file2', + $dirName . DIRECTORY_SEPARATOR . 'theDir' . DIRECTORY_SEPARATOR . 'file1', + $dirName . DIRECTORY_SEPARATOR . 'theDir' . DIRECTORY_SEPARATOR . 'file2', + ]; + $this->assertEquals($expected, FileHelper::findFiles($dirName)); + } + + /** + * @depends testFindFiles + */ + public function testFindFilesNotRecursive() + { + $dirName = 'test_dir'; + $this->createFileStructure([ + $dirName => [ + 'theDir' => [ + 'file1' => 'abc', + 'file2' => 'def', + ], + 'symDir' => ['symlink', 'theDir'], + 'file3' => 'root' + ], + ]); + $dirName = $this->testFilePath . DIRECTORY_SEPARATOR . $dirName; + + $expected = [ + $dirName . DIRECTORY_SEPARATOR . 'file3', + ]; + $this->assertEquals($expected, FileHelper::findFiles($dirName, ['recursive' => false])); + } + /** * @depends testFindFiles */ From 11c58549d4e155dcd00d36cd4cea4fe346ee85f1 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Thu, 15 Dec 2016 23:36:16 +0200 Subject: [PATCH 088/105] Fixed FileHelperTest::testFindFilesRecursiveWithSymLink() --- tests/framework/helpers/FileHelperTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/framework/helpers/FileHelperTest.php b/tests/framework/helpers/FileHelperTest.php index 178274f73a..90b3b6e87e 100644 --- a/tests/framework/helpers/FileHelperTest.php +++ b/tests/framework/helpers/FileHelperTest.php @@ -536,7 +536,9 @@ class FileHelperTest extends TestCase $dirName . DIRECTORY_SEPARATOR . 'theDir' . DIRECTORY_SEPARATOR . 'file1', $dirName . DIRECTORY_SEPARATOR . 'theDir' . DIRECTORY_SEPARATOR . 'file2', ]; - $this->assertEquals($expected, FileHelper::findFiles($dirName)); + $result = FileHelper::findFiles($dirName); + sort($result); + $this->assertEquals($expected, $result); } /** From cfbdb06d6dd6365db02a19a46a37c51dc8bf8953 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Fri, 16 Dec 2016 00:12:12 +0200 Subject: [PATCH 089/105] Test empty commit From f1a34773e1ace10c08deabd6a3b3d37416c1c472 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Fri, 16 Dec 2016 00:20:21 +0200 Subject: [PATCH 090/105] Test empty commit From e83624df9e695b96b92fd82bbe8dfd0b8b25e8c3 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 16 Dec 2016 01:43:54 +0300 Subject: [PATCH 091/105] Closes #11418: Added test for #11418 --- tests/framework/widgets/ActiveFieldTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/framework/widgets/ActiveFieldTest.php b/tests/framework/widgets/ActiveFieldTest.php index 07d750890d..49d4873e37 100644 --- a/tests/framework/widgets/ActiveFieldTest.php +++ b/tests/framework/widgets/ActiveFieldTest.php @@ -497,6 +497,14 @@ EOD; $this->assertEqualsWithoutLE($expectedValue, $actualValue); } + public function testEmptyTag() + { + $this->activeField->options = ['tag' => false]; + $expectedValue = ''; + $actualValue = $this->activeField->hiddenInput()->label(false)->error(false)->hint(false)->render(); + $this->assertEqualsWithoutLE($expectedValue, trim($actualValue)); + } + /** * Helper methods */ From 9821c589c5a8db97a5e690d7ce13b287d6c95939 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Thu, 15 Dec 2016 23:59:27 +0100 Subject: [PATCH 092/105] fixed issue with rest\UrlRule extraPatterns url creation did not work when no HTTP verb was specified. fixes #13200 --- framework/CHANGELOG.md | 3 + framework/rest/UrlRule.php | 2 +- tests/framework/rest/UrlRuleTest.php | 142 ++++++++++++++++++--------- 3 files changed, 97 insertions(+), 50 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 736460db0a..5fa969c9b6 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -30,6 +30,9 @@ Yii Framework 2 Change Log - Bug #13071: Help option for commands was not working in modules (arogachev, haimanman) - Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov) - Bug #13105: Fixed `validate()` method in `yii.activeForm.js` to prevent unexpected form submit when `forceValidate` set to `true` (silverfire) +- Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev) +- Bug #13200: Creating Urls for routes specified in `yii\rest\UrlRule::$extraPatterns` did not work if no HTTP verb was specified (cebe) +- Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) - Bug #13128: Fixed incorrect position of {pos} string in ColumnSchemaBuilder `__toString` (df2) - Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev) diff --git a/framework/rest/UrlRule.php b/framework/rest/UrlRule.php index d83b062f81..a4464c8ed7 100644 --- a/framework/rest/UrlRule.php +++ b/framework/rest/UrlRule.php @@ -203,7 +203,7 @@ class UrlRule extends CompositeUrlRule $config['verb'] = $verbs; $config['pattern'] = rtrim($prefix . '/' . strtr($pattern, $this->tokens), '/'); $config['route'] = $action; - if (!in_array('GET', $verbs)) { + if (!empty($verbs) && !in_array('GET', $verbs)) { $config['mode'] = \yii\web\UrlRule::PARSING_ONLY; } $config['suffix'] = $this->suffix; diff --git a/tests/framework/rest/UrlRuleTest.php b/tests/framework/rest/UrlRuleTest.php index 51565af5fe..4129235531 100644 --- a/tests/framework/rest/UrlRuleTest.php +++ b/tests/framework/rest/UrlRuleTest.php @@ -200,8 +200,9 @@ class UrlRuleTest extends TestCase * Proviedes test cases for createUrl() method * * - first param are properties of the UrlRule - * - second param is the route to create - * - third param is the expected URL + * - second param is an array of test cases, containing two element arrays: + * - first element is the route to create + * - second element is the expected URL */ public function createUrlDataProvider() { @@ -212,40 +213,46 @@ class UrlRuleTest extends TestCase 'controller' => 'v1/channel', 'pluralize' => true, ], - ['v1/channel/index'], // route - 'v1/channels', // expected + [ // test cases: route, expected + [ ['v1/channel/index'], 'v1/channels' ], + [ ['v1/channel/index', 'offset' => 1], 'v1/channels?offset=1' ], + [ ['v1/channel/view', 'id' => 42], 'v1/channels/42' ], + [ ['v1/channel/options'], 'v1/channels' ], + [ ['v1/channel/options', 'id' => 42], 'v1/channels/42' ], + [ ['v1/channel/delete'], false ], + ], ], [ [ // Rule properties 'controller' => ['v1/channel'], 'pluralize' => true, ], - ['v1/channel/index'], // route - 'v1/channels', // expected + [ // test cases: route, expected + [ ['v1/channel/index'], 'v1/channels' ], + [ ['v1/channel/index', 'offset' => 1], 'v1/channels?offset=1' ], + [ ['v1/channel/view', 'id' => 42], 'v1/channels/42' ], + [ ['v1/channel/options'], 'v1/channels' ], + [ ['v1/channel/options', 'id' => 42], 'v1/channels/42' ], + [ ['v1/channel/delete'], false ], + ], ], [ [ // Rule properties 'controller' => ['v1/channel', 'v1/u' => 'v1/user'], 'pluralize' => true, ], - ['v1/channel/index'], // route - 'v1/channels', // expected - ], - [ - [ // Rule properties - 'controller' => ['v1/channel', 'v1/u' => 'v1/user'], - 'pluralize' => true, + [ // test cases: route, expected + [ ['v1/channel/index'], 'v1/channels' ], + [ ['v1/channel/view', 'id' => 42], 'v1/channels/42' ], + [ ['v1/channel/options'], 'v1/channels' ], + [ ['v1/channel/options', 'id' => 42], 'v1/channels/42' ], + [ ['v1/channel/delete'], false ], + [ ['v1/user/index'], 'v1/u' ], + [ ['v1/user/view', 'id' => 1], 'v1/u/1' ], + [ ['v1/channel/options'], 'v1/channels' ], + [ ['v1/channel/options', 'id' => 42], 'v1/channels/42' ], + [ ['v1/user/delete'], false ], ], - ['v1/user/index'], // route - 'v1/u', // expected - ], - [ - [ // Rule properties - 'controller' => 'v1/channel', - 'pluralize' => true, - ], - ['v1/channel/index', 'offset' => 1], // route - 'v1/channels?offset=1', // expected ], @@ -255,60 +262,97 @@ class UrlRuleTest extends TestCase 'controller' => 'v1/channel', 'pluralize' => false, ], - ['v1/channel/index'], // route - 'v1/channel', // expected + [ // test cases: route, expected + [ ['v1/channel/index'], 'v1/channel' ], + [ ['v1/channel/index', 'offset' => 1], 'v1/channel?offset=1' ], + [ ['v1/channel/view', 'id' => 42], 'v1/channel/42' ], + [ ['v1/channel/options'], 'v1/channel' ], + [ ['v1/channel/options', 'id' => 42], 'v1/channel/42' ], + [ ['v1/channel/delete'], false ], + ], ], [ [ // Rule properties 'controller' => ['v1/channel'], 'pluralize' => false, ], - ['v1/channel/index'], // route - 'v1/channel', // expected + [ // test cases: route, expected + [ ['v1/channel/index'], 'v1/channel' ], + [ ['v1/channel/index', 'offset' => 1], 'v1/channel?offset=1' ], + [ ['v1/channel/view', 'id' => 42], 'v1/channel/42' ], + [ ['v1/channel/options'], 'v1/channel' ], + [ ['v1/channel/options', 'id' => 42], 'v1/channel/42' ], + [ ['v1/channel/delete'], false ], + ], ], [ [ // Rule properties 'controller' => ['v1/channel', 'v1/u' => 'v1/user'], 'pluralize' => false, ], - ['v1/channel/index'], // route - 'v1/channel', // expected - ], - [ - [ // Rule properties - 'controller' => ['v1/channel', 'v1/u' => 'v1/user'], - 'pluralize' => false, + [ // test cases: route, expected + [ ['v1/channel/index'], 'v1/channel' ], + [ ['v1/channel/view', 'id' => 42], 'v1/channel/42' ], + [ ['v1/channel/options'], 'v1/channel' ], + [ ['v1/channel/options', 'id' => 42], 'v1/channel/42' ], + [ ['v1/channel/delete'], false ], + [ ['v1/user/index'], 'v1/u' ], + [ ['v1/user/view', 'id' => 1], 'v1/u/1' ], + [ ['v1/user/options'], 'v1/u' ], + [ ['v1/user/options', 'id' => 42], 'v1/u/42' ], + [ ['v1/user/delete'], false ], ], - ['v1/user/index'], // route - 'v1/u', // expected ], + + // using extra patterns [ [ // Rule properties 'controller' => 'v1/channel', - 'pluralize' => false, + 'pluralize' => true, + 'extraPatterns' => [ + '{id}/my' => 'my', + 'my' => 'my', + // this should not create a URL, no GET definition + 'POST {id}/my2' => 'my2', + ], + ], + [ // test cases: route, expected + // normal actions should behave as before + [ ['v1/channel/index'], 'v1/channels' ], + [ ['v1/channel/index', 'offset' => 1], 'v1/channels?offset=1' ], + [ ['v1/channel/view', 'id' => 42], 'v1/channels/42' ], + [ ['v1/channel/options'], 'v1/channels' ], + [ ['v1/channel/options', 'id' => 42], 'v1/channels/42' ], + [ ['v1/channel/delete'], false ], + + [ ['v1/channel/my'], 'v1/channels/my' ], + [ ['v1/channel/my', 'id' => 42], 'v1/channels/42/my' ], + [ ['v1/channel/my2'], false ], + [ ['v1/channel/my2', 'id' => 42], false ], ], - ['v1/channel/index', 'offset' => 1], // route - 'v1/channel?offset=1', // expected ], - // --- ]; } /** * @dataProvider createUrlDataProvider */ - public function testCreateUrl($rule, $params, $expected) + public function testCreateUrl($rule, $tests) { - $this->mockWebApplication(); - Yii::$app->set('request', new Request(['hostInfo' => 'http://api.example.com', 'scriptUrl' => '/index.php'])); - $route = array_shift($params); + foreach($tests as $test) { + list($params, $expected) = $test; - $manager = new UrlManager([ - 'cache' => null, - ]); - $rule = new UrlRule($rule); - $this->assertEquals($expected, $rule->createUrl($manager, $route, $params)); + $this->mockWebApplication(); + Yii::$app->set('request', new Request(['hostInfo' => 'http://api.example.com', 'scriptUrl' => '/index.php'])); + $route = array_shift($params); + + $manager = new UrlManager([ + 'cache' => null, + ]); + $rule = new UrlRule($rule); + $this->assertEquals($expected, $rule->createUrl($manager, $route, $params)); + } } } From 6d4c61ce1664247a31509bb543fcc89adc1353c4 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 16 Dec 2016 00:04:00 +0100 Subject: [PATCH 093/105] fixed changelog --- framework/CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 5fa969c9b6..afaa3fd00b 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -30,13 +30,11 @@ Yii Framework 2 Change Log - Bug #13071: Help option for commands was not working in modules (arogachev, haimanman) - Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov) - Bug #13105: Fixed `validate()` method in `yii.activeForm.js` to prevent unexpected form submit when `forceValidate` set to `true` (silverfire) -- Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev) -- Bug #13200: Creating Urls for routes specified in `yii\rest\UrlRule::$extraPatterns` did not work if no HTTP verb was specified (cebe) -- Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) - Bug #13128: Fixed incorrect position of {pos} string in ColumnSchemaBuilder `__toString` (df2) - Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev) - Bug #13198: Fixed order of checks in `yii\validators\IpValidator` that sometimes caused wrong error message (silverfire) +- Bug #13200: Creating Urls for routes specified in `yii\rest\UrlRule::$extraPatterns` did not work if no HTTP verb was specified (cebe) - Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire) - Enh #6242: Access to validator in inline validation (arogachev) - Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) From 4496b3e79929ceb5f722d1c54a424a8cdc4aa3a2 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 16 Dec 2016 00:26:18 +0100 Subject: [PATCH 094/105] note about disabling serializer on arraycache close #12984 --- framework/caching/ArrayCache.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework/caching/ArrayCache.php b/framework/caching/ArrayCache.php index 752ae54e94..3c6efb5fc2 100644 --- a/framework/caching/ArrayCache.php +++ b/framework/caching/ArrayCache.php @@ -15,6 +15,8 @@ namespace yii\caching; * Unlike the [[Cache]], ArrayCache allows the expire parameter of [[set]], [[add]], [[multiSet]] and [[multiAdd]] to * be a floating point number, so you may specify the time in milliseconds (e.g. 0.1 will be 100 milliseconds). * + * For enhanced performance of ArrayCache, you can disable serialization of the stored data by setting [[$serializer]] to `false`. + * * For more details and usage information on Cache, see the [guide article on caching](guide:caching-overview). * * @author Carsten Brandt From 22787571b43d5c13aea4a7a978c969c8db3782af Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 16 Dec 2016 00:58:17 +0100 Subject: [PATCH 095/105] Update UPGRADE.md --- framework/UPGRADE.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index aedf2c3b66..3db7d170a0 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -60,13 +60,13 @@ Upgrade from Yii 2.0.10 * `yii\validators\FileValidator::getClientOptions()` and `yii\validators\ImageValidator::getClientOptions()` are now public. If you extend from these classes and override these methods, you must make them public as well. -Upgrade from Yii 2.0.10 ----------------------- +* PJAX: Auto generated IDs of the Pjax widget have been changed to use their own prefix to avoid conflicts. + Auto generated IDs are now prefixed with `p` instead of `w`. This is defined by the `$autoIdPrefix` + property of `yii\widgets\Pjax`. If you have any PHP or Javascript code that depends on autogenerated IDs + you should update these to match this new value. It is not a good idea to rely on auto generated values anyway, so + you better fix these cases by specifying an explicit ID. + -* PJAX: The value of the `$autoIdPrefix` property of `yii\widgets\Pjax` has been changed to `p`. If you have any PHP or Javascript -code dependant on this property (like i.e. tests or events), you should update these to match this new value. - - Upgrade from Yii 2.0.9 ---------------------- From 18bac955c1edb61a52e0e09e14b114a16c66b575 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 16 Dec 2016 03:08:02 +0100 Subject: [PATCH 096/105] docs about JSON and UTF-8 fixes #10761 --- framework/helpers/BaseJson.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/framework/helpers/BaseJson.php b/framework/helpers/BaseJson.php index faf6bc8ce9..28b2af34f7 100644 --- a/framework/helpers/BaseJson.php +++ b/framework/helpers/BaseJson.php @@ -40,9 +40,14 @@ class BaseJson /** * Encodes the given value into a JSON string. + * * The method enhances `json_encode()` by supporting JavaScript expressions. * In particular, the method will not encode a JavaScript expression that is * represented in terms of a [[JsExpression]] object. + * + * Note that data encoded as JSON must be UTF-8 encoded according to the JSON specification. + * You must ensure strings passed to this method have proper encoding before passing them. + * * @param mixed $value the data to be encoded. * @param int $options the encoding options. For more details please refer to * . Default is `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`. @@ -65,10 +70,14 @@ class BaseJson /** * Encodes the given value into a JSON string HTML-escaping entities so it is safe to be embedded in HTML code. + * * The method enhances `json_encode()` by supporting JavaScript expressions. * In particular, the method will not encode a JavaScript expression that is * represented in terms of a [[JsExpression]] object. * + * Note that data encoded as JSON must be UTF-8 encoded according to the JSON specification. + * You must ensure strings passed to this method have proper encoding before passing them. + * * @param mixed $value the data to be encoded * @return string the encoding result * @since 2.0.4 From 350e93e6260409f94728f2cd7b687b198a7151f6 Mon Sep 17 00:00:00 2001 From: Brandon Kelly Date: Fri, 16 Dec 2016 02:19:14 -0800 Subject: [PATCH 097/105] Fixes #12612: Query conditions added with `yii\db\Query::andWhere()` now get appended to the existing conditions if they were already being joined with the `and` operator --- framework/CHANGELOG.md | 2 +- framework/db/Query.php | 2 ++ tests/framework/db/QueryTest.php | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 43b02081df..7afabacc88 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -165,11 +165,11 @@ Yii Framework 2 Change Log - Enh #12440: Added `yii\base\Event::offAll()` method allowing clear all registered class-level event handlers (klimov-paul) - Enh #12499: When AJAX validation in enabled, `yii.activeForm.js` will run it forcefully on form submit to display all possible errors (silverfire) - Enh #12580: Make `yii.js` comply with strict and non-strict javascript mode to allow concatenation with external code (mikehaertl) +- Enh #12612: Query conditions added with `yii\db\Query::andWhere()` now get appended to the existing conditions if they were already being joined with the `and` operator (brandonkelly) - Enh #12664: Added support for wildcards for `optional` at `yii\filters\auth\AuthMethod` (mg-code) - Enh #12744: Added `afterInit` event to `yii.activeForm.js` (werew01f) - Enh: Method `yii\console\controllers\AssetController::getAssetManager()` automatically enables `yii\web\AssetManager::forceCopy` in case it is not explicitly specified (pana1990, klimov-paul) - 2.0.9 July 11, 2016 ------------------- diff --git a/framework/db/Query.php b/framework/db/Query.php index 9402a7921f..5b4e249972 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -585,6 +585,8 @@ class Query extends Component implements QueryInterface { if ($this->where === null) { $this->where = $condition; + } elseif (is_array($this->where) && isset($this->where[0]) && strcasecmp($this->where[0], 'and') === 0) { + $this->where[] = $condition; } else { $this->where = ['and', $this->where, $condition]; } diff --git a/tests/framework/db/QueryTest.php b/tests/framework/db/QueryTest.php index 4ade380dd5..95ce860506 100644 --- a/tests/framework/db/QueryTest.php +++ b/tests/framework/db/QueryTest.php @@ -344,11 +344,11 @@ abstract class QueryTest extends DatabaseTestCase $query->andFilterCompare('name', 'Doe', 'like'); $this->assertEquals($condition, $query->where); - $condition = ['and', $condition, ['>', 'rating', '9']]; + $condition[] = ['>', 'rating', '9']; $query->andFilterCompare('rating', '>9'); $this->assertEquals($condition, $query->where); - $condition = ['and', $condition, ['<=', 'value', '100']]; + $condition[] = ['<=', 'value', '100']; $query->andFilterCompare('value', '<=100'); $this->assertEquals($condition, $query->where); } From 483c6c1867c6b839a2745fda84ddc48ae2196b15 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 16 Dec 2016 23:35:46 +0300 Subject: [PATCH 098/105] Fixes #12713: Fixed `yii\caching\FileDependency` to clear stat cache before reading filemtime --- framework/CHANGELOG.md | 1 + framework/caching/FileDependency.php | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 7afabacc88..7434941c47 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -131,6 +131,7 @@ Yii Framework 2 Change Log - Bug #12605: Make 'safe' validator work on write-only properties (arthibald, CeBe) - Bug #12629: Fixed `yii\widgets\ActiveField::widget()` to call `adjustLabelFor()` for `InputWidget` descendants (coderlex) - Bug #12649: Fixed consistency of `indexBy` handling for `yii\db\Query::column()` (silverfire) +- Bug #12713: Fixed `yii\caching\FileDependency` to clear stat cache before reading filemtime (SG5) - Enh #384: Added ability to run migration from several locations via `yii\console\controllers\BaseMigrateController::$migrationNamespaces` (klimov-paul) - Enh #6996: Added `yii\web\MultipartFormDataParser`, which allows proper processing of 'multipart/form-data' encoded non POST requests (klimov-paul) - Enh #8719: Add support for HTML5 attributes on submitbutton (formaction/formmethod...) for ActiveForm (VirtualRJ) diff --git a/framework/caching/FileDependency.php b/framework/caching/FileDependency.php index 177734e283..49c4e1e4c4 100644 --- a/framework/caching/FileDependency.php +++ b/framework/caching/FileDependency.php @@ -43,6 +43,8 @@ class FileDependency extends Dependency throw new InvalidConfigException('FileDependency::fileName must be set'); } - return @filemtime(Yii::getAlias($this->fileName)); + $fileName = Yii::getAlias($this->fileName); + clearstatcache(false, $fileName); + return @filemtime($fileName); } } From 66e815871609a87f01b3a396fd974a466dafdd5e Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 16 Dec 2016 21:48:08 +0100 Subject: [PATCH 099/105] Fixes #11771: Fixed semantics of `yii\di\ServiceLocator::__isset()` to match the behavior of `__get()` which fixes inconsistent behavior on newer PHP versions --- framework/CHANGELOG.md | 1 + framework/di/ServiceLocator.php | 2 +- tests/framework/di/ServiceLocatorTest.php | 25 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 7434941c47..81e11fd1c1 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -9,6 +9,7 @@ Yii Framework 2 Change Log - Bug #9305: Fixed MSSQL `Schema::TYPE_TIMESTAMP` to be 'datetime' instead of 'timestamp', which is just an incremental number (nkovacs) - Bug #9616: Fixed mysql\Schema::loadColumnSchema to set enumValues attribute correctly if enum definition contains commas (fphammerle) - Bug #9796: Initialization of not existing `yii\grid\ActionColumn` default buttons (arogachev) +- Bug #11771: Fixed semantics of `yii\di\ServiceLocator::__isset()` to match the behavior of `__get()` which fixes inconsistent behavior on newer PHP versions (cebe) - Bug #12213: Fixed `yii\db\ActiveRecord::unlinkAll()` to respect `onCondition()` of the relational query (silverfire) - Bug #12681: Changed `data` column type from `text` to `blob` to handle null-byte (`\0`) in serialized RBAC rule properly (silverfire) - Bug #12714: Fixed `yii\validation\EmailValidator` to prevent false-positives checks when property `checkDns` is set to `true` (silverfire) diff --git a/framework/di/ServiceLocator.php b/framework/di/ServiceLocator.php index 09485fa4bd..0abf09d55f 100644 --- a/framework/di/ServiceLocator.php +++ b/framework/di/ServiceLocator.php @@ -84,7 +84,7 @@ class ServiceLocator extends Component */ public function __isset($name) { - if ($this->has($name, true)) { + if ($this->has($name)) { return true; } else { return parent::__isset($name); diff --git a/tests/framework/di/ServiceLocatorTest.php b/tests/framework/di/ServiceLocatorTest.php index 8df9b55078..5e3dd5f1be 100644 --- a/tests/framework/di/ServiceLocatorTest.php +++ b/tests/framework/di/ServiceLocatorTest.php @@ -86,4 +86,29 @@ class ServiceLocatorTest extends TestCase $this->assertTrue($object2 instanceof $className); $this->assertTrue($object === $object2); } + + /** + * https://github.com/yiisoft/yii2/issues/11771 + */ + public function testModulePropertyIsset() + { + $config = [ + 'components' => [ + 'captcha' => [ + 'name' => 'foo bar', + 'class' => 'yii\captcha\Captcha', + ], + ], + ]; + + $app = new ServiceLocator($config); + + $this->assertTrue(isset($app->captcha->name)); + $this->assertFalse(empty($app->captcha->name)); + + $this->assertEquals('foo bar', $app->captcha->name); + + $this->assertTrue(isset($app->captcha->name)); + $this->assertFalse(empty($app->captcha->name)); + } } From 7b5efe516188cd38234c575ffb1f223bd8988bb8 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Sat, 17 Dec 2016 10:17:30 +0200 Subject: [PATCH 100/105] Enhanced `handleAction()` in `yii.js` Closes #12737 --- framework/CHANGELOG.md | 2 +- framework/assets/yii.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 81e11fd1c1..f157eb2431 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -31,7 +31,7 @@ Yii Framework 2 Change Log - Bug #13071: Help option for commands was not working in modules (arogachev, haimanman) - Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov) - Bug #13105: Fixed `validate()` method in `yii.activeForm.js` to prevent unexpected form submit when `forceValidate` set to `true` (silverfire) -- Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) +- Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire, arisk) - Bug #13128: Fixed incorrect position of {pos} string in ColumnSchemaBuilder `__toString` (df2) - Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev) - Bug #13198: Fixed order of checks in `yii\validators\IpValidator` that sometimes caused wrong error message (silverfire) diff --git a/framework/assets/yii.js b/framework/assets/yii.js index 372ae45ae4..73484efb3c 100644 --- a/framework/assets/yii.js +++ b/framework/assets/yii.js @@ -154,6 +154,7 @@ window.yii = (function ($) { action = $e.attr('href'), params = $e.data('params'), pjax = $e.data('pjax') || 0, + usePjax = pjax !== 0 && $.support.pjax, pjaxPushState = !!$e.data('pjax-push-state'), pjaxReplaceState = !!$e.data('pjax-replace-state'), pjaxTimeout = $e.data('pjax-timeout'), @@ -164,7 +165,7 @@ window.yii = (function ($) { pjaxContainer, pjaxOptions = {}; - if (pjax !== 0 && $.support.pjax) { + if (usePjax) { if ($e.data('pjax-container')) { pjaxContainer = $e.data('pjax-container'); } else { @@ -190,13 +191,13 @@ window.yii = (function ($) { if (method === undefined) { if (action && action != '#') { - if (pjax !== 0 && $.support.pjax) { + if (usePjax) { $.pjax.click(event, pjaxOptions); } else { window.location = action; } } else if ($e.is(':submit') && $form.length) { - if (pjax !== 0 && $.support.pjax) { + if (usePjax) { $form.on('submit',function(e){ $.pjax.submit(e, pjaxOptions); }) @@ -249,7 +250,7 @@ window.yii = (function ($) { oldAction = $form.attr('action'); $form.attr('action', action); } - if (pjax !== 0 && $.support.pjax) { + if (usePjax) { $form.on('submit',function(e){ $.pjax.submit(e, pjaxOptions); }) From 779b687415d4861d2127db3fc425620d1cc7a5f8 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Sat, 17 Dec 2016 10:40:00 +0200 Subject: [PATCH 101/105] Try to fix travis build on PHP < 7 --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 587a0fd5d1..0ffdbe430e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -107,6 +107,11 @@ install: # disable xdebug for performance reasons when code coverage is not needed. note: xdebug on hhvm is disabled by default phpenv config-rm xdebug.ini || echo "xdebug is not installed" fi + - | + if [[ $TRAVIS_PHP_VERSION < 7 ]]; then + # install GD + sudo apt-get install php$(phpenv version-name)-gd + fi - travis_retry composer self-update && composer --version - travis_retry composer global require "fxp/composer-asset-plugin:^1.2.0" --no-plugins - export PATH="$HOME/.composer/vendor/bin:$PATH" From da5fbbd20e96af276c15ca42a4c88e9568760950 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Sat, 17 Dec 2016 10:43:34 +0200 Subject: [PATCH 102/105] Revert "Try to fix travis build on PHP < 7" This reverts commit 779b687415d4861d2127db3fc425620d1cc7a5f8. --- .travis.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0ffdbe430e..587a0fd5d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -107,11 +107,6 @@ install: # disable xdebug for performance reasons when code coverage is not needed. note: xdebug on hhvm is disabled by default phpenv config-rm xdebug.ini || echo "xdebug is not installed" fi - - | - if [[ $TRAVIS_PHP_VERSION < 7 ]]; then - # install GD - sudo apt-get install php$(phpenv version-name)-gd - fi - travis_retry composer self-update && composer --version - travis_retry composer global require "fxp/composer-asset-plugin:^1.2.0" --no-plugins - export PATH="$HOME/.composer/vendor/bin:$PATH" From dd4b5afb5bed0334d16b0aa7ef877e0474077323 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Sat, 17 Dec 2016 11:33:43 +0200 Subject: [PATCH 103/105] Try #2 to fix travis build on PHP < 7 --- .travis.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.travis.yml b/.travis.yml index 587a0fd5d1..755ad1dfc4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,6 +58,21 @@ matrix: services: - mysql - postgresql + - php: 5.4 + addons: + apt: + packages: + - php5.4-gd + - php: 5.5 + addons: + apt: + packages: + - php5.5-gd + - php: 5.6 + addons: + apt: + packages: + - php5.6-gd # have a separate branch for javascript tests - language: node_js node_js: 6 From 5795b3937eab3764a79e180f259dbabd6f655dad Mon Sep 17 00:00:00 2001 From: Ni-san Date: Sat, 17 Dec 2016 12:37:36 +0200 Subject: [PATCH 104/105] Added rbac\BaseManager::hasNoAssignments() --- framework/CHANGELOG.md | 1 + framework/rbac/BaseManager.php | 12 ++++++++++++ framework/rbac/DbManager.php | 5 +++++ framework/rbac/PhpManager.php | 5 +++++ tests/framework/rbac/ManagerTestCase.php | 10 ++++++++++ 5 files changed, 33 insertions(+) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index f157eb2431..c0d0b07682 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -63,6 +63,7 @@ Yii Framework 2 Change Log - Enh #12748: Added Migration tool automatic generation reference column for foreignKey (MKiselev) - Enh #12748: Migration generator now tries to fetch reference column name for foreignKey from schema if it's not set explicitly (MKiselev) - Enh #12750: `yii\widgets\ListView::itemOptions` can be a closure now (webdevsega, silverfire) +- Enh #12771: Skip \yii\rbac\PhpManager::checkAccessRecursive and \yii\rbac\DbManager::checkAccessRecursive if role assignments are empty (Ni-san) - Enh #12790: Added `scrollToErrorOffset` option for `yii\widgets\ActiveForm` which adds ability to specify offset in pixels when scrolling to error (mg-code) - Enh #12798: Changed `yii\cache\Dependency::getHasChanged()` (deprecated, to be removed in 2.1) to `yii\cache\Dependency::isChanged()` (dynasource) - Enh #12807: Added console controller checks for `yii\console\controllers\HelpController` (schmunk42) diff --git a/framework/rbac/BaseManager.php b/framework/rbac/BaseManager.php index 98460ff198..f1e4b31861 100644 --- a/framework/rbac/BaseManager.php +++ b/framework/rbac/BaseManager.php @@ -222,4 +222,16 @@ abstract class BaseManager extends Component implements ManagerInterface throw new InvalidConfigException("Rule not found: {$item->ruleName}"); } } + + /** + * Checks whether $assignments array is empty and [[defaultRoles]] are empty as well. + * + * @param Assignment[] $assignments array of user's assignments + * @return bool whether $assignments array is empty and [[defaultRoles]] are empty as well + * @since 2.0.11 + */ + protected function hasNoAssignments(array $assignments) + { + return empty($assignments) && empty($this->defaultRoles); + } } diff --git a/framework/rbac/DbManager.php b/framework/rbac/DbManager.php index b38d5fecad..1935bdf6e6 100644 --- a/framework/rbac/DbManager.php +++ b/framework/rbac/DbManager.php @@ -121,6 +121,11 @@ class DbManager extends BaseManager public function checkAccess($userId, $permissionName, $params = []) { $assignments = $this->getAssignments($userId); + + if ($this->hasNoAssignments($assignments)) { + return false; + } + $this->loadFromCache(); if ($this->items !== null) { return $this->checkAccessFromCache($userId, $permissionName, $params, $assignments); diff --git a/framework/rbac/PhpManager.php b/framework/rbac/PhpManager.php index 432653d376..71cce73b93 100644 --- a/framework/rbac/PhpManager.php +++ b/framework/rbac/PhpManager.php @@ -99,6 +99,11 @@ class PhpManager extends BaseManager public function checkAccess($userId, $permissionName, $params = []) { $assignments = $this->getAssignments($userId); + + if ($this->hasNoAssignments($assignments)) { + return false; + } + return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments); } diff --git a/tests/framework/rbac/ManagerTestCase.php b/tests/framework/rbac/ManagerTestCase.php index 4fc10d260d..70c30b1193 100644 --- a/tests/framework/rbac/ManagerTestCase.php +++ b/tests/framework/rbac/ManagerTestCase.php @@ -182,6 +182,16 @@ abstract class ManagerTestCase extends TestCase 'blablabla' => false, null => false, ], + 'guest' => [ + // all actions denied for guest (user not exists) + 'createPost' => false, + 'readPost' => false, + 'updatePost' => false, + 'deletePost' => false, + 'updateAnyPost' => false, + 'blablabla' => false, + null => false, + ], ]; $params = ['authorID' => 'author B']; From 4e32fb1eba1cb3a86f04048fbbee84b34b8cc9ff Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Sat, 17 Dec 2016 12:44:39 +0200 Subject: [PATCH 105/105] Enhanced PHPDocs --- framework/rbac/BaseManager.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/rbac/BaseManager.php b/framework/rbac/BaseManager.php index f1e4b31861..37b54dc9ee 100644 --- a/framework/rbac/BaseManager.php +++ b/framework/rbac/BaseManager.php @@ -224,10 +224,10 @@ abstract class BaseManager extends Component implements ManagerInterface } /** - * Checks whether $assignments array is empty and [[defaultRoles]] are empty as well. + * Checks whether array of $assignments is empty and [[defaultRoles]] property is empty as well * * @param Assignment[] $assignments array of user's assignments - * @return bool whether $assignments array is empty and [[defaultRoles]] are empty as well + * @return bool whether array of $assignments is empty and [[defaultRoles]] property is empty as well * @since 2.0.11 */ protected function hasNoAssignments(array $assignments)