Skip to content

Commit b0aac33

Browse files
Merge pull request #61543 from nextcloud/feat/noid/shareReviewDeleteEvent
feat(share): add public share-review API under OCP\Share\ShareReview
2 parents ae1e771 + 74cde74 commit b0aac33

11 files changed

Lines changed: 664 additions & 0 deletions

lib/composer/composer/autoload_classmap.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,11 @@
891891
'OCP\\Share\\IShareProviderSupportsAccept' => $baseDir . '/lib/public/Share/IShareProviderSupportsAccept.php',
892892
'OCP\\Share\\IShareProviderSupportsAllSharesInFolder' => $baseDir . '/lib/public/Share/IShareProviderSupportsAllSharesInFolder.php',
893893
'OCP\\Share\\IShareProviderWithNotification' => $baseDir . '/lib/public/Share/IShareProviderWithNotification.php',
894+
'OCP\\Share\\ShareReview\\Events\\ShareReviewAccessCheckEvent' => $baseDir . '/lib/public/Share/ShareReview/Events/ShareReviewAccessCheckEvent.php',
895+
'OCP\\Share\\ShareReview\\IShareReviewSource' => $baseDir . '/lib/public/Share/ShareReview/IShareReviewSource.php',
896+
'OCP\\Share\\ShareReview\\RegisterShareReviewSourceEvent' => $baseDir . '/lib/public/Share/ShareReview/RegisterShareReviewSourceEvent.php',
897+
'OCP\\Share\\ShareReview\\ShareReviewEntry' => $baseDir . '/lib/public/Share/ShareReview/ShareReviewEntry.php',
898+
'OCP\\Share\\ShareReview\\ShareReviewPermission' => $baseDir . '/lib/public/Share/ShareReview/ShareReviewPermission.php',
894899
'OCP\\Snowflake\\ISnowflakeDecoder' => $baseDir . '/lib/public/Snowflake/ISnowflakeDecoder.php',
895900
'OCP\\Snowflake\\ISnowflakeGenerator' => $baseDir . '/lib/public/Snowflake/ISnowflakeGenerator.php',
896901
'OCP\\Snowflake\\Snowflake' => $baseDir . '/lib/public/Snowflake/Snowflake.php',

lib/composer/composer/autoload_static.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
932932
'OCP\\Share\\IShareProviderSupportsAccept' => __DIR__ . '/../../..' . '/lib/public/Share/IShareProviderSupportsAccept.php',
933933
'OCP\\Share\\IShareProviderSupportsAllSharesInFolder' => __DIR__ . '/../../..' . '/lib/public/Share/IShareProviderSupportsAllSharesInFolder.php',
934934
'OCP\\Share\\IShareProviderWithNotification' => __DIR__ . '/../../..' . '/lib/public/Share/IShareProviderWithNotification.php',
935+
'OCP\\Share\\ShareReview\\Events\\ShareReviewAccessCheckEvent' => __DIR__ . '/../../..' . '/lib/public/Share/ShareReview/Events/ShareReviewAccessCheckEvent.php',
936+
'OCP\\Share\\ShareReview\\IShareReviewSource' => __DIR__ . '/../../..' . '/lib/public/Share/ShareReview/IShareReviewSource.php',
937+
'OCP\\Share\\ShareReview\\RegisterShareReviewSourceEvent' => __DIR__ . '/../../..' . '/lib/public/Share/ShareReview/RegisterShareReviewSourceEvent.php',
938+
'OCP\\Share\\ShareReview\\ShareReviewEntry' => __DIR__ . '/../../..' . '/lib/public/Share/ShareReview/ShareReviewEntry.php',
939+
'OCP\\Share\\ShareReview\\ShareReviewPermission' => __DIR__ . '/../../..' . '/lib/public/Share/ShareReview/ShareReviewPermission.php',
935940
'OCP\\Snowflake\\ISnowflakeDecoder' => __DIR__ . '/../../..' . '/lib/public/Snowflake/ISnowflakeDecoder.php',
936941
'OCP\\Snowflake\\ISnowflakeGenerator' => __DIR__ . '/../../..' . '/lib/public/Snowflake/ISnowflakeGenerator.php',
937942
'OCP\\Snowflake\\Snowflake' => __DIR__ . '/../../..' . '/lib/public/Snowflake/Snowflake.php',
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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\Share\ShareReview\Events;
11+
12+
use OCP\AppFramework\Attribute\Consumable;
13+
use OCP\EventDispatcher\Event;
14+
15+
/**
16+
* Authorization gate for deleting an app-managed share through a share-review app.
17+
*
18+
* Background: Apps such as Deck or Tables manage their own shares outside of
19+
* the regular sharing backend ({@see \OCP\Share\IManager}). They can expose
20+
* those shares to a share-review app — a compliance tool that lets designated
21+
* operators audit and revoke shares across all apps — by implementing
22+
* {@see \OCP\Share\ShareReview\IShareReviewSource}. When a share-review
23+
* operator requests the deletion of such a share, the deletion is executed by
24+
* the app that owns the share, not by the share-review app. The owning app
25+
* has no way of knowing whether the acting user is actually authorized to
26+
* perform share reviews — only the share-review app knows that. This event
27+
* closes that gap: it lets the owning app ask "may the current user delete
28+
* this share on behalf of a share review?" before deleting anything.
29+
*
30+
* Dispatched by: the app that owns the share, i.e. the
31+
* {@see \OCP\Share\ShareReview\IShareReviewSource} implementation, at the
32+
* beginning of its deleteShare() method:
33+
*
34+
* public function deleteShare(string $shareId): bool {
35+
* $event = new ShareReviewAccessCheckEvent('MyApp', $shareId);
36+
* $this->dispatcher->dispatchTyped($event);
37+
* if (!$event->isHandled() || !$event->isGranted()) {
38+
* return false; // default-deny: no listener means no access
39+
* }
40+
* // ... actually delete the share ...
41+
* }
42+
*
43+
* Listened to by: the share-review app. Its listener decides whether the
44+
* current user is an authorized share-review operator (e.g. the app is
45+
* enabled for the user) and answers with grantAccess() or denyAccess():
46+
*
47+
* public function handle(Event $event): void {
48+
* if (!$event instanceof ShareReviewAccessCheckEvent) {
49+
* return;
50+
* }
51+
* if ($this->isShareReviewOperator()) {
52+
* $event->grantAccess();
53+
* } else {
54+
* $event->denyAccess('User is not a share-review operator.');
55+
* }
56+
* }
57+
*
58+
* Apps that merely expose shares must not listen to this event; answering it
59+
* is the responsibility of the share-review app that triggered the deletion.
60+
*
61+
* Semantics:
62+
* - Default-deny: if no listener responds (isHandled() is false, e.g. no
63+
* share-review app is installed), the dispatcher must not delete the share.
64+
* - Deny wins: once denyAccess() is called, further grantAccess() calls are
65+
* ignored and propagation is stopped immediately.
66+
* - Multiple grants are harmless; the last listener to deny is authoritative.
67+
*
68+
* @since 34.0.2
69+
*/
70+
#[Consumable(since: '34.0.2')]
71+
class ShareReviewAccessCheckEvent extends Event {
72+
73+
private bool $handled = false;
74+
private bool $granted = false;
75+
private ?string $reason = null;
76+
77+
/**
78+
* @param string $sourceName Stable, non-translated identifier for the app
79+
* registering the share source (e.g. 'Deck', 'Tables').
80+
* @param string $shareId App-internal identifier of the share being deleted.
81+
*
82+
* @since 34.0.2
83+
*/
84+
public function __construct(
85+
private readonly string $sourceName,
86+
private readonly string $shareId,
87+
) {
88+
parent::__construct();
89+
}
90+
91+
/**
92+
* Stable, non-translated identifier of the app that owns this share source.
93+
*
94+
* @since 34.0.2
95+
*/
96+
public function getSourceName(): string {
97+
return $this->sourceName;
98+
}
99+
100+
/**
101+
* App-internal identifier of the share being deleted.
102+
*
103+
* @since 34.0.2
104+
*/
105+
public function getShareId(): string {
106+
return $this->shareId;
107+
}
108+
109+
/**
110+
* Grant access to delete the share.
111+
*
112+
* Has no effect if denyAccess() was already called on this event — deny wins.
113+
*
114+
* @since 34.0.2
115+
*/
116+
public function grantAccess(): void {
117+
if ($this->handled && !$this->granted) {
118+
return; // deny wins — a prior denyAccess() cannot be escalated to a grant
119+
}
120+
$this->handled = true;
121+
$this->granted = true;
122+
}
123+
124+
/**
125+
* Deny access and provide a human-readable reason.
126+
*
127+
* Stops event propagation immediately — no further listeners will run.
128+
*
129+
* @since 34.0.2
130+
*/
131+
public function denyAccess(string $reason): void {
132+
$this->handled = true;
133+
$this->granted = false;
134+
$this->reason = $reason;
135+
$this->stopPropagation();
136+
}
137+
138+
/**
139+
* Whether any listener has responded to this event.
140+
*
141+
* @since 34.0.2
142+
*/
143+
public function isHandled(): bool {
144+
return $this->handled;
145+
}
146+
147+
/**
148+
* Whether access was granted.
149+
*
150+
* @since 34.0.2
151+
*/
152+
public function isGranted(): bool {
153+
return $this->granted;
154+
}
155+
156+
/**
157+
* Human-readable denial reason, or null if access was granted or the event
158+
* has not been handled yet.
159+
*
160+
* @since 34.0.2
161+
*/
162+
public function getReason(): ?string {
163+
return $this->reason;
164+
}
165+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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\Share\ShareReview;
11+
12+
use OCP\AppFramework\Attribute\Implementable;
13+
14+
/**
15+
* Interface to be implemented by apps that want to expose their app-managed
16+
* shares to a share-review app. Implementations are registered through
17+
* {@see RegisterShareReviewSourceEvent} and resolved from the dependency
18+
* injection container.
19+
*
20+
* @since 34.0.2
21+
*/
22+
#[Implementable(since: '34.0.2')]
23+
interface IShareReviewSource {
24+
/**
25+
* The name of the app, used in the review table
26+
*
27+
* @since 34.0.2
28+
*/
29+
public function getName(): string;
30+
31+
/**
32+
* Return all app-specific shares.
33+
*
34+
* The app name is added by the share-review app from getName().
35+
*
36+
* @return list<ShareReviewEntry>
37+
*
38+
* @since 34.0.2
39+
*/
40+
public function getShares(): array;
41+
42+
/**
43+
* Delete an app-specific share.
44+
*
45+
* @since 34.0.2
46+
*/
47+
public function deleteShare(string $shareId): bool;
48+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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\Share\ShareReview;
11+
12+
use OCP\AppFramework\Attribute\Dispatchable;
13+
use OCP\AppFramework\Attribute\Listenable;
14+
use OCP\EventDispatcher\Event;
15+
16+
/**
17+
* Event dispatched by a share-review app to collect share sources from other
18+
* apps. Listeners register the class name of their {@see IShareReviewSource}
19+
* implementation; the share-review app resolves it from the dependency
20+
* injection container.
21+
*
22+
* @since 34.0.2
23+
*/
24+
#[Listenable(since: '34.0.2')]
25+
#[Dispatchable(since: '34.0.2')]
26+
class RegisterShareReviewSourceEvent extends Event {
27+
28+
/** @var array<int, class-string<IShareReviewSource>> */
29+
private array $sources = [];
30+
31+
/**
32+
* @param class-string<IShareReviewSource> $source
33+
* @since 34.0.2
34+
*/
35+
public function registerSource(string $source): void {
36+
$this->sources[] = $source;
37+
}
38+
39+
/**
40+
* @return array<int, class-string<IShareReviewSource>>
41+
* @since 34.0.2
42+
*/
43+
public function getSources(): array {
44+
return $this->sources;
45+
}
46+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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\Share\ShareReview;
11+
12+
use OCP\AppFramework\Attribute\Consumable;
13+
use OCP\Share\IShare;
14+
15+
/**
16+
* Holds a single app-managed share as exposed to a share-review app through
17+
* {@see IShareReviewSource::getShares()}.
18+
*
19+
* @since 34.0.2
20+
*/
21+
#[Consumable(since: '34.0.2')]
22+
final class ShareReviewEntry {
23+
/**
24+
* @param string $id Unique app-specific identifier for the share, passed
25+
* to {@see IShareReviewSource::deleteShare()}.
26+
* @param string $object Name or title of the shared object, such as a
27+
* file path or report name.
28+
* @param string $initiator User ID of the initiator.
29+
* @param IShare::TYPE_* $type {@see \OCP\Share\IShare} type of the share.
30+
* @param string $recipient User ID of the owner or the token of a link.
31+
* @param int $lastModifiedTimestamp Unix timestamp of the share's creation
32+
* or last modification, whichever is
33+
* later; used for sorting and for the
34+
* new-since-last-review filter. Pass 0
35+
* if the app tracks neither.
36+
* @param list<ShareReviewPermission> $permissions Permissions granted by
37+
* the share. An empty list
38+
* means the share grants
39+
* nothing beyond existing.
40+
* @param string $action Optional deletion identifier override. An empty
41+
* string means $id is used.
42+
* @param bool $hasPassword Whether the share is password protected. Never
43+
* the password itself.
44+
* @param int|null $expirationTimestamp Optional expiration Unix timestamp
45+
* of the share.
46+
* @param string|null $parent Optional identifier of the parent share.
47+
*
48+
* @since 34.0.2
49+
*/
50+
public function __construct(
51+
public readonly string $id,
52+
public readonly string $object,
53+
public readonly string $initiator,
54+
public readonly int $type,
55+
public readonly string $recipient,
56+
public readonly int $lastModifiedTimestamp,
57+
public readonly array $permissions = [],
58+
public readonly string $action = '',
59+
public readonly bool $hasPassword = false,
60+
public readonly ?int $expirationTimestamp = null,
61+
public readonly ?string $parent = null,
62+
) {
63+
}
64+
}

0 commit comments

Comments
 (0)