Completed
Push — federated-circles ( 2a3998...f4a386 )
by Maxence
02:39
created

FederatedService::sendRemoteShare()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 25
rs 8.8571
cc 3
eloc 16
nc 3
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\CirclesMapper;
33
use OCA\Circles\Db\FederatedLinksRequest;
34
use OCA\Circles\Db\MembersMapper;
35
use OCA\Circles\Exceptions\FederatedCircleLinkFormatException;
36
use OCA\Circles\Exceptions\FederatedCircleNotAllowedException;
37
use OCA\Circles\Exceptions\CircleTypeNotValid;
38
use OCA\Circles\Exceptions\FederatedLinkCreationIssue;
39
use OCA\Circles\Exceptions\FederatedRemoteCircleDoesNotExistException;
40
use OCA\Circles\Exceptions\FederatedRemoteDoesNotAllowException;
41
use OCA\Circles\Exceptions\MemberIsNotAdminException;
42
use OCA\Circles\Model\Circle;
43
use OCA\Circles\Model\FederatedLink;
44
use OCA\Circles\Model\SharingFrame;
45
use OCP\IL10N;
46
47
class FederatedService {
48
49
50
	/** @var string */
51
	private $userId;
52
53
	/** @var IL10N */
54
	private $l10n;
55
56
	/** @var ConfigService */
57
	private $configService;
58
59
	/** @var CirclesService */
60
	private $circlesService;
61
62
	/** @var FederatedLinksRequest */
63
	private $federatedLinksRequest;
64
65
	/** @var CirclesMapper */
66
	private $dbCircles;
67
68
	/** @var MembersMapper */
69
	private $dbMembers;
70
71
	/** @var ClientService */
72
	private $clientService;
73
74
	/** @var MiscService */
75
	private $miscService;
76
77
78
	/**
79
	 * CirclesService constructor.
80
	 *
81
	 * @param $userId
82
	 * @param IL10N $l10n
83
	 * @param ConfigService $configService
84
	 * @param DatabaseService $databaseService
85
	 * @param CirclesService $circlesService
86
	 * @param FederatedLinksRequest $federatedLinksRequest
87
	 * @param string $serverHost
88
	 * @param ClientService $clientService
89
	 * @param MiscService $miscService
90
	 */
91
	public function __construct(
92
		$userId,
93
		IL10N $l10n,
94
		ConfigService $configService,
95
		DatabaseService $databaseService,
96
		CirclesService $circlesService,
97
		FederatedLinksRequest $federatedLinksRequest,
98
		string $serverHost,
99
		ClientService $clientService,
100
		MiscService $miscService
101
	) {
102
		$this->userId = $userId;
103
		$this->l10n = $l10n;
104
		$this->configService = $configService;
105
		$this->circlesService = $circlesService;
106
		$this->federatedLinksRequest = $federatedLinksRequest;
107
		$this->serverHost = $serverHost;
0 ignored issues
show
Bug introduced by
The property serverHost does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
108
		$this->clientService = $clientService;
109
110
		$this->miscService = $miscService;
111
112
		$this->dbCircles = $databaseService->getCirclesMapper();
113
		$this->dbMembers = $databaseService->getMembersMapper();
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 int $circleId
128
	 * @param string $remote
129
	 *
130
	 * @throws Exception
131
	 * @throws FederatedCircleLinkFormatException
132
	 * @throws CircleTypeNotValid
133
	 * @throws MemberIsNotAdminException
134
	 *
135
	 * @return FederatedLink
136
	 */
137
	public function linkCircle($circleId, $remote) {
138
139
		if (!$this->configService->isFederatedAllowed()) {
140
			throw new FederatedCircleNotAllowedException(
141
				$this->l10n->t("Federated circles are not allowed on this Nextcloud")
142
			);
143
		}
144
145
		if (strpos($remote, '@') === false) {
146
			throw new FederatedCircleLinkFormatException(
147
				$this->l10n->t("Federated link does not have a valid format")
148
			);
149
		}
150
151
		try {
152
			return $this->requestLinkWithCircle($circleId, $remote);
153
		} catch (Exception $e) {
154
			throw $e;
155
		}
156
	}
157
158
159
	/**
160
	 * requestLinkWithCircle()
161
	 *
162
	 * Using CircleId, function will get more infos from the database.
163
	 * Will check if author is not admin and initiate a FederatedLink, save it
164
	 * in the database and send a request to the remote circle using requestLink()
165
	 * If any issue, entry is removed from the database.
166
	 *
167
	 * @param $circleId
168
	 * @param $remote
169
	 *
170
	 * @return FederatedLink
171
	 * @throws Exception
172
	 */
173
	private function requestLinkWithCircle($circleId, $remote) {
174
175
		$link = null;
176
		try {
177
			list($remoteCircle, $remoteAddress) = explode('@', $remote, 2);
178
179
			$circle = $this->circlesService->detailsCircle($circleId);
180
			$circle->getUser()
181
				   ->hasToBeAdmin();
182
			$circle->cantBePersonal();
183
184
			$link = new FederatedLink();
185
			$link->setCircleId($circleId)
186
				 ->setLocalAddress($this->serverHost)
187
				 ->setAddress($remoteAddress)
188
				 ->setRemoteCircleName($remoteCircle)
189
				 ->setStatus(FederatedLink::STATUS_LINK_SETUP)
190
				 ->generateToken();
191
192
			$this->federatedLinksRequest->create($link);
193
			$this->requestLink($circle, $link);
194
195
		} catch (Exception $e) {
196
			$this->federatedLinksRequest->delete($link);
0 ignored issues
show
Bug introduced by
It seems like $link defined by null on line 175 can be null; however, OCA\Circles\Db\FederatedLinksRequest::delete() 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...
197
			throw $e;
198
		}
199
200
		return $link;
201
	}
202
203
204
	/**
205
	 * @param string $remote
206
	 *
207
	 * @return string
208
	 */
209 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...
210
		if (strpos($remote, 'http') !== 0) {
211
			$remote = 'https://' . $remote;
212
		}
213
214
		return rtrim($remote, '/') . '/index.php/apps/circles/circles/link/';
215
	}
216
217
	/**
218
	 * @param string $remote
219
	 *
220
	 * @return string
221
	 */
222 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...
223
		if (strpos($remote, 'http') !== 0) {
224
			$remote = 'http://' . $remote;
225
		}
226
227
		return rtrim($remote, '/') . '/index.php/apps/circles/circles/payload/';
228
	}
229
230
231
	/**
232
	 * requestLink()
233
	 *
234
	 *
235
	 * @param Circle $circle
236
	 * @param FederatedLink $link
237
	 *
238
	 * @return int
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean?

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

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
403
					$this->generatePayloadDeliveryURL($link->getAddress()), [
404
																			  'body'            => $args,
405
																			  'timeout'         => 10,
406
																			  'connect_timeout' => 10,
407
																		  ]
408
				);
409
			} catch (Exception $e) {
410
				throw $e;
411
			}
412
		}
413
	}
414
415
}