Completed
Pull Request — master (#516)
by Maxence
02:25
created

GSUpstreamService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0
cc 1
nc 1
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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