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