Skip to content

Commit 57be2ee

Browse files
committed
feat(OCP): Add Interaction API
Signed-off-by: provokateurin <kate@provokateurin.de>
1 parent ed56cf1 commit 57be2ee

10 files changed

Lines changed: 275 additions & 0 deletions

build/rector-strict.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
$nextcloudDir . '/build/psalm/ITypedQueryBuilderTest.php',
2828
$nextcloudDir . '/lib/private/DB/QueryBuilder/TypedQueryBuilder.php',
2929
$nextcloudDir . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php',
30+
$nextcloudDir . '/lib/public/Interaction',
31+
$nextcloudDir . '/tests/lib/Interaction',
3032
])
3133
->withAutoloadPaths([
3234
// ensure rector properly autoload the public interfaces

lib/composer/composer/autoload_classmap.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,11 @@
664664
'OCP\\IUserSession' => $baseDir . '/lib/public/IUserSession.php',
665665
'OCP\\Image' => $baseDir . '/lib/public/Image.php',
666666
'OCP\\Install\\Events\\InstallationCompletedEvent' => $baseDir . '/lib/public/Install/Events/InstallationCompletedEvent.php',
667+
'OCP\\Interaction\\InteractionAction' => $baseDir . '/lib/public/Interaction/InteractionAction.php',
668+
'OCP\\Interaction\\InteractionReceiver' => $baseDir . '/lib/public/Interaction/InteractionReceiver.php',
669+
'OCP\\Interaction\\InteractionResource' => $baseDir . '/lib/public/Interaction/InteractionResource.php',
670+
'OCP\\Interaction\\InteractionRestrictedException' => $baseDir . '/lib/public/Interaction/InteractionRestrictedException.php',
671+
'OCP\\Interaction\\RestrictInteractionEvent' => $baseDir . '/lib/public/Interaction/RestrictInteractionEvent.php',
667672
'OCP\\L10N\\IFactory' => $baseDir . '/lib/public/L10N/IFactory.php',
668673
'OCP\\L10N\\ILanguageIterator' => $baseDir . '/lib/public/L10N/ILanguageIterator.php',
669674
'OCP\\LDAP\\Exceptions\\MultipleUsersReturnedException' => $baseDir . '/lib/public/LDAP/Exceptions/MultipleUsersReturnedException.php',

lib/composer/composer/autoload_static.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
705705
'OCP\\IUserSession' => __DIR__ . '/../../..' . '/lib/public/IUserSession.php',
706706
'OCP\\Image' => __DIR__ . '/../../..' . '/lib/public/Image.php',
707707
'OCP\\Install\\Events\\InstallationCompletedEvent' => __DIR__ . '/../../..' . '/lib/public/Install/Events/InstallationCompletedEvent.php',
708+
'OCP\\Interaction\\InteractionAction' => __DIR__ . '/../../..' . '/lib/public/Interaction/InteractionAction.php',
709+
'OCP\\Interaction\\InteractionReceiver' => __DIR__ . '/../../..' . '/lib/public/Interaction/InteractionReceiver.php',
710+
'OCP\\Interaction\\InteractionResource' => __DIR__ . '/../../..' . '/lib/public/Interaction/InteractionResource.php',
711+
'OCP\\Interaction\\InteractionRestrictedException' => __DIR__ . '/../../..' . '/lib/public/Interaction/InteractionRestrictedException.php',
712+
'OCP\\Interaction\\RestrictInteractionEvent' => __DIR__ . '/../../..' . '/lib/public/Interaction/RestrictInteractionEvent.php',
708713
'OCP\\L10N\\IFactory' => __DIR__ . '/../../..' . '/lib/public/L10N/IFactory.php',
709714
'OCP\\L10N\\ILanguageIterator' => __DIR__ . '/../../..' . '/lib/public/L10N/ILanguageIterator.php',
710715
'OCP\\LDAP\\Exceptions\\MultipleUsersReturnedException' => __DIR__ . '/../../..' . '/lib/public/LDAP/Exceptions/MultipleUsersReturnedException.php',
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCP\Interaction;
11+
12+
use OCP\AppFramework\Attribute\Implementable;
13+
14+
/**
15+
* @since 34.0.2
16+
*/
17+
#[Implementable(since: '34.0.2')]
18+
interface InteractionAction {
19+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCP\Interaction;
11+
12+
use OCP\AppFramework\Attribute\Implementable;
13+
14+
/**
15+
* @since 34.0.2
16+
*/
17+
#[Implementable(since: '34.0.2')]
18+
interface InteractionReceiver {
19+
/**
20+
* Returns the ID that uniquely identifies this receiver.
21+
*
22+
* @since 34.0.2
23+
*/
24+
public function getID(): ?string;
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCP\Interaction;
11+
12+
use OCP\AppFramework\Attribute\Implementable;
13+
14+
/**
15+
* @since 34.0.2
16+
*/
17+
#[Implementable(since: '34.0.2')]
18+
interface InteractionResource {
19+
/**
20+
* Returns the ID that uniquely identifies this resource.
21+
*
22+
* @since 34.0.2
23+
*/
24+
public function getID(): string;
25+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCP\Interaction;
11+
12+
use Exception;
13+
use OCP\AppFramework\Attribute\Implementable;
14+
15+
/**
16+
* @since 34.0.2
17+
*/
18+
#[Implementable(since: '34.0.2')]
19+
final class InteractionRestrictedException extends Exception {
20+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCP\Interaction;
11+
12+
use OCP\AppFramework\Attribute\Consumable;
13+
use OCP\EventDispatcher\Event;
14+
use OCP\EventDispatcher\IEventDispatcher;
15+
use OCP\IUser;
16+
use OCP\Log\Audit\CriticalActionPerformedEvent;
17+
use OCP\Server;
18+
19+
/**
20+
* This event can be emitted to check if a user is allowed to perform an action, such that the receiver would become aware of the resource.
21+
* If the resource has a distinction between an owner and a initiator, the event must be emitted for both.
22+
* Emitters may omit one or multiple properties, if they are not known.
23+
* Emitters must call {@see isInteractionRestricted} instead of dispatching the event manually, to ensure exception handling and audit logging is done correctly.
24+
* Listeners may ignore any of the properties and only check the ones that are relevant to them.
25+
* Listeners must throw an {@see InteractionRestrictedException}, if they want to restrict the interaction.
26+
*
27+
* @since 34.0.2
28+
*/
29+
#[Consumable(since: '34.0.2')]
30+
final class RestrictInteractionEvent extends Event {
31+
/**
32+
* @since 34.0.2
33+
*/
34+
public function __construct(
35+
public readonly IUser $user,
36+
public readonly ?InteractionResource $resource,
37+
public readonly ?InteractionAction $action,
38+
public readonly ?InteractionReceiver $receiver,
39+
) {
40+
parent::__construct();
41+
}
42+
43+
/**
44+
* @since 34.0.2
45+
*/
46+
public function isInteractionRestricted(): bool {
47+
$eventDispatcher = Server::get(IEventDispatcher::class);
48+
49+
$params = [
50+
$this->action instanceof InteractionAction ? $this->action::class : '?',
51+
$this->user->getUID(),
52+
$this->resource?->getID() ?? '?',
53+
$this->receiver?->getID() ?? '?',
54+
];
55+
56+
try {
57+
$eventDispatcher->dispatchTyped($this);
58+
59+
$eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent(
60+
'Interaction "%s" from user "%s" on "%s" to "%s" is allowed.',
61+
$params,
62+
));
63+
64+
return false;
65+
} catch (InteractionRestrictedException $interactionRestrictedException) {
66+
$eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent(
67+
'Interaction "%s" from user "%s" on "%s" to "%s" is restricted: ' . $interactionRestrictedException->getMessage(),
68+
$params,
69+
));
70+
71+
return true;
72+
}
73+
}
74+
}

psalm-strict.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
<file name="lib/public/Share/IShareHelper.php"/>
3838
<file name="lib/public/SystemTag/Events/*.php"/>
3939
<directory name="apps/appstore/lib" />
40+
<directory name="lib/public/Interaction"/>
41+
<directory name="tests/lib/Interaction"/>
4042
<ignoreFiles>
4143
<!-- Missing types of the AppFetcher and the OC_Apps class for return types -->
4244
<file name="apps/appstore/lib/Controller/ApiController.php" />
@@ -57,6 +59,8 @@
5759
<directory name="lib/private"/>
5860
<directory name="lib/public"/>
5961
<directory name="3rdparty"/>
62+
<directory name="tests/lib"/>
63+
<directory name="vendor-bin/phpunit/vendor/phpunit/phpunit"/>
6064
</extraFiles>
6165
<stubs>
6266
<!-- Psalm does not find methods in here through <extraFiles/> 🤷‍♀️ -->
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace Test\Interaction;
11+
12+
use OCP\EventDispatcher\IEventDispatcher;
13+
use OCP\Interaction\InteractionAction;
14+
use OCP\Interaction\InteractionReceiver;
15+
use OCP\Interaction\InteractionResource;
16+
use OCP\Interaction\InteractionRestrictedException;
17+
use OCP\Interaction\RestrictInteractionEvent;
18+
use OCP\IUser;
19+
use OCP\Log\Audit\CriticalActionPerformedEvent;
20+
use OCP\Server;
21+
use PHPUnit\Framework\Attributes\DataProvider;
22+
use Test\TestCase;
23+
24+
final class RestrictInteractionEventTest extends TestCase {
25+
/**
26+
* @return list<array{bool}>
27+
*/
28+
public static function dataIsInteractionRestricted(): array {
29+
return [
30+
[true],
31+
[false],
32+
];
33+
}
34+
35+
#[DataProvider('dataIsInteractionRestricted')]
36+
public function testIsInteractionRestricted(bool $isRestricted): void {
37+
$eventDispatcher = Server::get(IEventDispatcher::class);
38+
39+
$auditEvents = [];
40+
$auditEventListener = function (CriticalActionPerformedEvent $event) use (&$auditEvents): void {
41+
$auditEvents[] = $event;
42+
};
43+
$eventDispatcher->addListener(CriticalActionPerformedEvent::class, $auditEventListener);
44+
45+
/** @psalm-suppress UnusedClosureParam */
46+
$restrictInteractionEventListener = function (RestrictInteractionEvent $event) use ($isRestricted): void {
47+
if ($isRestricted) {
48+
throw new InteractionRestrictedException('my restriction');
49+
}
50+
};
51+
$eventDispatcher->addListener(RestrictInteractionEvent::class, $restrictInteractionEventListener);
52+
53+
$user = $this->createMock(IUser::class);
54+
$user
55+
->method('getUID')
56+
->willReturn('my-uid');
57+
58+
$resource = $this->createMock(InteractionResource::class);
59+
$resource
60+
->method('getID')
61+
->willReturn('my-resource');
62+
63+
$action = $this->createStub(InteractionAction::class);
64+
65+
$receiver = $this->createMock(InteractionReceiver::class);
66+
$receiver
67+
->method('getID')
68+
->willReturn('my-receiver');
69+
70+
$event = new RestrictInteractionEvent(
71+
$user,
72+
$resource,
73+
$action,
74+
$receiver,
75+
);
76+
77+
$this->assertEquals($isRestricted, $event->isInteractionRestricted());
78+
79+
$this->assertEquals([
80+
new CriticalActionPerformedEvent(
81+
$isRestricted
82+
? 'Interaction "%s" from user "%s" on "%s" to "%s" is restricted: my restriction'
83+
: 'Interaction "%s" from user "%s" on "%s" to "%s" is allowed.',
84+
[
85+
$action::class,
86+
'my-uid',
87+
'my-resource',
88+
'my-receiver',
89+
],
90+
),
91+
], $auditEvents);
92+
93+
$eventDispatcher->removeListener(CriticalActionPerformedEvent::class, $auditEventListener);
94+
$eventDispatcher->removeListener(RestrictInteractionEvent::class, $restrictInteractionEventListener);
95+
}
96+
}

0 commit comments

Comments
 (0)