nextcloud/apps/federatedfilesharing/lib/OCM/CloudFederationProviderFiles.php
provokateurin 045ad43237
fix(federatedfilesharing): Do not set the share id for an existing share
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-03-12 10:31:25 +01:00

687 lines
23 KiB
PHP

<?php
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\FederatedFileSharing\OCM;
use OC\AppFramework\Http;
use OC\Files\Filesystem;
use OCA\FederatedFileSharing\AddressHandler;
use OCA\FederatedFileSharing\FederatedShareProvider;
use OCA\Federation\TrustedServers;
use OCA\Files_Sharing\Activity\Providers\RemoteShares;
use OCA\Files_Sharing\External\ExternalShare;
use OCA\Files_Sharing\External\ExternalShareMapper;
use OCA\Files_Sharing\External\Manager;
use OCA\GlobalSiteSelector\Service\SlaveService;
use OCA\Polls\Db\Share;
use OCP\Activity\IManager as IActivityManager;
use OCP\App\IAppManager;
use OCP\Constants;
use OCP\Federation\Exceptions\ActionNotSupportedException;
use OCP\Federation\Exceptions\AuthenticationFailedException;
use OCP\Federation\Exceptions\BadRequestException;
use OCP\Federation\Exceptions\ProviderCouldNotAddShareException;
use OCP\Federation\ICloudFederationFactory;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Federation\ICloudFederationShare;
use OCP\Federation\ICloudIdManager;
use OCP\Federation\ISignedCloudFederationProvider;
use OCP\Files\IFilenameValidator;
use OCP\Files\ISetupManager;
use OCP\Files\NotFoundException;
use OCP\HintException;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager as INotificationManager;
use OCP\Server;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IProviderFactory;
use OCP\Share\IShare;
use OCP\Util;
use Override;
use Psr\Log\LoggerInterface;
use SensitiveParameter;
class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
public function __construct(
private readonly IAppManager $appManager,
private readonly FederatedShareProvider $federatedShareProvider,
private readonly AddressHandler $addressHandler,
private readonly IUserManager $userManager,
private readonly IManager $shareManager,
private readonly ICloudIdManager $cloudIdManager,
private readonly IActivityManager $activityManager,
private readonly INotificationManager $notificationManager,
private readonly IURLGenerator $urlGenerator,
private readonly ICloudFederationFactory $cloudFederationFactory,
private readonly ICloudFederationProviderManager $cloudFederationProviderManager,
private readonly IGroupManager $groupManager,
private readonly IConfig $config,
private readonly Manager $externalShareManager,
private readonly LoggerInterface $logger,
private readonly IFilenameValidator $filenameValidator,
private readonly IProviderFactory $shareProviderFactory,
private readonly ISetupManager $setupManager,
private readonly ExternalShareMapper $externalShareMapper,
) {
}
#[Override]
public function getShareType(): string {
return 'file';
}
#[Override]
public function shareReceived(ICloudFederationShare $share): string {
if (!$this->isS2SEnabled(true)) {
throw new ProviderCouldNotAddShareException('Server does not support federated cloud sharing', '', Http::STATUS_SERVICE_UNAVAILABLE);
}
$protocol = $share->getProtocol();
if ($protocol['name'] !== 'webdav') {
throw new ProviderCouldNotAddShareException('Unsupported protocol for data exchange.', '', Http::STATUS_NOT_IMPLEMENTED);
}
[, $remote] = $this->addressHandler->splitUserRemote($share->getOwner());
// for backward compatibility make sure that the remote url stored in the
// database ends with a trailing slash
if (!str_ends_with($remote, '/')) {
$remote = $remote . '/';
}
$token = $share->getShareSecret();
$name = $share->getResourceName();
$owner = $share->getOwnerDisplayName() ?: $share->getOwner();
$shareWith = $share->getShareWith();
$remoteId = $share->getProviderId();
$sharedByFederatedId = $share->getSharedBy();
$ownerFederatedId = $share->getOwner();
$shareType = $this->mapShareTypeToNextcloud($share->getShareType());
// if no explicit information about the person who created the share was sent
// we assume that the share comes from the owner
if ($sharedByFederatedId === null) {
$sharedByFederatedId = $ownerFederatedId;
}
if ($remote && $token && $name && $owner && $remoteId && $shareWith) {
if (!$this->filenameValidator->isFilenameValid($name)) {
throw new ProviderCouldNotAddShareException('The mountpoint name contains invalid characters.', '', Http::STATUS_BAD_REQUEST);
}
$user = null;
$group = null;
if ($shareType === IShare::TYPE_USER) {
$this->logger->debug('shareWith before, ' . $shareWith, ['app' => 'files_sharing']);
Util::emitHook(
'\OCA\Files_Sharing\API\Server2Server',
'preLoginNameUsedAsUserName',
['uid' => &$shareWith]
);
$this->logger->debug('shareWith after, ' . $shareWith, ['app' => 'files_sharing']);
$user = $this->userManager->get($shareWith);
if ($user === null) {
throw new ProviderCouldNotAddShareException('User does not exists', '', Http::STATUS_BAD_REQUEST);
}
$this->setupManager->setupForUser($user);
} else {
$group = $this->groupManager->get($shareWith);
if ($group === null) {
throw new ProviderCouldNotAddShareException('Group does not exists', '', Http::STATUS_BAD_REQUEST);
}
}
$externalShare = new ExternalShare();
$externalShare->generateId();
$externalShare->setRemote($remote);
$externalShare->setRemoteId($remoteId);
$externalShare->setShareToken($token);
$externalShare->setPassword('');
$externalShare->setName($name);
$externalShare->setOwner($owner);
$externalShare->setShareType($shareType);
$externalShare->setAccepted(IShare::STATUS_PENDING);
try {
$this->externalShareManager->addShare($externalShare, $user ?: $group);
// get DisplayName about the owner of the share
$ownerDisplayName = $this->getUserDisplayName($ownerFederatedId);
$trustedServers = null;
if ($this->appManager->isEnabledForAnyone('federation')
&& class_exists(TrustedServers::class)) {
try {
$trustedServers = Server::get(TrustedServers::class);
} catch (\Throwable $e) {
$this->logger->debug('Failed to create TrustedServers', ['exception' => $e]);
}
}
if ($shareType === IShare::TYPE_USER) {
$event = $this->activityManager->generateEvent();
$event->setApp('files_sharing')
->setType('remote_share')
->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_RECEIVED, [$ownerFederatedId, trim($name, '/'), $ownerDisplayName])
->setAffectedUser($shareWith)
->setObject('remote_share', (string)$externalShare->getId(), $name);
Server::get(IActivityManager::class)->publish($event);
$this->notifyAboutNewShare($shareWith, (string)$externalShare->getId(), $ownerFederatedId, $sharedByFederatedId, $name, $ownerDisplayName);
// If auto-accept is enabled, accept the share
if ($this->federatedShareProvider->isFederatedTrustedShareAutoAccept() && $trustedServers?->isTrustedServer($remote) === true) {
$this->externalShareManager->acceptShare($externalShare, $user);
}
} else {
$groupMembers = $group->getUsers();
foreach ($groupMembers as $user) {
$event = $this->activityManager->generateEvent();
$event->setApp('files_sharing')
->setType('remote_share')
->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_RECEIVED, [$ownerFederatedId, trim($name, '/'), $ownerDisplayName])
->setAffectedUser($user->getUID())
->setObject('remote_share', (string)$externalShare->getId(), $name);
Server::get(IActivityManager::class)->publish($event);
$this->notifyAboutNewShare($user->getUID(), (string)$externalShare->getId(), $ownerFederatedId, $sharedByFederatedId, $name, $ownerDisplayName);
// If auto-accept is enabled, accept the share
if ($this->federatedShareProvider->isFederatedTrustedShareAutoAccept() && $trustedServers?->isTrustedServer($remote) === true) {
$this->externalShareManager->acceptShare($externalShare, $user);
}
}
}
return (string)$externalShare->getId();
} catch (\Exception $e) {
$this->logger->error('Server can not add remote share.', [
'app' => 'files_sharing',
'exception' => $e,
]);
throw new ProviderCouldNotAddShareException('internal server error, was not able to add share from ' . $remote, '', HTTP::STATUS_INTERNAL_SERVER_ERROR);
}
}
throw new ProviderCouldNotAddShareException('server can not add remote share, missing parameter', '', HTTP::STATUS_BAD_REQUEST);
}
#[Override]
public function notificationReceived(string $notificationType, string $providerId, array $notification): array {
return match ($notificationType) {
'SHARE_ACCEPTED' => $this->shareAccepted($providerId, $notification),
'SHARE_DECLINED' => $this->shareDeclined($providerId, $notification),
'SHARE_UNSHARED' => $this->unshare($providerId, $notification),
'REQUEST_RESHARE' => $this->reshareRequested($providerId, $notification),
'RESHARE_UNDO' => $this->undoReshare($providerId, $notification),
'RESHARE_CHANGE_PERMISSION' => $this->updateResharePermissions($providerId, $notification),
default => throw new BadRequestException([$notificationType]),
};
}
/**
* Map OCM share type (strings) to Nextcloud internal share types (integer)
* @return IShare::TYPE_GROUP|IShare::TYPE_USER
*/
private function mapShareTypeToNextcloud(string $shareType): int {
return $shareType === 'group' ? IShare::TYPE_GROUP : IShare::TYPE_USER;
}
private function notifyAboutNewShare(string $shareWith, string $shareId, $ownerFederatedId, $sharedByFederatedId, string $name, string $displayName): void {
$notification = $this->notificationManager->createNotification();
$notification->setApp('files_sharing')
->setUser($shareWith)
->setDateTime(new \DateTime())
->setObject('remote_share', $shareId)
->setSubject('remote_share', [$ownerFederatedId, $sharedByFederatedId, trim($name, '/'), $displayName]);
$declineAction = $notification->createAction();
$declineAction->setLabel('decline')
->setLink($this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkTo('', 'ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/' . $shareId)), 'DELETE');
$notification->addAction($declineAction);
$acceptAction = $notification->createAction();
$acceptAction->setLabel('accept')
->setLink($this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkTo('', 'ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/' . $shareId)), 'POST');
$notification->addAction($acceptAction);
$this->notificationManager->notify($notification);
}
/**
* process notification that the recipient accepted a share
*
* @param array{sharedSecret?: string} $notification
* @return array<string>
* @throws ActionNotSupportedException
* @throws AuthenticationFailedException
* @throws BadRequestException
* @throws HintException
*/
private function shareAccepted(string $id, array $notification): array {
if (!$this->isS2SEnabled()) {
throw new ActionNotSupportedException('Server does not support federated cloud sharing');
}
if (!isset($notification['sharedSecret'])) {
throw new BadRequestException(['sharedSecret']);
}
$token = $notification['sharedSecret'];
$share = $this->federatedShareProvider->getShareById($id);
$this->verifyShare($share, $token);
$this->executeAcceptShare($share);
if ($share->getShareOwner() !== $share->getSharedBy()
&& !$this->userManager->userExists($share->getSharedBy())) {
// only if share was initiated from another instance
[, $remote] = $this->addressHandler->splitUserRemote($share->getSharedBy());
$remoteId = $this->federatedShareProvider->getRemoteId($share);
$notification = $this->cloudFederationFactory->getCloudFederationNotification();
$notification->setMessage(
'SHARE_ACCEPTED',
'file',
$remoteId,
[
'sharedSecret' => $token,
'message' => 'Recipient accepted the re-share'
]
);
$this->cloudFederationProviderManager->sendNotification($remote, $notification);
}
return [];
}
/**
* @throws ShareNotFound
*/
protected function executeAcceptShare(IShare $share): void {
$user = $this->getCorrectUser($share);
try {
$fileId = $share->getNode()->getId();
[$file, $link] = $this->getFile($user, $fileId);
} catch (\Exception) {
throw new ShareNotFound();
}
$event = $this->activityManager->generateEvent();
$event->setApp('files_sharing')
->setType('remote_share')
->setAffectedUser($user->getUID())
->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_ACCEPTED, [$share->getSharedWith(), [$fileId => $file]])
->setObject('files', $fileId, $file)
->setLink($link);
$this->activityManager->publish($event);
}
/**
* process notification that the recipient declined a share
*
* @param array{sharedSecret?: string} $notification
* @return array<string>
* @throws ActionNotSupportedException
* @throws AuthenticationFailedException
* @throws BadRequestException
* @throws ShareNotFound
* @throws HintException
*
*/
protected function shareDeclined(string $id, array $notification): array {
if (!$this->isS2SEnabled()) {
throw new ActionNotSupportedException('Server does not support federated cloud sharing');
}
if (!isset($notification['sharedSecret'])) {
throw new BadRequestException(['sharedSecret']);
}
$token = $notification['sharedSecret'];
$share = $this->federatedShareProvider->getShareById($id);
$this->verifyShare($share, $token);
if ($share->getShareOwner() !== $share->getSharedBy()) {
[, $remote] = $this->addressHandler->splitUserRemote($share->getSharedBy());
$remoteId = $this->federatedShareProvider->getRemoteId($share);
$notification = $this->cloudFederationFactory->getCloudFederationNotification();
$notification->setMessage(
'SHARE_DECLINED',
'file',
$remoteId,
[
'sharedSecret' => $token,
'message' => 'Recipient declined the re-share'
]
);
$this->cloudFederationProviderManager->sendNotification($remote, $notification);
}
$this->executeDeclineShare($share);
return [];
}
/**
* delete declined share and create a activity
*
* @throws ShareNotFound
*/
protected function executeDeclineShare(IShare $share): void {
$this->federatedShareProvider->removeShareFromTable($share->getId());
$user = $this->getCorrectUser($share);
try {
$fileId = $share->getNode()->getId();
[$file, $link] = $this->getFile($user, $fileId);
} catch (\Exception) {
throw new ShareNotFound();
}
$event = $this->activityManager->generateEvent();
$event->setApp('files_sharing')
->setType('remote_share')
->setAffectedUser($user->getUID())
->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_DECLINED, [$share->getSharedWith(), [$fileId => $file]])
->setObject('files', $fileId, $file)
->setLink($link);
$this->activityManager->publish($event);
}
/**
* received the notification that the owner unshared a file from you
*
* @param array{sharedSecret?: string} $notification
* @return array<string>
* @throws AuthenticationFailedException
* @throws BadRequestException
*/
private function undoReshare(string $id, array $notification): array {
if (!isset($notification['sharedSecret'])) {
throw new BadRequestException(['sharedSecret']);
}
$token = $notification['sharedSecret'];
$share = $this->federatedShareProvider->getShareById($id);
$this->verifyShare($share, $token);
$this->federatedShareProvider->removeShareFromTable($share->getId());
return [];
}
/**
* unshare file from self
*
* @param array{sharedSecret?: string} $notification
* @return array<string>
* @throws ActionNotSupportedException
* @throws BadRequestException
*/
private function unshare(string $id, array $notification): array {
if (!$this->isS2SEnabled(true)) {
throw new ActionNotSupportedException('incoming shares disabled!');
}
if (!isset($notification['sharedSecret'])) {
throw new BadRequestException(['sharedSecret']);
}
$token = $notification['sharedSecret'];
$share = $this->externalShareMapper->getShareByRemoteIdAndToken($id, $token);
if ($token && $id && $share !== null) {
$remote = $this->cleanupRemote($share->getRemote());
$owner = $this->cloudIdManager->getCloudId($share->getOwner(), $remote);
$mountpoint = $share->getMountpoint();
$user = $share->getUser();
$this->externalShareMapper->delete($share);
$ownerDisplayName = $this->getUserDisplayName($owner->getId());
if ($share->getShareType() === IShare::TYPE_USER) {
if ($share->getAccepted()) {
$path = trim($mountpoint, '/');
} else {
$path = trim($share->getName(), '/');
}
$notification = $this->notificationManager->createNotification();
$notification->setApp('files_sharing')
->setUser($share->getUser())
->setObject('remote_share', (string)$share->getId());
$this->notificationManager->markProcessed($notification);
$event = $this->activityManager->generateEvent();
$event->setApp('files_sharing')
->setType('remote_share')
->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_UNSHARED, [$owner->getId(), $path, $ownerDisplayName])
->setAffectedUser($user)
->setObject('remote_share', $share->getId(), $path);
Server::get(IActivityManager::class)->publish($event);
}
}
return [];
}
private function cleanupRemote(string $remote): string {
$remote = substr($remote, strpos($remote, '://') + 3);
return rtrim($remote, '/');
}
/**
* recipient of a share request to re-share the file with another user
*
* @param array{sharedSecret?: string, shareWith?: string, senderId?: string} $notification
* @return array<string>
* @throws AuthenticationFailedException
* @throws BadRequestException
* @throws ProviderCouldNotAddShareException
* @throws ShareNotFound
*/
protected function reshareRequested(string $id, array $notification) {
if (!isset($notification['sharedSecret'])) {
throw new BadRequestException(['sharedSecret']);
}
$token = $notification['sharedSecret'];
if (!isset($notification['shareWith'])) {
throw new BadRequestException(['shareWith']);
}
$shareWith = $notification['shareWith'];
if (!isset($notification['senderId'])) {
throw new BadRequestException(['senderId']);
}
$senderId = $notification['senderId'];
$share = $this->federatedShareProvider->getShareById($id);
// We have to respect the default share permissions
$permissions = $share->getPermissions() & (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL);
$share->setPermissions($permissions);
// don't allow to share a file back to the owner
try {
[$user, $remote] = $this->addressHandler->splitUserRemote($shareWith);
$owner = $share->getShareOwner();
$currentServer = $this->addressHandler->generateRemoteURL();
if ($this->addressHandler->compareAddresses($user, $remote, $owner, $currentServer)) {
throw new ProviderCouldNotAddShareException('Resharing back to the owner is not allowed: ' . $id);
}
} catch (\Exception $e) {
throw new ProviderCouldNotAddShareException($e->getMessage());
}
$this->verifyShare($share, $token);
// check if re-sharing is allowed
if ($share->getPermissions() & Constants::PERMISSION_SHARE) {
// the recipient of the initial share is now the initiator for the re-share
$share->setSharedBy($share->getSharedWith());
$share->setSharedWith($shareWith);
$result = $this->federatedShareProvider->create($share);
$this->federatedShareProvider->storeRemoteId($result->getId(), $senderId);
return ['token' => $result->getToken(), 'providerId' => $result->getId()];
} else {
throw new ProviderCouldNotAddShareException('resharing not allowed for share: ' . $id);
}
}
/**
* Update permission of a re-share so that the share dialog shows the right
* permission if the owner or the sender changes the permission
*
* @return string[]
* @throws AuthenticationFailedException
* @throws BadRequestException
*/
protected function updateResharePermissions(string $id, array $notification): array {
throw new HintException('Updating reshares not allowed');
}
/**
* @return list{?string, string} with internal path of the file and a absolute link to it
*/
private function getFile(IUser $user, int $fileSource): array {
$this->setupManager->setupForUser($user);
try {
$file = Filesystem::getPath($fileSource);
} catch (NotFoundException $e) {
$file = null;
}
$args = Filesystem::is_dir($file) ? ['dir' => $file] : ['dir' => dirname($file), 'scrollto' => $file];
$link = Util::linkToAbsolute('files', 'index.php', $args);
return [$file, $link];
}
/**
* Check if we are the initiator or the owner of a re-share and return the correct UID
*/
protected function getCorrectUser(IShare $share): IUser {
if ($user = $this->userManager->get($share->getShareOwner())) {
return $user;
}
$user = $this->userManager->get($share->getSharedBy());
if ($user === null) {
throw new \RuntimeException('Neither the share owner or the share initiator exist');
}
return $user;
}
/**
* check if we got the right share
*
* @throws AuthenticationFailedException
*/
protected function verifyShare(IShare $share, string $token): bool {
if (
$share->getShareType() === IShare::TYPE_REMOTE
&& $share->getToken() === $token
) {
return true;
}
if ($share->getShareType() === IShare::TYPE_CIRCLE) {
try {
$knownShare = $this->shareManager->getShareByToken($token);
if ($knownShare->getId() === $share->getId()) {
return true;
}
} catch (ShareNotFound $e) {
}
}
throw new AuthenticationFailedException();
}
/**
* Check if server-to-server sharing is enabled
*/
private function isS2SEnabled(bool $incoming = false): bool {
$result = $this->appManager->isEnabledForUser('files_sharing');
if ($incoming) {
$result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled();
} else {
$result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
}
return $result;
}
#[Override]
public function getSupportedShareTypes(): array {
return ['user', 'group'];
}
public function getUserDisplayName(string $userId): string {
// check if gss is enabled and available
if (!$this->appManager->isEnabledForAnyone('globalsiteselector')
|| !class_exists('\OCA\GlobalSiteSelector\Service\SlaveService')) {
return '';
}
try {
$slaveService = Server::get(SlaveService::class);
} catch (\Throwable $e) {
$this->logger->error(
$e->getMessage(),
['exception' => $e]
);
return '';
}
return $slaveService->getUserDisplayName($this->cloudIdManager->removeProtocolFromUrl($userId), false);
}
#[Override]
public function getFederationIdFromSharedSecret(
#[SensitiveParameter]
string $sharedSecret,
array $payload,
): string {
$provider = $this->shareProviderFactory->getProviderForType(IShare::TYPE_REMOTE);
try {
$share = $provider->getShareByToken($sharedSecret);
} catch (ShareNotFound) {
// Maybe we're dealing with a share federated from another server
$share = $this->externalShareManager->getShareByToken($sharedSecret);
if ($share === false) {
return '';
}
return $share->getUser() . '@' . $share->getRemote();
}
// if uid_owner is a local account, the request comes from the recipient
// if not, request comes from the instance that owns the share and recipient is the re-sharer
if ($this->userManager->get($share->getShareOwner()) !== null) {
return $share->getSharedWith();
} else {
return $share->getShareOwner();
}
}
}