1<?php
2/**
3 * @copyright Copyright (c) 2016, ownCloud, Inc.
4 *
5 * @author Bjoern Schiessle <bjoern@schiessle.org>
6 * @author Björn Schießle <bjoern@schiessle.org>
7 * @author Christoph Wurst <christoph@winzerhof-wurst.at>
8 * @author Julius Härtl <jus@bitgrid.net>
9 * @author Lukas Reschke <lukas@statuscode.ch>
10 * @author Morris Jobke <hey@morrisjobke.de>
11 * @author Samuel <faust64@gmail.com>
12 *
13 * @license AGPL-3.0
14 *
15 * This code is free software: you can redistribute it and/or modify
16 * it under the terms of the GNU Affero General Public License, version 3,
17 * as published by the Free Software Foundation.
18 *
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU Affero General Public License for more details.
23 *
24 * You should have received a copy of the GNU Affero General Public License, version 3,
25 * along with this program. If not, see <http://www.gnu.org/licenses/>
26 *
27 */
28namespace OCA\FederatedFileSharing;
29
30use OCA\FederatedFileSharing\Events\FederatedShareAddedEvent;
31use OCP\AppFramework\Http;
32use OCP\BackgroundJob\IJobList;
33use OCP\EventDispatcher\IEventDispatcher;
34use OCP\Federation\ICloudFederationFactory;
35use OCP\Federation\ICloudFederationProviderManager;
36use OCP\Http\Client\IClientService;
37use OCP\ILogger;
38use OCP\OCS\IDiscoveryService;
39
40class Notifications {
41	public const RESPONSE_FORMAT = 'json'; // default response format for ocs calls
42
43	/** @var AddressHandler */
44	private $addressHandler;
45
46	/** @var IClientService */
47	private $httpClientService;
48
49	/** @var IDiscoveryService */
50	private $discoveryService;
51
52	/** @var IJobList  */
53	private $jobList;
54
55	/** @var ICloudFederationProviderManager */
56	private $federationProviderManager;
57
58	/** @var ICloudFederationFactory */
59	private $cloudFederationFactory;
60
61	/** @var IEventDispatcher */
62	private $eventDispatcher;
63
64	/** @var ILogger */
65	private $logger;
66
67	public function __construct(
68		AddressHandler $addressHandler,
69		IClientService $httpClientService,
70		IDiscoveryService $discoveryService,
71		ILogger $logger,
72		IJobList $jobList,
73		ICloudFederationProviderManager $federationProviderManager,
74		ICloudFederationFactory $cloudFederationFactory,
75		IEventDispatcher $eventDispatcher
76	) {
77		$this->addressHandler = $addressHandler;
78		$this->httpClientService = $httpClientService;
79		$this->discoveryService = $discoveryService;
80		$this->jobList = $jobList;
81		$this->logger = $logger;
82		$this->federationProviderManager = $federationProviderManager;
83		$this->cloudFederationFactory = $cloudFederationFactory;
84		$this->eventDispatcher = $eventDispatcher;
85	}
86
87	/**
88	 * send server-to-server share to remote server
89	 *
90	 * @param string $token
91	 * @param string $shareWith
92	 * @param string $name
93	 * @param string $remoteId
94	 * @param string $owner
95	 * @param string $ownerFederatedId
96	 * @param string $sharedBy
97	 * @param string $sharedByFederatedId
98	 * @param int $shareType (can be a remote user or group share)
99	 * @return bool
100	 * @throws \OCP\HintException
101	 * @throws \OC\ServerNotAvailableException
102	 */
103	public function sendRemoteShare($token, $shareWith, $name, $remoteId, $owner, $ownerFederatedId, $sharedBy, $sharedByFederatedId, $shareType) {
104		[$user, $remote] = $this->addressHandler->splitUserRemote($shareWith);
105
106		if ($user && $remote) {
107			$local = $this->addressHandler->generateRemoteURL();
108
109			$fields = [
110				'shareWith' => $user,
111				'token' => $token,
112				'name' => $name,
113				'remoteId' => $remoteId,
114				'owner' => $owner,
115				'ownerFederatedId' => $ownerFederatedId,
116				'sharedBy' => $sharedBy,
117				'sharedByFederatedId' => $sharedByFederatedId,
118				'remote' => $local,
119				'shareType' => $shareType
120			];
121
122			$result = $this->tryHttpPostToShareEndpoint($remote, '', $fields);
123			$status = json_decode($result['result'], true);
124
125			$ocsStatus = isset($status['ocs']);
126			$ocsSuccess = $ocsStatus && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200);
127
128			if ($result['success'] && (!$ocsStatus || $ocsSuccess)) {
129				$event = new FederatedShareAddedEvent($remote);
130				$this->eventDispatcher->dispatchTyped($event);
131				return true;
132			} else {
133				$this->logger->info(
134					"failed sharing $name with $shareWith",
135					['app' => 'federatedfilesharing']
136				);
137			}
138		} else {
139			$this->logger->info(
140				"could not share $name, invalid contact $shareWith",
141				['app' => 'federatedfilesharing']
142			);
143		}
144
145		return false;
146	}
147
148	/**
149	 * ask owner to re-share the file with the given user
150	 *
151	 * @param string $token
152	 * @param string $id remote Id
153	 * @param string $shareId internal share Id
154	 * @param string $remote remote address of the owner
155	 * @param string $shareWith
156	 * @param int $permission
157	 * @param string $filename
158	 * @return array|false
159	 * @throws \OCP\HintException
160	 * @throws \OC\ServerNotAvailableException
161	 */
162	public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission, $filename) {
163		$fields = [
164			'shareWith' => $shareWith,
165			'token' => $token,
166			'permission' => $permission,
167			'remoteId' => $shareId,
168		];
169
170		$ocmFields = $fields;
171		$ocmFields['remoteId'] = (string)$id;
172		$ocmFields['localId'] = $shareId;
173		$ocmFields['name'] = $filename;
174
175		$ocmResult = $this->tryOCMEndPoint($remote, $ocmFields, 'reshare');
176		if (is_array($ocmResult) && isset($ocmResult['token']) && isset($ocmResult['providerId'])) {
177			return [$ocmResult['token'], $ocmResult['providerId']];
178		}
179
180		$result = $this->tryLegacyEndPoint(rtrim($remote, '/'), '/' . $id . '/reshare', $fields);
181		$status = json_decode($result['result'], true);
182
183		$httpRequestSuccessful = $result['success'];
184		$ocsCallSuccessful = $status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200;
185		$validToken = isset($status['ocs']['data']['token']) && is_string($status['ocs']['data']['token']);
186		$validRemoteId = isset($status['ocs']['data']['remoteId']);
187
188		if ($httpRequestSuccessful && $ocsCallSuccessful && $validToken && $validRemoteId) {
189			return [
190				$status['ocs']['data']['token'],
191				$status['ocs']['data']['remoteId']
192			];
193		} elseif (!$validToken) {
194			$this->logger->info(
195				"invalid or missing token requesting re-share for $filename to $remote",
196				['app' => 'federatedfilesharing']
197			);
198		} elseif (!$validRemoteId) {
199			$this->logger->info(
200				"missing remote id requesting re-share for $filename to $remote",
201				['app' => 'federatedfilesharing']
202			);
203		} else {
204			$this->logger->info(
205				"failed requesting re-share for $filename to $remote",
206				['app' => 'federatedfilesharing']
207			);
208		}
209
210		return false;
211	}
212
213	/**
214	 * send server-to-server unshare to remote server
215	 *
216	 * @param string $remote url
217	 * @param string $id share id
218	 * @param string $token
219	 * @return bool
220	 */
221	public function sendRemoteUnShare($remote, $id, $token) {
222		$this->sendUpdateToRemote($remote, $id, $token, 'unshare');
223	}
224
225	/**
226	 * send server-to-server unshare to remote server
227	 *
228	 * @param string $remote url
229	 * @param string $id share id
230	 * @param string $token
231	 * @return bool
232	 */
233	public function sendRevokeShare($remote, $id, $token) {
234		$this->sendUpdateToRemote($remote, $id, $token, 'reshare_undo');
235	}
236
237	/**
238	 * send notification to remote server if the permissions was changed
239	 *
240	 * @param string $remote
241	 * @param string $remoteId
242	 * @param string $token
243	 * @param int $permissions
244	 * @return bool
245	 */
246	public function sendPermissionChange($remote, $remoteId, $token, $permissions) {
247		$this->sendUpdateToRemote($remote, $remoteId, $token, 'permissions', ['permissions' => $permissions]);
248	}
249
250	/**
251	 * forward accept reShare to remote server
252	 *
253	 * @param string $remote
254	 * @param string $remoteId
255	 * @param string $token
256	 */
257	public function sendAcceptShare($remote, $remoteId, $token) {
258		$this->sendUpdateToRemote($remote, $remoteId, $token, 'accept');
259	}
260
261	/**
262	 * forward decline reShare to remote server
263	 *
264	 * @param string $remote
265	 * @param string $remoteId
266	 * @param string $token
267	 */
268	public function sendDeclineShare($remote, $remoteId, $token) {
269		$this->sendUpdateToRemote($remote, $remoteId, $token, 'decline');
270	}
271
272	/**
273	 * inform remote server whether server-to-server share was accepted/declined
274	 *
275	 * @param string $remote
276	 * @param string $token
277	 * @param string $remoteId Share id on the remote host
278	 * @param string $action possible actions: accept, decline, unshare, revoke, permissions
279	 * @param array $data
280	 * @param int $try
281	 * @return boolean
282	 */
283	public function sendUpdateToRemote($remote, $remoteId, $token, $action, $data = [], $try = 0) {
284		$fields = [
285			'token' => $token,
286			'remoteId' => $remoteId
287		];
288		foreach ($data as $key => $value) {
289			$fields[$key] = $value;
290		}
291
292		$result = $this->tryHttpPostToShareEndpoint(rtrim($remote, '/'), '/' . $remoteId . '/' . $action, $fields, $action);
293		$status = json_decode($result['result'], true);
294
295		if ($result['success'] &&
296			($status['ocs']['meta']['statuscode'] === 100 ||
297				$status['ocs']['meta']['statuscode'] === 200
298			)
299		) {
300			return true;
301		} elseif ($try === 0) {
302			// only add new job on first try
303			$this->jobList->add('OCA\FederatedFileSharing\BackgroundJob\RetryJob',
304				[
305					'remote' => $remote,
306					'remoteId' => $remoteId,
307					'token' => $token,
308					'action' => $action,
309					'data' => json_encode($data),
310					'try' => $try,
311					'lastRun' => $this->getTimestamp()
312				]
313			);
314		}
315
316		return false;
317	}
318
319
320	/**
321	 * return current timestamp
322	 *
323	 * @return int
324	 */
325	protected function getTimestamp() {
326		return time();
327	}
328
329	/**
330	 * try http post with the given protocol, if no protocol is given we pick
331	 * the secure one (https)
332	 *
333	 * @param string $remoteDomain
334	 * @param string $urlSuffix
335	 * @param array $fields post parameters
336	 * @param string $action define the action (possible values: share, reshare, accept, decline, unshare, revoke, permissions)
337	 * @return array
338	 * @throws \Exception
339	 */
340	protected function tryHttpPostToShareEndpoint($remoteDomain, $urlSuffix, array $fields, $action = "share") {
341		if ($this->addressHandler->urlContainProtocol($remoteDomain) === false) {
342			$remoteDomain = 'https://' . $remoteDomain;
343		}
344
345		$result = [
346			'success' => false,
347			'result' => '',
348		];
349
350		// if possible we use the new OCM API
351		$ocmResult = $this->tryOCMEndPoint($remoteDomain, $fields, $action);
352		if (is_array($ocmResult)) {
353			$result['success'] = true;
354			$result['result'] = json_encode([
355				'ocs' => ['meta' => ['statuscode' => 200]]]);
356			return $result;
357		}
358
359		return $this->tryLegacyEndPoint($remoteDomain, $urlSuffix, $fields);
360	}
361
362	/**
363	 * try old federated sharing API if the OCM api doesn't work
364	 *
365	 * @param $remoteDomain
366	 * @param $urlSuffix
367	 * @param array $fields
368	 * @return mixed
369	 * @throws \Exception
370	 */
371	protected function tryLegacyEndPoint($remoteDomain, $urlSuffix, array $fields) {
372		$result = [
373			'success' => false,
374			'result' => '',
375		];
376
377		// Fall back to old API
378		$client = $this->httpClientService->newClient();
379		$federationEndpoints = $this->discoveryService->discover($remoteDomain, 'FEDERATED_SHARING');
380		$endpoint = isset($federationEndpoints['share']) ? $federationEndpoints['share'] : '/ocs/v2.php/cloud/shares';
381		try {
382			$response = $client->post($remoteDomain . $endpoint . $urlSuffix . '?format=' . self::RESPONSE_FORMAT, [
383				'body' => $fields,
384				'timeout' => 10,
385				'connect_timeout' => 10,
386			]);
387			$result['result'] = $response->getBody();
388			$result['success'] = true;
389		} catch (\Exception $e) {
390			// if flat re-sharing is not supported by the remote server
391			// we re-throw the exception and fall back to the old behaviour.
392			// (flat re-shares has been introduced in Nextcloud 9.1)
393			if ($e->getCode() === Http::STATUS_INTERNAL_SERVER_ERROR) {
394				throw $e;
395			}
396		}
397
398		return $result;
399	}
400
401	/**
402	 * send action regarding federated sharing to the remote server using the OCM API
403	 *
404	 * @param $remoteDomain
405	 * @param $fields
406	 * @param $action
407	 *
408	 * @return array|false
409	 */
410	protected function tryOCMEndPoint($remoteDomain, $fields, $action) {
411		switch ($action) {
412			case 'share':
413				$share = $this->cloudFederationFactory->getCloudFederationShare(
414					$fields['shareWith'] . '@' . $remoteDomain,
415					$fields['name'],
416					'',
417					$fields['remoteId'],
418					$fields['ownerFederatedId'],
419					$fields['owner'],
420					$fields['sharedByFederatedId'],
421					$fields['sharedBy'],
422					$fields['token'],
423					$fields['shareType'],
424					'file'
425				);
426				return $this->federationProviderManager->sendShare($share);
427			case 'reshare':
428				// ask owner to reshare a file
429				$notification = $this->cloudFederationFactory->getCloudFederationNotification();
430				$notification->setMessage('REQUEST_RESHARE',
431					'file',
432					$fields['remoteId'],
433					[
434						'sharedSecret' => $fields['token'],
435						'shareWith' => $fields['shareWith'],
436						'senderId' => $fields['localId'],
437						'shareType' => $fields['shareType'],
438						'message' => 'Ask owner to reshare the file'
439					]
440				);
441				return $this->federationProviderManager->sendNotification($remoteDomain, $notification);
442			case 'unshare':
443				//owner unshares the file from the recipient again
444				$notification = $this->cloudFederationFactory->getCloudFederationNotification();
445				$notification->setMessage('SHARE_UNSHARED',
446					'file',
447					$fields['remoteId'],
448					[
449						'sharedSecret' => $fields['token'],
450						'messgage' => 'file is no longer shared with you'
451					]
452				);
453				return $this->federationProviderManager->sendNotification($remoteDomain, $notification);
454			case 'reshare_undo':
455				// if a reshare was unshared we send the information to the initiator/owner
456				$notification = $this->cloudFederationFactory->getCloudFederationNotification();
457				$notification->setMessage('RESHARE_UNDO',
458					'file',
459					$fields['remoteId'],
460					[
461						'sharedSecret' => $fields['token'],
462						'message' => 'reshare was revoked'
463					]
464				);
465				return $this->federationProviderManager->sendNotification($remoteDomain, $notification);
466		}
467
468		return false;
469	}
470}
471