Browse Source

Merge branch 'master' of github.com:yiisoft/yii2

tags/2.0.0-rc
RichWeber 10 years ago
parent
commit
d5bd6816da
  1. 4
      apps/advanced/backend/config/main.php
  2. 7
      apps/advanced/composer.json
  3. 9
      apps/advanced/environments/dev/backend/config/main-local.php
  4. 9
      apps/advanced/environments/dev/frontend/config/main-local.php
  5. 24
      apps/advanced/environments/index.php
  6. 6
      apps/advanced/environments/prod/backend/config/main-local.php
  7. 6
      apps/advanced/environments/prod/frontend/config/main-local.php
  8. 4
      apps/advanced/frontend/config/main.php
  9. 250
      apps/advanced/init
  10. 2
      docs/guide/db-migrations.md
  11. 15
      docs/guide/db-query-builder.md
  12. 12
      docs/guide/tutorial-template-engines.md
  13. 20
      extensions/gii/generators/model/Generator.php
  14. 32
      extensions/sphinx/QueryBuilder.php
  15. 22
      extensions/twig/ViewRenderer.php
  16. 2
      framework/CHANGELOG.md
  17. 153
      framework/assets/yii.validation.js
  18. 11
      framework/base/Model.php
  19. 5
      framework/behaviors/AttributeBehavior.php
  20. 2
      framework/behaviors/BlameableBehavior.php
  21. 4
      framework/behaviors/TimestampBehavior.php
  22. 6
      framework/console/controllers/MessageController.php
  23. 32
      framework/db/QueryBuilder.php
  24. 18
      framework/db/QueryTrait.php
  25. 59
      framework/validators/FileValidator.php
  26. 62
      framework/validators/ImageValidator.php
  27. 2
      tests/unit/data/base/Singer.php
  28. 7
      tests/unit/extensions/gii/GeneratorsTest.php
  29. 9
      tests/unit/framework/base/ModelTest.php
  30. 102
      tests/unit/framework/db/QueryBuilderTest.php
  31. 2
      tests/unit/framework/rbac/PhpManagerTest.php
  32. 6
      tests/unit/framework/widgets/ActiveFieldTest.php

4
apps/advanced/backend/config/main.php

@ -13,10 +13,6 @@ return [
'bootstrap' => ['log'],
'modules' => [],
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
],
'user' => [
'identityClass' => 'common\models\User',
'enableAutoLogin' => true,

7
apps/advanced/composer.json

@ -32,8 +32,7 @@
},
"scripts": {
"post-create-project-cmd": [
"yii\\composer\\Installer::setPermission",
"yii\\composer\\Installer::generateCookieValidationKey"
"yii\\composer\\Installer::setPermission"
]
},
"config": {
@ -46,10 +45,6 @@
"frontend/runtime",
"frontend/web/assets"
],
"config": [
"frontend/config/main.php",
"backend/config/main.php"
]
}
}

9
apps/advanced/environments/dev/backend/config/main-local.php

@ -1,6 +1,13 @@
<?php
$config = [];
$config = [
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
],
],
];
if (!YII_ENV_TEST) {
// configuration adjustments for 'dev' environment

9
apps/advanced/environments/dev/frontend/config/main-local.php

@ -1,6 +1,13 @@
<?php
$config = [];
$config = [
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
],
],
];
if (!YII_ENV_TEST) {
// configuration adjustments for 'dev' environment

24
apps/advanced/environments/index.php

@ -9,9 +9,15 @@
* return [
* 'environment name' => [
* 'path' => 'directory storing the local files',
* 'writable' => [
* 'setWritable' => [
* // list of directories that should be set writable
* ],
* 'setExecutable' => [
* // list of directories that should be set executable
* ],
* 'setCookieValidationKey' => [
* // list of config files that need to be inserted with automatically generated cookie validation keys
* ],
* ],
* ];
* ```
@ -19,26 +25,34 @@
return [
'Development' => [
'path' => 'dev',
'writable' => [
'setWritable' => [
'backend/runtime',
'backend/web/assets',
'frontend/runtime',
'frontend/web/assets',
],
'executable' => [
'setExecutable' => [
'yii',
],
'setCookieValidationKey' => [
'backend/config/main-local.php',
'frontend/config/main-local.php',
],
],
'Production' => [
'path' => 'prod',
'writable' => [
'setWritable' => [
'backend/runtime',
'backend/web/assets',
'frontend/runtime',
'frontend/web/assets',
],
'executable' => [
'setExecutable' => [
'yii',
],
'setCookieValidationKey' => [
'backend/config/main-local.php',
'frontend/config/main-local.php',
],
],
];

6
apps/advanced/environments/prod/backend/config/main-local.php

@ -1,3 +1,9 @@
<?php
return [
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
],
],
];

6
apps/advanced/environments/prod/frontend/config/main-local.php

@ -1,3 +1,9 @@
<?php
return [
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
],
],
];

4
apps/advanced/frontend/config/main.php

@ -12,10 +12,6 @@ return [
'bootstrap' => ['log'],
'controllerNamespace' => 'frontend\controllers',
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
],
'user' => [
'identityClass' => 'common\models\User',
'enableAutoLogin' => true,

250
apps/advanced/init

@ -14,6 +14,10 @@
* @license http://www.yiiframework.com/license/
*/
if (!extension_loaded('mcrypt')) {
die('The mcrypt PHP extension is required by Yii2.');
}
$params = getParams();
$root = str_replace('\\', '/', __DIR__);
$envs = require("$root/environments/index.php");
@ -23,147 +27,169 @@ echo "Yii Application Initialization Tool v1.0\n\n";
$envName = null;
if (empty($params['env']) || $params['env'] === '1') {
echo "Which environment do you want the application to be initialized in?\n\n";
foreach ($envNames as $i => $name) {
echo " [$i] $name\n";
}
echo "\n Your choice [0-" . (count($envs) - 1) . ', or "q" to quit] ';
$answer = trim(fgets(STDIN));
if (!ctype_digit($answer) || !in_array($answer, range(0, count($envs) - 1))) {
echo "\n Quit initialization.\n";
exit(0);
}
if (isset($envNames[$answer])) {
$envName = $envNames[$answer];
}
echo "Which environment do you want the application to be initialized in?\n\n";
foreach ($envNames as $i => $name) {
echo " [$i] $name\n";
}
echo "\n Your choice [0-" . (count($envs) - 1) . ', or "q" to quit] ';
$answer = trim(fgets(STDIN));
if (!ctype_digit($answer) || !in_array($answer, range(0, count($envs) - 1))) {
echo "\n Quit initialization.\n";
exit(0);
}
if (isset($envNames[$answer])) {
$envName = $envNames[$answer];
}
} else {
$envName = $params['env'];
$envName = $params['env'];
}
if (!in_array($envName, $envNames)) {
$envsList = implode(', ', $envNames);
echo "\n $envName is not a valid environment. Try one of the following: $envsList. \n";
exit(2);
$envsList = implode(', ', $envNames);
echo "\n $envName is not a valid environment. Try one of the following: $envsList. \n";
exit(2);
}
$env = $envs[$envName];
if (empty($params['env'])) {
echo "\n Initialize the application under '{$envNames[$answer]}' environment? [yes|no] ";
$answer = trim(fgets(STDIN));
if (strncasecmp($answer, 'y', 1)) {
echo "\n Quit initialization.\n";
exit(0);
}
echo "\n Initialize the application under '{$envNames[$answer]}' environment? [yes|no] ";
$answer = trim(fgets(STDIN));
if (strncasecmp($answer, 'y', 1)) {
echo "\n Quit initialization.\n";
exit(0);
}
}
echo "\n Start initialization ...\n\n";
$files = getFileList("$root/environments/{$env['path']}");
$all = false;
foreach ($files as $file) {
if (!copyFile($root, "environments/{$env['path']}/$file", $file, $all, $params)) {
break;
}
}
if (isset($env['writable'])) {
foreach ($env['writable'] as $writable) {
echo " chmod 0777 $writable\n";
@chmod("$root/$writable", 0777);
}
if (!copyFile($root, "environments/{$env['path']}/$file", $file, $all, $params)) {
break;
}
}
if (isset($env['executable'])) {
foreach ($env['executable'] as $executable) {
echo " chmod 0755 $executable\n";
@chmod("$root/$executable", 0755);
}
$callbacks = ['setCookieValidationKey', 'setWritable', 'setExecutable'];
foreach ($callbacks as $callback) {
if (!empty($env[$callback])) {
$callback($root, $env[$callback]);
}
}
echo "\n ... initialization completed.\n\n";
function getFileList($root, $basePath = '')
{
$files = [];
$handle = opendir($root);
while (($path = readdir($handle)) !== false) {
if ($path === '.svn' || $path === '.' || $path === '..') {
continue;
}
$fullPath = "$root/$path";
$relativePath = $basePath === '' ? $path : "$basePath/$path";
if (is_dir($fullPath)) {
$files = array_merge($files, getFileList($fullPath, $relativePath));
} else {
$files[] = $relativePath;
}
}
closedir($handle);
return $files;
$files = [];
$handle = opendir($root);
while (($path = readdir($handle)) !== false) {
if ($path === '.svn' || $path === '.' || $path === '..') {
continue;
}
$fullPath = "$root/$path";
$relativePath = $basePath === '' ? $path : "$basePath/$path";
if (is_dir($fullPath)) {
$files = array_merge($files, getFileList($fullPath, $relativePath));
} else {
$files[] = $relativePath;
}
}
closedir($handle);
return $files;
}
function copyFile($root, $source, $target, &$all, $params)
{
if (!is_file($root . '/' . $source)) {
echo " skip $target ($source not exist)\n";
return true;
}
if (is_file($root . '/' . $target)) {
if (file_get_contents($root . '/' . $source) === file_get_contents($root . '/' . $target)) {
echo " unchanged $target\n";
return true;
}
if ($all) {
echo " overwrite $target\n";
} else {
echo " exist $target\n";
echo " ...overwrite? [Yes|No|All|Quit] ";
$answer = !empty($params['overwrite']) ? $params['overwrite'] : trim(fgets(STDIN));
if (!strncasecmp($answer, 'q', 1)) {
return false;
} else {
if (!strncasecmp($answer, 'y', 1)) {
echo " overwrite $target\n";
} else {
if (!strncasecmp($answer, 'a', 1)) {
echo " overwrite $target\n";
$all = true;
} else {
echo " skip $target\n";
return true;
}
}
}
}
file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source));
return true;
}
echo " generate $target\n";
@mkdir(dirname($root . '/' . $target), 0777, true);
file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source));
return true;
if (!is_file($root . '/' . $source)) {
echo " skip $target ($source not exist)\n";
return true;
}
if (is_file($root . '/' . $target)) {
if (file_get_contents($root . '/' . $source) === file_get_contents($root . '/' . $target)) {
echo " unchanged $target\n";
return true;
}
if ($all) {
echo " overwrite $target\n";
} else {
echo " exist $target\n";
echo " ...overwrite? [Yes|No|All|Quit] ";
$answer = !empty($params['overwrite']) ? $params['overwrite'] : trim(fgets(STDIN));
if (!strncasecmp($answer, 'q', 1)) {
return false;
} else {
if (!strncasecmp($answer, 'y', 1)) {
echo " overwrite $target\n";
} else {
if (!strncasecmp($answer, 'a', 1)) {
echo " overwrite $target\n";
$all = true;
} else {
echo " skip $target\n";
return true;
}
}
}
}
file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source));
return true;
}
echo " generate $target\n";
@mkdir(dirname($root . '/' . $target), 0777, true);
file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source));
return true;
}
function getParams()
{
$rawParams = [];
if (isset($_SERVER['argv'])) {
$rawParams = $_SERVER['argv'];
array_shift($rawParams);
}
$params = [];
foreach ($rawParams as $param) {
if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) {
$name = $matches[1];
$params[$name] = isset($matches[3]) ? $matches[3] : true;
} else {
$params[] = $param;
}
}
return $params;
$rawParams = [];
if (isset($_SERVER['argv'])) {
$rawParams = $_SERVER['argv'];
array_shift($rawParams);
}
$params = [];
foreach ($rawParams as $param) {
if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) {
$name = $matches[1];
$params[$name] = isset($matches[3]) ? $matches[3] : true;
} else {
$params[] = $param;
}
}
return $params;
}
function setWritable($root, $paths)
{
foreach ($paths as $writable) {
echo " chmod 0777 $writable\n";
@chmod("$root/$writable", 0777);
}
}
function setExecutable($root, $paths)
{
foreach ($paths as $executable) {
echo " chmod 0755 $executable\n";
@chmod("$root/$executable", 0755);
}
}
function setCookieValidationKey($root, $paths)
{
foreach ($paths as $file) {
echo " generate cookie validation key in $file\n";
$file = $root . '/' . $file;
$length = 32;
$bytes = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
$key = strtr(substr(base64_encode($bytes), 0, $length), '+/=', '_-.');
$content = preg_replace('/(("|\')cookieValidationKey("|\')\s*=>\s*)(""|\'\')/', "\\1'$key'", file_get_contents($file));
file_put_contents($file, $content);
}
}

2
docs/guide/db-migrations.md

@ -97,7 +97,7 @@ class m101129_185401_create_news_table extends \yii\db\Migration
}
```
The base class [\yii\db\Migration] exposes a database connection via `db`
The base class [[\yii\db\Migration]] exposes a database connection via `db`
property. You can use it for manipulating data and schema of a database.
The column types used in this example are abstract types that will be replaced

15
docs/guide/db-query-builder.md

@ -249,6 +249,19 @@ Operator can be one of the following:
- `not exists`: similar to the `exists` operator and builds a `NOT EXISTS (sub-query)` expression.
Additionally you can specify anything as operator:
```php
$userQuery = (new Query)->select('id')->from('user');
$query->where(['>=', 'id', 10]);
```
It will result in:
```sql
SELECT id FROM user WHERE id >= 10;
```
If you are building parts of condition dynamically it's very convenient to use `andWhere()` and `orWhere()`:
```php
@ -305,8 +318,6 @@ $query->orderBy([
Here we are ordering by `id` ascending and then by `name` descending.
```
### `GROUP BY` and `HAVING`
In order to add `GROUP BY` to generated SQL you can use the following:

12
docs/guide/tutorial-template-engines.md

@ -56,7 +56,7 @@ return $this->render('renderer.twig', ['username' => 'Alex']);
### Template syntax
The best resource to learn Twig basics is its official documentation you can find at
[twig.sensiolabs.org](http://twig.sensiolabs.org/documentation). Additionally there are Yii-specific addtions
[twig.sensiolabs.org](http://twig.sensiolabs.org/documentation). Additionally there are Yii-specific syntax extensions
described below.
#### Method and function calls
@ -271,7 +271,13 @@ or `$this->renderPartial()` controller calls:
return $this->render('renderer.tpl', ['username' => 'Alex']);
```
### Additional functions
### Template syntax
The best resource to learn Smarty template syntax is its official documentation you can find at
[www.smarty.net](http://www.smarty.net/docs/en/). Additionally there are Yii-specific syntax extensions
described below.
#### Additional functions
Yii adds the following construct to the standard Smarty syntax:
@ -281,7 +287,7 @@ Yii adds the following construct to the standard Smarty syntax:
Internally, the `path()` function calls Yii's `Url::to()` method.
### Additional variables
#### Additional variables
Within Smarty templates, you can also make use of these variables:

20
extensions/gii/generators/model/Generator.php

@ -197,7 +197,7 @@ class Generator extends \yii\gii\Generator
$labels[$column->name] = 'ID';
} else {
$label = Inflector::camel2words($column->name);
if (!empty($label) && substr_compare($label, ' id', -3, 3, true)) {
if (!empty($label) && substr_compare($label, ' id', -3, 3, true) === 0) {
$label = substr($label, 0, -3) . ' ID';
}
$labels[$column->name] = $label;
@ -508,16 +508,16 @@ class Generator extends \yii\gii\Generator
}
}
private $_tableNames;
private $_classNames;
protected $tableNames;
protected $classNames;
/**
* @return array the table names that match the pattern specified by [[tableName]].
*/
protected function getTableNames()
{
if ($this->_tableNames !== null) {
return $this->_tableNames;
if ($this->tableNames !== null) {
return $this->tableNames;
}
$db = $this->getDbConnection();
if ($db === null) {
@ -540,10 +540,10 @@ class Generator extends \yii\gii\Generator
}
} elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) {
$tableNames[] = $this->tableName;
$this->_classNames[$this->tableName] = $this->modelClass;
$this->classNames[$this->tableName] = $this->modelClass;
}
return $this->_tableNames = $tableNames;
return $this->tableNames = $tableNames;
}
/**
@ -574,8 +574,8 @@ class Generator extends \yii\gii\Generator
*/
protected function generateClassName($tableName)
{
if (isset($this->_classNames[$tableName])) {
return $this->_classNames[$tableName];
if (isset($this->classNames[$tableName])) {
return $this->classNames[$tableName];
}
if (($pos = strrpos($tableName, '.')) !== false) {
@ -601,7 +601,7 @@ class Generator extends \yii\gii\Generator
}
}
return $this->_classNames[$tableName] = Inflector::id2camel($className, '_');
return $this->classNames[$tableName] = Inflector::id2camel($className, '_');
}
/**

32
extensions/sphinx/QueryBuilder.php

@ -617,12 +617,11 @@ class QueryBuilder extends Object
$operator = strtoupper($condition[0]);
if (isset($builders[$operator])) {
$method = $builders[$operator];
array_shift($condition);
return $this->$method($indexes, $operator, $condition, $params);
} else {
throw new Exception('Found unknown operator in query: ' . $operator);
$method = 'buildSimpleCondition';
}
array_shift($condition);
return $this->$method($indexes, $operator, $condition, $params);
} else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
return $this->buildHashCondition($indexes, $condition, $params);
@ -986,4 +985,29 @@ class QueryBuilder extends Object
return $phName;
}
}
/**
* Creates an SQL expressions like `"column" operator value`.
* @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
* @param array $operands contains two column names.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
*/
public function buildSimpleCondition($operator, $operands, &$params)
{
if (count($operands) !== 2) {
throw new InvalidParamException("Operator '$operator' requires two operands.");
}
list($column, $value) = $operands;
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value === null ? 'NULL' : $value;
return "$column $operator $phName";
}
}

22
extensions/twig/ViewRenderer.php

@ -144,16 +144,30 @@ class ViewRenderer extends BaseViewRenderer
{
$this->twig->addGlobal('this', $view);
$loader = new \Twig_Loader_Filesystem(dirname($file));
foreach (Yii::$aliases as $alias => $path) {
$loader->addPath($path, substr($alias, 1));
}
$this->addAliases($loader, Yii::$aliases);
$this->twig->setLoader($loader);
return $this->twig->render(pathinfo($file, PATHINFO_BASENAME), $params);
}
/**
* Adds aliases
*
* @param \Twig_Loader_Filesystem $loader
* @param array $aliases
*/
protected function addAliases($loader, $aliases)
{
foreach ($aliases as $alias => $path) {
if (is_array($path)) {
$this->addAliases($loader, $path);
} elseif (is_string($path) && is_dir($path)) {
$loader->addPath($path, substr($alias, 1));
}
}
}
/**
* Adds global objects or static classes
* @param array $globals @see self::$globals
*/

2
framework/CHANGELOG.md

@ -90,6 +90,7 @@ Yii Framework 2 Change Log
- Enh #1388: Added mapping from physical types to abstract types for OCI DB driver (qiangxue)
- Enh #1452: Added `Module::getInstance()` to allow accessing the module instance from anywhere within the module (qiangxue)
- Enh #2264: `CookieCollection::has()` will return false for expired or removed cookies (qiangxue)
- Enh #2315: Any operator now could be used with `yii\db\Query::->where()` operand format (samdark)
- Enh #2435: `yii\db\IntegrityException` is now thrown on database integrity errors instead of general `yii\db\Exception` (samdark)
- Enh #2558: Enhanced support for memcached by adding `yii\caching\MemCache::persistentId` and `yii\caching\MemCache::options` (qiangxue)
- Enh #2837: Error page now shows arguments in stack trace method calls (samdark)
@ -167,6 +168,7 @@ Yii Framework 2 Change Log
- Enh #4436: Added callback functions to AJAX-based form validation (thiagotalma)
- Enh #4485: Added support for deferred validation in `ActiveForm` (Alex-Code)
- Enh #4520: Added sasl support to `yii\caching\MemCache` (xjflyttp)
- Enh #4566: Added client validation support for image validator (Skysplit, qiangxue)
- Enh: Added support for using sub-queries when building a DB query with `IN` condition (qiangxue)
- Enh: Supported adding a new response formatter without the need to reconfigure existing formatters (qiangxue)
- Enh: Added `yii\web\UrlManager::addRules()` to simplify adding new URL rules (qiangxue)

153
framework/assets/yii.validation.js

@ -68,55 +68,67 @@ yii.validation = (function ($) {
pub.addMessage(messages, options.notEqual, value);
}
},
file: function (value, messages, options, attribute) {
var files = $(attribute.input).get(0).files,
index, ext;
if (options.message && !files) {
pub.addMessage(messages, options.message, value);
}
if (!options.skipOnEmpty && files.length == 0) {
pub.addMessage(messages, options.uploadRequired, value);
} else if (files.length == 0) {
return;
}
if (options.maxFiles && options.maxFiles < files.length) {
pub.addMessage(messages, options.tooMany);
}
file: function (attribute, messages, options) {
var files = getUploadedFiles(attribute, messages, options);
$.each(files, function (i, file) {
if (options.extensions && options.extensions.length > 0) {
index = file.name.lastIndexOf('.');
if (!~index) {
ext = '';
} else {
ext = file.name.substr(index + 1, file.name.length).toLowerCase();
}
validateFile(file, messages, options);
});
},
image: function (attribute, messages, options, deferred) {
var files = getUploadedFiles(attribute, messages, options);
$.each(files, function (i, file) {
validateFile(file, messages, options);
if (!~options.extensions.indexOf(ext)) {
messages.push(options.wrongExtension.replace(/\{file\}/g, file.name));
}
// Skip image validation if FileReader API is not available
if (typeof FileReader === "undefined") {
return;
}
if (options.mimeTypes && options.mimeTypes.length > 0) {
if (!~options.mimeTypes.indexOf(file.type)) {
messages.push(options.wrongMimeType.replace(/\{file\}/g, file.name));
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.maxSize && options.maxSize < file.size) {
messages.push(options.tooBig.replace(/\{file\}/g, file.name));
}
if (options.maxSize && options.minSize > file.size) {
messages.push(options.tooSmall.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);
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);
});
},
number: function (value, messages, options) {
@ -288,5 +300,60 @@ yii.validation = (function ($) {
}
}
};
function getUploadedFiles(attribute, messages, options) {
var files = $(attribute.input).get(0).files;
if (!files) {
messages.push(options.message);
return [];
}
if (files.length === 0) {
if (!options.skipOnEmpty) {
messages.push(options.uploadRequired);
}
return [];
}
if (options.maxFiles && options.maxFiles < files.length) {
messages.push(options.tooMany);
return [];
}
return files;
}
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();
}
if (!~options.extensions.indexOf(ext)) {
messages.push(options.wrongExtension.replace(/\{file\}/g, file.name));
}
}
if (options.mimeTypes && options.mimeTypes.length > 0) {
if (!~options.mimeTypes.indexOf(file.type)) {
messages.push(options.wrongMimeType.replace(/\{file\}/g, file.name));
}
}
if (options.maxSize && options.maxSize < file.size) {
messages.push(options.tooBig.replace(/\{file\}/g, file.name));
}
if (options.minSize && options.minSize > file.size) {
messages.push(options.tooSmall.replace(/\{file\}/g, file.name));
}
}
return pub;
})(jQuery);

11
framework/base/Model.php

@ -385,7 +385,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
if ($this->_validators === null) {
$this->_validators = $this->createValidators();
}
return $this->_validators;
}
@ -404,7 +403,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
$validators[] = $validator;
}
}
return $validators;
}
@ -427,7 +425,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.');
}
}
return $validators;
}
@ -436,6 +433,12 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
* This is determined by checking if the attribute is associated with a
* [[\yii\validators\RequiredValidator|required]] validation rule in the
* current [[scenario]].
*
* Note that when the validator has a conditional validation applied using
* [[\yii\validators\RequiredValidator::$when|$when]] this method will return
* `false` regardless of the `when` condition because it may be called be
* before the model is loaded with data.
*
* @param string $attribute attribute name
* @return boolean whether the attribute is required
*/
@ -446,7 +449,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
return true;
}
}
return false;
}
@ -482,7 +484,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
public function getAttributeLabel($attribute)
{
$labels = $this->attributeLabels();
return isset($labels[$attribute]) ? $labels[$attribute] : $this->generateAttributeLabel($attribute);
}

5
framework/behaviors/AttributeBehavior.php

@ -93,7 +93,10 @@ class AttributeBehavior extends Behavior
$attributes = (array) $this->attributes[$event->name];
$value = $this->getValue($event);
foreach ($attributes as $attribute) {
$this->owner->$attribute = $value;
// ignore attribute names which are not string (e.g. when set by TimestampBehavior::updatedAtAttribute)
if (is_string($attribute)) {
$this->owner->$attribute = $value;
}
}
}
}

2
framework/behaviors/BlameableBehavior.php

@ -54,10 +54,12 @@ class BlameableBehavior extends AttributeBehavior
{
/**
* @var string the attribute that will receive current user ID value
* Set this property to be null if you do not want to record the creator ID.
*/
public $createdByAttribute = 'created_by';
/**
* @var string the attribute that will receive current user ID value
* Set this property to be null if you do not want to record the updater ID.
*/
public $updatedByAttribute = 'updated_by';
/**

4
framework/behaviors/TimestampBehavior.php

@ -64,10 +64,12 @@ class TimestampBehavior extends AttributeBehavior
{
/**
* @var string the attribute that will receive timestamp value
* Set this property to be null if you do not want to record the creation time.
*/
public $createdAtAttribute = 'created_at';
/**
* @var string the attribute that will receive timestamp value
* @var string the attribute that will receive timestamp value.
* Set this property to be null if you do not want to record the update time.
*/
public $updatedAtAttribute = 'updated_at';
/**

6
framework/console/controllers/MessageController.php

@ -96,6 +96,9 @@ class MessageController extends Controller
if (!is_dir($config['sourcePath'])) {
throw new Exception("The source path {$config['sourcePath']} is not a valid directory.");
}
if (empty($config['format']) || !in_array($config['format'], ['php', 'po', 'db'])) {
throw new Exception('Format should be either "php", "po" or "db".');
}
if (in_array($config['format'], ['php', 'po'])) {
if (!isset($config['messagePath'])) {
throw new Exception('The configuration file must specify "messagePath".');
@ -106,9 +109,6 @@ class MessageController extends Controller
if (empty($config['languages'])) {
throw new Exception("Languages cannot be empty.");
}
if (empty($config['format']) || !in_array($config['format'], ['php', 'po', 'db'])) {
throw new Exception('Format should be either "php", "po" or "db".');
}
$files = FileHelper::findFiles(realpath($config['sourcePath']), $config);

32
framework/db/QueryBuilder.php

@ -868,7 +868,6 @@ class QueryBuilder extends \yii\base\Object
* on how to specify a condition.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidParamException if the condition is in bad format
*/
public function buildCondition($condition, &$params)
{
@ -882,11 +881,11 @@ class QueryBuilder extends \yii\base\Object
$operator = strtoupper($condition[0]);
if (isset($this->conditionBuilders[$operator])) {
$method = $this->conditionBuilders[$operator];
array_shift($condition);
return $this->$method($operator, $condition, $params);
} else {
throw new InvalidParamException('Found unknown operator in query: ' . $operator);
$method = 'buildSimpleCondition';
}
array_shift($condition);
return $this->$method($operator, $condition, $params);
} else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
return $this->buildHashCondition($condition, $params);
}
@ -1194,4 +1193,29 @@ class QueryBuilder extends \yii\base\Object
throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.');
}
}
/**
* Creates an SQL expressions like `"column" operator value`.
* @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
* @param array $operands contains two column names.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
*/
public function buildSimpleCondition($operator, $operands, &$params)
{
if (count($operands) !== 2) {
throw new InvalidParamException("Operator '$operator' requires two operands.");
}
list($column, $value) = $operands;
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value === null ? 'NULL' : $value;
return "$column $operator $phName";
}
}

18
framework/db/QueryTrait.php

@ -253,20 +253,6 @@ trait QueryTrait
return [];
}
break;
case 'IN':
case 'NOT IN':
case 'LIKE':
case 'OR LIKE':
case 'NOT LIKE':
case 'OR NOT LIKE':
case 'ILIKE': // PostgreSQL operator for case insensitive LIKE
case 'OR ILIKE':
case 'NOT ILIKE':
case 'OR NOT ILIKE':
if (array_key_exists(1, $condition) && $this->isEmpty($condition[1])) {
return [];
}
break;
case 'BETWEEN':
case 'NOT BETWEEN':
if (array_key_exists(1, $condition) && array_key_exists(2, $condition)) {
@ -276,7 +262,9 @@ trait QueryTrait
}
break;
default:
throw new NotSupportedException("Operator not supported: $operator");
if (array_key_exists(1, $condition) && $this->isEmpty($condition[1])) {
return [];
}
}
array_unshift($condition, $operator);

59
framework/validators/FileValidator.php

@ -122,7 +122,7 @@ class FileValidator extends Validator
* - {mimeTypes}: the value of [[mimeTypes]]
*/
public $wrongMimeType;
/**
* @inheritdoc
@ -150,12 +150,16 @@ class FileValidator extends Validator
}
if (!is_array($this->extensions)) {
$this->extensions = preg_split('/[\s,]+/', strtolower($this->extensions), -1, PREG_SPLIT_NO_EMPTY);
} else {
$this->extensions = array_map('strtolower', $this->extensions);
}
if ($this->wrongMimeType === null) {
$this->wrongMimeType = Yii::t('yii', 'Only files with these MIME types are allowed: {mimeTypes}.');
}
if (!is_array($this->mimeTypes)) {
$this->mimeTypes = preg_split('/[\s,]+/', strtolower($this->mimeTypes), -1, PREG_SPLIT_NO_EMPTY);
} else {
$this->mimeTypes = array_map('strtolower', $this->mimeTypes);
}
}
@ -330,58 +334,77 @@ class FileValidator extends Validator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view) {
public function clientValidateAttribute($object, $attribute, $view)
{
ValidationAsset::register($view);
$options = $this->getClientOptions($object, $attribute);
return 'yii.validation.file(attribute, messages, ' . json_encode($options) . ');';
}
/**
* Returns the client side validation options.
* @param \yii\base\Model $object the model being validated
* @param string $attribute the attribute name being validated
* @return array the client side validation options
*/
protected function getClientOptions($object, $attribute)
{
$label = $object->getAttributeLabel($attribute);
if ( $this->message !== null ){
$options['message'] = Yii::$app->getI18n()->format($this->message, [
'attribute' => $label,
], Yii::$app->language);
}
$options['skipOnEmpty'] = $this->skipOnEmpty;
if ( !$this->skipOnEmpty ) {
$options['uploadRequired'] = Yii::$app->getI18n()->format($this->uploadRequired, [], Yii::$app->language);
if ( !$this->skipOnEmpty ) {
$options['uploadRequired'] = Yii::$app->getI18n()->format($this->uploadRequired, [
'attribute' => $label,
], Yii::$app->language);
}
if ( $this->mimeTypes !== null ) {
$options['mimeTypes'] = $this->mimeTypes;
$options['wrongMimeType'] = Yii::$app->getI18n()->format($this->wrongMimeType, [
'attribute' => $label,
'mimeTypes' => join(', ', $this->mimeTypes)
], Yii::$app->language);
}
if ( $this->extensions !== null ) {
$options['extensions'] = $this->extensions;
$options['wrongExtension'] = Yii::$app->getI18n()->format($this->wrongExtension, [
'attribute' => $label,
'extensions' => join(', ', $this->extensions)
], Yii::$app->language);
}
if ( $this->minSize !== null ) {
$options['minSize'] = $this->minSize;
$options['tooSmall'] = Yii::$app->getI18n()->format($this->tooSmall, [
'attribute' => $label,
'limit' => $this->minSize
], Yii::$app->language);
}
if ( $this->maxSize !== null ) {
$options['maxSize'] = $this->maxSize;
$options['tooBig'] = Yii::$app->getI18n()->format($this->tooBig, [
'attribute' => $label,
'limit' => $this->maxSize
], Yii::$app->language);
}
], Yii::$app->language);
}
if ( $this->maxFiles !== null ) {
$options['maxFiles'] = $this->maxFiles;
$options['tooMany'] = Yii::$app->getI18n()->format($this->tooMany, [
'attribute' => $label,
'limit' => $this->maxFiles
], Yii::$app->language);
}
ValidationAsset::register($view);
return 'yii.validation.file(value, messages, ' . json_encode($options) . ', attribute);';
return $options;
}
}

62
framework/validators/ImageValidator.php

@ -134,7 +134,7 @@ class ImageValidator extends FileValidator
return [$this->notImage, ['file' => $image->name]];
}
list($width, $height, $type) = $imageInfo;
list($width, $height) = $imageInfo;
if ($width == 0 || $height == 0) {
return [$this->notImage, ['file' => $image->name]];
@ -158,4 +158,64 @@ class ImageValidator extends FileValidator
return null;
}
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
{
ValidationAsset::register($view);
$options = $this->getClientOptions($object, $attribute);
return 'yii.validation.image(attribute, messages, ' . json_encode($options) . ', deferred);';
}
/**
* @inheritdoc
*/
protected function getClientOptions($object, $attribute)
{
$options = parent::getClientOptions($object, $attribute);
$label = $object->getAttributeLabel($attribute);
if ($this->notImage !== null) {
$options['notImage'] = Yii::$app->getI18n()->format($this->notImage, [
'attribute' => $label
], Yii::$app->language);
}
if ($this->minWidth !== null) {
$options['minWidth'] = $this->minWidth;
$options['underWidth'] = Yii::$app->getI18n()->format($this->underWidth, [
'attribute' => $label,
'limit' => $this->minWidth
], Yii::$app->language);
}
if ($this->maxWidth !== null) {
$options['maxWidth'] = $this->maxWidth;
$options['overWidth'] = Yii::$app->getI18n()->format($this->overWidth, [
'attribute' => $label,
'limit' => $this->maxWidth
], Yii::$app->language);
}
if ($this->minHeight !== null) {
$options['minHeight'] = $this->minHeight;
$options['underHeight'] = Yii::$app->getI18n()->format($this->underHeight, [
'attribute' => $label,
'limit' => $this->maxHeight
], Yii::$app->language);
}
if ($this->maxHeight !== null) {
$options['maxHeight'] = $this->maxHeight;
$options['overHeight'] = Yii::$app->getI18n()->format($this->overHeight, [
'attribute' => $label,
'limit' => $this->maxHeight
], Yii::$app->language);
}
return $options;
}
}

2
tests/unit/data/base/Singer.php

@ -10,6 +10,7 @@ class Singer extends Model
{
public $firstName;
public $lastName;
public $test;
public function rules()
{
@ -17,6 +18,7 @@ class Singer extends Model
[['lastName'], 'default', 'value' => 'Lennon'],
[['lastName'], 'required'],
[['underscore_style'], 'yii\captcha\CaptchaValidator'],
[['test'], 'required', 'when' => function($model) { return $model->firstName === 'cebe'; }],
];
}
}

7
tests/unit/extensions/gii/GeneratorsTest.php

@ -1,6 +1,7 @@
<?php
namespace yiiunit\extensions\gii;
use yii\gii\CodeFile;
use yii\gii\generators\controller\Generator as ControllerGenerator;
use yii\gii\generators\crud\Generator as CRUDGenerator;
use yii\gii\generators\extension\Generator as ExtensionGenerator;
@ -53,7 +54,11 @@ class GeneratorsTest extends GiiTestCase
$generator->modelClass = 'Profile';
if ($generator->validate()) {
$generator->generate();
$files = $generator->generate();
$modelCode = $files[0]->content;
$this->assertTrue(strpos($modelCode, "'id' => 'ID'") !== false, "ID label should be there:\n" . $modelCode);
$this->assertTrue(strpos($modelCode, "'description' => 'Description',") !== false, "Description label should be there:\n" . $modelCode);
} else {
print_r($generator->getErrors());
}

9
tests/unit/framework/base/ModelTest.php

@ -216,7 +216,7 @@ class ModelTest extends TestCase
public function testDefaultScenarios()
{
$singer = new Singer();
$this->assertEquals(['default' => ['lastName', 'underscore_style']], $singer->scenarios());
$this->assertEquals(['default' => ['lastName', 'underscore_style', 'test']], $singer->scenarios());
$scenarios = [
'default' => ['id', 'name', 'description'],
@ -238,6 +238,13 @@ class ModelTest extends TestCase
$singer = new Singer();
$this->assertFalse($singer->isAttributeRequired('firstName'));
$this->assertTrue($singer->isAttributeRequired('lastName'));
// attribute is not marked as required when a conditional validation is applied using `$when`.
// the condition should not be applied because this info may be retrieved before model is loaded with data
$singer->firstName = 'qiang';
$this->assertFalse($singer->isAttributeRequired('test'));
$singer->firstName = 'cebe';
$this->assertFalse($singer->isAttributeRequired('test'));
}
public function testCreateValidators()

102
tests/unit/framework/db/QueryBuilderTest.php

@ -152,10 +152,93 @@ class QueryBuilderTest extends DatabaseTestCase
[ ['or like', 'name', ['heyho', 'abc']], '"name" LIKE :qp0 OR "name" LIKE :qp1', [':qp0' => '%heyho%', ':qp1' => '%abc%'] ],
[ ['or not like', 'name', ['heyho', 'abc']], '"name" NOT LIKE :qp0 OR "name" NOT LIKE :qp1', [':qp0' => '%heyho%', ':qp1' => '%abc%'] ],
// TODO add more conditions
// IN
// NOT
// ...
// not
[ ['not', 'name'], 'NOT (name)', [] ],
// and
[ ['and', 'id=1', 'id=2'], '(id=1) AND (id=2)', [] ],
[ ['and', 'type=1', ['or', 'id=1', 'id=2']], '(type=1) AND ((id=1) OR (id=2))', [] ],
// or
[ ['or', 'id=1', 'id=2'], '(id=1) OR (id=2)', [] ],
[ ['or', 'type=1', ['or', 'id=1', 'id=2']], '(type=1) OR ((id=1) OR (id=2))', [] ],
// between
[ ['between', 'id', 1, 10], '"id" BETWEEN :qp0 AND :qp1', [':qp0' => 1, ':qp1' => 10] ],
[ ['not between', 'id', 1, 10], '"id" NOT BETWEEN :qp0 AND :qp1', [':qp0' => 1, ':qp1' => 10] ],
// in
[ ['in', 'id', [1, 2, 3]], '"id" IN (:qp0, :qp1, :qp2)', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3] ],
[ ['not in', 'id', [1, 2, 3]], '"id" NOT IN (:qp0, :qp1, :qp2)', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3] ],
// TODO: exists and not exists
// simple conditions
[ ['=', 'a', 'b'], '"a" = :qp0', [':qp0' => 'b'] ],
[ ['>', 'a', 1], '"a" > :qp0', [':qp0' => 1] ],
[ ['>=', 'a', 'b'], '"a" >= :qp0', [':qp0' => 'b'] ],
[ ['<', 'a', 2], '"a" < :qp0', [':qp0' => 2] ],
[ ['<=', 'a', 'b'], '"a" <= :qp0', [':qp0' => 'b'] ],
[ ['<>', 'a', 3], '"a" <> :qp0', [':qp0' => 3] ],
[ ['!=', 'a', 'b'], '"a" != :qp0', [':qp0' => 'b'] ],
];
// adjust dbms specific escaping
foreach($conditions as $i => $condition) {
switch ($this->driverName) {
case 'mssql':
case 'mysql':
case 'sqlite':
$conditions[$i][1] = str_replace('"', '`', $condition[1]);
break;
}
}
return $conditions;
}
public function filterConditionProvider()
{
$conditions = [
// like
[ ['like', 'name', []], '', [] ],
[ ['not like', 'name', []], '', [] ],
[ ['or like', 'name', []], '', [] ],
[ ['or not like', 'name', []], '', [] ],
// not
[ ['not', ''], '', [] ],
// and
[ ['and', '', ''], '', [] ],
[ ['and', '', 'id=2'], '(id=2)', [] ],
[ ['and', 'id=1', ''], '(id=1)', [] ],
[ ['and', 'type=1', ['or', '', 'id=2']], '(type=1) AND ((id=2))', [] ],
// or
[ ['or', 'id=1', ''], '(id=1)', [] ],
[ ['or', 'type=1', ['or', '', 'id=2']], '(type=1) OR ((id=2))', [] ],
// between
[ ['between', 'id', 1, null], '', [] ],
[ ['not between', 'id', null, 10], '', [] ],
// in
[ ['in', 'id', []], '', [] ],
[ ['not in', 'id', []], '', [] ],
// TODO: exists and not exists
// simple conditions
[ ['=', 'a', ''], '', [] ],
[ ['>', 'a', ''], '', [] ],
[ ['>=', 'a', ''], '', [] ],
[ ['<', 'a', ''], '', [] ],
[ ['<=', 'a', ''], '', [] ],
[ ['<>', 'a', ''], '', [] ],
[ ['!=', 'a', ''], '', [] ],
];
// adjust dbms specific escaping
@ -183,6 +266,17 @@ class QueryBuilderTest extends DatabaseTestCase
$this->assertEquals('SELECT *' . (empty($expected) ? '' : ' WHERE ' . $expected), $sql);
}
/**
* @dataProvider filterConditionProvider
*/
public function testBuildFilterCondition($condition, $expected, $expectedParams)
{
$query = (new Query())->filterWhere($condition);
list($sql, $params) = $this->getQueryBuilder()->build($query);
$this->assertEquals($expectedParams, $params);
$this->assertEquals('SELECT *' . (empty($expected) ? '' : ' WHERE ' . $expected), $sql);
}
public function testAddDropPrimaryKey()
{
$tableName = 'constraints';

2
tests/unit/framework/rbac/PhpManagerTest.php

@ -75,13 +75,13 @@ class PhpManagerTest extends ManagerTestCase
public function testSaveLoad()
{
static::$filemtime = time();
$this->prepareData();
$items = $this->auth->items;
$children = $this->auth->children;
$assignments = $this->auth->assignments;
$rules = $this->auth->rules;
static::$filemtime = time();
$this->auth->save();
$this->auth = $this->createManager();

6
tests/unit/framework/widgets/ActiveFieldTest.php

@ -266,7 +266,7 @@ EOD;
$this->activeField->model->addRule($this->attributeName, 'yiiunit\framework\widgets\TestValidator');
$this->activeField->enableClientValidation = true;
$actualValue = $this->activeField->getClientOptions();
$expectedJsExpression = "function (attribute, value, messages) {return true;}";
$expectedJsExpression = "function (attribute, value, messages, deferred) {return true;}";
$expectedValidateOnChange = true;
$expectedValidateOnType = false;
$expectedValidationDelay = 200;
@ -286,7 +286,7 @@ EOD;
$this->activeField->enableAjaxValidation = true;
$this->activeField->model->addRule($this->attributeName, 'yiiunit\framework\widgets\TestValidator');
$actualValue = $this->activeField->getClientOptions();
$expectedJsExpression = "function (attribute, value, messages) {return true;}";
$expectedJsExpression = "function (attribute, value, messages, deferred) {return true;}";
$expectedValidateOnChange = true;
$expectedValidateOnType = false;
$expectedValidationDelay = 200;
@ -313,7 +313,7 @@ EOD;
}
$actualValue = $this->activeField->getClientOptions();
$expectedJsExpression = "function (attribute, value, messages) {if (function (attribute, value) "
$expectedJsExpression = "function (attribute, value, messages, deferred) {if (function (attribute, value) "
. "{ return 'yii2' == 'yii2'; }(attribute, value)) { return true; }}";
$expectedValidateOnChange = true;

Loading…
Cancel
Save