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

FederatedEventService::setFederatedEventBypass()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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