Skip to content
Open
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
16 changes: 14 additions & 2 deletions apps/sharing/lib/Controller/ApiV1Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use NCU\Sharing\Share;
use NCU\Sharing\ShareAccessContext;
use NCU\Sharing\ShareState;
use NCU\Sharing\ShareUser;
use NCU\Sharing\ShareUserStatus;
use NCU\Sharing\Source\IShareSourceType;
use NCU\Sharing\Source\ShareSource;
Expand All @@ -42,6 +43,7 @@
use OCP\IDBConnection;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
Expand Down Expand Up @@ -80,7 +82,7 @@ public function __construct(
* Search for recipients that can be added to a share.
*
* @param ?list<class-string<IShareRecipientType>> $filterRecipientTypeClasses Type classes of recipients to filter by
* @param string $query The query to search for
* @param string $query The query to search for, if the query is empty, recommended recipients will be returned
* @param int<1, 100> $limit The maximum number of participants
* @param non-negative-int $offset The offset of the participants
* @param ?string $id If provided, recipients that are already part of the share will not be returned.
Expand Down Expand Up @@ -109,11 +111,21 @@ public function searchRecipients(?array $filterRecipientTypeClasses, string $que
return new DataResponse('The offset is too low.', Http::STATUS_BAD_REQUEST);
}

if (!$this->accessContext->currentUser instanceof IUser) {
throw new \RuntimeException('No user in session for endpoint that requires authentication');
}

try {
try {
$this->dbConnection->beginTransaction();
$forShare = ($id === null) ? null : $this->manager->getShare($this->accessContext, $id);
$recipients = $this->manager->searchRecipients($this->accessContext, $filterRecipientTypeClasses, $query, $limit, $offset, $forShare);
if ($query !== '') {
$recipients = $this->manager->searchRecipients($this->accessContext, $filterRecipientTypeClasses, $query, $limit, $offset, $forShare);
} else {
$user = new ShareUser($this->accessContext->currentUser->getUID(), null);
$recipients = $this->manager->getRecipientsForUser($user, $filterRecipientTypeClasses, $forShare?->id, $limit, $offset);
}

$this->dbConnection->commit();
return new DataResponse(ShareRecipient::formatMultiple($this->registry, $this->l10nFactory, $this->urlGenerator, $this->userManager, $recipients));
} catch (Exception $exception) {
Expand Down
2 changes: 1 addition & 1 deletion apps/sharing/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@
{
"name": "query",
"in": "query",
"description": "The query to search for",
"description": "The query to search for, if the query is empty, recommended recipients will be returned",
"required": true,
"schema": {
"type": "string"
Expand Down
15 changes: 15 additions & 0 deletions apps/sharing/tests/Command/CommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use NCU\Sharing\Share;
use NCU\Sharing\ShareAccessContext;
use NCU\Sharing\ShareState;
use NCU\Sharing\ShareUser;
use NCU\Sharing\ShareUserStatus;
use NCU\Sharing\Source\ShareSource;
use OC\Core\Command\Base;
Expand Down Expand Up @@ -455,4 +456,18 @@ protected function getShares(ShareAccessContext $accessContext, ?string $filterS
/** @var SharingShare[] */
return json_decode($stdout, true, 512, JSON_THROW_ON_ERROR);
}

#[Override]
protected function getRecipientsForUser(
ShareUser $user,
?array $filterRecipientTypeClasses = null,
?string $notInShare = null,
int $count = 5,
int $offset = 0,
): array {
// We don't have a command for this, so we just call the real manager to make the test pass.
/** @psalm-suppress ArgumentTypeCoercion */
$recipients = $this->manager->getRecipientsForUser($user, $filterRecipientTypeClasses, $notInShare, $count, $offset);
return ShareRecipient::formatMultiple($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $recipients);
}
}
26 changes: 25 additions & 1 deletion apps/sharing/tests/Controller/ApiV1ControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use NCU\Sharing\Share;
use NCU\Sharing\ShareAccessContext;
use NCU\Sharing\ShareState;
use NCU\Sharing\ShareUser;
use NCU\Sharing\ShareUserStatus;
use NCU\Sharing\Source\ShareSource;
use OCA\Sharing\Controller\ApiV1Controller;
Expand All @@ -25,6 +26,7 @@
use OCP\IDBConnection;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
Expand All @@ -37,6 +39,7 @@

/**
* @psalm-import-type SharingShare from Share
* @psalm-import-type SharingRecipient from Share
*/
#[Group(name: 'DB')]
final class ApiV1ControllerTest extends AbstractSharingManagerTests {
Expand Down Expand Up @@ -97,7 +100,10 @@ private function executeRequest(ShareAccessContext $accessContext, Closure $clos

#[Override]
protected function searchRecipients(ShareAccessContext $accessContext, ?array $filterRecipientTypeClasses, string $query, int $limit, int $offset, ?Share $forShare = null): array {
/** @psalm-suppress ArgumentTypeCoercion */
/**
* @psalm-suppress ArgumentTypeCoercion
* @var SharingRecipient[]
*/
return $this->executeRequest($accessContext, fn (ApiV1Controller $controller): DataResponse => $controller->searchRecipients($filterRecipientTypeClasses, $query, $limit, $offset, $forShare?->id));
}

Expand Down Expand Up @@ -201,4 +207,22 @@ protected function getShares(ShareAccessContext $accessContext, ?string $filterS
return $controller->getShares($filterSourceTypeClass, $filterSourceTypeValue, $filterState?->value, $filterUserStatus?->value, $lastShareID);
});
}

#[Override]
protected function getRecipientsForUser(
ShareUser $user,
?array $filterRecipientTypeClasses = null,
?string $notInShare = null,
int $count = 5,
int $offset = 0,
): array {
$accessUser = $this->createMock(IUser::class);
$accessUser->method('getUID')->willReturn($user->userId);

$accessContext = new ShareAccessContext($accessUser);
/** @var SharingRecipient[] */
return $this->executeRequest($accessContext,
/** @psalm-suppress ArgumentTypeCoercion */
fn (ApiV1Controller $controller): DataResponse => $controller->searchRecipients($filterRecipientTypeClasses, '', $count, $offset, $notInShare));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,9 @@ public function least($x, $y): IQueryFunction {
public function now(): IQueryFunction {
return new QueryFunction('NOW()');
}

#[Override]
public function coalesce($value, $default): IQueryFunction {
return new QueryFunction('COALESCE(' . $this->helper->quoteColumnName($value) . ', ' . $this->helper->quoteColumnName($default) . ')');
}
}
106 changes: 104 additions & 2 deletions lib/private/Sharing/SharingBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,9 @@ private function list(

$shareRecipientPermissions[$shareId] ??= [];
$shareRecipientPermissions[$shareId][$recipientId] ??= [];
$shareRecipientPermissions[$shareId][$recipientId][$permissionTypeClass] = new SharePermission($permissionTypeClass, (bool)$row['permission_enabled']);
$shareRecipientPermissions[$shareId][$recipientId][$permissionTypeClass] = new SharePermission(
$permissionTypeClass, (bool)$row['permission_enabled']
);
}
}

Expand Down Expand Up @@ -1165,7 +1167,9 @@ private function list(
continue;
}

if (array_intersect($registryPropertyTypeCompatibleSourceTypeClasses[$propertyTypeClass], array_keys($shareSourceTypeClasses[$shareId])) === []) {
if (array_intersect(
$registryPropertyTypeCompatibleSourceTypeClasses[$propertyTypeClass], array_keys($shareSourceTypeClasses[$shareId])
) === []) {
// Skip properties that are currently not compatible, but don't remove them.
continue;
}
Expand Down Expand Up @@ -1408,6 +1412,104 @@ public function createSharePermissionDefaultValue(Share $share, string $permissi
);
}

#[\Override]
public function getRecipientsForUser(
ShareUser $user,
?array $filterRecipientTypeClasses = null,
?string $notInShare = null,
int $count = 5,
int $offset = 0,
): array {
$query = $this->connection->getTypedQueryBuilder();

// Add an `eq` constraint, or an `is null` constraint, depending on if the value is null
$eqOrNull = fn (string $table, ?string $value): string => ($value === null)
? $query->expr()->isNull($table)
: $query->expr()->eq($table, $query->createNamedParameter($value));

$query->selectColumns('recipient_class_id', 'recipient_value', 'recipient_instance', 'initiator_user_id', 'initiator_instance')
->selectAlias($query->func()->count('*'), 'count')
->from('sharing_share_recipients', 'r')
->innerJoin('r', 'sharing_share', 's', $query->expr()->eq('r.share_id', 's.id'))
->where(
$query->expr()->orX(
$query->expr()->andX(
$query->expr()->eq('s.owner_user_id', $query->createNamedParameter($user->userId)),
$eqOrNull('s.owner_instance', $user->instance),
),
$query->expr()->andX(
$query->expr()->eq('r.initiator_user_id', $query->createNamedParameter($user->userId)),
$eqOrNull('r.initiator_instance', $user->instance),
),
)
)
->groupBy('r.recipient_class_id', 'r.recipient_value', 'r.recipient_instance', 'r.initiator_user_id', 'r.initiator_instance')
->orderBy('count', \SortDirection::Descending)
// sort by recipient to get a stable output, and allow "after" to be deterministic
->addOrderBy(
'recipient_instance', \SortDirection::Ascending
)
->addOrderBy(
'recipient_value', \SortDirection::Ascending
)
->addOrderBy('recipient_class_id', \SortDirection::Ascending);

if ($filterRecipientTypeClasses !== null) {
$filterRecipientTypeClassIds = array_map($this->classMapper->getClassId(...), $filterRecipientTypeClasses);
$query = $query->andWhere(
$query->expr()->in('recipient_class_id', $query->createNamedParameter($filterRecipientTypeClassIds, IQueryBuilder::PARAM_INT_ARRAY))
);
}

if ($notInShare !== null) {
$fullRecipientId = $query->func()->concat(
'recipient_class_id',
$query->func()->coalesce('recipient_instance', $query->expr()->literal('-')),
'recipient_value'
);

$subQuery = $this->connection->getTypedQueryBuilder();
$subQuery->selectAlias($fullRecipientId, 'recipient')
->from('sharing_share_recipients')
->where($query->expr()->eq('share_id', $query->createNamedParameter($notInShare)));

$query = $query->andWhere(
$query->expr()->notIn(
$fullRecipientId,
$query->createFunction('(' . $subQuery->getSQL() . ')')
)
);
}

$query->setMaxResults($count);

if ($offset) {
$query = $query->setFirstResult($offset);
}

$rows = $query->executeQuery()->fetchAll();

return array_map(function (array $row): \NCU\Sharing\Recipient\ShareRecipient {
/** @var array{recipient_class_id: int|non-empty-string, recipient_value: non-empty-string, recipient_instance: ?non-empty-string, initiator_user_id: non-empty-string, initiator_instance: ?non-empty-string} $row */
$class = $this->classMapper->getClassName((int)$row['recipient_class_id']);
if (!isset($this->registry->getRecipientTypes()[$class])) {
throw new RuntimeException('The recipient type is not registered: ' . $class);
}

/** @var class-string<IShareRecipientType> $class */
return new ShareRecipient(
$class,
$row['recipient_value'],
$row['recipient_instance'],
null,
new ShareUser(
$row['initiator_user_id'],
$row['initiator_instance'],
)
);
}, $rows);
}

private static function parseTimestamp(string $timestampMs): \DateTimeImmutable {
if (method_exists(\DateTimeImmutable::class, 'createFromTimestamp')) {
// with php 8.3 the method doesn't exist and psalm doesn't know the return type
Expand Down
11 changes: 11 additions & 0 deletions lib/private/Sharing/SharingManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,17 @@ public function getShares(
return $this->backend->getShares($accessContext, $filterSourceTypeClass, $filterSourceTypeValue, $filterState, $filterUserStatus, $lastShareID, $limit);
}

#[\Override]
public function getRecipientsForUser(
ShareUser $user,
?array $filterRecipientTypeClasses = null,
?string $notInShare = null,
int $count = 5,
int $offset = 0,
): array {
return $this->backend->getRecipientsForUser($user, $filterRecipientTypeClasses, $notInShare, $count, $offset);
}

#[\Override]
public function handle(Event $event): void {
if ($event instanceof SharesDefaultSetEvent) {
Expand Down
8 changes: 8 additions & 0 deletions lib/public/DB/QueryBuilder/IFunctionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,12 @@ public function least($x, $y): IQueryFunction;
* @since 34.0.0
*/
public function now(): IQueryFunction;

/**
* @param string|ILiteral|IParameter|IQueryFunction $value The value that might be null
* @param string|ILiteral|IParameter|IQueryFunction $default The value to use if the first one is null
* @return IQueryFunction
* @since 35.0.0
*/
public function coalesce($value, $default): IQueryFunction;
}
20 changes: 20 additions & 0 deletions lib/unstable/Sharing/ISharingBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use NCU\Sharing\Permission\ISharePermissionPreset;
use NCU\Sharing\Permission\SharePermission;
use NCU\Sharing\Property\ShareProperty;
use NCU\Sharing\Recipient\IShareRecipientType;
use NCU\Sharing\Recipient\ShareRecipient;
use NCU\Sharing\Source\IShareSourceType;
use NCU\Sharing\Source\ShareSource;
Expand Down Expand Up @@ -212,4 +213,23 @@ public function setLastUpdated(array $ids, \DateTimeImmutable $lastUpdated): voi
* @experimental 35.0.0
*/
public function ensureDefaults(array $shares): array;

/**
* Get a list of recipients a user has shared with, ordered by share count
*
* "shared with" includes both shares owned by the user, and reshares initiated by the user
*
* @param ?list<class-string<IShareRecipientType>> $filterRecipientTypeClasses
* @param null|non-empty-string $notInShare
* @param non-negative-int $count
* @return list<ShareRecipient>
* @experimental 35.0.0
*/
public function getRecipientsForUser(
ShareUser $user,
?array $filterRecipientTypeClasses = null,
?string $notInShare = null,
int $count = 5,
int $offset = 0,
): array;
}
19 changes: 19 additions & 0 deletions lib/unstable/Sharing/ISharingManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,23 @@ public function getShare(ShareAccessContext $accessContext, string $id): Share;
* @experimental 35.0.0
*/
public function getShares(ShareAccessContext $accessContext, ?string $filterSourceTypeClass, ?string $filterSourceTypeValue, ?ShareState $filterState, ?ShareUserStatus $filterUserStatus, ?string $lastShareID, ?int $limit): array;

/**
* Get a list of recipients a user has shared with, ordered by share count
*
* "shared with" includes both shares owned by the user, and reshares initiated by the user
*
* @param ?list<class-string<IShareRecipientType>> $filterRecipientTypeClasses
* @param null|non-empty-string $notInShare
* @param non-negative-int $count
* @return list<ShareRecipient>
* @experimental 35.0.0
*/
public function getRecipientsForUser(
ShareUser $user,
?array $filterRecipientTypeClasses = null,
?string $notInShare = null,
int $count = 5,
int $offset = 0,
): array;
}
2 changes: 1 addition & 1 deletion openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -38025,7 +38025,7 @@
{
"name": "query",
"in": "query",
"description": "The query to search for",
"description": "The query to search for, if the query is empty, recommended recipients will be returned",
"required": true,
"schema": {
"type": "string"
Expand Down
Loading
Loading