diff --git a/src/Provider/Doctrine/Auditing/Transaction/TransactionProcessor.php b/src/Provider/Doctrine/Auditing/Transaction/TransactionProcessor.php index 051ddf5..4f77efa 100644 --- a/src/Provider/Doctrine/Auditing/Transaction/TransactionProcessor.php +++ b/src/Provider/Doctrine/Auditing/Transaction/TransactionProcessor.php @@ -210,7 +210,7 @@ private function audit(array $data): void $dt = new \DateTimeImmutable('now', new \DateTimeZone($this->provider->getAuditor()->getConfiguration()->getTimezone())); $diff = $data['diff']; $convertCharEncoding = (\is_string($diff) || \is_array($diff)); - $diff = $convertCharEncoding ? mb_convert_encoding($diff, 'UTF-8', 'UTF-8') : $diff; + $diff = $convertCharEncoding ? $this->convertEncoding($diff) : $diff; $payload = [ 'entity' => $data['entity'], @@ -232,6 +232,22 @@ private function audit(array $data): void $this->notify($payload); } + // Avoid warning (and dismissal) of objects in input array when using mb_convert_encoding + private function convertEncoding(mixed $input): mixed + { + if (\is_string($input)) { + return mb_convert_encoding($input, 'UTF-8', 'UTF-8'); + } + if (\is_array($input)) { + foreach ($input as $key => $value) { + $input[$this->convertEncoding($key)] = $this->convertEncoding($value); // inbuilt mb_convert_encoding also converts keys + } + } + + // Leave any other thing as is + return $input; + } + private function getDiscriminator(object $entity, int $inheritanceType): ?string { return ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE === $inheritanceType ? DoctrineHelper::getRealClassName($entity) : null; diff --git a/tests/Provider/Doctrine/Auditing/Transaction/TransactionProcessorTest.php b/tests/Provider/Doctrine/Auditing/Transaction/TransactionProcessorTest.php index 57c5783..e5998db 100644 --- a/tests/Provider/Doctrine/Auditing/Transaction/TransactionProcessorTest.php +++ b/tests/Provider/Doctrine/Auditing/Transaction/TransactionProcessorTest.php @@ -801,6 +801,50 @@ public function testProcessDeletions(): void $this->assertCount(1, $audits, 'TransactionProcessor::processDeletions() creates a "'.Transaction::REMOVE.'" audit entry.'); } + public function testEncodingConversion(): void + { + $processor = new TransactionProcessor($this->provider); + $method = $this->reflectMethod(TransactionProcessor::class, 'convertEncoding'); + + $invalidUtf8String = "\xC3\x28"; // Invalid UTF-8 byte sequence. + $convertedString = $method->invokeArgs($processor, [$invalidUtf8String]); + + // Invalid UTF-8 byte sequence should be converted to valid UTF-8. + $this->assertNotSame( + $invalidUtf8String, + $convertedString, + ); + $this->assertTrue(mb_check_encoding($convertedString, 'UTF-8')); + + $obj = new \stdClass(); + $complexArray = [ + 1, + false, + 'string', + [ + 'key' => 'value', + 'invalid' => $invalidUtf8String, + ], + $obj, + ]; + + $convertedArray = $method->invokeArgs($processor, [$complexArray]); + // Invalid UTF-8 byte sequence should be converted to valid UTF-8 and the rest of the array should remain unchanged. + $this->assertSame( + [ + 1, + false, + 'string', + [ + 'key' => 'value', + 'invalid' => $convertedString, + ], + $obj, + ], + $convertedArray, + ); + } + private function configureEntities(): void { $this->provider->getConfiguration()->setEntities([