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

FederatedEventService::confirmRequiredCondition()   B

Complexity

Conditions 10
Paths 9

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 7.6666
c 0
b 0
f 0
cc 10
nc 9
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Exceptions\FederatedEventDSyncException;
47
use OCA\Circles\Exceptions\FederatedEventException;
48
use OCA\Circles\Exceptions\FederatedItemException;
49
use OCA\Circles\Exceptions\InitiatorNotConfirmedException;
50
use OCA\Circles\Exceptions\JsonException;
51
use OCA\Circles\Exceptions\ModelException;
52
use OCA\Circles\Exceptions\OwnerNotFoundException;
53
use OCA\Circles\Exceptions\RemoteNotFoundException;
54
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
55
use OCA\Circles\Exceptions\UnknownRemoteException;
56
use OCA\Circles\IFederatedItem;
57
use OCA\Circles\IFederatedItemAsyncProcess;
58
use OCA\Circles\IFederatedItemCircleCheckNotRequired;
59
use OCA\Circles\IFederatedItemDataRequestOnly;
60
use OCA\Circles\IFederatedItemInitiatorCheckNotRequired;
61
use OCA\Circles\IFederatedItemInitiatorMembershipNotRequired;
62
use OCA\Circles\IFederatedItemInitiatorMustBeLocal;
63
use OCA\Circles\IFederatedItemLimitedToInstanceWithMembership;
64
use OCA\Circles\IFederatedItemMemberCheckNotRequired;
65
use OCA\Circles\IFederatedItemMemberEmpty;
66
use OCA\Circles\IFederatedItemMemberOptional;
67
use OCA\Circles\IFederatedItemMemberRequired;
68
use OCA\Circles\Model\Circle;
69
use OCA\Circles\Model\Federated\FederatedEvent;
70
use OCA\Circles\Model\Federated\RemoteInstance;
71
use OCA\Circles\Model\Federated\RemoteWrapper;
72
use OCA\Circles\Model\GlobalScale\GSWrapper;
73
use OCA\Circles\Model\Member;
74
use OCP\IL10N;
75
use ReflectionClass;
76
use ReflectionException;
77
78
79
/**
80
 * Class FederatedEventService
81
 *
82
 * @package OCA\Circles\Service
83
 */
84
class FederatedEventService extends NC21Signature {
85
86
87
	use TNC21Request;
88
	use TStringTools;
89
90
91
	/** @var IL10N */
92
	private $l10n;
93
94
	/** @var RemoteWrapperRequest */
95
	private $remoteWrapperRequest;
96
97
	/** @var RemoteRequest */
98
	private $remoteRequest;
99
100
	/** @var MemberRequest */
101
	private $memberRequest;
102
103
	/** @var RemoteUpstreamService */
104
	private $remoteUpstreamService;
105
106
	/** @var ConfigService */
107
	private $configService;
108
109
	/** @var MiscService */
110
	private $miscService;
111
112
113
	/**
114
	 * FederatedEventService constructor.
115
	 *
116
	 * @param IL10N $l10n
117
	 * @param RemoteWrapperRequest $remoteWrapperRequest
118
	 * @param RemoteRequest $remoteRequest
119
	 * @param MemberRequest $memberRequest
120
	 * @param RemoteUpstreamService $remoteUpstreamService
121
	 * @param ConfigService $configService
122
	 */
123
	public function __construct(
124
		IL10N $l10n, RemoteWrapperRequest $remoteWrapperRequest, RemoteRequest $remoteRequest,
125
		MemberRequest $memberRequest, RemoteUpstreamService $remoteUpstreamService,
126
		ConfigService $configService
127
	) {
128
		$this->l10n = $l10n;
129
		$this->remoteWrapperRequest = $remoteWrapperRequest;
130
		$this->remoteRequest = $remoteRequest;
131
		$this->memberRequest = $memberRequest;
132
		$this->remoteUpstreamService = $remoteUpstreamService;
133
		$this->configService = $configService;
134
	}
135
136
137
	/**
138
	 * Called when creating a new Event.
139
	 * This method will manage the event locally and upstream the payload if needed.
140
	 *
141
	 * @param FederatedEvent $event
142
	 *
143
	 * @return SimpleDataStore
144
	 * @throws InitiatorNotConfirmedException
145
	 * @throws OwnerNotFoundException
146
	 * @throws FederatedEventException
147
	 * @throws RequestNetworkException
148
	 * @throws RemoteNotFoundException
149
	 * @throws RemoteResourceNotFoundException
150
	 * @throws UnknownRemoteException
151
	 * @throws SignatoryException
152
	 * @throws FederatedItemException
153
	 * @throws FederatedEventDSyncException
154
	 */
155
	public function newEvent(FederatedEvent $event): SimpleDataStore {
156
		$event->setSource($this->configService->getLocalInstance());
157
158
		try {
159
			$federatedItem = $this->getFederatedItem($event, false);
160
		} catch (FederatedEventException $e) {
161
			$this->e($e);
162
			throw $e;
163
		}
164
165
		$this->confirmInitiator($event, true);
166
		if ($this->configService->isLocalInstance($event->getCircle()->getInstance())) {
167
			$event->setIncomingOrigin($event->getCircle()->getInstance());
168
169
			try {
170
				$federatedItem->verify($event);
171
				$reading = $event->getReadingOutcome();
172
				$reading->s('translated', $this->l10n->t($reading->g('message'), $reading->gArray('params')));
173
			} catch (FederatedItemException $e) {
174
				throw new FederatedItemException($this->l10n->t($e->getMessage(), $e->getParams()));
175
			}
176
177
			if ($event->isDataRequestOnly()) {
178
				return $event->getDataOutcome();
179
			}
180
181
			if (!$event->isAsync()) {
182
				$federatedItem->manage($event);
183
			}
184
185
			$this->initBroadcast($event);
186
		} else {
187
			$this->remoteUpstreamService->confirmEvent($event);
188
			if ($event->isDataRequestOnly()) {
189
				return $event->getDataOutcome();
190
			}
191
192
			if (!$event->isAsync()) {
193
				$federatedItem->manage($event);
194
			}
195
		}
196
197
		return $event->getOutcome();
198
	}
199
200
201
	/**
202
	 * This confirmation is optional, method is just here to avoid going too far away on the process
203
	 *
204
	 * @param FederatedEvent $event
205
	 * @param bool $local
206
	 *
207
	 * @throws InitiatorNotConfirmedException
208
	 */
209
	public function confirmInitiator(FederatedEvent $event, bool $local = false): void {
210
		if ($event->canBypass(FederatedEvent::BYPASS_INITIATORCHECK)) {
211
			return;
212
		}
213
214
		$circle = $event->getCircle();
215
		if (!$circle->hasInitiator()) {
216
			throw new InitiatorNotConfirmedException('Initiator does not exist');
217
		}
218
219
		if ($local) {
220
			if (!$this->configService->isLocalInstance($circle->getInitiator()->getInstance())) {
221
				throw new InitiatorNotConfirmedException(
222
					'Initiator is not from the instance at the origin of the request'
223
				);
224
			}
225
		} else {
226
			if ($circle->getInitiator()->getInstance() !== $event->getIncomingOrigin()) {
227
				throw new InitiatorNotConfirmedException(
228
					'Initiator must belong to the instance at the origin of the request'
229
				);
230
			}
231
		}
232
233
		if (!$event->canBypass(FederatedEvent::BYPASS_INITIATORMEMBERSHIP)
234
			&& $circle->getInitiator()->getLevel() < Member::LEVEL_MEMBER) {
235
			throw new InitiatorNotConfirmedException('Initiator must be a member of the Circle');
236
		}
237
	}
238
239
240
	/**
241
	 * @param FederatedEvent $event
242
	 * @param bool $checkLocalOnly
243
	 *
244
	 * @return IFederatedItem
245
	 * @throws FederatedEventException
246
	 */
247
	public function getFederatedItem(FederatedEvent $event, bool $checkLocalOnly = true): IFederatedItem {
248
		$class = $event->getClass();
249
		try {
250
			$test = new ReflectionClass($class);
251
		} catch (ReflectionException $e) {
252
			throw new FederatedEventException('ReflectionException with ' . $class . ': ' . $e->getMessage());
253
		}
254
255
		if (!in_array(IFederatedItem::class, $test->getInterfaceNames())) {
256
			throw new FederatedEventException($class . ' does not implements IFederatedItem');
257
		}
258
259
		$item = OC::$server->get($class);
260
		if (!($item instanceof IFederatedItem)) {
261
			throw new FederatedEventException($class . ' not an IFederatedItem');
262
		}
263
264
		$this->setFederatedEventBypass($event, $item);
265
		$this->confirmRequiredCondition($event, $item, $checkLocalOnly);
266
		$this->configureEvent($event, $item);
267
268
		return $item;
269
	}
270
271
272
	/**
273
	 * Some event might need to bypass some checks
274
	 *
275
	 * @param FederatedEvent $event
276
	 * @param IFederatedItem $item
277
	 */
278
	private function setFederatedEventBypass(FederatedEvent $event, IFederatedItem $item) {
279
		if ($item instanceof IFederatedItemCircleCheckNotRequired) {
280
			$event->bypass(FederatedEvent::BYPASS_LOCALCIRCLECHECK);
281
		}
282
		if ($item instanceof IFederatedItemMemberCheckNotRequired) {
283
			$event->bypass(FederatedEvent::BYPASS_LOCALMEMBERCHECK);
284
		}
285
		if ($item instanceof IFederatedItemInitiatorCheckNotRequired) {
286
			$event->bypass(FederatedEvent::BYPASS_INITIATORCHECK);
287
		}
288
		if ($item instanceof IFederatedItemInitiatorMembershipNotRequired) {
289
			$event->bypass(FederatedEvent::BYPASS_INITIATORMEMBERSHIP);
290
		}
291
	}
292
293
	/**
294
	 * Some event might require additional check
295
	 *
296
	 * @param FederatedEvent $event
297
	 * @param IFederatedItem $item
298
	 * @param bool $checkLocalOnly
299
	 *
300
	 * @throws FederatedEventException
301
	 */
302
	private function confirmRequiredCondition(
303
		FederatedEvent $event,
304
		IFederatedItem $item,
305
		bool $checkLocalOnly = true
306
	) {
307
		if (!$event->hasCircle()) {
308
			throw new FederatedEventException('FederatedEvent has no Circle linked');
309
		}
310
311
		// TODO: enforce IFederatedItemMemberEmpty if no member
312
		if ($item instanceof IFederatedItemMemberEmpty) {
313
			$event->setMember(null);
314
		}
315
316
		if ($item instanceof IFederatedItemMemberRequired && !$event->hasMember()) {
317
			throw new FederatedEventException('FederatedEvent has no Member linked');
318
		}
319
		if ($event->hasMember()
320
			&& !($item instanceof IFederatedItemMemberRequired)
321
			&& !($item instanceof IFederatedItemMemberOptional)) {
322
			throw new FederatedEventException(
323
				get_class($item)
324
				. ' does not implements IFederatedItemMemberOptional nor IFederatedItemMemberRequired'
325
			);
326
		}
327
		if ($item instanceof IFederatedItemInitiatorMustBeLocal && $checkLocalOnly) {
328
			throw new FederatedEventException('FederatedItem must be executed locally');
329
		}
330
	}
331
332
333
	/**
334
	 * @param FederatedEvent $event
335
	 * @param IFederatedItem $item
336
	 */
337
	private function configureEvent(FederatedEvent $event, IFederatedItem $item) {
338
		if ($item instanceof IFederatedItemAsyncProcess) {
339
			$event->setAsync(true);
340
		}
341
		if ($item instanceof IFederatedItemLimitedToInstanceWithMembership) {
342
			$event->setLimitedToInstanceWithMember(true);
343
		}
344
		if ($item instanceof IFederatedItemDataRequestOnly) {
345
			$event->setDataRequestOnly(true);
346
		}
347
	}
348
349
350
	/**
351
	 * async the process, generate a local request that will be closed.
352
	 *
353
	 * @param FederatedEvent $event
354
	 * @param array $filter
355
	 */
356
	public function initBroadcast(FederatedEvent $event, array $filter = []): void {
357
		$instances = array_diff($this->getInstances($event), $filter);
358
		if (empty($instances)) {
359
			return;
360
		}
361
362
		$wrapper = new RemoteWrapper();
363
		$wrapper->setEvent($event);
364
		$wrapper->setToken($this->uuid());
365
		$wrapper->setCreation(time());
366
		$wrapper->setSeverity($event->getSeverity());
367
368
		foreach ($instances as $instance) {
369
			$wrapper->setInstance($instance);
370
			$this->remoteWrapperRequest->create($wrapper);
371
		}
372
373
		$request = new NC21Request('', Request::TYPE_POST);
374
		$this->configService->configureRequest(
375
			$request, 'circles.RemoteWrapper.asyncBroadcast', ['token' => $wrapper->getToken()]
376
		);
377
378
		$event->setWrapperToken($wrapper->getToken());
379
380
		try {
381
			$this->doRequest($request);
382
		} catch (RequestNetworkException $e) {
383
			$this->e($e, ['wrapper' => $wrapper]);
384
		}
385
	}
386
387
388
	/**
389
	 * @param FederatedEvent $event
390
	 * @param Circle|null $circle
391
	 * // TODO: use of $circle ??
392
	 *
393
	 * @return array
394
	 */
395
	public function getInstances(FederatedEvent $event, ?Circle $circle = null): array {
396
		$local = $this->configService->getLocalInstance();
397
		$instances = $this->remoteRequest->getOutgoingRecipient($circle);
398
399
		$instances = array_map(
400
			function(RemoteInstance $instance): string {
401
				return $instance->getInstance();
402
			}, $instances
403
		);
404
405
		if ($event->isLimitedToInstanceWithMember()) {
406
			$instances =
407
				array_intersect(
408
					$instances, $this->memberRequest->getMemberInstances($event->getCircle()->getId())
409
				);
410
		}
411
412
		$instances = array_merge([$local], $instances);
413
414
		if ($event->isAsync()) {
415
			return $instances;
416
		}
417
418
		return array_values(
419
			array_diff($instances, array_merge($this->configService->getTrustedDomains(), [$local]))
420
		);
421
	}
422
423
424
	/**
425
	 * @param array $current
426
	 */
427
	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...
428
//		$known = $this->remoteRequest->getFromType(RemoteInstance::TYPE_GLOBAL_SCALE);
429
	}
430
431
	/**
432
	 * @return array
433
	 */
434
	private function getRemoteInstances(): array {
435
		return [];
436
	}
437
438
439
	/**
440
	 * should be used to manage results from events, like sending mails on user creation
441
	 *
442
	 * @param string $token
443
	 */
444
	public function manageResults(string $token): void {
445
		try {
446
			$wrappers = $this->remoteWrapperRequest->getByToken($token);
447
		} catch (JsonException | ModelException $e) {
448
			return;
449
		}
450
451
		$event = null;
452
		$events = [];
453
		foreach ($wrappers as $wrapper) {
454
			if ($wrapper->getStatus() !== GSWrapper::STATUS_DONE) {
455
				return;
456
			}
457
458
			$events[$wrapper->getInstance()] = $event = $wrapper->getEvent();
459
		}
460
461
		if ($event === null) {
462
			return;
463
		}
464
465
		try {
466
			$gs = $this->getFederatedItem($event, false);
467
			$gs->result($events);
468
		} catch (FederatedEventException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
469
		}
470
	}
471
472
}
473
474