Completed
Push — master ( 3de7d3...149977 )
by Maxence
22s queued 11s
created

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