Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/files/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
'OCA\\Files\\Listener\\NodeAddedToFavoriteListener' => $baseDir . '/../lib/Listener/NodeAddedToFavoriteListener.php',
'OCA\\Files\\Listener\\NodeRemovedFromFavoriteListener' => $baseDir . '/../lib/Listener/NodeRemovedFromFavoriteListener.php',
'OCA\\Files\\Listener\\RenderReferenceEventListener' => $baseDir . '/../lib/Listener/RenderReferenceEventListener.php',
'OCA\\Files\\Listener\\RestrictInteractionListener' => $baseDir . '/../lib/Listener/RestrictInteractionListener.php',
'OCA\\Files\\Listener\\SyncLivePhotosListener' => $baseDir . '/../lib/Listener/SyncLivePhotosListener.php',
'OCA\\Files\\Listener\\UserFirstTimeLoggedInListener' => $baseDir . '/../lib/Listener/UserFirstTimeLoggedInListener.php',
'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php',
Expand Down
1 change: 1 addition & 0 deletions apps/files/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class ComposerStaticInitFiles
'OCA\\Files\\Listener\\NodeAddedToFavoriteListener' => __DIR__ . '/..' . '/../lib/Listener/NodeAddedToFavoriteListener.php',
'OCA\\Files\\Listener\\NodeRemovedFromFavoriteListener' => __DIR__ . '/..' . '/../lib/Listener/NodeRemovedFromFavoriteListener.php',
'OCA\\Files\\Listener\\RenderReferenceEventListener' => __DIR__ . '/..' . '/../lib/Listener/RenderReferenceEventListener.php',
'OCA\\Files\\Listener\\RestrictInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/RestrictInteractionListener.php',
'OCA\\Files\\Listener\\SyncLivePhotosListener' => __DIR__ . '/..' . '/../lib/Listener/SyncLivePhotosListener.php',
'OCA\\Files\\Listener\\UserFirstTimeLoggedInListener' => __DIR__ . '/..' . '/../lib/Listener/UserFirstTimeLoggedInListener.php',
'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php',
Expand Down
3 changes: 3 additions & 0 deletions apps/files/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use OCA\Files\Listener\NodeAddedToFavoriteListener;
use OCA\Files\Listener\NodeRemovedFromFavoriteListener;
use OCA\Files\Listener\RenderReferenceEventListener;
use OCA\Files\Listener\RestrictInteractionListener;
use OCA\Files\Listener\SyncLivePhotosListener;
use OCA\Files\Listener\UserFirstTimeLoggedInListener;
use OCA\Files\Notification\Notifier;
Expand All @@ -40,6 +41,7 @@
use OCP\Files\Events\Node\NodeCopiedEvent;
use OCP\Files\Events\NodeAddedToFavorite;
use OCP\Files\Events\NodeRemovedFromFavorite;
use OCP\Interaction\RestrictInteractionEvent;
use OCP\User\Events\UserFirstTimeLoggedInEvent;
use OCP\Util;

Expand Down Expand Up @@ -78,6 +80,7 @@ public function register(IRegistrationContext $context): void {

$context->registerConfigLexicon(ConfigLexicon::class);

$context->registerEventListener(RestrictInteractionEvent::class, RestrictInteractionListener::class);
}

#[\Override]
Expand Down
43 changes: 43 additions & 0 deletions apps/files/lib/Listener/RestrictInteractionListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);

namespace OCA\Files\Listener;

use OCA\Files\AppInfo\Application;
use OCP\Constants;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IL10N;
use OCP\Interaction\InteractionRestrictedException;
use OCP\Interaction\Resources\NodeResource;
use OCP\Interaction\RestrictInteractionEvent;
use OCP\L10N\IFactory;

/**
* @template-implements IEventListener<RestrictInteractionEvent>
*/
final readonly class RestrictInteractionListener implements IEventListener {
private IL10N $l10n;

public function __construct(
IFactory $l10nFactory,
) {
$this->l10n = $l10nFactory->get(Application::APP_ID);
}

/**
* @param RestrictInteractionEvent $event
*/
#[\Override]
public function handle(Event $event): void {
if ($event->resource instanceof NodeResource && ($event->resource->getNodePermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ) {
throw new InteractionRestrictedException('No read permission on the node.', $this->l10n->t('No read permission on file.'));
}
}
}
60 changes: 60 additions & 0 deletions apps/files/tests/Listener/RestrictInteractionListenerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);

namespace OCA\Files\Tests\Listener;

use OCP\Constants;
use OCP\Files\IRootFolder;
use OCP\Files\ISetupManager;
use OCP\Interaction\Resources\NodeResource;
use OCP\Interaction\RestrictInteractionEvent;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Server;
use PHPUnit\Framework\Attributes\Group;
use Test\TestCase;

#[Group('DB')]
final class RestrictInteractionListenerTest extends TestCase {
private IUser $user;

#[\Override]
protected function setUp(): void {
parent::setUp();
$user = Server::get(IUserManager::class)->createUser('user', 'password');
$this->assertNotFalse($user);
$this->user = $user;

Server::get(ISetupManager::class)->setupForUser($user);
}

#[\Override]
protected function tearDown(): void {
Server::get(ISetupManager::class)->tearDown();

$this->assertTrue($this->user->delete());

parent::tearDown();
}

public function testNodeResourceShareActionMissingReadPermission(): void {
$userFolder = Server::get(IRootFolder::class)->getUserFolder($this->user->getUID());

$fileNode = $userFolder->newFile('foo.txt', 'bar');
$fileNode->getStorage()->getCache()->update($fileNode->getId(), ['permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_READ]);

$folderNode = $userFolder->newFolder('foo');
$folderNode->getStorage()->getCache()->update($folderNode->getId(), ['permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_READ]);

foreach ([$fileNode, $folderNode] as $node) {
$event = new RestrictInteractionEvent($this->user->getUID(), $this->user, new NodeResource($node->getId(), $this->user->getUID(), $node), null, null);
$this->assertEquals('No read permission on file.', $event->isInteractionRestricted());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => $baseDir . '/../lib/Listener/LoadAdditionalListener.php',
'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => $baseDir . '/../lib/Listener/LoadPublicFileRequestAuthListener.php',
'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
'OCA\\Files_Sharing\\Listener\\RestrictInteractionListener' => $baseDir . '/../lib/Listener/RestrictInteractionListener.php',
'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => $baseDir . '/../lib/Listener/ShareInteractionListener.php',
'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => $baseDir . '/../lib/Listener/SharesUpdatedListener.php',
'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => $baseDir . '/../lib/Listener/UserAddedToGroupListener.php',
Expand Down
1 change: 1 addition & 0 deletions apps/files_sharing/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalListener.php',
'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => __DIR__ . '/..' . '/../lib/Listener/LoadPublicFileRequestAuthListener.php',
'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
'OCA\\Files_Sharing\\Listener\\RestrictInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/RestrictInteractionListener.php',
'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/ShareInteractionListener.php',
'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => __DIR__ . '/..' . '/../lib/Listener/SharesUpdatedListener.php',
'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => __DIR__ . '/..' . '/../lib/Listener/UserAddedToGroupListener.php',
Expand Down
4 changes: 4 additions & 0 deletions apps/files_sharing/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use OCA\Files_Sharing\Listener\LoadAdditionalListener;
use OCA\Files_Sharing\Listener\LoadPublicFileRequestAuthListener;
use OCA\Files_Sharing\Listener\LoadSidebarListener;
use OCA\Files_Sharing\Listener\RestrictInteractionListener;
use OCA\Files_Sharing\Listener\ShareInteractionListener;
use OCA\Files_Sharing\Listener\SharesUpdatedListener;
use OCA\Files_Sharing\Listener\UserAddedToGroupListener;
Expand Down Expand Up @@ -55,6 +56,7 @@
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroup;
use OCP\Interaction\RestrictInteractionEvent;
use OCP\Share\Events\BeforeShareDeletedEvent;
use OCP\Share\Events\ShareCreatedEvent;
use OCP\Share\Events\ShareMovedEvent;
Expand Down Expand Up @@ -131,6 +133,8 @@ function () use ($c) {
$context->registerEventListener(UserHomeSetupEvent::class, UserHomeSetupListener::class);

$context->registerConfigLexicon(ConfigLexicon::class);

$context->registerEventListener(RestrictInteractionEvent::class, RestrictInteractionListener::class);
}

#[\Override]
Expand Down
23 changes: 0 additions & 23 deletions apps/files_sharing/lib/Controller/ShareAPIController.php
Original file line number Diff line number Diff line change
Expand Up @@ -724,10 +724,6 @@ public function createShare(
$share->setSharedWith($shareWith);
$share->setPermissions($permissions);
} elseif ($shareType === IShare::TYPE_GROUP) {
if (!$this->shareManager->allowGroupSharing()) {
throw new OCSNotFoundException($this->l->t('Group sharing is disabled by the administrator'));
}

// Valid group is required to share
if ($shareWith === null || !$this->groupManager->groupExists($shareWith)) {
throw new OCSNotFoundException($this->l->t('Please specify a valid group'));
Expand All @@ -737,11 +733,6 @@ public function createShare(
} elseif ($shareType === IShare::TYPE_LINK
|| $shareType === IShare::TYPE_EMAIL) {

// Can we even share links?
if (!$this->shareManager->shareApiAllowLinks()) {
throw new OCSNotFoundException($this->l->t('Public link sharing is disabled by the administrator'));
}

$this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload);
$share->setPermissions($permissions);

Expand Down Expand Up @@ -775,10 +766,6 @@ public function createShare(
$share->setSendPasswordByTalk(true);
}
} elseif ($shareType === IShare::TYPE_REMOTE) {
if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) {
throw new OCSForbiddenException($this->l->t('Sharing %1$s failed because the back end does not allow shares from type %2$s', [$node->getPath(), $shareType]));
}

if ($shareWith === null) {
throw new OCSNotFoundException($this->l->t('Please specify a valid federated account ID'));
}
Expand All @@ -787,10 +774,6 @@ public function createShare(
$share->setPermissions($permissions);
$share->setSharedWithDisplayName($this->getCachedFederatedDisplayName($shareWith, false));
} elseif ($shareType === IShare::TYPE_REMOTE_GROUP) {
if (!$this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
throw new OCSForbiddenException($this->l->t('Sharing %1$s failed because the back end does not allow shares from type %2$s', [$node->getPath(), $shareType]));
}

if ($shareWith === null) {
throw new OCSNotFoundException($this->l->t('Please specify a valid federated group ID'));
}
Expand Down Expand Up @@ -1049,12 +1032,6 @@ private function validateLinkSharePermissions(Node $node, int $permissions, ?boo
&& ($this->hasPermission($permissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($permissions, Constants::PERMISSION_DELETE))) {
throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set'));
}

// Check if public uploading was disabled
if ($this->hasPermission($permissions, Constants::PERMISSION_CREATE)
&& !$this->shareManager->shareApiLinkAllowPublicUpload()) {
throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator'));
}
}

/**
Expand Down
89 changes: 89 additions & 0 deletions apps/files_sharing/lib/Listener/RestrictInteractionListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);

namespace OCA\Files_Sharing\Listener;

use OCA\Files_Sharing\AppInfo\Application;
use OCP\Constants;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IL10N;
use OCP\Interaction\Actions\ShareAction;
use OCP\Interaction\InteractionRestrictedException;
use OCP\Interaction\Receivers\EmailReceiver;
use OCP\Interaction\Receivers\LinkReceiver;
use OCP\Interaction\Resources\NodeResource;
use OCP\Interaction\RestrictInteractionEvent;
use OCP\L10N\IFactory;
use OCP\Share\IManager;

/**
* @template-implements IEventListener<RestrictInteractionEvent>
*/
final class RestrictInteractionListener implements IEventListener {
private readonly IL10N $l10n;

public function __construct(
private IRootFolder $rootFolder,
private IManager $manager,
IFactory $l10nFactory,
) {
$this->l10n = $l10nFactory->get(Application::APP_ID);
}

/**
* @param RestrictInteractionEvent $event
*/
#[\Override]
public function handle(Event $event): void {
if ($event->resource instanceof NodeResource && $event->action instanceof ShareAction) {
if (!$event->resource->getNode()->isShareable()) {
throw new InteractionRestrictedException('Node is not shareable.', $this->l10n->t('You are not allowed to share "%s".', [$event->resource->getNode()->getName()]));
}

$userFolder = $this->rootFolder->getUserFolder($event->userId);
if ($event->resource->nodeId === $userFolder->getId()) {
throw new InteractionRestrictedException('Cannot share home folder node.', $this->l10n->t('You cannot share your home folder.'));
}

if ($event->action->filesSharingPermissions !== null) {
if (($event->action->filesSharingPermissions & ~$event->resource->getNodePermissions()) !== 0) {
$path = $userFolder->getRelativePath($event->resource->getNode()->getPath());
throw new InteractionRestrictedException('Cannot share node with more permissions than the node already has.', $this->l10n->t('You cannot share "%s" with more permission than you have yourself.', [$path]));
}

if ($event->resource->getNode() instanceof File) {
if (($event->action->filesSharingPermissions & Constants::PERMISSION_DELETE) === Constants::PERMISSION_DELETE) {
throw new InteractionRestrictedException('Cannot share file node with delete permission.', $this->l10n->t('File cannot be shared with delete permission.'));
}

if (($event->action->filesSharingPermissions & Constants::PERMISSION_CREATE) === Constants::PERMISSION_CREATE) {
throw new InteractionRestrictedException('Cannot share file node with create permission.', $this->l10n->t('File cannot be shared with create permission.'));
}
}

if (!$event->receiver instanceof LinkReceiver
&& !$event->receiver instanceof EmailReceiver
&& ($event->action->filesSharingPermissions & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ) {
throw new InteractionRestrictedException('No read permission on the share.', $this->l10n->t('File share needs at least read permission.'));
}

if (($event->receiver instanceof LinkReceiver || $event->receiver instanceof EmailReceiver)
&& $event->resource->getNode() instanceof Folder
&& ($event->action->filesSharingPermissions & (Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE)) !== 0
&& !$this->manager->shareApiLinkAllowPublicUpload()) {
throw new InteractionRestrictedException('Public upload is not allowed.', $this->l10n->t('Public upload is not allowed.'));
}
}
}
}
}
Loading
Loading