Completed
Push — master ( 462815...ee89ff )
by Maxence
03:04
created

checkUpdateLinkFromRemoteLinkUp()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 15
rs 9.4285
c 1
b 0
f 0
cc 3
eloc 9
nc 3
nop 3
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 OCA\Circles\Api\v1\Circles;
32
use OCA\Circles\AppInfo\Application;
33
use OCA\Circles\Db\CirclesRequest;
34
use OCA\Circles\Db\FederatedLinksRequest;
35
use OCA\Circles\Exceptions\CircleTypeNotValidException;
36
use OCA\Circles\Exceptions\FederatedCircleLinkFormatException;
37
use OCA\Circles\Exceptions\FederatedCircleNotAllowedException;
38
use OCA\Circles\Exceptions\FederatedCircleStatusUpdateException;
39
use OCA\Circles\Exceptions\FederatedLinkCreationException;
40
use OCA\Circles\Exceptions\FederatedLinkUpdateException;
41
use OCA\Circles\Exceptions\FederatedRemoteCircleDoesNotExistException;
42
use OCA\Circles\Exceptions\FederatedRemoteDoesNotAllowException;
43
use OCA\Circles\Exceptions\FederatedRemoteIsDownException;
44
use OCA\Circles\Exceptions\MemberIsNotAdminException;
45
use OCA\Circles\Model\Circle;
46
use OCA\Circles\Model\FederatedLink;
47
use OCP\Http\Client\IClientService;
48
use OCP\IL10N;
49
50
class FederatedLinkService {
51
52
	/** @var string */
53
	private $userId;
54
55
	/** @var IL10N */
56
	private $l10n;
57
58
	/** @var CirclesRequest */
59
	private $circlesRequest;
60
61
	/** @var ConfigService */
62
	private $configService;
63
64
	/** @var CirclesService */
65
	private $circlesService;
66
67
	/** @var BroadcastService */
68
	private $broadcastService;
69
70
	/** @var FederatedLinksRequest */
71
	private $federatedLinksRequest;
72
73
	/** @var EventsService */
74
	private $eventsService;
75
76
	/** @var IClientService */
77
	private $clientService;
78
79
	/** @var MiscService */
80
	private $miscService;
81
82
83
	/**
84
	 * CirclesService constructor.
85
	 *
86
	 * @param $userId
87
	 * @param IL10N $l10n
88
	 * @param CirclesRequest $circlesRequest
89
	 * @param ConfigService $configService
90
	 * @param CirclesService $circlesService
91
	 * @param BroadcastService $broadcastService
92
	 * @param FederatedLinksRequest $federatedLinksRequest
93
	 * @param EventsService $eventsService
94
	 * @param IClientService $clientService
95
	 * @param MiscService $miscService
96
	 */
97
	public function __construct(
98
		$userId, IL10N $l10n, CirclesRequest $circlesRequest, ConfigService $configService,
99
		CirclesService $circlesService, BroadcastService $broadcastService,
100
		FederatedLinksRequest $federatedLinksRequest, EventsService $eventsService,
101
		IClientService $clientService, MiscService $miscService
102
	) {
103
		$this->userId = $userId;
104
		$this->l10n = $l10n;
105
		$this->circlesRequest = $circlesRequest;
106
		$this->configService = $configService;
107
		$this->circlesService = $circlesService;
108
		$this->broadcastService = $broadcastService;
109
		$this->federatedLinksRequest = $federatedLinksRequest;
110
		$this->eventsService = $eventsService;
111
112
		$this->clientService = $clientService;
113
		$this->miscService = $miscService;
114
	}
115
116
117
	/**
118
	 * linkCircle();
119
	 *
120
	 * link to a circle.
121
	 * Function will check if settings allow Federated links between circles, and the format of
122
	 * the link ($remote). If no exception, a request to the remote circle will be initiated
123
	 * using requestLinkWithCircle()
124
	 *
125
	 * $remote format: <circle_name>@<remote_host>
126
	 *
127
	 * @param string $circleUniqueId
128
	 * @param string $remote
129
	 *
130
	 * @throws Exception
131
	 * @throws FederatedCircleLinkFormatException
132
	 * @throws CircleTypeNotValidException
133
	 *
134
	 * @return FederatedLink
135
	 */
136
	public function linkCircle($circleUniqueId, $remote) {
137
138
		if (!$this->configService->isFederatedCirclesAllowed()) {
139
			throw new FederatedCircleNotAllowedException(
140
				$this->l10n->t("Federated circles are not allowed on this Nextcloud")
141
			);
142
		}
143
144
		if (strpos($remote, '@') === false) {
145
			throw new FederatedCircleLinkFormatException(
146
				$this->l10n->t("Federated link does not have a valid format")
147
			);
148
		}
149
150
		try {
151
			return $this->requestLinkWithCircle($circleUniqueId, $remote);
152
		} catch (Exception $e) {
153
			throw $e;
154
		}
155
	}
156
157
158
	/**
159
	 * linkStatus()
160
	 *
161
	 * Update the status of a link.
162
	 * Function will check if user can edit the status, will update it and send the update to
163
	 * remote
164
	 *
165
	 * @param int $linkId
166
	 * @param int $status
167
	 *
168
	 * @throws Exception
169
	 * @throws FederatedCircleLinkFormatException
170
	 * @throws CircleTypeNotValidException
171
	 * @throws MemberIsNotAdminException
172
	 *
173
	 * @return FederatedLink[]
174
	 */
175
	public function linkStatus($linkId, $status) {
176
177
		$status = (int)$status;
178
		$link = null;
179
		try {
180
181
			$link = $this->circlesRequest->getLinkFromId($linkId);
182
			$circle = $this->circlesRequest->getCircle($link->getCircleId(), $this->userId);
183
			$circle->hasToBeFederated();
184
			$circle->getHigherViewer()
185
				   ->hasToBeAdmin();
186
			$link->hasToBeValidStatusUpdate($status);
187
188
			if (!$this->eventOnLinkStatus($circle, $link, $status)) {
189
				return $this->circlesRequest->getLinksFromCircle($circle->getUniqueId());
190
			}
191
192
		} catch (Exception $e) {
193
			throw $e;
194
		}
195
196
		$link->setStatus($status);
197
		$link->setCircleId($circle->getUniqueId(true));
198
199
		try {
200
			$this->updateLinkRemote($link);
201
		} catch (Exception $e) {
202
			if ($status !== FederatedLink::STATUS_LINK_REMOVE) {
203
				throw $e;
204
			}
205
		}
206
207
		$this->federatedLinksRequest->update($link);
208
209
		return $this->circlesRequest->getLinksFromCircle($circle->getUniqueId());
210
	}
211
212
213
	/**
214
	 * eventOnLinkStatus();
215
	 *
216
	 * Called by linkStatus() to manage events when status is changing.
217
	 * If status does not need update, returns false;
218
	 *
219
	 * @param Circle $circle
220
	 * @param FederatedLink $link
221
	 * @param $status
222
	 *
223
	 * @return bool
224
	 */
225
	private function eventOnLinkStatus(Circle $circle, FederatedLink $link, $status) {
226
		if ($link->getStatus() === $status) {
227
			return false;
228
		}
229
230
		if ($status === FederatedLink::STATUS_LINK_REMOVE) {
231
			$this->eventsService->onLinkRemove($circle, $link);
232
		}
233
234
		if ($status === FederatedLink::STATUS_LINK_UP) {
235
			$this->eventsService->onLinkRequestAccepting($circle, $link);
236
			$this->eventsService->onLinkUp($circle, $link);
237
		}
238
239
		return true;
240
	}
241
242
243
	/**
244
	 * requestLinkWithCircle()
245
	 *
246
	 * Using CircleId, function will get more infos from the database.
247
	 * Will check if author is at least admin and initiate a FederatedLink, save it
248
	 * in the database and send a request to the remote circle using requestLink()
249
	 * If any issue, entry is removed from the database.
250
	 *
251
	 * @param string $circleUniqueId
252
	 * @param string $remote
253
	 *
254
	 * @return FederatedLink
255
	 * @throws Exception
256
	 */
257
	private function requestLinkWithCircle($circleUniqueId, $remote) {
258
259
		$link = null;
260
		try {
261
			list($remoteCircle, $remoteAddress) = explode('@', $remote, 2);
262
263
			$circle = $this->circlesService->detailsCircle($circleUniqueId);
264
			$circle->getHigherViewer()
265
				   ->hasToBeAdmin();
266
			$circle->hasToBeFederated();
267
			$circle->cantBePersonal();
268
269
			$link = new FederatedLink();
270
			$link->setCircleId($circleUniqueId)
271
				 ->setLocalAddress($this->configService->getLocalAddress())
272
				 ->setAddress($remoteAddress)
273
				 ->setRemoteCircleName($remoteCircle)
274
				 ->setStatus(FederatedLink::STATUS_LINK_SETUP)
275
				 ->generateToken();
276
277
			$this->federatedLinksRequest->create($link);
278
			$this->requestLink($circle, $link);
279
280
		} catch (Exception $e) {
281
			if ($link !== null) {
282
				$this->federatedLinksRequest->delete($link);
283
			}
284
			throw $e;
285
		}
286
287
		return $link;
288
	}
289
290
291
	/**
292
	 * @param string $remote
293
	 *
294
	 * @return string
295
	 */
296
	private function generateLinkRemoteURL($remote) {
297
		return $this->configService->generateRemoteHost($remote) . Application::REMOTE_URL_LINK;
298
	}
299
300
301
	/**
302
	 * requestLink()
303
	 *
304
	 *
305
	 * @param Circle $circle
306
	 * @param FederatedLink $link
307
	 *
308
	 * @return boolean
309
	 * @throws Exception
310
	 */
311
	private function requestLink(Circle $circle, FederatedLink &$link) {
312
		$args = [
313
			'apiVersion' => Circles::version(),
314
			'token'      => $link->getToken(true),
315
			'uniqueId'   => $circle->getUniqueId(true),
316
			'sourceName' => $circle->getName(),
317
			'linkTo'     => $link->getRemoteCircleName(),
318
			'address'    => $link->getLocalAddress()
319
		];
320
321
		$client = $this->clientService->newClient();
322
323
		try {
324
			$request = $client->put(
325
				$this->generateLinkRemoteURL($link->getAddress()), [
326
																	 'body'            => $args,
327
																	 'timeout'         => 10,
328
																	 'connect_timeout' => 10,
329
																 ]
330
			);
331
332
			$result = json_decode($request->getBody(), true);
333
			if ($result === null) {
334
				throw new FederatedRemoteIsDownException(
335
					$this->l10n->t(
336
						'The remote host is down or the Circles app is not installed on it'
337
					)
338
				);
339
			}
340
341
			$this->eventOnRequestLink(
342
				$circle, $link, $result['status'],
343
				((key_exists('reason', $result)) ? $result['reason'] : '')
344
			);
345
346
			$link->setUniqueId($result['uniqueId']);
347
			$this->federatedLinksRequest->update($link);
348
349
			return true;
350
		} catch (Exception $e) {
351
			throw $e;
352
		}
353
	}
354
355
356
	/**
357
	 * eventOnRequestLink();
358
	 *
359
	 * Called by requestLink() will update status and event
360
	 * Will also manage errors returned by the remote link
361
	 *
362
	 * @param Circle $circle
363
	 * @param FederatedLink $link
364
	 * @param $status
365
	 * @param $reason
366
	 *
367
	 * @throws Exception
368
	 */
369
	private function eventOnRequestLink(Circle $circle, FederatedLink $link, $status, $reason) {
370
371
		try {
372
			if ($status === FederatedLink::STATUS_LINK_UP) {
373
				$link->setStatus(FederatedLink::STATUS_LINK_UP);
374
				$this->eventsService->onLinkUp($circle, $link);
375
			} else if ($status === FederatedLink::STATUS_LINK_REQUESTED) {
376
				$link->setStatus(FederatedLink::STATUS_REQUEST_SENT);
377
				$this->eventsService->onLinkRequestSent($circle, $link);
378
			} else {
379
				$this->parseRequestLinkError($reason);
380
			}
381
		} catch (Exception $e) {
382
			throw $e;
383
		}
384
	}
385
386
387
	/**
388
	 * parseRequestLinkError();
389
	 *
390
	 * Will parse the error reason returned by requestLink() and throw an Exception
391
	 *
392
	 * @param $reason
393
	 *
394
	 * @throws Exception
395
	 * @throws FederatedRemoteCircleDoesNotExistException
396
	 * @throws FederatedRemoteDoesNotAllowException
397
	 */
398
	private function parseRequestLinkError($reason) {
399
400
		if ($reason === 'federated_not_allowed') {
401
			throw new FederatedRemoteDoesNotAllowException(
402
				$this->l10n->t('Federated circles are not allowed on the remote Nextcloud')
403
			);
404
		}
405
406
		if ($reason === 'circle_links_disable') {
407
			throw new FederatedRemoteDoesNotAllowException(
408
				$this->l10n->t('The remote circle does not accept federated links')
409
			);
410
		}
411
412
		if ($reason === 'duplicate_unique_id') {
413
			throw new FederatedRemoteDoesNotAllowException(
414
				$this->l10n->t('It seems that you are trying to link a circle to itself')
415
			);
416
		}
417
418
		if ($reason === 'duplicate_link') {
419
			throw new FederatedRemoteDoesNotAllowException(
420
				$this->l10n->t('This link exists already')
421
			);
422
		}
423
424
		if ($reason === 'circle_does_not_exist') {
425
			throw new FederatedRemoteCircleDoesNotExistException(
426
				$this->l10n->t('The requested remote circle does not exist')
427
			);
428
		}
429
430
		throw new Exception($reason);
431
	}
432
433
434
	/**
435
	 * @param string $token
436
	 * @param string $uniqueId
437
	 * @param int $status
438
	 *
439
	 * @return FederatedLink
440
	 * @throws Exception
441
	 */
442
	public function updateLinkFromRemote($token, $uniqueId, $status) {
443
		try {
444
			$link = $this->circlesRequest->getLinkFromToken($token, $uniqueId);
445
			$circle = $this->circlesRequest->forceGetCircle($link->getCircleId());
446
			$circle->hasToBeFederated();
447
448
			$this->checkUpdateLinkFromRemote($status);
449
			$this->checkUpdateLinkFromRemoteLinkUp($circle, $link, $status);
450
			$this->checkUpdateLinkFromRemoteLinkRemove($circle, $link, $status);
451
452
			if ($link->getStatus() !== $status) {
453
				$this->federatedLinksRequest->update($link);
454
			}
455
456
			return $link;
457
		} catch (Exception $e) {
458
			throw $e;
459
		}
460
	}
461
462
	/**
463
	 * checkUpdateLinkFromRemote();
464
	 *
465
	 * will throw exception is the status sent by remote is not correct
466
	 *
467
	 * @param int $status
468
	 *
469
	 * @throws FederatedCircleStatusUpdateException
470
	 */
471
	private function checkUpdateLinkFromRemote($status) {
472
		$status = (int)$status;
473
		if ($status !== FederatedLink::STATUS_LINK_UP
474
			&& $status !== FederatedLink::STATUS_LINK_REMOVE
475
		) {
476
			throw new FederatedCircleStatusUpdateException(
477
				$this->l10n->t('Cannot proceed with this status update')
478
			);
479
		}
480
	}
481
482
483
	/**
484
	 * checkUpdateLinkFromRemoteLinkUp()
485
	 *
486
	 * in case of a request of status update from remote for a link up, we check the current
487
	 * status of the link locally.
488
	 *
489
	 * @param Circle $circle
490
	 * @param FederatedLink $link
491
	 * @param int $status
492
	 *
493
	 * @throws FederatedCircleStatusUpdateException
494
	 */
495
	private function checkUpdateLinkFromRemoteLinkUp(Circle $circle, FederatedLink $link, $status) {
496
		if ((int)$status !== FederatedLink::STATUS_LINK_UP) {
497
			return;
498
		}
499
500
		if ($link->getStatus() !== FederatedLink::STATUS_REQUEST_SENT) {
501
			throw new FederatedCircleStatusUpdateException(
502
				$this->l10n->t('Cannot proceed with this status update')
503
			);
504
		}
505
506
		$this->eventsService->onLinkRequestAccepted($circle, $link);
507
		$this->eventsService->onLinkUp($circle, $link);
508
		$link->setStatus($status);
509
	}
510
511
512
	/**
513
	 * checkUpdateLinkFromRemoteLinkRemove();
514
	 *
515
	 * in case of a request of status update from remote for a link down, we check the current
516
	 * status of the link locally
517
	 *
518
	 * @param Circle $circle
519
	 * @param FederatedLink $link
520
	 * @param int $status
521
	 *
522
	 * @throws FederatedCircleStatusUpdateException
523
	 */
524
	private function checkUpdateLinkFromRemoteLinkRemove(
525
		Circle $circle, FederatedLink $link, $status
526
	) {
527
		if ((int)$status !== FederatedLink::STATUS_LINK_REMOVE) {
528
			return;
529
		}
530
531 View Code Duplication
		if ($link->getStatus() === FederatedLink::STATUS_REQUEST_SENT) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
532
			$link->setStatus(FederatedLink::STATUS_REQUEST_DECLINED);
533
			$this->eventsService->onLinkRequestRejected($circle, $link);
534
535
			return;
536
		}
537
538 View Code Duplication
		if ($link->getStatus() === FederatedLink::STATUS_LINK_REQUESTED) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
539
			$link->setStatus(FederatedLink::STATUS_LINK_REMOVE);
540
			$this->eventsService->onLinkRequestCanceled($circle, $link);
541
542
			return;
543
		}
544
545 View Code Duplication
		if ($link->getStatus() > FederatedLink::STATUS_LINK_DOWN) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
546
			$link->setStatus(FederatedLink::STATUS_LINK_DOWN);
547
			$this->eventsService->onLinkDown($circle, $link);
548
549
			return;
550
		}
551
552
		throw new FederatedCircleStatusUpdateException(
553
			$this->l10n->t('Cannot proceed with this status update')
554
		);
555
	}
556
557
558
	/**
559
	 * updateLinkRemote()
560
	 *
561
	 * Send a request to the remote of the link to update its status.
562
	 *
563
	 * @param FederatedLink $link
564
	 *
565
	 * @return bool
566
	 * @throws Exception
567
	 */
568
	public function updateLinkRemote(FederatedLink &$link) {
569
		$args = [
570
			'apiVersion' => Circles::version(),
571
			'token'      => $link->getToken(true),
572
			'uniqueId'   => $link->getCircleId(true),
573
			'status'     => $link->getStatus()
574
		];
575
576
		$client = $this->clientService->newClient();
577
		try {
578
			$request = $client->post(
579
				$this->generateLinkRemoteURL($link->getAddress()), [
580
																	 'body'            => $args,
581
																	 'timeout'         => 10,
582
																	 'connect_timeout' => 10,
583
																 ]
584
			);
585
586
			$result = json_decode($request->getBody(), true);
587
			if ($result['status'] === -1) {
588
				throw new FederatedLinkUpdateException($result['reason']);
589
			}
590
591
			return true;
592
		} catch (Exception $e) {
593
			throw $e;
594
		}
595
	}
596
597
598
	/**
599
	 * Create a new link into database and assign the correct status.
600
	 *
601
	 * @param Circle $circle
602
	 * @param FederatedLink $link
603
	 *
604
	 * @throws Exception
605
	 */
606
	public function initiateLink(Circle $circle, FederatedLink &$link) {
607
608
		try {
609
			$this->checkLinkRequestValidity($circle, $link);
610
			$link->setCircleId($circle->getUniqueId());
611
612
			if ($circle->getSetting('allow_links_auto') === 'true') {
613
				$link->setStatus(FederatedLink::STATUS_LINK_UP);
614
				$this->eventsService->onLinkUp($circle, $link);
615
			} else {
616
				$link->setStatus(FederatedLink::STATUS_LINK_REQUESTED);
617
				$this->eventsService->onLinkRequestReceived($circle, $link);
618
			}
619
620
			$this->federatedLinksRequest->create($link);
621
		} catch (Exception $e) {
622
			throw $e;
623
		}
624
	}
625
626
627
	/**
628
	 * @param Circle $circle
629
	 * @param FederatedLink $link
630
	 *
631
	 * @throws FederatedLinkCreationException
632
	 */
633
	private function checkLinkRequestValidity($circle, $link) {
634
		if ($circle->getUniqueId(true) === $link->getUniqueId(true)) {
635
			throw new FederatedLinkCreationException('duplicate_unique_id');
636
		}
637
638
		if ($this->getLink($circle->getUniqueId(), $link->getUniqueId(true)) !== null) {
639
			throw new FederatedLinkCreationException('duplicate_link');
640
		}
641
642
		if ($circle->getSetting('allow_links') !== 'true') {
643
			throw new FederatedLinkCreationException('circle_links_disable');
644
		}
645
	}
646
647
648
	/**
649
	 * @param string $circleUniqueId
650
	 * @param string $uniqueId
651
	 *
652
	 * @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...
653
	 */
654
	public function getLink($circleUniqueId, $uniqueId) {
655
		return $this->federatedLinksRequest->getFromUniqueId($circleUniqueId, $uniqueId);
656
	}
657
658
659
	/**
660
	 * @param string $circleUniqueId
661
	 *
662
	 * @return FederatedLink[]
663
	 */
664
	public function getLinksFromCircle($circleUniqueId) {
665
		return $this->federatedLinksRequest->getLinked($circleUniqueId);
666
	}
667
668
}