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

FederatedEventService::confirmInitiator()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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