Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,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.14.2"
},
Expand Down
5 changes: 3 additions & 2 deletions src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
use function current;
use function is_array;
use function is_bool;
use function str_contains;
use function strlen;

/**
Expand Down Expand Up @@ -174,11 +175,11 @@ class Collection implements Stringable
*/
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);
}

Expand Down
3 changes: 2 additions & 1 deletion src/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@

use function is_array;
use function is_bool;
use function str_contains;
use function strlen;

/** @psalm-no-seal-properties */
Expand Down Expand Up @@ -112,7 +113,7 @@ class Database implements Stringable
*/
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);
}

Expand Down
9 changes: 7 additions & 2 deletions src/Operation/BulkWrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,6 +64,8 @@ final class BulkWrite

private array $options;

private string $namespace;

/**
* Constructs a bulk write operation.
*
Expand Down Expand Up @@ -142,8 +145,10 @@ final class BulkWrite
* @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');
}
Expand Down Expand Up @@ -232,7 +237,7 @@ public function execute(Server $server): BulkWriteResult
}
}

$writeResult = $server->executeBulkWrite($this->databaseName . '.' . $this->collectionName, $bulk, $this->createExecuteOptions());
$writeResult = $server->executeBulkWrite($this->namespace, $bulk, $this->createExecuteOptions());

return new BulkWriteResult($writeResult, $insertedIds);
}
Expand Down
9 changes: 7 additions & 2 deletions src/Operation/Delete.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -44,6 +45,8 @@ final class Delete implements Explainable
{
private const WIRE_VERSION_FOR_HINT = 9;

private string $namespace;

/**
* Constructs a delete command.
*
Expand Down Expand Up @@ -80,8 +83,10 @@ final class Delete implements 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);
}
Expand Down Expand Up @@ -140,7 +145,7 @@ public function execute(Server $server): DeleteResult
$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);
}
Expand Down
9 changes: 7 additions & 2 deletions src/Operation/Find.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use function is_bool;
use function is_integer;
use function is_string;
use function MongoDB\create_namespace;
use function MongoDB\is_document;

/**
Expand All @@ -49,6 +50,8 @@ final class Find implements Explainable
public const TAILABLE = 2;
public const TAILABLE_AWAIT = 3;

private string $namespace;

/**
* Constructs a find command.
*
Expand Down Expand Up @@ -130,8 +133,10 @@ final class Find implements 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);
}
Expand Down Expand Up @@ -260,7 +265,7 @@ public function execute(Server $server): CursorInterface
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']);
Expand Down
9 changes: 7 additions & 2 deletions src/Operation/InsertMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -45,6 +46,8 @@ final class InsertMany

private array $options;

private string $namespace;

/**
* Constructs an insert command.
*
Expand Down Expand Up @@ -75,8 +78,10 @@ final class InsertMany
* @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'])) {
Expand Down Expand Up @@ -131,7 +136,7 @@ public function execute(Server $server): InsertManyResult
$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);
}
Expand Down
9 changes: 7 additions & 2 deletions src/Operation/InsertOne.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use MongoDB\InsertOneResult;

use function is_bool;
use function MongoDB\create_namespace;
use function MongoDB\is_document;

/**
Expand All @@ -40,6 +41,8 @@ final class InsertOne
{
private array|object $document;

private string $namespace;

/**
* Constructs an insert command.
*
Expand All @@ -65,8 +68,10 @@ final class InsertOne
* @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');
}
Expand Down Expand Up @@ -111,7 +116,7 @@ public function execute(Server $server): InsertOneResult

$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);
}
Expand Down
5 changes: 3 additions & 2 deletions src/Operation/RenameCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use MongoDB\Exception\UnsupportedException;

use function is_bool;
use function MongoDB\create_namespace;

/**
* Operation for the renameCollection command.
Expand Down Expand Up @@ -81,8 +82,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);
}

/**
Expand Down
9 changes: 7 additions & 2 deletions src/Operation/Update.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -46,6 +47,8 @@ final class Update implements Explainable
{
private array $options;

private string $namespace;

/**
* Constructs a update command.
*
Expand Down Expand Up @@ -94,8 +97,10 @@ final class Update implements 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);
}
Expand Down Expand Up @@ -180,7 +185,7 @@ public function execute(Server $server): UpdateResult
$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);
}
Expand Down
25 changes: 25 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -435,6 +436,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.
*
Expand Down
24 changes: 21 additions & 3 deletions tests/Collection/CollectionFunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,45 @@
*/
class CollectionFunctionalTest extends FunctionalTestCase
{
#[DataProvider('provideInvalidDatabaseAndCollectionNames')]
#[DataProvider('provideInvalidDatabaseNames')]
public function testConstructorDatabaseNameArgument($databaseName, string $expectedExceptionClass): void
{
$this->expectException($expectedExceptionClass);
// TODO: Move to unit test once ManagerInterface can be mocked (PHPC-378)
new Collection($this->manager, $databaseName, $this->getCollectionName());
}

#[DataProvider('provideInvalidDatabaseAndCollectionNames')]
#[DataProvider('provideInvalidCollectionNames')]
public function testConstructorCollectionNameArgument($collectionName, string $expectedExceptionClass): void
{
$this->expectException($expectedExceptionClass);
// TODO: Move to unit test once ManagerInterface can be mocked (PHPC-378)
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],
];
}

Expand Down
2 changes: 2 additions & 0 deletions tests/Database/DatabaseFunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public static function provideInvalidDatabaseNames()
return [
[null, TypeError::class],
['', InvalidArgumentException::class],
['foo.bar', InvalidArgumentException::class],
["foo\0bar", InvalidArgumentException::class],
];
}

Expand Down
Loading
Loading