123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- <?php
- /**
- * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
- * SPDX-License-Identifier: AGPL-3.0-only
- */
- namespace OCA\FederatedFileSharing;
- use OCA\FederatedFileSharing\Events\FederatedShareAddedEvent;
- use OCP\AppFramework\Http;
- use OCP\BackgroundJob\IJobList;
- use OCP\EventDispatcher\IEventDispatcher;
- use OCP\Federation\ICloudFederationFactory;
- use OCP\Federation\ICloudFederationProviderManager;
- use OCP\HintException;
- use OCP\Http\Client\IClientService;
- use OCP\OCS\IDiscoveryService;
- use Psr\Log\LoggerInterface;
- class Notifications {
- public const RESPONSE_FORMAT = 'json'; // default response format for ocs calls
- public function __construct(
- private AddressHandler $addressHandler,
- private IClientService $httpClientService,
- private IDiscoveryService $discoveryService,
- private IJobList $jobList,
- private ICloudFederationProviderManager $federationProviderManager,
- private ICloudFederationFactory $cloudFederationFactory,
- private IEventDispatcher $eventDispatcher,
- private LoggerInterface $logger,
- ) {
- }
- /**
- * send server-to-server share to remote server
- *
- * @param string $token
- * @param string $shareWith
- * @param string $name
- * @param string $remoteId
- * @param string $owner
- * @param string $ownerFederatedId
- * @param string $sharedBy
- * @param string $sharedByFederatedId
- * @param int $shareType (can be a remote user or group share)
- * @return bool
- * @throws HintException
- * @throws \OC\ServerNotAvailableException
- */
- public function sendRemoteShare($token, $shareWith, $name, $remoteId, $owner, $ownerFederatedId, $sharedBy, $sharedByFederatedId, $shareType) {
- [$user, $remote] = $this->addressHandler->splitUserRemote($shareWith);
- if ($user && $remote) {
- $local = $this->addressHandler->generateRemoteURL();
- $fields = [
- 'shareWith' => $user,
- 'token' => $token,
- 'name' => $name,
- 'remoteId' => $remoteId,
- 'owner' => $owner,
- 'ownerFederatedId' => $ownerFederatedId,
- 'sharedBy' => $sharedBy,
- 'sharedByFederatedId' => $sharedByFederatedId,
- 'remote' => $local,
- 'shareType' => $shareType
- ];
- $result = $this->tryHttpPostToShareEndpoint($remote, '', $fields);
- $status = json_decode($result['result'], true);
- $ocsStatus = isset($status['ocs']);
- $ocsSuccess = $ocsStatus && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200);
- if ($result['success'] && (!$ocsStatus || $ocsSuccess)) {
- $event = new FederatedShareAddedEvent($remote);
- $this->eventDispatcher->dispatchTyped($event);
- return true;
- } else {
- $this->logger->info(
- "failed sharing $name with $shareWith",
- ['app' => 'federatedfilesharing']
- );
- }
- } else {
- $this->logger->info(
- "could not share $name, invalid contact $shareWith",
- ['app' => 'federatedfilesharing']
- );
- }
- return false;
- }
- /**
- * ask owner to re-share the file with the given user
- *
- * @param string $token
- * @param string $id remote Id
- * @param string $shareId internal share Id
- * @param string $remote remote address of the owner
- * @param string $shareWith
- * @param int $permission
- * @param string $filename
- * @return array|false
- * @throws HintException
- * @throws \OC\ServerNotAvailableException
- */
- public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission, $filename) {
- $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;
- }
- }
|