Skip to content

Commit 562c0e2

Browse files
Merge pull request #61188 from nextcloud/backport/60842/stable33
[stable33] fix(files_versions): guard null path in event listeners
2 parents c944cac + 9a0501f commit 562c0e2

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
@@ -206,6 +206,9 @@ public function write_hook(Node $node): void {
206206
}
207207

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

211214
// Store the result of the version creation so it can be used in post_write_hook.
@@ -310,6 +313,9 @@ public function remove_hook(Node $node): void {
310313
$node = $this->versionsDeleted[$path];
311314
$relativePath = $this->getPathForNode($node);
312315
unset($this->versionsDeleted[$path]);
316+
if ($relativePath === null) {
317+
return;
318+
}
313319
Storage::delete($relativePath);
314320
// If no new version was stored in the FS, no new version should be added in the DB.
315321
// So we simply update the associated version.
@@ -323,6 +329,9 @@ public function remove_hook(Node $node): void {
323329
*/
324330
public function pre_remove_hook(Node $node): void {
325331
$path = $this->getPathForNode($node);
332+
if ($path === null) {
333+
return;
334+
}
326335
Storage::markDeletedFile($path);
327336
$this->versionsDeleted[$node->getPath()] = $node;
328337
}
@@ -343,6 +352,9 @@ public function rename_hook(Node $source, Node $target): void {
343352

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

@@ -362,6 +374,9 @@ public function copy_hook(Node $source, Node $target): void {
362374

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

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)