Skip to content

Commit ed052e6

Browse files
Merge pull request #61128 from nextcloud/feat/ocp/interaction
feat(OCP): Add Interaction API
2 parents efe0f83 + fb17121 commit ed052e6

40 files changed

Lines changed: 2405 additions & 798 deletions

apps/files/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
'OCA\\Files\\Listener\\NodeAddedToFavoriteListener' => $baseDir . '/../lib/Listener/NodeAddedToFavoriteListener.php',
7878
'OCA\\Files\\Listener\\NodeRemovedFromFavoriteListener' => $baseDir . '/../lib/Listener/NodeRemovedFromFavoriteListener.php',
7979
'OCA\\Files\\Listener\\RenderReferenceEventListener' => $baseDir . '/../lib/Listener/RenderReferenceEventListener.php',
80+
'OCA\\Files\\Listener\\RestrictInteractionListener' => $baseDir . '/../lib/Listener/RestrictInteractionListener.php',
8081
'OCA\\Files\\Listener\\SyncLivePhotosListener' => $baseDir . '/../lib/Listener/SyncLivePhotosListener.php',
8182
'OCA\\Files\\Listener\\UserFirstTimeLoggedInListener' => $baseDir . '/../lib/Listener/UserFirstTimeLoggedInListener.php',
8283
'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php',

apps/files/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class ComposerStaticInitFiles
9292
'OCA\\Files\\Listener\\NodeAddedToFavoriteListener' => __DIR__ . '/..' . '/../lib/Listener/NodeAddedToFavoriteListener.php',
9393
'OCA\\Files\\Listener\\NodeRemovedFromFavoriteListener' => __DIR__ . '/..' . '/../lib/Listener/NodeRemovedFromFavoriteListener.php',
9494
'OCA\\Files\\Listener\\RenderReferenceEventListener' => __DIR__ . '/..' . '/../lib/Listener/RenderReferenceEventListener.php',
95+
'OCA\\Files\\Listener\\RestrictInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/RestrictInteractionListener.php',
9596
'OCA\\Files\\Listener\\SyncLivePhotosListener' => __DIR__ . '/..' . '/../lib/Listener/SyncLivePhotosListener.php',
9697
'OCA\\Files\\Listener\\UserFirstTimeLoggedInListener' => __DIR__ . '/..' . '/../lib/Listener/UserFirstTimeLoggedInListener.php',
9798
'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php',

apps/files/lib/AppInfo/Application.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use OCA\Files\Listener\NodeAddedToFavoriteListener;
2424
use OCA\Files\Listener\NodeRemovedFromFavoriteListener;
2525
use OCA\Files\Listener\RenderReferenceEventListener;
26+
use OCA\Files\Listener\RestrictInteractionListener;
2627
use OCA\Files\Listener\SyncLivePhotosListener;
2728
use OCA\Files\Listener\UserFirstTimeLoggedInListener;
2829
use OCA\Files\Notification\Notifier;
@@ -40,6 +41,7 @@
4041
use OCP\Files\Events\Node\NodeCopiedEvent;
4142
use OCP\Files\Events\NodeAddedToFavorite;
4243
use OCP\Files\Events\NodeRemovedFromFavorite;
44+
use OCP\Interaction\RestrictInteractionEvent;
4345
use OCP\Share\Events\ShareCreatedEvent;
4446
use OCP\Share\Events\ShareDeletedEvent;
4547
use OCP\Share\Events\ShareDeletedFromSelfEvent;
@@ -84,6 +86,7 @@ public function register(IRegistrationContext $context): void {
8486

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

89+
$context->registerEventListener(RestrictInteractionEvent::class, RestrictInteractionListener::class);
8790
}
8891

8992
#[\Override]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OCA\Files\Listener;
11+
12+
use OCA\Files\AppInfo\Application;
13+
use OCP\Constants;
14+
use OCP\EventDispatcher\Event;
15+
use OCP\EventDispatcher\IEventListener;
16+
use OCP\IL10N;
17+
use OCP\Interaction\InteractionRestrictedException;
18+
use OCP\Interaction\Resources\NodeResource;
19+
use OCP\Interaction\RestrictInteractionEvent;
20+
use OCP\L10N\IFactory;
21+
22+
/**
23+
* @template-implements IEventListener<RestrictInteractionEvent>
24+
*/
25+
final readonly class RestrictInteractionListener implements IEventListener {
26+
private IL10N $l10n;
27+
28+
public function __construct(
29+
IFactory $l10nFactory,
30+
) {
31+
$this->l10n = $l10nFactory->get(Application::APP_ID);
32+
}
33+
34+
/**
35+
* @param RestrictInteractionEvent $event
36+
*/
37+
#[\Override]
38+
public function handle(Event $event): void {
39+
if ($event->resource instanceof NodeResource && ($event->resource->getNodePermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ) {
40+
throw new InteractionRestrictedException('No read permission on the node.', $this->l10n->t('No read permission on file.'));
41+
}
42+
}
43+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OCA\Files\Tests\Listener;
11+
12+
use OCP\Constants;
13+
use OCP\Files\IRootFolder;
14+
use OCP\Files\ISetupManager;
15+
use OCP\Interaction\Resources\NodeResource;
16+
use OCP\Interaction\RestrictInteractionEvent;
17+
use OCP\IUser;
18+
use OCP\IUserManager;
19+
use OCP\Server;
20+
use PHPUnit\Framework\Attributes\Group;
21+
use Test\TestCase;
22+
23+
#[Group('DB')]
24+
final class RestrictInteractionListenerTest extends TestCase {
25+
private IUser $user;
26+
27+
#[\Override]
28+
protected function setUp(): void {
29+
parent::setUp();
30+
$user = Server::get(IUserManager::class)->createUser('user', 'password');
31+
$this->assertNotFalse($user);
32+
$this->user = $user;
33+
34+
Server::get(ISetupManager::class)->setupForUser($user);
35+
}
36+
37+
#[\Override]
38+
protected function tearDown(): void {
39+
Server::get(ISetupManager::class)->tearDown();
40+
41+
$this->assertTrue($this->user->delete());
42+
43+
parent::tearDown();
44+
}
45+
46+
public function testNodeResourceShareActionMissingReadPermission(): void {
47+
$userFolder = Server::get(IRootFolder::class)->getUserFolder($this->user->getUID());
48+
49+
$fileNode = $userFolder->newFile('foo.txt', 'bar');
50+
$fileNode->getStorage()->getCache()->update($fileNode->getId(), ['permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_READ]);
51+
52+
$folderNode = $userFolder->newFolder('foo');
53+
$folderNode->getStorage()->getCache()->update($folderNode->getId(), ['permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_READ]);
54+
55+
foreach ([$fileNode, $folderNode] as $node) {
56+
$event = new RestrictInteractionEvent($this->user->getUID(), $this->user, new NodeResource($node->getId(), $this->user->getUID(), $node), null, null);
57+
$this->assertEquals('No read permission on file.', $event->isInteractionRestricted());
58+
}
59+
}
60+
}

apps/files_sharing/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => $baseDir . '/../lib/Listener/LoadAdditionalListener.php',
7171
'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => $baseDir . '/../lib/Listener/LoadPublicFileRequestAuthListener.php',
7272
'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
73+
'OCA\\Files_Sharing\\Listener\\RestrictInteractionListener' => $baseDir . '/../lib/Listener/RestrictInteractionListener.php',
7374
'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => $baseDir . '/../lib/Listener/ShareInteractionListener.php',
7475
'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => $baseDir . '/../lib/Listener/SharesUpdatedListener.php',
7576
'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => $baseDir . '/../lib/Listener/UserAddedToGroupListener.php',

apps/files_sharing/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class ComposerStaticInitFiles_Sharing
8585
'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalListener.php',
8686
'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => __DIR__ . '/..' . '/../lib/Listener/LoadPublicFileRequestAuthListener.php',
8787
'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
88+
'OCA\\Files_Sharing\\Listener\\RestrictInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/RestrictInteractionListener.php',
8889
'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/ShareInteractionListener.php',
8990
'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => __DIR__ . '/..' . '/../lib/Listener/SharesUpdatedListener.php',
9091
'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => __DIR__ . '/..' . '/../lib/Listener/UserAddedToGroupListener.php',

apps/files_sharing/lib/AppInfo/Application.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use OCA\Files_Sharing\Listener\LoadAdditionalListener;
2525
use OCA\Files_Sharing\Listener\LoadPublicFileRequestAuthListener;
2626
use OCA\Files_Sharing\Listener\LoadSidebarListener;
27+
use OCA\Files_Sharing\Listener\RestrictInteractionListener;
2728
use OCA\Files_Sharing\Listener\ShareInteractionListener;
2829
use OCA\Files_Sharing\Listener\SharesUpdatedListener;
2930
use OCA\Files_Sharing\Listener\UserAddedToGroupListener;
@@ -56,6 +57,7 @@
5657
use OCP\IConfig;
5758
use OCP\IDBConnection;
5859
use OCP\IGroup;
60+
use OCP\Interaction\RestrictInteractionEvent;
5961
use OCP\Share\Events\BeforeShareDeletedEvent;
6062
use OCP\Share\Events\ShareCreatedEvent;
6163
use OCP\Share\Events\ShareMovedEvent;
@@ -132,6 +134,8 @@ function () use ($c) {
132134
$context->registerEventListener(UserHomeSetupEvent::class, UserHomeSetupListener::class);
133135

134136
$context->registerConfigLexicon(ConfigLexicon::class);
137+
138+
$context->registerEventListener(RestrictInteractionEvent::class, RestrictInteractionListener::class);
135139
}
136140

137141
#[\Override]

apps/files_sharing/lib/Controller/ShareAPIController.php

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -718,10 +718,6 @@ public function createShare(
718718
$share->setSharedWith($shareWith);
719719
$share->setPermissions($permissions);
720720
} elseif ($shareType === IShare::TYPE_GROUP) {
721-
if (!$this->shareManager->allowGroupSharing()) {
722-
throw new OCSNotFoundException($this->l->t('Group sharing is disabled by the administrator'));
723-
}
724-
725721
// Valid group is required to share
726722
if ($shareWith === null || !$this->groupManager->groupExists($shareWith)) {
727723
throw new OCSNotFoundException($this->l->t('Please specify a valid group'));
@@ -731,11 +727,6 @@ public function createShare(
731727
} elseif ($shareType === IShare::TYPE_LINK
732728
|| $shareType === IShare::TYPE_EMAIL) {
733729

734-
// Can we even share links?
735-
if (!$this->shareManager->shareApiAllowLinks()) {
736-
throw new OCSNotFoundException($this->l->t('Public link sharing is disabled by the administrator'));
737-
}
738-
739730
$this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload);
740731
$share->setPermissions($permissions);
741732

@@ -769,10 +760,6 @@ public function createShare(
769760
$share->setSendPasswordByTalk(true);
770761
}
771762
} elseif ($shareType === IShare::TYPE_REMOTE) {
772-
if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) {
773-
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]));
774-
}
775-
776763
if ($shareWith === null) {
777764
throw new OCSNotFoundException($this->l->t('Please specify a valid federated account ID'));
778765
}
@@ -781,10 +768,6 @@ public function createShare(
781768
$share->setPermissions($permissions);
782769
$share->setSharedWithDisplayName($this->getCachedFederatedDisplayName($shareWith, false));
783770
} elseif ($shareType === IShare::TYPE_REMOTE_GROUP) {
784-
if (!$this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
785-
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]));
786-
}
787-
788771
if ($shareWith === null) {
789772
throw new OCSNotFoundException($this->l->t('Please specify a valid federated group ID'));
790773
}
@@ -1043,12 +1026,6 @@ private function validateLinkSharePermissions(Node $node, int $permissions, ?boo
10431026
&& ($this->hasPermission($permissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($permissions, Constants::PERMISSION_DELETE))) {
10441027
throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set'));
10451028
}
1046-
1047-
// Check if public uploading was disabled
1048-
if ($this->hasPermission($permissions, Constants::PERMISSION_CREATE)
1049-
&& !$this->shareManager->shareApiLinkAllowPublicUpload()) {
1050-
throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator'));
1051-
}
10521029
}
10531030

10541031
/**
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
/*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OCA\Files_Sharing\Listener;
11+
12+
use OCA\Files_Sharing\AppInfo\Application;
13+
use OCP\Constants;
14+
use OCP\EventDispatcher\Event;
15+
use OCP\EventDispatcher\IEventListener;
16+
use OCP\Files\File;
17+
use OCP\Files\Folder;
18+
use OCP\Files\IRootFolder;
19+
use OCP\IL10N;
20+
use OCP\Interaction\Actions\ShareAction;
21+
use OCP\Interaction\InteractionRestrictedException;
22+
use OCP\Interaction\Receivers\EmailReceiver;
23+
use OCP\Interaction\Receivers\LinkReceiver;
24+
use OCP\Interaction\Resources\NodeResource;
25+
use OCP\Interaction\RestrictInteractionEvent;
26+
use OCP\L10N\IFactory;
27+
use OCP\Share\IManager;
28+
29+
/**
30+
* @template-implements IEventListener<RestrictInteractionEvent>
31+
*/
32+
final class RestrictInteractionListener implements IEventListener {
33+
private readonly IL10N $l10n;
34+
35+
public function __construct(
36+
private IRootFolder $rootFolder,
37+
private IManager $manager,
38+
IFactory $l10nFactory,
39+
) {
40+
$this->l10n = $l10nFactory->get(Application::APP_ID);
41+
}
42+
43+
/**
44+
* @param RestrictInteractionEvent $event
45+
*/
46+
#[\Override]
47+
public function handle(Event $event): void {
48+
if ($event->resource instanceof NodeResource && $event->action instanceof ShareAction) {
49+
if (!$event->resource->getNode()->isShareable()) {
50+
throw new InteractionRestrictedException('Node is not shareable.', $this->l10n->t('You are not allowed to share "%s".', [$event->resource->getNode()->getName()]));
51+
}
52+
53+
$userFolder = $this->rootFolder->getUserFolder($event->userId);
54+
if ($event->resource->nodeId === $userFolder->getId()) {
55+
throw new InteractionRestrictedException('Cannot share home folder node.', $this->l10n->t('You cannot share your home folder.'));
56+
}
57+
58+
if ($event->action->filesSharingPermissions !== null) {
59+
if (($event->action->filesSharingPermissions & ~$event->resource->getNodePermissions()) !== 0) {
60+
$path = $userFolder->getRelativePath($event->resource->getNode()->getPath());
61+
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]));
62+
}
63+
64+
if ($event->resource->getNode() instanceof File) {
65+
if (($event->action->filesSharingPermissions & Constants::PERMISSION_DELETE) === Constants::PERMISSION_DELETE) {
66+
throw new InteractionRestrictedException('Cannot share file node with delete permission.', $this->l10n->t('File cannot be shared with delete permission.'));
67+
}
68+
69+
if (($event->action->filesSharingPermissions & Constants::PERMISSION_CREATE) === Constants::PERMISSION_CREATE) {
70+
throw new InteractionRestrictedException('Cannot share file node with create permission.', $this->l10n->t('File cannot be shared with create permission.'));
71+
}
72+
}
73+
74+
if (!$event->receiver instanceof LinkReceiver
75+
&& !$event->receiver instanceof EmailReceiver
76+
&& ($event->action->filesSharingPermissions & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ) {
77+
throw new InteractionRestrictedException('No read permission on the share.', $this->l10n->t('File share needs at least read permission.'));
78+
}
79+
80+
if (($event->receiver instanceof LinkReceiver || $event->receiver instanceof EmailReceiver)
81+
&& $event->resource->getNode() instanceof Folder
82+
&& ($event->action->filesSharingPermissions & (Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE)) !== 0
83+
&& !$this->manager->shareApiLinkAllowPublicUpload()) {
84+
throw new InteractionRestrictedException('Public upload is not allowed.', $this->l10n->t('Public upload is not allowed.'));
85+
}
86+
}
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)