Completed
Push — master ( 138f00...e3741d )
by Maxence
16s queued 10s
created

GSUpstreamService   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 446
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 0
Metric Value
wmc 52
lcom 1
cbo 17
dl 0
loc 446
rs 7.44
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A signEvent() 0 3 1
B manageResults() 0 27 6
A getEventsByToken() 0 3 1
A synchronizeCircles() 0 12 3
A __construct() 0 19 1
A newEvent() 0 26 4
A broadcastWrapper() 0 17 3
A broadcastEvent() 0 19 2
A confirmEvent() 0 35 5
A isLocalEvent() 0 14 4
A synchronize() 0 7 1
A removeDeprecatedCircles() 0 14 4
A checkCircle() 0 8 2
C confirmCircleStatus() 0 78 13
A syncEvents() 0 3 1
A removeDeprecatedEvents() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like GSUpstreamService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GSUpstreamService, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
4
/**
5
 * Circles - Bring cloud-users closer together.
6
 *
7
 * This file is licensed under the Affero General Public License version 3 or
8
 * later. See the COPYING file.
9
 *
10
 * @author Maxence Lange <[email protected]>
11
 * @copyright 2017
12
 * @license GNU AGPL version 3 or any later version
13
 *
14
 * This program is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License as
16
 * published by the Free Software Foundation, either version 3 of the
17
 * License, or (at your option) any later version.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
26
 *
27
 */
28
29
30
namespace OCA\Circles\Service;
31
32
33
use daita\MySmallPhpTools\Exceptions\RequestContentException;
34
use daita\MySmallPhpTools\Exceptions\RequestNetworkException;
35
use daita\MySmallPhpTools\Exceptions\RequestResultNotJsonException;
36
use daita\MySmallPhpTools\Exceptions\RequestResultSizeException;
37
use daita\MySmallPhpTools\Exceptions\RequestServerException;
38
use daita\MySmallPhpTools\Model\Nextcloud\NC19Request;
39
use daita\MySmallPhpTools\Model\Request;
40
use daita\MySmallPhpTools\Model\SimpleDataStore;
41
use daita\MySmallPhpTools\Traits\Nextcloud\TNC19Request;
42
use daita\MySmallPhpTools\Traits\TArrayTools;
43
use Exception;
44
use OCA\Circles\Db\CirclesRequest;
45
use OCA\Circles\Db\GSEventsRequest;
46
use OCA\Circles\Db\MembersRequest;
47
use OCA\Circles\Exceptions\GlobalScaleEventException;
48
use OCA\Circles\Exceptions\GSStatusException;
49
use OCA\Circles\Exceptions\JsonException;
50
use OCA\Circles\Exceptions\ModelException;
51
use OCA\Circles\GlobalScale\CircleStatus;
52
use OCA\Circles\Model\Circle;
53
use OCA\Circles\Model\GlobalScale\GSEvent;
54
use OCA\Circles\Model\GlobalScale\GSWrapper;
55
use OCP\IURLGenerator;
56
57
58
/**
59
 * Class GSUpstreamService
60
 *
61
 * @package OCA\Circles\Service
62
 */
63
class GSUpstreamService {
64
65
66
	use TNC19Request;
67
	use TArrayTools;
68
69
70
	/** @var string */
71
	private $userId = '';
72
73
	/** @var IURLGenerator */
74
	private $urlGenerator;
75
76
	/** @var GSEventsRequest */
77
	private $gsEventsRequest;
78
79
	/** @var CirclesRequest */
80
	private $circlesRequest;
81
82
	/** @var MembersRequest */
83
	private $membersRequest;
84
85
	/** @var GlobalScaleService */
86
	private $globalScaleService;
87
88
	/** @var ConfigService */
89
	private $configService;
90
91
	/** @var MiscService */
92
	private $miscService;
93
94
95
	/**
96
	 * GSUpstreamService constructor.
97
	 *
98
	 * @param $userId
99
	 * @param IURLGenerator $urlGenerator
100
	 * @param GSEventsRequest $gsEventsRequest
101
	 * @param CirclesRequest $circlesRequest
102
	 * @param MembersRequest $membersRequest
103
	 * @param GlobalScaleService $globalScaleService
104
	 * @param ConfigService $configService
105
	 * @param MiscService $miscService
106
	 */
107
	public function __construct(
108
		$userId,
109
		IURLGenerator $urlGenerator,
110
		GSEventsRequest $gsEventsRequest,
111
		CirclesRequest $circlesRequest,
112
		MembersRequest $membersRequest,
113
		GlobalScaleService $globalScaleService,
114
		ConfigService $configService,
115
		MiscService $miscService
116
	) {
117
		$this->userId = $userId;
118
		$this->urlGenerator = $urlGenerator;
119
		$this->gsEventsRequest = $gsEventsRequest;
120
		$this->circlesRequest = $circlesRequest;
121
		$this->membersRequest = $membersRequest;
122
		$this->globalScaleService = $globalScaleService;
123
		$this->configService = $configService;
124
		$this->miscService = $miscService;
125
	}
126
127
128
	/**
129
	 * @param GSEvent $event
130
	 *
131
	 * @return string
132
	 * @throws Exception
133
	 */
134
	public function newEvent(GSEvent $event): string {
135
		$event->setSource($this->configService->getLocalCloudId());
136
137
		try {
138
			$gs = $this->globalScaleService->getGlobalScaleEvent($event);
139
140
			if ($this->isLocalEvent($event)) {
141
				$gs->verify($event, true);
142
				if (!$event->isAsync()) {
143
					$gs->manage($event);
144
				}
145
146
				return $this->globalScaleService->asyncBroadcast($event);
147
			} else {
148
				$this->confirmEvent($event);
149
150
				return '';
151
			}
152
		} catch (Exception $e) {
153
			$this->miscService->log(
154
				get_class($e) . ' on new event: ' . $e->getMessage() . ' - ' . json_encode($event), 1
155
			);
156
			throw $e;
157
		}
158
159
	}
160
161
162
	/**
163
	 * @param GSWrapper $wrapper
164
	 * @param string $protocol
165
	 */
166
	public function broadcastWrapper(GSWrapper $wrapper, string $protocol): void {
167
		$status = GSWrapper::STATUS_FAILED;
168
169
		try {
170
			$this->broadcastEvent($wrapper->getEvent(), $wrapper->getInstance(), $protocol);
171
			$status = GSWrapper::STATUS_DONE;
172
		} catch (RequestContentException | RequestNetworkException | RequestResultSizeException | RequestServerException | RequestResultNotJsonException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
173
		}
174
175
		if ($wrapper->getSeverity() === GSEvent::SEVERITY_HIGH) {
176
			$wrapper->setStatus($status);
177
		} else {
178
			$wrapper->setStatus(GSWrapper::STATUS_OVER);
179
		}
180
181
		$this->gsEventsRequest->update($wrapper);
182
	}
183
184
185
	/**
186
	 * @param GSEvent $event
187
	 * @param string $instance
188
	 * @param string $protocol
189
	 *
190
	 * @throws RequestContentException
191
	 * @throws RequestNetworkException
192
	 * @throws RequestResultNotJsonException
193
	 * @throws RequestResultSizeException
194
	 * @throws RequestServerException
195
	 */
196
	public function broadcastEvent(GSEvent $event, string $instance, string $protocol = ''): void {
197
		$this->signEvent($event);
198
199
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.broadcast');
200
		$request = new NC19Request($path, Request::TYPE_POST);
201
		$this->configService->configureRequest($request);
202
		$protocols = ['https', 'http'];
203
		if ($protocol !== '') {
204
			$protocols = [$protocol];
205
		}
206
207
		$request->setProtocols($protocols);
208
		$request->setDataSerialize($event);
209
210
		$request->setAddress($instance);
211
212
		$data = $this->retrieveJson($request);
213
		$event->setResult(new SimpleDataStore($this->getArray('result', $data, [])));
214
	}
215
216
217
	/**
218
	 * @param GSEvent $event
219
	 *
220
	 * @throws RequestContentException
221
	 * @throws RequestNetworkException
222
	 * @throws RequestResultSizeException
223
	 * @throws RequestServerException
224
	 * @throws RequestResultNotJsonException
225
	 * @throws GlobalScaleEventException
226
	 */
227
	public function confirmEvent(GSEvent &$event): void {
228
		$this->signEvent($event);
229
230
		$circle = $event->getCircle();
231
		$owner = $circle->getOwner();
232
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.event');
233
234
		$request = new NC19Request($path, Request::TYPE_POST);
235
		$this->configService->configureRequest($request);
236
237
		if ($this->get('REQUEST_SCHEME', $_SERVER) !== '') {
238
			$request->setProtocols([$_SERVER['REQUEST_SCHEME']]);
239
		} else {
240
			$request->setProtocols(['https', 'http']);
241
		}
242
		$request->setAddressFromUrl($owner->getInstance());
243
		$request->setDataSerialize($event);
244
245
		$result = $this->retrieveJson($request);
246
		$this->miscService->log('result ' . json_encode($result), 0);
247
		if ($this->getInt('status', $result) === 0) {
248
			throw new GlobalScaleEventException($this->get('error', $result));
249
		}
250
251
		$updatedData = $this->getArray('event', $result);
252
		$this->miscService->log('updatedEvent: ' . json_encode($updatedData), 0);
253
		if (!empty($updatedData)) {
254
			$updated = new GSEvent();
255
			try {
256
				$updated->import($updatedData);
257
				$event = $updated;
258
			} catch (Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
259
			}
260
		}
261
	}
262
263
264
	/**
265
	 * @param GSEvent $event
266
	 */
267
	private function signEvent(GSEvent $event) {
268
		$event->setKey($this->globalScaleService->getKey());
269
	}
270
271
272
	/**
273
	 * We check that the event can be managed/checked locally or if the owner of the circle belongs to
274
	 * an other instance of Nextcloud
275
	 *
276
	 * @param GSEvent $event
277
	 *
278
	 * @return bool
279
	 */
280
	private function isLocalEvent(GSEvent $event): bool {
281
		if ($event->isLocal()) {
282
			return true;
283
		}
284
285
		$circle = $event->getCircle();
286
		$owner = $circle->getOwner();
287
		if ($owner->getInstance() === ''
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return $owner->getInstan...->getTrustedDomains());.
Loading history...
288
			|| in_array($owner->getInstance(), $this->configService->getTrustedDomains())) {
289
			return true;
290
		}
291
292
		return false;
293
	}
294
295
296
	/**
297
	 * @param string $token
298
	 *
299
	 * @return GSWrapper[]
300
	 * @throws JsonException
301
	 * @throws ModelException
302
	 */
303
	public function getEventsByToken(string $token): array {
304
		return $this->gsEventsRequest->getByToken($token);
305
	}
306
307
308
	/**
309
	 * should be used to manage results from events, like sending mails on user creation
310
	 *
311
	 * @param string $token
312
	 */
313
	public function manageResults(string $token): void {
314
		try {
315
			$wrappers = $this->gsEventsRequest->getByToken($token);
316
		} catch (JsonException | ModelException $e) {
317
			return;
318
		}
319
320
		$event = null;
321
		$events = [];
322
		foreach ($wrappers as $wrapper) {
323
			if ($wrapper->getStatus() !== GSWrapper::STATUS_DONE) {
324
				return;
325
			}
326
327
			$events[$wrapper->getInstance()] = $event = $wrapper->getEvent();
328
		}
329
330
		if ($event === null) {
331
			return;
332
		}
333
334
		try {
335
			$gs = $this->globalScaleService->getGlobalScaleEvent($event);
336
			$gs->result($events);
337
		} catch (GlobalScaleEventException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
338
		}
339
	}
340
341
342
	/**
343
	 * @param array $sync
344
	 *
345
	 * @throws GSStatusException
346
	 */
347
	public function synchronize(array $sync) {
348
		$this->configService->getGSStatus();
349
350
		$this->synchronizeCircles($sync);
351
		$this->removeDeprecatedCircles();
352
		$this->removeDeprecatedEvents();
0 ignored issues
show
Unused Code introduced by
The call to the method OCA\Circles\Service\GSUp...emoveDeprecatedEvents() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
353
	}
354
355
356
	/**
357
	 * @param array $circles
358
	 */
359
	public function synchronizeCircles(array $circles): void {
360
		$event = new GSEvent(GSEvent::GLOBAL_SYNC, true);
361
		$event->setSource($this->configService->getLocalCloudId());
362
		$event->setData(new SimpleDataStore($circles));
363
364
		foreach ($this->globalScaleService->getInstances() as $instance) {
365
			try {
366
				$this->broadcastEvent($event, $instance);
367
			} catch (RequestContentException | RequestNetworkException | RequestResultSizeException | RequestServerException | RequestResultNotJsonException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
368
			}
369
		}
370
	}
371
372
373
	/**
374
	 *
375
	 */
376
	private function removeDeprecatedCircles() {
377
		$knownCircles = $this->circlesRequest->forceGetCircles();
378
		foreach ($knownCircles as $knownItem) {
379
			if ($knownItem->getOwner()
380
						  ->getInstance() === '') {
381
				continue;
382
			}
383
384
			try {
385
				$this->checkCircle($knownItem);
386
			} catch (GSStatusException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
387
			}
388
		}
389
	}
390
391
392
	/**
393
	 * @param Circle $circle
394
	 *
395
	 * @throws GSStatusException
396
	 */
397
	private function checkCircle(Circle $circle): void {
398
		$status = $this->confirmCircleStatus($circle);
399
400
		if (!$status) {
401
			$this->circlesRequest->destroyCircle($circle->getUniqueId());
402
			$this->membersRequest->removeAllFromCircle($circle->getUniqueId());
403
		}
404
	}
405
406
407
	/**
408
	 * @param Circle $circle
409
	 *
410
	 * @return bool
411
	 * @throws GSStatusException
412
	 */
413
	public function confirmCircleStatus(Circle $circle): bool {
414
		$event = new GSEvent(GSEvent::CIRCLE_STATUS, true);
415
		$event->setSource($this->configService->getLocalCloudId());
416
		$event->setCircle($circle);
417
418
		$this->signEvent($event);
419
420
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.status');
421
		$request = new NC19Request($path, Request::TYPE_POST);
422
		$this->configService->configureRequest($request);
423
		$request->setProtocols(['https', 'http']);
424
		$request->setDataSerialize($event);
425
426
		$requestIssue = false;
427
		$notFound = false;
428
		$foundWithNoOwner = false;
429
		foreach ($this->globalScaleService->getInstances() as $instance) {
430
			$request->setAddress($instance);
431
432
			try {
433
				$result = $this->retrieveJson($request);
434
//				$this->miscService->log('result: ' . json_encode($result));
435
				if ($this->getInt('status', $result, 0) !== 1) {
436
					throw new RequestContentException('result status is not good');
437
				}
438
439
				$status = $this->getInt('success.data.status', $result);
440
441
				// if error, we assume the circle might still exist.
442
				if ($status === CircleStatus::STATUS_ERROR) {
443
					return true;
444
				}
445
446
				if ($status === CircleStatus::STATUS_OK) {
447
					return true;
448
				}
449
450
				// TODO: check the data.supposedOwner entry.
451
				if ($status === CircleStatus::STATUS_NOT_OWNER) {
452
					$foundWithNoOwner = true;
453
				}
454
455
				if ($status === CircleStatus::STATUS_NOT_FOUND) {
456
					$notFound = true;
457
				}
458
459
			} catch (RequestContentException
460
			| RequestNetworkException
461
			| RequestResultNotJsonException
462
			| RequestResultSizeException
463
			| RequestServerException $e) {
464
				$requestIssue = true;
465
				// TODO: log instances that have network issue, after too many tries (7d), remove this circle.
466
				continue;
467
			}
468
		}
469
470
		// if no request issue, we can imagine that the instance that owns the circle is down.
471
		// We'll wait for more information (cf request exceptions management);
472
		if ($requestIssue) {
473
			return true;
474
		}
475
476
		// circle were not found in any other instances, we can easily says that the circle does not exists anymore
477
		if ($notFound && !$foundWithNoOwner) {
478
			return false;
479
		}
480
481
		// circle were found everywhere but with no owner on every instance. we need to assign a new owner.
482
		// This should be done by checking admin rights. if no admin rights, let's assume that circle should be removed.
483
		if (!$notFound && $foundWithNoOwner) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return !$notFound && $foundWithNoOwner;.
Loading history...
484
			// TODO: assign a new owner and check that when changing owner, we do check that the destination instance is updated FOR SURE!
485
			return true;
486
		}
487
488
		// some instances returned notFound, some returned circle with no owner. let's assume the circle is deprecated.
489
		return false;
490
	}
491
492
	/**
493
	 * @throws GSStatusException
494
	 */
495
	public function syncEvents() {
496
497
	}
498
499
	/**
500
	 *
501
	 */
502
	private function removeDeprecatedEvents() {
503
//		$this->deprecatedEvents();
504
505
	}
506
507
508
}
509
510