fix(files_versions): guard null path in event listeners

Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
This commit is contained in:
Git'Fellow 2026-05-29 17:59:38 +02:00
parent 1b1c74e848
commit 2e662fc5eb
2 changed files with 109 additions and 0 deletions

View file

@ -207,6 +207,9 @@ class FileEventsListener implements IEventListener {
}
$path = $this->getPathForNode($node);
if ($path === null) {
return;
}
$result = Storage::store($path);
// Store the result of the version creation so it can be used in post_write_hook.
@ -311,6 +314,9 @@ class FileEventsListener implements IEventListener {
$node = $this->versionsDeleted[$path];
$relativePath = $this->getPathForNode($node);
unset($this->versionsDeleted[$path]);
if ($relativePath === null) {
return;
}
Storage::delete($relativePath);
// If no new version was stored in the FS, no new version should be added in the DB.
// So we simply update the associated version.
@ -324,6 +330,9 @@ class FileEventsListener implements IEventListener {
*/
public function pre_remove_hook(Node $node): void {
$path = $this->getPathForNode($node);
if ($path === null) {
return;
}
Storage::markDeletedFile($path);
$this->versionsDeleted[$node->getPath()] = $node;
}
@ -344,6 +353,9 @@ class FileEventsListener implements IEventListener {
$oldPath = $this->getPathForNode($source);
$newPath = $this->getPathForNode($target);
if ($oldPath === null || $newPath === null) {
return;
}
Storage::renameOrCopy($oldPath, $newPath, 'rename');
}
@ -363,6 +375,9 @@ class FileEventsListener implements IEventListener {
$oldPath = $this->getPathForNode($source);
$newPath = $this->getPathForNode($target);
if ($oldPath === null || $newPath === null) {
return;
}
Storage::renameOrCopy($oldPath, $newPath, 'copy');
}

View file

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_Versions\Tests\Listener;
use OCA\Files_Versions\Listener\FileEventsListener;
use OCA\Files_Versions\Versions\IVersionManager;
use OCP\Files\File;
use OCP\Files\IMimeTypeLoader;
use OCP\Files\IRootFolder;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;
class FileEventsListenerTest extends TestCase {
private IRootFolder&MockObject $rootFolder;
private IVersionManager&MockObject $versionManager;
private IMimeTypeLoader&MockObject $mimeTypeLoader;
private IUserSession&MockObject $userSession;
private LoggerInterface&MockObject $logger;
private FileEventsListener $listener;
protected function setUp(): void {
parent::setUp();
$this->rootFolder = $this->createMock(IRootFolder::class);
$this->versionManager = $this->createMock(IVersionManager::class);
$this->mimeTypeLoader = $this->createMock(IMimeTypeLoader::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->listener = new FileEventsListener(
$this->rootFolder,
$this->versionManager,
$this->mimeTypeLoader,
$this->userSession,
$this->logger,
);
}
private function createUnresolvableFile(): File&MockObject {
$this->userSession->method('getUser')->willReturn(null);
$node = $this->createMock(File::class);
$node->method('getOwner')->willReturn(null);
$node->method('getPath')->willReturn('/test.txt');
$node->method('getId')->willReturn(42);
$node->method('getSize')->willReturn(100);
$node->method('getMTime')->willReturn(1234567890);
return $node;
}
private function getPrivateProperty(string $property): mixed {
$ref = new \ReflectionProperty(FileEventsListener::class, $property);
$ref->setAccessible(true);
return $ref->getValue($this->listener);
}
public function testGetPathForNodeReturnsNullWhenUnresolvable(): void {
$node = $this->createUnresolvableFile();
$this->logger->expects($this->once())
->method('debug')
->with('Failed to compute path for node', $this->anything());
$method = new \ReflectionMethod(FileEventsListener::class, 'getPathForNode');
$method->setAccessible(true);
$this->assertNull($method->invoke($this->listener, $node));
}
public function testWriteHookSkipsWhenPathUnresolvable(): void {
$node = $this->createUnresolvableFile();
$this->listener->write_hook($node);
$this->assertSame([], $this->getPrivateProperty('writeHookInfo'));
}
public function testPreRemoveHookSkipsWhenPathUnresolvable(): void {
$node = $this->createUnresolvableFile();
$this->listener->pre_remove_hook($node);
$this->assertSame([], $this->getPrivateProperty('versionsDeleted'));
}
}