diff options
Diffstat (limited to 'apps/federatedfilesharing/lib')
22 files changed, 3864 insertions, 1093 deletions
diff --git a/apps/federatedfilesharing/lib/AddressHandler.php b/apps/federatedfilesharing/lib/AddressHandler.php new file mode 100644 index 00000000000..4588e6da288 --- /dev/null +++ b/apps/federatedfilesharing/lib/AddressHandler.php @@ -0,0 +1,127 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\FederatedFileSharing; + +use OCP\Federation\ICloudIdManager; +use OCP\HintException; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\Util; + +/** + * Class AddressHandler - parse, modify and construct federated sharing addresses + * + * @package OCA\FederatedFileSharing + */ +class AddressHandler { + + /** + * AddressHandler constructor. + * + * @param IURLGenerator $urlGenerator + * @param IL10N $l + * @param ICloudIdManager $cloudIdManager + */ + public function __construct( + private IURLGenerator $urlGenerator, + private IL10N $l, + private ICloudIdManager $cloudIdManager, + ) { + } + + /** + * split user and remote from federated cloud id + * + * @param string $address federated share address + * @return array<string> [user, remoteURL] + * @throws HintException + */ + public function splitUserRemote($address) { + try { + $cloudId = $this->cloudIdManager->resolveCloudId($address); + return [$cloudId->getUser(), $cloudId->getRemote()]; + } catch (\InvalidArgumentException $e) { + $hint = $this->l->t('Invalid Federated Cloud ID'); + throw new HintException('Invalid Federated Cloud ID', $hint, 0, $e); + } + } + + /** + * generate remote URL part of federated ID + * + * @return string url of the current server + */ + public function generateRemoteURL() { + return $this->urlGenerator->getAbsoluteURL('/'); + } + + /** + * check if two federated cloud IDs refer to the same user + * + * @param string $user1 + * @param string $server1 + * @param string $user2 + * @param string $server2 + * @return bool true if both users and servers are the same + */ + public function compareAddresses($user1, $server1, $user2, $server2) { + $normalizedServer1 = strtolower($this->removeProtocolFromUrl($server1)); + $normalizedServer2 = strtolower($this->removeProtocolFromUrl($server2)); + + if (rtrim($normalizedServer1, '/') === rtrim($normalizedServer2, '/')) { + // FIXME this should be a method in the user management instead + Util::emitHook( + '\OCA\Files_Sharing\API\Server2Server', + 'preLoginNameUsedAsUserName', + ['uid' => &$user1] + ); + Util::emitHook( + '\OCA\Files_Sharing\API\Server2Server', + 'preLoginNameUsedAsUserName', + ['uid' => &$user2] + ); + + if ($user1 === $user2) { + return true; + } + } + + return false; + } + + /** + * remove protocol from URL + * + * @param string $url + * @return string + */ + public function removeProtocolFromUrl($url) { + if (str_starts_with($url, 'https://')) { + return substr($url, strlen('https://')); + } elseif (str_starts_with($url, 'http://')) { + return substr($url, strlen('http://')); + } + + return $url; + } + + /** + * check if the url contain the protocol (http or https) + * + * @param string $url + * @return bool + */ + public function urlContainProtocol($url) { + if (str_starts_with($url, 'https://') + || str_starts_with($url, 'http://')) { + return true; + } + + return false; + } +} diff --git a/apps/federatedfilesharing/lib/AppInfo/Application.php b/apps/federatedfilesharing/lib/AppInfo/Application.php new file mode 100644 index 00000000000..fda75c475b6 --- /dev/null +++ b/apps/federatedfilesharing/lib/AppInfo/Application.php @@ -0,0 +1,44 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\FederatedFileSharing\AppInfo; + +use Closure; +use OCA\FederatedFileSharing\Listeners\LoadAdditionalScriptsListener; +use OCA\FederatedFileSharing\Notifier; +use OCA\FederatedFileSharing\OCM\CloudFederationProviderFiles; +use OCA\Files\Event\LoadAdditionalScriptsEvent; +use OCP\AppFramework\App; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\AppFramework\IAppContainer; +use OCP\Federation\ICloudFederationProviderManager; + +class Application extends App implements IBootstrap { + public function __construct() { + parent::__construct('federatedfilesharing'); + } + + public function register(IRegistrationContext $context): void { + $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalScriptsListener::class); + $context->registerNotifierService(Notifier::class); + } + + public function boot(IBootContext $context): void { + $context->injectFn(Closure::fromCallable([$this, 'registerCloudFederationProvider'])); + } + + private function registerCloudFederationProvider(ICloudFederationProviderManager $manager, + IAppContainer $appContainer): void { + $manager->addCloudFederationProvider('file', + 'Federated Files Sharing', + function () use ($appContainer): CloudFederationProviderFiles { + return $appContainer->get(CloudFederationProviderFiles::class); + }); + } +} diff --git a/apps/federatedfilesharing/lib/BackgroundJob/RetryJob.php b/apps/federatedfilesharing/lib/BackgroundJob/RetryJob.php new file mode 100644 index 00000000000..9d66cd71812 --- /dev/null +++ b/apps/federatedfilesharing/lib/BackgroundJob/RetryJob.php @@ -0,0 +1,91 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\FederatedFileSharing\BackgroundJob; + +use OCA\FederatedFileSharing\Notifications; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\BackgroundJob\Job; + +/** + * Class RetryJob + * + * Background job to re-send update of federated re-shares to the remote server in + * case the server was not available on the first try + * + * @package OCA\FederatedFileSharing\BackgroundJob + */ +class RetryJob extends Job { + private bool $retainJob = true; + + /** @var int max number of attempts to send the request */ + private int $maxTry = 20; + + /** @var int how much time should be between two tries (10 minutes) */ + private int $interval = 600; + + public function __construct( + private Notifications $notifications, + ITimeFactory $time, + ) { + parent::__construct($time); + } + + /** + * Run the job, then remove it from the jobList + */ + public function start(IJobList $jobList): void { + if ($this->shouldRun($this->argument)) { + parent::start($jobList); + $jobList->remove($this, $this->argument); + if ($this->retainJob) { + $this->reAddJob($jobList, $this->argument); + } + } + } + + protected function run($argument) { + $remote = $argument['remote']; + $remoteId = $argument['remoteId']; + $token = $argument['token']; + $action = $argument['action']; + $data = json_decode($argument['data'], true); + $try = (int)$argument['try'] + 1; + + $result = $this->notifications->sendUpdateToRemote($remote, $remoteId, $token, $action, $data, $try); + + if ($result === true || $try > $this->maxTry) { + $this->retainJob = false; + } + } + + /** + * Re-add background job with new arguments + */ + protected function reAddJob(IJobList $jobList, array $argument): void { + $jobList->add(RetryJob::class, + [ + 'remote' => $argument['remote'], + 'remoteId' => $argument['remoteId'], + 'token' => $argument['token'], + 'data' => $argument['data'], + 'action' => $argument['action'], + 'try' => (int)$argument['try'] + 1, + 'lastRun' => $this->time->getTime() + ] + ); + } + + /** + * Test if it is time for the next run + */ + protected function shouldRun(array $argument): bool { + $lastRun = (int)$argument['lastRun']; + return (($this->time->getTime() - $lastRun) > $this->interval); + } +} diff --git a/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php b/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php new file mode 100644 index 00000000000..b8d2090713b --- /dev/null +++ b/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php @@ -0,0 +1,184 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\FederatedFileSharing\Controller; + +use OCA\DAV\Connector\Sabre\PublicAuth; +use OCA\FederatedFileSharing\AddressHandler; +use OCA\FederatedFileSharing\FederatedShareProvider; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\JSONResponse; +use OCP\Constants; +use OCP\Federation\ICloudIdManager; +use OCP\HintException; +use OCP\Http\Client\IClientService; +use OCP\IL10N; +use OCP\IRequest; +use OCP\ISession; +use OCP\IUserSession; +use OCP\Share\IManager; +use OCP\Share\IShare; +use Psr\Log\LoggerInterface; + +/** + * Class MountPublicLinkController + * + * convert public links to federated shares + * + * @package OCA\FederatedFileSharing\Controller + */ +class MountPublicLinkController extends Controller { + /** + * MountPublicLinkController constructor. + */ + public function __construct( + string $appName, + IRequest $request, + private FederatedShareProvider $federatedShareProvider, + private IManager $shareManager, + private AddressHandler $addressHandler, + private ISession $session, + private IL10N $l, + private IUserSession $userSession, + private IClientService $clientService, + private ICloudIdManager $cloudIdManager, + private LoggerInterface $logger, + ) { + parent::__construct($appName, $request); + } + + /** + * send federated share to a user of a public link + * + * @param string $shareWith Username to share with + * @param string $token Token of the share + * @param string $password Password of the share + * @return JSONResponse<Http::STATUS_OK, array{remoteUrl: string}, array{}>|JSONResponse<Http::STATUS_BAD_REQUEST, array{message: string}, array{}> + * + * 200: Remote URL returned + * 400: Creating share is not possible + */ + #[NoCSRFRequired] + #[PublicPage] + #[BruteForceProtection(action: 'publicLink2FederatedShare')] + #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] + public function createFederatedShare($shareWith, $token, $password = '') { + if (!$this->federatedShareProvider->isOutgoingServer2serverShareEnabled()) { + return new JSONResponse( + ['message' => 'This server doesn\'t support outgoing federated shares'], + Http::STATUS_BAD_REQUEST + ); + } + + try { + [, $server] = $this->addressHandler->splitUserRemote($shareWith); + $share = $this->shareManager->getShareByToken($token); + } catch (HintException $e) { + $response = new JSONResponse(['message' => $e->getHint()], Http::STATUS_BAD_REQUEST); + $response->throttle(); + return $response; + } + + // make sure that user is authenticated in case of a password protected link + $storedPassword = $share->getPassword(); + $authenticated = $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId() + || $this->shareManager->checkPassword($share, $password); + if (!empty($storedPassword) && !$authenticated) { + $response = new JSONResponse( + ['message' => 'No permission to access the share'], + Http::STATUS_BAD_REQUEST + ); + $response->throttle(); + return $response; + } + + if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) { + $response = new JSONResponse( + ['message' => 'Mounting file drop not supported'], + Http::STATUS_BAD_REQUEST + ); + $response->throttle(); + return $response; + } + + $share->setSharedWith($shareWith); + $share->setShareType(IShare::TYPE_REMOTE); + + try { + $this->federatedShareProvider->create($share); + } catch (\Exception $e) { + $this->logger->warning($e->getMessage(), [ + 'app' => 'federatedfilesharing', + 'exception' => $e, + ]); + return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + } + + return new JSONResponse(['remoteUrl' => $server]); + } + + /** + * ask other server to get a federated share + * + * @param string $token + * @param string $remote + * @param string $password + * @param string $owner (only for legacy reasons, can be removed with legacyMountPublicLink()) + * @param string $ownerDisplayName (only for legacy reasons, can be removed with legacyMountPublicLink()) + * @param string $name (only for legacy reasons, can be removed with legacyMountPublicLink()) + * @return JSONResponse + */ + #[NoAdminRequired] + public function askForFederatedShare($token, $remote, $password = '', $owner = '', $ownerDisplayName = '', $name = '') { + // check if server admin allows to mount public links from other servers + if ($this->federatedShareProvider->isIncomingServer2serverShareEnabled() === false) { + return new JSONResponse(['message' => $this->l->t('Server to server sharing is not enabled on this server')], Http::STATUS_BAD_REQUEST); + } + + $cloudId = $this->cloudIdManager->getCloudId($this->userSession->getUser()->getUID(), $this->addressHandler->generateRemoteURL()); + + $httpClient = $this->clientService->newClient(); + + try { + $response = $httpClient->post($remote . '/index.php/apps/federatedfilesharing/createFederatedShare', + [ + 'body' => [ + 'token' => $token, + 'shareWith' => rtrim($cloudId->getId(), '/'), + 'password' => $password + ], + 'connect_timeout' => 10, + ] + ); + } catch (\Exception $e) { + if (empty($password)) { + $message = $this->l->t("Couldn't establish a federated share."); + } else { + $message = $this->l->t("Couldn't establish a federated share, maybe the password was wrong."); + } + return new JSONResponse(['message' => $message], Http::STATUS_BAD_REQUEST); + } + + $body = $response->getBody(); + $result = json_decode($body, true); + + if (is_array($result) && isset($result['remoteUrl'])) { + return new JSONResponse(['message' => $this->l->t('Federated Share request sent, you will receive an invitation. Check your notifications.')]); + } + + // if we doesn't get the expected response we assume that we try to add + // a federated share from a Nextcloud <= 9 server + $message = $this->l->t("Couldn't establish a federated share, it looks like the server to federate with is too old (Nextcloud <= 9)."); + return new JSONResponse(['message' => $message], Http::STATUS_BAD_REQUEST); + } +} diff --git a/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php b/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php new file mode 100644 index 00000000000..7fdd718cbfe --- /dev/null +++ b/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php @@ -0,0 +1,418 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\FederatedFileSharing\Controller; + +use OCA\FederatedFileSharing\AddressHandler; +use OCA\FederatedFileSharing\FederatedShareProvider; +use OCA\FederatedFileSharing\Notifications; +use OCP\App\IAppManager; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSException; +use OCP\AppFramework\OCSController; +use OCP\Constants; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Federation\Exceptions\ProviderCouldNotAddShareException; +use OCP\Federation\Exceptions\ProviderDoesNotExistsException; +use OCP\Federation\ICloudFederationFactory; +use OCP\Federation\ICloudFederationProviderManager; +use OCP\Federation\ICloudIdManager; +use OCP\HintException; +use OCP\IDBConnection; +use OCP\IRequest; +use OCP\IUserManager; +use OCP\Log\Audit\CriticalActionPerformedEvent; +use OCP\Server; +use OCP\Share; +use OCP\Share\Exceptions\ShareNotFound; +use Psr\Log\LoggerInterface; + +#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] +class RequestHandlerController extends OCSController { + + public function __construct( + string $appName, + IRequest $request, + private FederatedShareProvider $federatedShareProvider, + private IDBConnection $connection, + private Share\IManager $shareManager, + private Notifications $notifications, + private AddressHandler $addressHandler, + private IUserManager $userManager, + private ICloudIdManager $cloudIdManager, + private LoggerInterface $logger, + private ICloudFederationFactory $cloudFederationFactory, + private ICloudFederationProviderManager $cloudFederationProviderManager, + private IEventDispatcher $eventDispatcher, + ) { + parent::__construct($appName, $request); + } + + /** + * create a new share + * + * @param string|null $remote Address of the remote + * @param string|null $token Shared secret between servers + * @param string|null $name Name of the shared resource + * @param string|null $owner Display name of the receiver + * @param string|null $sharedBy Display name of the sender + * @param string|null $shareWith ID of the user that receives the share + * @param int|null $remoteId ID of the remote + * @param string|null $sharedByFederatedId Federated ID of the sender + * @param string|null $ownerFederatedId Federated ID of the receiver + * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSException + * + * 200: Share created successfully + */ + #[NoCSRFRequired] + #[PublicPage] + public function createShare( + ?string $remote = null, + ?string $token = null, + ?string $name = null, + ?string $owner = null, + ?string $sharedBy = null, + ?string $shareWith = null, + ?int $remoteId = null, + ?string $sharedByFederatedId = null, + ?string $ownerFederatedId = null, + ) { + if ($ownerFederatedId === null) { + $ownerFederatedId = $this->cloudIdManager->getCloudId($owner, $this->cleanupRemote($remote))->getId(); + } + // if the owner of the share and the initiator are the same user + // we also complete the federated share ID for the initiator + if ($sharedByFederatedId === null && $owner === $sharedBy) { + $sharedByFederatedId = $ownerFederatedId; + } + + $share = $this->cloudFederationFactory->getCloudFederationShare( + $shareWith, + $name, + '', + $remoteId, + $ownerFederatedId, + $owner, + $sharedByFederatedId, + $sharedBy, + $token, + 'user', + 'file' + ); + + try { + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); + $provider->shareReceived($share); + if ($sharedByFederatedId === $ownerFederatedId) { + $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('A new federated share with "%s" was created by "%s" and shared with "%s"', [$name, $ownerFederatedId, $shareWith])); + } else { + $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('A new federated share with "%s" was shared by "%s" (resource owner is: "%s") and shared with "%s"', [$name, $sharedByFederatedId, $ownerFederatedId, $shareWith])); + } + } catch (ProviderDoesNotExistsException $e) { + throw new OCSException('Server does not support federated cloud sharing', 503); + } catch (ProviderCouldNotAddShareException $e) { + throw new OCSException($e->getMessage(), 400); + } catch (\Exception $e) { + throw new OCSException('internal server error, was not able to add share from ' . $remote, 500); + } + + return new DataResponse(); + } + + /** + * create re-share on behalf of another user + * + * @param int $id ID of the share + * @param string|null $token Shared secret between servers + * @param string|null $shareWith ID of the user that receives the share + * @param int|null $remoteId ID of the remote + * @return Http\DataResponse<Http::STATUS_OK, array{token: string, remoteId: string}, array{}> + * @throws OCSBadRequestException Re-sharing is not possible + * @throws OCSException + * + * 200: Remote share returned + */ + #[NoCSRFRequired] + #[PublicPage] + public function reShare(int $id, ?string $token = null, ?string $shareWith = null, ?int $remoteId = 0) { + if ($token === null + || $shareWith === null + || $remoteId === null + ) { + throw new OCSBadRequestException(); + } + + $notification = [ + 'sharedSecret' => $token, + 'shareWith' => $shareWith, + 'senderId' => $remoteId, + 'message' => 'Recipient of a share ask the owner to reshare the file' + ]; + + try { + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); + [$newToken, $localId] = $provider->notificationReceived('REQUEST_RESHARE', $id, $notification); + return new DataResponse([ + 'token' => $newToken, + 'remoteId' => $localId + ]); + } catch (ProviderDoesNotExistsException $e) { + throw new OCSException('Server does not support federated cloud sharing', 503); + } catch (ShareNotFound $e) { + $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]); + } catch (\Exception $e) { + $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]); + } + + throw new OCSBadRequestException(); + } + + + /** + * accept server-to-server share + * + * @param int $id ID of the remote share + * @param string|null $token Shared secret between servers + * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSException + * @throws ShareNotFound + * @throws HintException + * + * 200: Share accepted successfully + */ + #[NoCSRFRequired] + #[PublicPage] + public function acceptShare(int $id, ?string $token = null) { + $notification = [ + 'sharedSecret' => $token, + 'message' => 'Recipient accept the share' + ]; + + try { + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); + $provider->notificationReceived('SHARE_ACCEPTED', $id, $notification); + $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('Federated share with id "%s" was accepted', [$id])); + } catch (ProviderDoesNotExistsException $e) { + throw new OCSException('Server does not support federated cloud sharing', 503); + } catch (ShareNotFound $e) { + $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]); + } catch (\Exception $e) { + $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]); + } + + return new DataResponse(); + } + + /** + * decline server-to-server share + * + * @param int $id ID of the remote share + * @param string|null $token Shared secret between servers + * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSException + * + * 200: Share declined successfully + */ + #[NoCSRFRequired] + #[PublicPage] + public function declineShare(int $id, ?string $token = null) { + $notification = [ + 'sharedSecret' => $token, + 'message' => 'Recipient declined the share' + ]; + + try { + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); + $provider->notificationReceived('SHARE_DECLINED', $id, $notification); + $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('Federated share with id "%s" was declined', [$id])); + } catch (ProviderDoesNotExistsException $e) { + throw new OCSException('Server does not support federated cloud sharing', 503); + } catch (ShareNotFound $e) { + $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]); + } catch (\Exception $e) { + $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]); + } + + return new DataResponse(); + } + + /** + * remove server-to-server share if it was unshared by the owner + * + * @param int $id ID of the share + * @param string|null $token Shared secret between servers + * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSException + * + * 200: Share unshared successfully + */ + #[NoCSRFRequired] + #[PublicPage] + public function unshare(int $id, ?string $token = null) { + if (!$this->isS2SEnabled()) { + throw new OCSException('Server does not support federated cloud sharing', 503); + } + + try { + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); + $notification = ['sharedSecret' => $token]; + $provider->notificationReceived('SHARE_UNSHARED', $id, $notification); + $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('Federated share with id "%s" was unshared', [$id])); + } catch (\Exception $e) { + $this->logger->debug('processing unshare notification failed: ' . $e->getMessage(), ['exception' => $e]); + } + + return new DataResponse(); + } + + private function cleanupRemote($remote) { + $remote = substr($remote, strpos($remote, '://') + 3); + + return rtrim($remote, '/'); + } + + + /** + * federated share was revoked, either by the owner or the re-sharer + * + * @param int $id ID of the share + * @param string|null $token Shared secret between servers + * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSBadRequestException Revoking the share is not possible + * + * 200: Share revoked successfully + */ + #[NoCSRFRequired] + #[PublicPage] + public function revoke(int $id, ?string $token = null) { + try { + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); + $notification = ['sharedSecret' => $token]; + $provider->notificationReceived('RESHARE_UNDO', $id, $notification); + return new DataResponse(); + } catch (\Exception $e) { + throw new OCSBadRequestException(); + } + } + + /** + * check if server-to-server sharing is enabled + * + * @param bool $incoming + * @return bool + */ + private function isS2SEnabled($incoming = false) { + $result = Server::get(IAppManager::class)->isEnabledForUser('files_sharing'); + + if ($incoming) { + $result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled(); + } else { + $result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled(); + } + + return $result; + } + + /** + * update share information to keep federated re-shares in sync + * + * @param int $id ID of the share + * @param string|null $token Shared secret between servers + * @param int|null $permissions New permissions + * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSBadRequestException Updating permissions is not possible + * + * 200: Permissions updated successfully + */ + #[NoCSRFRequired] + #[PublicPage] + public function updatePermissions(int $id, ?string $token = null, ?int $permissions = null) { + $ncPermissions = $permissions; + + try { + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); + $ocmPermissions = $this->ncPermissions2ocmPermissions((int)$ncPermissions); + $notification = ['sharedSecret' => $token, 'permission' => $ocmPermissions]; + $provider->notificationReceived('RESHARE_CHANGE_PERMISSION', $id, $notification); + $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('Federated share with id "%s" has updated permissions "%s"', [$id, implode(', ', $ocmPermissions)])); + } catch (\Exception $e) { + $this->logger->debug($e->getMessage(), ['exception' => $e]); + throw new OCSBadRequestException(); + } + + return new DataResponse(); + } + + /** + * translate Nextcloud permissions to OCM Permissions + * + * @param $ncPermissions + * @return array + */ + protected function ncPermissions2ocmPermissions($ncPermissions) { + $ocmPermissions = []; + + if ($ncPermissions & Constants::PERMISSION_SHARE) { + $ocmPermissions[] = 'share'; + } + + if ($ncPermissions & Constants::PERMISSION_READ) { + $ocmPermissions[] = 'read'; + } + + if (($ncPermissions & Constants::PERMISSION_CREATE) + || ($ncPermissions & Constants::PERMISSION_UPDATE)) { + $ocmPermissions[] = 'write'; + } + + return $ocmPermissions; + } + + /** + * change the owner of a server-to-server share + * + * @param int $id ID of the share + * @param string|null $token Shared secret between servers + * @param string|null $remote Address of the remote + * @param string|null $remote_id ID of the remote + * @return Http\DataResponse<Http::STATUS_OK, array{remote: string, owner: string}, array{}> + * @throws OCSBadRequestException Moving share is not possible + * + * 200: Share moved successfully + */ + #[NoCSRFRequired] + #[PublicPage] + public function move(int $id, ?string $token = null, ?string $remote = null, ?string $remote_id = null) { + if (!$this->isS2SEnabled()) { + throw new OCSException('Server does not support federated cloud sharing', 503); + } + + $newRemoteId = (string)($remote_id ?? $id); + $cloudId = $this->cloudIdManager->resolveCloudId($remote); + + $qb = $this->connection->getQueryBuilder(); + $query = $qb->update('share_external') + ->set('remote', $qb->createNamedParameter($cloudId->getRemote())) + ->set('owner', $qb->createNamedParameter($cloudId->getUser())) + ->set('remote_id', $qb->createNamedParameter($newRemoteId)) + ->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('share_token', $qb->createNamedParameter($token))); + $affected = $query->executeStatement(); + + if ($affected > 0) { + return new DataResponse(['remote' => $cloudId->getRemote(), 'owner' => $cloudId->getUser()]); + } else { + throw new OCSBadRequestException('Share not found or token invalid'); + } + } +} diff --git a/apps/federatedfilesharing/lib/Events/FederatedShareAddedEvent.php b/apps/federatedfilesharing/lib/Events/FederatedShareAddedEvent.php new file mode 100644 index 00000000000..2a79f434b8c --- /dev/null +++ b/apps/federatedfilesharing/lib/Events/FederatedShareAddedEvent.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\FederatedFileSharing\Events; + +use OCP\EventDispatcher\Event; + +/** + * This event is triggered when a federated share is successfully added + * + * @since 20.0.0 + */ +class FederatedShareAddedEvent extends Event { + + /** + * @since 20.0.0 + */ + public function __construct( + private string $remote, + ) { + } + + /** + * @since 20.0.0 + */ + public function getRemote(): string { + return $this->remote; + } +} diff --git a/apps/federatedfilesharing/lib/FederatedShareProvider.php b/apps/federatedfilesharing/lib/FederatedShareProvider.php new file mode 100644 index 00000000000..8a2c12e0ac8 --- /dev/null +++ b/apps/federatedfilesharing/lib/FederatedShareProvider.php @@ -0,0 +1,1089 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\FederatedFileSharing; + +use OC\Share20\Exception\InvalidShare; +use OC\Share20\Share; +use OCP\Constants; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Federation\ICloudFederationProviderManager; +use OCP\Federation\ICloudIdManager; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\HintException; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\IUserManager; +use OCP\Share\Exceptions\GenericShareException; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IShare; +use OCP\Share\IShareProvider; +use OCP\Share\IShareProviderSupportsAllSharesInFolder; +use Psr\Log\LoggerInterface; + +/** + * Class FederatedShareProvider + * + * @package OCA\FederatedFileSharing + */ +class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAllSharesInFolder { + public const SHARE_TYPE_REMOTE = 6; + + /** @var string */ + private $externalShareTable = 'share_external'; + + /** @var array list of supported share types */ + private $supportedShareType = [IShare::TYPE_REMOTE_GROUP, IShare::TYPE_REMOTE, IShare::TYPE_CIRCLE]; + + /** + * DefaultShareProvider constructor. + */ + public function __construct( + private IDBConnection $dbConnection, + private AddressHandler $addressHandler, + private Notifications $notifications, + private TokenHandler $tokenHandler, + private IL10N $l, + private IRootFolder $rootFolder, + private IConfig $config, + private IUserManager $userManager, + private ICloudIdManager $cloudIdManager, + private \OCP\GlobalScale\IConfig $gsConfig, + private ICloudFederationProviderManager $cloudFederationProviderManager, + private LoggerInterface $logger, + ) { + } + + /** + * Return the identifier of this provider. + * + * @return string Containing only [a-zA-Z0-9] + */ + public function identifier() { + return 'ocFederatedSharing'; + } + + /** + * Share a path + * + * @param IShare $share + * @return IShare The share object + * @throws ShareNotFound + * @throws \Exception + */ + public function create(IShare $share) { + $shareWith = $share->getSharedWith(); + $itemSource = $share->getNodeId(); + $itemType = $share->getNodeType(); + $permissions = $share->getPermissions(); + $sharedBy = $share->getSharedBy(); + $shareType = $share->getShareType(); + $expirationDate = $share->getExpirationDate(); + + if ($shareType === IShare::TYPE_REMOTE_GROUP + && !$this->isOutgoingServer2serverGroupShareEnabled() + ) { + $message = 'It is not allowed to send federated group shares from this server.'; + $message_t = $this->l->t('It is not allowed to send federated group shares from this server.'); + $this->logger->debug($message, ['app' => 'Federated File Sharing']); + throw new \Exception($message_t); + } + + /* + * Check if file is not already shared with the remote user + */ + $alreadyShared = $this->getSharedWith($shareWith, IShare::TYPE_REMOTE, $share->getNode(), 1, 0); + $alreadySharedGroup = $this->getSharedWith($shareWith, IShare::TYPE_REMOTE_GROUP, $share->getNode(), 1, 0); + if (!empty($alreadyShared) || !empty($alreadySharedGroup)) { + $message = 'Sharing %1$s failed, because this item is already shared with %2$s'; + $message_t = $this->l->t('Sharing %1$s failed, because this item is already shared with the account %2$s', [$share->getNode()->getName(), $shareWith]); + $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']); + throw new \Exception($message_t); + } + + + // don't allow federated shares if source and target server are the same + $cloudId = $this->cloudIdManager->resolveCloudId($shareWith); + $currentServer = $this->addressHandler->generateRemoteURL(); + $currentUser = $sharedBy; + if ($this->addressHandler->compareAddresses($cloudId->getUser(), $cloudId->getRemote(), $currentUser, $currentServer)) { + $message = 'Not allowed to create a federated share to the same account.'; + $message_t = $this->l->t('Not allowed to create a federated share to the same account'); + $this->logger->debug($message, ['app' => 'Federated File Sharing']); + throw new \Exception($message_t); + } + + // Federated shares always have read permissions + if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) { + $message = 'Federated shares require read permissions'; + $message_t = $this->l->t('Federated shares require read permissions'); + $this->logger->debug($message, ['app' => 'Federated File Sharing']); + throw new \Exception($message_t); + } + + $share->setSharedWith($cloudId->getId()); + + try { + $remoteShare = $this->getShareFromExternalShareTable($share); + } catch (ShareNotFound $e) { + $remoteShare = null; + } + + if ($remoteShare) { + try { + $ownerCloudId = $this->cloudIdManager->getCloudId($remoteShare['owner'], $remoteShare['remote']); + $shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ownerCloudId->getId(), $permissions, 'tmp_token_' . time(), $shareType, $expirationDate); + $share->setId($shareId); + [$token, $remoteId] = $this->askOwnerToReShare($shareWith, $share, $shareId); + // remote share was create successfully if we get a valid token as return + $send = is_string($token) && $token !== ''; + } catch (\Exception $e) { + // fall back to old re-share behavior if the remote server + // doesn't support flat re-shares (was introduced with Nextcloud 9.1) + $this->removeShareFromTable($share); + $shareId = $this->createFederatedShare($share); + } + if ($send) { + $this->updateSuccessfulReshare($shareId, $token); + $this->storeRemoteId($shareId, $remoteId); + } else { + $this->removeShareFromTable($share); + $message_t = $this->l->t('File is already shared with %s', [$shareWith]); + throw new \Exception($message_t); + } + } else { + $shareId = $this->createFederatedShare($share); + } + + $data = $this->getRawShare($shareId); + return $this->createShareObject($data); + } + + /** + * create federated share and inform the recipient + * + * @param IShare $share + * @return int + * @throws ShareNotFound + * @throws \Exception + */ + protected function createFederatedShare(IShare $share) { + $token = $this->tokenHandler->generateToken(); + $shareId = $this->addShareToDB( + $share->getNodeId(), + $share->getNodeType(), + $share->getSharedWith(), + $share->getSharedBy(), + $share->getShareOwner(), + $share->getPermissions(), + $token, + $share->getShareType(), + $share->getExpirationDate() + ); + + $failure = false; + + try { + $sharedByFederatedId = $share->getSharedBy(); + if ($this->userManager->userExists($sharedByFederatedId)) { + $cloudId = $this->cloudIdManager->getCloudId($sharedByFederatedId, $this->addressHandler->generateRemoteURL()); + $sharedByFederatedId = $cloudId->getId(); + } + $ownerCloudId = $this->cloudIdManager->getCloudId($share->getShareOwner(), $this->addressHandler->generateRemoteURL()); + $send = $this->notifications->sendRemoteShare( + $token, + $share->getSharedWith(), + $share->getNode()->getName(), + $shareId, + $share->getShareOwner(), + $ownerCloudId->getId(), + $share->getSharedBy(), + $sharedByFederatedId, + $share->getShareType() + ); + + if ($send === false) { + $failure = true; + } + } catch (\Exception $e) { + $this->logger->error('Failed to notify remote server of federated share, removing share.', [ + 'app' => 'federatedfilesharing', + 'exception' => $e, + ]); + $failure = true; + } + + if ($failure) { + $this->removeShareFromTableById($shareId); + $message_t = $this->l->t('Sharing %1$s failed, could not find %2$s, maybe the server is currently unreachable or uses a self-signed certificate.', + [$share->getNode()->getName(), $share->getSharedWith()]); + throw new \Exception($message_t); + } + + return $shareId; + } + + /** + * @param string $shareWith + * @param IShare $share + * @param string $shareId internal share Id + * @return array + * @throws \Exception + */ + protected function askOwnerToReShare($shareWith, IShare $share, $shareId) { + $remoteShare = $this->getShareFromExternalShareTable($share); + $token = $remoteShare['share_token']; + $remoteId = $remoteShare['remote_id']; + $remote = $remoteShare['remote']; + + [$token, $remoteId] = $this->notifications->requestReShare( + $token, + $remoteId, + $shareId, + $remote, + $shareWith, + $share->getPermissions(), + $share->getNode()->getName(), + $share->getShareType(), + ); + + return [$token, $remoteId]; + } + + /** + * get federated share from the share_external table but exclude mounted link shares + * + * @param IShare $share + * @return array + * @throws ShareNotFound + */ + protected function getShareFromExternalShareTable(IShare $share) { + $query = $this->dbConnection->getQueryBuilder(); + $query->select('*')->from($this->externalShareTable) + ->where($query->expr()->eq('user', $query->createNamedParameter($share->getShareOwner()))) + ->andWhere($query->expr()->eq('mountpoint', $query->createNamedParameter($share->getTarget()))); + $qResult = $query->executeQuery(); + $result = $qResult->fetchAll(); + $qResult->closeCursor(); + + if (isset($result[0]) && (int)$result[0]['remote_id'] > 0) { + return $result[0]; + } + + throw new ShareNotFound('share not found in share_external table'); + } + + /** + * add share to the database and return the ID + * + * @param int $itemSource + * @param string $itemType + * @param string $shareWith + * @param string $sharedBy + * @param string $uidOwner + * @param int $permissions + * @param string $token + * @param int $shareType + * @param \DateTime $expirationDate + * @return int + */ + private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $shareType, $expirationDate) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert('share') + ->setValue('share_type', $qb->createNamedParameter($shareType)) + ->setValue('item_type', $qb->createNamedParameter($itemType)) + ->setValue('item_source', $qb->createNamedParameter($itemSource)) + ->setValue('file_source', $qb->createNamedParameter($itemSource)) + ->setValue('share_with', $qb->createNamedParameter($shareWith)) + ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) + ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) + ->setValue('permissions', $qb->createNamedParameter($permissions)) + ->setValue('expiration', $qb->createNamedParameter($expirationDate, IQueryBuilder::PARAM_DATETIME_MUTABLE)) + ->setValue('token', $qb->createNamedParameter($token)) + ->setValue('stime', $qb->createNamedParameter(time())); + + /* + * Added to fix https://github.com/owncloud/core/issues/22215 + * Can be removed once we get rid of ajax/share.php + */ + $qb->setValue('file_target', $qb->createNamedParameter('')); + + $qb->executeStatement(); + return $qb->getLastInsertId(); + } + + /** + * Update a share + * + * @param IShare $share + * @return IShare The share object + */ + public function update(IShare $share) { + /* + * We allow updating the permissions of federated shares + */ + $qb = $this->dbConnection->getQueryBuilder(); + $qb->update('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) + ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) + ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) + ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATETIME_MUTABLE)) + ->executeStatement(); + + // send the updated permission to the owner/initiator, if they are not the same + if ($share->getShareOwner() !== $share->getSharedBy()) { + $this->sendPermissionUpdate($share); + } + + return $share; + } + + /** + * send the updated permission to the owner/initiator, if they are not the same + * + * @param IShare $share + * @throws ShareNotFound + * @throws HintException + */ + protected function sendPermissionUpdate(IShare $share) { + $remoteId = $this->getRemoteId($share); + // if the local user is the owner we send the permission change to the initiator + if ($this->userManager->userExists($share->getShareOwner())) { + [, $remote] = $this->addressHandler->splitUserRemote($share->getSharedBy()); + } else { // ... if not we send the permission change to the owner + [, $remote] = $this->addressHandler->splitUserRemote($share->getShareOwner()); + } + $this->notifications->sendPermissionChange($remote, $remoteId, $share->getToken(), $share->getPermissions()); + } + + + /** + * update successful reShare with the correct token + * + * @param int $shareId + * @param string $token + */ + protected function updateSuccessfulReShare($shareId, $token) { + $query = $this->dbConnection->getQueryBuilder(); + $query->update('share') + ->where($query->expr()->eq('id', $query->createNamedParameter($shareId))) + ->set('token', $query->createNamedParameter($token)) + ->executeStatement(); + } + + /** + * store remote ID in federated reShare table + * + * @param $shareId + * @param $remoteId + */ + public function storeRemoteId(int $shareId, string $remoteId): void { + $query = $this->dbConnection->getQueryBuilder(); + $query->insert('federated_reshares') + ->values( + [ + 'share_id' => $query->createNamedParameter($shareId), + 'remote_id' => $query->createNamedParameter($remoteId), + ] + ); + $query->executeStatement(); + } + + /** + * get share ID on remote server for federated re-shares + * + * @param IShare $share + * @return string + * @throws ShareNotFound + */ + public function getRemoteId(IShare $share): string { + $query = $this->dbConnection->getQueryBuilder(); + $query->select('remote_id')->from('federated_reshares') + ->where($query->expr()->eq('share_id', $query->createNamedParameter((int)$share->getId()))); + $result = $query->executeQuery(); + $data = $result->fetch(); + $result->closeCursor(); + + if (!is_array($data) || !isset($data['remote_id'])) { + throw new ShareNotFound(); + } + + return (string)$data['remote_id']; + } + + /** + * @inheritdoc + */ + public function move(IShare $share, $recipient) { + /* + * This function does nothing yet as it is just for outgoing + * federated shares. + */ + return $share; + } + + public function getChildren(IShare $parent): array { + $children = []; + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) + ->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY))) + ->orderBy('id'); + + $cursor = $qb->executeQuery(); + while ($data = $cursor->fetch()) { + $children[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + return $children; + } + + /** + * Delete a share (owner unShares the file) + * + * @param IShare $share + * @throws ShareNotFound + * @throws HintException + */ + public function delete(IShare $share) { + [, $remote] = $this->addressHandler->splitUserRemote($share->getSharedWith()); + + // if the local user is the owner we can send the unShare request directly... + if ($this->userManager->userExists($share->getShareOwner())) { + $this->notifications->sendRemoteUnShare($remote, $share->getId(), $share->getToken()); + $this->revokeShare($share, true); + } else { // ... if not we need to correct ID for the unShare request + $remoteId = $this->getRemoteId($share); + $this->notifications->sendRemoteUnShare($remote, $remoteId, $share->getToken()); + $this->revokeShare($share, false); + } + + // only remove the share when all messages are send to not lose information + // about the share to early + $this->removeShareFromTable($share); + } + + /** + * in case of a re-share we need to send the other use (initiator or owner) + * a message that the file was unshared + * + * @param IShare $share + * @param bool $isOwner the user can either be the owner or the user who re-sahred it + * @throws ShareNotFound + * @throws HintException + */ + protected function revokeShare($share, $isOwner) { + if ($this->userManager->userExists($share->getShareOwner()) && $this->userManager->userExists($share->getSharedBy())) { + // If both the owner and the initiator of the share are local users we don't have to notify anybody else + return; + } + + // also send a unShare request to the initiator, if this is a different user than the owner + if ($share->getShareOwner() !== $share->getSharedBy()) { + if ($isOwner) { + [, $remote] = $this->addressHandler->splitUserRemote($share->getSharedBy()); + } else { + [, $remote] = $this->addressHandler->splitUserRemote($share->getShareOwner()); + } + $remoteId = $this->getRemoteId($share); + $this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken()); + } + } + + /** + * remove share from table + * + * @param IShare $share + */ + public function removeShareFromTable(IShare $share) { + $this->removeShareFromTableById($share->getId()); + } + + /** + * remove share from table + * + * @param string $shareId + */ + private function removeShareFromTableById($shareId) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId))) + ->andWhere($qb->expr()->neq('share_type', $qb->createNamedParameter(IShare::TYPE_CIRCLE))); + $qb->executeStatement(); + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete('federated_reshares') + ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($shareId))); + $qb->executeStatement(); + } + + /** + * @inheritdoc + */ + public function deleteFromSelf(IShare $share, $recipient) { + // nothing to do here. Technically deleteFromSelf in the context of federated + // shares is a umount of an external storage. This is handled here + // apps/files_sharing/lib/external/manager.php + // TODO move this code over to this app + } + + public function restore(IShare $share, string $recipient): IShare { + throw new GenericShareException('not implemented'); + } + + + public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = true) { + if (!$shallow) { + throw new \Exception('non-shallow getSharesInFolder is no longer supported'); + } + return $this->getSharesInFolderInternal($userId, $node, $reshares); + } + + public function getAllSharesInFolder(Folder $node): array { + return $this->getSharesInFolderInternal(null, $node, null); + } + + /** + * @return array<int, list<IShare>> + */ + private function getSharesInFolderInternal(?string $userId, Folder $node, ?bool $reshares): array { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share', 's') + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))) + ->andWhere( + $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_REMOTE)) + ); + + if ($userId !== null) { + /** + * Reshares for this user are shares where they are the owner. + */ + if ($reshares !== true) { + $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))); + } else { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) + ) + ); + } + } + + $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')); + + $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); + + $qb->orderBy('id'); + + $cursor = $qb->executeQuery(); + $shares = []; + while ($data = $cursor->fetch()) { + $shares[$data['fileid']][] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + return $shares; + } + + /** + * @inheritdoc + */ + public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share'); + + $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter($shareType))); + + /** + * Reshares for this user are shares where they are the owner. + */ + if ($reshares === false) { + //Special case for old shares created via the web UI + $or1 = $qb->expr()->andX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->isNull('uid_initiator') + ); + + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), + $or1 + ) + ); + } else { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) + ) + ); + } + + if ($node !== null) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); + } + + if ($limit !== -1) { + $qb->setMaxResults($limit); + } + + $qb->setFirstResult($offset); + $qb->orderBy('id'); + + $cursor = $qb->executeQuery(); + $shares = []; + while ($data = $cursor->fetch()) { + $shares[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + return $shares; + } + + /** + * @inheritdoc + */ + public function getShareById($id, $recipientId = null) { + $qb = $this->dbConnection->getQueryBuilder(); + + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY))); + + $cursor = $qb->executeQuery(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new ShareNotFound('Can not find share with ID: ' . $id); + } + + try { + $share = $this->createShareObject($data); + } catch (InvalidShare $e) { + throw new ShareNotFound(); + } + + return $share; + } + + /** + * Get shares for a given path + * + * @param Node $path + * @return IShare[] + */ + public function getSharesByPath(Node $path) { + $qb = $this->dbConnection->getQueryBuilder(); + + // get federated user shares + $cursor = $qb->select('*') + ->from('share') + ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) + ->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY))) + ->executeQuery(); + + $shares = []; + while ($data = $cursor->fetch()) { + $shares[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + return $shares; + } + + /** + * @inheritdoc + */ + public function getSharedWith($userId, $shareType, $node, $limit, $offset) { + /** @var IShare[] $shares */ + $shares = []; + + //Get shares directly with this user + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share'); + + // Order by id + $qb->orderBy('id'); + + // Set limit and offset + if ($limit !== -1) { + $qb->setMaxResults($limit); + } + $qb->setFirstResult($offset); + + $qb->where($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY))); + $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); + + // Filter by node if provided + if ($node !== null) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); + } + + $cursor = $qb->executeQuery(); + + while ($data = $cursor->fetch()) { + $shares[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + + return $shares; + } + + /** + * Get a share by token + * + * @param string $token + * @return IShare + * @throws ShareNotFound + */ + public function getShareByToken($token) { + $qb = $this->dbConnection->getQueryBuilder(); + + $cursor = $qb->select('*') + ->from('share') + ->where($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->executeQuery(); + + $data = $cursor->fetch(); + + if ($data === false) { + throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); + } + + try { + $share = $this->createShareObject($data); + } catch (InvalidShare $e) { + throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); + } + + return $share; + } + + /** + * get database row of a give share + * + * @param $id + * @return array + * @throws ShareNotFound + */ + private function getRawShare($id) { + // Now fetch the inserted share and create a complete share object + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); + + $cursor = $qb->executeQuery(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new ShareNotFound; + } + + return $data; + } + + /** + * Create a share object from an database row + * + * @param array $data + * @return IShare + * @throws InvalidShare + * @throws ShareNotFound + */ + private function createShareObject($data) { + $share = new Share($this->rootFolder, $this->userManager); + $share->setId((int)$data['id']) + ->setShareType((int)$data['share_type']) + ->setPermissions((int)$data['permissions']) + ->setTarget($data['file_target']) + ->setMailSend((bool)$data['mail_send']) + ->setStatus((int)$data['accepted']) + ->setToken($data['token']); + + $shareTime = new \DateTime(); + $shareTime->setTimestamp((int)$data['stime']); + $share->setShareTime($shareTime); + $share->setSharedWith($data['share_with']); + + if ($data['uid_initiator'] !== null) { + $share->setShareOwner($data['uid_owner']); + $share->setSharedBy($data['uid_initiator']); + } else { + //OLD SHARE + $share->setSharedBy($data['uid_owner']); + $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']); + + $owner = $path->getOwner(); + $share->setShareOwner($owner->getUID()); + } + + $share->setNodeId((int)$data['file_source']); + $share->setNodeType($data['item_type']); + + $share->setProviderId($this->identifier()); + + if ($data['expiration'] !== null) { + $expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']); + $share->setExpirationDate($expiration); + } + + return $share; + } + + /** + * Get the node with file $id for $user + * + * @param string $userId + * @param int $id + * @return Node + * @throws InvalidShare + */ + private function getNode($userId, $id) { + try { + $userFolder = $this->rootFolder->getUserFolder($userId); + } catch (NotFoundException $e) { + throw new InvalidShare(); + } + + $node = $userFolder->getFirstNodeById($id); + + if (!$node) { + throw new InvalidShare(); + } + + return $node; + } + + /** + * A user is deleted from the system + * So clean up the relevant shares. + * + * @param string $uid + * @param int $shareType + */ + public function userDeleted($uid, $shareType) { + //TODO: probably a good idea to send unshare info to remote servers + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_REMOTE))) + ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid))) + ->executeStatement(); + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete('share_external') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) + ->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($uid))) + ->executeStatement(); + } + + public function groupDeleted($gid) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('id') + ->from('share_external') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) + // This is not a typo, the group ID is really stored in the 'user' column + ->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($gid))); + $cursor = $qb->executeQuery(); + $parentShareIds = $cursor->fetchAll(\PDO::FETCH_COLUMN); + $cursor->closeCursor(); + if ($parentShareIds === []) { + return; + } + + $qb = $this->dbConnection->getQueryBuilder(); + $parentShareIdsParam = $qb->createNamedParameter($parentShareIds, IQueryBuilder::PARAM_INT_ARRAY); + $qb->delete('share_external') + ->where($qb->expr()->in('id', $parentShareIdsParam)) + ->orWhere($qb->expr()->in('parent', $parentShareIdsParam)) + ->executeStatement(); + } + + public function userDeletedFromGroup($uid, $gid) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('id') + ->from('share_external') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) + // This is not a typo, the group ID is really stored in the 'user' column + ->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($gid))); + $cursor = $qb->executeQuery(); + $parentShareIds = $cursor->fetchAll(\PDO::FETCH_COLUMN); + $cursor->closeCursor(); + if ($parentShareIds === []) { + return; + } + + $qb = $this->dbConnection->getQueryBuilder(); + $parentShareIdsParam = $qb->createNamedParameter($parentShareIds, IQueryBuilder::PARAM_INT_ARRAY); + $qb->delete('share_external') + ->where($qb->expr()->in('parent', $parentShareIdsParam)) + ->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($uid))) + ->executeStatement(); + } + + /** + * Check if users from other Nextcloud instances are allowed to mount public links share by this instance + */ + public function isOutgoingServer2serverShareEnabled(): bool { + if ($this->gsConfig->onlyInternalFederation()) { + return false; + } + $result = $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes'); + return $result === 'yes'; + } + + /** + * Check if users are allowed to mount public links from other Nextclouds + */ + public function isIncomingServer2serverShareEnabled(): bool { + if ($this->gsConfig->onlyInternalFederation()) { + return false; + } + $result = $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes'); + return $result === 'yes'; + } + + + /** + * Check if users from other Nextcloud instances are allowed to send federated group shares + */ + public function isOutgoingServer2serverGroupShareEnabled(): bool { + if ($this->gsConfig->onlyInternalFederation()) { + return false; + } + $result = $this->config->getAppValue('files_sharing', 'outgoing_server2server_group_share_enabled', 'no'); + return $result === 'yes'; + } + + /** + * Check if users are allowed to receive federated group shares + */ + public function isIncomingServer2serverGroupShareEnabled(): bool { + if ($this->gsConfig->onlyInternalFederation()) { + return false; + } + $result = $this->config->getAppValue('files_sharing', 'incoming_server2server_group_share_enabled', 'no'); + return $result === 'yes'; + } + + /** + * Check if federated group sharing is supported, therefore the OCM API need to be enabled + */ + public function isFederatedGroupSharingSupported(): bool { + return $this->cloudFederationProviderManager->isReady(); + } + + /** + * Check if querying sharees on the lookup server is enabled + */ + public function isLookupServerQueriesEnabled(): bool { + // in a global scale setup we should always query the lookup server + if ($this->gsConfig->isGlobalScaleEnabled()) { + return true; + } + $result = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'no') === 'yes'; + // TODO: Reenable if lookup server is used again + // return $result; + return false; + } + + + /** + * Check if it is allowed to publish user specific data to the lookup server + */ + public function isLookupServerUploadEnabled(): bool { + // in a global scale setup the admin is responsible to keep the lookup server up-to-date + if ($this->gsConfig->isGlobalScaleEnabled()) { + return false; + } + $result = $this->config->getAppValue('files_sharing', 'lookupServerUploadEnabled', 'no') === 'yes'; + // TODO: Reenable if lookup server is used again + // return $result; + return false; + } + + /** + * Check if auto accepting incoming shares from trusted servers is enabled + */ + public function isFederatedTrustedShareAutoAccept(): bool { + $result = $this->config->getAppValue('files_sharing', 'federatedTrustedShareAutoAccept', 'yes'); + return $result === 'yes'; + } + + public function getAccessList($nodes, $currentAccess) { + $ids = []; + foreach ($nodes as $node) { + $ids[] = $node->getId(); + } + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('share_with', 'token', 'file_source') + ->from('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_REMOTE))) + ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))); + $cursor = $qb->executeQuery(); + + if ($currentAccess === false) { + $remote = $cursor->fetch() !== false; + $cursor->closeCursor(); + + return ['remote' => $remote]; + } + + $remote = []; + while ($row = $cursor->fetch()) { + $remote[$row['share_with']] = [ + 'node_id' => $row['file_source'], + 'token' => $row['token'], + ]; + } + $cursor->closeCursor(); + + return ['remote' => $remote]; + } + + public function getAllShares(): iterable { + $qb = $this->dbConnection->getQueryBuilder(); + + $qb->select('*') + ->from('share') + ->where($qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_REMOTE_GROUP, IShare::TYPE_REMOTE], IQueryBuilder::PARAM_INT_ARRAY))); + + $cursor = $qb->executeQuery(); + while ($data = $cursor->fetch()) { + try { + $share = $this->createShareObject($data); + } catch (InvalidShare $e) { + continue; + } catch (ShareNotFound $e) { + continue; + } + + yield $share; + } + $cursor->closeCursor(); + } +} diff --git a/apps/federatedfilesharing/lib/Listeners/LoadAdditionalScriptsListener.php b/apps/federatedfilesharing/lib/Listeners/LoadAdditionalScriptsListener.php new file mode 100644 index 00000000000..34fbd85db5a --- /dev/null +++ b/apps/federatedfilesharing/lib/Listeners/LoadAdditionalScriptsListener.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\FederatedFileSharing\Listeners; + +use OCA\FederatedFileSharing\FederatedShareProvider; +use OCA\Files\Event\LoadAdditionalScriptsEvent; +use OCP\App\IAppManager; +use OCP\AppFramework\Services\IInitialState; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Util; + +/** @template-implements IEventListener<LoadAdditionalScriptsEvent> */ +class LoadAdditionalScriptsListener implements IEventListener { + public function __construct( + private FederatedShareProvider $federatedShareProvider, + private IInitialState $initialState, + private IAppManager $appManager, + ) { + $this->federatedShareProvider = $federatedShareProvider; + $this->initialState = $initialState; + $this->appManager = $appManager; + } + + public function handle(Event $event): void { + if (!$event instanceof LoadAdditionalScriptsEvent) { + return; + } + + if ($this->federatedShareProvider->isIncomingServer2serverShareEnabled()) { + $this->initialState->provideInitialState('notificationsEnabled', $this->appManager->isEnabledForUser('notifications')); + Util::addInitScript('federatedfilesharing', 'external'); + } + } +} diff --git a/apps/federatedfilesharing/lib/Migration/Version1010Date20200630191755.php b/apps/federatedfilesharing/lib/Migration/Version1010Date20200630191755.php new file mode 100644 index 00000000000..27197ab67f6 --- /dev/null +++ b/apps/federatedfilesharing/lib/Migration/Version1010Date20200630191755.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\FederatedFileSharing\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1010Date20200630191755 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('federated_reshares')) { + $table = $schema->createTable('federated_reshares'); + $table->addColumn('share_id', Types::BIGINT, [ + 'notnull' => true, + ]); + $table->addColumn('remote_id', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + 'default' => '', + ]); + $table->setPrimaryKey(['share_id'], 'federated_res_pk'); + // $table->addUniqueIndex(['share_id'], 'share_id_index'); + } + return $schema; + } +} diff --git a/apps/federatedfilesharing/lib/Migration/Version1011Date20201120125158.php b/apps/federatedfilesharing/lib/Migration/Version1011Date20201120125158.php new file mode 100644 index 00000000000..e78c93ec1a5 --- /dev/null +++ b/apps/federatedfilesharing/lib/Migration/Version1011Date20201120125158.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\FederatedFileSharing\Migration; + +use Closure; +use Doctrine\DBAL\Types\Type; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1011Date20201120125158 extends SimpleMigrationStep { + + public function __construct( + private IDBConnection $connection, + ) { + } + + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if ($schema->hasTable('federated_reshares')) { + $table = $schema->getTable('federated_reshares'); + $remoteIdColumn = $table->getColumn('remote_id'); + if ($remoteIdColumn && $remoteIdColumn->getType()->getName() !== Types::STRING) { + $remoteIdColumn->setNotnull(false); + $remoteIdColumn->setType(Type::getType(Types::STRING)); + $remoteIdColumn->setOptions(['length' => 255]); + $remoteIdColumn->setDefault(''); + return $schema; + } + } + + return null; + } + + public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) { + $qb = $this->connection->getQueryBuilder(); + $qb->update('federated_reshares') + ->set('remote_id', $qb->createNamedParameter('')) + ->where($qb->expr()->eq('remote_id', $qb->createNamedParameter('-1'))); + $qb->execute(); + } +} diff --git a/apps/federatedfilesharing/lib/Notifications.php b/apps/federatedfilesharing/lib/Notifications.php new file mode 100644 index 00000000000..613c05613ef --- /dev/null +++ b/apps/federatedfilesharing/lib/Notifications.php @@ -0,0 +1,421 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\FederatedFileSharing; + +use OCA\FederatedFileSharing\Events\FederatedShareAddedEvent; +use OCP\AppFramework\Http; +use OCP\BackgroundJob\IJobList; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Federation\ICloudFederationFactory; +use OCP\Federation\ICloudFederationProviderManager; +use OCP\HintException; +use OCP\Http\Client\IClientService; +use OCP\OCS\IDiscoveryService; +use Psr\Log\LoggerInterface; + +class Notifications { + public const RESPONSE_FORMAT = 'json'; // default response format for ocs calls + + public function __construct( + private AddressHandler $addressHandler, + private IClientService $httpClientService, + private IDiscoveryService $discoveryService, + private IJobList $jobList, + private ICloudFederationProviderManager $federationProviderManager, + private ICloudFederationFactory $cloudFederationFactory, + private IEventDispatcher $eventDispatcher, + private LoggerInterface $logger, + ) { + } + + /** + * send server-to-server share to remote server + * + * @param string $token + * @param string $shareWith + * @param string $name + * @param string $remoteId + * @param string $owner + * @param string $ownerFederatedId + * @param string $sharedBy + * @param string $sharedByFederatedId + * @param int $shareType (can be a remote user or group share) + * @return bool + * @throws HintException + * @throws \OC\ServerNotAvailableException + */ + public function sendRemoteShare($token, $shareWith, $name, $remoteId, $owner, $ownerFederatedId, $sharedBy, $sharedByFederatedId, $shareType) { + [$user, $remote] = $this->addressHandler->splitUserRemote($shareWith); + + if ($user && $remote) { + $local = $this->addressHandler->generateRemoteURL(); + + $fields = [ + 'shareWith' => $user, + 'token' => $token, + 'name' => $name, + 'remoteId' => $remoteId, + 'owner' => $owner, + 'ownerFederatedId' => $ownerFederatedId, + 'sharedBy' => $sharedBy, + 'sharedByFederatedId' => $sharedByFederatedId, + 'remote' => $local, + 'shareType' => $shareType + ]; + + $result = $this->tryHttpPostToShareEndpoint($remote, '', $fields); + $status = json_decode($result['result'], true); + + $ocsStatus = isset($status['ocs']); + $ocsSuccess = $ocsStatus && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200); + + if ($result['success'] && (!$ocsStatus || $ocsSuccess)) { + $event = new FederatedShareAddedEvent($remote); + $this->eventDispatcher->dispatchTyped($event); + return true; + } else { + $this->logger->info( + "failed sharing $name with $shareWith", + ['app' => 'federatedfilesharing'] + ); + } + } else { + $this->logger->info( + "could not share $name, invalid contact $shareWith", + ['app' => 'federatedfilesharing'] + ); + } + + return false; + } + + /** + * ask owner to re-share the file with the given user + * + * @param string $token + * @param string $id remote Id + * @param string $shareId internal share Id + * @param string $remote remote address of the owner + * @param string $shareWith + * @param int $permission + * @param string $filename + * @return array|false + * @throws HintException + * @throws \OC\ServerNotAvailableException + */ + public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission, $filename, $shareType) { + $fields = [ + 'shareWith' => $shareWith, + 'token' => $token, + 'permission' => $permission, + 'remoteId' => $shareId, + 'shareType' => $shareType, + ]; + + $ocmFields = $fields; + $ocmFields['remoteId'] = (string)$id; + $ocmFields['localId'] = $shareId; + $ocmFields['name'] = $filename; + + $ocmResult = $this->tryOCMEndPoint($remote, $ocmFields, 'reshare'); + if (is_array($ocmResult) && isset($ocmResult['token']) && isset($ocmResult['providerId'])) { + return [$ocmResult['token'], $ocmResult['providerId']]; + } + + $result = $this->tryLegacyEndPoint(rtrim($remote, '/'), '/' . $id . '/reshare', $fields); + $status = json_decode($result['result'], true); + + $httpRequestSuccessful = $result['success']; + $ocsCallSuccessful = $status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200; + $validToken = isset($status['ocs']['data']['token']) && is_string($status['ocs']['data']['token']); + $validRemoteId = isset($status['ocs']['data']['remoteId']); + + if ($httpRequestSuccessful && $ocsCallSuccessful && $validToken && $validRemoteId) { + return [ + $status['ocs']['data']['token'], + $status['ocs']['data']['remoteId'] + ]; + } elseif (!$validToken) { + $this->logger->info( + "invalid or missing token requesting re-share for $filename to $remote", + ['app' => 'federatedfilesharing'] + ); + } elseif (!$validRemoteId) { + $this->logger->info( + "missing remote id requesting re-share for $filename to $remote", + ['app' => 'federatedfilesharing'] + ); + } else { + $this->logger->info( + "failed requesting re-share for $filename to $remote", + ['app' => 'federatedfilesharing'] + ); + } + + return false; + } + + /** + * send server-to-server unshare to remote server + * + * @param string $remote url + * @param string $id share id + * @param string $token + * @return bool + */ + public function sendRemoteUnShare($remote, $id, $token) { + $this->sendUpdateToRemote($remote, $id, $token, 'unshare'); + } + + /** + * send server-to-server unshare to remote server + * + * @param string $remote url + * @param string $id share id + * @param string $token + * @return bool + */ + public function sendRevokeShare($remote, $id, $token) { + $this->sendUpdateToRemote($remote, $id, $token, 'reshare_undo'); + } + + /** + * send notification to remote server if the permissions was changed + * + * @param string $remote + * @param string $remoteId + * @param string $token + * @param int $permissions + * @return bool + */ + public function sendPermissionChange($remote, $remoteId, $token, $permissions) { + $this->sendUpdateToRemote($remote, $remoteId, $token, 'permissions', ['permissions' => $permissions]); + } + + /** + * forward accept reShare to remote server + * + * @param string $remote + * @param string $remoteId + * @param string $token + */ + public function sendAcceptShare($remote, $remoteId, $token) { + $this->sendUpdateToRemote($remote, $remoteId, $token, 'accept'); + } + + /** + * forward decline reShare to remote server + * + * @param string $remote + * @param string $remoteId + * @param string $token + */ + public function sendDeclineShare($remote, $remoteId, $token) { + $this->sendUpdateToRemote($remote, $remoteId, $token, 'decline'); + } + + /** + * inform remote server whether server-to-server share was accepted/declined + * + * @param string $remote + * @param string $token + * @param string $remoteId Share id on the remote host + * @param string $action possible actions: accept, decline, unshare, revoke, permissions + * @param array $data + * @param int $try + * @return boolean + */ + public function sendUpdateToRemote($remote, $remoteId, $token, $action, $data = [], $try = 0) { + $fields = [ + 'token' => $token, + 'remoteId' => $remoteId + ]; + foreach ($data as $key => $value) { + $fields[$key] = $value; + } + + $result = $this->tryHttpPostToShareEndpoint(rtrim($remote, '/'), '/' . $remoteId . '/' . $action, $fields, $action); + $status = json_decode($result['result'], true); + + if ($result['success'] + && isset($status['ocs']['meta']['statuscode']) + && ($status['ocs']['meta']['statuscode'] === 100 + || $status['ocs']['meta']['statuscode'] === 200 + ) + ) { + return true; + } elseif ($try === 0) { + // only add new job on first try + $this->jobList->add('OCA\FederatedFileSharing\BackgroundJob\RetryJob', + [ + 'remote' => $remote, + 'remoteId' => $remoteId, + 'token' => $token, + 'action' => $action, + 'data' => json_encode($data), + 'try' => $try, + 'lastRun' => $this->getTimestamp() + ] + ); + } + + return false; + } + + + /** + * return current timestamp + * + * @return int + */ + protected function getTimestamp() { + return time(); + } + + /** + * try http post with the given protocol, if no protocol is given we pick + * the secure one (https) + * + * @param string $remoteDomain + * @param string $urlSuffix + * @param array $fields post parameters + * @param string $action define the action (possible values: share, reshare, accept, decline, unshare, revoke, permissions) + * @return array + * @throws \Exception + */ + protected function tryHttpPostToShareEndpoint($remoteDomain, $urlSuffix, array $fields, $action = 'share') { + if ($this->addressHandler->urlContainProtocol($remoteDomain) === false) { + $remoteDomain = 'https://' . $remoteDomain; + } + + $result = [ + 'success' => false, + 'result' => '', + ]; + + // if possible we use the new OCM API + $ocmResult = $this->tryOCMEndPoint($remoteDomain, $fields, $action); + if (is_array($ocmResult)) { + $result['success'] = true; + $result['result'] = json_encode([ + 'ocs' => ['meta' => ['statuscode' => 200]]]); + return $result; + } + + return $this->tryLegacyEndPoint($remoteDomain, $urlSuffix, $fields); + } + + /** + * try old federated sharing API if the OCM api doesn't work + * + * @param $remoteDomain + * @param $urlSuffix + * @param array $fields + * @return mixed + * @throws \Exception + */ + protected function tryLegacyEndPoint($remoteDomain, $urlSuffix, array $fields) { + $result = [ + 'success' => false, + 'result' => '', + ]; + + // Fall back to old API + $client = $this->httpClientService->newClient(); + $federationEndpoints = $this->discoveryService->discover($remoteDomain, 'FEDERATED_SHARING'); + $endpoint = $federationEndpoints['share'] ?? '/ocs/v2.php/cloud/shares'; + try { + $response = $client->post($remoteDomain . $endpoint . $urlSuffix . '?format=' . self::RESPONSE_FORMAT, [ + 'body' => $fields, + 'timeout' => 10, + 'connect_timeout' => 10, + ]); + $result['result'] = $response->getBody(); + $result['success'] = true; + } catch (\Exception $e) { + // if flat re-sharing is not supported by the remote server + // we re-throw the exception and fall back to the old behaviour. + // (flat re-shares has been introduced in Nextcloud 9.1) + if ($e->getCode() === Http::STATUS_INTERNAL_SERVER_ERROR) { + throw $e; + } + } + + return $result; + } + + /** + * send action regarding federated sharing to the remote server using the OCM API + * + * @param $remoteDomain + * @param $fields + * @param $action + * + * @return array|false + */ + protected function tryOCMEndPoint($remoteDomain, $fields, $action) { + switch ($action) { + case 'share': + $share = $this->cloudFederationFactory->getCloudFederationShare( + $fields['shareWith'] . '@' . $remoteDomain, + $fields['name'], + '', + $fields['remoteId'], + $fields['ownerFederatedId'], + $fields['owner'], + $fields['sharedByFederatedId'], + $fields['sharedBy'], + $fields['token'], + $fields['shareType'], + 'file' + ); + return $this->federationProviderManager->sendShare($share); + case 'reshare': + // ask owner to reshare a file + $notification = $this->cloudFederationFactory->getCloudFederationNotification(); + $notification->setMessage('REQUEST_RESHARE', + 'file', + $fields['remoteId'], + [ + 'sharedSecret' => $fields['token'], + 'shareWith' => $fields['shareWith'], + 'senderId' => $fields['localId'], + 'shareType' => $fields['shareType'], + 'message' => 'Ask owner to reshare the file' + ] + ); + return $this->federationProviderManager->sendNotification($remoteDomain, $notification); + case 'unshare': + //owner unshares the file from the recipient again + $notification = $this->cloudFederationFactory->getCloudFederationNotification(); + $notification->setMessage('SHARE_UNSHARED', + 'file', + $fields['remoteId'], + [ + 'sharedSecret' => $fields['token'], + 'message' => 'file is no longer shared with you' + ] + ); + return $this->federationProviderManager->sendNotification($remoteDomain, $notification); + case 'reshare_undo': + // if a reshare was unshared we send the information to the initiator/owner + $notification = $this->cloudFederationFactory->getCloudFederationNotification(); + $notification->setMessage('RESHARE_UNDO', + 'file', + $fields['remoteId'], + [ + 'sharedSecret' => $fields['token'], + 'message' => 'reshare was revoked' + ] + ); + return $this->federationProviderManager->sendNotification($remoteDomain, $notification); + } + + return false; + } +} diff --git a/apps/federatedfilesharing/lib/Notifier.php b/apps/federatedfilesharing/lib/Notifier.php new file mode 100644 index 00000000000..10b57c578a2 --- /dev/null +++ b/apps/federatedfilesharing/lib/Notifier.php @@ -0,0 +1,246 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\FederatedFileSharing; + +use OCP\Contacts\IManager; +use OCP\Federation\ICloudId; +use OCP\Federation\ICloudIdManager; +use OCP\HintException; +use OCP\IURLGenerator; +use OCP\L10N\IFactory; +use OCP\Notification\INotification; +use OCP\Notification\INotifier; +use OCP\Notification\UnknownNotificationException; + +class Notifier implements INotifier { + /** @var array */ + protected $federatedContacts; + + /** + * @param IFactory $factory + * @param IManager $contactsManager + * @param IURLGenerator $url + * @param ICloudIdManager $cloudIdManager + */ + public function __construct( + protected IFactory $factory, + protected IManager $contactsManager, + protected IURLGenerator $url, + protected ICloudIdManager $cloudIdManager, + ) { + } + + /** + * Identifier of the notifier, only use [a-z0-9_] + * + * @return string + * @since 17.0.0 + */ + public function getID(): string { + return 'federatedfilesharing'; + } + + /** + * Human readable name describing the notifier + * + * @return string + * @since 17.0.0 + */ + public function getName(): string { + return $this->factory->get('federatedfilesharing')->t('Federated sharing'); + } + + /** + * @param INotification $notification + * @param string $languageCode The code of the language that should be used to prepare the notification + * @return INotification + * @throws UnknownNotificationException + */ + public function prepare(INotification $notification, string $languageCode): INotification { + if ($notification->getApp() !== 'files_sharing' || $notification->getObjectType() !== 'remote_share') { + // Not my app => throw + throw new UnknownNotificationException(); + } + + // Read the language from the notification + $l = $this->factory->get('federatedfilesharing', $languageCode); + + switch ($notification->getSubject()) { + // Deal with known subjects + case 'remote_share': + $notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg'))); + + $params = $notification->getSubjectParameters(); + $displayName = (count($params) > 3) ? $params[3] : ''; + if ($params[0] !== $params[1] && $params[1] !== null) { + $remoteInitiator = $this->createRemoteUser($params[0], $displayName); + $remoteOwner = $this->createRemoteUser($params[1]); + $params[3] = $remoteInitiator['name'] . '@' . $remoteInitiator['server']; + $params[4] = $remoteOwner['name'] . '@' . $remoteOwner['server']; + + $notification->setRichSubject( + $l->t('You received {share} as a remote share from {user} (on behalf of {behalf})'), + [ + 'share' => [ + 'type' => 'pending-federated-share', + 'id' => $notification->getObjectId(), + 'name' => $params[2], + ], + 'user' => $remoteInitiator, + 'behalf' => $remoteOwner, + ] + ); + } else { + $remoteOwner = $this->createRemoteUser($params[0], $displayName); + $params[3] = $remoteOwner['name'] . '@' . $remoteOwner['server']; + + $notification->setRichSubject( + $l->t('You received {share} as a remote share from {user}'), + [ + 'share' => [ + 'type' => 'pending-federated-share', + 'id' => $notification->getObjectId(), + 'name' => $params[2], + ], + 'user' => $remoteOwner, + ] + ); + } + + // Deal with the actions for a known subject + foreach ($notification->getActions() as $action) { + switch ($action->getLabel()) { + case 'accept': + $action->setParsedLabel( + $l->t('Accept') + ) + ->setPrimary(true); + break; + + case 'decline': + $action->setParsedLabel( + $l->t('Decline') + ); + break; + } + + $notification->addParsedAction($action); + } + return $notification; + + default: + // Unknown subject => Unknown notification => throw + throw new UnknownNotificationException(); + } + } + + /** + * @param string $cloudId + * @param string $displayName - overwrite display name + * + * @return array + */ + protected function createRemoteUser(string $cloudId, string $displayName = '') { + try { + $resolvedId = $this->cloudIdManager->resolveCloudId($cloudId); + if ($displayName === '') { + $displayName = $this->getDisplayName($resolvedId); + } + $user = $resolvedId->getUser(); + $server = $resolvedId->getRemote(); + } catch (HintException $e) { + $user = $cloudId; + $displayName = ($displayName !== '') ? $displayName : $cloudId; + $server = ''; + } + + return [ + 'type' => 'user', + 'id' => $user, + 'name' => $displayName, + 'server' => $server, + ]; + } + + /** + * Try to find the user in the contacts + * + * @param ICloudId $cloudId + * @return string + */ + protected function getDisplayName(ICloudId $cloudId): string { + $server = $cloudId->getRemote(); + $user = $cloudId->getUser(); + if (str_starts_with($server, 'http://')) { + $server = substr($server, strlen('http://')); + } elseif (str_starts_with($server, 'https://')) { + $server = substr($server, strlen('https://')); + } + + try { + // contains protocol in the ID + return $this->getDisplayNameFromContact($cloudId->getId()); + } catch (\OutOfBoundsException $e) { + } + + try { + // does not include protocol, as stored in addressbooks + return $this->getDisplayNameFromContact($cloudId->getDisplayId()); + } catch (\OutOfBoundsException $e) { + } + + try { + return $this->getDisplayNameFromContact($user . '@http://' . $server); + } catch (\OutOfBoundsException $e) { + } + + try { + return $this->getDisplayNameFromContact($user . '@https://' . $server); + } catch (\OutOfBoundsException $e) { + } + + return $cloudId->getId(); + } + + /** + * Try to find the user in the contacts + * + * @param string $federatedCloudId + * @return string + * @throws \OutOfBoundsException when there is no contact for the id + */ + protected function getDisplayNameFromContact($federatedCloudId) { + if (isset($this->federatedContacts[$federatedCloudId])) { + if ($this->federatedContacts[$federatedCloudId] !== '') { + return $this->federatedContacts[$federatedCloudId]; + } else { + throw new \OutOfBoundsException('No contact found for federated cloud id'); + } + } + + $addressBookEntries = $this->contactsManager->search($federatedCloudId, ['CLOUD'], [ + 'limit' => 1, + 'enumeration' => false, + 'fullmatch' => false, + 'strict_search' => true, + ]); + foreach ($addressBookEntries as $entry) { + if (isset($entry['CLOUD'])) { + foreach ($entry['CLOUD'] as $cloudID) { + if ($cloudID === $federatedCloudId) { + $this->federatedContacts[$federatedCloudId] = $entry['FN']; + return $entry['FN']; + } + } + } + } + + $this->federatedContacts[$federatedCloudId] = ''; + throw new \OutOfBoundsException('No contact found for federated cloud id'); + } +} diff --git a/apps/federatedfilesharing/lib/OCM/CloudFederationProviderFiles.php b/apps/federatedfilesharing/lib/OCM/CloudFederationProviderFiles.php new file mode 100644 index 00000000000..1ce639532e8 --- /dev/null +++ b/apps/federatedfilesharing/lib/OCM/CloudFederationProviderFiles.php @@ -0,0 +1,814 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\FederatedFileSharing\OCM; + +use NCU\Federation\ISignedCloudFederationProvider; +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\Manager; +use OCA\GlobalSiteSelector\Service\SlaveService; +use OCP\Activity\IManager as IActivityManager; +use OCP\App\IAppManager; +use OCP\AppFramework\QueryException; +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\Files\IFilenameValidator; +use OCP\Files\NotFoundException; +use OCP\HintException; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IURLGenerator; +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 Psr\Log\LoggerInterface; +use SensitiveParameter; + +class CloudFederationProviderFiles implements ISignedCloudFederationProvider { + /** + * CloudFederationProvider constructor. + */ + public function __construct( + private IAppManager $appManager, + private FederatedShareProvider $federatedShareProvider, + private AddressHandler $addressHandler, + private IUserManager $userManager, + private IManager $shareManager, + private ICloudIdManager $cloudIdManager, + private IActivityManager $activityManager, + private INotificationManager $notificationManager, + private IURLGenerator $urlGenerator, + private ICloudFederationFactory $cloudFederationFactory, + private ICloudFederationProviderManager $cloudFederationProviderManager, + private IDBConnection $connection, + private IGroupManager $groupManager, + private IConfig $config, + private Manager $externalShareManager, + private LoggerInterface $logger, + private IFilenameValidator $filenameValidator, + private readonly IProviderFactory $shareProviderFactory, + ) { + } + + /** + * @return string + */ + public function getShareType() { + return 'file'; + } + + /** + * share received from another server + * + * @param ICloudFederationShare $share + * @return string provider specific unique ID of the share + * + * @throws ProviderCouldNotAddShareException + * @throws QueryException + * @throws HintException + * @since 14.0.0 + */ + public function shareReceived(ICloudFederationShare $share) { + 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); + } + + [$ownerUid, $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(); + $sharedBy = $share->getSharedByDisplayName(); + $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 send + // we assume that the share comes from the owner + if ($sharedByFederatedId === null) { + $sharedBy = $owner; + $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); + } + + // FIXME this should be a method in the user management instead + 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']); + + if (!$this->userManager->userExists($shareWith)) { + throw new ProviderCouldNotAddShareException('User does not exists', '', Http::STATUS_BAD_REQUEST); + } + + \OC_Util::setupFS($shareWith); + } + + if ($shareType === IShare::TYPE_GROUP && !$this->groupManager->groupExists($shareWith)) { + throw new ProviderCouldNotAddShareException('Group does not exists', '', Http::STATUS_BAD_REQUEST); + } + + try { + $this->externalShareManager->addShare($remote, $token, '', $name, $owner, $shareType, false, $shareWith, $remoteId); + $shareId = Server::get(IDBConnection::class)->lastInsertId('*PREFIX*share_external'); + + // 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', $shareId, $name); + Server::get(IActivityManager::class)->publish($event); + $this->notifyAboutNewShare($shareWith, $shareId, $ownerFederatedId, $sharedByFederatedId, $name, $ownerDisplayName); + + // If auto-accept is enabled, accept the share + if ($this->federatedShareProvider->isFederatedTrustedShareAutoAccept() && $trustedServers?->isTrustedServer($remote) === true) { + $this->externalShareManager->acceptShare($shareId, $shareWith); + } + } else { + $groupMembers = $this->groupManager->get($shareWith)->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', $shareId, $name); + Server::get(IActivityManager::class)->publish($event); + $this->notifyAboutNewShare($user->getUID(), $shareId, $ownerFederatedId, $sharedByFederatedId, $name, $ownerDisplayName); + + // If auto-accept is enabled, accept the share + if ($this->federatedShareProvider->isFederatedTrustedShareAutoAccept() && $trustedServers?->isTrustedServer($remote) === true) { + $this->externalShareManager->acceptShare($shareId, $user->getUID()); + } + } + } + + return $shareId; + } 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); + } + + /** + * notification received from another server + * + * @param string $notificationType (e.g. SHARE_ACCEPTED) + * @param string $providerId id of the share + * @param array $notification payload of the notification + * @return array<string> data send back to the sender + * + * @throws ActionNotSupportedException + * @throws AuthenticationFailedException + * @throws BadRequestException + * @throws HintException + * @since 14.0.0 + */ + public function notificationReceived($notificationType, $providerId, array $notification) { + switch ($notificationType) { + case 'SHARE_ACCEPTED': + return $this->shareAccepted($providerId, $notification); + case 'SHARE_DECLINED': + return $this->shareDeclined($providerId, $notification); + case 'SHARE_UNSHARED': + return $this->unshare($providerId, $notification); + case 'REQUEST_RESHARE': + return $this->reshareRequested($providerId, $notification); + case 'RESHARE_UNDO': + return $this->undoReshare($providerId, $notification); + case 'RESHARE_CHANGE_PERMISSION': + return $this->updateResharePermissions($providerId, $notification); + } + + + throw new BadRequestException([$notificationType]); + } + + /** + * map OCM share type (strings) to Nextcloud internal share types (integer) + * + * @param string $shareType + * @return int + */ + private function mapShareTypeToNextcloud($shareType) { + $result = IShare::TYPE_USER; + if ($shareType === 'group') { + $result = IShare::TYPE_GROUP; + } + + return $result; + } + + private function notifyAboutNewShare($shareWith, $shareId, $ownerFederatedId, $sharedByFederatedId, $name, $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 string $id + * @param array $notification + * @return array<string> + * @throws ActionNotSupportedException + * @throws AuthenticationFailedException + * @throws BadRequestException + * @throws HintException + */ + private function shareAccepted($id, array $notification) { + 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 []; + } + + /** + * @param IShare $share + * @throws ShareNotFound + */ + protected function executeAcceptShare(IShare $share) { + try { + $fileId = (int)$share->getNode()->getId(); + [$file, $link] = $this->getFile($this->getCorrectUid($share), $fileId); + } catch (\Exception $e) { + throw new ShareNotFound(); + } + + $event = $this->activityManager->generateEvent(); + $event->setApp('files_sharing') + ->setType('remote_share') + ->setAffectedUser($this->getCorrectUid($share)) + ->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 string $id + * @param array $notification + * @return array<string> + * @throws ActionNotSupportedException + * @throws AuthenticationFailedException + * @throws BadRequestException + * @throws ShareNotFound + * @throws HintException + * + */ + protected function shareDeclined($id, array $notification) { + 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 + * + * @param IShare $share + * @throws ShareNotFound + */ + protected function executeDeclineShare(IShare $share) { + $this->federatedShareProvider->removeShareFromTable($share); + + try { + $fileId = (int)$share->getNode()->getId(); + [$file, $link] = $this->getFile($this->getCorrectUid($share), $fileId); + } catch (\Exception $e) { + throw new ShareNotFound(); + } + + $event = $this->activityManager->generateEvent(); + $event->setApp('files_sharing') + ->setType('remote_share') + ->setAffectedUser($this->getCorrectUid($share)) + ->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 string $id + * @param array $notification + * @return array<string> + * @throws AuthenticationFailedException + * @throws BadRequestException + */ + private function undoReshare($id, array $notification) { + if (!isset($notification['sharedSecret'])) { + throw new BadRequestException(['sharedSecret']); + } + $token = $notification['sharedSecret']; + + $share = $this->federatedShareProvider->getShareById($id); + + $this->verifyShare($share, $token); + $this->federatedShareProvider->removeShareFromTable($share); + return []; + } + + /** + * unshare file from self + * + * @param string $id + * @param array $notification + * @return array<string> + * @throws ActionNotSupportedException + * @throws BadRequestException + */ + private function unshare($id, array $notification) { + if (!$this->isS2SEnabled(true)) { + throw new ActionNotSupportedException('incoming shares disabled!'); + } + + if (!isset($notification['sharedSecret'])) { + throw new BadRequestException(['sharedSecret']); + } + $token = $notification['sharedSecret']; + + $qb = $this->connection->getQueryBuilder(); + $qb->select('*') + ->from('share_external') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('remote_id', $qb->createNamedParameter($id)), + $qb->expr()->eq('share_token', $qb->createNamedParameter($token)) + ) + ); + + $result = $qb->executeQuery(); + $share = $result->fetch(); + $result->closeCursor(); + + if ($token && $id && !empty($share)) { + $remote = $this->cleanupRemote($share['remote']); + + $owner = $this->cloudIdManager->getCloudId($share['owner'], $remote); + $mountpoint = $share['mountpoint']; + $user = $share['user']; + + $qb = $this->connection->getQueryBuilder(); + $qb->delete('share_external') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('remote_id', $qb->createNamedParameter($id)), + $qb->expr()->eq('share_token', $qb->createNamedParameter($token)) + ) + ); + + $qb->executeStatement(); + + // delete all child in case of a group share + $qb = $this->connection->getQueryBuilder(); + $qb->delete('share_external') + ->where($qb->expr()->eq('parent', $qb->createNamedParameter((int)$share['id']))); + $qb->executeStatement(); + + $ownerDisplayName = $this->getUserDisplayName($owner->getId()); + + if ((int)$share['share_type'] === IShare::TYPE_USER) { + if ($share['accepted']) { + $path = trim($mountpoint, '/'); + } else { + $path = trim($share['name'], '/'); + } + $notification = $this->notificationManager->createNotification(); + $notification->setApp('files_sharing') + ->setUser($share['user']) + ->setObject('remote_share', (string)$share['id']); + $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', (int)$share['id'], $path); + Server::get(IActivityManager::class)->publish($event); + } + } + + return []; + } + + private function cleanupRemote($remote) { + $remote = substr($remote, strpos($remote, '://') + 3); + + return rtrim($remote, '/'); + } + + /** + * recipient of a share request to re-share the file with another user + * + * @param string $id + * @param array $notification + * @return array<string> + * @throws AuthenticationFailedException + * @throws BadRequestException + * @throws ProviderCouldNotAddShareException + * @throws ShareNotFound + */ + protected function reshareRequested($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((int)$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 + * + * @param string $id + * @param array $notification + * @return array<string> + * @throws AuthenticationFailedException + * @throws BadRequestException + */ + protected function updateResharePermissions($id, array $notification) { + throw new HintException('Updating reshares not allowed'); + } + + /** + * translate OCM Permissions to Nextcloud permissions + * + * @param array $ocmPermissions + * @return int + * @throws BadRequestException + */ + protected function ocmPermissions2ncPermissions(array $ocmPermissions) { + $ncPermissions = 0; + foreach ($ocmPermissions as $permission) { + switch (strtolower($permission)) { + case 'read': + $ncPermissions += Constants::PERMISSION_READ; + break; + case 'write': + $ncPermissions += Constants::PERMISSION_CREATE + Constants::PERMISSION_UPDATE; + break; + case 'share': + $ncPermissions += Constants::PERMISSION_SHARE; + break; + default: + throw new BadRequestException(['permission']); + } + } + + return $ncPermissions; + } + + /** + * update permissions in database + * + * @param IShare $share + * @param int $permissions + */ + protected function updatePermissionsInDatabase(IShare $share, $permissions) { + $query = $this->connection->getQueryBuilder(); + $query->update('share') + ->where($query->expr()->eq('id', $query->createNamedParameter($share->getId()))) + ->set('permissions', $query->createNamedParameter($permissions)) + ->executeStatement(); + } + + + /** + * get file + * + * @param string $user + * @param int $fileSource + * @return array with internal path of the file and a absolute link to it + */ + private function getFile($user, $fileSource) { + \OC_Util::setupFS($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 + * + * @param IShare $share + * @return string + */ + protected function getCorrectUid(IShare $share) { + if ($this->userManager->userExists($share->getShareOwner())) { + return $share->getShareOwner(); + } + + return $share->getSharedBy(); + } + + + + /** + * check if we got the right share + * + * @param IShare $share + * @param string $token + * @return bool + * @throws AuthenticationFailedException + */ + protected function verifyShare(IShare $share, $token) { + 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 + * + * @param bool $incoming + * @return bool + */ + private function isS2SEnabled($incoming = false) { + $result = $this->appManager->isEnabledForUser('files_sharing'); + + if ($incoming) { + $result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled(); + } else { + $result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled(); + } + + return $result; + } + + + /** + * get the supported share types, e.g. "user", "group", etc. + * + * @return array + * + * @since 14.0.0 + */ + public function getSupportedShareTypes() { + 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) { + Server::get(LoggerInterface::class)->error( + $e->getMessage(), + ['exception' => $e] + ); + return ''; + } + + return $slaveService->getUserDisplayName($this->cloudIdManager->removeProtocolFromUrl($userId), false); + } + + /** + * @inheritDoc + * + * @param string $sharedSecret + * @param array $payload + * @return string + */ + 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['user'] . '@' . $share['remote']; + } + + // 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(); + } + } +} diff --git a/apps/federatedfilesharing/lib/Settings/Admin.php b/apps/federatedfilesharing/lib/Settings/Admin.php new file mode 100644 index 00000000000..fc685f952c7 --- /dev/null +++ b/apps/federatedfilesharing/lib/Settings/Admin.php @@ -0,0 +1,85 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\FederatedFileSharing\Settings; + +use OCA\FederatedFileSharing\FederatedShareProvider; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\GlobalScale\IConfig; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\Settings\IDelegatedSettings; + +class Admin implements IDelegatedSettings { + /** + * Admin constructor. + */ + public function __construct( + private FederatedShareProvider $fedShareProvider, + private IConfig $gsConfig, + private IL10N $l, + private IURLGenerator $urlGenerator, + private IInitialState $initialState, + ) { + } + + /** + * @return TemplateResponse + */ + public function getForm() { + + $this->initialState->provideInitialState('internalOnly', $this->gsConfig->onlyInternalFederation()); + $this->initialState->provideInitialState('sharingFederatedDocUrl', $this->urlGenerator->linkToDocs('admin-sharing-federated')); + $this->initialState->provideInitialState('outgoingServer2serverShareEnabled', $this->fedShareProvider->isOutgoingServer2serverShareEnabled()); + $this->initialState->provideInitialState('incomingServer2serverShareEnabled', $this->fedShareProvider->isIncomingServer2serverShareEnabled()); + $this->initialState->provideInitialState('federatedGroupSharingSupported', $this->fedShareProvider->isFederatedGroupSharingSupported()); + $this->initialState->provideInitialState('outgoingServer2serverGroupShareEnabled', $this->fedShareProvider->isOutgoingServer2serverGroupShareEnabled()); + $this->initialState->provideInitialState('incomingServer2serverGroupShareEnabled', $this->fedShareProvider->isIncomingServer2serverGroupShareEnabled()); + $this->initialState->provideInitialState('lookupServerEnabled', $this->fedShareProvider->isLookupServerQueriesEnabled()); + $this->initialState->provideInitialState('lookupServerUploadEnabled', $this->fedShareProvider->isLookupServerUploadEnabled()); + $this->initialState->provideInitialState('federatedTrustedShareAutoAccept', $this->fedShareProvider->isFederatedTrustedShareAutoAccept()); + + return new TemplateResponse('federatedfilesharing', 'settings-admin', [], ''); + } + + /** + * @return string the section ID, e.g. 'sharing' + */ + public function getSection() { + return 'sharing'; + } + + /** + * @return int whether the form should be rather on the top or bottom of + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * + * E.g.: 70 + */ + public function getPriority() { + return 20; + } + + public function getName(): ?string { + return $this->l->t('Federated Cloud Sharing'); + } + + public function getAuthorizedAppConfig(): array { + return [ + 'files_sharing' => [ + 'outgoing_server2server_share_enabled', + 'incoming_server2server_share_enabled', + 'federatedGroupSharingSupported', + 'outgoingServer2serverGroupShareEnabled', + 'incomingServer2serverGroupShareEnabled', + 'lookupServerEnabled', + 'lookupServerUploadEnabled', + 'federatedTrustedShareAutoAccept', + ], + ]; + } +} diff --git a/apps/federatedfilesharing/lib/Settings/Personal.php b/apps/federatedfilesharing/lib/Settings/Personal.php new file mode 100644 index 00000000000..2889fb77c1f --- /dev/null +++ b/apps/federatedfilesharing/lib/Settings/Personal.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\FederatedFileSharing\Settings; + +use OCA\FederatedFileSharing\FederatedShareProvider; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\Defaults; +use OCP\IURLGenerator; +use OCP\IUserSession; +use OCP\Settings\ISettings; + +class Personal implements ISettings { + public function __construct( + private FederatedShareProvider $federatedShareProvider, + private IUserSession $userSession, + private Defaults $defaults, + private IInitialState $initialState, + private IURLGenerator $urlGenerator, + ) { + } + + /** + * @return TemplateResponse returns the instance with all parameters set, ready to be rendered + * @since 9.1 + */ + public function getForm(): TemplateResponse { + $cloudID = $this->userSession->getUser()->getCloudId(); + $url = 'https://nextcloud.com/sharing#' . $cloudID; + + $this->initialState->provideInitialState('color', $this->defaults->getDefaultColorPrimary()); + $this->initialState->provideInitialState('textColor', $this->defaults->getDefaultTextColorPrimary()); + $this->initialState->provideInitialState('logoPath', $this->defaults->getLogo()); + $this->initialState->provideInitialState('reference', $url); + $this->initialState->provideInitialState('cloudId', $cloudID); + $this->initialState->provideInitialState('docUrlFederated', $this->urlGenerator->linkToDocs('user-sharing-federated')); + + return new TemplateResponse('federatedfilesharing', 'settings-personal', [], TemplateResponse::RENDER_AS_BLANK); + } + + /** + * @return string the section ID, e.g. 'sharing' + * @since 9.1 + */ + public function getSection(): ?string { + if ($this->federatedShareProvider->isIncomingServer2serverShareEnabled() + || $this->federatedShareProvider->isIncomingServer2serverGroupShareEnabled()) { + return 'sharing'; + } + return null; + } + + /** + * @return int whether the form should be rather on the top or bottom of + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * + * E.g.: 70 + * @since 9.1 + */ + public function getPriority(): int { + return 40; + } +} diff --git a/apps/federatedfilesharing/lib/Settings/PersonalSection.php b/apps/federatedfilesharing/lib/Settings/PersonalSection.php new file mode 100644 index 00000000000..eea10e39393 --- /dev/null +++ b/apps/federatedfilesharing/lib/Settings/PersonalSection.php @@ -0,0 +1,64 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\FederatedFileSharing\Settings; + +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\Settings\IIconSection; + +class PersonalSection implements IIconSection { + public function __construct( + private IURLGenerator $urlGenerator, + private IL10N $l, + ) { + } + + /** + * returns the relative path to an 16*16 icon describing the section. + * e.g. '/core/img/places/files.svg' + * + * @returns string + * @since 13.0.0 + */ + public function getIcon() { + return $this->urlGenerator->imagePath('core', 'actions/share.svg'); + } + + /** + * returns the ID of the section. It is supposed to be a lower case string, + * e.g. 'ldap' + * + * @returns string + * @since 9.1 + */ + public function getID() { + return 'sharing'; + } + + /** + * returns the translated name as it should be displayed, e.g. 'LDAP / AD + * integration'. Use the L10N service to translate it. + * + * @return string + * @since 9.1 + */ + public function getName() { + return $this->l->t('Sharing'); + } + + /** + * @return int whether the form should be rather on the top or bottom of + * the settings navigation. The sections are arranged in ascending order of + * the priority values. It is required to return a value between 0 and 99. + * + * E.g.: 70 + * @since 9.1 + */ + public function getPriority() { + return 15; + } +} diff --git a/apps/federatedfilesharing/lib/TokenHandler.php b/apps/federatedfilesharing/lib/TokenHandler.php new file mode 100644 index 00000000000..0151d12f5d9 --- /dev/null +++ b/apps/federatedfilesharing/lib/TokenHandler.php @@ -0,0 +1,41 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\FederatedFileSharing; + +use OCP\Security\ISecureRandom; + +/** + * Class TokenHandler + * + * @package OCA\FederatedFileSharing + */ +class TokenHandler { + public const TOKEN_LENGTH = 15; + + /** + * TokenHandler constructor. + * + * @param ISecureRandom $secureRandom + */ + public function __construct( + private ISecureRandom $secureRandom, + ) { + } + + /** + * generate to token used to authenticate federated shares + * + * @return string + */ + public function generateToken() { + $token = $this->secureRandom->generate( + self::TOKEN_LENGTH, + ISecureRandom::CHAR_ALPHANUMERIC); + return $token; + } +} diff --git a/apps/federatedfilesharing/lib/addresshandler.php b/apps/federatedfilesharing/lib/addresshandler.php deleted file mode 100644 index 92768f11b95..00000000000 --- a/apps/federatedfilesharing/lib/addresshandler.php +++ /dev/null @@ -1,184 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\FederatedFileSharing; -use OC\HintException; -use OCP\IL10N; -use OCP\IURLGenerator; - -/** - * Class AddressHandler - parse, modify and construct federated sharing addresses - * - * @package OCA\FederatedFileSharing - */ -class AddressHandler { - - /** @var IL10N */ - private $l; - - /** @var IURLGenerator */ - private $urlGenerator; - - /** - * AddressHandler constructor. - * - * @param IURLGenerator $urlGenerator - * @param IL10N $il10n - */ - public function __construct( - IURLGenerator $urlGenerator, - IL10N $il10n - ) { - $this->l = $il10n; - $this->urlGenerator = $urlGenerator; - } - - /** - * split user and remote from federated cloud id - * - * @param string $address federated share address - * @return array [user, remoteURL] - * @throws HintException - */ - public function splitUserRemote($address) { - if (strpos($address, '@') === false) { - $hint = $this->l->t('Invalid Federated Cloud ID'); - throw new HintException('Invalid Federated Cloud ID', $hint); - } - - // Find the first character that is not allowed in user names - $id = str_replace('\\', '/', $address); - $posSlash = strpos($id, '/'); - $posColon = strpos($id, ':'); - - if ($posSlash === false && $posColon === false) { - $invalidPos = strlen($id); - } else if ($posSlash === false) { - $invalidPos = $posColon; - } else if ($posColon === false) { - $invalidPos = $posSlash; - } else { - $invalidPos = min($posSlash, $posColon); - } - - // Find the last @ before $invalidPos - $pos = $lastAtPos = 0; - while ($lastAtPos !== false && $lastAtPos <= $invalidPos) { - $pos = $lastAtPos; - $lastAtPos = strpos($id, '@', $pos + 1); - } - - if ($pos !== false) { - $user = substr($id, 0, $pos); - $remote = substr($id, $pos + 1); - $remote = $this->fixRemoteURL($remote); - if (!empty($user) && !empty($remote)) { - return array($user, $remote); - } - } - - $hint = $this->l->t('Invalid Federated Cloud ID'); - throw new HintException('Invalid Federated Cloud ID', $hint); - } - - /** - * generate remote URL part of federated ID - * - * @return string url of the current server - */ - public function generateRemoteURL() { - $url = $this->urlGenerator->getAbsoluteURL('/'); - return $url; - } - - /** - * check if two federated cloud IDs refer to the same user - * - * @param string $user1 - * @param string $server1 - * @param string $user2 - * @param string $server2 - * @return bool true if both users and servers are the same - */ - public function compareAddresses($user1, $server1, $user2, $server2) { - $normalizedServer1 = strtolower($this->removeProtocolFromUrl($server1)); - $normalizedServer2 = strtolower($this->removeProtocolFromUrl($server2)); - - if (rtrim($normalizedServer1, '/') === rtrim($normalizedServer2, '/')) { - // FIXME this should be a method in the user management instead - \OCP\Util::emitHook( - '\OCA\Files_Sharing\API\Server2Server', - 'preLoginNameUsedAsUserName', - array('uid' => &$user1) - ); - \OCP\Util::emitHook( - '\OCA\Files_Sharing\API\Server2Server', - 'preLoginNameUsedAsUserName', - array('uid' => &$user2) - ); - - if ($user1 === $user2) { - return true; - } - } - - return false; - } - - /** - * remove protocol from URL - * - * @param string $url - * @return string - */ - public function removeProtocolFromUrl($url) { - if (strpos($url, 'https://') === 0) { - return substr($url, strlen('https://')); - } else if (strpos($url, 'http://') === 0) { - return substr($url, strlen('http://')); - } - - return $url; - } - - /** - * Strips away a potential file names and trailing slashes: - * - http://localhost - * - http://localhost/ - * - http://localhost/index.php - * - http://localhost/index.php/s/{shareToken} - * - * all return: http://localhost - * - * @param string $remote - * @return string - */ - protected function fixRemoteURL($remote) { - $remote = str_replace('\\', '/', $remote); - if ($fileNamePosition = strpos($remote, '/index.php')) { - $remote = substr($remote, 0, $fileNamePosition); - } - $remote = rtrim($remote, '/'); - - return $remote; - } - -} diff --git a/apps/federatedfilesharing/lib/discoverymanager.php b/apps/federatedfilesharing/lib/discoverymanager.php deleted file mode 100644 index 51ea71195fa..00000000000 --- a/apps/federatedfilesharing/lib/discoverymanager.php +++ /dev/null @@ -1,136 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\FederatedFileSharing; - -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\ConnectException; -use OCP\Http\Client\IClient; -use OCP\Http\Client\IClientService; -use OCP\ICache; -use OCP\ICacheFactory; - -/** - * Class DiscoveryManager handles the discovery of endpoints used by Federated - * Cloud Sharing. - * - * @package OCA\FederatedFileSharing - */ -class DiscoveryManager { - /** @var ICache */ - private $cache; - /** @var IClient */ - private $client; - - /** - * @param ICacheFactory $cacheFactory - * @param IClientService $clientService - */ - public function __construct(ICacheFactory $cacheFactory, - IClientService $clientService) { - $this->cache = $cacheFactory->create('ocs-discovery'); - $this->client = $clientService->newClient(); - } - - /** - * Returns whether the specified URL includes only safe characters, if not - * returns false - * - * @param string $url - * @return bool - */ - private function isSafeUrl($url) { - return (bool)preg_match('/^[\/\.A-Za-z0-9]+$/', $url); - } - - /** - * Discover the actual data and do some naive caching to ensure that the data - * is not requested multiple times. - * - * If no valid discovery data is found the ownCloud defaults are returned. - * - * @param string $remote - * @return array - */ - private function discover($remote) { - // Check if something is in the cache - if($cacheData = $this->cache->get($remote)) { - return json_decode($cacheData, true); - } - - // Default response body - $discoveredServices = [ - 'webdav' => '/public.php/webdav', - 'share' => '/ocs/v1.php/cloud/shares', - ]; - - // Read the data from the response body - try { - $response = $this->client->get($remote . '/ocs-provider/'); - if($response->getStatusCode() === 200) { - $decodedService = json_decode($response->getBody(), true); - if(is_array($decodedService)) { - $endpoints = [ - 'webdav', - 'share', - ]; - - foreach($endpoints as $endpoint) { - if(isset($decodedService['services']['FEDERATED_SHARING']['endpoints'][$endpoint])) { - $endpointUrl = (string)$decodedService['services']['FEDERATED_SHARING']['endpoints'][$endpoint]; - if($this->isSafeUrl($endpointUrl)) { - $discoveredServices[$endpoint] = $endpointUrl; - } - } - } - } - } - } catch (ClientException $e) { - // Don't throw any exception since exceptions are handled before - } catch (ConnectException $e) { - // Don't throw any exception since exceptions are handled before - } - - // Write into cache - $this->cache->set($remote, json_encode($discoveredServices)); - return $discoveredServices; - } - - /** - * Return the public WebDAV endpoint used by the specified remote - * - * @param string $host - * @return string - */ - public function getWebDavEndpoint($host) { - return $this->discover($host)['webdav']; - } - - /** - * Return the sharing endpoint used by the specified remote - * - * @param string $host - * @return string - */ - public function getShareEndpoint($host) { - return $this->discover($host)['share']; - } -} diff --git a/apps/federatedfilesharing/lib/federatedshareprovider.php b/apps/federatedfilesharing/lib/federatedshareprovider.php deleted file mode 100644 index e54ce08fb04..00000000000 --- a/apps/federatedfilesharing/lib/federatedshareprovider.php +++ /dev/null @@ -1,566 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\FederatedFileSharing; - -use OC\Share20\Share; -use OCP\Files\IRootFolder; -use OCP\IL10N; -use OCP\ILogger; -use OCP\Share\IShare; -use OCP\Share\IShareProvider; -use OC\Share20\Exception\InvalidShare; -use OCP\Share\Exceptions\ShareNotFound; -use OCP\Files\NotFoundException; -use OCP\IDBConnection; -use OCP\Files\Node; - -/** - * Class FederatedShareProvider - * - * @package OCA\FederatedFileSharing - */ -class FederatedShareProvider implements IShareProvider { - - const SHARE_TYPE_REMOTE = 6; - - /** @var IDBConnection */ - private $dbConnection; - - /** @var AddressHandler */ - private $addressHandler; - - /** @var Notifications */ - private $notifications; - - /** @var TokenHandler */ - private $tokenHandler; - - /** @var IL10N */ - private $l; - - /** @var ILogger */ - private $logger; - - /** @var IRootFolder */ - private $rootFolder; - - /** - * DefaultShareProvider constructor. - * - * @param IDBConnection $connection - * @param AddressHandler $addressHandler - * @param Notifications $notifications - * @param TokenHandler $tokenHandler - * @param IL10N $l10n - * @param ILogger $logger - * @param IRootFolder $rootFolder - */ - public function __construct( - IDBConnection $connection, - AddressHandler $addressHandler, - Notifications $notifications, - TokenHandler $tokenHandler, - IL10N $l10n, - ILogger $logger, - IRootFolder $rootFolder - ) { - $this->dbConnection = $connection; - $this->addressHandler = $addressHandler; - $this->notifications = $notifications; - $this->tokenHandler = $tokenHandler; - $this->l = $l10n; - $this->logger = $logger; - $this->rootFolder = $rootFolder; - } - - /** - * Return the identifier of this provider. - * - * @return string Containing only [a-zA-Z0-9] - */ - public function identifier() { - return 'ocFederatedSharing'; - } - - /** - * Share a path - * - * @param IShare $share - * @return IShare The share object - * @throws ShareNotFound - * @throws \Exception - */ - public function create(IShare $share) { - - $shareWith = $share->getSharedWith(); - $itemSource = $share->getNodeId(); - $itemType = $share->getNodeType(); - $uidOwner = $share->getShareOwner(); - $permissions = $share->getPermissions(); - $sharedBy = $share->getSharedBy(); - - /* - * Check if file is not already shared with the remote user - */ - $alreadyShared = $this->getSharedWith($shareWith, self::SHARE_TYPE_REMOTE, $share->getNode(), 1, 0); - if (!empty($alreadyShared)) { - $message = 'Sharing %s failed, because this item is already shared with %s'; - $message_t = $this->l->t('Sharing %s failed, because this item is already shared with %s', array($share->getNode()->getName(), $shareWith)); - $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']); - throw new \Exception($message_t); - } - - - // don't allow federated shares if source and target server are the same - list($user, $remote) = $this->addressHandler->splitUserRemote($shareWith); - $currentServer = $this->addressHandler->generateRemoteURL(); - $currentUser = $sharedBy; - if ($this->addressHandler->compareAddresses($user, $remote, $currentUser, $currentServer)) { - $message = 'Not allowed to create a federated share with the same user.'; - $message_t = $this->l->t('Not allowed to create a federated share with the same user'); - $this->logger->debug($message, ['app' => 'Federated File Sharing']); - throw new \Exception($message_t); - } - - $token = $this->tokenHandler->generateToken(); - - $shareWith = $user . '@' . $remote; - - $shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token); - - $send = $this->notifications->sendRemoteShare( - $token, - $shareWith, - $share->getNode()->getName(), - $shareId, - $share->getSharedBy() - ); - - $data = $this->getRawShare($shareId); - $share = $this->createShare($data); - - if ($send === false) { - $this->delete($share); - $message_t = $this->l->t('Sharing %s failed, could not find %s, maybe the server is currently unreachable.', - [$share->getNode()->getName(), $shareWith]); - throw new \Exception($message_t); - } - - return $share; - } - - /** - * add share to the database and return the ID - * - * @param int $itemSource - * @param string $itemType - * @param string $shareWith - * @param string $sharedBy - * @param string $uidOwner - * @param int $permissions - * @param string $token - * @return int - */ - private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token) { - $qb = $this->dbConnection->getQueryBuilder(); - $qb->insert('share') - ->setValue('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)) - ->setValue('item_type', $qb->createNamedParameter($itemType)) - ->setValue('item_source', $qb->createNamedParameter($itemSource)) - ->setValue('file_source', $qb->createNamedParameter($itemSource)) - ->setValue('share_with', $qb->createNamedParameter($shareWith)) - ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) - ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) - ->setValue('permissions', $qb->createNamedParameter($permissions)) - ->setValue('token', $qb->createNamedParameter($token)) - ->setValue('stime', $qb->createNamedParameter(time())); - - /* - * Added to fix https://github.com/owncloud/core/issues/22215 - * Can be removed once we get rid of ajax/share.php - */ - $qb->setValue('file_target', $qb->createNamedParameter('')); - - $qb->execute(); - $id = $qb->getLastInsertId(); - - return (int)$id; - } - - /** - * Update a share - * - * @param IShare $share - * @return IShare The share object - */ - public function update(IShare $share) { - /* - * We allow updating the permissions of federated shares - */ - $qb = $this->dbConnection->getQueryBuilder(); - $qb->update('share') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) - ->set('permissions', $qb->createNamedParameter($share->getPermissions())) - ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) - ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) - ->execute(); - - return $share; - } - - /** - * @inheritdoc - */ - public function move(IShare $share, $recipient) { - /* - * This function does nothing yet as it is just for outgoing - * federated shares. - */ - return $share; - } - - /** - * Get all children of this share - * - * @param IShare $parent - * @return IShare[] - */ - public function getChildren(IShare $parent) { - $children = []; - - $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) - ->orderBy('id'); - - $cursor = $qb->execute(); - while($data = $cursor->fetch()) { - $children[] = $this->createShare($data); - } - $cursor->closeCursor(); - - return $children; - } - - /** - * Delete a share - * - * @param IShare $share - */ - public function delete(IShare $share) { - $qb = $this->dbConnection->getQueryBuilder(); - $qb->delete('share') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))); - $qb->execute(); - - list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedWith()); - $this->notifications->sendRemoteUnShare($remote, $share->getId(), $share->getToken()); - } - - /** - * @inheritdoc - */ - public function deleteFromSelf(IShare $share, $recipient) { - // nothing to do here. Technically deleteFromSelf in the context of federated - // shares is a umount of a external storage. This is handled here - // apps/files_sharing/lib/external/manager.php - // TODO move this code over to this app - return; - } - - /** - * @inheritdoc - */ - public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { - $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('*') - ->from('share'); - - $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); - - /** - * Reshares for this user are shares where they are the owner. - */ - if ($reshares === false) { - //Special case for old shares created via the web UI - $or1 = $qb->expr()->andX( - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), - $qb->expr()->isNull('uid_initiator') - ); - - $qb->andWhere( - $qb->expr()->orX( - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), - $or1 - ) - ); - } else { - $qb->andWhere( - $qb->expr()->orX( - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) - ) - ); - } - - if ($node !== null) { - $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); - } - - if ($limit !== -1) { - $qb->setMaxResults($limit); - } - - $qb->setFirstResult($offset); - $qb->orderBy('id'); - - $cursor = $qb->execute(); - $shares = []; - while($data = $cursor->fetch()) { - $shares[] = $this->createShare($data); - } - $cursor->closeCursor(); - - return $shares; - } - - /** - * @inheritdoc - */ - public function getShareById($id, $recipientId = null) { - $qb = $this->dbConnection->getQueryBuilder(); - - $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); - - $cursor = $qb->execute(); - $data = $cursor->fetch(); - $cursor->closeCursor(); - - if ($data === false) { - throw new ShareNotFound(); - } - - try { - $share = $this->createShare($data); - } catch (InvalidShare $e) { - throw new ShareNotFound(); - } - - return $share; - } - - /** - * Get shares for a given path - * - * @param \OCP\Files\Node $path - * @return IShare[] - */ - public function getSharesByPath(Node $path) { - $qb = $this->dbConnection->getQueryBuilder(); - - $cursor = $qb->select('*') - ->from('share') - ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) - ->execute(); - - $shares = []; - while($data = $cursor->fetch()) { - $shares[] = $this->createShare($data); - } - $cursor->closeCursor(); - - return $shares; - } - - /** - * @inheritdoc - */ - public function getSharedWith($userId, $shareType, $node, $limit, $offset) { - /** @var IShare[] $shares */ - $shares = []; - - //Get shares directly with this user - $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('*') - ->from('share'); - - // Order by id - $qb->orderBy('id'); - - // Set limit and offset - if ($limit !== -1) { - $qb->setMaxResults($limit); - } - $qb->setFirstResult($offset); - - $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); - $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); - - // Filter by node if provided - if ($node !== null) { - $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); - } - - $cursor = $qb->execute(); - - while($data = $cursor->fetch()) { - $shares[] = $this->createShare($data); - } - $cursor->closeCursor(); - - - return $shares; - } - - /** - * Get a share by token - * - * @param string $token - * @return IShare - * @throws ShareNotFound - */ - public function getShareByToken($token) { - $qb = $this->dbConnection->getQueryBuilder(); - - $cursor = $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) - ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) - ->execute(); - - $data = $cursor->fetch(); - - if ($data === false) { - throw new ShareNotFound(); - } - - try { - $share = $this->createShare($data); - } catch (InvalidShare $e) { - throw new ShareNotFound(); - } - - return $share; - } - - /** - * get database row of a give share - * - * @param $id - * @return array - * @throws ShareNotFound - */ - private function getRawShare($id) { - - // Now fetch the inserted share and create a complete share object - $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); - - $cursor = $qb->execute(); - $data = $cursor->fetch(); - $cursor->closeCursor(); - - if ($data === false) { - throw new ShareNotFound; - } - - return $data; - } - - /** - * Create a share object from an database row - * - * @param array $data - * @return IShare - * @throws InvalidShare - * @throws ShareNotFound - */ - private function createShare($data) { - - $share = new Share($this->rootFolder); - $share->setId((int)$data['id']) - ->setShareType((int)$data['share_type']) - ->setPermissions((int)$data['permissions']) - ->setTarget($data['file_target']) - ->setMailSend((bool)$data['mail_send']) - ->setToken($data['token']); - - $shareTime = new \DateTime(); - $shareTime->setTimestamp((int)$data['stime']); - $share->setShareTime($shareTime); - $share->setSharedWith($data['share_with']); - - if ($data['uid_initiator'] !== null) { - $share->setShareOwner($data['uid_owner']); - $share->setSharedBy($data['uid_initiator']); - } else { - //OLD SHARE - $share->setSharedBy($data['uid_owner']); - $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']); - - $owner = $path->getOwner(); - $share->setShareOwner($owner->getUID()); - } - - $share->setNodeId((int)$data['file_source']); - $share->setNodeType($data['item_type']); - - $share->setProviderId($this->identifier()); - - return $share; - } - - /** - * Get the node with file $id for $user - * - * @param string $userId - * @param int $id - * @return \OCP\Files\File|\OCP\Files\Folder - * @throws InvalidShare - */ - private function getNode($userId, $id) { - try { - $userFolder = $this->rootFolder->getUserFolder($userId); - } catch (NotFoundException $e) { - throw new InvalidShare(); - } - - $nodes = $userFolder->getById($id); - - if (empty($nodes)) { - throw new InvalidShare(); - } - - return $nodes[0]; - } - -} diff --git a/apps/federatedfilesharing/lib/notifications.php b/apps/federatedfilesharing/lib/notifications.php deleted file mode 100644 index 4ec21e81cc7..00000000000 --- a/apps/federatedfilesharing/lib/notifications.php +++ /dev/null @@ -1,146 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Lukas Reschke <lukas@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - - -namespace OCA\FederatedFileSharing; - -use OCP\Http\Client\IClientService; - -class Notifications { - const RESPONSE_FORMAT = 'json'; // default response format for ocs calls - - /** @var AddressHandler */ - private $addressHandler; - /** @var IClientService */ - private $httpClientService; - /** @var DiscoveryManager */ - private $discoveryManager; - - /** - * @param AddressHandler $addressHandler - * @param IClientService $httpClientService - * @param DiscoveryManager $discoveryManager - */ - public function __construct( - AddressHandler $addressHandler, - IClientService $httpClientService, - DiscoveryManager $discoveryManager - ) { - $this->addressHandler = $addressHandler; - $this->httpClientService = $httpClientService; - $this->discoveryManager = $discoveryManager; - } - - /** - * send server-to-server share to remote server - * - * @param string $token - * @param string $shareWith - * @param string $name - * @param int $remote_id - * @param string $owner - * @return bool - */ - public function sendRemoteShare($token, $shareWith, $name, $remote_id, $owner) { - - list($user, $remote) = $this->addressHandler->splitUserRemote($shareWith); - - if ($user && $remote) { - $url = $remote; - $local = $this->addressHandler->generateRemoteURL(); - - $fields = array( - 'shareWith' => $user, - 'token' => $token, - 'name' => $name, - 'remoteId' => $remote_id, - 'owner' => $owner, - 'remote' => $local, - ); - - $url = $this->addressHandler->removeProtocolFromUrl($url); - $result = $this->tryHttpPostToShareEndpoint($url, '', $fields); - $status = json_decode($result['result'], true); - - if ($result['success'] && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200)) { - \OC_Hook::emit('OCP\Share', 'federated_share_added', ['server' => $remote]); - return true; - } - - } - - return false; - } - - /** - * send server-to-server unshare to remote server - * - * @param string $remote url - * @param int $id share id - * @param string $token - * @return bool - */ - public function sendRemoteUnShare($remote, $id, $token) { - $url = rtrim($remote, '/'); - $fields = array('token' => $token, 'format' => 'json'); - $url = $this->addressHandler->removeProtocolFromUrl($url); - $result = $this->tryHttpPostToShareEndpoint($url, '/'.$id.'/unshare', $fields); - $status = json_decode($result['result'], true); - - return ($result['success'] && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200)); - } - - /** - * try http post first with https and then with http as a fallback - * - * @param string $remoteDomain - * @param string $urlSuffix - * @param array $fields post parameters - * @return array - */ - private function tryHttpPostToShareEndpoint($remoteDomain, $urlSuffix, array $fields) { - $client = $this->httpClientService->newClient(); - $protocol = 'https://'; - $result = [ - 'success' => false, - 'result' => '', - ]; - $try = 0; - - while ($result['success'] === false && $try < 2) { - $endpoint = $this->discoveryManager->getShareEndpoint($protocol . $remoteDomain); - try { - $response = $client->post($protocol . $remoteDomain . $endpoint . $urlSuffix . '?format=' . self::RESPONSE_FORMAT, [ - 'body' => $fields - ]); - $result['result'] = $response->getBody(); - $result['success'] = true; - break; - } catch (\Exception $e) { - $try++; - $protocol = 'http://'; - } - } - - return $result; - } -} diff --git a/apps/federatedfilesharing/lib/tokenhandler.php b/apps/federatedfilesharing/lib/tokenhandler.php deleted file mode 100644 index ec5f73127d6..00000000000 --- a/apps/federatedfilesharing/lib/tokenhandler.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - - -namespace OCA\FederatedFileSharing; - - -use OCP\Security\ISecureRandom; - -/** - * Class TokenHandler - * - * @package OCA\FederatedFileSharing - */ -class TokenHandler { - - const TOKEN_LENGTH = 15; - - /** @var ISecureRandom */ - private $secureRandom; - - /** - * TokenHandler constructor. - * - * @param ISecureRandom $secureRandom - */ - public function __construct(ISecureRandom $secureRandom) { - $this->secureRandom = $secureRandom; - } - - /** - * generate to token used to authenticate federated shares - * - * @return string - */ - public function generateToken() { - $token = $this->secureRandom->generate( - self::TOKEN_LENGTH, - ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS); - return $token; - } - -} |