Completed
Pull Request — master (#490)
by Maxence
02:09
created

GSUpstreamService::getEventsByToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
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\Nextcloud\NC19Request;
39
use daita\MySmallPhpTools\Model\Request;
40
use daita\MySmallPhpTools\Model\SimpleDataStore;
41
use daita\MySmallPhpTools\Traits\TArrayTools;
42
use daita\MySmallPhpTools\Traits\TRequest;
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 TRequest;
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
	 *asyncBroadcast
278
	 *
279
	 * @return bool
280
	 */
281
	private function isLocalEvent(GSEvent $event): bool {
282
		if ($event->isLocal()) {
283
			return true;
284
		}
285
286
		$circle = $event->getCircle();
287
		$owner = $circle->getOwner();
288
		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...
289
			|| in_array($owner->getInstance(), $this->configService->getTrustedDomains())) {
290
			return true;
291
		}
292
293
		return false;
294
	}
295
296
297
	/**
298
	 * @param string $token
299
	 *
300
	 * @return GSWrapper[]
301
	 * @throws JsonException
302
	 * @throws ModelException
303
	 */
304
	public function getEventsByToken(string $token): array {
305
		return $this->gsEventsRequest->getByToken($token);
306
	}
307
308
309
	/**
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
	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 NC19Request($path, Request::TYPE_POST);
447
		$this->configService->configureRequest($request);
448
		$request->setProtocols(['https', 'http']);
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