Skip to content

Commit 81a7bf9

Browse files
committed
feat: add oc-ownerid and oc-permissions headers on PUT DAV requests
Signed-off-by: Salvatore Martire <[email protected]>
1 parent a075b23 commit 81a7bf9

File tree

6 files changed

+208
-0
lines changed

6 files changed

+208
-0
lines changed

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@
210210
'OCA\\DAV\\Connector\\LegacyDAVACL' => $baseDir . '/../lib/Connector/LegacyDAVACL.php',
211211
'OCA\\DAV\\Connector\\LegacyPublicAuth' => $baseDir . '/../lib/Connector/LegacyPublicAuth.php',
212212
'OCA\\DAV\\Connector\\PermissionsTrait' => $baseDir . '/../lib/Connector/PermissionsTrait.php',
213+
'OCA\\DAV\\Connector\\Sabre\\AddExtraHeadersPlugin' => $baseDir . '/../lib/Connector/Sabre/AddExtraHeadersPlugin.php',
213214
'OCA\\DAV\\Connector\\Sabre\\AnonymousOptionsPlugin' => $baseDir . '/../lib/Connector/Sabre/AnonymousOptionsPlugin.php',
214215
'OCA\\DAV\\Connector\\Sabre\\AppleQuirksPlugin' => $baseDir . '/../lib/Connector/Sabre/AppleQuirksPlugin.php',
215216
'OCA\\DAV\\Connector\\Sabre\\Auth' => $baseDir . '/../lib/Connector/Sabre/Auth.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ class ComposerStaticInitDAV
225225
'OCA\\DAV\\Connector\\LegacyDAVACL' => __DIR__ . '/..' . '/../lib/Connector/LegacyDAVACL.php',
226226
'OCA\\DAV\\Connector\\LegacyPublicAuth' => __DIR__ . '/..' . '/../lib/Connector/LegacyPublicAuth.php',
227227
'OCA\\DAV\\Connector\\PermissionsTrait' => __DIR__ . '/..' . '/../lib/Connector/PermissionsTrait.php',
228+
'OCA\\DAV\\Connector\\Sabre\\AddExtraHeadersPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AddExtraHeadersPlugin.php',
228229
'OCA\\DAV\\Connector\\Sabre\\AnonymousOptionsPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AnonymousOptionsPlugin.php',
229230
'OCA\\DAV\\Connector\\Sabre\\AppleQuirksPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AppleQuirksPlugin.php',
230231
'OCA\\DAV\\Connector\\Sabre\\Auth' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Auth.php',
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OCA\DAV\Connector\Sabre;
10+
11+
use OCA\DAV\Connector\PermissionsTrait;
12+
use Psr\Log\LoggerInterface;
13+
use Sabre\DAV\Exception\NotFound;
14+
use Sabre\DAV\Server;
15+
use Sabre\HTTP\RequestInterface;
16+
use Sabre\HTTP\ResponseInterface;
17+
18+
/**
19+
* Adds the "OC-OwnerId" and "OC-Permissions" after PUT requests so that
20+
* clients don't need to do a propfind after uploading a file to decide what
21+
* to display.
22+
*/
23+
class AddExtraHeadersPlugin extends \Sabre\DAV\ServerPlugin {
24+
25+
use PermissionsTrait;
26+
27+
private ?Server $server = null;
28+
29+
public function __construct(
30+
private LoggerInterface $logger,
31+
private bool $isPublic = false,
32+
) {
33+
}
34+
35+
public function initialize(Server $server): void {
36+
$this->server = $server;
37+
38+
$server->on('afterMethod:PUT', $this->afterPut(...));
39+
}
40+
41+
private function afterPut(RequestInterface $request, ResponseInterface $response): void {
42+
if ($this->server === null) {
43+
return;
44+
}
45+
46+
$node = null;
47+
try {
48+
$node = $this->server->tree->getNodeForPath($request->getPath());
49+
} catch (NotFound) {
50+
$this->logger->error("Cannot set extra headers for non-existing file '{$request->getPath()}'");
51+
return;
52+
}
53+
54+
if (!$node instanceof Node) {
55+
$nodeType = get_debug_type($node);
56+
$this->logger->error("Cannot set extra headers for node of type {$nodeType} for file '{$request->getPath()}'");
57+
return;
58+
}
59+
60+
if (!$this->isPublic) {
61+
$ownerId = $node->getOwner()?->getUID();
62+
if ($ownerId !== null) {
63+
$response->setHeader('X-NC-OwnerId', $ownerId);
64+
}
65+
}
66+
67+
$permissions = $this->getPermissionString(
68+
$this->isPublic,
69+
$node->getDavPermissions()
70+
);
71+
$response->setHeader('X-NC-Permissions', $permissions);
72+
}
73+
}

apps/dav/lib/Connector/Sabre/ServerFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ public function createServer(
209209
);
210210
}
211211
$server->addPlugin(new CopyEtagHeaderPlugin());
212+
$server->addPlugin(new AddExtraHeadersPlugin($this->logger, $isPublicShare));
212213

213214
// Load dav plugins from apps
214215
$event = new SabrePluginEvent($server);

apps/dav/lib/Server.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use OCA\DAV\CardDAV\Security\CardDavRateLimitingPlugin;
2828
use OCA\DAV\CardDAV\Validation\CardDavValidatePlugin;
2929
use OCA\DAV\Comments\CommentsPlugin;
30+
use OCA\DAV\Connector\Sabre\AddExtraHeadersPlugin;
3031
use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
3132
use OCA\DAV\Connector\Sabre\AppleQuirksPlugin;
3233
use OCA\DAV\Connector\Sabre\Auth;
@@ -384,6 +385,7 @@ public function __construct(
384385
)
385386
);
386387
}
388+
$this->server->addPlugin(new AddExtraHeadersPlugin($logger, false));
387389
$this->server->addPlugin(new EnablePlugin(
388390
\OCP\Server::get(IConfig::class),
389391
\OCP\Server::get(BirthdayService::class),
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace unit\Connector\Sabre;
10+
11+
use LogicException;
12+
use OCA\DAV\Connector\Sabre\AddExtraHeadersPlugin;
13+
use OCA\DAV\Connector\Sabre\Node;
14+
use OCA\DAV\Connector\Sabre\Server;
15+
use OCP\IUser;
16+
use PHPUnit\Framework\Attributes\DataProvider;
17+
use PHPUnit\Framework\MockObject\MockObject;
18+
use Psr\Log\LoggerInterface;
19+
use Sabre\DAV\Exception\NotFound;
20+
use Sabre\DAV\Tree;
21+
use Sabre\HTTP\RequestInterface;
22+
use Sabre\HTTP\ResponseInterface;
23+
use Test\TestCase;
24+
25+
class AddExtraHeadersPluginTest extends TestCase {
26+
27+
private AddExtraHeadersPlugin $plugin;
28+
private Server&MockObject $server;
29+
private LoggerInterface&MockObject $logger;
30+
private RequestInterface&MockObject $request;
31+
private ResponseInterface&MockObject $response;
32+
private Tree&MockObject $tree;
33+
34+
public static function afterPutData(): array {
35+
return [
36+
'owner and permissions present' => [
37+
'user', true, 'PERMISSIONS', true, 2
38+
],
39+
'permissions only' => [
40+
null, false, 'PERMISSIONS', true, 1
41+
],
42+
];
43+
}
44+
45+
public function testAfterPutNotFoundException(): void {
46+
$afterPut = null;
47+
$this->server->expects($this->once())
48+
->method('on')
49+
->willReturnCallback(
50+
function ($method, $callback) use (&$afterPut) {
51+
$this->assertSame('afterMethod:PUT', $method);
52+
$afterPut = $callback;
53+
});
54+
55+
$this->plugin->initialize($this->server);
56+
$node = $this->createMock(Node::class);
57+
$this->tree->expects($this->once())->method('getNodeForPath')
58+
->willThrowException(new NotFound());
59+
60+
$this->logger->expects($this->once())->method('error');
61+
62+
$afterPut($this->request, $this->response);
63+
}
64+
65+
#[DataProvider('afterPutData')]
66+
public function testAfterPut(?string $ownerId, bool $expectOwnerIdHeader,
67+
?string $permissions, bool $expectPermissionsHeader,
68+
int $expectedInvocations): void {
69+
$afterPut = null;
70+
$this->server->expects($this->once())
71+
->method('on')
72+
->willReturnCallback(
73+
function ($method, $callback) use (&$afterPut) {
74+
$this->assertSame('afterMethod:PUT', $method);
75+
$afterPut = $callback;
76+
});
77+
78+
$this->plugin->initialize($this->server);
79+
$node = $this->createMock(Node::class);
80+
$this->tree->expects($this->once())->method('getNodeForPath')
81+
->willReturn($node);
82+
83+
$user = $this->createMock(IUser::class);
84+
$node->expects($this->once())->method('getOwner')->willReturn($user);
85+
$user->expects($this->once())->method('getUID')->willReturn($ownerId);
86+
$node->expects($this->once())->method('getDavPermissions')->willReturn($permissions);
87+
88+
$matcher = $this->exactly($expectedInvocations);
89+
$this->response->expects($matcher)->method('setHeader')
90+
->willReturnCallback(function ($name, $value) use (
91+
$expectedInvocations,
92+
$expectPermissionsHeader,
93+
$expectOwnerIdHeader,
94+
$matcher,
95+
$ownerId, $permissions) {
96+
$invocationNumber = $matcher->numberOfInvocations();
97+
if ($invocationNumber === 0) {
98+
throw new LogicException('No invocations were expected');
99+
}
100+
101+
if (($expectOwnerIdHeader && $expectedInvocations === 1)
102+
|| ($expectedInvocations
103+
=== 2 && $invocationNumber === 1)) {
104+
$this->assertEquals('X-NC-OwnerId', $name);
105+
$this->assertEquals($ownerId, $value);
106+
}
107+
108+
if (($expectPermissionsHeader && $expectedInvocations === 1)
109+
|| ($expectedInvocations
110+
=== 2 && $invocationNumber === 2)) {
111+
$this->assertEquals('X-NC-Permissions', $name);
112+
$this->assertEquals($permissions, $value);
113+
}
114+
});
115+
116+
$afterPut($this->request, $this->response);
117+
}
118+
119+
protected function setUp(): void {
120+
parent::setUp();
121+
122+
$this->server = $this->createMock(Server::class);
123+
$this->tree = $this->createMock(Tree::class);
124+
$this->server->tree = $this->tree;
125+
$this->logger = $this->createMock(LoggerInterface::class);
126+
$this->plugin = new AddExtraHeadersPlugin($this->logger, false);
127+
$this->request = $this->createMock(RequestInterface::class);
128+
$this->response = $this->createMock(ResponseInterface::class);
129+
}
130+
}

0 commit comments

Comments
 (0)