diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 4243face3d9..a2b6f351e79 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -4,6 +4,7 @@ Yii Framework 2 Change Log 2.0.17 under development ------------------------ +- Bug #9438, #13740, #15037: Handle DB session callback custom fields before session closed (lubosdz) - Bug #16681: `ActiveField::inputOptions` were not used during some widgets rendering (GHopperMSK) - Bug #17133: Fixed aliases rendering during help generation for a console command (GHopperMSK) - Bug #17185: Fixed `AssetManager` timestamp appending when a file is published manually (GHopperMSK) diff --git a/framework/web/DbSession.php b/framework/web/DbSession.php index 7bd63e98bd6..b00df3be049 100644 --- a/framework/web/DbSession.php +++ b/framework/web/DbSession.php @@ -76,6 +76,11 @@ class DbSession extends MultiFieldSession */ public $sessionTable = '{{%session}}'; + /** + * @var array Session fields to be written into session table columns + * @since 2.0.17 + */ + protected $fields = []; /** * Initializes the DbSession component. @@ -136,6 +141,19 @@ public function regenerateID($deleteOldSession = false) } } + /** + * Ends the current session and store session data. + * @since 2.0.17 + */ + public function close() + { + if ($this->getIsActive()) { + // prepare writeCallback fields before session closes + $this->fields = $this->composeFields(); + YII_DEBUG ? session_write_close() : @session_write_close(); + } + } + /** * Session read handler. * @internal Do not call this method directly. @@ -169,14 +187,28 @@ public function writeSession($id, $data) // exception must be caught in session write handler // https://secure.php.net/manual/en/function.session-set-save-handler.php#refsect1-function.session-set-save-handler-notes try { - $fields = $this->composeFields($id, $data); - $fields = $this->typecastFields($fields); - $this->db->createCommand()->upsert($this->sessionTable, $fields)->execute(); + // ensure backwards compatability (fixed #9438) + if ($this->writeCallback && !$this->fields) { + $this->fields = $this->composeFields(); + } + // ensure data consistency + if (!isset($this->fields['data'])) { + $this->fields['data'] = $data; + } else { + $_SESSION = $this->fields['data']; + } + // ensure 'id' and 'expire' are never affected by [[writeCallback]] + $this->fields = array_merge($this->fields, [ + 'id' => $id, + 'expire' => time() + $this->getTimeout(), + ]); + $this->fields = $this->typecastFields($this->fields); + $this->db->createCommand()->upsert($this->sessionTable, $this->fields)->execute(); + $this->fields = []; } catch (\Exception $e) { Yii::$app->errorHandler->handleException($e); return false; } - return true; } diff --git a/framework/web/MultiFieldSession.php b/framework/web/MultiFieldSession.php index 56ff24037f0..cd26d120aca 100644 --- a/framework/web/MultiFieldSession.php +++ b/framework/web/MultiFieldSession.php @@ -89,30 +89,19 @@ public function getUseCustomStorage() /** * Composes storage field set for session writing. - * @param string $id session id - * @param string $data session data + * @param string $id Optional session id + * @param string $data Optional session data * @return array storage fields */ - protected function composeFields($id, $data) + protected function composeFields($id = null, $data = null) { - $fields = [ - 'data' => $data, - ]; - if ($this->writeCallback !== null) { - $fields = array_merge( - $fields, - call_user_func($this->writeCallback, $this) - ); - if (!is_string($fields['data'])) { - $_SESSION = $fields['data']; - $fields['data'] = session_encode(); - } + $fields = $this->writeCallback ? call_user_func($this->writeCallback, $this) : []; + if ($id !== null) { + $fields['id'] = $id; + } + if ($data !== null) { + $fields['data'] = $data; } - // ensure 'id' and 'expire' are never affected by [[writeCallback]] - $fields = array_merge($fields, [ - 'id' => $id, - 'expire' => time() + $this->getTimeout(), - ]); return $fields; } diff --git a/tests/framework/web/session/AbstractDbSessionTest.php b/tests/framework/web/session/AbstractDbSessionTest.php index a215e81d0ee..a2a4375c296 100644 --- a/tests/framework/web/session/AbstractDbSessionTest.php +++ b/tests/framework/web/session/AbstractDbSessionTest.php @@ -10,6 +10,7 @@ use Yii; use yii\db\Connection; use yii\db\Query; +use yii\db\Migration; use yii\web\DbSession; use yiiunit\framework\console\controllers\EchoMigrateController; use yiiunit\TestCase; @@ -147,6 +148,33 @@ public function testWriteCustomField() $this->assertSame('changed by callback data', $session->readSession('test')); } + /** + * @depends testReadWrite + */ + public function testWriteCustomFieldWithUserId() + { + $session = new DbSession(); + $session->open(); + $session->set('user_id', 12345); + + // add mapped custom column + $migration = new Migration; + $migration->addColumn($session->sessionTable, 'user_id', $migration->integer()); + + $session->writeCallback = function ($session) { + return ['user_id' => $session['user_id']]; + }; + + // here used to be error, fixed issue #9438 + $session->close(); + + // reopen & read session from DB + $session->open(); + $loadedUserId = empty($session['user_id']) ? null : $session['user_id']; + $this->assertSame($loadedUserId, 12345); + $session->close(); + } + protected function buildObjectForSerialization() { $object = new \stdClass();