Completed
Pull Request — master (#551)
by Maxence
02:25
created

FederatedEventService   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 419
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 0
Metric Value
wmc 59
lcom 1
cbo 21
dl 0
loc 419
rs 4.08
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 1
B newEvent() 0 44 8
B confirmInitiator() 0 29 8
A getFederatedItem() 0 24 4
A setFederatedEventBypass() 0 14 5
B confirmRequiredCondition() 0 29 10
A confirmSharedItem() 0 14 4
A configureEvent() 0 11 4
A initBroadcast() 0 30 4
A getInstances() 0 27 3
A updateGlobalScaleInstances() 0 3 1
A getRemoteInstances() 0 3 1
B manageResults() 0 27 6

How to fix   Complexity   

Complex Class

Complex classes like FederatedEventService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FederatedEventService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
6
/**
7
 * Circles - Bring cloud-users closer together.
8
 *
9
 * This file is licensed under the Affero General Public License version 3 or
10
 * later. See the COPYING file.
11
 *
12
 * @author Maxence Lange <[email protected]>
13
 * @copyright 2021
14
 * @license GNU AGPL version 3 or any later version
15
 *
16
 * This program is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License as
18
 * published by the Free Software Foundation, either version 3 of the
19
 * License, or (at your option) any later version.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License
27
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
28
 *
29
 */
30
31
namespace OCA\Circles\Service;
32
33
34
use daita\MySmallPhpTools\ActivityPub\Nextcloud\nc21\NC21Signature;
35
use daita\MySmallPhpTools\Exceptions\RequestNetworkException;
36
use daita\MySmallPhpTools\Exceptions\SignatoryException;
37
use daita\MySmallPhpTools\Model\Nextcloud\nc21\NC21Request;
38
use daita\MySmallPhpTools\Model\Request;
39
use daita\MySmallPhpTools\Model\SimpleDataStore;
40
use daita\MySmallPhpTools\Traits\Nextcloud\nc21\TNC21Request;
41
use daita\MySmallPhpTools\Traits\TStringTools;
42
use OC;
43
use OCA\Circles\Db\MemberRequest;
44
use OCA\Circles\Db\RemoteRequest;
45
use OCA\Circles\Db\RemoteWrapperRequest;
46
use OCA\Circles\Db\ShareLockRequest;
47
use OCA\Circles\Exceptions\FederatedEventDSyncException;
48
use OCA\Circles\Exceptions\FederatedEventException;
49
use OCA\Circles\Exceptions\FederatedItemException;
50
use OCA\Circles\Exceptions\FederatedShareNotFoundException;
51
use OCA\Circles\Exceptions\InitiatorNotConfirmedException;
52
use OCA\Circles\Exceptions\JsonException;
53
use OCA\Circles\Exceptions\ModelException;
54
use OCA\Circles\Exceptions\OwnerNotFoundException;
55
use OCA\Circles\Exceptions\RemoteNotFoundException;
56
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
57
use OCA\Circles\Exceptions\UnknownRemoteException;
58
use OCA\Circles\IFederatedItem;
59
use OCA\Circles\IFederatedItemAsyncProcess;
60
use OCA\Circles\IFederatedItemCircleCheckNotRequired;
61
use OCA\Circles\IFederatedItemDataRequestOnly;
62
use OCA\Circles\IFederatedItemInitiatorCheckNotRequired;
63
use OCA\Circles\IFederatedItemInitiatorMembershipNotRequired;
64
use OCA\Circles\IFederatedItemInitiatorMustBeLocal;
65
use OCA\Circles\IFederatedItemLimitedToInstanceWithMembership;
66
use OCA\Circles\IFederatedItemMemberCheckNotRequired;
67
use OCA\Circles\IFederatedItemMemberEmpty;
68
use OCA\Circles\IFederatedItemMemberOptional;
69
use OCA\Circles\IFederatedItemMemberRequired;
70
use OCA\Circles\IFederatedItemSharedItem;
71
use OCA\Circles\Model\Circle;
72
use OCA\Circles\Model\Federated\FederatedEvent;
73
use OCA\Circles\Model\Federated\RemoteInstance;
74
use OCA\Circles\Model\Federated\RemoteWrapper;
75
use OCA\Circles\Model\GlobalScale\GSWrapper;
76
use OCA\Circles\Model\Member;
77
use OCP\IL10N;
78
use ReflectionClass;
79
use ReflectionException;
80
81
82
/**
83
 * Class FederatedEventService
84
 *
85
 * @package OCA\Circles\Service
86
 */
87
class FederatedEventService extends NC21Signature {
88
89
90
	use TNC21Request;
91
	use TStringTools;
92
93
94
	/** @var IL10N */
95
	private $l10n;
96
97
	/** @var RemoteWrapperRequest */
98
	private $remoteWrapperRequest;
99
100
	/** @var RemoteRequest */
101
	private $remoteRequest;
102
103
	/** @var ShareLockRequest */
104
	private $shareLockRequest;
105
106
	/** @var MemberRequest */
107
	private $memberRequest;
108
109
	/** @var RemoteUpstreamService */
110
	private $remoteUpstreamService;
111
112
	/** @var ConfigService */
113
	private $configService;
114
115
	/** @var MiscService */
116
	private $miscService;
117
118
119
	/**
120
	 * FederatedEventService constructor.
121
	 *
122
	 * @param IL10N $l10n
123
	 * @param RemoteWrapperRequest $remoteWrapperRequest
124
	 * @param RemoteRequest $remoteRequest
125
	 * @param MemberRequest $memberRequest
126
	 * @param ShareLockRequest $shareLockRequest
127
	 * @param RemoteUpstreamService $remoteUpstreamService
128
	 * @param ConfigService $configService
129
	 */
130
	public function __construct(
131
		IL10N $l10n, RemoteWrapperRequest $remoteWrapperRequest, RemoteRequest $remoteRequest,
132
		MemberRequest $memberRequest, ShareLockRequest $shareLockRequest,
133
		RemoteUpstreamService $remoteUpstreamService,
134
		ConfigService $configService
135
	) {
136
		$this->l10n = $l10n;
137
		$this->remoteWrapperRequest = $remoteWrapperRequest;
138
		$this->remoteRequest = $remoteRequest;
139
		$this->shareLockRequest = $shareLockRequest;
140
		$this->memberRequest = $memberRequest;
141
		$this->remoteUpstreamService = $remoteUpstreamService;
142
		$this->configService = $configService;
143
	}
144
145
146
	/**
147
	 * Called when creating a new Event.
148
	 * This method will manage the event locally and upstream the payload if needed.
149
	 *
150
	 * @param FederatedEvent $event
151
	 *
152
	 * @return SimpleDataStore
153
	 * @throws InitiatorNotConfirmedException
154
	 * @throws OwnerNotFoundException
155
	 * @throws FederatedEventException
156
	 * @throws RequestNetworkException
157
	 * @throws RemoteNotFoundException
158
	 * @throws RemoteResourceNotFoundException
159
	 * @throws UnknownRemoteException
160
	 * @throws SignatoryException
161
	 * @throws FederatedItemException
162
	 * @throws FederatedEventDSyncException
163
	 */
164
	public function newEvent(FederatedEvent $event): SimpleDataStore {
165
		$event->setSource($this->configService->getFrontalInstance());
166
167
		try {
168
			$federatedItem = $this->getFederatedItem($event, false);
169
		} catch (FederatedEventException $e) {
170
			$this->e($e);
171
			throw $e;
172
		}
173
174
		$this->confirmInitiator($event, true);
175
		if ($this->configService->isLocalInstance($event->getCircle()->getInstance())) {
176
			$event->setIncomingOrigin($event->getCircle()->getInstance());
177
178
			try {
179
				$federatedItem->verify($event);
180
				$reading = $event->getReadingOutcome();
181
				$reading->s('translated', $this->l10n->t($reading->g('message'), $reading->gArray('params')));
182
			} catch (FederatedItemException $e) {
183
				throw new FederatedItemException($this->l10n->t($e->getMessage(), $e->getParams()));
184
			}
185
186
			if ($event->isDataRequestOnly()) {
187
				return $event->getDataOutcome();
188
			}
189
190
			if (!$event->isAsync()) {
191
				$federatedItem->manage($event);
192
			}
193
194
			$this->initBroadcast($event);
195
		} else {
196
			$this->remoteUpstreamService->confirmEvent($event);
197
			if ($event->isDataRequestOnly()) {
198
				return $event->getDataOutcome();
199
			}
200
201
			if (!$event->isAsync()) {
202
				$federatedItem->manage($event);
203
			}
204
		}
205
206
		return $event->getOutcome();
207
	}
208
209
210
	/**
211
	 * This confirmation is optional, method is just here to avoid going too far away on the process
212
	 *
213
	 * @param FederatedEvent $event
214
	 * @param bool $local
215
	 *
216
	 * @throws InitiatorNotConfirmedException
217
	 */
218
	public function confirmInitiator(FederatedEvent $event, bool $local = false): void {
219
		if ($event->canBypass(FederatedEvent::BYPASS_INITIATORCHECK)) {
220
			return;
221
		}
222
223
		$circle = $event->getCircle();
224
		if (!$circle->hasInitiator()) {
225
			throw new InitiatorNotConfirmedException('Initiator does not exist');
226
		}
227
228
		if ($local) {
229
			if (!$this->configService->isLocalInstance($circle->getInitiator()->getInstance())) {
230
				throw new InitiatorNotConfirmedException(
231
					'Initiator is not from the instance at the origin of the request'
232
				);
233
			}
234
		} else {
235
			if ($circle->getInitiator()->getInstance() !== $event->getIncomingOrigin()) {
236
				throw new InitiatorNotConfirmedException(
237
					'Initiator must belong to the instance at the origin of the request'
238
				);
239
			}
240
		}
241
242
		if (!$event->canBypass(FederatedEvent::BYPASS_INITIATORMEMBERSHIP)
243
			&& $circle->getInitiator()->getLevel() < Member::LEVEL_MEMBER) {
244
			throw new InitiatorNotConfirmedException('Initiator must be a member of the Circle');
245
		}
246
	}
247
248
249
	/**
250
	 * @param FederatedEvent $event
251
	 * @param bool $checkLocalOnly
252
	 *
253
	 * @return IFederatedItem
254
	 * @throws FederatedEventException
255
	 */
256
	public function getFederatedItem(FederatedEvent $event, bool $checkLocalOnly = true): IFederatedItem {
257
		$class = $event->getClass();
258
		try {
259
			$test = new ReflectionClass($class);
260
		} catch (ReflectionException $e) {
261
			throw new FederatedEventException('ReflectionException with ' . $class . ': ' . $e->getMessage());
262
		}
263
264
		if (!in_array(IFederatedItem::class, $test->getInterfaceNames())) {
265
			throw new FederatedEventException($class . ' does not implements IFederatedItem');
266
		}
267
268
		$item = OC::$server->get($class);
269
		if (!($item instanceof IFederatedItem)) {
270
			throw new FederatedEventException($class . ' not an IFederatedItem');
271
		}
272
273
		$this->setFederatedEventBypass($event, $item);
274
		$this->confirmRequiredCondition($event, $item, $checkLocalOnly);
275
		$this->confirmSharedItem($event, $item);
276
		$this->configureEvent($event, $item);
277
278
		return $item;
279
	}
280
281
282
	/**
283
	 * Some event might need to bypass some checks
284
	 *
285
	 * @param FederatedEvent $event
286
	 * @param IFederatedItem $item
287
	 */
288
	private function setFederatedEventBypass(FederatedEvent $event, IFederatedItem $item) {
289
		if ($item instanceof IFederatedItemCircleCheckNotRequired) {
290
			$event->bypass(FederatedEvent::BYPASS_LOCALCIRCLECHECK);
291
		}
292
		if ($item instanceof IFederatedItemMemberCheckNotRequired) {
293
			$event->bypass(FederatedEvent::BYPASS_LOCALMEMBERCHECK);
294
		}
295
		if ($item instanceof IFederatedItemInitiatorCheckNotRequired) {
296
			$event->bypass(FederatedEvent::BYPASS_INITIATORCHECK);
297
		}
298
		if ($item instanceof IFederatedItemInitiatorMembershipNotRequired) {
299
			$event->bypass(FederatedEvent::BYPASS_INITIATORMEMBERSHIP);
300
		}
301
	}
302
303
	/**
304
	 * Some event might require additional check
305
	 *
306
	 * @param FederatedEvent $event
307
	 * @param IFederatedItem $item
308
	 * @param bool $checkLocalOnly
309
	 *
310
	 * @throws FederatedEventException
311
	 */
312
	private function confirmRequiredCondition(
313
		FederatedEvent $event,
314
		IFederatedItem $item,
315
		bool $checkLocalOnly = true
316
	) {
317
		if (!$event->hasCircle()) {
318
			throw new FederatedEventException('FederatedEvent has no Circle linked');
319
		}
320
321
		// TODO: enforce IFederatedItemMemberEmpty if no member
322
		if ($item instanceof IFederatedItemMemberEmpty) {
323
			$event->setMember(null);
324
		}
325
326
		if ($item instanceof IFederatedItemMemberRequired && !$event->hasMember()) {
327
			throw new FederatedEventException('FederatedEvent has no Member linked');
328
		}
329
		if ($event->hasMember()
330
			&& !($item instanceof IFederatedItemMemberRequired)
331
			&& !($item instanceof IFederatedItemMemberOptional)) {
332
			throw new FederatedEventException(
333
				get_class($item)
334
				. ' does not implements IFederatedItemMemberOptional nor IFederatedItemMemberRequired'
335
			);
336
		}
337
		if ($item instanceof IFederatedItemInitiatorMustBeLocal && $checkLocalOnly) {
338
			throw new FederatedEventException('FederatedItem must be executed locally');
339
		}
340
	}
341
342
343
	/**
344
	 * @param FederatedEvent $event
345
	 * @param IFederatedItem $item
346
	 *
347
	 * @throws FederatedShareNotFoundException
348
	 * @throws FederatedEventException
349
	 */
350
	private function confirmSharedItem(FederatedEvent $event, IFederatedItem $item): void {
351
		if (!$item instanceof IFederatedItemSharedItem) {
352
			return;
353
		}
354
355
		if ($event->getItemId() === '') {
356
			throw new FederatedEventException('FederatedItem must contains ItemId');
357
		}
358
359
		$shareLock = $this->shareLockRequest->getShare($event->getItemId());
360
		if ($shareLock->getInstance() !== $event->getIncomingOrigin()) {
361
			throw new FederatedEventException('ShareLock belongs to another instance');
362
		}
363
	}
364
365
366
	/**
367
	 * @param FederatedEvent $event
368
	 * @param IFederatedItem $item
369
	 */
370
	private function configureEvent(FederatedEvent $event, IFederatedItem $item) {
371
		if ($item instanceof IFederatedItemAsyncProcess) {
372
			$event->setAsync(true);
373
		}
374
		if ($item instanceof IFederatedItemLimitedToInstanceWithMembership) {
375
			$event->setLimitedToInstanceWithMember(true);
376
		}
377
		if ($item instanceof IFederatedItemDataRequestOnly) {
378
			$event->setDataRequestOnly(true);
379
		}
380
	}
381
382
383
	/**
384
	 * async the process, generate a local request that will be closed.
385
	 *
386
	 * @param FederatedEvent $event
387
	 * @param array $filter
388
	 */
389
	public function initBroadcast(FederatedEvent $event, array $filter = []): void {
390
		$instances = array_diff($this->getInstances($event), $filter);
391
		if (empty($instances)) {
392
			return;
393
		}
394
395
		$wrapper = new RemoteWrapper();
396
		$wrapper->setEvent($event);
397
		$wrapper->setToken($this->uuid());
398
		$wrapper->setCreation(time());
399
		$wrapper->setSeverity($event->getSeverity());
400
401
		foreach ($instances as $instance) {
402
			$wrapper->setInstance($instance);
403
			$this->remoteWrapperRequest->create($wrapper);
404
		}
405
406
		$request = new NC21Request('', Request::TYPE_POST);
407
		$this->configService->configureRequest(
408
			$request, 'circles.RemoteWrapper.asyncBroadcast', ['token' => $wrapper->getToken()]
409
		);
410
411
		$event->setWrapperToken($wrapper->getToken());
412
413
		try {
414
			$this->doRequest($request);
415
		} catch (RequestNetworkException $e) {
416
			$this->e($e, ['wrapper' => $wrapper]);
417
		}
418
	}
419
420
421
	/**
422
	 * @param FederatedEvent $event
423
	 * @param Circle|null $circle
424
	 * // TODO: use of $circle ??
425
	 *
426
	 * @return array
427
	 */
428
	public function getInstances(FederatedEvent $event, ?Circle $circle = null): array {
429
		$local = $this->configService->getFrontalInstance();
430
		$instances = $this->remoteRequest->getOutgoingRecipient($circle);
431
432
		$instances = array_map(
433
			function(RemoteInstance $instance): string {
434
				return $instance->getInstance();
435
			}, $instances
436
		);
437
438
		if ($event->isLimitedToInstanceWithMember()) {
439
			$instances =
440
				array_intersect(
441
					$instances, $this->memberRequest->getMemberInstances($event->getCircle()->getId())
442
				);
443
		}
444
445
		$instances = array_merge([$local], $instances);
446
447
		if ($event->isAsync()) {
448
			return $instances;
449
		}
450
451
		return array_values(
452
			array_diff($instances, array_merge($this->configService->getTrustedDomains(), [$local]))
453
		);
454
	}
455
456
457
	/**
458
	 * @param array $current
459
	 */
460
	private function updateGlobalScaleInstances(array $current): void {
0 ignored issues
show
Unused Code introduced by
The parameter $current is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
461
//		$known = $this->remoteRequest->getFromType(RemoteInstance::TYPE_GLOBAL_SCALE);
462
	}
463
464
	/**
465
	 * @return array
466
	 */
467
	private function getRemoteInstances(): array {
468
		return [];
469
	}
470
471
472
	/**
473
	 * should be used to manage results from events, like sending mails on user creation
474
	 *
475
	 * @param string $token
476
	 */
477
	public function manageResults(string $token): void {
478
		try {
479
			$wrappers = $this->remoteWrapperRequest->getByToken($token);
480
		} catch (JsonException | ModelException $e) {
481
			return;
482
		}
483
484
		$event = null;
485
		$events = [];
486
		foreach ($wrappers as $wrapper) {
487
			if ($wrapper->getStatus() !== GSWrapper::STATUS_DONE) {
488
				return;
489
			}
490
491
			$events[$wrapper->getInstance()] = $event = $wrapper->getEvent();
492
		}
493
494
		if ($event === null) {
495
			return;
496
		}
497
498
		try {
499
			$gs = $this->getFederatedItem($event, false);
500
			$gs->result($events);
501
		} catch (FederatedEventException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
502
		}
503
	}
504
505
}
506
507