Completed
Pull Request — master (#362)
by Maxence
03:19
created

GSUpstreamService::checkCircle()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
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
				if (!$event->isAsync()) {
140
					$gs->manage($event);
141
				}
142
143
				$this->globalScaleService->asyncBroadcast($event);
144
			} else {
145
				$gs->verify($event); // needed ? as we check event on the 'master' of the circle
146
				$this->confirmEvent($event);
147
				$gs->manage($event); // needed ? as we manage it throw the confirmEvent
148
			}
149
		} catch (Exception $e) {
150
			$this->miscService->log(
151
				get_class($e) . ' on new event: ' . $e->getMessage() . ' - ' . json_encode($event), 1
152
			);
153
			throw $e;
154
		}
155
	}
156
157
158
	/**
159
	 * @param GSWrapper $wrapper
160
	 * @param string $protocol
161
	 */
162
	public function broadcastWrapper(GSWrapper $wrapper, string $protocol): void {
163
		$status = GSWrapper::STATUS_FAILED;
164
165
		try {
166
			$this->broadcastEvent($wrapper->getEvent(), $wrapper->getInstance(), $protocol);
167
			$status = GSWrapper::STATUS_DONE;
168
		} 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...
169
		}
170
171
		if ($wrapper->getSeverity() === GSEvent::SEVERITY_HIGH) {
172
			$wrapper->setStatus($status);
173
		} else {
174
			$wrapper->setStatus(GSWrapper::STATUS_OVER);
175
		}
176
177
		$this->gsEventsRequest->update($wrapper);
178
	}
179
180
181
	/**
182
	 * @param GSEvent $event
183
	 * @param string $instance
184
	 * @param string $protocol
185
	 *
186
	 * @throws RequestContentException
187
	 * @throws RequestNetworkException
188
	 * @throws RequestResultNotJsonException
189
	 * @throws RequestResultSizeException
190
	 * @throws RequestServerException
191
	 */
192
	public function broadcastEvent(GSEvent $event, string $instance, string $protocol = ''): void {
193
		$this->signEvent($event);
194
195
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.broadcast');
196
		$request = new Request($path, Request::TYPE_POST);
197
198
		$protocols = ['https', 'http'];
199
		if ($protocol !== '') {
200
			$protocols = [$protocol];
201
		}
202
203
		$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...
204
		$request->setDataSerialize($event);
205
206
		$request->setAddress($instance);
207
208
		$data = $this->retrieveJson($request);
209
		$event->setResult(new SimpleDataStore($this->getArray('result', $data, [])));
210
	}
211
212
213
	/**
214
	 * @param GSEvent $event
215
	 *
216
	 * @throws RequestContentException
217
	 * @throws RequestNetworkException
218
	 * @throws RequestResultSizeException
219
	 * @throws RequestServerException
220
	 * @throws RequestResultNotJsonException
221
	 * @throws GlobalScaleEventException
222
	 */
223
	public function confirmEvent(GSEvent $event): void {
224
		$this->signEvent($event);
225
226
		$circle = $event->getCircle();
227
		$owner = $circle->getOwner();
228
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.event');
229
230
		$request = new Request($path, Request::TYPE_POST);
231
		if ($this->get('REQUEST_SCHEME', $_SERVER) !== '') {
232
			$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...
233
		} else {
234
			$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...
235
		}
236
		$request->setAddressFromUrl($owner->getInstance());
237
		$request->setDataSerialize($event);
238
239
		$result = $this->retrieveJson($request);
240
		$this->miscService->log('result ' . json_encode($result));
241
		if ($this->getInt('status', $result) === 0) {
242
			throw new GlobalScaleEventException($this->get('error', $result));
243
		}
244
	}
245
246
247
	/**
248
	 * @param GSEvent $event
249
	 *
250
	 * @throws GSStatusException
251
	 */
252
	private function fillEvent(GSEvent $event): void {
253
		if (!$this->configService->getGSStatus(ConfigService::GS_ENABLED)) {
254
			return;
255
		}
256
257
		$event->setSource($this->configService->getLocalCloudId());
258
	}
259
260
261
	/**
262
	 * @param GSEvent $event
263
	 */
264
	private function signEvent(GSEvent $event) {
265
		$event->setKey($this->globalScaleService->getKey());
266
	}
267
268
269
	/**
270
	 * We check that the event can be managed/checked locally or if the owner of the circle belongs to
271
	 * an other instance of Nextcloud
272
	 *
273
	 * @param GSEvent $event
274
	 *asyncBroadcast
275
	 *
276
	 * @return bool
277
	 */
278
	private function isLocalEvent(GSEvent $event): bool {
279
		if ($event->isLocal()) {
280
			return true;
281
		}
282
283
		$circle = $event->getCircle();
284
		$owner = $circle->getOwner();
285
		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...
286
			|| in_array(
287
				$owner->getInstance(), $this->configService->getTrustedDomains()
288
			)) {
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
	 * Deprecated ?
310
	 * should be used to manage results from events, like sending mails on user creation
311
	 *
312
	 * @param string $token
313
	 */
314
	public function manageResults(string $token): void {
315
		try {
316
			$wrappers = $this->gsEventsRequest->getByToken($token);
317
		} catch (JsonException | ModelException $e) {
318
			return;
319
		}
320
321
		$event = null;
322
		$events = [];
323
		foreach ($wrappers as $wrapper) {
324
			if ($wrapper->getStatus() !== GSWrapper::STATUS_DONE) {
325
				return;
326
			}
327
328
			$events[$wrapper->getInstance()] = $event = $wrapper->getEvent();
329
		}
330
331
		if ($event === null) {
332
			return;
333
		}
334
335
		try {
336
			$gs = $this->globalScaleService->getGlobalScaleEvent($event);
337
			$gs->result($events);
338
		} catch (GlobalScaleEventException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
339
		}
340
	}
341
342
343
	/**
344
	 * @throws GSStatusException
345
	 */
346
	public function synchronize() {
347
		$this->configService->getGSStatus();
348
349
		$sync = $this->getCirclesToSync();
350
		$this->synchronizeCircles($sync);
351
		$this->removeDeprecatedCircles();
352
353
		$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...
354
	}
355
356
357
	/**
358
	 * @param array $circles
359
	 *
360
	 * @throws GSStatusException
361
	 */
362
	public function synchronizeCircles(array $circles): void {
363
		$event = new GSEvent(GSEvent::GLOBAL_SYNC, true);
364
		$event->setSource($this->configService->getLocalCloudId());
365
		$event->setData(new SimpleDataStore($circles));
366
367
		foreach ($this->globalScaleService->getInstances() as $instance) {
368
			try {
369
				$this->broadcastEvent($event, $instance);
370
			} 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...
371
			}
372
		}
373
	}
374
375
376
	/**
377
	 * @return Circle[]
378
	 */
379
	private function getCirclesToSync(): array {
380
		$circles = $this->circlesRequest->forceGetCircles();
381
382
		$sync = [];
383
		foreach ($circles as $circle) {
384
			if ($circle->getOwner()
385
					   ->getInstance() !== ''
386
				|| $circle->getType() === Circle::CIRCLES_PERSONAL) {
387
				continue;
388
			}
389
390
			$members = $this->membersRequest->forceGetMembers($circle->getUniqueId());
391
			$circle->setMembers($members);
392
393
			$sync[] = $circle;
394
		}
395
396
		return $sync;
397
	}
398
399
400
	/**
401
	 *
402
	 */
403
	private function removeDeprecatedCircles() {
404
		$knownCircles = $this->circlesRequest->forceGetCircles();
405
		foreach ($knownCircles as $knownItem) {
406
			if ($knownItem->getOwner()
407
						  ->getInstance() === '') {
408
				continue;
409
			}
410
411
			try {
412
				$this->checkCircle($knownItem);
413
			} catch (GSStatusException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
414
			}
415
		}
416
	}
417
418
419
	/**
420
	 * @param Circle $circle
421
	 *
422
	 * @throws GSStatusException
423
	 */
424
	private function checkCircle(Circle $circle): void {
425
		$status = $this->confirmCircleStatus($circle);
426
427
		if (!$status) {
428
			$this->circlesRequest->destroyCircle($circle->getUniqueId());
429
			$this->membersRequest->removeAllFromCircle($circle->getUniqueId());
430
		}
431
	}
432
433
434
	/**
435
	 * @param Circle $circle
436
	 *
437
	 * @return bool
438
	 * @throws GSStatusException
439
	 */
440
	public function confirmCircleStatus(Circle $circle): bool {
441
		$event = new GSEvent(GSEvent::CIRCLE_STATUS, true);
442
		$event->setSource($this->configService->getLocalCloudId());
443
		$event->setCircle($circle);
444
445
		$this->signEvent($event);
446
447
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.status');
448
		$request = new Request($path, Request::TYPE_POST);
449
450
		$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...
451
		$request->setDataSerialize($event);
452
453
		$requestIssue = false;
454
		$notFound = false;
455
		$foundWithNoOwner = false;
456
		foreach ($this->globalScaleService->getInstances() as $instance) {
457
			$request->setAddress($instance);
458
459
			try {
460
				$result = $this->retrieveJson($request);
461
				$this->miscService->log('result: ' . json_encode($result));
462
				if ($this->getInt('status', $result, 0) !== 1) {
463
					throw new RequestContentException('result status is not good');
464
				}
465
466
				$status = $this->getInt('success.data.status', $result);
467
468
				// if error, we assume the circle might still exist.
469
				if ($status === CircleStatus::STATUS_ERROR) {
470
					return true;
471
				}
472
473
				if ($status === CircleStatus::STATUS_OK) {
474
					return true;
475
				}
476
477
				// TODO: check the data.supposedOwner entry.
478
				if ($status === CircleStatus::STATUS_NOT_OWNER) {
479
					$foundWithNoOwner = true;
480
				}
481
482
				if ($status === CircleStatus::STATUS_NOT_FOUND) {
483
					$notFound = true;
484
				}
485
486
			} catch (RequestContentException
487
			| RequestNetworkException
488
			| RequestResultNotJsonException
489
			| RequestResultSizeException
490
			| RequestServerException $e) {
491
				$requestIssue = true;
492
				// TODO: log instances that have network issue, after too many tries (7d), remove this circle.
493
				continue;
494
			}
495
		}
496
497
		// if no request issue, we can imagine that the instance that owns the circle is down.
498
		// We'll wait for more information (cf request exceptions management);
499
		if ($requestIssue) {
500
			return true;
501
		}
502
503
		// circle were not found in any other instances, we can easily says that the circle does not exists anymore
504
		if ($notFound && !$foundWithNoOwner) {
505
			return false;
506
		}
507
508
		// circle were found everywhere but with no owner on every instance. we need to assign a new owner.
509
		// This should be done by checking admin rights. if no admin rights, let's assume that circle should be removed.
510
		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...
511
			// TODO: assign a new owner and check that when changing owner, we do check that the destination instance is updated FOR SURE!
512
			return true;
513
		}
514
515
		// some instances returned notFound, some returned circle with no owner. let's assume the circle is deprecated.
516
		return false;
517
	}
518
519
	/**
520
	 * @throws GSStatusException
521
	 */
522
	public function syncEvents() {
523
524
	}
525
526
	/**
527
	 *
528
	 */
529
	private function removeDeprecatedEvents() {
530
//		$this->deprecatedEvents();
531
532
	}
533
534
535
}
536
537