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;
<?= $form->field($model, 'subject') ?>
<?= $form->field($model, 'body')->textArea(['rows' => 6]) ?>
<?= $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>',
]) ?>
<div class="form-group">
......
......@@ -7,5 +7,11 @@ return [
'db' => [
'dsn' => 'mysql:host=localhost;dbname=yii2basic_functional',
],
'request' => [
'enableCsrfValidation' => false,
],
'urlManager' => [
'baseUrl' => '/web/index.php',
],
],
];
<?php
$config = yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/../../config/web.php'),
require(__DIR__ . '/../../config/codeception/functional.php')
);
$application = new yii\web\Application($config);
// create an application instance to support URL creation before running any test
Yii::createObject(require(__DIR__ . '/../../web/index-test-functional.php'));
......@@ -34,7 +34,6 @@ $this->params['breadcrumbs'][] = $this->title;
<?= $form->field($model, 'subject') ?>
<?= $form->field($model, 'body')->textArea(['rows' => 6]) ?>
<?= $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>',
]) ?>
<div class="form-group">
......
......@@ -50,6 +50,7 @@
},
"minimum-stability": "dev",
"replace": {
"yiisoft/yii2-authclient": "self.version",
"yiisoft/yii2-bootstrap": "self.version",
"yiisoft/yii2-codeception": "self.version",
"yiisoft/yii2-debug": "self.version",
......@@ -96,6 +97,7 @@
},
"autoload": {
"psr-0": {
"yii\\authclient\\": "extensions/",
"yii\\bootstrap\\": "extensions/",
"yii\\codeception\\": "extensions/",
"yii\\debug\\": "extensions/",
......
......@@ -139,6 +139,13 @@ Validates that the attribute value matches the specified pattern defined by regu
- `pattern` the regular expression to be matched with.
- `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]]
Validates that the specified attribute does not have null or empty value.
......
......@@ -21,7 +21,7 @@ or add
"yiisoft/yii2-authclient": "*"
```
to the require section of your composer.json.
to the `require` section of your composer.json.
Usage & Documentation
......@@ -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
......@@ -68,7 +68,7 @@ class SiteController extends Controller
public function successCallback($client)
{
$atributes = $client->getUserAttributes();
$attributes = $client->getUserAttributes();
// user login or signup comes here
}
}
......@@ -79,5 +79,5 @@ You may use [[yii\authclient\widgets\Choice]] to compose auth client selection:
```
<?= yii\authclient\Choice::widget([
'baseAuthUrl' => ['site/auth']
]); ?>
```
\ No newline at end of file
]) ?>
```
......@@ -56,7 +56,7 @@ class Choice extends Widget
private $_clients;
/**
* @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';
/**
......@@ -226,4 +226,4 @@ class Choice extends Widget
}
echo Html::endTag('div');
}
}
\ No newline at end of file
}
......@@ -4,7 +4,7 @@ Yii Framework 2 debug extension Change Log
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
-----------------------------
......
......@@ -64,7 +64,7 @@ class DefaultController extends Controller
public function actionToolbar($tag)
{
$this->loadData($tag);
$this->loadData($tag, 5);
return $this->renderPartial('toolbar', [
'tag' => $tag,
'panels' => $this->module->panels,
......@@ -78,9 +78,12 @@ class DefaultController extends Controller
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';
if (is_file($indexFile)) {
$this->_manifest = array_reverse(unserialize(file_get_contents($indexFile)), true);
......@@ -91,24 +94,31 @@ class DefaultController extends Controller
return $this->_manifest;
}
public function loadData($tag)
public function loadData($tag, $maxRetry = 0)
{
$manifest = $this->getManifest();
if (isset($manifest[$tag])) {
$dataFile = $this->module->dataPath . "/$tag.data";
$data = unserialize(file_get_contents($dataFile));
foreach ($this->module->panels as $id => $panel) {
if (isset($data[$id])) {
$panel->tag = $tag;
$panel->load($data[$id]);
} else {
// remove the panel since it has not received any data
unset($this->module->panels[$id]);
// retry loading debug data because the debug data is logged in shutdown function
// which may be delayed in some environment if xdebug is enabled.
// See: https://github.com/yiisoft/yii2/issues/1504
for ($retry = 0; $retry <= $maxRetry; ++$retry) {
$manifest = $this->getManifest($retry > 0);
if (isset($manifest[$tag])) {
$dataFile = $this->module->dataPath . "/$tag.data";
$data = unserialize(file_get_contents($dataFile));
foreach ($this->module->panels as $id => $panel) {
if (isset($data[$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'];
} else {
throw new NotFoundHttpException("Unable to find debug data tagged with '$tag'.");
sleep(1);
}
throw new NotFoundHttpException("Unable to find debug data tagged with '$tag'.");
}
}
......@@ -13,6 +13,7 @@ use yii\db\Connection;
use yii\db\Schema;
use yii\gii\CodeFile;
use yii\helpers\Inflector;
use yii\base\NotSupportedException;
/**
* This generator will generate one or multiple ActiveRecord classes for the specified database table.
......@@ -239,7 +240,6 @@ class Generator extends \yii\gii\Generator
}
}
}
$rules = [];
foreach ($types as $type => $columns) {
$rules[] = "[['" . implode("', '", $columns) . "'], '$type']";
......@@ -248,6 +248,28 @@ class Generator extends \yii\gii\Generator
$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;
}
......@@ -552,4 +574,20 @@ class Generator extends \yii\gii\Generator
{
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
*/
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]]:
```php
......@@ -102,3 +104,8 @@ $models = $provider->getModels();
This extension supports [MongoGridFS](http://docs.mongodb.org/manual/core/gridfs/) via
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
- Bug #1446: Logging while logs are processed causes infinite loop (qiangxue)
- 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 #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 #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 #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 #1591: StringValidator is accessing undefined property (qiangxue)
- 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
- 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 #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 `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue)
- Enh: Support for file aliases in console command 'message' (omnilight)
- Enh: Sort and Pagination can now create absolute URLs (cebe)
- Chg #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 `ActiveRecord::getPopulatedRelations()` to `getRelatedRecords()` (qiangxue)
- Chg: Renamed `attributeName` and `className` to `targetAttribute` and `targetClass` for `UniqueValidator` and `ExistValidator` (qiangxue)
......
......@@ -48,6 +48,10 @@ class Captcha extends InputWidget
* while `{input}` will be replaced with the text input tag.
*/
public $template = '{image} {input}';
/**
* @var array the HTML attributes for the input tag.
*/
public $options = ['class' => 'form-control'];
/**
* Initializes the widget.
......
......@@ -366,12 +366,18 @@ class ActiveQuery extends Query implements ActiveQueryInterface
$parentTable = $this->getQueryTableName($parent);
$childTable = $this->getQueryTableName($child);
if (strpos($parentTable, '{{') === false) {
$parentTable = '{{' . $parentTable . '}}';
}
if (strpos($childTable, '{{') === false) {
$childTable = '{{' . $childTable . '}}';
}
if (!empty($child->link)) {
$on = [];
foreach ($child->link as $childColumn => $parentColumn) {
$on[] = '{{' . $parentTable . "}}.[[$parentColumn]] = {{" . $childTable . "}}.[[$childColumn]]";
$on[] = "$parentTable.[[$parentColumn]] = $childTable.[[$childColumn]]";
}
$on = implode(' AND ', $on);
} else {
......
......@@ -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.
* @param string $name the name of the attribute
* @return boolean whether the attribute has been changed
......@@ -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
*/
protected function updateInternal($attributes = null)
......
......@@ -122,7 +122,7 @@ class ActionColumn extends Column
*/
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];
if (isset($this->buttons[$name])) {
$url = $this->createUrl($name, $model, $key, $index);
......
......@@ -241,7 +241,7 @@ class BaseHtml
$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
{
if (isset($this->translations[$category])) {
$source = $this->translations[$category];
if ($source instanceof MessageSource) {
return $source;
} else {
return $this->translations[$category] = Yii::createObject($source);
}
} else {
// try wildcard matching
foreach ($this->translations as $pattern => $config) {
if ($pattern === '*' || substr($pattern, -1) === '*' && strpos($category, rtrim($pattern, '*')) === 0) {
$source = $config;
break;
if ($config instanceof MessageSource) {
return $config;
} else {
return $this->translations[$category] = $this->translations[$pattern] = Yii::createObject($config);
}
}
}
}
if (isset($source)) {
return $source instanceof MessageSource ? $source : Yii::createObject($source);
} else {
throw new InvalidConfigException("Unable to locate message source for category '$category'.");
}
throw new InvalidConfigException("Unable to locate message source for category '$category'.");
}
}
......@@ -105,7 +105,7 @@ class MessageSource extends Component
}
if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') {
return $this->_messages[$key][$message];
} elseif ($this->hasEventHandlers('missingTranslation')) {
} elseif ($this->hasEventHandlers(self::EVENT_MISSING_TRANSLATION)) {
$event = new MissingTranslationEvent([
'category' => $category,
'message' => $message,
......
......@@ -10,6 +10,7 @@ namespace yii\web;
use Yii;
use yii\base\InvalidConfigException;
use yii\helpers\Security;
use yii\helpers\StringHelper;
/**
* The web Request class represents an HTTP request
......@@ -83,6 +84,10 @@ class Request extends \yii\base\Request
* The name of the HTTP header for sending 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
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.
*/
......@@ -1040,7 +1082,7 @@ class Request extends \yii\base\Request
{
$options = $this->csrfCookie;
$options['name'] = $this->csrfVar;
$options['value'] = sha1(uniqid(mt_rand(), true));
$options['value'] = Security::generateRandomKey();
return new Cookie($options);
}
......@@ -1072,6 +1114,20 @@ class Request extends \yii\base\Request
$token = $this->getPost($this->csrfVar);
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
$request = Yii::$app->getRequest();
if ($request instanceof \yii\web\Request && $request->enableCsrfValidation) {
$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)) {
......
......@@ -671,6 +671,40 @@ trait ActiveRecordTestTrait
$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()
{
$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