Completed
Push — master ( 93c19b...93c19b )
by Maxence
07:41 queued 04:53
created

FederatedService::linkCircle()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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