aboutsummaryrefslogtreecommitdiffstats
path: root/apps/federatedfilesharing/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/federatedfilesharing/lib')
-rw-r--r--apps/federatedfilesharing/lib/AddressHandler.php127
-rw-r--r--apps/federatedfilesharing/lib/AppInfo/Application.php44
-rw-r--r--apps/federatedfilesharing/lib/BackgroundJob/RetryJob.php91
-rw-r--r--apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php184
-rw-r--r--apps/federatedfilesharing/lib/Controller/RequestHandlerController.php418
-rw-r--r--apps/federatedfilesharing/lib/Events/FederatedShareAddedEvent.php34
-rw-r--r--apps/federatedfilesharing/lib/FederatedShareProvider.php1089
-rw-r--r--apps/federatedfilesharing/lib/Listeners/LoadAdditionalScriptsListener.php41
-rw-r--r--apps/federatedfilesharing/lib/Migration/Version1010Date20200630191755.php43
-rw-r--r--apps/federatedfilesharing/lib/Migration/Version1011Date20201120125158.php52
-rw-r--r--apps/federatedfilesharing/lib/Notifications.php421
-rw-r--r--apps/federatedfilesharing/lib/Notifier.php246
-rw-r--r--apps/federatedfilesharing/lib/OCM/CloudFederationProviderFiles.php814
-rw-r--r--apps/federatedfilesharing/lib/Settings/Admin.php85
-rw-r--r--apps/federatedfilesharing/lib/Settings/Personal.php70
-rw-r--r--apps/federatedfilesharing/lib/Settings/PersonalSection.php64
-rw-r--r--apps/federatedfilesharing/lib/TokenHandler.php41
-rw-r--r--apps/federatedfilesharing/lib/addresshandler.php184
-rw-r--r--apps/federatedfilesharing/lib/discoverymanager.php136
-rw-r--r--apps/federatedfilesharing/lib/federatedshareprovider.php566
-rw-r--r--apps/federatedfilesharing/lib/notifications.php146
-rw-r--r--apps/federatedfilesharing/lib/tokenhandler.php61
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;
- }
-
-}