Notifications.php 12 KB

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