diff --git a/lib/private/Log/ExceptionSerializer.php b/lib/private/Log/ExceptionSerializer.php index c39f3e005f065..3b52669593a0a 100644 --- a/lib/private/Log/ExceptionSerializer.php +++ b/lib/private/Log/ExceptionSerializer.php @@ -251,7 +251,12 @@ private function encodeArg($arg, $nestingLevel = 5) { } $objectInfo = [ '__class__' => get_class($arg) ]; - $objectVars = get_object_vars($arg); + try { + $objectVars = get_object_vars($arg); + } catch (\Throwable $e) { + // get_object_vars may throw if $arg was a ghost and failed to build + $objectVars = ['__failure__' => $e->getMessage()]; + } return array_map(function ($arg) use ($nestingLevel) { return $this->encodeArg($arg, $nestingLevel - 1); }, array_merge($objectInfo, $objectVars)); diff --git a/tests/lib/Log/ExceptionSerializerTest.php b/tests/lib/Log/ExceptionSerializerTest.php index e543c34f1be18..b07c8f68827c2 100644 --- a/tests/lib/Log/ExceptionSerializerTest.php +++ b/tests/lib/Log/ExceptionSerializerTest.php @@ -11,19 +11,37 @@ use OC\Log\ExceptionSerializer; use OC\SystemConfig; +use PHPUnit\Framework\Attributes\RequiresPhp; use Test\TestCase; +class LazyObjectFixture { + public string $value = 'value'; +} + class ExceptionSerializerTest extends TestCase { private ExceptionSerializer $serializer; + private string|false $exceptionIgnoreArgs; #[\Override] public function setUp(): void { parent::setUp(); + // traces only contain arguments if this is disabled + $this->exceptionIgnoreArgs = ini_set('zend.exception_ignore_args', '0'); + $config = $this->createMock(SystemConfig::class); $this->serializer = new ExceptionSerializer($config); } + #[\Override] + public function tearDown(): void { + if ($this->exceptionIgnoreArgs !== false) { + ini_set('zend.exception_ignore_args', $this->exceptionIgnoreArgs); + } + + parent::tearDown(); + } + private function emit($arguments) { \call_user_func_array([$this, 'bind'], $arguments); } @@ -36,6 +54,10 @@ private function customMagicAuthThing(string $login, string $parole): void { throw new \Exception('expected custom auth exception'); } + private function throwsWithObject(object $object): void { + throw new \Exception('expected exception'); + } + /** * this test ensures that the serializer does not overwrite referenced * variables. It is crafted after a scenario we experienced: the DAV server @@ -65,4 +87,20 @@ public function testSerializerWithRegisteredMethods(): void { $this->assertFalse(isset($serializedData['Trace'][0]['args'][1])); } } + + #[RequiresPhp('>= 8.4')] + public function testSerializerDoesNotInitializeLazyObjects(): void { + $ghost = (new \ReflectionClass(LazyObjectFixture::class))->newLazyGhost(function (): void { + throw new \RuntimeException('Lazy ghost could not be initialized'); + }); + + try { + $this->throwsWithObject($ghost); + } catch (\Exception $e) { + $serializedData = $this->serializer->serializeException($e); + } + + $this->assertSame(LazyObjectFixture::class, $serializedData['Trace'][0]['args'][0]['__class__']); + $this->assertSame('Lazy ghost could not be initialized', $serializedData['Trace'][0]['args'][0]['__failure__']); + } }