Completed
Pull Request — master (#597)
by Maxence
02:57
created

FederatedEventService::newEvent()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 37
rs 8.7057
c 0
b 0
f 0
cc 6
nc 10
nop 1
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\nc22\NC22Signature;
35
use daita\MySmallPhpTools\Exceptions\RequestNetworkException;
36
use daita\MySmallPhpTools\Model\Nextcloud\nc22\NC22Request;
37
use daita\MySmallPhpTools\Model\Request;
38
use daita\MySmallPhpTools\Traits\Nextcloud\nc22\TNC22Request;
39
use daita\MySmallPhpTools\Traits\TStringTools;
40
use OC;
41
use OCA\Circles\Db\EventWrapperRequest;
42
use OCA\Circles\Db\MemberRequest;
43
use OCA\Circles\Db\RemoteRequest;
44
use OCA\Circles\Db\ShareLockRequest;
45
use OCA\Circles\Exceptions\FederatedEventException;
46
use OCA\Circles\Exceptions\FederatedItemException;
47
use OCA\Circles\Exceptions\FederatedShareBelongingException;
48
use OCA\Circles\Exceptions\FederatedShareNotFoundException;
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\RemoteInstanceException;
54
use OCA\Circles\Exceptions\RemoteNotFoundException;
55
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
56
use OCA\Circles\Exceptions\RequestBuilderException;
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\IFederatedItemLimitedToInstanceWithMembership;
65
use OCA\Circles\IFederatedItemLoopbackTest;
66
use OCA\Circles\IFederatedItemMemberCheckNotRequired;
67
use OCA\Circles\IFederatedItemMemberEmpty;
68
use OCA\Circles\IFederatedItemMemberOptional;
69
use OCA\Circles\IFederatedItemMemberRequired;
70
use OCA\Circles\IFederatedItemMustBeInitializedLocally;
71
use OCA\Circles\IFederatedItemSharedItem;
72
use OCA\Circles\Model\Circle;
73
use OCA\Circles\Model\Federated\EventWrapper;
74
use OCA\Circles\Model\Federated\FederatedEvent;
75
use OCA\Circles\Model\Federated\RemoteInstance;
76
use OCA\Circles\Model\Member;
77
use ReflectionClass;
78
use ReflectionException;
79
80
81
/**
82
 * Class FederatedEventService
83
 *
84
 * @package OCA\Circles\Service
85
 */
86
class FederatedEventService extends NC22Signature {
87
88
89
	use TNC22Request;
90
	use TStringTools;
91
92
93
	/** @var EventWrapperRequest */
94
	private $eventWrapperRequest;
95
96
	/** @var RemoteRequest */
97
	private $remoteRequest;
98
99
	/** @var ShareLockRequest */
100
	private $shareLockRequest;
101
102
	/** @var MemberRequest */
103
	private $memberRequest;
104
105
	/** @var RemoteUpstreamService */
106
	private $remoteUpstreamService;
107
108
	/** @var InterfaceService */
109
	private $interfaceService;
110
111
	/** @var ConfigService */
112
	private $configService;
113
114
115
	/**
116
	 * FederatedEventService constructor.
117
	 *
118
	 * @param EventWrapperRequest $eventWrapperRequest
119
	 * @param RemoteRequest $remoteRequest
120
	 * @param MemberRequest $memberRequest
121
	 * @param ShareLockRequest $shareLockRequest
122
	 * @param RemoteUpstreamService $remoteUpstreamService
123
	 * @param InterfaceService $interfaceService
124
	 * @param ConfigService $configService
125
	 */
126
	public function __construct(
127
		EventWrapperRequest $eventWrapperRequest,
128
		RemoteRequest $remoteRequest,
129
		MemberRequest $memberRequest,
130
		ShareLockRequest $shareLockRequest,
131
		RemoteUpstreamService $remoteUpstreamService,
132
		InterfaceService $interfaceService,
133
		ConfigService $configService
134
	) {
135
		$this->eventWrapperRequest = $eventWrapperRequest;
136
		$this->remoteRequest = $remoteRequest;
137
		$this->shareLockRequest = $shareLockRequest;
138
		$this->memberRequest = $memberRequest;
139
		$this->remoteUpstreamService = $remoteUpstreamService;
140
		$this->interfaceService = $interfaceService;
141
		$this->configService = $configService;
142
	}
143
144
145
	/**
146
	 * Called when creating a new Event.
147
	 * This method will manage the event locally and upstream the payload if needed.
148
	 *
149
	 * @param FederatedEvent $event
150
	 *
151
	 * @return array
152
	 * @throws FederatedEventException
153
	 * @throws FederatedItemException
154
	 * @throws InitiatorNotConfirmedException
155
	 * @throws OwnerNotFoundException
156
	 * @throws RemoteNotFoundException
157
	 * @throws RemoteResourceNotFoundException
158
	 * @throws UnknownRemoteException
159
	 * @throws RemoteInstanceException
160
	 * @throws RequestBuilderException
161
	 */
162
	public function newEvent(FederatedEvent $event): array {
163
		$federatedItem = $this->getFederatedItem($event, false);
164
		$this->confirmInitiator($event, true);
165
166
		if ($event->canBypass(FederatedEvent::BYPASS_CIRCLE)) {
167
			$instance = $this->interfaceService->getLocalInstance();
168
		} else {
169
			$instance = $event->getCircle()->getInstance();
170
		}
171
172
		$event->setSource($instance);
173
		if ($this->configService->isLocalInstance($instance)) {
174
			$event->setIncomingOrigin($instance);
175
			$federatedItem->verify($event);
176
177
			if ($event->isDataRequestOnly()) {
178
				return $event->getOutcome();
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->getOutcome();
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
//		$this->confirmSharedItem($event, $item);
269
270
		return $item;
271
	}
272
273
274
	/**
275
	 * Some event might need to bypass some checks
276
	 *
277
	 * @param FederatedEvent $event
278
	 * @param IFederatedItem $item
279
	 */
280
	private function setFederatedEventBypass(FederatedEvent $event, IFederatedItem $item) {
281
		if ($item instanceof IFederatedItemLoopbackTest) {
282
			$event->bypass(FederatedEvent::BYPASS_CIRCLE);
283
			$event->bypass(FederatedEvent::BYPASS_INITIATORCHECK);
284
		}
285
		if ($item instanceof IFederatedItemCircleCheckNotRequired) {
286
			$event->bypass(FederatedEvent::BYPASS_LOCALCIRCLECHECK);
287
		}
288
		if ($item instanceof IFederatedItemMemberCheckNotRequired) {
289
			$event->bypass(FederatedEvent::BYPASS_LOCALMEMBERCHECK);
290
		}
291
		if ($item instanceof IFederatedItemInitiatorCheckNotRequired) {
292
			$event->bypass(FederatedEvent::BYPASS_INITIATORCHECK);
293
		}
294
		if ($item instanceof IFederatedItemInitiatorMembershipNotRequired) {
295
			$event->bypass(FederatedEvent::BYPASS_INITIATORMEMBERSHIP);
296
		}
297
	}
298
299
	/**
300
	 * Some event might require additional check
301
	 *
302
	 * @param FederatedEvent $event
303
	 * @param IFederatedItem $item
304
	 * @param bool $checkLocalOnly
305
	 *
306
	 * @throws FederatedEventException
307
	 */
308
	private function confirmRequiredCondition(
309
		FederatedEvent $event,
310
		IFederatedItem $item,
311
		bool $checkLocalOnly = true
312
	) {
313
		if (!$event->canBypass(FederatedEvent::BYPASS_CIRCLE) && !$event->hasCircle()) {
314
			throw new FederatedEventException('FederatedEvent has no Circle linked');
315
		}
316
317
		// TODO: enforce IFederatedItemMemberEmpty if no member
318
		if ($item instanceof IFederatedItemMemberEmpty) {
319
			$event->setMember(null);
320
		} else if ($item instanceof IFederatedItemMemberRequired && !$event->hasMember()) {
321
			throw new FederatedEventException('FederatedEvent has no Member linked');
322
		}
323
324
		if ($event->hasMember()
325
			&& !($item instanceof IFederatedItemMemberRequired)
326
			&& !($item instanceof IFederatedItemMemberOptional)) {
327
			throw new FederatedEventException(
328
				get_class($item)
329
				. ' does not implements IFederatedItemMemberOptional nor IFederatedItemMemberRequired'
330
			);
331
		}
332
333
		if ($item instanceof IFederatedItemMustBeInitializedLocally && $checkLocalOnly) {
334
			throw new FederatedEventException('FederatedItem must be executed locally');
335
		}
336
	}
337
338
339
	/**
340
	 * @param FederatedEvent $event
341
	 * @param IFederatedItem $item
342
	 *
343
	 * @throws FederatedEventException
344
	 * @throws FederatedShareBelongingException
345
	 * @throws FederatedShareNotFoundException
346
	 * @throws OwnerNotFoundException
347
	 */
348
	private function confirmSharedItem(FederatedEvent $event, IFederatedItem $item): void {
349
		if (!$item instanceof IFederatedItemSharedItem) {
350
			return;
351
		}
352
353
		if ($event->getItemId() === '') {
354
			throw new FederatedEventException('FederatedItem must contains ItemId');
355
		}
356
357
		if ($this->configService->isLocalInstance($event->getCircle()->getInstance())) {
358
			$shareLock = $this->shareLockRequest->getShare($event->getItemId());
359
			if ($shareLock->getInstance() !== $event->getIncomingOrigin()) {
360
				throw new FederatedShareBelongingException('ShareLock belongs to another instance');
361
			}
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
	 *
388
	 * @throws RequestBuilderException
389
	 */
390
	public function initBroadcast(FederatedEvent $event): void {
391
		$instances = $this->getInstances($event);
392
		if (empty($instances) && !$event->isAsync()) {
393
			return;
394
		}
395
396
		$wrapper = new EventWrapper();
397
		$wrapper->setEvent($event);
398
		$wrapper->setToken($this->uuid());
399
		$wrapper->setCreation(time());
400
		$wrapper->setSeverity($event->getSeverity());
401
402
		if ($event->isAsync()) {
403
			$wrapper->setInstance($this->configService->getLoopbackInstance());
404
			$this->eventWrapperRequest->save($wrapper);
405
			echo json_encode($wrapper);
406
		}
407
408
		foreach ($instances as $instance) {
409
			if ($event->getCircle()->isConfig(Circle::CFG_LOCAL)) {
410
				break;
411
			}
412
413
			$wrapper->setInstance($instance->getInstance());
414
			$wrapper->setInterface($instance->getInterface());
415
			$this->eventWrapperRequest->save($wrapper);
416
		}
417
418
		$request = new NC22Request('', Request::TYPE_POST);
419
		$this->configService->configureLoopbackRequest(
420
			$request,
421
			'circles.EventWrapper.asyncBroadcast',
422
			['token' => $wrapper->getToken()]
423
		);
424
425
		$event->setWrapperToken($wrapper->getToken());
426
427
		try {
428
			$this->doRequest($request);
429
		} catch (RequestNetworkException $e) {
430
			$this->e($e, ['wrapper' => $wrapper]);
431
		}
432
	}
433
434
435
	/**
436
	 * @param FederatedEvent $event
437
	 *
438
	 * @return RemoteInstance[]
439
	 * @throws RequestBuilderException
440
	 */
441
	public function getInstances(FederatedEvent $event): array {
442
		if (!$event->hasCircle()) {
443
			return [];
444
		}
445
446
		$circle = $event->getCircle();
447
		$broadcastAsFederated = $event->getData()->gBool('broadcastAsFederated');
448
		$instances = $this->remoteRequest->getOutgoingRecipient($circle, $broadcastAsFederated);
449
450
		if ($event->isLimitedToInstanceWithMember()) {
451
			$knownInstances = $this->memberRequest->getMemberInstances($circle->getSingleId());
452
			$instances = array_filter(
453
				array_map(
454
					function(RemoteInstance $instance) use ($knownInstances) {
455
						if (!in_array($instance->getInstance(), $knownInstances)) {
456
							return null;
457
						}
458
459
						return $instance;
460
					}, $instances
461
				)
462
			);
463
		}
464
465
		// Check that in case of event has Member, the instance of that member is in the list.
466
		if ($event->hasMember()
467
			&& !$this->configService->isLocalInstance($event->getMember()->getInstance())) {
468
			$currentInstances = array_map(
469
				function(RemoteInstance $instance): string {
470
					return $instance->getInstance();
471
				}, $instances
472
			);
473
474
			if (!in_array($event->getMember()->getInstance(), $currentInstances)) {
475
				try {
476
					$instances[] = $this->remoteRequest->getFromInstance($event->getMember()->getInstance());
477
				} catch (RemoteNotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
478
				}
479
			}
480
		}
481
482
		return $instances;
483
	}
484
485
486
	/**
487
	 * should be used to manage results from events, like sending mails on user creation
488
	 *
489
	 * @param string $token
490
	 */
491
	public function manageResults(string $token): void {
492
		try {
493
			$wrappers = $this->eventWrapperRequest->getByToken($token);
494
		} catch (JsonException | ModelException $e) {
495
			return;
496
		}
497
498
		$event = null;
499
		$results = [];
500
		foreach ($wrappers as $wrapper) {
501
			if ($wrapper->getStatus() !== EventWrapper::STATUS_DONE) {
502
				return;
503
			}
504
505
			if (is_null($event)) {
506
				$event = $wrapper->getEvent();
507
			}
508
509
			$results[$wrapper->getInstance()] = $wrapper->getResult();
510
		}
511
512
		if (is_null($event)) {
513
			return;
514
		}
515
516
		try {
517
			$gs = $this->getFederatedItem($event, false);
518
			$gs->result($event, $results);
519
		} catch (FederatedEventException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
520
		}
521
	}
522
523
}
524
525