MINI Sh3ll
<?php
namespace MongoDB\Tests\UnifiedSpecTests;
use ArrayIterator;
use MongoDB\Driver\Monitoring\CommandFailedEvent;
use MongoDB\Driver\Monitoring\CommandStartedEvent;
use MongoDB\Driver\Monitoring\CommandSubscriber;
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
use MongoDB\Tests\UnifiedSpecTests\Constraint\Matches;
use MultipleIterator;
use PHPUnit\Framework\Assert;
use stdClass;
use function array_reverse;
use function count;
use function current;
use function get_class;
use function is_object;
use function key;
use function MongoDB\Driver\Monitoring\addSubscriber;
use function MongoDB\Driver\Monitoring\removeSubscriber;
use function PHPUnit\Framework\assertArrayHasKey;
use function PHPUnit\Framework\assertCount;
use function PHPUnit\Framework\assertInstanceOf;
use function PHPUnit\Framework\assertIsBool;
use function PHPUnit\Framework\assertIsObject;
use function PHPUnit\Framework\assertIsString;
use function PHPUnit\Framework\assertNotEmpty;
use function PHPUnit\Framework\assertObjectHasAttribute;
use function PHPUnit\Framework\assertSame;
use function PHPUnit\Framework\assertThat;
use function sprintf;
/**
* EventObserver handles "observeEvents" for client entities and assertions for
* "expectEvents" and special operations (e.g. assertSameLsidOnLastTwoCommands).
*/
final class EventObserver implements CommandSubscriber
{
/**
* These commands are always considered sensitive (i.e. command and reply
* documents should be redacted).
*
* @see https://github.com/mongodb/specifications/blob/master/source/command-monitoring/command-monitoring.rst#security
* @var array
*/
private static $sensitiveCommands = [
'authenticate' => 1,
'saslStart' => 1,
'saslContinue' => 1,
'getnonce' => 1,
'createUser' => 1,
'updateUser' => 1,
'copydbgetnonce' => 1,
'copydbsaslstart' => 1,
'copydb' => 1,
];
/**
* These commands are only considered sensitive when the command or reply
* document includes a speculativeAuthenticate field.
*
* @see https://github.com/mongodb/specifications/blob/master/source/command-monitoring/command-monitoring.rst#security
* @var array
*/
private static $sensitiveCommandsWithSpeculativeAuthenticate = [
'ismaster' => 1,
'isMaster' => 1,
'hello' => 1,
];
/** @var array */
private static $supportedEvents = [
'commandStartedEvent' => CommandStartedEvent::class,
'commandSucceededEvent' => CommandSucceededEvent::class,
'commandFailedEvent' => CommandFailedEvent::class,
];
/**
* These events are defined in the specification but unsupported by PHPLIB
* (e.g. CMAP events).
*
* @var array
*/
private static $unsupportedEvents = [
'poolCreatedEvent' => 1,
'poolReadyEvent' => 1,
'poolClearedEvent' => 1,
'poolClosedEvent' => 1,
'connectionCreatedEvent' => 1,
'connectionReadyEvent' => 1,
'connectionClosedEvent' => 1,
'connectionCheckOutStartedEvent' => 1,
'connectionCheckOutFailedEvent' => 1,
'connectionCheckedOutEvent' => 1,
'connectionCheckedInEvent' => 1,
];
/** @var array */
private $actualEvents = [];
/** @var string */
private $clientId;
/** @var Context */
private $context;
/**
* The configureFailPoint command (used by failPoint and targetedFailPoint
* operations) is always ignored.
*
* @var array
*/
private $ignoreCommands = ['configureFailPoint' => 1];
/** @var array */
private $observeEvents = [];
/** @var bool */
private $observeSensitiveCommands;
public function __construct(array $observeEvents, array $ignoreCommands, bool $observeSensitiveCommands, string $clientId, Context $context)
{
assertNotEmpty($observeEvents);
foreach ($observeEvents as $event) {
assertIsString($event);
/* Unlike Context::assertExpectedEventsForClients, which runs within
* a test, EventObserver is constructed via createEntities (before
* all tests). Ignoring events here allows tests within the file
* that don't assert these events to still execute. */
if (isset(self::$unsupportedEvents[$event])) {
continue;
}
assertArrayHasKey($event, self::$supportedEvents);
$this->observeEvents[self::$supportedEvents[$event]] = 1;
}
foreach ($ignoreCommands as $command) {
assertIsString($command);
$this->ignoreCommands[$command] = 1;
}
$this->observeSensitiveCommands = $observeSensitiveCommands;
$this->clientId = $clientId;
$this->context = $context;
}
/**
* @see https://www.php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandfailed.php
*/
public function commandFailed(CommandFailedEvent $event): void
{
$this->handleEvent($event);
}
/**
* @see https://www.php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandstarted.php
*/
public function commandStarted(CommandStartedEvent $event): void
{
$this->handleEvent($event);
}
/**
* @see https://www.php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandsucceeded.php
*/
public function commandSucceeded(CommandSucceededEvent $event): void
{
$this->handleEvent($event);
}
public function start(): void
{
addSubscriber($this);
}
public function stop(): void
{
removeSubscriber($this);
}
public function getLsidsOnLastTwoCommands(): array
{
$lsids = [];
foreach (array_reverse($this->actualEvents) as $event) {
if (! $event instanceof CommandStartedEvent) {
continue;
}
$command = $event->getCommand();
assertObjectHasAttribute('lsid', $command);
$lsids[] = $command->lsid;
if (count($lsids) === 2) {
return $lsids;
}
}
Assert::fail('Not enough CommandStartedEvents observed');
}
public function assert(array $expectedEvents): void
{
assertCount(count($expectedEvents), $this->actualEvents);
$mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
$mi->attachIterator(new ArrayIterator($expectedEvents));
$mi->attachIterator(new ArrayIterator($this->actualEvents));
foreach ($mi as $keys => $events) {
[$expectedEvent, $actualEvent] = $events;
assertIsObject($expectedEvent);
$expectedEvent = (array) $expectedEvent;
assertCount(1, $expectedEvent);
$type = key($expectedEvent);
assertArrayHasKey($type, self::$supportedEvents);
$data = current($expectedEvent);
assertIsObject($data);
// Message is used for actual event assertions (not test structure)
$message = sprintf('%s event[%d]', $this->clientId, $keys[0]);
assertInstanceOf(self::$supportedEvents[$type], $actualEvent, $message . ': type matches');
$this->assertEvent($actualEvent, $data, $message);
}
}
private function assertEvent($actual, stdClass $expected, string $message)
{
assertIsObject($actual);
switch (get_class($actual)) {
case CommandStartedEvent::class:
return $this->assertCommandStartedEvent($actual, $expected, $message);
case CommandSucceededEvent::class:
return $this->assertCommandSucceededEvent($actual, $expected, $message);
case CommandFailedEvent::class:
return $this->assertCommandFailedEvent($actual, $expected, $message);
default:
Assert::fail($message . ': Unsupported event type: ' . get_class($actual));
}
}
private function assertCommandStartedEvent(CommandStartedEvent $actual, stdClass $expected, string $message): void
{
Util::assertHasOnlyKeys($expected, ['command', 'commandName', 'databaseName', 'hasServiceId']);
if (isset($expected->command)) {
assertIsObject($expected->command);
$constraint = new Matches($expected->command, $this->context->getEntityMap());
assertThat($actual->getCommand(), $constraint, $message . ': command matches');
}
if (isset($expected->commandName)) {
assertIsString($expected->commandName);
assertSame($actual->getCommandName(), $expected->commandName, $message . ': commandName matches');
}
if (isset($expected->databaseName)) {
assertIsString($expected->databaseName);
assertSame($actual->getDatabaseName(), $expected->databaseName, $message . ': databaseName matches');
}
if (isset($expected->hasServiceId)) {
assertIsBool($expected->hasServiceId);
assertSame($actual->getServiceId() !== null, $expected->hasServiceId, $message . ': hasServiceId matches');
}
}
private function assertCommandSucceededEvent(CommandSucceededEvent $actual, stdClass $expected, string $message): void
{
Util::assertHasOnlyKeys($expected, ['reply', 'commandName', 'hasServiceId']);
if (isset($expected->reply)) {
assertIsObject($expected->reply);
$constraint = new Matches($expected->reply, $this->context->getEntityMap());
assertThat($actual->getReply(), $constraint, $message . ': reply matches');
}
if (isset($expected->commandName)) {
assertIsString($expected->commandName);
assertSame($actual->getCommandName(), $expected->commandName, $message . ': commandName matches');
}
if (isset($expected->hasServiceId)) {
assertIsBool($expected->hasServiceId);
assertSame($actual->getServiceId() !== null, $expected->hasServiceId, $message . ': hasServiceId matches');
}
}
private function assertCommandFailedEvent(CommandFailedEvent $actual, stdClass $expected, string $message): void
{
Util::assertHasOnlyKeys($expected, ['commandName', 'hasServiceId']);
if (isset($expected->commandName)) {
assertIsString($expected->commandName);
assertSame($actual->getCommandName(), $expected->commandName, $message . ': commandName matches');
}
if (isset($expected->hasServiceId)) {
assertIsBool($expected->hasServiceId);
assertSame($actual->getServiceId() !== null, $expected->hasServiceId, $message . ': hasServiceId matches');
}
}
/** @param CommandStartedEvent|CommandSucceededEvent|CommandFailedEvent $event */
private function handleEvent($event): void
{
if (! $this->context->isActiveClient($this->clientId)) {
return;
}
if (! is_object($event)) {
return;
}
if (! isset($this->observeEvents[get_class($event)])) {
return;
}
if (isset($this->ignoreCommands[$event->getCommandName()])) {
return;
}
if (! $this->observeSensitiveCommands && $this->isSensitiveCommand($event)) {
return;
}
$this->actualEvents[] = $event;
}
/** @param CommandStartedEvent|CommandSucceededEvent|CommandFailedEvent $event */
private function isSensitiveCommand($event): bool
{
if (isset(self::$sensitiveCommands[$event->getCommandName()])) {
return true;
}
/* If the command or reply included a speculativeAuthenticate field,
* libmongoc will already have redacted it (CDRIVER-4000). Therefore, we
* can infer that the command was sensitive if its command or reply is
* empty. */
if (isset(self::$sensitiveCommandsWithSpeculativeAuthenticate[$event->getCommandName()])) {
$commandOrReply = $event instanceof CommandStartedEvent ? $event->getCommand() : $event->getReply();
return (array) $commandOrReply === [];
}
return false;
}
}
OHA YOOOO