Completed
Pull Request — master (#597)
by Maxence
07:37 queued 05:01
created

FederatedEventService::newEvent()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 35
rs 8.7377
c 0
b 0
f 0
cc 6
nc 5
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 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 NC22Signature {
88
89
90
	use TNC22Request;
91
	use TStringTools;
92
93
94
	/** @var IL10N */
95
	private $l10n;
96
97
	/** @var EventWrapperRequest */
98
	private $eventWrapperRequest;
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 EventWrapperRequest $eventWrapperRequest
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, EventWrapperRequest $eventWrapperRequest, RemoteRequest $remoteRequest,
132
		MemberRequest $memberRequest, ShareLockRequest $shareLockRequest,
133
		RemoteUpstreamService $remoteUpstreamService,
134
		ConfigService $configService
135
	) {
136
		$this->l10n = $l10n;
137
		$this->eventWrapperRequest = $eventWrapperRequest;
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 array
153
	 * @throws FederatedEventException
154
	 * @throws FederatedItemException
155
	 * @throws InitiatorNotConfirmedException
156
	 * @throws OwnerNotFoundException
157
	 * @throws RemoteNotFoundException
158
	 * @throws RemoteResourceNotFoundException
159
	 * @throws UnknownRemoteException
160
	 * @throws RemoteInstanceException
161
	 * @throws RequestBuilderException
162
	 */
163
	public function newEvent(FederatedEvent $event): array {
164
		$event->setSource($this->configService->getLocalInstance());
165
166
		$federatedItem = $this->getFederatedItem($event, false);
167
168
		$this->confirmInitiator($event, true);
169
		if ($event->canBypass(FederatedEvent::BYPASS_CIRCLE)
170
			|| $this->configService->isLocalInstance($event->getCircle()->getInstance())) {
171
//			$event->setIncomingOrigin($event->getCircle()->getInstance());
172
			$event->setIncomingOrigin($this->configService->getLocalInstance());
173
174
			$federatedItem->verify($event);
175
176
			if ($event->isDataRequestOnly()) {
177
				return $event->getOutcome();
178
			}
179
180
			if (!$event->isAsync()) {
181
				$federatedItem->manage($event);
182
			}
183
184
			$this->initBroadcast($event);
185
		} else {
186
			$this->remoteUpstreamService->confirmEvent($event);
187
			if ($event->isDataRequestOnly()) {
188
				return $event->getOutcome();
189
			}
190
191
//			if (!$event->isAsync()) {
192
//				$federatedItem->manage($event);
193
//			}
194
		}
195
196
		return $event->getOutcome();
197
	}
198
199
200
	/**
201
	 * This confirmation is optional, method is just here to avoid going too far away on the process
202
	 *
203
	 * @param FederatedEvent $event
204
	 * @param bool $local
205
	 *
206
	 * @throws InitiatorNotConfirmedException
207
	 */
208
	public function confirmInitiator(FederatedEvent $event, bool $local = false): void {
209
		if ($event->canBypass(FederatedEvent::BYPASS_INITIATORCHECK)) {
210
			return;
211
		}
212
213
		$circle = $event->getCircle();
214
		if (!$circle->hasInitiator()) {
215
			throw new InitiatorNotConfirmedException('Initiator does not exist');
216
		}
217
218
		if ($local) {
219
			if (!$this->configService->isLocalInstance($circle->getInitiator()->getInstance())) {
220
				throw new InitiatorNotConfirmedException(
221
					'Initiator is not from the instance at the origin of the request'
222
				);
223
			}
224
		} else {
225
			if ($circle->getInitiator()->getInstance() !== $event->getIncomingOrigin()) {
226
				throw new InitiatorNotConfirmedException(
227
					'Initiator must belong to the instance at the origin of the request'
228
				);
229
			}
230
		}
231
232
		if (!$event->canBypass(FederatedEvent::BYPASS_INITIATORMEMBERSHIP)
233
			&& $circle->getInitiator()->getLevel() < Member::LEVEL_MEMBER) {
234
			throw new InitiatorNotConfirmedException('Initiator must be a member of the Circle');
235
		}
236
	}
237
238
239
	/**
240
	 * @param FederatedEvent $event
241
	 * @param bool $checkLocalOnly
242
	 *
243
	 * @return IFederatedItem
244
	 * @throws FederatedEventException
245
	 */
246
	public function getFederatedItem(FederatedEvent $event, bool $checkLocalOnly = true): IFederatedItem {
247
		$class = $event->getClass();
248
		try {
249
			$test = new ReflectionClass($class);
250
		} catch (ReflectionException $e) {
251
			throw new FederatedEventException('ReflectionException with ' . $class . ': ' . $e->getMessage());
252
		}
253
254
		if (!in_array(IFederatedItem::class, $test->getInterfaceNames())) {
255
			throw new FederatedEventException($class . ' does not implements IFederatedItem');
256
		}
257
258
		$item = OC::$server->get($class);
259
		if (!($item instanceof IFederatedItem)) {
260
			throw new FederatedEventException($class . ' not an IFederatedItem');
261
		}
262
263
		$this->setFederatedEventBypass($event, $item);
264
		$this->confirmRequiredCondition($event, $item, $checkLocalOnly);
265
		$this->configureEvent($event, $item);
266
267
//		$this->confirmSharedItem($event, $item);
268
269
		return $item;
270
	}
271
272
273
	/**
274
	 * Some event might need to bypass some checks
275
	 *
276
	 * @param FederatedEvent $event
277
	 * @param IFederatedItem $item
278
	 */
279
	private function setFederatedEventBypass(FederatedEvent $event, IFederatedItem $item) {
280
		if ($item instanceof IFederatedItemLoopbackTest) {
281
			$event->bypass(FederatedEvent::BYPASS_CIRCLE);
282
			$event->bypass(FederatedEvent::BYPASS_INITIATORCHECK);
283
		}
284
		if ($item instanceof IFederatedItemCircleCheckNotRequired) {
285
			$event->bypass(FederatedEvent::BYPASS_LOCALCIRCLECHECK);
286
		}
287
		if ($item instanceof IFederatedItemMemberCheckNotRequired) {
288
			$event->bypass(FederatedEvent::BYPASS_LOCALMEMBERCHECK);
289
		}
290
		if ($item instanceof IFederatedItemInitiatorCheckNotRequired) {
291
			$event->bypass(FederatedEvent::BYPASS_INITIATORCHECK);
292
		}
293
		if ($item instanceof IFederatedItemInitiatorMembershipNotRequired) {
294
			$event->bypass(FederatedEvent::BYPASS_INITIATORMEMBERSHIP);
295
		}
296
	}
297
298
	/**
299
	 * Some event might require additional check
300
	 *
301
	 * @param FederatedEvent $event
302
	 * @param IFederatedItem $item
303
	 * @param bool $checkLocalOnly
304
	 *
305
	 * @throws FederatedEventException
306
	 */
307
	private function confirmRequiredCondition(
308
		FederatedEvent $event,
309
		IFederatedItem $item,
310
		bool $checkLocalOnly = true
311
	) {
312
		if (!$event->canBypass(FederatedEvent::BYPASS_CIRCLE) && !$event->hasCircle()) {
313
			throw new FederatedEventException('FederatedEvent has no Circle linked');
314
		}
315
316
		// TODO: enforce IFederatedItemMemberEmpty if no member
317
		if ($item instanceof IFederatedItemMemberEmpty) {
318
			$event->setMember(null);
319
		} else if ($item instanceof IFederatedItemMemberRequired && !$event->hasMember()) {
320
			throw new FederatedEventException('FederatedEvent has no Member linked');
321
		}
322
323
		if ($event->hasMember()
324
			&& !($item instanceof IFederatedItemMemberRequired)
325
			&& !($item instanceof IFederatedItemMemberOptional)) {
326
			throw new FederatedEventException(
327
				get_class($item)
328
				. ' does not implements IFederatedItemMemberOptional nor IFederatedItemMemberRequired'
329
			);
330
		}
331
332
		if ($item instanceof IFederatedItemMustBeInitializedLocally && $checkLocalOnly) {
333
			throw new FederatedEventException('FederatedItem must be executed locally');
334
		}
335
	}
336
337
338
	/**
339
	 * @param FederatedEvent $event
340
	 * @param IFederatedItem $item
341
	 *
342
	 * @throws FederatedEventException
343
	 * @throws FederatedShareBelongingException
344
	 * @throws FederatedShareNotFoundException
345
	 * @throws OwnerNotFoundException
346
	 */
347
	private function confirmSharedItem(FederatedEvent $event, IFederatedItem $item): void {
348
		if (!$item instanceof IFederatedItemSharedItem) {
349
			return;
350
		}
351
352
		if ($event->getItemId() === '') {
353
			throw new FederatedEventException('FederatedItem must contains ItemId');
354
		}
355
356
		if ($this->configService->isLocalInstance($event->getCircle()->getInstance())) {
357
			$shareLock = $this->shareLockRequest->getShare($event->getItemId());
358
			if ($shareLock->getInstance() !== $event->getIncomingOrigin()) {
359
				throw new FederatedShareBelongingException('ShareLock belongs to another instance');
360
			}
361
		}
362
	}
363
364
365
	/**
366
	 * @param FederatedEvent $event
367
	 * @param IFederatedItem $item
368
	 */
369
	private function configureEvent(FederatedEvent $event, IFederatedItem $item) {
370
		if ($item instanceof IFederatedItemAsyncProcess) {
371
			$event->setAsync(true);
372
		}
373
		if ($item instanceof IFederatedItemLimitedToInstanceWithMembership) {
374
			$event->setLimitedToInstanceWithMember(true);
375
		}
376
		if ($item instanceof IFederatedItemDataRequestOnly) {
377
			$event->setDataRequestOnly(true);
378
		}
379
	}
380
381
382
	/**
383
	 * async the process, generate a local request that will be closed.
384
	 *
385
	 * @param FederatedEvent $event
386
	 * @param array $filter
387
	 *
388
	 * @throws RequestBuilderException
389
	 */
390
	public function initBroadcast(FederatedEvent $event, array $filter = []): void {
391
		$instances = array_diff($this->getInstances($event), $filter);
392
		if (empty($instances)) {
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
		foreach ($instances as $instance) {
403
			if ($event->hasCircle()
404
				&& $event->getCircle()->isConfig(Circle::CFG_LOCAL)
405
				&& !$this->configService->isLocalInstance($instance)) {
406
				continue;
407
			}
408
409
			$wrapper->setInstance($instance);
410
			$this->eventWrapperRequest->create($wrapper);
411
		}
412
413
		$request = new NC22Request('', Request::TYPE_POST);
414
		$this->configService->configureLoopbackRequest(
415
			$request,
416
			'circles.EventWrapper.asyncBroadcast',
417
			['token' => $wrapper->getToken()]
418
		);
419
420
		$event->setWrapperToken($wrapper->getToken());
421
422
		try {
423
			$this->doRequest($request);
424
		} catch (RequestNetworkException $e) {
425
			$this->e($e, ['wrapper' => $wrapper]);
426
		}
427
	}
428
429
430
	/**
431
	 * @param FederatedEvent $event
432
	 *
433
	 * @return array
434
	 * @throws RequestBuilderException
435
	 */
436
	public function getInstances(FederatedEvent $event): array {
437
		$local = $this->configService->getFrontalInstance();
438
439
		if (!$event->hasCircle()) {
440
			return [$this->configService->getLoopbackInstance()];
441
		}
442
443
		$circle = $event->getCircle();
444
		$instances = array_map(
445
			function(RemoteInstance $instance): string {
446
				return $instance->getInstance();
447
			},
448
			$this->remoteRequest->getOutgoingRecipient(
449
				$circle,
450
				$event->getData()->gBool('broadcastAsFederated')
451
			)
452
		);
453
454
		if ($event->isLimitedToInstanceWithMember()) {
455
			$instances =
456
				array_intersect(
457
					$instances, $this->memberRequest->getMemberInstances($circle->getSingleId())
458
				);
459
		}
460
461
		if ($event->hasMember()
462
			&& !$this->configService->isLocalInstance(($event->getMember()->getInstance()))
463
			&& !in_array($event->getMember()->getInstance(), $instances)) {
464
			// At that point, we know that the member belongs to a _known_ remote instance
465
			$instances[] = $event->getMember()->getInstance();
466
		}
467
468
		$instances = array_merge([$local], $instances);
469
470
		if ($event->isAsync()) {
471
			return $instances;
472
		}
473
474
		return array_values(
475
			array_diff($instances, array_merge($this->configService->getTrustedDomains(), [$local]))
476
		);
477
	}
478
479
480
	/**
481
	 * @param array $current
482
	 */
483
	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...
484
//		$known = $this->remoteRequest->getFromType(RemoteInstance::TYPE_GLOBAL_SCALE);
485
	}
486
487
	/**
488
	 * @return array
489
	 */
490
	private function getRemoteInstances(): array {
491
		return [];
492
	}
493
494
495
	/**
496
	 * should be used to manage results from events, like sending mails on user creation
497
	 *
498
	 * @param string $token
499
	 */
500
	public function manageResults(string $token): void {
501
		try {
502
			$wrappers = $this->eventWrapperRequest->getByToken($token);
503
		} catch (JsonException | ModelException $e) {
504
			return;
505
		}
506
507
		$event = null;
508
		$results = [];
509
		foreach ($wrappers as $wrapper) {
510
			if ($wrapper->getStatus() !== EventWrapper::STATUS_DONE) {
511
				return;
512
			}
513
514
			if (is_null($event)) {
515
				$event = $wrapper->getEvent();
516
			}
517
518
			$results[$wrapper->getInstance()] = $wrapper->getResult();
519
		}
520
521
		if (is_null($event)) {
522
			return;
523
		}
524
525
		try {
526
			$gs = $this->getFederatedItem($event, false);
527
			$gs->result($event, $results);
528
		} catch (FederatedEventException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
529
		}
530
	}
531
532
}
533
534