From bd07bc1dcdc1291e5f11e8ab5f0810768c6a88c9 Mon Sep 17 00:00:00 2001 From: Luciano Baraglia Date: Thu, 26 Dec 2013 02:18:40 -0300 Subject: [PATCH 01/21] GII generates rules for unique indexes --- extensions/yii/gii/generators/model/Generator.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/extensions/yii/gii/generators/model/Generator.php b/extensions/yii/gii/generators/model/Generator.php index 591cb3b..bcc2ea2 100644 --- a/extensions/yii/gii/generators/model/Generator.php +++ b/extensions/yii/gii/generators/model/Generator.php @@ -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. @@ -247,7 +248,24 @@ class Generator extends \yii\gii\Generator foreach ($lengths as $length => $columns) { $rules[] = "[['" . implode("', '", $columns) . "'], 'string', 'max' => $length]"; } - + // Unique indexes rules + try { + $db = $this->getDbConnection(); + $uniqueIndexes = $db->getSchema()->findUniqueIndexes($table); + foreach ($uniqueIndexes as $indexName => $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; } From bdafb4becb1df20c459ad3d3fe4476d397b6c018 Mon Sep 17 00:00:00 2001 From: Luciano Baraglia Date: Thu, 26 Dec 2013 02:43:35 -0300 Subject: [PATCH 02/21] Unique indexes rules for single columns into array --- extensions/yii/gii/generators/model/Generator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/yii/gii/generators/model/Generator.php b/extensions/yii/gii/generators/model/Generator.php index bcc2ea2..0a00610 100644 --- a/extensions/yii/gii/generators/model/Generator.php +++ b/extensions/yii/gii/generators/model/Generator.php @@ -255,7 +255,7 @@ class Generator extends \yii\gii\Generator foreach ($uniqueIndexes as $indexName => $uniqueColumns) { $attributesCount = count($uniqueColumns); if ($attributesCount == 1) { - $rules[] = "['" . $uniqueColumns[0] . "', 'unique']"; + $rules[] = "[['" . $uniqueColumns[0] . "'], 'unique']"; } elseif ($attributesCount > 1) { $labels = array_intersect_key($this->generateLabels($table), array_flip($uniqueColumns)); $lastLabel = array_pop($labels); From 2d3ac6b4e24d3c3f85bb6777bc064d51edc3e208 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 26 Dec 2013 09:29:46 -0500 Subject: [PATCH 03/21] Fixes #1638: prevent table names from being enclosed within curly brackets twice. --- framework/yii/db/ActiveQuery.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index 26b0c6e..a9a1b19 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -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 { From d5fccf672d37171710c07a3d69ff090dcd8e0b54 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 26 Dec 2013 15:32:26 +0100 Subject: [PATCH 04/21] Number validator was missing --- docs/guide/validation.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/guide/validation.md b/docs/guide/validation.md index 5067cb6..66637c8 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -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. From f77e3b4bba66b26113ede35301798b066a5bdd0a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 26 Dec 2013 09:46:02 -0500 Subject: [PATCH 05/21] Fixes #1611: Added `BaseActiveRecord::markAttributeDirty()` --- framework/CHANGELOG.md | 1 + framework/yii/db/BaseActiveRecord.php | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index e2a18ff..ce15891 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -29,6 +29,7 @@ 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: 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) diff --git a/framework/yii/db/BaseActiveRecord.php b/framework/yii/db/BaseActiveRecord.php index e20501b..4db9d5f 100644 --- a/framework/yii/db/BaseActiveRecord.php +++ b/framework/yii/db/BaseActiveRecord.php @@ -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 From dcee382e762e1ed8bef1083fa2a449252fb57027 Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Thu, 26 Dec 2013 17:39:48 +0200 Subject: [PATCH 06/21] Mongo README.md updated. --- extensions/yii/mongodb/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/extensions/yii/mongodb/README.md b/extensions/yii/mongodb/README.md index d56e030..28e110d 100644 --- a/extensions/yii/mongodb/README.md +++ b/extensions/yii/mongodb/README.md @@ -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 From 68cb074c6aece594521b4508b09fbc02fc41d7f1 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 26 Dec 2013 10:55:36 -0500 Subject: [PATCH 07/21] Fixed #1504: Debug toolbar isn't loaded successfully in some environments when xdebug is enabled --- extensions/yii/debug/CHANGELOG.md | 2 +- .../yii/debug/controllers/DefaultController.php | 45 +++++++++++++--------- framework/CHANGELOG.md | 1 + 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/extensions/yii/debug/CHANGELOG.md b/extensions/yii/debug/CHANGELOG.md index 1f83ca4..21bbcfa 100644 --- a/extensions/yii/debug/CHANGELOG.md +++ b/extensions/yii/debug/CHANGELOG.md @@ -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 ----------------------------- diff --git a/extensions/yii/debug/controllers/DefaultController.php b/extensions/yii/debug/controllers/DefaultController.php index 0c8d6e9..5c3c444 100644 --- a/extensions/yii/debug/controllers/DefaultController.php +++ b/extensions/yii/debug/controllers/DefaultController.php @@ -64,7 +64,7 @@ class DefaultController extends Controller public function actionToolbar($tag) { - $this->loadData($tag); + $this->loadData($tag, 3); 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,30 @@ 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'."); } + + throw new NotFoundHttpException("Unable to find debug data tagged with '$tag'."); } } diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index ce15891..e8e324b 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -7,6 +7,7 @@ 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. From 7eccd9d926ea6ff96a82825f5ae831a80e727c97 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 26 Dec 2013 12:31:13 -0500 Subject: [PATCH 08/21] Fixes #1641: Added `BaseActiveRecord::updateAttributes()` --- framework/CHANGELOG.md | 1 + framework/yii/db/BaseActiveRecord.php | 31 ++++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index e8e324b..152b3e9 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -31,6 +31,7 @@ Yii Framework 2 Change Log - 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 #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) diff --git a/framework/yii/db/BaseActiveRecord.php b/framework/yii/db/BaseActiveRecord.php index 4db9d5f..9dbcf99 100644 --- a/framework/yii/db/BaseActiveRecord.php +++ b/framework/yii/db/BaseActiveRecord.php @@ -637,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) From 7654fff26d8c058e01f1c06e3210d9c5cb41e401 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 26 Dec 2013 12:39:38 -0500 Subject: [PATCH 09/21] Added unit test for ActiveRecord::updateAttributes(). --- tests/unit/framework/ar/ActiveRecordTestTrait.php | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/unit/framework/ar/ActiveRecordTestTrait.php b/tests/unit/framework/ar/ActiveRecordTestTrait.php index 95def9d..43cb52a 100644 --- a/tests/unit/framework/ar/ActiveRecordTestTrait.php +++ b/tests/unit/framework/ar/ActiveRecordTestTrait.php @@ -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(); From 85abc2c70b33012dad962e869987af7ac83f0476 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 26 Dec 2013 13:09:42 -0500 Subject: [PATCH 10/21] Added sleep(). --- extensions/yii/debug/controllers/DefaultController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/yii/debug/controllers/DefaultController.php b/extensions/yii/debug/controllers/DefaultController.php index 5c3c444..f51bbd6 100644 --- a/extensions/yii/debug/controllers/DefaultController.php +++ b/extensions/yii/debug/controllers/DefaultController.php @@ -116,6 +116,7 @@ class DefaultController extends Controller $this->summary = $data['summary']; return; } + sleep(2); } throw new NotFoundHttpException("Unable to find debug data tagged with '$tag'."); From c1db25e3e0cc9ffb8b8487c9dcad337685fbe544 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 26 Dec 2013 13:15:10 -0500 Subject: [PATCH 11/21] updated debug retry params. --- extensions/yii/debug/controllers/DefaultController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/yii/debug/controllers/DefaultController.php b/extensions/yii/debug/controllers/DefaultController.php index f51bbd6..4d525f7 100644 --- a/extensions/yii/debug/controllers/DefaultController.php +++ b/extensions/yii/debug/controllers/DefaultController.php @@ -64,7 +64,7 @@ class DefaultController extends Controller public function actionToolbar($tag) { - $this->loadData($tag, 3); + $this->loadData($tag, 5); return $this->renderPartial('toolbar', [ 'tag' => $tag, 'panels' => $this->module->panels, @@ -116,7 +116,7 @@ class DefaultController extends Controller $this->summary = $data['summary']; return; } - sleep(2); + sleep(1); } throw new NotFoundHttpException("Unable to find debug data tagged with '$tag'."); From b1fc13a31ceadc9317d334ca8c37bb11e9224cae Mon Sep 17 00:00:00 2001 From: Luciano Baraglia Date: Thu, 26 Dec 2013 16:42:26 -0300 Subject: [PATCH 12/21] GII unique indexes avoid autoIncrement columns --- extensions/yii/gii/generators/model/Generator.php | 38 +++++++++++++++++------ 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/extensions/yii/gii/generators/model/Generator.php b/extensions/yii/gii/generators/model/Generator.php index 0a00610..785d311 100644 --- a/extensions/yii/gii/generators/model/Generator.php +++ b/extensions/yii/gii/generators/model/Generator.php @@ -240,7 +240,6 @@ class Generator extends \yii\gii\Generator } } } - $rules = []; foreach ($types as $type => $columns) { $rules[] = "[['" . implode("', '", $columns) . "'], '$type']"; @@ -248,19 +247,24 @@ class Generator extends \yii\gii\Generator foreach ($lengths as $length => $columns) { $rules[] = "[['" . implode("', '", $columns) . "'], 'string', 'max' => $length]"; } + // Unique indexes rules try { $db = $this->getDbConnection(); $uniqueIndexes = $db->getSchema()->findUniqueIndexes($table); foreach ($uniqueIndexes as $indexName => $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.']"; + // 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) { @@ -570,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; + } } From 2686403c0e3d4c431eb24006a678eafe874a85c5 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 26 Dec 2013 16:19:09 -0500 Subject: [PATCH 13/21] Use better random CSRF token. --- framework/yii/web/Request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/web/Request.php b/framework/yii/web/Request.php index 9736043..8849ed3 100644 --- a/framework/yii/web/Request.php +++ b/framework/yii/web/Request.php @@ -1040,7 +1040,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); } From c8960168c52bff34d2879b1c585f3e48ecfc8ffc Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 26 Dec 2013 17:51:14 -0500 Subject: [PATCH 14/21] Fixes #1634: Use masked CSRF tokens to prevent BREACH exploits --- framework/CHANGELOG.md | 1 + framework/yii/helpers/BaseHtml.php | 2 +- framework/yii/web/Request.php | 58 +++++++++++++++++++++++++++++++++++++- framework/yii/web/View.php | 2 +- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 152b3e9..6bebff1 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -31,6 +31,7 @@ Yii Framework 2 Change Log - 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) diff --git a/framework/yii/helpers/BaseHtml.php b/framework/yii/helpers/BaseHtml.php index 49fe832..b3a88c1 100644 --- a/framework/yii/helpers/BaseHtml.php +++ b/framework/yii/helpers/BaseHtml.php @@ -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()); } } diff --git a/framework/yii/web/Request.php b/framework/yii/web/Request.php index 8849ed3..aae2e3c 100644 --- a/framework/yii/web/Request.php +++ b/framework/yii/web/Request.php @@ -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. */ @@ -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; } } diff --git a/framework/yii/web/View.php b/framework/yii/web/View.php index 790e4fd..f29a1e4 100644 --- a/framework/yii/web/View.php +++ b/framework/yii/web/View.php @@ -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)) { From 5a8517f194ced987b4c913ed4326e67f9a6f740e Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 26 Dec 2013 22:16:18 -0500 Subject: [PATCH 15/21] minor doc fix. --- extensions/yii/authclient/README.md | 10 +++++----- extensions/yii/authclient/widgets/Choice.php | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/yii/authclient/README.md b/extensions/yii/authclient/README.md index 5aff122..ea7f217 100644 --- a/extensions/yii/authclient/README.md +++ b/extensions/yii/authclient/README.md @@ -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: ``` ['site/auth'] -]); ?> -``` \ No newline at end of file +]) ?> +``` diff --git a/extensions/yii/authclient/widgets/Choice.php b/extensions/yii/authclient/widgets/Choice.php index fe20735..336ca80 100644 --- a/extensions/yii/authclient/widgets/Choice.php +++ b/extensions/yii/authclient/widgets/Choice.php @@ -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 +} From 6eb6e226309c8bfc6bc7dec7bdecd5fb1fe1354b Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 26 Dec 2013 22:22:26 -0500 Subject: [PATCH 16/21] fixed composer.json --- composer.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composer.json b/composer.json index b468a43..3af7dd3 100644 --- a/composer.json +++ b/composer.json @@ -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/", From a82281a8f9a16f0a23352b23037e7169561aa8b9 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 27 Dec 2013 00:49:26 -0500 Subject: [PATCH 17/21] fixed functional test when enablePrettyUrl is false. --- apps/basic/config/codeception/functional.php | 6 ++++++ apps/basic/tests/functional/_bootstrap.php | 8 ++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/basic/config/codeception/functional.php b/apps/basic/config/codeception/functional.php index 760dcef..210b97d 100644 --- a/apps/basic/config/codeception/functional.php +++ b/apps/basic/config/codeception/functional.php @@ -7,5 +7,11 @@ return [ 'db' => [ 'dsn' => 'mysql:host=localhost;dbname=yii2basic_functional', ], + 'request' => [ + 'enableCsrfValidation' => false, + ], + 'urlManager' => [ + 'baseUrl' => '/web/index.php', + ], ], ]; diff --git a/apps/basic/tests/functional/_bootstrap.php b/apps/basic/tests/functional/_bootstrap.php index 6a117fd..6692104 100644 --- a/apps/basic/tests/functional/_bootstrap.php +++ b/apps/basic/tests/functional/_bootstrap.php @@ -1,8 +1,4 @@ Date: Fri, 27 Dec 2013 09:12:58 -0500 Subject: [PATCH 18/21] Added SecurityTest. --- tests/unit/framework/helpers/SecurityTest.php | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/unit/framework/helpers/SecurityTest.php diff --git a/tests/unit/framework/helpers/SecurityTest.php b/tests/unit/framework/helpers/SecurityTest.php new file mode 100644 index 0000000..6a1d2fd --- /dev/null +++ b/tests/unit/framework/helpers/SecurityTest.php @@ -0,0 +1,43 @@ +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); + } +} From b10c8240b346dd2e7436e0d8e1269077ed4f33ff Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 27 Dec 2013 09:19:27 -0500 Subject: [PATCH 19/21] =?UTF-8?q?Allow=20dash=20char=20in=20ActionColumn?= =?UTF-8?q?=E2=80=99s=20button=20names.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- framework/yii/grid/ActionColumn.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/grid/ActionColumn.php b/framework/yii/grid/ActionColumn.php index e97f535..26ed1c3 100644 --- a/framework/yii/grid/ActionColumn.php +++ b/framework/yii/grid/ActionColumn.php @@ -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); From 5e8a48a60a9fc3b855a8fac730044cc2dee15056 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 27 Dec 2013 09:32:17 -0500 Subject: [PATCH 20/21] Fixes #1654: Fixed the issue that a new message source object is generated for every new message being translated --- framework/CHANGELOG.md | 1 + framework/yii/i18n/I18N.php | 19 ++++++++++++------- framework/yii/i18n/MessageSource.php | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 6bebff1..6991525 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -11,6 +11,7 @@ Yii Framework 2 Change Log - 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) diff --git a/framework/yii/i18n/I18N.php b/framework/yii/i18n/I18N.php index c59a6d2..3955e84 100644 --- a/framework/yii/i18n/I18N.php +++ b/framework/yii/i18n/I18N.php @@ -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'."); } } diff --git a/framework/yii/i18n/MessageSource.php b/framework/yii/i18n/MessageSource.php index 95f907d..07871bb 100644 --- a/framework/yii/i18n/MessageSource.php +++ b/framework/yii/i18n/MessageSource.php @@ -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, From 4148912b82da1dd00c1ac34ce27abab5ce6348f4 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 27 Dec 2013 10:46:13 -0500 Subject: [PATCH 21/21] Fixes #1643: Added default value for `Captcha::options` --- apps/advanced/frontend/views/site/contact.php | 1 - apps/basic/views/site/contact.php | 1 - framework/CHANGELOG.md | 1 + framework/yii/captcha/Captcha.php | 4 ++++ 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/advanced/frontend/views/site/contact.php b/apps/advanced/frontend/views/site/contact.php index 17c4f79..9aa98e6 100644 --- a/apps/advanced/frontend/views/site/contact.php +++ b/apps/advanced/frontend/views/site/contact.php @@ -26,7 +26,6 @@ $this->params['breadcrumbs'][] = $this->title; field($model, 'subject') ?> field($model, 'body')->textArea(['rows' => 6]) ?> field($model, 'verifyCode')->widget(Captcha::className(), [ - 'options' => ['class' => 'form-control'], 'template' => '
{image}
{input}
', ]) ?>
diff --git a/apps/basic/views/site/contact.php b/apps/basic/views/site/contact.php index d2e59af..ebd148e 100644 --- a/apps/basic/views/site/contact.php +++ b/apps/basic/views/site/contact.php @@ -34,7 +34,6 @@ $this->params['breadcrumbs'][] = $this->title; field($model, 'subject') ?> field($model, 'body')->textArea(['rows' => 6]) ?> field($model, 'verifyCode')->widget(Captcha::className(), [ - 'options' => ['class' => 'form-control'], 'template' => '
{image}
{input}
', ]) ?>
diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 6991525..747e59d 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -39,6 +39,7 @@ Yii Framework 2 Change Log - 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) diff --git a/framework/yii/captcha/Captcha.php b/framework/yii/captcha/Captcha.php index 18b8765..9e69b9e 100644 --- a/framework/yii/captcha/Captcha.php +++ b/framework/yii/captcha/Captcha.php @@ -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.