Completed
Push — master ( 7d64a0...795920 )
by Maxence
03:11
created

FederatedService::parsingRequestLinkResult()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 28
rs 8.439
cc 5
eloc 14
nc 5
nop 1
1
<?php
2
/**
3
 * Circles - Bring cloud-users closer together.
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the COPYING file.
7
 *
8
 * @author Maxence Lange <[email protected]>
9
 * @copyright 2017
10
 * @license GNU AGPL version 3 or any later version
11
 *
12
 * This program is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License as
14
 * published by the Free Software Foundation, either version 3 of the
15
 * License, or (at your option) any later version.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License
23
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
24
 *
25
 */
26
27
namespace OCA\Circles\Service;
28
29
30
use Exception;
31
use OC\Http\Client\ClientService;
32
use OCA\Circles\Db\CirclesRequest;
33
use OCA\Circles\Db\FederatedLinksRequest;
34
use OCA\Circles\Exceptions\FederatedCircleLinkFormatException;
35
use OCA\Circles\Exceptions\FederatedCircleNotAllowedException;
36
use OCA\Circles\Exceptions\CircleTypeNotValid;
37
use OCA\Circles\Exceptions\FederatedLinkDoesNotExistException;
38
use OCA\Circles\Exceptions\FederatedRemoteCircleDoesNotExistException;
39
use OCA\Circles\Exceptions\FederatedRemoteDoesNotAllowException;
40
use OCA\Circles\Exceptions\FrameAlreadyExistException;
41
use OCA\Circles\Exceptions\LinkCreationException;
42
use OCA\Circles\Exceptions\MemberIsNotAdminException;
43
use OCA\Circles\Model\Circle;
44
use OCA\Circles\Model\FederatedLink;
45
use OCA\Circles\Model\SharingFrame;
46
use OCP\IL10N;
47
48
class FederatedService {
49
50
51
	/** @var string */
52
	private $userId;
53
54
	/** @var IL10N */
55
	private $l10n;
56
57
	/** @var CirclesRequest */
58
	private $circlesRequest;
59
60
	/** @var ConfigService */
61
	private $configService;
62
63
	/** @var CirclesService */
64
	private $circlesService;
65
66
	/** @var BroadcastService */
67
	private $broadcastService;
68
69
	/** @var FederatedLinksRequest */
70
	private $federatedLinksRequest;
71
72
	/** @var string */
73
	private $serverHost;
74
75
	/** @var ClientService */
76
	private $clientService;
77
78
	/** @var MiscService */
79
	private $miscService;
80
81
82
	/**
83
	 * CirclesService constructor.
84
	 *
85
	 * @param $userId
86
	 * @param IL10N $l10n
87
	 * @param CirclesRequest $circlesRequest
88
	 * @param ConfigService $configService
89
	 * @param CirclesService $circlesService
90
	 * @param BroadcastService $broadcastService
91
	 * @param FederatedLinksRequest $federatedLinksRequest
92
	 * @param string $serverHost
93
	 * @param ClientService $clientService
94
	 * @param MiscService $miscService
95
	 */
96
	public function __construct(
97
		$userId,
98
		IL10N $l10n,
99
		CirclesRequest $circlesRequest,
100
		ConfigService $configService,
101
		CirclesService $circlesService,
102
		BroadcastService $broadcastService,
103
		FederatedLinksRequest $federatedLinksRequest,
104
		$serverHost,
105
		ClientService $clientService,
106
		MiscService $miscService
107
	) {
108
		$this->userId = $userId;
109
		$this->l10n = $l10n;
110
		$this->circlesRequest = $circlesRequest;
111
		$this->configService = $configService;
112
		$this->circlesService = $circlesService;
113
		$this->broadcastService = $broadcastService;
114
		$this->federatedLinksRequest = $federatedLinksRequest;
115
		$this->serverHost = (string)$serverHost;
116
		$this->clientService = $clientService;
117
		$this->miscService = $miscService;
118
	}
119
120
121
	/**
122
	 * linkCircle()
123
	 *
124
	 * link to a circle.
125
	 * Function will check if settings allow Federated links between circles, and the format of
126
	 * the link ($remote). If no exception, a request to the remote circle will be initiated
127
	 * using requestLinkWithCircle()
128
	 *
129
	 * $remote format: <circle_name>@<remote_host>
130
	 *
131
	 * @param int $circleId
132
	 * @param string $remote
133
	 *
134
	 * @throws Exception
135
	 * @throws FederatedCircleLinkFormatException
136
	 * @throws CircleTypeNotValid
137
	 * @throws MemberIsNotAdminException
138
	 *
139
	 * @return FederatedLink
140
	 */
141
	public function linkCircle($circleId, $remote) {
142
143
		if (!$this->configService->isFederatedAllowed()) {
144
			throw new FederatedCircleNotAllowedException(
145
				$this->l10n->t("Federated circles are not allowed on this Nextcloud")
146
			);
147
		}
148
149
		if (strpos($remote, '@') === false) {
150
			throw new FederatedCircleLinkFormatException(
151
				$this->l10n->t("Federated link does not have a valid format")
152
			);
153
		}
154
155
		try {
156
			return $this->requestLinkWithCircle($circleId, $remote);
157
		} catch (Exception $e) {
158
			throw $e;
159
		}
160
	}
161
162
163
	/**
164
	 * requestLinkWithCircle()
165
	 *
166
	 * Using CircleId, function will get more infos from the database.
167
	 * Will check if author is not admin and initiate a FederatedLink, save it
168
	 * in the database and send a request to the remote circle using requestLink()
169
	 * If any issue, entry is removed from the database.
170
	 *
171
	 * @param integer $circleId
172
	 * @param string $remote
173
	 *
174
	 * @return FederatedLink
175
	 * @throws Exception
176
	 */
177
	private function requestLinkWithCircle($circleId, $remote) {
178
179
		$link = null;
180
		try {
181
			list($remoteCircle, $remoteAddress) = explode('@', $remote, 2);
182
183
			$circle = $this->circlesService->detailsCircle($circleId);
184
			$circle->getUser()
185
				   ->hasToBeAdmin();
186
			$circle->cantBePersonal();
187
188
			$link = new FederatedLink();
189
			$link->setCircleId($circleId)
190
				 ->setLocalAddress($this->serverHost)
191
				 ->setAddress($remoteAddress)
192
				 ->setRemoteCircleName($remoteCircle)
193
				 ->setStatus(FederatedLink::STATUS_LINK_SETUP)
194
				 ->generateToken();
195
196
			$this->federatedLinksRequest->create($link);
197
			$this->requestLink($circle, $link);
198
199
		} catch (Exception $e) {
200
			if ($link !== null) {
201
				$this->federatedLinksRequest->delete($link);
202
			}
203
			throw $e;
204
		}
205
206
		return $link;
207
	}
208
209
210
	/**
211
	 * @param string $remote
212
	 *
213
	 * @return string
214
	 */
215 View Code Duplication
	private function generateLinkRemoteURL($remote) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
216
		if (strpos($remote, 'https') !== 0) {
217
			$remote = 'https://' . $remote;
218
		}
219
220
		return rtrim($remote, '/') . '/index.php/apps/circles/v1/circles/link/';
221
	}
222
223
224
	/**
225
	 * @param string $remote
226
	 *
227
	 * @return string
228
	 */
229 View Code Duplication
	private function generatePayloadDeliveryURL($remote) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
230
		if (strpos($remote, 'https') !== 0) {
231
			$remote = 'https://' . $remote;
232
		}
233
234
		return rtrim($remote, '/') . '/index.php/apps/circles/v1/circles/payload/';
235
	}
236
237
238
	/**
239
	 * requestLink()
240
	 *
241
	 *
242
	 * @param Circle $circle
243
	 * @param FederatedLink $link
244
	 *
245
	 * @return boolean
246
	 * @throws Exception
247
	 */
248
	private function requestLink(Circle $circle, FederatedLink & $link) {
249
		$args = [
250
			'token'      => $link->getToken(),
251
			'uniqueId'   => $circle->getUniqueId(),
252
			'sourceName' => $circle->getName(),
253
			'linkTo'     => $link->getRemoteCircleName(),
254
			'address'    => $link->getLocalAddress()
255
		];
256
257
		$client = $this->clientService->newClient();
258
259
		try {
260
			$request = $client->post(
261
				$this->generateLinkRemoteURL($link->getAddress()), [
262
																	 'body'            => $args,
263
																	 'timeout'         => 10,
264
																	 'connect_timeout' => 10,
265
																 ]
266
			);
267
268
			$result = json_decode($request->getBody(), true);
269
270
			$link->setStatus($result['status']);
271
			if (!$link->isValid()) {
272
				$this->parsingRequestLinkResult($result);
273
			}
274
275
			$link->setUniqueId($result['uniqueId']);
276
			$this->federatedLinksRequest->update($link);
277
278
			return true;
279
		} catch (Exception $e) {
280
			throw $e;
281
		}
282
	}
283
284
285
	private function parsingRequestLinkResult($result) {
286
287
		if ($result['reason'] === 'federated_not_allowed') {
288
			throw new FederatedRemoteDoesNotAllowException(
289
				$this->l10n->t('Federated circles are not allowed on the remote Nextcloud')
290
			);
291
		}
292
293
		if ($result['reason'] === 'duplicate_unique_id') {
294
			throw new FederatedRemoteDoesNotAllowException(
295
				$this->l10n->t('It seems that you are trying to link a circle to itself')
296
			);
297
		}
298
299
		if ($result['reason'] === 'duplicate_link') {
300
			throw new FederatedRemoteDoesNotAllowException(
301
				$this->l10n->t('This link exists already')
302
			);
303
		}
304
305
		if ($result['reason'] === 'circle_does_not_exist') {
306
			throw new FederatedRemoteCircleDoesNotExistException(
307
				$this->l10n->t('The requested remote circle does not exist')
308
			);
309
		}
310
311
		throw new Exception($result['reason']);
312
	}
313
314
315
	/**
316
	 * Create a new link into database and assign the correct status.
317
	 *
318
	 * @param Circle $circle
319
	 * @param FederatedLink $link
320
	 *
321
	 * @throws Exception
322
	 */
323
	public function initiateLink(Circle $circle, FederatedLink & $link) {
324
325
		try {
326
			$this->checkLinkRequestValidity($circle, $link);
327
			$link->setCircleId($circle->getId());
328
329
			if ($circle->getType() === Circle::CIRCLES_PUBLIC) {
330
				$link->setStatus(FederatedLink::STATUS_LINK_UP);
331
			} else {
332
				$link->setStatus(FederatedLink::STATUS_REQUEST_SENT);
333
			}
334
335
			$this->federatedLinksRequest->create($link);
336
		} catch (Exception $e) {
337
			throw $e;
338
		}
339
	}
340
341
342
	/**
343
	 * @param Circle $circle
344
	 * @param FederatedLink $link
345
	 *
346
	 * @throws LinkCreationException
347
	 */
348
	private function checkLinkRequestValidity($circle, $link) {
349
350
		if ($circle === null) {
351
			throw new LinkCreationException('circle_does_not_exist');
352
		}
353
354
		if ($circle->getUniqueId() === $link->getUniqueId()) {
355
			throw new LinkCreationException('duplicate_unique_id');
356
		}
357
358
		if ($this->getLink($circle->getId(), $link->getUniqueId()) !== null) {
359
			throw new LinkCreationException('duplicate_link');
360
		}
361
	}
362
363
364
	/**
365
	 * @param string $token
366
	 * @param string $uniqueId
367
	 * @param SharingFrame $frame
368
	 *
369
	 * @return bool
370
	 * @throws Exception
371
	 */
372
	public function receiveFrame($token, $uniqueId, SharingFrame & $frame) {
373
374
		$link = $this->circlesRequest->getLinkFromToken((string)$token, (string)$uniqueId);
375
		if ($link === null) {
376
			throw new FederatedLinkDoesNotExistException('unknown_link');
377
		}
378
379
		if ($this->circlesRequest->getFrame($link->getCircleId(), $frame->getUniqueId())) {
380
			$this->miscService->log("Frame already exist");
381
			throw new FrameAlreadyExistException('shares_is_already_known');
382
		}
383
384
		$circle = $this->circlesRequest->getDetails($link->getCircleId());
385
		if ($circle === null) {
386
			throw new Exception('unknown_circle');
387
		}
388
389
		$frame->setCircleId($link->getCircleId());
390
		$frame->setCircleName($circle->getName());
391
392
		$this->circlesRequest->saveFrame($frame);
393
		$this->broadcastService->broadcastFrame($frame->getHeader('broadcast'), $frame);
394
395
		return true;
396
	}
397
398
	/**
399
	 * @param integer $circleId
400
	 * @param string $uniqueId
401
	 *
402
	 * @return FederatedLink
0 ignored issues
show
Documentation introduced by
Should the return type not be FederatedLink|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
403
	 */
404
	public function getLink($circleId, $uniqueId) {
405
		return $this->federatedLinksRequest->getFromUniqueId($circleId, $uniqueId);
406
	}
407
408
409
	/**
410
	 * @param integer $circleId
411
	 *
412
	 * @return FederatedLink[]
413
	 */
414
	public function getLinks($circleId) {
415
		return $this->federatedLinksRequest->getLinked($circleId);
416
	}
417
418
419
	/**
420
	 * @param int $circleId
421
	 * @param string $uniqueId
422
	 *
423
	 * @return bool
424
	 * @throws Exception
425
	 */
426
	public function initiateRemoteShare($circleId, $uniqueId) {
427
		$args = [
428
			'circleId' => (int)$circleId,
429
			'uniqueId' => (string)$uniqueId
430
		];
431
432
		$client = $this->clientService->newClient();
433
		try {
434
			$request = $client->post(
435
				$this->generatePayloadDeliveryURL($this->serverHost), [
436
																		'body'            => $args,
437
																		'timeout'         => 10,
438
																		'connect_timeout' => 10,
439
																	]
440
			);
441
442
			$result = json_decode($request->getBody(), true);
443
			$this->miscService->log(
444
				"initiateRemoteShare result: " . $uniqueId . '  ----  ' . var_export($result, true)
445
			);
446
447
			return true;
448
		} catch (Exception $e) {
449
			throw $e;
450
		}
451
	}
452
453
454
	/**
455
	 * @param SharingFrame $frame
456
	 *
457
	 * @throws Exception
458
	 */
459
	public function sendRemoteShare(SharingFrame $frame) {
460
461
		$circle = $this->circlesRequest->getDetails($frame->getCircleId());
462
		if ($circle === null) {
463
			throw new Exception('unknown_circle');
464
		}
465
466
		$links = $this->getLinks($frame->getCircleId());
467
		foreach ($links AS $link) {
468
469
			$args = [
470
				'token'    => $link->getToken(),
471
				'uniqueId' => $circle->getUniqueId(),
472
				'item'     => json_encode($frame)
473
			];
474
475
			$client = $this->clientService->newClient();
476
			try {
477
				$client->put(
478
					$this->generatePayloadDeliveryURL($link->getAddress()), [
479
																			  'body'            => $args,
480
																			  'timeout'         => 10,
481
																			  'connect_timeout' => 10,
482
																		  ]
483
				);
484
			} catch (Exception $e) {
485
				throw $e;
486
			}
487
		}
488
	}
489
490
491
	/**
492
	 * generateHeaders()
493
	 *
494
	 * Generate new headers for the current Payload, and save them in the SharingFrame.
495
	 *
496
	 * @param SharingFrame $frame
497
	 */
498
	public function updateFrameWithCloudId(SharingFrame $frame) {
499
		$frame->setCloudId($this->serverHost);
500
		$this->circlesRequest->updateFrame($frame);
501
	}
502
503
}