diff --git a/CHANGELOG.md b/CHANGELOG.md index c7672b201..8fbe290cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ Yii Framework 2 mongodb extension Change Log ============================================ -2.1.10 under development +2.2.0 under development ------------------------ - Bug #308: Fix `yii\mongodb\file\Upload::addFile()` error when uploading file with readonly permissions (sparchatus) +- Enh #319: Added support for the 'session.use_strict_mode' ini directive in `yii\web\Session` (rhertogh) 2.1.9 November 19, 2019 diff --git a/UPGRADE.md b/UPGRADE.md index f59bcebf4..90efcd19b 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -8,7 +8,47 @@ if you want to upgrade from version A to version C and there is version B between A and C, you need to following the instructions for both A and B. -Upgrade from Yii 2.0.5 +Upgrade from 2.1.9 +---------------------- + +* `yii\mongodb\Session` now respects the 'session.use_strict_mode' ini directive. + In case you use a custom `Session` class and have overwritten the `Session::openSession()` and/or + `Session::writeSession()` functions changes might be required: + * When in strict mode the `openSession()` function should check if the requested session id exists + (and mark it for forced regeneration if not). + For example, the `yii\mongodb\Session` does this at the beginning of the function as follows: + ```php + if ($this->getUseStrictMode()) { + $id = $this->getId(); + $collection = $this->db->getCollection($this->sessionCollection); + $condition = [ + 'id' => $id, + 'expire' => ['$gt' => time()], + ]; + if (!$collection->documentExists($condition)) { + //This session id does not exist, mark it for forced regeneration + $this->_forceRegenerateId = $id; + } + } + // ... normal function continues ... + ``` + * When in strict mode the `writeSession()` function should ignore writing the session under the old id. + For example, the `yii\mongodb\Session` does this at the beginning of the function as follows: + ```php + if ($this->getUseStrictMode() && $id === $this->_forceRegenerateId) { + //Ignore write when forceRegenerate is active for this id + return true; + } + // ... normal function continues ... + ``` + > Note: In case your custom functions call their `parent` functions, there are probably no changes needed to your + code if those parents implement the `useStrictMode` checks. + + > Warning: in case `openSession()` and/or `writeSession()` functions do not implement the `useStrictMode` code + the session could be stored under the forced id without warning even if `useStrictMode` is enabled. + + +Upgrade from 2.0.5 ---------------------- * PHP [mongodb](http://php.net/manual/en/set.mongodb.php) extension is now used instead of [mongo](http://php.net/manual/en/book.mongo.php). @@ -38,13 +78,13 @@ Upgrade from Yii 2.0.5 * Cursor composed via `yii\mongodb\file\Collection::find()` now returns result in the same format as `yii\mongodb\file\Query::one()`. If you wish to perform file manipulations on returned row you should use `file` key instead of direct method invocations. -Upgrade from Yii 2.0.1 +Upgrade from 2.0.1 ---------------------- * MongoDB PHP extension min version raised up to 1.5.0. You should upgrade your environment in case you are using older version. -Upgrade from Yii 2.0.0 +Upgrade from 2.0.0 ---------------------- * MongoDB PHP extension min version raised up to 1.4.0. You should upgrade your environment in case you are diff --git a/composer.json b/composer.json index 859d1864d..bb961c854 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,9 @@ "email": "klimov.paul@gmail.com" } ], + "minimum-stability": "dev", "require": { - "yiisoft/yii2": "~2.0.14", + "yiisoft/yii2": "~2.0.39", "ext-mongodb": ">=1.0.0" }, "require-dev": { diff --git a/src/Collection.php b/src/Collection.php index c897fd4cb..7d39aaf49 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -241,6 +241,17 @@ public function findOne($condition = [], $fields = [], $options = []) return empty($rows) ? null : current($rows); } + /** + * Returns if a document exists. + * @param array $condition Query condition. + * @return bool + * @since 2.0.39 + */ + public function documentExists($condition = []) + { + return static::findOne($condition, ['_id' => 1]) !== null; + } + /** * Updates a document and returns it. * @param array $condition query condition diff --git a/src/Session.php b/src/Session.php index dc8ddc4fe..de365497b 100644 --- a/src/Session.php +++ b/src/Session.php @@ -74,6 +74,31 @@ public function init() $this->db = Instance::ensure($this->db, Connection::className()); } + /** + * Session open handler. + * @internal Do not call this method directly. + * @param string $savePath session save path + * @param string $sessionName session name + * @return bool whether session is opened successfully + */ + public function openSession($savePath, $sessionName) + { + if ($this->getUseStrictMode()) { + $id = $this->getId(); + $collection = $this->db->getCollection($this->sessionCollection); + $condition = [ + 'id' => $id, + 'expire' => ['$gt' => time()], + ]; + if (!$collection->documentExists($condition)) { + //This session id does not exist, mark it for forced regeneration + $this->_forceRegenerateId = $id; + } + } + + return parent::openSession($savePath, $sessionName); + } + /** * Updates the current session ID with a newly generated one. * Please refer to for more details. @@ -142,6 +167,11 @@ public function readSession($id) */ public function writeSession($id, $data) { + if ($this->getUseStrictMode() && $id === $this->_forceRegenerateId) { + //Ignore write when forceRegenerate is active for this id + return true; + } + // exception must be caught in session write handler // http://us.php.net/manual/en/function.session-set-save-handler.php try { @@ -211,4 +241,4 @@ public function gcSession($maxLifetime) return true; } -} \ No newline at end of file +} diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php index a18c5330d..fd1df344d 100644 --- a/tests/CollectionTest.php +++ b/tests/CollectionTest.php @@ -560,4 +560,22 @@ public function testDistinct() $this->assertFalse($rows === false); $this->assertCount(1, $rows); } + + /** + * @depends testInsert + * @depends testFindOne + */ + public function testDocumentExists() + { + $name = uniqid('customer_', true); + $collection = $this->getConnection()->getCollection('customer'); + $data = [ + 'name' => $name, + 'address' => 'test address', + ]; + $collection->insert($data); + + $this->assertTrue($collection->documentExists(['name' => $name])); + $this->assertFalse($collection->documentExists(['name' => 'non-existing'])); + } } diff --git a/tests/SessionTest.php b/tests/SessionTest.php index b1f8e5173..4f1baba0c 100755 --- a/tests/SessionTest.php +++ b/tests/SessionTest.php @@ -4,6 +4,7 @@ use yii\mongodb\Session; use Yii; +use yii\helpers\ArrayHelper; class SessionTest extends TestCase { @@ -22,13 +23,13 @@ protected function tearDown() * Creates test session instance. * @return Session session instance. */ - protected function createSession() + protected function createSession($config = []) { - return Yii::createObject([ + return Yii::createObject(ArrayHelper::merge([ 'class' => Session::className(), 'db' => $this->getConnection(), 'sessionCollection' => static::$sessionCollection, - ]); + ], $config)); } // Tests: @@ -157,4 +158,47 @@ public function testWriteCustomField() $this->assertEquals('session data', $rows[0]['data']); $this->assertEquals(15, $rows[0]['user_id']); } -} \ No newline at end of file + + /** + * @depends testWriteSession + * @runInSeparateProcess + */ + public function testStrictMode() + { + //non-strict-mode test + $nonStrictSession = $this->createSession([ + 'useStrictMode' => false, + ]); + $nonStrictSession->close(); + $nonStrictSession->destroySession('non-existing-non-strict'); + $nonStrictSession->setId('non-existing-non-strict'); + $nonStrictSession->open(); + $this->assertEquals('non-existing-non-strict', $nonStrictSession->getId()); + $nonStrictSession->close(); + + //strict-mode test + $strictSession = $this->createSession([ + 'useStrictMode' => true, + ]); + $strictSession->close(); + $strictSession->destroySession('non-existing-strict'); + $strictSession->setId('non-existing-strict'); + $strictSession->open(); + $id = $strictSession->getId(); + $this->assertNotEquals('non-existing-strict', $id); + $strictSession->set('strict_mode_test', 'session data'); + $strictSession->close(); + //Ensure session was not stored under forced id + $strictSession->setId('non-existing-strict'); + $strictSession->open(); + $this->assertNotEquals('session data', $strictSession->get('strict_mode_test')); + $strictSession->close(); + //Ensure session can be accessed with the new (and thus existing) id. + $strictSession->setId($id); + $strictSession->open(); + $this->assertNotEmpty($id); + $this->assertEquals($id, $strictSession->getId()); + $this->assertEquals('session data', $strictSession->get('strict_mode_test')); + $strictSession->close(); + } +}