Completed
Push — master ( 4edf58...b5cbd9 )
by Maxence
19s queued 10s
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
	 * @throws Exception
131
	 */
132
	public function newEvent(GSEvent $event) {
133
		$event->setSource($this->configService->getLocalCloudId());
134
135
		try {
136
			$gs = $this->globalScaleService->getGlobalScaleEvent($event);
137
138
			if ($this->isLocalEvent($event)) {
139
				$gs->verify($event, true);
140
				if (!$event->isAsync()) {
141
					$gs->manage($event);
142
				}
143
144
				$this->globalScaleService->asyncBroadcast($event);
145
			} else {
146
//				$gs->verify($event); // needed ? as we check event on the 'master' of the circle
147
				$this->confirmEvent($event);
148
				$this->miscService->log('confirmed: ' . json_encode($event));
149
//				$gs->manage($event); // needed ? as we manage it throw the confirmEvent
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
	 * @param GSWrapper $wrapper
162
	 * @param string $protocol
163
	 */
164
	public function broadcastWrapper(GSWrapper $wrapper, string $protocol): void {
165
		$status = GSWrapper::STATUS_FAILED;
166
167
		try {
168
			$this->broadcastEvent($wrapper->getEvent(), $wrapper->getInstance(), $protocol);
169
			$status = GSWrapper::STATUS_DONE;
170
		} 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...
171
		}
172
173
		if ($wrapper->getSeverity() === GSEvent::SEVERITY_HIGH) {
174
			$wrapper->setStatus($status);
175
		} else {
176
			$wrapper->setStatus(GSWrapper::STATUS_OVER);
177
		}
178
179
		$this->gsEventsRequest->update($wrapper);
180
	}
181
182
183
	/**
184
	 * @param GSEvent $event
185
	 * @param string $instance
186
	 * @param string $protocol
187
	 *
188
	 * @throws RequestContentException
189
	 * @throws RequestNetworkException
190
	 * @throws RequestResultNotJsonException
191
	 * @throws RequestResultSizeException
192
	 * @throws RequestServerException
193
	 */
194
	public function broadcastEvent(GSEvent $event, string $instance, string $protocol = ''): void {
195
		$this->signEvent($event);
196
197
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.broadcast');
198
		$request = new Request($path, Request::TYPE_POST);
199
200
		$protocols = ['https', 'http'];
201
		if ($protocol !== '') {
202
			$protocols = [$protocol];
203
		}
204
205
		$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...
206
		$request->setDataSerialize($event);
207
208
		$request->setAddress($instance);
209
210
		$data = $this->retrieveJson($request);
211
		$event->setResult(new SimpleDataStore($this->getArray('result', $data, [])));
212
	}
213
214
215
	/**
216
	 * @param GSEvent $event
217
	 *
218
	 * @throws RequestContentException
219
	 * @throws RequestNetworkException
220
	 * @throws RequestResultSizeException
221
	 * @throws RequestServerException
222
	 * @throws RequestResultNotJsonException
223
	 * @throws GlobalScaleEventException
224
	 */
225
	public function confirmEvent(GSEvent &$event): void {
226
		$this->signEvent($event);
227
228
		$circle = $event->getCircle();
229
		$owner = $circle->getOwner();
230
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.event');
231
232
		$request = new Request($path, Request::TYPE_POST);
233
		if ($this->get('REQUEST_SCHEME', $_SERVER) !== '') {
234
			$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...
235
		} else {
236
			$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...
237
		}
238
		$request->setAddressFromUrl($owner->getInstance());
239
		$request->setDataSerialize($event);
240
241
		$result = $this->retrieveJson($request);
242
		$this->miscService->log('result ' . json_encode($result));
243
		if ($this->getInt('status', $result) === 0) {
244
			throw new GlobalScaleEventException($this->get('error', $result));
245
		}
246
247
		$updatedData = $this->getArray('event', $result);
248
		$this->miscService->log('updatedEvent: ' . json_encode($updatedData));
249
		if (!empty($updatedData)) {
250
			$updated = new GSEvent();
251
			try {
252
				$updated->import($updatedData);
253
				$event = $updated;
254
			} catch (Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
255
			}
256
		}
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
	public function synchronizeCircles(array $circles): void {
360
		$event = new GSEvent(GSEvent::GLOBAL_SYNC, true);
361
		$event->setSource($this->configService->getLocalCloudId());
362
		$event->setData(new SimpleDataStore($circles));
363
364
		foreach ($this->globalScaleService->getInstances() as $instance) {
365
			try {
366
				$this->broadcastEvent($event, $instance);
367
			} 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...
368
			}
369
		}
370
	}
371
372
373
	/**
374
	 * @return Circle[]
375
	 */
376
	private function getCirclesToSync(): array {
377
		$circles = $this->circlesRequest->forceGetCircles();
378
379
		$sync = [];
380
		foreach ($circles as $circle) {
381
			if ($circle->getOwner()
382
					   ->getInstance() !== ''
383
				|| $circle->getType() === Circle::CIRCLES_PERSONAL) {
384
				continue;
385
			}
386
387
			$members = $this->membersRequest->forceGetMembers($circle->getUniqueId());
388
			$circle->setMembers($members);
389
390
			$sync[] = $circle;
391
		}
392
393
		return $sync;
394
	}
395
396
397
	/**
398
	 *
399
	 */
400
	private function removeDeprecatedCircles() {
401
		$knownCircles = $this->circlesRequest->forceGetCircles();
402
		foreach ($knownCircles as $knownItem) {
403
			if ($knownItem->getOwner()
404
						  ->getInstance() === '') {
405
				continue;
406
			}
407
408
			try {
409
				$this->checkCircle($knownItem);
410
			} catch (GSStatusException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
411
			}
412
		}
413
	}
414
415
416
	/**
417
	 * @param Circle $circle
418
	 *
419
	 * @throws GSStatusException
420
	 */
421
	private function checkCircle(Circle $circle): void {
422
		$status = $this->confirmCircleStatus($circle);
423
424
		if (!$status) {
425
			$this->circlesRequest->destroyCircle($circle->getUniqueId());
426
			$this->membersRequest->removeAllFromCircle($circle->getUniqueId());
427
		}
428
	}
429
430
431
	/**
432
	 * @param Circle $circle
433
	 *
434
	 * @return bool
435
	 * @throws GSStatusException
436
	 */
437
	public function confirmCircleStatus(Circle $circle): bool {
438
		$event = new GSEvent(GSEvent::CIRCLE_STATUS, true);
439
		$event->setSource($this->configService->getLocalCloudId());
440
		$event->setCircle($circle);
441
442
		$this->signEvent($event);
443
444
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.status');
445
		$request = new Request($path, Request::TYPE_POST);
446
447
		$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...
448
		$request->setDataSerialize($event);
449
450
		$requestIssue = false;
451
		$notFound = false;
452
		$foundWithNoOwner = false;
453
		foreach ($this->globalScaleService->getInstances() as $instance) {
454
			$request->setAddress($instance);
455
456
			try {
457
				$result = $this->retrieveJson($request);
458
//				$this->miscService->log('result: ' . json_encode($result));
459
				if ($this->getInt('status', $result, 0) !== 1) {
460
					throw new RequestContentException('result status is not good');
461
				}
462
463
				$status = $this->getInt('success.data.status', $result);
464
465
				// if error, we assume the circle might still exist.
466
				if ($status === CircleStatus::STATUS_ERROR) {
467
					return true;
468
				}
469
470
				if ($status === CircleStatus::STATUS_OK) {
471
					return true;
472
				}
473
474
				// TODO: check the data.supposedOwner entry.
475
				if ($status === CircleStatus::STATUS_NOT_OWNER) {
476
					$foundWithNoOwner = true;
477
				}
478
479
				if ($status === CircleStatus::STATUS_NOT_FOUND) {
480
					$notFound = true;
481
				}
482
483
			} catch (RequestContentException
484
			| RequestNetworkException
485
			| RequestResultNotJsonException
486
			| RequestResultSizeException
487
			| RequestServerException $e) {
488
				$requestIssue = true;
489
				// TODO: log instances that have network issue, after too many tries (7d), remove this circle.
490
				continue;
491
			}
492
		}
493
494
		// if no request issue, we can imagine that the instance that owns the circle is down.
495
		// We'll wait for more information (cf request exceptions management);
496
		if ($requestIssue) {
497
			return true;
498
		}
499
500
		// circle were not found in any other instances, we can easily says that the circle does not exists anymore
501
		if ($notFound && !$foundWithNoOwner) {
502
			return false;
503
		}
504
505
		// circle were found everywhere but with no owner on every instance. we need to assign a new owner.
506
		// This should be done by checking admin rights. if no admin rights, let's assume that circle should be removed.
507
		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...
508
			// TODO: assign a new owner and check that when changing owner, we do check that the destination instance is updated FOR SURE!
509
			return true;
510
		}
511
512
		// some instances returned notFound, some returned circle with no owner. let's assume the circle is deprecated.
513
		return false;
514
	}
515
516
	/**
517
	 * @throws GSStatusException
518
	 */
519
	public function syncEvents() {
520
521
	}
522
523
	/**
524
	 *
525
	 */
526
	private function removeDeprecatedEvents() {
527
//		$this->deprecatedEvents();
528
529
	}
530
531
532
}
533
534