Completed
Push — master ( 574ca3...69f17d )
by Maxence
04:30 queued 02:09
created

GSUpstreamService::syncEvents()   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 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\Nextcloud\NC19Request;
39
use daita\MySmallPhpTools\Model\Request;
40
use daita\MySmallPhpTools\Model\SimpleDataStore;
41
use daita\MySmallPhpTools\Traits\Nextcloud\TNC19Request;
42
use daita\MySmallPhpTools\Traits\TArrayTools;
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 TNC19Request;
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->getLocalInstance());
136
137
		try {
138
			$gs = $this->globalScaleService->getGlobalScaleEvent($event);
139
			if ($this->isLocalEvent($event)) {
140
				$gs->verify($event, true);
141
				if (!$event->isAsync()) {
142
					$gs->manage($event);
143
				}
144
145
				return $this->globalScaleService->asyncBroadcast($event);
146
			} else {
147
				$this->confirmEvent($event);
148
149
				return '';
150
			}
151
		} catch (Exception $e) {
152
			$this->miscService->log(
153
				get_class($e) . ' on new event: ' . $e->getMessage() . ' - ' . json_encode($event), 1
154
			);
155
			throw $e;
156
		}
157
158
	}
159
160
161
	/**
162
	 * @param GSWrapper $wrapper
163
	 * @param string $protocol
164
	 */
165
	public function broadcastWrapper(GSWrapper $wrapper, string $protocol): void {
166
		$status = GSWrapper::STATUS_FAILED;
167
168
		try {
169
			$this->broadcastEvent($wrapper->getEvent(), $wrapper->getInstance(), $protocol);
170
			$status = GSWrapper::STATUS_DONE;
171
		} 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...
172
		}
173
174
		if ($wrapper->getSeverity() === GSEvent::SEVERITY_HIGH) {
175
			$wrapper->setStatus($status);
176
		} else {
177
			$wrapper->setStatus(GSWrapper::STATUS_OVER);
178
		}
179
180
		$this->gsEventsRequest->update($wrapper);
181
	}
182
183
184
	/**
185
	 * @param GSEvent $event
186
	 * @param string $instance
187
	 * @param string $protocol
188
	 *
189
	 * @throws RequestContentException
190
	 * @throws RequestNetworkException
191
	 * @throws RequestResultNotJsonException
192
	 * @throws RequestResultSizeException
193
	 * @throws RequestServerException
194
	 */
195
	public function broadcastEvent(GSEvent $event, string $instance, string $protocol = ''): void {
196
		$this->signEvent($event);
197
198
		if ($this->configService->isLocalInstance($instance)) {
199
			$request = new NC19Request('', Request::TYPE_POST);
200
			$this->configService->configureRequest($request, 'circles.GlobalScale.broadcast');
201
		} else {
202
			$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.broadcast');
203
			$request = new NC19Request($path, Request::TYPE_POST);
204
			$this->configService->configureRequest($request);
205
			$protocols = ['https', 'http'];
206
			if ($protocol !== '') {
207
				$protocols = [$protocol];
208
			}
209
			$request->setInstance($instance);
0 ignored issues
show
Bug introduced by
The method setInstance() does not seem to exist on object<daita\MySmallPhpT...\Nextcloud\NC19Request>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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