Completed
Pull Request — master (#362)
by Maxence
02:14
created

GSUpstreamService::synchronizeCircles()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 3
nc 3
nop 1
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\Request;
39
use daita\MySmallPhpTools\Model\SimpleDataStore;
40
use daita\MySmallPhpTools\Traits\TArrayTools;
41
use daita\MySmallPhpTools\Traits\TRequest;
42
use Exception;
43
use OCA\Circles\Db\CirclesRequest;
44
use OCA\Circles\Db\GSEventsRequest;
45
use OCA\Circles\Db\MembersRequest;
46
use OCA\Circles\Exceptions\GlobalScaleEventException;
47
use OCA\Circles\Exceptions\GSStatusException;
48
use OCA\Circles\Exceptions\JsonException;
49
use OCA\Circles\Exceptions\ModelException;
50
use OCA\Circles\GlobalScale\CircleStatus;
51
use OCA\Circles\Model\Circle;
52
use OCA\Circles\Model\GlobalScale\GSEvent;
53
use OCA\Circles\Model\GlobalScale\GSWrapper;
54
use OCP\IURLGenerator;
55
56
57
/**
58
 * Class GSUpstreamService
59
 *
60
 * @package OCA\Circles\Service
61
 */
62
class GSUpstreamService {
63
64
65
	use TRequest;
66
	use TArrayTools;
67
68
69
	/** @var string */
70
	private $userId = '';
71
72
	/** @var IURLGenerator */
73
	private $urlGenerator;
74
75
	/** @var GSEventsRequest */
76
	private $gsEventsRequest;
77
78
	/** @var CirclesRequest */
79
	private $circlesRequest;
80
81
	/** @var MembersRequest */
82
	private $membersRequest;
83
84
	/** @var GlobalScaleService */
85
	private $globalScaleService;
86
87
	/** @var ConfigService */
88
	private $configService;
89
90
	/** @var MiscService */
91
	private $miscService;
92
93
94
	/**
95
	 * GSUpstreamService constructor.
96
	 *
97
	 * @param $userId
98
	 * @param IURLGenerator $urlGenerator
99
	 * @param GSEventsRequest $gsEventsRequest
100
	 * @param CirclesRequest $circlesRequest
101
	 * @param MembersRequest $membersRequest
102
	 * @param GlobalScaleService $globalScaleService
103
	 * @param ConfigService $configService
104
	 * @param MiscService $miscService
105
	 */
106
	public function __construct(
107
		$userId,
108
		IURLGenerator $urlGenerator,
109
		GSEventsRequest $gsEventsRequest,
110
		CirclesRequest $circlesRequest,
111
		MembersRequest $membersRequest,
112
		GlobalScaleService $globalScaleService,
113
		ConfigService $configService,
114
		MiscService $miscService
115
	) {
116
		$this->userId = $userId;
117
		$this->urlGenerator = $urlGenerator;
118
		$this->gsEventsRequest = $gsEventsRequest;
119
		$this->circlesRequest = $circlesRequest;
120
		$this->membersRequest = $membersRequest;
121
		$this->globalScaleService = $globalScaleService;
122
		$this->configService = $configService;
123
		$this->miscService = $miscService;
124
	}
125
126
127
	/**
128
	 * @param GSEvent $event
129
	 *
130
	 * @throws Exception
131
	 */
132
	public function newEvent(GSEvent $event) {
133
		try {
134
			$gs = $this->globalScaleService->getGlobalScaleEvent($event);
135
136
			$this->fillEvent($event);
137
			if ($this->isLocalEvent($event)) {
138
				$gs->verify($event, true);
139
				$gs->manage($event);
140
141
				$this->globalScaleService->asyncBroadcast($event);
142
			} else {
143
				$gs->verify($event); // needed ? as we check event on the 'master' of the circle
144
				$this->confirmEvent($event);
145
				$gs->manage($event);
146
			}
147
		} catch (Exception $e) {
148
			$this->miscService->log(
149
				get_class($e) . ' on new event: ' . $e->getMessage() . ' - ' . json_encode($event), 1
150
			);
151
			throw $e;
152
		}
153
	}
154
155
156
	/**
157
	 * @param GSWrapper $wrapper
158
	 * @param string $protocol
159
	 */
160
	public function broadcastWrapper(GSWrapper $wrapper, string $protocol): void {
161
		$status = GSWrapper::STATUS_FAILED;
162
163
		try {
164
			$this->broadcastEvent($wrapper->getEvent(), $wrapper->getInstance(), $protocol);
165
			$status = GSWrapper::STATUS_DONE;
166
		} 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...
167
		}
168
169
		if ($wrapper->getSeverity() === GSEvent::SEVERITY_HIGH) {
170
			$wrapper->setStatus($status);
171
		} else {
172
			$wrapper->setStatus(GSWrapper::STATUS_OVER);
173
		}
174
175
		$this->gsEventsRequest->update($wrapper);
176
	}
177
178
179
	/**
180
	 * @param GSEvent $event
181
	 * @param string $instance
182
	 * @param string $protocol
183
	 *
184
	 * @throws RequestContentException
185
	 * @throws RequestNetworkException
186
	 * @throws RequestResultNotJsonException
187
	 * @throws RequestResultSizeException
188
	 * @throws RequestServerException
189
	 */
190
	public function broadcastEvent(GSEvent $event, string $instance, string $protocol = ''): void {
191
		$this->signEvent($event);
192
193
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.broadcast');
194
		$request = new Request($path, Request::TYPE_POST);
195
196
		$protocols = ['https', 'http'];
197
		if ($protocol !== '') {
198
			$protocols = [$protocol];
199
		}
200
201
		$request->setProtocols($protocols);
0 ignored issues
show
Bug introduced by
The method setProtocols() does not exist on daita\MySmallPhpTools\Model\Request. Did you maybe mean setProtocol()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
202
		$request->setDataSerialize($event);
203
204
		$request->setAddress($instance);
205
206
		$data = $this->retrieveJson($request);
207
		$event->setResult(new SimpleDataStore($this->getArray('result', $data, [])));
208
	}
209
210
211
	/**
212
	 * @param GSEvent $event
213
	 *
214
	 * @throws RequestContentException
215
	 * @throws RequestNetworkException
216
	 * @throws RequestResultSizeException
217
	 * @throws RequestServerException
218
	 * @throws RequestResultNotJsonException
219
	 * @throws GlobalScaleEventException
220
	 */
221
	public function confirmEvent(GSEvent $event): void {
222
		$this->signEvent($event);
223
224
		$circle = $event->getCircle();
225
		$owner = $circle->getOwner();
226
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.event');
227
228
		$request = new Request($path, Request::TYPE_POST);
229
		if ($this->get('REQUEST_SCHEME', $_SERVER) !== '') {
230
			$request->setProtocols([$_SERVER['REQUEST_SCHEME']]);
0 ignored issues
show
Bug introduced by
The method setProtocols() does not exist on daita\MySmallPhpTools\Model\Request. Did you maybe mean setProtocol()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
231
		} else {
232
			$request->setProtocols(['https', 'http']);
0 ignored issues
show
Bug introduced by
The method setProtocols() does not exist on daita\MySmallPhpTools\Model\Request. Did you maybe mean setProtocol()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
233
		}
234
		$request->setAddressFromUrl($owner->getInstance());
235
		$request->setDataSerialize($event);
236
237
		$result = $this->retrieveJson($request);
238
		$this->miscService->log('result ' . json_encode($result));
239
		if ($this->getInt('status', $result) === 0) {
240
			throw new GlobalScaleEventException($this->get('error', $result));
241
		}
242
	}
243
244
245
	/**
246
	 * @param GSEvent $event
247
	 *
248
	 * @throws GSStatusException
249
	 */
250
	private function fillEvent(GSEvent $event): void {
251
		if (!$this->configService->getGSStatus(ConfigService::GS_ENABLED)) {
252
			return;
253
		}
254
255
		$event->setSource($this->configService->getLocalCloudId());
256
	}
257
258
259
	/**
260
	 * @param GSEvent $event
261
	 */
262
	private function signEvent(GSEvent $event) {
263
		$event->setKey($this->globalScaleService->getKey());
264
	}
265
266
267
	/**
268
	 * We check that the event can be managed/checked locally or if the owner of the circle belongs to
269
	 * an other instance of Nextcloud
270
	 *
271
	 * @param GSEvent $event
272
	 *asyncBroadcast
273
	 *
274
	 * @return bool
275
	 */
276
	private function isLocalEvent(GSEvent $event): bool {
277
		if ($event->isLocal()) {
278
			return true;
279
		}
280
281
		$circle = $event->getCircle();
282
		$owner = $circle->getOwner();
283
		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...
284
			|| in_array(
285
				$owner->getInstance(), $this->configService->getTrustedDomains()
286
			)) {
287
			return true;
288
		}
289
290
		return false;
291
	}
292
293
294
	/**
295
	 * @param string $token
296
	 *
297
	 * @return GSWrapper[]
298
	 * @throws JsonException
299
	 * @throws ModelException
300
	 */
301
	public function getEventsByToken(string $token): array {
302
		return $this->gsEventsRequest->getByToken($token);
303
	}
304
305
306
	/**
307
	 * Deprecated ?
308
	 * should be used to manage results from events, like sending mails on user creation
309
	 *
310
	 * @param string $token
311
	 */
312
	public function manageResults(string $token): void {
313
		try {
314
			$wrappers = $this->gsEventsRequest->getByToken($token);
315
		} catch (JsonException | ModelException $e) {
316
			return;
317
		}
318
319
		$event = null;
320
		$events = [];
321
		foreach ($wrappers as $wrapper) {
322
			if ($wrapper->getStatus() !== GSWrapper::STATUS_DONE) {
323
				return;
324
			}
325
326
			$events[$wrapper->getInstance()] = $event = $wrapper->getEvent();
327
		}
328
329
		if ($event === null) {
330
			return;
331
		}
332
333
		try {
334
			$gs = $this->globalScaleService->getGlobalScaleEvent($event);
335
			$gs->result($events);
336
		} catch (GlobalScaleEventException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
337
		}
338
	}
339
340
341
	/**
342
	 * @throws GSStatusException
343
	 */
344
	public function synchronize() {
345
		$this->configService->getGSStatus();
346
347
		$sync = $this->getCirclesToSync();
348
		$this->synchronizeCircles($sync);
349
		$this->removeDeprecatedCircles();
350
351
		$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...
352
	}
353
354
355
	/**
356
	 * @param array $circles
357
	 *
358
	 * @throws GSStatusException
359
	 */
360
	public function synchronizeCircles(array $circles): void {
361
		$event = new GSEvent(GSEvent::GLOBAL_SYNC, true);
362
		$event->setSource($this->configService->getLocalCloudId());
363
		$event->setData(new SimpleDataStore($circles));
364
365
		foreach ($this->globalScaleService->getInstances() as $instance) {
366
			try {
367
				$this->broadcastEvent($event, $instance);
368
			} 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...
369
			}
370
		}
371
	}
372
373
374
	/**
375
	 * @return Circle[]
376
	 */
377
	private function getCirclesToSync(): array {
378
		$circles = $this->circlesRequest->forceGetCircles();
379
380
		$sync = [];
381
		foreach ($circles as $circle) {
382
			if ($circle->getOwner()
383
					   ->getInstance() !== ''
384
				|| $circle->getType() === Circle::CIRCLES_PERSONAL) {
385
				continue;
386
			}
387
388
			$members = $this->membersRequest->forceGetMembers($circle->getUniqueId());
389
			$circle->setMembers($members);
390
391
			$sync[] = $circle;
392
		}
393
394
		return $sync;
395
	}
396
397
398
	/**
399
	 *
400
	 */
401
	private function removeDeprecatedCircles() {
402
		$knownCircles = $this->circlesRequest->forceGetCircles();
403
		foreach ($knownCircles as $knownItem) {
404
			if ($knownItem->getOwner()
405
						  ->getInstance() === '') {
406
				continue;
407
			}
408
409
			try {
410
				$this->checkCircle($knownItem);
411
			} catch (GSStatusException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
412
			}
413
		}
414
	}
415
416
417
	/**
418
	 * @param Circle $circle
419
	 *
420
	 * @throws GSStatusException
421
	 */
422
	private function checkCircle(Circle $circle): void {
423
		$status = $this->confirmCircleStatus($circle);
424
425
		if (!$status) {
426
			$this->circlesRequest->destroyCircle($circle->getUniqueId());
427
			$this->membersRequest->removeAllFromCircle($circle->getUniqueId());
428
		}
429
	}
430
431
432
	/**
433
	 * @param Circle $circle
434
	 *
435
	 * @return bool
436
	 * @throws GSStatusException
437
	 */
438
	public function confirmCircleStatus(Circle $circle): bool {
439
		$event = new GSEvent(GSEvent::CIRCLE_STATUS, true);
440
		$event->setSource($this->configService->getLocalCloudId());
441
		$event->setCircle($circle);
442
443
		$this->signEvent($event);
444
445
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.status');
446
		$request = new Request($path, Request::TYPE_POST);
447
448
		$request->setProtocols(['https', 'http']);
0 ignored issues
show
Bug introduced by
The method setProtocols() does not exist on daita\MySmallPhpTools\Model\Request. Did you maybe mean setProtocol()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
449
		$request->setDataSerialize($event);
450
451
		$requestIssue = false;
452
		$notFound = false;
453
		$foundWithNoOwner = false;
454
		foreach ($this->globalScaleService->getInstances() as $instance) {
455
			$request->setAddress($instance);
456
457
			try {
458
				$result = $this->retrieveJson($request);
459
				$this->miscService->log('result: ' . json_encode($result));
460
				if ($this->getInt('status', $result, 0) !== 1) {
461
					throw new RequestContentException('result status is not good');
462
				}
463
464
				$status = $this->getInt('success.data.status', $result);
465
466
				// if error, we assume the circle might still exist.
467
				if ($status === CircleStatus::STATUS_ERROR) {
468
					return true;
469
				}
470
471
				if ($status === CircleStatus::STATUS_OK) {
472
					return true;
473
				}
474
475
				// TODO: check the data.supposedOwner entry.
476
				if ($status === CircleStatus::STATUS_NOT_OWNER) {
477
					$foundWithNoOwner = true;
478
				}
479
480
				if ($status === CircleStatus::STATUS_NOT_FOUND) {
481
					$notFound = true;
482
				}
483
484
			} catch (RequestContentException
485
			| RequestNetworkException
486
			| RequestResultNotJsonException
487
			| RequestResultSizeException
488
			| RequestServerException $e) {
489
				$requestIssue = true;
490
				// TODO: log instances that have network issue, after too many tries (7d), remove this circle.
491
				continue;
492
			}
493
		}
494
495
		// if no request issue, we can imagine that the instance that owns the circle is down.
496
		// We'll wait for more information (cf request exceptions management);
497
		if ($requestIssue) {
498
			return true;
499
		}
500
501
		// circle were not found in any other instances, we can easily says that the circle does not exists anymore
502
		if ($notFound && !$foundWithNoOwner) {
503
			return false;
504
		}
505
506
		// circle were found everywhere but with no owner on every instance. we need to assign a new owner.
507
		// This should be done by checking admin rights. if no admin rights, let's assume that circle should be removed.
508
		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...
509
			// TODO: assign a new owner and check that when changing owner, we do check that the destination instance is updated FOR SURE!
510
			return true;
511
		}
512
513
		// some instances returned notFound, some returned circle with no owner. let's assume the circle is deprecated.
514
		return false;
515
	}
516
517
	/**
518
	 * @throws GSStatusException
519
	 */
520
	public function syncEvents() {
521
522
	}
523
524
	/**
525
	 *
526
	 */
527
	private function removeDeprecatedEvents() {
528
//		$this->deprecatedEvents();
529
530
	}
531
532
533
}
534
535