Skip to content

Commit 2e662fc

Browse files
committed
fix(files_versions): guard null path in event listeners
Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
1 parent 1b1c74e commit 2e662fc

2 files changed

Lines changed: 109 additions & 0 deletions

File tree

apps/files_versions/lib/Listener/FileEventsListener.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ public function write_hook(Node $node): void {
207207
}
208208

209209
$path = $this->getPathForNode($node);
210+
if ($path === null) {
211+
return;
212+
}
210213
$result = Storage::store($path);
211214

212215
// Store the result of the version creation so it can be used in post_write_hook.
@@ -311,6 +314,9 @@ public function remove_hook(Node $node): void {
311314
$node = $this->versionsDeleted[$path];
312315
$relativePath = $this->getPathForNode($node);
313316
unset($this->versionsDeleted[$path]);
317+
if ($relativePath === null) {
318+
return;
319+
}
314320
Storage::delete($relativePath);
315321
// If no new version was stored in the FS, no new version should be added in the DB.
316322
// So we simply update the associated version.
@@ -324,6 +330,9 @@ public function remove_hook(Node $node): void {
324330
*/
325331
public function pre_remove_hook(Node $node): void {
326332
$path = $this->getPathForNode($node);
333+
if ($path === null) {
334+
return;
335+
}
327336
Storage::markDeletedFile($path);
328337
$this->versionsDeleted[$node->getPath()] = $node;
329338
}
@@ -344,6 +353,9 @@ public function rename_hook(Node $source, Node $target): void {
344353

345354
$oldPath = $this->getPathForNode($source);
346355
$newPath = $this->getPathForNode($target);
356+
if ($oldPath === null || $newPath === null) {
357+
return;
358+
}
347359
Storage::renameOrCopy($oldPath, $newPath, 'rename');
348360
}
349361

@@ -363,6 +375,9 @@ public function copy_hook(Node $source, Node $target): void {
363375

364376
$oldPath = $this->getPathForNode($source);
365377
$newPath = $this->getPathForNode($target);
378+
if ($oldPath === null || $newPath === null) {
379+
return;
380+
}
366381
Storage::renameOrCopy($oldPath, $newPath, 'copy');
367382
}
368383

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Files_Versions\Tests\Listener;
10+
11+
use OCA\Files_Versions\Listener\FileEventsListener;
12+
use OCA\Files_Versions\Versions\IVersionManager;
13+
use OCP\Files\File;
14+
use OCP\Files\IMimeTypeLoader;
15+
use OCP\Files\IRootFolder;
16+
use OCP\IUserSession;
17+
use PHPUnit\Framework\MockObject\MockObject;
18+
use Psr\Log\LoggerInterface;
19+
use Test\TestCase;
20+
21+
class FileEventsListenerTest extends TestCase {
22+
private IRootFolder&MockObject $rootFolder;
23+
private IVersionManager&MockObject $versionManager;
24+
private IMimeTypeLoader&MockObject $mimeTypeLoader;
25+
private IUserSession&MockObject $userSession;
26+
private LoggerInterface&MockObject $logger;
27+
private FileEventsListener $listener;
28+
29+
protected function setUp(): void {
30+
parent::setUp();
31+
32+
$this->rootFolder = $this->createMock(IRootFolder::class);
33+
$this->versionManager = $this->createMock(IVersionManager::class);
34+
$this->mimeTypeLoader = $this->createMock(IMimeTypeLoader::class);
35+
$this->userSession = $this->createMock(IUserSession::class);
36+
$this->logger = $this->createMock(LoggerInterface::class);
37+
38+
$this->listener = new FileEventsListener(
39+
$this->rootFolder,
40+
$this->versionManager,
41+
$this->mimeTypeLoader,
42+
$this->userSession,
43+
$this->logger,
44+
);
45+
}
46+
47+
private function createUnresolvableFile(): File&MockObject {
48+
$this->userSession->method('getUser')->willReturn(null);
49+
50+
$node = $this->createMock(File::class);
51+
$node->method('getOwner')->willReturn(null);
52+
$node->method('getPath')->willReturn('/test.txt');
53+
$node->method('getId')->willReturn(42);
54+
$node->method('getSize')->willReturn(100);
55+
$node->method('getMTime')->willReturn(1234567890);
56+
57+
return $node;
58+
}
59+
60+
private function getPrivateProperty(string $property): mixed {
61+
$ref = new \ReflectionProperty(FileEventsListener::class, $property);
62+
$ref->setAccessible(true);
63+
return $ref->getValue($this->listener);
64+
}
65+
66+
public function testGetPathForNodeReturnsNullWhenUnresolvable(): void {
67+
$node = $this->createUnresolvableFile();
68+
69+
$this->logger->expects($this->once())
70+
->method('debug')
71+
->with('Failed to compute path for node', $this->anything());
72+
73+
$method = new \ReflectionMethod(FileEventsListener::class, 'getPathForNode');
74+
$method->setAccessible(true);
75+
76+
$this->assertNull($method->invoke($this->listener, $node));
77+
}
78+
79+
public function testWriteHookSkipsWhenPathUnresolvable(): void {
80+
$node = $this->createUnresolvableFile();
81+
82+
$this->listener->write_hook($node);
83+
84+
$this->assertSame([], $this->getPrivateProperty('writeHookInfo'));
85+
}
86+
87+
public function testPreRemoveHookSkipsWhenPathUnresolvable(): void {
88+
$node = $this->createUnresolvableFile();
89+
90+
$this->listener->pre_remove_hook($node);
91+
92+
$this->assertSame([], $this->getPrivateProperty('versionsDeleted'));
93+
}
94+
}

0 commit comments

Comments
 (0)