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

GSUpstreamService::broadcastEvent()   A

Complexity

Conditions 2
Paths 2

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 2
nc 2
nop 3
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
		$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...
230
		$request->setAddressFromUrl($owner->getInstance());
231
		$request->setDataSerialize($event);
232
233
		$result = $this->retrieveJson($request);
234
		$this->miscService->log('result ' . json_encode($result));
235
		if ($this->getInt('status', $result) === 0) {
236
			throw new GlobalScaleEventException($this->get('error', $result));
237
		}
238
	}
239
240
241
	/**
242
	 * @param GSEvent $event
243
	 *
244
	 * @throws GSStatusException
245
	 */
246
	private function fillEvent(GSEvent $event): void {
247
		if (!$this->configService->getGSStatus(ConfigService::GS_ENABLED)) {
248
			return;
249
		}
250
251
		$event->setSource($this->configService->getLocalCloudId());
252
	}
253
254
255
	/**
256
	 * @param GSEvent $event
257
	 */
258
	private function signEvent(GSevent $event) {
259
		$event->setKey($this->globalScaleService->getKey());
260
	}
261
262
263
	/**
264
	 * We check that the event can be managed/checked locally or if the owner of the circle belongs to
265
	 * an other instance of Nextcloud
266
	 *
267
	 * @param GSEvent $event
268
	 *asyncBroadcast
269
	 *
270
	 * @return bool
271
	 */
272
	private function isLocalEvent(GSEvent $event): bool {
273
		if ($event->isLocal()) {
274
			return true;
275
		}
276
277
		$circle = $event->getCircle();
278
		$owner = $circle->getOwner();
279
		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...
280
			|| in_array(
281
				$owner->getInstance(), $this->configService->getTrustedDomains()
282
			)) {
283
			return true;
284
		}
285
286
		return false;
287
	}
288
289
290
	/**
291
	 * @param string $token
292
	 *
293
	 * @return GSWrapper[]
294
	 * @throws JsonException
295
	 * @throws ModelException
296
	 */
297
	public function getEventsByToken(string $token): array {
298
		return $this->gsEventsRequest->getByToken($token);
299
	}
300
301
302
	/**
303
	 * Deprecated ?
304
	 * should be used to manage results from events, like sending mails on user creation
305
	 *
306
	 * @param string $token
307
	 */
308
	public function manageResults(string $token): void {
309
		try {
310
			$wrappers = $this->gsEventsRequest->getByToken($token);
311
		} catch (JsonException | ModelException $e) {
312
			return;
313
		}
314
315
		$event = null;
316
		$events = [];
317
		foreach ($wrappers as $wrapper) {
318
			if ($wrapper->getStatus() !== GSWrapper::STATUS_DONE) {
319
				return;
320
			}
321
322
			$events[$wrapper->getInstance()] = $event = $wrapper->getEvent();
323
		}
324
325
		if ($event === null) {
326
			return;
327
		}
328
329
		try {
330
			$gs = $this->globalScaleService->getGlobalScaleEvent($event);
331
			$gs->result($events);
332
		} catch (GlobalScaleEventException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
333
		}
334
	}
335
336
337
	/**
338
	 * @throws GSStatusException
339
	 */
340
	public function synchronize() {
341
		$this->configService->getGSStatus();
342
343
		$sync = $this->getCirclesToSync();
344
		$this->synchronizeCircles($sync);
345
		$this->removeDeprecatedCircles();
346
347
		$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...
348
	}
349
350
351
	/**
352
	 * @param array $circles
353
	 *
354
	 * @throws GSStatusException
355
	 */
356
	public function synchronizeCircles(array $circles): void {
357
		$event = new GSEvent(GSEvent::GLOBAL_SYNC, true);
358
		$event->setSource($this->configService->getLocalCloudId());
359
		$event->setData(new SimpleDataStore($circles));
360
361
		foreach ($this->globalScaleService->getInstances() as $instance) {
362
			try {
363
				$this->broadcastEvent($event, $instance);
364
			} 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...
365
			}
366
		}
367
	}
368
369
370
	/**
371
	 * @return Circle[]
372
	 */
373
	private function getCirclesToSync(): array {
374
		$circles = $this->circlesRequest->forceGetCircles();
375
376
		$sync = [];
377
		foreach ($circles as $circle) {
378
			if ($circle->getOwner()
379
					   ->getInstance() !== ''
380
				|| $circle->getType() === Circle::CIRCLES_PERSONAL) {
381
				continue;
382
			}
383
384
			$members = $this->membersRequest->forceGetMembers($circle->getUniqueId());
385
			$circle->setMembers($members);
386
387
			$sync[] = $circle;
388
		}
389
390
		return $sync;
391
	}
392
393
394
	/**
395
	 *
396
	 */
397
	private function removeDeprecatedCircles() {
398
		$knownCircles = $this->circlesRequest->forceGetCircles();
399
400
		foreach ($knownCircles as $knownItem) {
401
			if ($knownItem->getOwner()
402
						  ->getInstance() === '') {
403
				continue;
404
			}
405
406
			try {
407
				$this->checkCircle($knownItem);
408
			} catch (GSStatusException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
409
			}
410
		}
411
	}
412
413
414
	/**
415
	 * @param Circle $circle
416
	 *
417
	 * @throws GSStatusException
418
	 */
419
	private function checkCircle(Circle $circle): void {
420
		$status = $this->confirmCircleStatus($circle);
421
422
		if (!$status) {
423
			$this->circlesRequest->destroyCircle($circle->getUniqueId());
424
			$this->membersRequest->removeAllFromCircle($circle->getUniqueId());
425
		}
426
	}
427
428
429
	/**
430
	 * @param Circle $circle
431
	 *
432
	 * @return bool
433
	 * @throws GSStatusException
434
	 */
435
	public function confirmCircleStatus(Circle $circle): bool {
436
		$event = new GSEvent(GSEvent::CIRCLE_STATUS, true);
437
		$event->setSource($this->configService->getLocalCloudId());
438
		$event->setCircle($circle);
439
440
		$this->signEvent($event);
441
442
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.status');
443
		$request = new Request($path, Request::TYPE_POST);
444
445
		$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...
446
		$request->setDataSerialize($event);
447
448
		$requestIssue = false;
449
		$notFound = false;
450
		$foundWithNoOwner = false;
451
		foreach ($this->globalScaleService->getInstances() as $instance) {
452
			$request->setAddress($instance);
453
454
			try {
455
				$result = $this->retrieveJson($request);
456
				$this->miscService->log('result: ' . json_encode($result));
457
				if ($this->getInt('status', $result, 0) !== 1) {
458
					throw new RequestContentException('result status is not good');
459
				}
460
461
				$status = $this->getInt('success.data.status', $result);
462
463
				// if error, we assume the circle might still exist.
464
				if ($status === CircleStatus::STATUS_ERROR) {
465
					return true;
466
				}
467
468
				if ($status === CircleStatus::STATUS_OK) {
469
					return true;
470
				}
471
472
				// TODO: check the data.supposedOwner entry.
473
				if ($status === CircleStatus::STATUS_NOT_OWNER) {
474
					$foundWithNoOwner = true;
475
				}
476
477
				if ($status === CircleStatus::STATUS_NOT_FOUND) {
478
					$notFound = true;
479
				}
480
481
			} catch (RequestContentException
482
			| RequestNetworkException
483
			| RequestResultNotJsonException
484
			| RequestResultSizeException
485
			| RequestServerException $e) {
486
				$requestIssue = true;
487
				// TODO: log instances that have network issue, after too many tries (7d), remove this circle.
488
				continue;
489
			}
490
		}
491
492
		// if no request issue, we can imagine that the instance that owns the circle is down.
493
		// We'll wait for more information (cf request exceptions management);
494
		if ($requestIssue) {
495
			return true;
496
		}
497
498
		// circle were not found in any other instances, we can easily says that the circle does not exists anymore
499
		if ($notFound && !$foundWithNoOwner) {
500
			return false;
501
		}
502
503
		// circle were found everywhere but with no owner on every instance. we need to assign a new owner.
504
		// This should be done by checking admin rights. if no admin rights, let's assume that circle should be removed.
505
		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...
506
			// TODO: assign a new owner and check that when changing owner, we do check that the destination instance is updated FOR SURE!
507
			return true;
508
		}
509
510
		// some instances returned notFound, some returned circle with no owner. let's assume the circle is deprecated.
511
		return false;
512
	}
513
514
	/**
515
	 * @throws GSStatusException
516
	 */
517
	public function syncEvents() {
518
519
	}
520
521
	/**
522
	 *
523
	 */
524
	private function removeDeprecatedEvents() {
525
//		$this->deprecatedEvents();
526
527
	}
528
529
530
}
531
532