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

FederatedEventService::configureEvent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 2
nc 2
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
			try {
154
				$federatedItem->verify($event);
155
				$reading = $event->getReadingOutcome();
156
				$reading->s('translated', $this->l10n->t($reading->g('message'), $reading->gArray('params')));
157
			} catch (FederatedItemException $e) {
158
				throw new FederatedItemException($this->l10n->t($e->getMessage(), $e->getParams()));
159
			}
160
161
			if (!$event->isAsync()) {
162
				$federatedItem->manage($event);
163
			}
164
165
			$this->initBroadcast($event);
166
		} else {
167
			$this->remoteUpstreamService->confirmEvent($event);
168
			if (!$event->isAsync()) {
169
				$federatedItem->manage($event);
170
			}
171
		}
172
173
	}
174
175
176
	/**
177
	 * This confirmation is optional, method is just here to avoid going too far away on the process
178
	 *
179
	 * @param FederatedEvent $event
180
	 * @param bool $local
181
	 *
182
	 * @throws InitiatorNotConfirmedException
183
	 */
184
	public function confirmInitiator(FederatedEvent $event, bool $local = false): void {
185
		if ($event->canBypass(FederatedEvent::BYPASS_INITIATORCHECK)) {
186
			return;
187
		}
188
189
		$circle = $event->getCircle();
190
		if (!$circle->hasInitiator()) {
191
			throw new InitiatorNotConfirmedException('Initiator does not exist');
192
		}
193
194
		if ($local) {
195
			if (!$this->configService->isLocalInstance($circle->getInitiator()->getInstance())) {
196
				throw new InitiatorNotConfirmedException(
197
					'Initiator is not from the instance at the origin of the request'
198
				);
199
			}
200
		} else {
201
			if ($circle->getInitiator()->getInstance() !== $event->getIncomingOrigin()) {
202
				throw new InitiatorNotConfirmedException(
203
					'Initiator must belong to the instance at the origin of the request'
204
				);
205
			}
206
		}
207
	}
208
209
210
211
//	/**
212
//	 * We check that the event can be managed/checked locally or if the owner of the circle belongs to
213
//	 * an other instance of Nextcloud
214
//	 *
215
//	 * @param RemoteEvent $event
216
//	 *
217
//	 * @return bool
218
//	 * @throws CircleNotFoundException
219
//	 * @throws OwnerNotFoundException
220
//	 */
221
//	public function isLocalEvent(RemoteEvent $event): bool {
222
////		if ($event->isLocal()) {
223
////			return true;
224
////		}
225
//
226
//		$circle = $event->getCircle();
227
//
228
////		if (!$circle->hasOwner()) {
229
//		return ($this->configService->isLocalInstance($circle->getInstance()));
230
////		}
231
//
232
////		if ($event->isVerifiedCircle()) {
233
////			$localCircle = $event->getCircle();
234
////		} else {
235
////			$localCircle = $this->circleRequest->getCircle($circle->getId());
236
////		}
237
////
238
////		$owner = $localCircle->getOwner();
239
////		if ($owner->getInstance() === ''
240
////			|| $this->configService->isLocalInstance($owner->getInstance())) {
241
////			return true;
242
////		}
243
////
244
////		return false;
245
//	}
246
247
248
	/**
249
	 * @param FederatedEvent $event
250
	 * @param bool $checkLocalOnly
251
	 *
252
	 * @return IFederatedItem
253
	 * @throws FederatedEventException
254
	 */
255
	public function getFederatedItem(FederatedEvent $event, bool $checkLocalOnly = true): IFederatedItem {
256
		$class = $event->getClass();
257
		try {
258
			$test = new ReflectionClass($class);
259
		} catch (ReflectionException $e) {
260
			throw new FederatedEventException('ReflectionException with ' . $class . ': ' . $e->getMessage());
261
		}
262
263
		if (!in_array(IFederatedItem::class, $test->getInterfaceNames())) {
264
			throw new FederatedEventException($class . ' does not implements IFederatedItem');
265
		}
266
267
		$item = OC::$server->get($class);
268
		if (!($item instanceof IFederatedItem)) {
269
			throw new FederatedEventException($class . ' not an IFederatedItem');
270
		}
271
272
		$this->setFederatedEventBypass($event, $item);
273
		$this->confirmRequiredCondition($event, $item, $checkLocalOnly);
274
		$this->configureEvent($event, $item);
275
276
		return $item;
277
	}
278
279
280
	/**
281
	 * Some event might need to bypass some checks
282
	 *
283
	 * @param FederatedEvent $event
284
	 * @param IFederatedItem $gs
285
	 */
286
	private function setFederatedEventBypass(FederatedEvent $event, IFederatedItem $gs) {
287
		if ($gs instanceof IFederatedItemCircleCheckNotRequired) {
288
			$event->bypass(FederatedEvent::BYPASS_LOCALCIRCLECHECK);
289
		}
290
		if ($gs instanceof IFederatedItemMemberCheckNotRequired) {
291
			$event->bypass(FederatedEvent::BYPASS_LOCALMEMBERCHECK);
292
		}
293
		if ($gs instanceof IFederatedItemInitiatorCheckNotRequired) {
294
			$event->bypass(FederatedEvent::BYPASS_INITIATORCHECK);
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->hasCircle()) {
313
			throw new FederatedEventException('FederatedEvent has no Circle linked');
314
		}
315
		if ($item instanceof IFederatedItemMemberEmpty) {
316
			$event->setMember(null);
317
		}
318
		if ($item instanceof IFederatedItemMemberRequired && !$event->hasMember()) {
319
			throw new FederatedEventException('FederatedEvent has no Member linked');
320
		}
321
		if ($event->hasMember()
322
			&& !($item instanceof IFederatedItemMemberRequired)
323
			&& !($item instanceof IFederatedItemMemberOptional)) {
324
			throw new FederatedEventException(
325
				get_class($item)
326
				. ' does not implements IFederatedItemMemberOptional nor IFederatedItemMemberRequired'
327
			);
328
		}
329
		if ($item instanceof IFederatedItemLocalOnly && $checkLocalOnly) {
330
			throw new FederatedEventException('FederatedItem must be executed locally');
331
		}
332
	}
333
334
335
	/**
336
	 * @param FederatedEvent $event
337
	 * @param IFederatedItem $item
338
	 */
339
	private function configureEvent(FederatedEvent $event, IFederatedItem $item) {
340
		if ($item instanceof IFederatedItemAsync) {
341
			$event->setAsync(true);
342
		}
343
	}
344
345
346
	/**
347
	 * async the process, generate a local request that will be closed.
348
	 *
349
	 * @param FederatedEvent $event
350
	 * @param array $filter
351
	 */
352
	public function initBroadcast(FederatedEvent $event, array $filter = []): void {
353
		$instances = array_diff($this->getInstances($event->isAsync()), $filter);
354
		if (empty($instances)) {
355
			return;
356
		}
357
358
		$wrapper = new RemoteWrapper();
359
		$wrapper->setEvent($event);
360
		$wrapper->setToken($this->uuid());
361
		$wrapper->setCreation(time());
362
		$wrapper->setSeverity($event->getSeverity());
363
364
		foreach ($instances as $instance) {
365
			$wrapper->setInstance($instance);
366
			$this->remoteWrapperRequest->create($wrapper);
367
		}
368
369
		$request = new NC21Request('', Request::TYPE_POST);
370
		$this->configService->configureRequest(
371
			$request, 'circles.RemoteWrapper.asyncBroadcast', ['token' => $wrapper->getToken()]
372
		);
373
374
		$event->setWrapperToken($wrapper->getToken());
375
376
		try {
377
			$this->doRequest($request);
378
		} catch (RequestNetworkException $e) {
379
			$this->e($e, ['wrapper' => $wrapper]);
380
		}
381
	}
382
383
384
	/**
385
	 * @param bool $all
386
	 * @param Circle|null $circle
387
	 *
388
	 * @return array
389
	 */
390
	public function getInstances(bool $all = false, ?Circle $circle = null): array {
391
		$local = $this->configService->getLocalInstance();
392
		$instances = $this->remoteRequest->getOutgoingRecipient($circle);
393
		$instances = array_merge(
394
			[$local], array_map(
395
						function(RemoteInstance $instance): string {
396
							return $instance->getInstance();
397
						}, $instances
398
					)
399
		);
400
401
		if ($all) {
402
			return $instances;
403
		}
404
405
		return array_values(
406
			array_diff($instances, array_merge($this->configService->getTrustedDomains(), [$local]))
407
		);
408
	}
409
410
411
	/**
412
	 * @param array $current
413
	 */
414
	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...
415
//		$known = $this->remoteRequest->getFromType(RemoteInstance::TYPE_GLOBAL_SCALE);
416
	}
417
418
	/**
419
	 * @return array
420
	 */
421
	private function getRemoteInstances(): array {
422
		return [];
423
	}
424
425
426
	/**
427
	 * should be used to manage results from events, like sending mails on user creation
428
	 *
429
	 * @param string $token
430
	 */
431
	public function manageResults(string $token): void {
432
		try {
433
			$wrappers = $this->remoteWrapperRequest->getByToken($token);
434
		} catch (JsonException | ModelException $e) {
435
			return;
436
		}
437
438
		$event = null;
439
		$events = [];
440
		foreach ($wrappers as $wrapper) {
441
			if ($wrapper->getStatus() !== GSWrapper::STATUS_DONE) {
442
				return;
443
			}
444
445
			$events[$wrapper->getInstance()] = $event = $wrapper->getEvent();
446
		}
447
448
		if ($event === null) {
449
			return;
450
		}
451
452
		try {
453
			$gs = $this->getFederatedItem($event, false);
454
			$gs->result($events);
455
		} catch (FederatedEventException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
456
		}
457
	}
458
459
}
460
461