migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,156 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
*
* @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\Federation\ICloudIdManager;
use OCP\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;
/** @var ICloudIdManager */
private $cloudIdManager;
/**
* AddressHandler constructor.
*
* @param IURLGenerator $urlGenerator
* @param IL10N $il10n
* @param ICloudIdManager $cloudIdManager
*/
public function __construct(
IURLGenerator $urlGenerator,
IL10N $il10n,
ICloudIdManager $cloudIdManager
) {
$this->l = $il10n;
$this->urlGenerator = $urlGenerator;
$this->cloudIdManager = $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
\OCP\Util::emitHook(
'\OCA\Files_Sharing\API\Server2Server',
'preLoginNameUsedAsUserName',
['uid' => &$user1]
);
\OCP\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;
}
}
@@ -0,0 +1,63 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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\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);
});
}
}
@@ -0,0 +1,110 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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\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;
private Notifications $notifications;
/** @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(Notifications $notifications,
ITimeFactory $time) {
parent::__construct($time);
$this->notifications = $notifications;
}
/**
* 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);
}
}
@@ -0,0 +1,205 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @copyright Copyright (c) 2016, Björn Schießle <bjoern@schiessle.org>
*
* @author Allan Nordhøy <epost@anotheragency.no>
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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\Controller;
use OCA\FederatedFileSharing\AddressHandler;
use OCA\FederatedFileSharing\FederatedShareProvider;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
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
*/
#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
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
*
* @NoCSRFRequired
* @PublicPage
* @BruteForceProtection(action=publicLink2FederatedShare)
*
* @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
*/
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('public_link_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
*
* @NoAdminRequired
*
* @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
*/
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);
}
}
@@ -0,0 +1,488 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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\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\OpenAPI;
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\IDBConnection;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\Log\Audit\CriticalActionPerformedEvent;
use OCP\Share;
use OCP\Share\Exceptions\ShareNotFound;
use Psr\Log\LoggerInterface;
#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
class RequestHandlerController extends OCSController {
/** @var FederatedShareProvider */
private $federatedShareProvider;
/** @var IDBConnection */
private $connection;
/** @var Share\IManager */
private $shareManager;
/** @var Notifications */
private $notifications;
/** @var AddressHandler */
private $addressHandler;
/** @var IUserManager */
private $userManager;
/** @var string */
private $shareTable = 'share';
/** @var ICloudIdManager */
private $cloudIdManager;
/** @var LoggerInterface */
private $logger;
/** @var ICloudFederationFactory */
private $cloudFederationFactory;
/** @var ICloudFederationProviderManager */
private $cloudFederationProviderManager;
/** @var IEventDispatcher */
private $eventDispatcher;
public function __construct(string $appName,
IRequest $request,
FederatedShareProvider $federatedShareProvider,
IDBConnection $connection,
Share\IManager $shareManager,
Notifications $notifications,
AddressHandler $addressHandler,
IUserManager $userManager,
ICloudIdManager $cloudIdManager,
LoggerInterface $logger,
ICloudFederationFactory $cloudFederationFactory,
ICloudFederationProviderManager $cloudFederationProviderManager,
IEventDispatcher $eventDispatcher
) {
parent::__construct($appName, $request);
$this->federatedShareProvider = $federatedShareProvider;
$this->connection = $connection;
$this->shareManager = $shareManager;
$this->notifications = $notifications;
$this->addressHandler = $addressHandler;
$this->userManager = $userManager;
$this->cloudIdManager = $cloudIdManager;
$this->logger = $logger;
$this->cloudFederationFactory = $cloudFederationFactory;
$this->cloudFederationProviderManager = $cloudFederationProviderManager;
$this->eventDispatcher = $eventDispatcher;
}
/**
* @NoCSRFRequired
* @PublicPage
*
* 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, array<empty>, array{}>
* @throws OCSException
*
* 200: Share created successfully
*/
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 Http\DataResponse();
}
/**
* @NoCSRFRequired
* @PublicPage
*
* 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
*/
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 Http\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();
}
/**
* @NoCSRFRequired
* @PublicPage
*
* 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, array<empty>, array{}>
* @throws OCSException
* @throws ShareNotFound
* @throws \OCP\HintException
*
* 200: Share accepted successfully
*/
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 Http\DataResponse();
}
/**
* @NoCSRFRequired
* @PublicPage
*
* 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, array<empty>, array{}>
* @throws OCSException
*
* 200: Share declined successfully
*/
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 Http\DataResponse();
}
/**
* @NoCSRFRequired
* @PublicPage
*
* 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, array<empty>, array{}>
* @throws OCSException
*
* 200: Share unshared successfully
*/
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 Http\DataResponse();
}
private function cleanupRemote($remote) {
$remote = substr($remote, strpos($remote, '://') + 3);
return rtrim($remote, '/');
}
/**
* @NoCSRFRequired
* @PublicPage
*
* 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, array<empty>, array{}>
* @throws OCSBadRequestException Revoking the share is not possible
*
* 200: Share revoked successfully
*/
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 Http\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 = \OCP\Server::get(IAppManager::class)->isEnabledForUser('files_sharing');
if ($incoming) {
$result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled();
} else {
$result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
}
return $result;
}
/**
* @NoCSRFRequired
* @PublicPage
*
* 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, array<empty>, array{}>
* @throws OCSBadRequestException Updating permissions is not possible
*
* 200: Permissions updated successfully
*/
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 Http\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;
}
/**
* @NoCSRFRequired
* @PublicPage
*
* 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
*/
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 Http\DataResponse(['remote' => $cloudId->getRemote(), 'owner' => $cloudId->getUser()]);
} else {
throw new OCSBadRequestException('Share not found or token invalid');
}
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020, Morris Jobke <hey@morrisjobke.de>
*
* @author Morris Jobke <hey@morrisjobke.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 {
/** @var string */
private $remote;
/**
* @since 20.0.0
*/
public function __construct(string $remote) {
$this->remote = $remote;
}
/**
* @since 20.0.0
*/
public function getRemote(): string {
return $this->remote;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Morris Jobke <hey@morrisjobke.de>
*
* @author Morris Jobke <hey@morrisjobke.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FederatedFileSharing\Listeners;
use OCA\FederatedFileSharing\FederatedShareProvider;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
class LoadAdditionalScriptsListener implements IEventListener {
/** @var FederatedShareProvider */
protected $federatedShareProvider;
public function __construct(FederatedShareProvider $federatedShareProvider) {
$this->federatedShareProvider = $federatedShareProvider;
}
public function handle(Event $event): void {
if (!$event instanceof LoadAdditionalScriptsEvent) {
return;
}
if ($this->federatedShareProvider->isIncomingServer2serverShareEnabled()) {
\OCP\Util::addScript('federatedfilesharing', 'external');
}
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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;
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 {
/** @var IDBConnection */
private $connection;
public function __construct(IDBConnection $connection) {
$this->connection = $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();
}
}
@@ -0,0 +1,439 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Samuel <faust64@gmail.com>
*
* @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 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\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 \OCP\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 \OCP\HintException
* @throws \OC\ServerNotAvailableException
*/
public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission, $filename) {
$fields = [
'shareWith' => $shareWith,
'token' => $token,
'permission' => $permission,
'remoteId' => $shareId,
];
$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'],
'messgage' => '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;
}
}
@@ -0,0 +1,272 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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\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;
class Notifier implements INotifier {
/** @var IFactory */
protected $factory;
/** @var IManager */
protected $contactsManager;
/** @var IURLGenerator */
protected $url;
/** @var array */
protected $federatedContacts;
/** @var ICloudIdManager */
protected $cloudIdManager;
/**
* @param IFactory $factory
* @param IManager $contactsManager
* @param IURLGenerator $url
* @param ICloudIdManager $cloudIdManager
*/
public function __construct(IFactory $factory, IManager $contactsManager, IURLGenerator $url, ICloudIdManager $cloudIdManager) {
$this->factory = $factory;
$this->contactsManager = $contactsManager;
$this->url = $url;
$this->cloudIdManager = $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 \InvalidArgumentException
*/
public function prepare(INotification $notification, string $languageCode): INotification {
if ($notification->getApp() !== 'files_sharing' || $notification->getObjectType() !== 'remote_share') {
// Not my app => throw
throw new \InvalidArgumentException();
}
// Read the language from the notification
$l = $this->factory->get('files_sharing', $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 \InvalidArgumentException();
}
}
/**
* @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');
}
}
@@ -0,0 +1,769 @@
<?php
/**
* @copyright Copyright (c) 2018 Bjoern Schiessle <bjoern@schiessle.org>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FederatedFileSharing\OCM;
use OC\AppFramework\Http;
use OC\Files\Filesystem;
use OCA\FederatedFileSharing\AddressHandler;
use OCA\FederatedFileSharing\FederatedShareProvider;
use OCA\Files_Sharing\Activity\Providers\RemoteShares;
use OCA\Files_Sharing\External\Manager;
use OCP\Activity\IManager as IActivityManager;
use OCP\App\IAppManager;
use OCP\Constants;
use OCP\Federation\Exceptions\ActionNotSupportedException;
use OCP\Federation\Exceptions\AuthenticationFailedException;
use OCP\Federation\Exceptions\BadRequestException;
use OCP\Federation\Exceptions\ProviderCouldNotAddShareException;
use OCP\Federation\ICloudFederationFactory;
use OCP\Federation\ICloudFederationProvider;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Federation\ICloudFederationShare;
use OCP\Federation\ICloudIdManager;
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\IShare;
use OCP\Util;
use Psr\Log\LoggerInterface;
class CloudFederationProviderFiles implements ICloudFederationProvider {
/**
* 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,
) {
}
/**
* @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 \OCP\AppFramework\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 (!Util::isValidFileName($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 = \OC::$server->getDatabaseConnection()->lastInsertId('*PREFIX*share_external');
// get DisplayName about the owner of the share
$ownerDisplayName = $this->getUserDisplayName($ownerFederatedId);
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);
\OC::$server->getActivityManager()->publish($event);
$this->notifyAboutNewShare($shareWith, $shareId, $ownerFederatedId, $sharedByFederatedId, $name, $ownerDisplayName);
} 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);
\OC::$server->getActivityManager()->publish($event);
$this->notifyAboutNewShare($user->getUID(), $shareId, $ownerFederatedId, $sharedByFederatedId, $name, $ownerDisplayName);
}
}
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()) {
[, $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->execute();
$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->execute();
// 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->execute();
$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', (int)$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);
\OC::$server->getActivityManager()->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))
->execute();
}
/**
* 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->isInstalled('globalsiteselector')
|| !class_exists('\OCA\GlobalSiteSelector\Service\SlaveService')) {
return '';
}
try {
$slaveService = Server::get(\OCA\GlobalSiteSelector\Service\SlaveService::class);
} catch (\Throwable $e) {
Server::get(LoggerInterface::class)->error(
$e->getMessage(),
['exception' => $e]
);
return '';
}
return $slaveService->getUserDisplayName($this->cloudIdManager->removeProtocolFromUrl($userId), false);
}
}
@@ -0,0 +1,112 @@
<?php
/**
* @copyright Copyright (c) 2016 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Lukas Reschke <lukas@statuscode.ch>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 {
private FederatedShareProvider $fedShareProvider;
private IConfig $gsConfig;
private IL10N $l;
private IURLGenerator $urlGenerator;
private IInitialState $initialState;
/**
* Admin constructor.
*/
public function __construct(
FederatedShareProvider $fedShareProvider,
IConfig $globalScaleConfig,
IL10N $l,
IURLGenerator $urlGenerator,
IInitialState $initialState
) {
$this->fedShareProvider = $fedShareProvider;
$this->gsConfig = $globalScaleConfig;
$this->l = $l;
$this->urlGenerator = $urlGenerator;
$this->initialState = $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());
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',
],
];
}
}
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Jos Poortvliet <jos@opensuse.org>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 {
private FederatedShareProvider $federatedShareProvider;
private IUserSession $userSession;
private Defaults $defaults;
private IInitialState $initialState;
private IURLGenerator $urlGenerator;
public function __construct(
FederatedShareProvider $federatedShareProvider,
IUserSession $userSession,
Defaults $defaults,
IInitialState $initialState,
IURLGenerator $urlGenerator
) {
$this->federatedShareProvider = $federatedShareProvider;
$this->userSession = $userSession;
$this->defaults = $defaults;
$this->initialState = $initialState;
$this->urlGenerator = $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;
}
}
@@ -0,0 +1,84 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FederatedFileSharing\Settings;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class PersonalSection implements IIconSection {
/** @var IURLGenerator */
private $urlGenerator;
/** @var IL10N */
private $l;
public function __construct(IURLGenerator $urlGenerator, IL10N $l) {
$this->urlGenerator = $urlGenerator;
$this->l = $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;
}
}
@@ -0,0 +1,58 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 {
public 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_ALPHANUMERIC);
return $token;
}
}