Notifications.php 12 KB

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