From 3a462213bb4fd46efa58a1937b06b1b0553b67a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 27 Aug 2026 16:27:33 +0200 Subject: [PATCH 1/5] PHPLIB-1927: Reject "." and NUL bytes in database and collection names Database and collection names were only checked for being non-empty. A "." in a database name, or a NUL byte in a database or collection name, shifts the namespace split performed by the server, so operations end up targeting a different database or collection than the caller intended. Reject "." and NUL bytes in database names, and NUL bytes in collection names. Dots remain legal in collection names. Add a create_namespace() helper that validates both names and returns the concatenated namespace, and use it in Collection, Database, and all Operation classes that build a namespace by concatenating database and collection names (BulkWrite, Delete, Find, InsertMany, InsertOne, RenameCollection, Update). Move the shared invalid name data provider to the base TestCase, and add prose test 17 for database and collection name validation. --- src/Collection.php | 5 +- src/Database.php | 3 +- src/Operation/BulkWrite.php | 9 +++- src/Operation/Delete.php | 9 +++- src/Operation/Find.php | 9 +++- src/Operation/InsertMany.php | 9 +++- src/Operation/InsertOne.php | 9 +++- src/Operation/RenameCollection.php | 5 +- src/Operation/Update.php | 9 +++- src/functions.php | 25 ++++++++++ tests/Collection/CollectionFunctionalTest.php | 24 +++++++-- tests/Database/DatabaseFunctionalTest.php | 2 + tests/Operation/BulkWriteTest.php | 9 ++++ tests/Operation/DeleteTest.php | 7 +++ tests/Operation/FindTest.php | 7 +++ tests/Operation/InsertManyTest.php | 7 +++ tests/Operation/InsertOneTest.php | 7 +++ tests/Operation/RenameCollectionTest.php | 19 +++++++ tests/Operation/UpdateTest.php | 7 +++ ...atabaseAndCollectionNameValidationTest.php | 49 +++++++++++++++++++ tests/TestCase.php | 9 ++++ 21 files changed, 219 insertions(+), 20 deletions(-) create mode 100644 tests/SpecTests/Crud/Prose17_DatabaseAndCollectionNameValidationTest.php diff --git a/src/Collection.php b/src/Collection.php index f9bfba63c..a079e3137 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -79,6 +79,7 @@ use function is_array; use function is_bool; use function sprintf; +use function str_contains; use function strlen; use function trigger_error; @@ -144,11 +145,11 @@ class Collection */ public function __construct(private Manager $manager, private string $databaseName, private string $collectionName, array $options = []) { - if (strlen($databaseName) < 1) { + if (strlen($databaseName) < 1 || str_contains($databaseName, '.') || str_contains($databaseName, "\0")) { throw new InvalidArgumentException('$databaseName is invalid: ' . $databaseName); } - if (strlen($collectionName) < 1) { + if (strlen($collectionName) < 1 || str_contains($collectionName, "\0")) { throw new InvalidArgumentException('$collectionName is invalid: ' . $collectionName); } diff --git a/src/Database.php b/src/Database.php index fbba0661c..7f0a98e4c 100644 --- a/src/Database.php +++ b/src/Database.php @@ -57,6 +57,7 @@ use function is_array; use function is_bool; use function sprintf; +use function str_contains; use function strlen; use function trigger_error; @@ -117,7 +118,7 @@ class Database */ public function __construct(private Manager $manager, private string $databaseName, array $options = []) { - if (strlen($databaseName) < 1) { + if (strlen($databaseName) < 1 || str_contains($databaseName, '.') || str_contains($databaseName, "\0")) { throw new InvalidArgumentException('$databaseName is invalid: ' . $databaseName); } diff --git a/src/Operation/BulkWrite.php b/src/Operation/BulkWrite.php index c8f695675..7908cf696 100644 --- a/src/Operation/BulkWrite.php +++ b/src/Operation/BulkWrite.php @@ -36,6 +36,7 @@ use function is_array; use function is_bool; use function key; +use function MongoDB\create_namespace; use function MongoDB\is_document; use function MongoDB\is_first_key_operator; use function MongoDB\is_pipeline; @@ -62,6 +63,8 @@ class BulkWrite implements Executable private array $options; + private string $namespace; + /** * Constructs a bulk write operation. * @@ -140,8 +143,10 @@ class BulkWrite implements Executable * @param array $options Command options * @throws InvalidArgumentException for parameter/option parsing errors */ - public function __construct(private string $databaseName, private string $collectionName, array $operations, array $options = []) + public function __construct(string $databaseName, string $collectionName, array $operations, array $options = []) { + $this->namespace = create_namespace($databaseName, $collectionName); + if (empty($operations)) { throw new InvalidArgumentException('$operations is empty'); } @@ -232,7 +237,7 @@ public function execute(Server $server) } } - $writeResult = $server->executeBulkWrite($this->databaseName . '.' . $this->collectionName, $bulk, $this->createExecuteOptions()); + $writeResult = $server->executeBulkWrite($this->namespace, $bulk, $this->createExecuteOptions()); return new BulkWriteResult($writeResult, $insertedIds); } diff --git a/src/Operation/Delete.php b/src/Operation/Delete.php index c5826a6d9..d37cb6cda 100644 --- a/src/Operation/Delete.php +++ b/src/Operation/Delete.php @@ -27,6 +27,7 @@ use MongoDB\Exception\UnsupportedException; use function is_string; +use function MongoDB\create_namespace; use function MongoDB\is_document; use function MongoDB\is_write_concern_acknowledged; use function MongoDB\server_supports_feature; @@ -44,6 +45,8 @@ class Delete implements Executable, Explainable { private const WIRE_VERSION_FOR_HINT = 9; + private string $namespace; + /** * Constructs a delete command. * @@ -80,8 +83,10 @@ class Delete implements Executable, Explainable * @param array $options Command options * @throws InvalidArgumentException for parameter/option parsing errors */ - public function __construct(private string $databaseName, private string $collectionName, private array|object $filter, private int $limit, private array $options = []) + public function __construct(string $databaseName, private string $collectionName, private array|object $filter, private int $limit, private array $options = []) { + $this->namespace = create_namespace($databaseName, $collectionName); + if (! is_document($filter)) { throw InvalidArgumentException::expectedDocumentType('$filter', $filter); } @@ -142,7 +147,7 @@ public function execute(Server $server) $bulk = new Bulk($this->createBulkWriteOptions()); $bulk->delete($this->filter, $this->createDeleteOptions()); - $writeResult = $server->executeBulkWrite($this->databaseName . '.' . $this->collectionName, $bulk, $this->createExecuteOptions()); + $writeResult = $server->executeBulkWrite($this->namespace, $bulk, $this->createExecuteOptions()); return new DeleteResult($writeResult); } diff --git a/src/Operation/Find.php b/src/Operation/Find.php index a4a010010..58fdf9aaf 100644 --- a/src/Operation/Find.php +++ b/src/Operation/Find.php @@ -36,6 +36,7 @@ use function is_integer; use function is_object; use function is_string; +use function MongoDB\create_namespace; use function MongoDB\document_to_array; use function MongoDB\is_document; @@ -54,6 +55,8 @@ class Find implements Executable, Explainable public const TAILABLE = 2; public const TAILABLE_AWAIT = 3; + private string $namespace; + /** * Constructs a find command. * @@ -155,8 +158,10 @@ class Find implements Executable, Explainable * @param array $options Command options * @throws InvalidArgumentException for parameter/option parsing errors */ - public function __construct(private string $databaseName, private string $collectionName, private array|object $filter, private array $options = []) + public function __construct(string $databaseName, private string $collectionName, private array|object $filter, private array $options = []) { + $this->namespace = create_namespace($databaseName, $collectionName); + if (! is_document($filter)) { throw InvalidArgumentException::expectedDocumentType('$filter', $filter); } @@ -303,7 +308,7 @@ public function execute(Server $server) throw UnsupportedException::readConcernNotSupportedInTransaction(); } - $cursor = $server->executeQuery($this->databaseName . '.' . $this->collectionName, new Query($this->filter, $this->createQueryOptions()), $this->createExecuteOptions()); + $cursor = $server->executeQuery($this->namespace, new Query($this->filter, $this->createQueryOptions()), $this->createExecuteOptions()); if (isset($this->options['codec'])) { return CodecCursor::fromCursor($cursor, $this->options['codec']); diff --git a/src/Operation/InsertMany.php b/src/Operation/InsertMany.php index 5e227cbc2..2cd95f70c 100644 --- a/src/Operation/InsertMany.php +++ b/src/Operation/InsertMany.php @@ -29,6 +29,7 @@ use function array_is_list; use function is_bool; +use function MongoDB\create_namespace; use function MongoDB\is_document; use function sprintf; @@ -47,6 +48,8 @@ class InsertMany implements Executable private array $options; + private string $namespace; + /** * Constructs an insert command. * @@ -77,8 +80,10 @@ class InsertMany implements Executable * @param array $options Command options * @throws InvalidArgumentException for parameter/option parsing errors */ - public function __construct(private string $databaseName, private string $collectionName, array $documents, array $options = []) + public function __construct(string $databaseName, string $collectionName, array $documents, array $options = []) { + $this->namespace = create_namespace($databaseName, $collectionName); + $options += ['ordered' => true]; if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) { @@ -135,7 +140,7 @@ public function execute(Server $server) $insertedIds[$i] = $bulk->insert($document); } - $writeResult = $server->executeBulkWrite($this->databaseName . '.' . $this->collectionName, $bulk, $this->createExecuteOptions()); + $writeResult = $server->executeBulkWrite($this->namespace, $bulk, $this->createExecuteOptions()); return new InsertManyResult($writeResult, $insertedIds); } diff --git a/src/Operation/InsertOne.php b/src/Operation/InsertOne.php index 6d8f58cd8..bbf57666e 100644 --- a/src/Operation/InsertOne.php +++ b/src/Operation/InsertOne.php @@ -28,6 +28,7 @@ use MongoDB\InsertOneResult; use function is_bool; +use function MongoDB\create_namespace; use function MongoDB\is_document; /** @@ -42,6 +43,8 @@ class InsertOne implements Executable { private array|object $document; + private string $namespace; + /** * Constructs an insert command. * @@ -67,8 +70,10 @@ class InsertOne implements Executable * @param array $options Command options * @throws InvalidArgumentException for parameter/option parsing errors */ - public function __construct(private string $databaseName, private string $collectionName, array|object $document, private array $options = []) + public function __construct(string $databaseName, string $collectionName, array|object $document, private array $options = []) { + $this->namespace = create_namespace($databaseName, $collectionName); + if (isset($this->options['bypassDocumentValidation']) && ! is_bool($this->options['bypassDocumentValidation'])) { throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $this->options['bypassDocumentValidation'], 'boolean'); } @@ -115,7 +120,7 @@ public function execute(Server $server) $insertedId = $bulk->insert($this->document); - $writeResult = $server->executeBulkWrite($this->databaseName . '.' . $this->collectionName, $bulk, $this->createExecuteOptions()); + $writeResult = $server->executeBulkWrite($this->namespace, $bulk, $this->createExecuteOptions()); return new InsertOneResult($writeResult, $insertedId); } diff --git a/src/Operation/RenameCollection.php b/src/Operation/RenameCollection.php index 245231a13..90bcd8c29 100644 --- a/src/Operation/RenameCollection.php +++ b/src/Operation/RenameCollection.php @@ -28,6 +28,7 @@ use function current; use function is_array; use function is_bool; +use function MongoDB\create_namespace; /** * Operation for the renameCollection command. @@ -92,8 +93,8 @@ public function __construct(string $fromDatabaseName, string $fromCollectionName throw InvalidArgumentException::invalidType('"dropTarget" option', $this->options['dropTarget'], 'boolean'); } - $this->fromNamespace = $fromDatabaseName . '.' . $fromCollectionName; - $this->toNamespace = $toDatabaseName . '.' . $toCollectionName; + $this->fromNamespace = create_namespace($fromDatabaseName, $fromCollectionName); + $this->toNamespace = create_namespace($toDatabaseName, $toCollectionName); } /** diff --git a/src/Operation/Update.php b/src/Operation/Update.php index 2016d89a8..0f85cee9e 100644 --- a/src/Operation/Update.php +++ b/src/Operation/Update.php @@ -29,6 +29,7 @@ use function is_array; use function is_bool; use function is_string; +use function MongoDB\create_namespace; use function MongoDB\is_document; use function MongoDB\is_first_key_operator; use function MongoDB\is_pipeline; @@ -50,6 +51,8 @@ class Update implements Executable, Explainable private array $options; + private string $namespace; + /** * Constructs a update command. * @@ -98,8 +101,10 @@ class Update implements Executable, Explainable * @param array $options Command options * @throws InvalidArgumentException for parameter/option parsing errors */ - public function __construct(private string $databaseName, private string $collectionName, private array|object $filter, private array|object $update, array $options = []) + public function __construct(string $databaseName, private string $collectionName, private array|object $filter, private array|object $update, array $options = []) { + $this->namespace = create_namespace($databaseName, $collectionName); + if (! is_document($filter)) { throw InvalidArgumentException::expectedDocumentType('$filter', $filter); } @@ -195,7 +200,7 @@ public function execute(Server $server) $bulk = new Bulk($this->createBulkWriteOptions()); $bulk->update($this->filter, $this->update, $this->createUpdateOptions()); - $writeResult = $server->executeBulkWrite($this->databaseName . '.' . $this->collectionName, $bulk, $this->createExecuteOptions()); + $writeResult = $server->executeBulkWrite($this->namespace, $bulk, $this->createExecuteOptions()); return new UpdateResult($writeResult); } diff --git a/src/functions.php b/src/functions.php index b38fc1fae..468b8a37c 100644 --- a/src/functions.php +++ b/src/functions.php @@ -45,6 +45,7 @@ use function is_array; use function is_object; use function is_string; +use function str_contains; use function str_ends_with; use function substr; @@ -456,6 +457,30 @@ function is_string_array(mixed $input): bool return true; } +/** + * Validates a database and collection name and returns the namespace formed + * by concatenating them. + * + * A "." or NUL byte in the database name, or a NUL byte in the collection + * name, would shift the namespace split performed by the server and cause + * the operation to silently target a different database or collection. + * + * @internal + * @throws InvalidArgumentException if either name is invalid + */ +function create_namespace(string $databaseName, string $collectionName): string +{ + if ($databaseName === '' || str_contains($databaseName, '.') || str_contains($databaseName, "\0")) { + throw new InvalidArgumentException('$databaseName is invalid: ' . $databaseName); + } + + if ($collectionName === '' || str_contains($collectionName, "\0")) { + throw new InvalidArgumentException('$collectionName is invalid: ' . $collectionName); + } + + return $databaseName . '.' . $collectionName; +} + /** * Performs a deep copy of a value. * diff --git a/tests/Collection/CollectionFunctionalTest.php b/tests/Collection/CollectionFunctionalTest.php index d937f7bee..deb513dfb 100644 --- a/tests/Collection/CollectionFunctionalTest.php +++ b/tests/Collection/CollectionFunctionalTest.php @@ -39,7 +39,7 @@ */ class CollectionFunctionalTest extends FunctionalTestCase { - #[DataProvider('provideInvalidDatabaseAndCollectionNames')] + #[DataProvider('provideInvalidDatabaseNames')] public function testConstructorDatabaseNameArgument($databaseName, string $expectedExceptionClass): void { $this->expectException($expectedExceptionClass); @@ -47,7 +47,7 @@ public function testConstructorDatabaseNameArgument($databaseName, string $expec new Collection($this->manager, $databaseName, $this->getCollectionName()); } - #[DataProvider('provideInvalidDatabaseAndCollectionNames')] + #[DataProvider('provideInvalidCollectionNames')] public function testConstructorCollectionNameArgument($collectionName, string $expectedExceptionClass): void { $this->expectException($expectedExceptionClass); @@ -55,11 +55,29 @@ public function testConstructorCollectionNameArgument($collectionName, string $e new Collection($this->manager, $this->getDatabaseName(), $collectionName); } - public static function provideInvalidDatabaseAndCollectionNames() + public function testConstructorAllowsDotInCollectionName(): void + { + $collection = new Collection($this->manager, $this->getDatabaseName(), 'foo.bar'); + + $this->assertSame('foo.bar', $collection->getCollectionName()); + } + + public static function provideInvalidDatabaseNames() + { + return [ + [null, TypeError::class], + ['', InvalidArgumentException::class], + ['foo.bar', InvalidArgumentException::class], + ["foo\0bar", InvalidArgumentException::class], + ]; + } + + public static function provideInvalidCollectionNames() { return [ [null, TypeError::class], ['', InvalidArgumentException::class], + ["foo\0bar", InvalidArgumentException::class], ]; } diff --git a/tests/Database/DatabaseFunctionalTest.php b/tests/Database/DatabaseFunctionalTest.php index fa9d9c926..ab39162b6 100644 --- a/tests/Database/DatabaseFunctionalTest.php +++ b/tests/Database/DatabaseFunctionalTest.php @@ -39,6 +39,8 @@ public static function provideInvalidDatabaseNames() return [ [null, TypeError::class], ['', InvalidArgumentException::class], + ['foo.bar', InvalidArgumentException::class], + ["foo\0bar", InvalidArgumentException::class], ]; } diff --git a/tests/Operation/BulkWriteTest.php b/tests/Operation/BulkWriteTest.php index 30d5c6374..a0de6cb21 100644 --- a/tests/Operation/BulkWriteTest.php +++ b/tests/Operation/BulkWriteTest.php @@ -13,6 +13,15 @@ class BulkWriteTest extends TestCase { + #[DataProvider('provideInvalidDatabaseAndCollectionNames')] + public function testConstructorDatabaseAndCollectionNameChecks(string $databaseName, string $collectionName): void + { + $this->expectException(InvalidArgumentException::class); + new BulkWrite($databaseName, $collectionName, [ + [BulkWrite::INSERT_ONE => [['x' => 1]]], + ]); + } + public function testOperationsMustNotBeEmpty(): void { $this->expectException(InvalidArgumentException::class); diff --git a/tests/Operation/DeleteTest.php b/tests/Operation/DeleteTest.php index e1cd5bd74..3360f9cf4 100644 --- a/tests/Operation/DeleteTest.php +++ b/tests/Operation/DeleteTest.php @@ -16,6 +16,13 @@ class DeleteTest extends TestCase { + #[DataProvider('provideInvalidDatabaseAndCollectionNames')] + public function testConstructorDatabaseAndCollectionNameChecks(string $databaseName, string $collectionName): void + { + $this->expectException(InvalidArgumentException::class); + new Delete($databaseName, $collectionName, ['x' => 1], 1); + } + #[DataProvider('provideInvalidDocumentValues')] public function testConstructorFilterArgumentTypeCheck($filter): void { diff --git a/tests/Operation/FindTest.php b/tests/Operation/FindTest.php index d7eccf7f3..2c2749fa5 100644 --- a/tests/Operation/FindTest.php +++ b/tests/Operation/FindTest.php @@ -12,6 +12,13 @@ class FindTest extends TestCase { + #[DataProvider('provideInvalidDatabaseAndCollectionNames')] + public function testConstructorDatabaseAndCollectionNameChecks(string $databaseName, string $collectionName): void + { + $this->expectException(InvalidArgumentException::class); + new Find($databaseName, $collectionName, ['x' => 1]); + } + #[DataProvider('provideInvalidDocumentValues')] public function testConstructorFilterArgumentTypeCheck($filter): void { diff --git a/tests/Operation/InsertManyTest.php b/tests/Operation/InsertManyTest.php index 8ee11ff5d..e37a93f8c 100644 --- a/tests/Operation/InsertManyTest.php +++ b/tests/Operation/InsertManyTest.php @@ -10,6 +10,13 @@ class InsertManyTest extends TestCase { + #[DataProvider('provideInvalidDatabaseAndCollectionNames')] + public function testConstructorDatabaseAndCollectionNameChecks(string $databaseName, string $collectionName): void + { + $this->expectException(InvalidArgumentException::class); + new InsertMany($databaseName, $collectionName, [['x' => 1]]); + } + public function testConstructorDocumentsMustNotBeEmpty(): void { $this->expectException(InvalidArgumentException::class); diff --git a/tests/Operation/InsertOneTest.php b/tests/Operation/InsertOneTest.php index 1f641ea88..6dc821827 100644 --- a/tests/Operation/InsertOneTest.php +++ b/tests/Operation/InsertOneTest.php @@ -12,6 +12,13 @@ class InsertOneTest extends TestCase { + #[DataProvider('provideInvalidDatabaseAndCollectionNames')] + public function testConstructorDatabaseAndCollectionNameChecks(string $databaseName, string $collectionName): void + { + $this->expectException(InvalidArgumentException::class); + new InsertOne($databaseName, $collectionName, ['x' => 1]); + } + #[DataProvider('provideInvalidDocumentValues')] public function testConstructorDocumentArgumentTypeCheck($document): void { diff --git a/tests/Operation/RenameCollectionTest.php b/tests/Operation/RenameCollectionTest.php index bae80e9ee..1ed40fdea 100644 --- a/tests/Operation/RenameCollectionTest.php +++ b/tests/Operation/RenameCollectionTest.php @@ -30,4 +30,23 @@ public static function provideInvalidConstructorOptions() 'writeConcern' => self::getInvalidWriteConcernValues(), ]); } + + #[DataProvider('provideInvalidRenameDatabaseAndCollectionNames')] + public function testConstructorDatabaseAndCollectionNameChecks(string $fromDatabaseName, string $fromCollectionName, string $toDatabaseName, string $toCollectionName): void + { + $this->expectException(InvalidArgumentException::class); + new RenameCollection($fromDatabaseName, $fromCollectionName, $toDatabaseName, $toCollectionName); + } + + public static function provideInvalidRenameDatabaseAndCollectionNames(): array + { + return [ + 'dot in fromDatabaseName' => ['foo.bar', 'coll', 'db', 'coll'], + 'NUL byte in fromDatabaseName' => ["foo\0bar", 'coll', 'db', 'coll'], + 'NUL byte in fromCollectionName' => ['db', "foo\0bar", 'db', 'coll'], + 'dot in toDatabaseName' => ['db', 'coll', 'foo.bar', 'coll'], + 'NUL byte in toDatabaseName' => ['db', 'coll', "foo\0bar", 'coll'], + 'NUL byte in toCollectionName' => ['db', 'coll', 'db', "foo\0bar"], + ]; + } } diff --git a/tests/Operation/UpdateTest.php b/tests/Operation/UpdateTest.php index 9617c77e8..a36aba299 100644 --- a/tests/Operation/UpdateTest.php +++ b/tests/Operation/UpdateTest.php @@ -11,6 +11,13 @@ class UpdateTest extends TestCase { + #[DataProvider('provideInvalidDatabaseAndCollectionNames')] + public function testConstructorDatabaseAndCollectionNameChecks(string $databaseName, string $collectionName): void + { + $this->expectException(InvalidArgumentException::class); + new Update($databaseName, $collectionName, ['x' => 1], ['$set' => ['x' => 1]]); + } + #[DataProvider('provideInvalidDocumentValues')] public function testConstructorFilterArgumentTypeCheck($filter): void { diff --git a/tests/SpecTests/Crud/Prose17_DatabaseAndCollectionNameValidationTest.php b/tests/SpecTests/Crud/Prose17_DatabaseAndCollectionNameValidationTest.php new file mode 100644 index 000000000..cb91310b1 --- /dev/null +++ b/tests/SpecTests/Crud/Prose17_DatabaseAndCollectionNameValidationTest.php @@ -0,0 +1,49 @@ +expectException(InvalidArgumentException::class); + $client->getDatabase('foo.bar')->getCollection('coll')->insertOne([]); + } + + public function testDotInDatabaseNameViaGetCollection(): void + { + $client = self::createTestClient(); + + $this->expectException(InvalidArgumentException::class); + $client->getCollection('foo.bar', 'coll')->insertOne([]); + } + + public function testNulByteInDatabaseName(): void + { + $client = self::createTestClient(); + + $this->expectException(InvalidArgumentException::class); + $client->getDatabase("foo\0bar")->getCollection('coll')->insertOne([]); + } + + public function testNulByteInCollectionName(): void + { + $client = self::createTestClient(); + + $this->expectException(InvalidArgumentException::class); + $client->getDatabase('db')->getCollection("foo\0bar")->insertOne([]); + } + + // testNulByteInBulkWriteDatabaseName and testNulByteInBulkWriteCollectionName + // are not implemented: MongoDB\ClientBulkWrite does not exist in this version. +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 2b6b86eb2..e21baf3a4 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -159,6 +159,15 @@ final public static function provideInvalidStringValues(): array return self::wrapValuesForDataProvider(self::getInvalidStringValues()); } + final public static function provideInvalidDatabaseAndCollectionNames(): array + { + return [ + 'dot in databaseName' => ['foo.bar', 'coll'], + 'NUL byte in databaseName' => ["foo\0bar", 'coll'], + 'NUL byte in collectionName' => ['db', "foo\0bar"], + ]; + } + protected function assertDeprecated(callable $execution): mixed { return $this->assertError(E_USER_DEPRECATED | E_DEPRECATED, $execution); From 83001473792dcbb75e586767cb835b2bbef33d40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:03:28 -0400 Subject: [PATCH 2/5] Bump mongodb-labs/drivers-github-tools from 2 to 3 (#1775) Bumps [mongodb-labs/drivers-github-tools](https://github.com/mongodb-labs/drivers-github-tools) from 2 to 3. - [Release notes](https://github.com/mongodb-labs/drivers-github-tools/releases) - [Commits](https://github.com/mongodb-labs/drivers-github-tools/compare/v2...v3) --- updated-dependencies: - dependency-name: mongodb-labs/drivers-github-tools dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d22c6c6c..0923c8bd9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: run: echo '🎬 Release process for version ${{ inputs.version }} started by @${{ github.triggering_actor }}' >> $GITHUB_STEP_SUMMARY - name: "Generate token and checkout repository" - uses: mongodb-labs/drivers-github-tools/secure-checkout@v2 + uses: mongodb-labs/drivers-github-tools/secure-checkout@v3 with: app_id: ${{ vars.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} @@ -77,7 +77,7 @@ jobs: # - name: "Set up drivers-github-tools" - uses: mongodb-labs/drivers-github-tools/setup@v2 + uses: mongodb-labs/drivers-github-tools/setup@v3 with: aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} aws_region_name: ${{ vars.AWS_REGION_NAME }} @@ -93,7 +93,7 @@ jobs: run: echo "RELEASE_URL=$(gh release create ${{ inputs.version }} --target ${{ github.ref_name }} --title "${{ inputs.version }}" --notes-file release-message --draft)" >> "$GITHUB_ENV" - name: "Create release tag" - uses: mongodb-labs/drivers-github-tools/tag-version@v2 + uses: mongodb-labs/drivers-github-tools/tag-version@v3 with: version: ${{ inputs.version }} tag_message_template: 'Release ${VERSION}' @@ -132,7 +132,7 @@ jobs: steps: - name: "Generate token and checkout repository" - uses: mongodb-labs/drivers-github-tools/secure-checkout@v2 + uses: mongodb-labs/drivers-github-tools/secure-checkout@v3 with: app_id: ${{ vars.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} @@ -140,14 +140,14 @@ jobs: # Sets the S3_ASSETS environment variable used later - name: "Set up drivers-github-tools" - uses: mongodb-labs/drivers-github-tools/setup@v2 + uses: mongodb-labs/drivers-github-tools/setup@v3 with: aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} aws_region_name: ${{ vars.AWS_REGION_NAME }} aws_secret_id: ${{ secrets.AWS_SECRET_ID }} - name: "Generate SSDLC Reports" - uses: mongodb-labs/drivers-github-tools/full-report@v2 + uses: mongodb-labs/drivers-github-tools/full-report@v3 with: product_name: "MongoDB PHP Driver (library)" release_version: ${{ inputs.version }} @@ -158,7 +158,7 @@ jobs: continue-on-error: true - name: Upload S3 assets - uses: mongodb-labs/drivers-github-tools/upload-s3-assets@v2 + uses: mongodb-labs/drivers-github-tools/upload-s3-assets@v3 with: version: ${{ inputs.version }} product_name: mongo-php-library From caa1a1128d1f553ce5f4ad7acb02e9b99d086b73 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Wed, 12 Mar 2025 14:31:25 -0400 Subject: [PATCH 3/5] Support creating release branches when releasing new versions (#1627) * Support creating release branches when releasing new versions * Use same logic as in laravel integration * Update .github/workflows/release.yml Co-authored-by: Jeremy Mikola --------- Co-authored-by: Jeremy Mikola --- .github/workflows/release.yml | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0923c8bd9..293a487ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,9 @@ jobs: - name: "Store version numbers in env variables" run: | echo RELEASE_VERSION=${{ inputs.version }} >> $GITHUB_ENV + echo RELEASE_VERSION_WITHOUT_STABILITY=$(echo ${{ inputs.version }} | awk -F- '{print $1}') >> $GITHUB_ENV echo RELEASE_BRANCH=v$(echo ${{ inputs.version }} | cut -d '.' -f-2) >> $GITHUB_ENV + echo DEV_BRANCH=v$(echo ${{ inputs.version }} | cut -d '.' -f-1).x >> $GITHUB_ENV - name: "Ensure release tag does not already exist" run: | @@ -66,12 +68,31 @@ jobs: exit 1 fi - - name: "Fail if branch names don't match" - if: ${{ github.ref_name != env.RELEASE_BRANCH }} + # For patch releases (A.B.C where C != 0), we expect the release to be + # triggered from the A.B maintenance branch + - name: "Fail if patch release is created from wrong release branch" + if: ${{ !endsWith(env.RELEASE_VERSION_WITHOUT_STABILITY, '.0') && env.RELEASE_BRANCH != github.ref_name }} run: | echo '❌ Release failed due to branch mismatch: expected ${{ inputs.version }} to be released from ${{ env.RELEASE_BRANCH }}, got ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY exit 1 + # For non-patch releases (A.B.C where C == 0), we expect the release to + # be triggered from the A.B maintenance branch or A.x development branch + - name: "Fail if non-patch release is created from wrong release branch" + if: ${{ endsWith(env.RELEASE_VERSION_WITHOUT_STABILITY, '.0') && env.RELEASE_BRANCH != github.ref_name && env.DEV_BRANCH != github.ref_name }} + run: | + echo '❌ Release failed due to branch mismatch: expected ${{ inputs.version }} to be released from ${{ env.RELEASE_BRANCH }} or ${{ env.DEV_BRANCH }}, got ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY + exit 1 + + # If a non-patch release is created from its A.x development branch, + # create the A.B maintenance branch from the current one and push it + - name: "Create and push new release branch for non-patch release" + if: ${{ endsWith(env.RELEASE_VERSION_WITHOUT_STABILITY, '.0') && env.DEV_BRANCH == github.ref_name }} + run: | + echo '🆕 Creating new release branch ${RELEASE_BRANCH} from ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY + git checkout -b ${RELEASE_BRANCH} + git push origin ${RELEASE_BRANCH} + # # Preliminary checks done - commence the release process # @@ -90,7 +111,7 @@ jobs: EOL - name: "Create draft release" - run: echo "RELEASE_URL=$(gh release create ${{ inputs.version }} --target ${{ github.ref_name }} --title "${{ inputs.version }}" --notes-file release-message --draft)" >> "$GITHUB_ENV" + run: echo "RELEASE_URL=$(gh release create ${{ inputs.version }} --target ${{ env.RELEASE_BRANCH }} --title "${{ inputs.version }}" --notes-file release-message --draft)" >> "$GITHUB_ENV" - name: "Create release tag" uses: mongodb-labs/drivers-github-tools/tag-version@v3 From 934c89deb92cb25cef3188faa46caf08855b87a4 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Tue, 27 May 2025 15:28:38 +0200 Subject: [PATCH 4/5] Fix missing placeholder replacement in release output (#1717) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 293a487ee..37bb2ed3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -89,7 +89,7 @@ jobs: - name: "Create and push new release branch for non-patch release" if: ${{ endsWith(env.RELEASE_VERSION_WITHOUT_STABILITY, '.0') && env.DEV_BRANCH == github.ref_name }} run: | - echo '🆕 Creating new release branch ${RELEASE_BRANCH} from ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY + echo '🆕 Creating new release branch ${{ env.RELEASE_BRANCH }} from ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY git checkout -b ${RELEASE_BRANCH} git push origin ${RELEASE_BRANCH} From c1a419de96d1eb3903712e9a771f1fbb4b8da28e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 27 Aug 2026 19:47:57 +0200 Subject: [PATCH 5/5] Freeze rector version to 2.6.4 (#1973) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d6ec0bbc9..8e56decaa 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "require-dev": { "doctrine/coding-standard": "^12.0", "phpunit/phpunit": "^10.5.35", - "rector/rector": "^2.3.4", + "rector/rector": "~2.5.9", "squizlabs/php_codesniffer": "^3.7", "vimeo/psalm": "6.5.*" },