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

FederatedEventService::getFederatedItem()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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