Completed
Pull Request — master (#362)
by Maxence
01:48
created

GSUpstreamService::removeDeprecatedCircles()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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