Skip to content

Commit 0a0a96a

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

10 files changed

Lines changed: 269 additions & 2 deletions

build/rector-strict.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
$nextcloudDir . '/build/psalm/ITypedQueryBuilderTest.php',
2626
$nextcloudDir . '/lib/private/DB/QueryBuilder/TypedQueryBuilder.php',
2727
$nextcloudDir . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php',
28+
$nextcloudDir . '/lib/public/Interaction',
29+
$nextcloudDir . '/tests/lib/Interaction',
2830
])
2931
->withAutoloadPaths([
3032
// ensure rector properly autoload the public interfaces

lib/composer/composer/autoload_classmap.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,10 @@
662662
'OCP\\IUserSession' => $baseDir . '/lib/public/IUserSession.php',
663663
'OCP\\Image' => $baseDir . '/lib/public/Image.php',
664664
'OCP\\Install\\Events\\InstallationCompletedEvent' => $baseDir . '/lib/public/Install/Events/InstallationCompletedEvent.php',
665+
'OCP\\Interaction\\InteractionAction' => $baseDir . '/lib/public/Interaction/InteractionAction.php',
666+
'OCP\\Interaction\\InteractionReceiver' => $baseDir . '/lib/public/Interaction/InteractionReceiver.php',
667+
'OCP\\Interaction\\InteractionResource' => $baseDir . '/lib/public/Interaction/InteractionResource.php',
668+
'OCP\\Interaction\\RestrictInteractionEvent' => $baseDir . '/lib/public/Interaction/RestrictInteractionEvent.php',
665669
'OCP\\L10N\\IFactory' => $baseDir . '/lib/public/L10N/IFactory.php',
666670
'OCP\\L10N\\ILanguageIterator' => $baseDir . '/lib/public/L10N/ILanguageIterator.php',
667671
'OCP\\LDAP\\Exceptions\\MultipleUsersReturnedException' => $baseDir . '/lib/public/LDAP/Exceptions/MultipleUsersReturnedException.php',

lib/composer/composer/autoload_static.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
703703
'OCP\\IUserSession' => __DIR__ . '/../../..' . '/lib/public/IUserSession.php',
704704
'OCP\\Image' => __DIR__ . '/../../..' . '/lib/public/Image.php',
705705
'OCP\\Install\\Events\\InstallationCompletedEvent' => __DIR__ . '/../../..' . '/lib/public/Install/Events/InstallationCompletedEvent.php',
706+
'OCP\\Interaction\\InteractionAction' => __DIR__ . '/../../..' . '/lib/public/Interaction/InteractionAction.php',
707+
'OCP\\Interaction\\InteractionReceiver' => __DIR__ . '/../../..' . '/lib/public/Interaction/InteractionReceiver.php',
708+
'OCP\\Interaction\\InteractionResource' => __DIR__ . '/../../..' . '/lib/public/Interaction/InteractionResource.php',
709+
'OCP\\Interaction\\RestrictInteractionEvent' => __DIR__ . '/../../..' . '/lib/public/Interaction/RestrictInteractionEvent.php',
706710
'OCP\\L10N\\IFactory' => __DIR__ . '/../../..' . '/lib/public/L10N/IFactory.php',
707711
'OCP\\L10N\\ILanguageIterator' => __DIR__ . '/../../..' . '/lib/public/L10N/ILanguageIterator.php',
708712
'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.1
16+
*/
17+
#[Implementable(since: '34.0.1')]
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.1
16+
*/
17+
#[Implementable(since: '34.0.1')]
18+
interface InteractionReceiver {
19+
/**
20+
* Returns the ID that uniquely identifies this receiver.
21+
*
22+
* @since 34.0.1
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.1
16+
*/
17+
#[Implementable(since: '34.0.1')]
18+
interface InteractionResource {
19+
/**
20+
* Returns the ID that uniquely identifies this resource.
21+
*
22+
* @since 34.0.1
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.1
17+
*/
18+
#[Implementable(since: '34.0.1')]
19+
final class InteractionRestrictedException extends Exception {
20+
}
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: 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+
* Emitters may omit one or multiple properties, if they are not known.
22+
* Emitters must call {@see isInteractionRestricted} instead of dispatching the event manually, to ensure exception handling and audit logging is done correctly.
23+
* Listeners may ignore any of the properties and only check the ones that are relevant to them.
24+
* Listeners must throw an {@see InteractionRestrictedException}, if they want to restrict the interaction.
25+
*
26+
* @since 34.0.1
27+
*/
28+
#[Consumable(since: '34.0.1')]
29+
final class RestrictInteractionEvent extends Event {
30+
/**
31+
* @since 34.0.1
32+
*/
33+
public function __construct(
34+
public readonly IUser $initiator,
35+
public readonly ?InteractionResource $resource,
36+
public readonly ?InteractionAction $action,
37+
public readonly ?InteractionReceiver $receiver,
38+
) {
39+
parent::__construct();
40+
}
41+
42+
/**
43+
* @since 34.0.1
44+
*/
45+
public function isInteractionRestricted(): bool {
46+
$eventDispatcher = Server::get(IEventDispatcher::class);
47+
48+
$params = [
49+
$this->action instanceof InteractionAction ? $this->action::class : '?',
50+
$this->initiator->getUID(),
51+
$this->resource?->getID() ?? '?',
52+
$this->receiver?->getID() ?? '?',
53+
];
54+
55+
try {
56+
$eventDispatcher->dispatchTyped($this);
57+
58+
$eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent(
59+
'Interaction "%s" from user "%s" on "%s" to "%s" is allowed.',
60+
$params,
61+
));
62+
63+
return false;
64+
} catch (InteractionRestrictedException $interactionRestrictedException) {
65+
$eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent(
66+
'Interaction "%s" from user "%s" on "%s" to "%s" is restricted: ' . $interactionRestrictedException->getMessage(),
67+
$params,
68+
));
69+
70+
return true;
71+
}
72+
}
73+
}

psalm-strict.xml

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

0 commit comments

Comments
 (0)