mirror of
https://github.com/nextcloud/server.git
synced 2026-06-13 10:40:40 -04:00
Merge pull request #54542 from nextcloud/54088-providePermissionsAndOwnerIdInPut
This commit is contained in:
commit
7cb4b4bbdb
9 changed files with 219 additions and 9 deletions
|
|
@ -210,6 +210,7 @@ return array(
|
|||
'OCA\\DAV\\ConfigLexicon' => $baseDir . '/../lib/ConfigLexicon.php',
|
||||
'OCA\\DAV\\Connector\\LegacyDAVACL' => $baseDir . '/../lib/Connector/LegacyDAVACL.php',
|
||||
'OCA\\DAV\\Connector\\LegacyPublicAuth' => $baseDir . '/../lib/Connector/LegacyPublicAuth.php',
|
||||
'OCA\\DAV\\Connector\\Sabre\\AddExtraHeadersPlugin' => $baseDir . '/../lib/Connector/Sabre/AddExtraHeadersPlugin.php',
|
||||
'OCA\\DAV\\Connector\\Sabre\\AnonymousOptionsPlugin' => $baseDir . '/../lib/Connector/Sabre/AnonymousOptionsPlugin.php',
|
||||
'OCA\\DAV\\Connector\\Sabre\\AppleQuirksPlugin' => $baseDir . '/../lib/Connector/Sabre/AppleQuirksPlugin.php',
|
||||
'OCA\\DAV\\Connector\\Sabre\\Auth' => $baseDir . '/../lib/Connector/Sabre/Auth.php',
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\ConfigLexicon' => __DIR__ . '/..' . '/../lib/ConfigLexicon.php',
|
||||
'OCA\\DAV\\Connector\\LegacyDAVACL' => __DIR__ . '/..' . '/../lib/Connector/LegacyDAVACL.php',
|
||||
'OCA\\DAV\\Connector\\LegacyPublicAuth' => __DIR__ . '/..' . '/../lib/Connector/LegacyPublicAuth.php',
|
||||
'OCA\\DAV\\Connector\\Sabre\\AddExtraHeadersPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AddExtraHeadersPlugin.php',
|
||||
'OCA\\DAV\\Connector\\Sabre\\AnonymousOptionsPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AnonymousOptionsPlugin.php',
|
||||
'OCA\\DAV\\Connector\\Sabre\\AppleQuirksPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AppleQuirksPlugin.php',
|
||||
'OCA\\DAV\\Connector\\Sabre\\Auth' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Auth.php',
|
||||
|
|
|
|||
69
apps/dav/lib/Connector/Sabre/AddExtraHeadersPlugin.php
Normal file
69
apps/dav/lib/Connector/Sabre/AddExtraHeadersPlugin.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\DAV\Connector\Sabre;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
use Sabre\DAV\Server;
|
||||
use Sabre\HTTP\RequestInterface;
|
||||
use Sabre\HTTP\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Adds the "OC-OwnerId" and "OC-Permissions" after PUT requests so that
|
||||
* clients don't need to do a propfind after uploading a file to decide what
|
||||
* to display.
|
||||
*/
|
||||
class AddExtraHeadersPlugin extends \Sabre\DAV\ServerPlugin {
|
||||
|
||||
private ?Server $server = null;
|
||||
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private bool $isPublic = false,
|
||||
) {
|
||||
}
|
||||
|
||||
public function initialize(Server $server): void {
|
||||
$this->server = $server;
|
||||
|
||||
$server->on('afterMethod:PUT', $this->afterPut(...));
|
||||
}
|
||||
|
||||
private function afterPut(RequestInterface $request, ResponseInterface $response): void {
|
||||
if ($this->server === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$node = null;
|
||||
try {
|
||||
$node = $this->server->tree->getNodeForPath($request->getPath());
|
||||
} catch (NotFound) {
|
||||
$this->logger->error("Cannot set extra headers for non-existing file '{$request->getPath()}'");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$node instanceof Node) {
|
||||
$nodeType = get_debug_type($node);
|
||||
$this->logger->error("Cannot set extra headers for node of type {$nodeType} for file '{$request->getPath()}'");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->isPublic) {
|
||||
$ownerId = $node->getOwner()?->getUID();
|
||||
if ($ownerId !== null) {
|
||||
$response->setHeader('X-NC-OwnerId', $ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
$permissions = $this->isPublic ? $node->getPublicDavPermissions()
|
||||
: $node->getDavPermissions();
|
||||
|
||||
$response->setHeader('X-NC-Permissions', $permissions);
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ use Sabre\HTTP\RequestInterface;
|
|||
use Sabre\HTTP\ResponseInterface;
|
||||
|
||||
class FilesPlugin extends ServerPlugin {
|
||||
|
||||
// namespace
|
||||
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
|
||||
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
|
||||
|
|
@ -311,12 +312,7 @@ class FilesPlugin extends ServerPlugin {
|
|||
});
|
||||
|
||||
$propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
|
||||
$perms = $node->getDavPermissions();
|
||||
if ($this->isPublic) {
|
||||
// remove mount information
|
||||
$perms = str_replace(['S', 'M'], '', $perms);
|
||||
}
|
||||
return $perms;
|
||||
return $this->isPublic ? $node->getPublicDavPermissions() : $node->getDavPermissions();
|
||||
});
|
||||
|
||||
$propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
|
||||
|
|
|
|||
|
|
@ -323,6 +323,13 @@ abstract class Node implements INode {
|
|||
return DavUtil::getDavPermissions($this->info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the DAV Permissions with share and mount infromation stripped.
|
||||
*/
|
||||
public function getPublicDavPermissions(): string {
|
||||
return str_replace(['S', 'M'], '', $this->getDavPermissions());
|
||||
}
|
||||
|
||||
public function getOwner(): ?IUser {
|
||||
return $this->info->getOwner();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ class ServerFactory {
|
|||
);
|
||||
}
|
||||
$server->addPlugin(new CopyEtagHeaderPlugin());
|
||||
$server->addPlugin(new AddExtraHeadersPlugin($this->logger, $isPublicShare));
|
||||
|
||||
// Load dav plugins from apps
|
||||
$event = new SabrePluginEvent($server);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ use OCA\DAV\CardDAV\PhotoCache;
|
|||
use OCA\DAV\CardDAV\Security\CardDavRateLimitingPlugin;
|
||||
use OCA\DAV\CardDAV\Validation\CardDavValidatePlugin;
|
||||
use OCA\DAV\Comments\CommentsPlugin;
|
||||
use OCA\DAV\Connector\Sabre\AddExtraHeadersPlugin;
|
||||
use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
|
||||
use OCA\DAV\Connector\Sabre\AppleQuirksPlugin;
|
||||
use OCA\DAV\Connector\Sabre\Auth;
|
||||
|
|
@ -385,6 +386,7 @@ class Server {
|
|||
)
|
||||
);
|
||||
}
|
||||
$this->server->addPlugin(new AddExtraHeadersPlugin($logger, false));
|
||||
$this->server->addPlugin(new EnablePlugin(
|
||||
\OCP\Server::get(IConfig::class),
|
||||
\OCP\Server::get(BirthdayService::class),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace unit\Connector\Sabre;
|
||||
|
||||
use LogicException;
|
||||
use OCA\DAV\Connector\Sabre\AddExtraHeadersPlugin;
|
||||
use OCA\DAV\Connector\Sabre\Node;
|
||||
use OCA\DAV\Connector\Sabre\Server;
|
||||
use OCP\IUser;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
use Sabre\DAV\Tree;
|
||||
use Sabre\HTTP\RequestInterface;
|
||||
use Sabre\HTTP\ResponseInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
class AddExtraHeadersPluginTest extends TestCase {
|
||||
|
||||
private AddExtraHeadersPlugin $plugin;
|
||||
private Server&MockObject $server;
|
||||
private LoggerInterface&MockObject $logger;
|
||||
private RequestInterface&MockObject $request;
|
||||
private ResponseInterface&MockObject $response;
|
||||
private Tree&MockObject $tree;
|
||||
|
||||
public static function afterPutData(): array {
|
||||
return [
|
||||
'owner and permissions present' => [
|
||||
'user', true, 'PERMISSIONS', true, 2
|
||||
],
|
||||
'permissions only' => [
|
||||
null, false, 'PERMISSIONS', true, 1
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testAfterPutNotFoundException(): void {
|
||||
$afterPut = null;
|
||||
$this->server->expects($this->once())
|
||||
->method('on')
|
||||
->willReturnCallback(
|
||||
function ($method, $callback) use (&$afterPut) {
|
||||
$this->assertSame('afterMethod:PUT', $method);
|
||||
$afterPut = $callback;
|
||||
});
|
||||
|
||||
$this->plugin->initialize($this->server);
|
||||
$node = $this->createMock(Node::class);
|
||||
$this->tree->expects($this->once())->method('getNodeForPath')
|
||||
->willThrowException(new NotFound());
|
||||
|
||||
$this->logger->expects($this->once())->method('error');
|
||||
|
||||
$afterPut($this->request, $this->response);
|
||||
}
|
||||
|
||||
#[DataProvider('afterPutData')]
|
||||
public function testAfterPut(?string $ownerId, bool $expectOwnerIdHeader,
|
||||
?string $permissions, bool $expectPermissionsHeader,
|
||||
int $expectedInvocations): void {
|
||||
$afterPut = null;
|
||||
$this->server->expects($this->once())
|
||||
->method('on')
|
||||
->willReturnCallback(
|
||||
function ($method, $callback) use (&$afterPut) {
|
||||
$this->assertSame('afterMethod:PUT', $method);
|
||||
$afterPut = $callback;
|
||||
});
|
||||
|
||||
$this->plugin->initialize($this->server);
|
||||
$node = $this->createMock(Node::class);
|
||||
$this->tree->expects($this->once())->method('getNodeForPath')
|
||||
->willReturn($node);
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$node->expects($this->once())->method('getOwner')->willReturn($user);
|
||||
$user->expects($this->once())->method('getUID')->willReturn($ownerId);
|
||||
$node->expects($this->once())->method('getDavPermissions')->willReturn($permissions);
|
||||
|
||||
$matcher = $this->exactly($expectedInvocations);
|
||||
$this->response->expects($matcher)->method('setHeader')
|
||||
->willReturnCallback(function ($name, $value) use (
|
||||
$expectedInvocations,
|
||||
$expectPermissionsHeader,
|
||||
$expectOwnerIdHeader,
|
||||
$matcher,
|
||||
$ownerId, $permissions) {
|
||||
$invocationNumber = $matcher->numberOfInvocations();
|
||||
if ($invocationNumber === 0) {
|
||||
throw new LogicException('No invocations were expected');
|
||||
}
|
||||
|
||||
if (($expectOwnerIdHeader && $expectedInvocations === 1)
|
||||
|| ($expectedInvocations
|
||||
=== 2 && $invocationNumber === 1)) {
|
||||
$this->assertEquals('X-NC-OwnerId', $name);
|
||||
$this->assertEquals($ownerId, $value);
|
||||
}
|
||||
|
||||
if (($expectPermissionsHeader && $expectedInvocations === 1)
|
||||
|| ($expectedInvocations
|
||||
=== 2 && $invocationNumber === 2)) {
|
||||
$this->assertEquals('X-NC-Permissions', $name);
|
||||
$this->assertEquals($permissions, $value);
|
||||
}
|
||||
});
|
||||
|
||||
$afterPut($this->request, $this->response);
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->server = $this->createMock(Server::class);
|
||||
$this->tree = $this->createMock(Tree::class);
|
||||
$this->server->tree = $this->tree;
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->plugin = new AddExtraHeadersPlugin($this->logger, false);
|
||||
$this->request = $this->createMock(RequestInterface::class);
|
||||
$this->response = $this->createMock(ResponseInterface::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -332,9 +332,12 @@ class FilesPluginTest extends TestCase {
|
|||
|
||||
/** @var File&MockObject $node */
|
||||
$node = $this->createTestNode(File::class);
|
||||
$node->expects($this->any())
|
||||
->method('getDavPermissions')
|
||||
->willReturn('DWCKMSR');
|
||||
$node->expects($this->once())
|
||||
->method('getPublicDavPermissions')
|
||||
->willReturn('DWCKR');
|
||||
|
||||
$node->expects($this->never())
|
||||
->method('getDavPermissions');
|
||||
|
||||
$this->plugin->handleGetProperties(
|
||||
$propFind,
|
||||
|
|
|
|||
Loading…
Reference in a new issue