Commit 6f932d99 by Antonio Ramirez

Merge branch 'upstream' into 364-toAscii

* upstream: (21 commits) Fixes #1643: Added default value for `Captcha::options` Fixes #1654: Fixed the issue that a new message source object is generated for every new message being translated Allow dash char in ActionColumn’s button names. Added SecurityTest. fixed functional test when enablePrettyUrl is false. fixed composer.json minor doc fix. Fixes #1634: Use masked CSRF tokens to prevent BREACH exploits Use better random CSRF token. GII unique indexes avoid autoIncrement columns updated debug retry params. Added sleep(). Added unit test for ActiveRecord::updateAttributes(). Fixes #1641: Added `BaseActiveRecord::updateAttributes()` Fixed #1504: Debug toolbar isn't loaded successfully in some environments when xdebug is enabled Mongo README.md updated. Fixes #1611: Added `BaseActiveRecord::markAttributeDirty()` Number validator was missing Fixes #1638: prevent table names from being enclosed within curly brackets twice. Unique indexes rules for single columns into array ...
parents 08aaeda3 4148912b
...@@ -26,7 +26,6 @@ $this->params['breadcrumbs'][] = $this->title; ...@@ -26,7 +26,6 @@ $this->params['breadcrumbs'][] = $this->title;
<?= $form->field($model, 'subject') ?> <?= $form->field($model, 'subject') ?>
<?= $form->field($model, 'body')->textArea(['rows' => 6]) ?> <?= $form->field($model, 'body')->textArea(['rows' => 6]) ?>
<?= $form->field($model, 'verifyCode')->widget(Captcha::className(), [ <?= $form->field($model, 'verifyCode')->widget(Captcha::className(), [
'options' => ['class' => 'form-control'],
'template' => '<div class="row"><div class="col-lg-3">{image}</div><div class="col-lg-6">{input}</div></div>', 'template' => '<div class="row"><div class="col-lg-3">{image}</div><div class="col-lg-6">{input}</div></div>',
]) ?> ]) ?>
<div class="form-group"> <div class="form-group">
......
...@@ -7,5 +7,11 @@ return [ ...@@ -7,5 +7,11 @@ return [
'db' => [ 'db' => [
'dsn' => 'mysql:host=localhost;dbname=yii2basic_functional', 'dsn' => 'mysql:host=localhost;dbname=yii2basic_functional',
], ],
'request' => [
'enableCsrfValidation' => false,
],
'urlManager' => [
'baseUrl' => '/web/index.php',
],
], ],
]; ];
<?php <?php
$config = yii\helpers\ArrayHelper::merge( // create an application instance to support URL creation before running any test
require(__DIR__ . '/../../config/web.php'), Yii::createObject(require(__DIR__ . '/../../web/index-test-functional.php'));
require(__DIR__ . '/../../config/codeception/functional.php')
);
$application = new yii\web\Application($config);
...@@ -34,7 +34,6 @@ $this->params['breadcrumbs'][] = $this->title; ...@@ -34,7 +34,6 @@ $this->params['breadcrumbs'][] = $this->title;
<?= $form->field($model, 'subject') ?> <?= $form->field($model, 'subject') ?>
<?= $form->field($model, 'body')->textArea(['rows' => 6]) ?> <?= $form->field($model, 'body')->textArea(['rows' => 6]) ?>
<?= $form->field($model, 'verifyCode')->widget(Captcha::className(), [ <?= $form->field($model, 'verifyCode')->widget(Captcha::className(), [
'options' => ['class' => 'form-control'],
'template' => '<div class="row"><div class="col-lg-3">{image}</div><div class="col-lg-6">{input}</div></div>', 'template' => '<div class="row"><div class="col-lg-3">{image}</div><div class="col-lg-6">{input}</div></div>',
]) ?> ]) ?>
<div class="form-group"> <div class="form-group">
......
...@@ -50,6 +50,7 @@ ...@@ -50,6 +50,7 @@
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"replace": { "replace": {
"yiisoft/yii2-authclient": "self.version",
"yiisoft/yii2-bootstrap": "self.version", "yiisoft/yii2-bootstrap": "self.version",
"yiisoft/yii2-codeception": "self.version", "yiisoft/yii2-codeception": "self.version",
"yiisoft/yii2-debug": "self.version", "yiisoft/yii2-debug": "self.version",
...@@ -96,6 +97,7 @@ ...@@ -96,6 +97,7 @@
}, },
"autoload": { "autoload": {
"psr-0": { "psr-0": {
"yii\\authclient\\": "extensions/",
"yii\\bootstrap\\": "extensions/", "yii\\bootstrap\\": "extensions/",
"yii\\codeception\\": "extensions/", "yii\\codeception\\": "extensions/",
"yii\\debug\\": "extensions/", "yii\\debug\\": "extensions/",
......
...@@ -139,6 +139,13 @@ Validates that the attribute value matches the specified pattern defined by regu ...@@ -139,6 +139,13 @@ Validates that the attribute value matches the specified pattern defined by regu
- `pattern` the regular expression to be matched with. - `pattern` the regular expression to be matched with.
- `not` whether to invert the validation logic. _(false)_ - `not` whether to invert the validation logic. _(false)_
### `number`: [[NumberValidator]]
Validates that the attribute value is a number.
- `max` limit of the number. _(null)_
- `min` lower limit of the number. _(null)_
### `required`: [[RequiredValidator]] ### `required`: [[RequiredValidator]]
Validates that the specified attribute does not have null or empty value. Validates that the specified attribute does not have null or empty value.
......
...@@ -21,7 +21,7 @@ or add ...@@ -21,7 +21,7 @@ or add
"yiisoft/yii2-authclient": "*" "yiisoft/yii2-authclient": "*"
``` ```
to the require section of your composer.json. to the `require` section of your composer.json.
Usage & Documentation Usage & Documentation
...@@ -51,7 +51,7 @@ You need to setup auth client collection application component: ...@@ -51,7 +51,7 @@ You need to setup auth client collection application component:
] ]
``` ```
Then you need to apply [[yii\authclient\AuthAction]] to some of your web controllers: Then you need to add [[yii\authclient\AuthAction]] to some of your web controllers:
``` ```
class SiteController extends Controller class SiteController extends Controller
...@@ -68,7 +68,7 @@ class SiteController extends Controller ...@@ -68,7 +68,7 @@ class SiteController extends Controller
public function successCallback($client) public function successCallback($client)
{ {
$atributes = $client->getUserAttributes(); $attributes = $client->getUserAttributes();
// user login or signup comes here // user login or signup comes here
} }
} }
...@@ -79,5 +79,5 @@ You may use [[yii\authclient\widgets\Choice]] to compose auth client selection: ...@@ -79,5 +79,5 @@ You may use [[yii\authclient\widgets\Choice]] to compose auth client selection:
``` ```
<?= yii\authclient\Choice::widget([ <?= yii\authclient\Choice::widget([
'baseAuthUrl' => ['site/auth'] 'baseAuthUrl' => ['site/auth']
]); ?> ]) ?>
``` ```
\ No newline at end of file
...@@ -56,7 +56,7 @@ class Choice extends Widget ...@@ -56,7 +56,7 @@ class Choice extends Widget
private $_clients; private $_clients;
/** /**
* @var string name of the auth client collection application component. * @var string name of the auth client collection application component.
* This component will be used to fetch {@link services} value if it is not set. * This component will be used to fetch services value if it is not set.
*/ */
public $clientCollection = 'authClientCollection'; public $clientCollection = 'authClientCollection';
/** /**
...@@ -226,4 +226,4 @@ class Choice extends Widget ...@@ -226,4 +226,4 @@ class Choice extends Widget
} }
echo Html::endTag('div'); echo Html::endTag('div');
} }
} }
\ No newline at end of file
...@@ -4,7 +4,7 @@ Yii Framework 2 debug extension Change Log ...@@ -4,7 +4,7 @@ Yii Framework 2 debug extension Change Log
2.0.0 beta under development 2.0.0 beta under development
---------------------------- ----------------------------
- no changes in this release. - Bug #1504: Debug toolbar isn't loaded successfully in some environments when xdebug is enabled (qiangxue)
2.0.0 alpha, December 1, 2013 2.0.0 alpha, December 1, 2013
----------------------------- -----------------------------
......
...@@ -64,7 +64,7 @@ class DefaultController extends Controller ...@@ -64,7 +64,7 @@ class DefaultController extends Controller
public function actionToolbar($tag) public function actionToolbar($tag)
{ {
$this->loadData($tag); $this->loadData($tag, 5);
return $this->renderPartial('toolbar', [ return $this->renderPartial('toolbar', [
'tag' => $tag, 'tag' => $tag,
'panels' => $this->module->panels, 'panels' => $this->module->panels,
...@@ -78,9 +78,12 @@ class DefaultController extends Controller ...@@ -78,9 +78,12 @@ class DefaultController extends Controller
private $_manifest; private $_manifest;
protected function getManifest() protected function getManifest($forceReload = false)
{ {
if ($this->_manifest === null) { if ($this->_manifest === null || $forceReload) {
if ($forceReload) {
clearstatcache();
}
$indexFile = $this->module->dataPath . '/index.data'; $indexFile = $this->module->dataPath . '/index.data';
if (is_file($indexFile)) { if (is_file($indexFile)) {
$this->_manifest = array_reverse(unserialize(file_get_contents($indexFile)), true); $this->_manifest = array_reverse(unserialize(file_get_contents($indexFile)), true);
...@@ -91,24 +94,31 @@ class DefaultController extends Controller ...@@ -91,24 +94,31 @@ class DefaultController extends Controller
return $this->_manifest; return $this->_manifest;
} }
public function loadData($tag) public function loadData($tag, $maxRetry = 0)
{ {
$manifest = $this->getManifest(); // retry loading debug data because the debug data is logged in shutdown function
if (isset($manifest[$tag])) { // which may be delayed in some environment if xdebug is enabled.
$dataFile = $this->module->dataPath . "/$tag.data"; // See: https://github.com/yiisoft/yii2/issues/1504
$data = unserialize(file_get_contents($dataFile)); for ($retry = 0; $retry <= $maxRetry; ++$retry) {
foreach ($this->module->panels as $id => $panel) { $manifest = $this->getManifest($retry > 0);
if (isset($data[$id])) { if (isset($manifest[$tag])) {
$panel->tag = $tag; $dataFile = $this->module->dataPath . "/$tag.data";
$panel->load($data[$id]); $data = unserialize(file_get_contents($dataFile));
} else { foreach ($this->module->panels as $id => $panel) {
// remove the panel since it has not received any data if (isset($data[$id])) {
unset($this->module->panels[$id]); $panel->tag = $tag;
$panel->load($data[$id]);
} else {
// remove the panel since it has not received any data
unset($this->module->panels[$id]);
}
} }
$this->summary = $data['summary'];
return;
} }
$this->summary = $data['summary']; sleep(1);
} else {
throw new NotFoundHttpException("Unable to find debug data tagged with '$tag'.");
} }
throw new NotFoundHttpException("Unable to find debug data tagged with '$tag'.");
} }
} }
...@@ -13,6 +13,7 @@ use yii\db\Connection; ...@@ -13,6 +13,7 @@ use yii\db\Connection;
use yii\db\Schema; use yii\db\Schema;
use yii\gii\CodeFile; use yii\gii\CodeFile;
use yii\helpers\Inflector; use yii\helpers\Inflector;
use yii\base\NotSupportedException;
/** /**
* This generator will generate one or multiple ActiveRecord classes for the specified database table. * This generator will generate one or multiple ActiveRecord classes for the specified database table.
...@@ -239,7 +240,6 @@ class Generator extends \yii\gii\Generator ...@@ -239,7 +240,6 @@ class Generator extends \yii\gii\Generator
} }
} }
} }
$rules = []; $rules = [];
foreach ($types as $type => $columns) { foreach ($types as $type => $columns) {
$rules[] = "[['" . implode("', '", $columns) . "'], '$type']"; $rules[] = "[['" . implode("', '", $columns) . "'], '$type']";
...@@ -248,6 +248,28 @@ class Generator extends \yii\gii\Generator ...@@ -248,6 +248,28 @@ class Generator extends \yii\gii\Generator
$rules[] = "[['" . implode("', '", $columns) . "'], 'string', 'max' => $length]"; $rules[] = "[['" . implode("', '", $columns) . "'], 'string', 'max' => $length]";
} }
// Unique indexes rules
try {
$db = $this->getDbConnection();
$uniqueIndexes = $db->getSchema()->findUniqueIndexes($table);
foreach ($uniqueIndexes as $indexName => $uniqueColumns) {
// Avoid validating auto incrementable columns
if (!$this->isUniqueColumnAutoIncrementable($table, $uniqueColumns)) {
$attributesCount = count($uniqueColumns);
if ($attributesCount == 1) {
$rules[] = "[['" . $uniqueColumns[0] . "'], 'unique']";
} elseif ($attributesCount > 1) {
$labels = array_intersect_key($this->generateLabels($table), array_flip($uniqueColumns));
$lastLabel = array_pop($labels);
$columnsList = implode("', '", $uniqueColumns);
$rules[] = "[['" . $columnsList . "'], 'unique', 'targetAttribute' => ['" . $columnsList . "'], 'message' => 'The combination of " . implode(', ', $labels) . " and " . $lastLabel . " has already been taken.']";
}
}
}
} catch (NotSupportedException $e) {
// doesn't support unique indexes information...do nothing
}
return $rules; return $rules;
} }
...@@ -552,4 +574,20 @@ class Generator extends \yii\gii\Generator ...@@ -552,4 +574,20 @@ class Generator extends \yii\gii\Generator
{ {
return Yii::$app->{$this->db}; return Yii::$app->{$this->db};
} }
/**
* Checks if any of the specified columns of an unique index is auto incrementable.
* @param \yii\db\TableSchema $table the table schema
* @param array $columns columns to check for autoIncrement property
* @return boolean whether any of the specified columns is auto incrementable.
*/
protected function isUniqueColumnAutoIncrementable($table, $columns)
{
foreach ($columns as $column) {
if ($table->columns[$column]->autoIncrement) {
return true;
}
}
return false;
}
} }
...@@ -65,11 +65,13 @@ class Customer extends ActiveRecord ...@@ -65,11 +65,13 @@ class Customer extends ActiveRecord
*/ */
public function attributes() public function attributes()
{ {
return ['name', 'email', 'address', 'status']; return ['_id', 'name', 'email', 'address', 'status'];
} }
} }
``` ```
Note: collection primary key name ('_id') should be always explicitly setup as an attribute.
You can use [[\yii\data\ActiveDataProvider]] with [[\yii\mongodb\Query]] and [[\yii\mongodb\ActiveQuery]]: You can use [[\yii\data\ActiveDataProvider]] with [[\yii\mongodb\Query]] and [[\yii\mongodb\ActiveQuery]]:
```php ```php
...@@ -102,3 +104,8 @@ $models = $provider->getModels(); ...@@ -102,3 +104,8 @@ $models = $provider->getModels();
This extension supports [MongoGridFS](http://docs.mongodb.org/manual/core/gridfs/) via This extension supports [MongoGridFS](http://docs.mongodb.org/manual/core/gridfs/) via
classes under namespace "\yii\mongodb\file". classes under namespace "\yii\mongodb\file".
This extension supports logging and profiling, however log messages does not contain
actual text of the performed queries, they contains only a “close approximation” of it
composed on the values which can be extracted from PHP Mongo extension classes.
If you need to see actual query text, you should use specific tools for that.
\ No newline at end of file
...@@ -7,9 +7,11 @@ Yii Framework 2 Change Log ...@@ -7,9 +7,11 @@ Yii Framework 2 Change Log
- Bug #1446: Logging while logs are processed causes infinite loop (qiangxue) - Bug #1446: Logging while logs are processed causes infinite loop (qiangxue)
- Bug #1497: Localized view files are not correctly returned (mintao) - Bug #1497: Localized view files are not correctly returned (mintao)
- Bug #1500: Log messages exported to files are not separated by newlines (omnilight, qiangxue) - Bug #1500: Log messages exported to files are not separated by newlines (omnilight, qiangxue)
- Bug #1504: Debug toolbar isn't loaded successfully in some environments when xdebug is enabled (qiangxue)
- Bug #1509: The SQL for creating Postgres RBAC tables is incorrect (qiangxue) - Bug #1509: The SQL for creating Postgres RBAC tables is incorrect (qiangxue)
- Bug #1545: It was not possible to execute db Query twice, params where missing (cebe) - Bug #1545: It was not possible to execute db Query twice, params where missing (cebe)
- Bug #1550: fixed the issue that JUI input widgets did not property input IDs. - Bug #1550: fixed the issue that JUI input widgets did not property input IDs.
- Bug #1654: Fixed the issue that a new message source object is generated for every new message being translated (qiangxue)
- Bug #1582: Error messages shown via client-side validation should not be double encoded (qiangxue) - Bug #1582: Error messages shown via client-side validation should not be double encoded (qiangxue)
- Bug #1591: StringValidator is accessing undefined property (qiangxue) - Bug #1591: StringValidator is accessing undefined property (qiangxue)
- Bug #1597: Added `enableAutoLogin` to basic and advanced application templates so "remember me" now works properly (samdark) - Bug #1597: Added `enableAutoLogin` to basic and advanced application templates so "remember me" now works properly (samdark)
...@@ -29,11 +31,15 @@ Yii Framework 2 Change Log ...@@ -29,11 +31,15 @@ Yii Framework 2 Change Log
- Enh #1579: throw exception when the given AR relation name does not match in a case sensitive manner (qiangxue) - Enh #1579: throw exception when the given AR relation name does not match in a case sensitive manner (qiangxue)
- Enh #1581: Added `ActiveQuery::joinWith()` and `ActiveQuery::innerJoinWith()` to support joining with relations (qiangxue) - Enh #1581: Added `ActiveQuery::joinWith()` and `ActiveQuery::innerJoinWith()` to support joining with relations (qiangxue)
- Enh #1601: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight) - Enh #1601: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight)
- Enh #1611: Added `BaseActiveRecord::markAttributeDirty()` (qiangxue)
- Enh #1634: Use masked CSRF tokens to prevent BREACH exploits (qiangxue)
- Enh #1641: Added `BaseActiveRecord::updateAttributes()` (qiangxue)
- Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark) - Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark)
- Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue)
- Enh: Support for file aliases in console command 'message' (omnilight) - Enh: Support for file aliases in console command 'message' (omnilight)
- Enh: Sort and Pagination can now create absolute URLs (cebe) - Enh: Sort and Pagination can now create absolute URLs (cebe)
- Chg #1610: `Html::activeCheckboxList()` and `Html::activeRadioList()` will submit an empty string if no checkbox/radio is selected (qiangxue) - Chg #1610: `Html::activeCheckboxList()` and `Html::activeRadioList()` will submit an empty string if no checkbox/radio is selected (qiangxue)
- Chg #1643: Added default value for `Captcha::options` (qiangxue)
- Chg: Renamed `yii\jui\Widget::clientEventsMap` to `clientEventMap` (qiangxue) - Chg: Renamed `yii\jui\Widget::clientEventsMap` to `clientEventMap` (qiangxue)
- Chg: Renamed `ActiveRecord::getPopulatedRelations()` to `getRelatedRecords()` (qiangxue) - Chg: Renamed `ActiveRecord::getPopulatedRelations()` to `getRelatedRecords()` (qiangxue)
- Chg: Renamed `attributeName` and `className` to `targetAttribute` and `targetClass` for `UniqueValidator` and `ExistValidator` (qiangxue) - Chg: Renamed `attributeName` and `className` to `targetAttribute` and `targetClass` for `UniqueValidator` and `ExistValidator` (qiangxue)
......
...@@ -48,6 +48,10 @@ class Captcha extends InputWidget ...@@ -48,6 +48,10 @@ class Captcha extends InputWidget
* while `{input}` will be replaced with the text input tag. * while `{input}` will be replaced with the text input tag.
*/ */
public $template = '{image} {input}'; public $template = '{image} {input}';
/**
* @var array the HTML attributes for the input tag.
*/
public $options = ['class' => 'form-control'];
/** /**
* Initializes the widget. * Initializes the widget.
......
...@@ -366,12 +366,18 @@ class ActiveQuery extends Query implements ActiveQueryInterface ...@@ -366,12 +366,18 @@ class ActiveQuery extends Query implements ActiveQueryInterface
$parentTable = $this->getQueryTableName($parent); $parentTable = $this->getQueryTableName($parent);
$childTable = $this->getQueryTableName($child); $childTable = $this->getQueryTableName($child);
if (strpos($parentTable, '{{') === false) {
$parentTable = '{{' . $parentTable . '}}';
}
if (strpos($childTable, '{{') === false) {
$childTable = '{{' . $childTable . '}}';
}
if (!empty($child->link)) { if (!empty($child->link)) {
$on = []; $on = [];
foreach ($child->link as $childColumn => $parentColumn) { foreach ($child->link as $childColumn => $parentColumn) {
$on[] = '{{' . $parentTable . "}}.[[$parentColumn]] = {{" . $childTable . "}}.[[$childColumn]]"; $on[] = "$parentTable.[[$parentColumn]] = $childTable.[[$childColumn]]";
} }
$on = implode(' AND ', $on); $on = implode(' AND ', $on);
} else { } else {
......
...@@ -494,6 +494,17 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface ...@@ -494,6 +494,17 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
} }
/** /**
* Marks an attribute dirty.
* This method may be called to force updating a record when calling [[update()]],
* even if there is no change being made to the record.
* @param string $name the attribute name
*/
public function markAttributeDirty($name)
{
unset($this->_oldAttributes[$name]);
}
/**
* Returns a value indicating whether the named attribute has been changed. * Returns a value indicating whether the named attribute has been changed.
* @param string $name the name of the attribute * @param string $name the name of the attribute
* @return boolean whether the attribute has been changed * @return boolean whether the attribute has been changed
...@@ -626,7 +637,36 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface ...@@ -626,7 +637,36 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
} }
/** /**
* @see CActiveRecord::update() * Updates the specified attributes.
*
* This method is a shortcut to [[update()]] when data validation is not needed
* and only a list of attributes need to be updated.
*
* You may specify the attributes to be updated as name list or name-value pairs.
* If the latter, the corresponding attribute values will be modified accordingly.
* The method will then save the specified attributes into database.
*
* Note that this method will NOT perform data validation.
*
* @param array $attributes the attributes (names or name-value pairs) to be updated
* @return integer|boolean the number of rows affected, or false if [[beforeSave()]] stops the updating process.
*/
public function updateAttributes($attributes)
{
$attrs = [];
foreach ($attributes as $name => $value) {
if (is_integer($name)) {
$attrs[] = $value;
} else {
$this->$name = $value;
$attrs[] = $name;
}
}
return $this->update(false, $attrs);
}
/**
* @see update()
* @throws StaleObjectException * @throws StaleObjectException
*/ */
protected function updateInternal($attributes = null) protected function updateInternal($attributes = null)
......
...@@ -122,7 +122,7 @@ class ActionColumn extends Column ...@@ -122,7 +122,7 @@ class ActionColumn extends Column
*/ */
protected function renderDataCellContent($model, $key, $index) protected function renderDataCellContent($model, $key, $index)
{ {
return preg_replace_callback('/\\{(\w+)\\}/', function ($matches) use ($model, $key, $index) { return preg_replace_callback('/\\{([\w\-]+)\\}/', function ($matches) use ($model, $key, $index) {
$name = $matches[1]; $name = $matches[1];
if (isset($this->buttons[$name])) { if (isset($this->buttons[$name])) {
$url = $this->createUrl($name, $model, $key, $index); $url = $this->createUrl($name, $model, $key, $index);
......
...@@ -241,7 +241,7 @@ class BaseHtml ...@@ -241,7 +241,7 @@ class BaseHtml
$method = 'post'; $method = 'post';
} }
if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) { if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) {
$hiddenInputs[] = static::hiddenInput($request->csrfVar, $request->getCsrfToken()); $hiddenInputs[] = static::hiddenInput($request->csrfVar, $request->getMaskedCsrfToken());
} }
} }
......
...@@ -157,19 +157,24 @@ class I18N extends Component ...@@ -157,19 +157,24 @@ class I18N extends Component
{ {
if (isset($this->translations[$category])) { if (isset($this->translations[$category])) {
$source = $this->translations[$category]; $source = $this->translations[$category];
if ($source instanceof MessageSource) {
return $source;
} else {
return $this->translations[$category] = Yii::createObject($source);
}
} else { } else {
// try wildcard matching // try wildcard matching
foreach ($this->translations as $pattern => $config) { foreach ($this->translations as $pattern => $config) {
if ($pattern === '*' || substr($pattern, -1) === '*' && strpos($category, rtrim($pattern, '*')) === 0) { if ($pattern === '*' || substr($pattern, -1) === '*' && strpos($category, rtrim($pattern, '*')) === 0) {
$source = $config; if ($config instanceof MessageSource) {
break; return $config;
} else {
return $this->translations[$category] = $this->translations[$pattern] = Yii::createObject($config);
}
} }
} }
} }
if (isset($source)) {
return $source instanceof MessageSource ? $source : Yii::createObject($source); throw new InvalidConfigException("Unable to locate message source for category '$category'.");
} else {
throw new InvalidConfigException("Unable to locate message source for category '$category'.");
}
} }
} }
...@@ -105,7 +105,7 @@ class MessageSource extends Component ...@@ -105,7 +105,7 @@ class MessageSource extends Component
} }
if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') { if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') {
return $this->_messages[$key][$message]; return $this->_messages[$key][$message];
} elseif ($this->hasEventHandlers('missingTranslation')) { } elseif ($this->hasEventHandlers(self::EVENT_MISSING_TRANSLATION)) {
$event = new MissingTranslationEvent([ $event = new MissingTranslationEvent([
'category' => $category, 'category' => $category,
'message' => $message, 'message' => $message,
......
...@@ -10,6 +10,7 @@ namespace yii\web; ...@@ -10,6 +10,7 @@ namespace yii\web;
use Yii; use Yii;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\helpers\Security; use yii\helpers\Security;
use yii\helpers\StringHelper;
/** /**
* The web Request class represents an HTTP request * The web Request class represents an HTTP request
...@@ -83,6 +84,10 @@ class Request extends \yii\base\Request ...@@ -83,6 +84,10 @@ class Request extends \yii\base\Request
* The name of the HTTP header for sending CSRF token. * The name of the HTTP header for sending CSRF token.
*/ */
const CSRF_HEADER = 'X-CSRF-Token'; const CSRF_HEADER = 'X-CSRF-Token';
/**
* The length of the CSRF token mask.
*/
const CSRF_MASK_LENGTH = 8;
/** /**
...@@ -1021,6 +1026,43 @@ class Request extends \yii\base\Request ...@@ -1021,6 +1026,43 @@ class Request extends \yii\base\Request
return $this->_csrfCookie->value; return $this->_csrfCookie->value;
} }
private $_maskedCsrfToken;
/**
* Returns the masked CSRF token.
* This method will apply a mask to [[csrfToken]] so that the resulting CSRF token
* will not be exploited by [BREACH attacks](http://breachattack.com/).
* @return string the masked CSRF token.
*/
public function getMaskedCsrfToken()
{
if ($this->_maskedCsrfToken === null) {
$token = $this->getCsrfToken();
$mask = Security::generateRandomKey(self::CSRF_MASK_LENGTH);
$this->_maskedCsrfToken = base64_encode($mask . $this->xorTokens($token, $mask));
}
return $this->_maskedCsrfToken;
}
/**
* Returns the XOR result of two strings.
* If the two strings are of different lengths, the shorter one will be padded to the length of the longer one.
* @param string $token1
* @param string $token2
* @return string the XOR result
*/
private function xorTokens($token1, $token2)
{
$n1 = StringHelper::byteLength($token1);
$n2 = StringHelper::byteLength($token2);
if ($n1 > $n2) {
$token2 = str_pad($token2, $n1, $token2);
} elseif ($n1 < $n2) {
$token1 = str_pad($token1, $n2, $token1);
}
return $token1 ^ $token2;
}
/** /**
* @return string the CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned if no such header is sent. * @return string the CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned if no such header is sent.
*/ */
...@@ -1040,7 +1082,7 @@ class Request extends \yii\base\Request ...@@ -1040,7 +1082,7 @@ class Request extends \yii\base\Request
{ {
$options = $this->csrfCookie; $options = $this->csrfCookie;
$options['name'] = $this->csrfVar; $options['name'] = $this->csrfVar;
$options['value'] = sha1(uniqid(mt_rand(), true)); $options['value'] = Security::generateRandomKey();
return new Cookie($options); return new Cookie($options);
} }
...@@ -1072,6 +1114,20 @@ class Request extends \yii\base\Request ...@@ -1072,6 +1114,20 @@ class Request extends \yii\base\Request
$token = $this->getPost($this->csrfVar); $token = $this->getPost($this->csrfVar);
break; break;
} }
return $token === $trueToken || $this->getCsrfTokenFromHeader() === $trueToken; return $this->validateCsrfTokenInternal($token, $trueToken)
|| $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken);
}
private function validateCsrfTokenInternal($token, $trueToken)
{
$token = base64_decode($token);
$n = StringHelper::byteLength($token);
if ($n <= self::CSRF_MASK_LENGTH) {
return false;
}
$mask = StringHelper::byteSubstr($token, 0, self::CSRF_MASK_LENGTH);
$token = StringHelper::byteSubstr($token, self::CSRF_MASK_LENGTH, $n - self::CSRF_MASK_LENGTH);
$token = $this->xorTokens($mask, $token);
return $token === $trueToken;
} }
} }
...@@ -388,7 +388,7 @@ class View extends \yii\base\View ...@@ -388,7 +388,7 @@ class View extends \yii\base\View
$request = Yii::$app->getRequest(); $request = Yii::$app->getRequest();
if ($request instanceof \yii\web\Request && $request->enableCsrfValidation) { if ($request instanceof \yii\web\Request && $request->enableCsrfValidation) {
$lines[] = Html::tag('meta', '', ['name' => 'csrf-var', 'content' => $request->csrfVar]); $lines[] = Html::tag('meta', '', ['name' => 'csrf-var', 'content' => $request->csrfVar]);
$lines[] = Html::tag('meta', '', ['name' => 'csrf-token', 'content' => $request->getCsrfToken()]); $lines[] = Html::tag('meta', '', ['name' => 'csrf-token', 'content' => $request->getMaskedCsrfToken()]);
} }
if (!empty($this->linkTags)) { if (!empty($this->linkTags)) {
......
...@@ -671,6 +671,40 @@ trait ActiveRecordTestTrait ...@@ -671,6 +671,40 @@ trait ActiveRecordTestTrait
$this->assertEquals(0, $ret); $this->assertEquals(0, $ret);
} }
public function testUpdateAttributes()
{
$customerClass = $this->getCustomerClass();
/** @var TestCase|ActiveRecordTestTrait $this */
// save
$customer = $this->callCustomerFind(2);
$this->assertTrue($customer instanceof $customerClass);
$this->assertEquals('user2', $customer->name);
$this->assertFalse($customer->isNewRecord);
static::$afterSaveNewRecord = null;
static::$afterSaveInsert = null;
$customer->updateAttributes(['name' => 'user2x']);
$this->afterSave();
$this->assertEquals('user2x', $customer->name);
$this->assertFalse($customer->isNewRecord);
$this->assertFalse(static::$afterSaveNewRecord);
$this->assertFalse(static::$afterSaveInsert);
$customer2 = $this->callCustomerFind(2);
$this->assertEquals('user2x', $customer2->name);
$customer = $this->callCustomerFind(1);
$this->assertEquals('user1', $customer->name);
$this->assertEquals(1, $customer->status);
$customer->name = 'user1x';
$customer->status = 2;
$customer->updateAttributes(['name']);
$this->assertEquals('user1x', $customer->name);
$this->assertEquals(2, $customer->status);
$customer = $this->callCustomerFind(1);
$this->assertEquals('user1x', $customer->name);
$this->assertEquals(1, $customer->status);
}
public function testUpdateCounters() public function testUpdateCounters()
{ {
$orderItemClass = $this->getOrderItemClass(); $orderItemClass = $this->getOrderItemClass();
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\helpers;
use yiiunit\TestCase;
use yii\helpers\Security;
class SecurityTest extends TestCase
{
public function testPasswordHash()
{
$password = 'secret';
$hash = Security::generatePasswordHash($password);
$this->assertTrue(Security::validatePassword($password, $hash));
$this->assertFalse(Security::validatePassword('test', $hash));
}
public function testHashData()
{
$data = 'known data';
$key = 'secret';
$hashedData = Security::hashData($data, $key);
$this->assertFalse($data === $hashedData);
$this->assertEquals($data, Security::validateData($hashedData, $key));
$hashedData[strlen($hashedData) - 1] = 'A';
$this->assertFalse(Security::validateData($hashedData, $key));
}
public function testEncrypt()
{
$data = 'known data';
$key = 'secret';
$encryptedData = Security::encrypt($data, $key);
$this->assertFalse($data === $encryptedData);
$decryptedData = Security::decrypt($encryptedData, $key);
$this->assertEquals($data, $decryptedData);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment