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

FederatedEventService::getFederatedItem()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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