Completed
Push — master ( 90f02e...53fcb4 )
by Maxence
02:59
created

FederatedService::initiateRemoteShare()   B

Complexity

Conditions 2
Paths 4

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 27
rs 8.8571
cc 2
eloc 18
nc 4
nop 2
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\Api\v1\Circles;
33
use OCA\Circles\Db\CirclesRequest;
34
use OCA\Circles\Db\FederatedLinksRequest;
35
use OCA\Circles\Exceptions\FederatedCircleLinkFormatException;
36
use OCA\Circles\Exceptions\FederatedCircleNotAllowedException;
37
use OCA\Circles\Exceptions\CircleTypeNotValid;
38
use OCA\Circles\Exceptions\FederatedLinkDoesNotExistException;
39
use OCA\Circles\Exceptions\FederatedRemoteCircleDoesNotExistException;
40
use OCA\Circles\Exceptions\FederatedRemoteDoesNotAllowException;
41
use OCA\Circles\Exceptions\FrameAlreadyExistException;
42
use OCA\Circles\Exceptions\LinkCreationException;
43
use OCA\Circles\Exceptions\MemberIsNotAdminException;
44
use OCA\Circles\Model\Circle;
45
use OCA\Circles\Model\FederatedLink;
46
use OCA\Circles\Model\SharingFrame;
47
use OCP\IL10N;
48
49
class FederatedService {
50
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 string */
74
	private $serverHost;
75
76
	/** @var ClientService */
77
	private $clientService;
78
79
	/** @var MiscService */
80
	private $miscService;
81
82
	/** @var bool */
83
	private $localTest = false;
84
85
	/**
86
	 * CirclesService constructor.
87
	 *
88
	 * @param $userId
89
	 * @param IL10N $l10n
90
	 * @param CirclesRequest $circlesRequest
91
	 * @param ConfigService $configService
92
	 * @param CirclesService $circlesService
93
	 * @param BroadcastService $broadcastService
94
	 * @param FederatedLinksRequest $federatedLinksRequest
95
	 * @param string $serverHost
96
	 * @param ClientService $clientService
97
	 * @param MiscService $miscService
98
	 */
99
	public function __construct(
100
		$userId,
101
		IL10N $l10n,
102
		CirclesRequest $circlesRequest,
103
		ConfigService $configService,
104
		CirclesService $circlesService,
105
		BroadcastService $broadcastService,
106
		FederatedLinksRequest $federatedLinksRequest,
107
		$serverHost,
108
		ClientService $clientService,
109
		MiscService $miscService
110
	) {
111
		$this->userId = $userId;
112
		$this->l10n = $l10n;
113
		$this->circlesRequest = $circlesRequest;
114
		$this->configService = $configService;
115
		$this->circlesService = $circlesService;
116
		$this->broadcastService = $broadcastService;
117
		$this->federatedLinksRequest = $federatedLinksRequest;
118
		$this->serverHost = (string)$serverHost;
119
		$this->clientService = $clientService;
120
		$this->miscService = $miscService;
121
	}
122
123
124
	/**
125
	 * linkCircle()
126
	 *
127
	 * link to a circle.
128
	 * Function will check if settings allow Federated links between circles, and the format of
129
	 * the link ($remote). If no exception, a request to the remote circle will be initiated
130
	 * using requestLinkWithCircle()
131
	 *
132
	 * $remote format: <circle_name>@<remote_host>
133
	 *
134
	 * @param int $circleId
135
	 * @param string $remote
136
	 *
137
	 * @throws Exception
138
	 * @throws FederatedCircleLinkFormatException
139
	 * @throws CircleTypeNotValid
140
	 * @throws MemberIsNotAdminException
141
	 *
142
	 * @return FederatedLink
143
	 */
144
	public function linkCircle($circleId, $remote) {
145
146
		if (!$this->configService->isFederatedAllowed()) {
147
			throw new FederatedCircleNotAllowedException(
148
				$this->l10n->t("Federated circles are not allowed on this Nextcloud")
149
			);
150
		}
151
152
		if (strpos($remote, '@') === false) {
153
			throw new FederatedCircleLinkFormatException(
154
				$this->l10n->t("Federated link does not have a valid format")
155
			);
156
		}
157
158
		try {
159
			return $this->requestLinkWithCircle($circleId, $remote);
160
		} catch (Exception $e) {
161
			throw $e;
162
		}
163
	}
164
165
166
	/**
167
	 * linkStatus()
168
	 *
169
	 * Update the status of a link.
170
	 * Function will check if user can edit the status, will update it and send the update to
171
	 * remote
172
	 *
173
	 * @param int $linkId
174
	 * @param int $status
175
	 *
176
	 * @throws Exception
177
	 * @throws FederatedCircleLinkFormatException
178
	 * @throws CircleTypeNotValid
179
	 * @throws MemberIsNotAdminException
180
	 *
181
	 * @return FederatedLink[]
182
	 */
183
	public function linkStatus($linkId, $status) {
184
185
		if (!$this->configService->isFederatedAllowed()) {
186
			throw new FederatedCircleNotAllowedException(
187
				$this->l10n->t("Federated circles are not allowed on this Nextcloud")
188
			);
189
		}
190
191
		$link = null;
192
		try {
193
194
			$link = $this->circlesRequest->getLinkFromId($linkId);
195
			$circle = $this->circlesRequest->getCircleFromId($link->getCircleId(), $this->userId);
196
			$circle->hasToBeFederated();
197
198
			$link->hasToBeValidStatusUpdate($status);
199
			$link->setStatus($status);
200
			$this->federatedLinksRequest->update($link);
0 ignored issues
show
Bug introduced by
It seems like $link defined by $this->circlesRequest->getLinkFromId($linkId) on line 194 can be null; however, OCA\Circles\Db\FederatedLinksRequest::update() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
201
202
			return $this->circlesRequest->getLinksFromCircle($circle->getId());
203
		} catch (Exception $e) {
204
			throw $e;
205
		}
206
207
	}
208
209
210
	/**
211
	 * requestLinkWithCircle()
212
	 *
213
	 * Using CircleId, function will get more infos from the database.
214
	 * Will check if author is at least admin and initiate a FederatedLink, save it
215
	 * in the database and send a request to the remote circle using requestLink()
216
	 * If any issue, entry is removed from the database.
217
	 *
218
	 * @param integer $circleId
219
	 * @param string $remote
220
	 *
221
	 * @return FederatedLink
222
	 * @throws Exception
223
	 */
224
	private function requestLinkWithCircle($circleId, $remote) {
225
226
		$link = null;
227
		try {
228
			list($remoteCircle, $remoteAddress) = explode('@', $remote, 2);
229
230
			$circle = $this->circlesService->detailsCircle($circleId);
231
			$circle->getUser()
232
				   ->hasToBeAdmin();
233
			$circle->hasToBeFederated();
234
			$circle->cantBePersonal();
235
236
			$link = new FederatedLink();
237
			$link->setCircleId($circleId)
238
				 ->setLocalAddress($this->serverHost)
239
				 ->setAddress($remoteAddress)
240
				 ->setRemoteCircleName($remoteCircle)
241
				 ->setStatus(FederatedLink::STATUS_LINK_SETUP)
242
				 ->generateToken();
243
244
			$this->federatedLinksRequest->create($link);
245
			$this->requestLink($circle, $link);
246
247
		} catch (Exception $e) {
248
			if ($link !== null) {
249
				$this->federatedLinksRequest->delete($link);
250
			}
251
			throw $e;
252
		}
253
254
		return $link;
255
	}
256
257
258
	/**
259
	 * @param string $remote
260
	 *
261
	 * @return string
262
	 */
263 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...
264
		if ($this->localTest === false && strpos($remote, 'https') !== 0) {
265
			$remote = 'https://' . $remote;
266
		}
267
268
		return rtrim($remote, '/') . '/index.php/apps/circles/v1/circles/link/';
269
	}
270
271
272
	/**
273
	 * @param string $remote
274
	 *
275
	 * @return string
276
	 */
277 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...
278
		if ($this->localTest === false && strpos($remote, 'https') !== 0) {
279
			$remote = 'https://' . $remote;
280
		}
281
282
		return rtrim($remote, '/') . '/index.php/apps/circles/v1/circles/payload/';
283
	}
284
285
286
	public function allowNonSSLLink() {
287
		$this->localTest = true;
288
	}
289
290
291
	/**
292
	 * requestLink()
293
	 *
294
	 *
295
	 * @param Circle $circle
296
	 * @param FederatedLink $link
297
	 *
298
	 * @return boolean
299
	 * @throws Exception
300
	 */
301
	private function requestLink(Circle $circle, FederatedLink & $link) {
302
		$args = [
303
			'apiVersion' => Circles::API_VERSION,
304
			'token'       => $link->getToken(),
305
			'uniqueId'    => $circle->getUniqueId(),
306
			'sourceName'  => $circle->getName(),
307
			'linkTo'      => $link->getRemoteCircleName(),
308
			'address'     => $link->getLocalAddress()
309
		];
310
311
		$client = $this->clientService->newClient();
312
313
		try {
314
			$request = $client->post(
315
				$this->generateLinkRemoteURL($link->getAddress()), [
316
																	 'body'            => $args,
317
																	 'timeout'         => 10,
318
																	 'connect_timeout' => 10,
319
																 ]
320
			);
321
322
			$result = json_decode($request->getBody(), true);
323
324
			if ($result['status'] === FederatedLink::STATUS_LINK_UP) {
325
				$link->setStatus(FederatedLink::STATUS_LINK_UP);
326
			} else if ($result['status'] === FederatedLink::STATUS_LINK_REQUESTED) {
327
				$link->setStatus(FederatedLink::STATUS_REQUEST_SENT);
328
			} else {
329
				$this->parsingRequestLinkResult($result);
330
			}
331
332
			$link->setUniqueId($result['uniqueId']);
333
			$this->federatedLinksRequest->uniqueness($link);
334
			$this->federatedLinksRequest->update($link);
335
336
			return true;
337
		} catch (Exception $e) {
338
			throw $e;
339
		}
340
	}
341
342
343
	private function parsingRequestLinkResult($result) {
344
345
		if ($result['reason'] === 'federated_not_allowed') {
346
			throw new FederatedRemoteDoesNotAllowException(
347
				$this->l10n->t('Federated circles are not allowed on the remote Nextcloud')
348
			);
349
		}
350
351
		if ($result['reason'] === 'circle_links_disable') {
352
			throw new FederatedRemoteDoesNotAllowException(
353
				$this->l10n->t('The remote circle does not accept Federated Links')
354
			);
355
		}
356
357
		if ($result['reason'] === 'duplicate_unique_id') {
358
			throw new FederatedRemoteDoesNotAllowException(
359
				$this->l10n->t('It seems that you are trying to link a circle to itself')
360
			);
361
		}
362
363
		if ($result['reason'] === 'duplicate_link') {
364
			throw new FederatedRemoteDoesNotAllowException(
365
				$this->l10n->t('This link exists already')
366
			);
367
		}
368
369
		if ($result['reason'] === 'circle_does_not_exist') {
370
			throw new FederatedRemoteCircleDoesNotExistException(
371
				$this->l10n->t('The requested remote circle does not exist')
372
			);
373
		}
374
375
		throw new Exception($result['reason']);
376
	}
377
378
379
	/**
380
	 * Create a new link into database and assign the correct status.
381
	 *
382
	 * @param Circle $circle
383
	 * @param FederatedLink $link
384
	 *
385
	 * @throws Exception
386
	 */
387
	public function initiateLink(Circle $circle, FederatedLink & $link) {
388
389
		try {
390
			$this->checkLinkRequestValidity($circle, $link);
391
			$link->setCircleId($circle->getId());
392
393
			if ($circle->getSetting('allow_links_auto') === 'true') {
394
				$link->setStatus(FederatedLink::STATUS_LINK_UP);
395
			} else {
396
				$link->setStatus(FederatedLink::STATUS_LINK_REQUESTED);
397
			}
398
399
			$this->federatedLinksRequest->create($link);
400
		} catch (Exception $e) {
401
			throw $e;
402
		}
403
	}
404
405
406
	/**
407
	 * @param Circle $circle
408
	 * @param FederatedLink $link
409
	 *
410
	 * @throws LinkCreationException
411
	 */
412
	private function checkLinkRequestValidity($circle, $link) {
413
		if ($circle->getUniqueId() === $link->getUniqueId()) {
414
			throw new LinkCreationException('duplicate_unique_id');
415
		}
416
417
		if ($this->getLink($circle->getId(), $link->getUniqueId()) !== null) {
418
			throw new LinkCreationException('duplicate_link');
419
		}
420
421
		if ($circle->getSetting('allow_links') !== 'true') {
422
			throw new LinkCreationException('circle_links_disable');
423
		}
424
	}
425
426
427
	/**
428
	 * @param string $token
429
	 * @param string $uniqueId
430
	 * @param SharingFrame $frame
431
	 *
432
	 * @return bool
433
	 * @throws Exception
434
	 */
435
	public function receiveFrame($token, $uniqueId, SharingFrame & $frame) {
436
437
		$link = $this->circlesRequest->getLinkFromToken((string)$token, (string)$uniqueId);
438
		if ($link === null) {
439
			throw new FederatedLinkDoesNotExistException('unknown_link');
440
		}
441
442
		if ($this->circlesRequest->getFrame($link->getCircleId(), $frame->getUniqueId())) {
443
			$this->miscService->log("Frame already exist");
444
			throw new FrameAlreadyExistException('shares_is_already_known');
445
		}
446
447
		$circle = $this->circlesRequest->getCircleFromId($link->getCircleId());
448
		if ($circle === null) {
449
			throw new Exception('unknown_circle');
450
		}
451
452
		$frame->setCircleId($link->getCircleId());
453
		$frame->setCircleName($circle->getName());
454
455
		$this->circlesRequest->saveFrame($frame);
456
		$this->broadcastService->broadcastFrame($frame->getHeader('broadcast'), $frame);
457
458
		return true;
459
	}
460
461
	/**
462
	 * @param integer $circleId
463
	 * @param string $uniqueId
464
	 *
465
	 * @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...
466
	 */
467
	public function getLink($circleId, $uniqueId) {
468
		return $this->federatedLinksRequest->getFromUniqueId($circleId, $uniqueId);
469
	}
470
471
472
	/**
473
	 * @param integer $circleId
474
	 *
475
	 * @return FederatedLink[]
476
	 */
477
	public function getLinks($circleId) {
478
		return $this->federatedLinksRequest->getLinked($circleId);
479
	}
480
481
482
	/**
483
	 * @param int $circleId
484
	 * @param string $uniqueId
485
	 *
486
	 * @return bool
487
	 * @throws Exception
488
	 */
489
	public function initiateRemoteShare($circleId, $uniqueId) {
490
		$args = [
491
			'apiVersion' => Circles::API_VERSION,
492
			'circleId'   => (int)$circleId,
493
			'uniqueId'   => (string)$uniqueId
494
		];
495
496
		$client = $this->clientService->newClient();
497
		try {
498
			$request = $client->post(
499
				$this->generatePayloadDeliveryURL($this->serverHost), [
500
																		'body'            => $args,
501
																		'timeout'         => 10,
502
																		'connect_timeout' => 10,
503
																	]
504
			);
505
506
			$result = json_decode($request->getBody(), true);
507
			$this->miscService->log(
508
				"initiateRemoteShare result: " . $uniqueId . '  ----  ' . var_export($result, true)
509
			);
510
511
			return true;
512
		} catch (Exception $e) {
513
			throw $e;
514
		}
515
	}
516
517
518
	/**
519
	 * @param SharingFrame $frame
520
	 *
521
	 * @throws Exception
522
	 */
523
	public function sendRemoteShare(SharingFrame $frame) {
524
525
		$circle = $this->circlesRequest->getCircleFromId($frame->getCircleId());
526
		if ($circle === null) {
527
			throw new Exception('unknown_circle');
528
		}
529
530
		$links = $this->getLinks($frame->getCircleId());
531
		foreach ($links AS $link) {
532
533
			$args = [
534
				'apiVersion' => Circles::API_VERSION,
535
				'token'      => $link->getToken(),
536
				'uniqueId'   => $circle->getUniqueId(),
537
				'item'       => json_encode($frame)
538
			];
539
540
			$client = $this->clientService->newClient();
541
			try {
542
				$client->put(
543
					$this->generatePayloadDeliveryURL($link->getAddress()), [
544
																			  'body'            => $args,
545
																			  'timeout'         => 10,
546
																			  'connect_timeout' => 10,
547
																		  ]
548
				);
549
			} catch (Exception $e) {
550
				throw $e;
551
			}
552
		}
553
	}
554
555
556
	/**
557
	 * generateHeaders()
558
	 *
559
	 * Generate new headers for the current Payload, and save them in the SharingFrame.
560
	 *
561
	 * @param SharingFrame $frame
562
	 */
563
	public function updateFrameWithCloudId(SharingFrame $frame) {
564
		$frame->setCloudId($this->serverHost);
565
		$this->circlesRequest->updateFrame($frame);
566
	}
567
568
}