Notifications.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\FederatedFileSharing;
  8. use OCA\FederatedFileSharing\Events\FederatedShareAddedEvent;
  9. use OCP\AppFramework\Http;
  10. use OCP\BackgroundJob\IJobList;
  11. use OCP\EventDispatcher\IEventDispatcher;
  12. use OCP\Federation\ICloudFederationFactory;
  13. use OCP\Federation\ICloudFederationProviderManager;
  14. use OCP\Http\Client\IClientService;
  15. use OCP\OCS\IDiscoveryService;
  16. use Psr\Log\LoggerInterface;
  17. class Notifications {
  18. public const RESPONSE_FORMAT = 'json'; // default response format for ocs calls
  19. public function __construct(
  20. private AddressHandler $addressHandler,
  21. private IClientService $httpClientService,
  22. private IDiscoveryService $discoveryService,
  23. private IJobList $jobList,
  24. private ICloudFederationProviderManager $federationProviderManager,
  25. private ICloudFederationFactory $cloudFederationFactory,
  26. private IEventDispatcher $eventDispatcher,
  27. private LoggerInterface $logger,
  28. ) {
  29. }
  30. /**
  31. * send server-to-server share to remote server
  32. *
  33. * @param string $token
  34. * @param string $shareWith
  35. * @param string $name
  36. * @param string $remoteId
  37. * @param string $owner
  38. * @param string $ownerFederatedId
  39. * @param string $sharedBy
  40. * @param string $sharedByFederatedId
  41. * @param int $shareType (can be a remote user or group share)
  42. * @return bool
  43. * @throws \OCP\HintException
  44. * @throws \OC\ServerNotAvailableException
  45. */
  46. public function sendRemoteShare($token, $shareWith, $name, $remoteId, $owner, $ownerFederatedId, $sharedBy, $sharedByFederatedId, $shareType) {
  47. [$user, $remote] = $this->addressHandler->splitUserRemote($shareWith);
  48. if ($user && $remote) {
  49. $local = $this->addressHandler->generateRemoteURL();
  50. $fields = [
  51. 'shareWith' => $user,
  52. 'token' => $token,
  53. 'name' => $name,
  54. 'remoteId' => $remoteId,
  55. 'owner' => $owner,
  56. 'ownerFederatedId' => $ownerFederatedId,
  57. 'sharedBy' => $sharedBy,
  58. 'sharedByFederatedId' => $sharedByFederatedId,
  59. 'remote' => $local,
  60. 'shareType' => $shareType
  61. ];
  62. $result = $this->tryHttpPostToShareEndpoint($remote, '', $fields);
  63. $status = json_decode($result['result'], true);
  64. $ocsStatus = isset($status['ocs']);
  65. $ocsSuccess = $ocsStatus && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200);
  66. if ($result['success'] && (!$ocsStatus || $ocsSuccess)) {
  67. $event = new FederatedShareAddedEvent($remote);
  68. $this->eventDispatcher->dispatchTyped($event);
  69. return true;
  70. } else {
  71. $this->logger->info(
  72. "failed sharing $name with $shareWith",
  73. ['app' => 'federatedfilesharing']
  74. );
  75. }
  76. } else {
  77. $this->logger->info(
  78. "could not share $name, invalid contact $shareWith",
  79. ['app' => 'federatedfilesharing']
  80. );
  81. }
  82. return false;
  83. }
  84. /**
  85. * ask owner to re-share the file with the given user
  86. *
  87. * @param string $token
  88. * @param string $id remote Id
  89. * @param string $shareId internal share Id
  90. * @param string $remote remote address of the owner
  91. * @param string $shareWith
  92. * @param int $permission
  93. * @param string $filename
  94. * @return array|false
  95. * @throws \OCP\HintException
  96. * @throws \OC\ServerNotAvailableException
  97. */
  98. public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission, $filename) {
  99. $fields = [
  100. 'shareWith' => $shareWith,
  101. 'token' => $token,
  102. 'permission' => $permission,
  103. 'remoteId' => $shareId,
  104. ];
  105. $ocmFields = $fields;
  106. $ocmFields['remoteId'] = (string)$id;
  107. $ocmFields['localId'] = $shareId;
  108. $ocmFields['name'] = $filename;
  109. $ocmResult = $this->tryOCMEndPoint($remote, $ocmFields, 'reshare');
  110. if (is_array($ocmResult) && isset($ocmResult['token']) && isset($ocmResult['providerId'])) {
  111. return [$ocmResult['token'], $ocmResult['providerId']];
  112. }
  113. $result = $this->tryLegacyEndPoint(rtrim($remote, '/'), '/' . $id . '/reshare', $fields);
  114. $status = json_decode($result['result'], true);
  115. $httpRequestSuccessful = $result['success'];
  116. $ocsCallSuccessful = $status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200;
  117. $validToken = isset($status['ocs']['data']['token']) && is_string($status['ocs']['data']['token']);
  118. $validRemoteId = isset($status['ocs']['data']['remoteId']);
  119. if ($httpRequestSuccessful && $ocsCallSuccessful && $validToken && $validRemoteId) {
  120. return [
  121. $status['ocs']['data']['token'],
  122. $status['ocs']['data']['remoteId']
  123. ];
  124. } elseif (!$validToken) {
  125. $this->logger->info(
  126. "invalid or missing token requesting re-share for $filename to $remote",
  127. ['app' => 'federatedfilesharing']
  128. );
  129. } elseif (!$validRemoteId) {
  130. $this->logger->info(
  131. "missing remote id requesting re-share for $filename to $remote",
  132. ['app' => 'federatedfilesharing']
  133. );
  134. } else {
  135. $this->logger->info(
  136. "failed requesting re-share for $filename to $remote",
  137. ['app' => 'federatedfilesharing']
  138. );
  139. }
  140. return false;
  141. }
  142. /**
  143. * send server-to-server unshare to remote server
  144. *
  145. * @param string $remote url
  146. * @param string $id share id
  147. * @param string $token
  148. * @return bool
  149. */
  150. public function sendRemoteUnShare($remote, $id, $token) {
  151. $this->sendUpdateToRemote($remote, $id, $token, 'unshare');
  152. }
  153. /**
  154. * send server-to-server unshare to remote server
  155. *
  156. * @param string $remote url
  157. * @param string $id share id
  158. * @param string $token
  159. * @return bool
  160. */
  161. public function sendRevokeShare($remote, $id, $token) {
  162. $this->sendUpdateToRemote($remote, $id, $token, 'reshare_undo');
  163. }
  164. /**
  165. * send notification to remote server if the permissions was changed
  166. *
  167. * @param string $remote
  168. * @param string $remoteId
  169. * @param string $token
  170. * @param int $permissions
  171. * @return bool
  172. */
  173. public function sendPermissionChange($remote, $remoteId, $token, $permissions) {
  174. $this->sendUpdateToRemote($remote, $remoteId, $token, 'permissions', ['permissions' => $permissions]);
  175. }
  176. /**
  177. * forward accept reShare to remote server
  178. *
  179. * @param string $remote
  180. * @param string $remoteId
  181. * @param string $token
  182. */
  183. public function sendAcceptShare($remote, $remoteId, $token) {
  184. $this->sendUpdateToRemote($remote, $remoteId, $token, 'accept');
  185. }
  186. /**
  187. * forward decline reShare to remote server
  188. *
  189. * @param string $remote
  190. * @param string $remoteId
  191. * @param string $token
  192. */
  193. public function sendDeclineShare($remote, $remoteId, $token) {
  194. $this->sendUpdateToRemote($remote, $remoteId, $token, 'decline');
  195. }
  196. /**
  197. * inform remote server whether server-to-server share was accepted/declined
  198. *
  199. * @param string $remote
  200. * @param string $token
  201. * @param string $remoteId Share id on the remote host
  202. * @param string $action possible actions: accept, decline, unshare, revoke, permissions
  203. * @param array $data
  204. * @param int $try
  205. * @return boolean
  206. */
  207. public function sendUpdateToRemote($remote, $remoteId, $token, $action, $data = [], $try = 0) {
  208. $fields = [
  209. 'token' => $token,
  210. 'remoteId' => $remoteId
  211. ];
  212. foreach ($data as $key => $value) {
  213. $fields[$key] = $value;
  214. }
  215. $result = $this->tryHttpPostToShareEndpoint(rtrim($remote, '/'), '/' . $remoteId . '/' . $action, $fields, $action);
  216. $status = json_decode($result['result'], true);
  217. if ($result['success'] &&
  218. isset($status['ocs']['meta']['statuscode']) &&
  219. ($status['ocs']['meta']['statuscode'] === 100 ||
  220. $status['ocs']['meta']['statuscode'] === 200
  221. )
  222. ) {
  223. return true;
  224. } elseif ($try === 0) {
  225. // only add new job on first try
  226. $this->jobList->add('OCA\FederatedFileSharing\BackgroundJob\RetryJob',
  227. [
  228. 'remote' => $remote,
  229. 'remoteId' => $remoteId,
  230. 'token' => $token,
  231. 'action' => $action,
  232. 'data' => json_encode($data),
  233. 'try' => $try,
  234. 'lastRun' => $this->getTimestamp()
  235. ]
  236. );
  237. }
  238. return false;
  239. }
  240. /**
  241. * return current timestamp
  242. *
  243. * @return int
  244. */
  245. protected function getTimestamp() {
  246. return time();
  247. }
  248. /**
  249. * try http post with the given protocol, if no protocol is given we pick
  250. * the secure one (https)
  251. *
  252. * @param string $remoteDomain
  253. * @param string $urlSuffix
  254. * @param array $fields post parameters
  255. * @param string $action define the action (possible values: share, reshare, accept, decline, unshare, revoke, permissions)
  256. * @return array
  257. * @throws \Exception
  258. */
  259. protected function tryHttpPostToShareEndpoint($remoteDomain, $urlSuffix, array $fields, $action = "share") {
  260. if ($this->addressHandler->urlContainProtocol($remoteDomain) === false) {
  261. $remoteDomain = 'https://' . $remoteDomain;
  262. }
  263. $result = [
  264. 'success' => false,
  265. 'result' => '',
  266. ];
  267. // if possible we use the new OCM API
  268. $ocmResult = $this->tryOCMEndPoint($remoteDomain, $fields, $action);
  269. if (is_array($ocmResult)) {
  270. $result['success'] = true;
  271. $result['result'] = json_encode([
  272. 'ocs' => ['meta' => ['statuscode' => 200]]]);
  273. return $result;
  274. }
  275. return $this->tryLegacyEndPoint($remoteDomain, $urlSuffix, $fields);
  276. }
  277. /**
  278. * try old federated sharing API if the OCM api doesn't work
  279. *
  280. * @param $remoteDomain
  281. * @param $urlSuffix
  282. * @param array $fields
  283. * @return mixed
  284. * @throws \Exception
  285. */
  286. protected function tryLegacyEndPoint($remoteDomain, $urlSuffix, array $fields) {
  287. $result = [
  288. 'success' => false,
  289. 'result' => '',
  290. ];
  291. // Fall back to old API
  292. $client = $this->httpClientService->newClient();
  293. $federationEndpoints = $this->discoveryService->discover($remoteDomain, 'FEDERATED_SHARING');
  294. $endpoint = $federationEndpoints['share'] ?? '/ocs/v2.php/cloud/shares';
  295. try {
  296. $response = $client->post($remoteDomain . $endpoint . $urlSuffix . '?format=' . self::RESPONSE_FORMAT, [
  297. 'body' => $fields,
  298. 'timeout' => 10,
  299. 'connect_timeout' => 10,
  300. ]);
  301. $result['result'] = $response->getBody();
  302. $result['success'] = true;
  303. } catch (\Exception $e) {
  304. // if flat re-sharing is not supported by the remote server
  305. // we re-throw the exception and fall back to the old behaviour.
  306. // (flat re-shares has been introduced in Nextcloud 9.1)
  307. if ($e->getCode() === Http::STATUS_INTERNAL_SERVER_ERROR) {
  308. throw $e;
  309. }
  310. }
  311. return $result;
  312. }
  313. /**
  314. * send action regarding federated sharing to the remote server using the OCM API
  315. *
  316. * @param $remoteDomain
  317. * @param $fields
  318. * @param $action
  319. *
  320. * @return array|false
  321. */
  322. protected function tryOCMEndPoint($remoteDomain, $fields, $action) {
  323. switch ($action) {
  324. case 'share':
  325. $share = $this->cloudFederationFactory->getCloudFederationShare(
  326. $fields['shareWith'] . '@' . $remoteDomain,
  327. $fields['name'],
  328. '',
  329. $fields['remoteId'],
  330. $fields['ownerFederatedId'],
  331. $fields['owner'],
  332. $fields['sharedByFederatedId'],
  333. $fields['sharedBy'],
  334. $fields['token'],
  335. $fields['shareType'],
  336. 'file'
  337. );
  338. return $this->federationProviderManager->sendShare($share);
  339. case 'reshare':
  340. // ask owner to reshare a file
  341. $notification = $this->cloudFederationFactory->getCloudFederationNotification();
  342. $notification->setMessage('REQUEST_RESHARE',
  343. 'file',
  344. $fields['remoteId'],
  345. [
  346. 'sharedSecret' => $fields['token'],
  347. 'shareWith' => $fields['shareWith'],
  348. 'senderId' => $fields['localId'],
  349. 'shareType' => $fields['shareType'],
  350. 'message' => 'Ask owner to reshare the file'
  351. ]
  352. );
  353. return $this->federationProviderManager->sendNotification($remoteDomain, $notification);
  354. case 'unshare':
  355. //owner unshares the file from the recipient again
  356. $notification = $this->cloudFederationFactory->getCloudFederationNotification();
  357. $notification->setMessage('SHARE_UNSHARED',
  358. 'file',
  359. $fields['remoteId'],
  360. [
  361. 'sharedSecret' => $fields['token'],
  362. 'messgage' => 'file is no longer shared with you'
  363. ]
  364. );
  365. return $this->federationProviderManager->sendNotification($remoteDomain, $notification);
  366. case 'reshare_undo':
  367. // if a reshare was unshared we send the information to the initiator/owner
  368. $notification = $this->cloudFederationFactory->getCloudFederationNotification();
  369. $notification->setMessage('RESHARE_UNDO',
  370. 'file',
  371. $fields['remoteId'],
  372. [
  373. 'sharedSecret' => $fields['token'],
  374. 'message' => 'reshare was revoked'
  375. ]
  376. );
  377. return $this->federationProviderManager->sendNotification($remoteDomain, $notification);
  378. }
  379. return false;
  380. }
  381. }