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

RemoteEventService::isLocalEvent()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 8.5226
c 0
b 0
f 0
cc 7
nc 7
nop 1
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\Model\Nextcloud\nc21\NC21Request;
37
use daita\MySmallPhpTools\Model\Request;
38
use daita\MySmallPhpTools\Traits\Nextcloud\nc21\TNC21Request;
39
use daita\MySmallPhpTools\Traits\TStringTools;
40
use OC;
41
use OC\Security\IdentityProof\Signer;
42
use OCA\Circles\Db\CircleRequest;
43
use OCA\Circles\Db\GSEventsRequest;
44
use OCA\Circles\Exceptions\CircleNotFoundException;
45
use OCA\Circles\Exceptions\GSStatusException;
46
use OCA\Circles\Exceptions\JsonException;
47
use OCA\Circles\Exceptions\ModelException;
48
use OCA\Circles\Exceptions\RemoteEventException;
49
use OCA\Circles\IRemoteEvent;
50
use OCA\Circles\Model\GlobalScale\GSWrapper;
51
use OCA\Circles\Model\Member;
52
use OCA\Circles\Model\Remote\RemoteEvent;
53
use OCA\Circles\Model\Remote\RemoteWrapper;
54
use OCP\IURLGenerator;
55
use OCP\IUserManager;
56
use OCP\IUserSession;
57
use ReflectionClass;
58
use ReflectionException;
59
60
61
/**
62
 * Class RemoteService
63
 *
64
 * @package OCA\Circles\Service
65
 */
66
class RemoteEventService extends NC21Signature {
67
68
69
	use TNC21Request;
70
	use TStringTools;
71
72
73
	/** @var IURLGenerator */
74
	private $urlGenerator;
75
76
	/** @var IUserManager */
77
	private $userManager;
78
79
	/** @var IUserSession */
80
	private $userSession;
81
82
	/** @var Signer */
83
	private $signer;
84
85
	/** @var GSEventsRequest */
86
	private $gsEventsRequest;
87
88
89
	private $circleRequest;
90
91
	/** @var ConfigService */
92
	private $configService;
93
94
	/** @var MiscService */
95
	private $miscService;
96
97
98
	/**
99
	 * GlobalScaleService constructor.
100
	 *
101
	 * @param IURLGenerator $urlGenerator
102
	 * @param IUserManager $userManager
103
	 * @param IUserSession $userSession
104
	 * @param Signer $signer
105
	 * @param GSEventsRequest $gsEventsRequest
106
	 * @param CircleRequest $circleRequest
107
	 * @param ConfigService $configService
108
	 * @param MiscService $miscService
109
	 */
110
	public function __construct(
111
		IURLGenerator $urlGenerator,
112
		IUserManager $userManager,
113
		IUserSession $userSession,
114
		Signer $signer,
115
		GSEventsRequest $gsEventsRequest,
116
		CircleRequest $circleRequest,
117
		ConfigService $configService,
118
		MiscService $miscService
119
	) {
120
		$this->urlGenerator = $urlGenerator;
121
		$this->userManager = $userManager;
122
		$this->userSession = $userSession;
123
		$this->signer = $signer;
124
		$this->gsEventsRequest = $gsEventsRequest;
125
		$this->circleRequest = $circleRequest;
126
		$this->configService = $configService;
127
		$this->miscService = $miscService;
128
	}
129
130
131
	/**
132
	 * Called when creating a new Event.
133
	 * This method will manage the event locally and upstream the payload if needed.
134
	 *
135
	 * @param RemoteEvent $event
136
	 *
137
	 * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
138
	 * @throws RemoteEventException
139
	 */
140
	public function newEvent(RemoteEvent $event): void {
141
		// TODO: setSource
142
		// TODO: Verify Viewer
143
		//$event->setSource($this->configService->getLocalInstance());
144
		$this->verifyViewer($event);
145
		try {
146
			$gs = $this->getRemoteEvent($event);
147
		} catch (RemoteEventException $e) {
148
			$this->e($e);
149
			throw $e;
150
		}
151
152
		try {
153
			if ($this->isLocalEvent($event)) {
154
				$gs->verify($event);
155
				if (!$event->isAsync()) {
156
					$gs->manage($event);
157
				}
158
159
				$this->initBroadcast($event);
160
			} else {
161
				//	$this->confirmEvent($event);
162
			}
163
		} catch (CircleNotFoundException $e) {
164
			$this->e($e, ['event' => $event]);
165
		}
166
167
	}
168
169
170
	/**
171
	 * async the process, generate a local request that will be closed.
172
	 *
173
	 * @param RemoteEvent $event
174
	 */
175
	public function initBroadcast(RemoteEvent $event): void {
176
		$wrapper = new RemoteWrapper();
177
		$wrapper->setEvent($event);
178
		$wrapper->setToken($this->uuid());
179
		$wrapper->setCreation(time());
180
		$wrapper->setSeverity($event->getSeverity());
181
182
		foreach ($this->getInstances($event->isAsync()) as $instance) {
183
			$wrapper->setInstance($instance);
184
			$wrapper = $this->gsEventsRequest->create($wrapper);
0 ignored issues
show
Bug introduced by
It seems like $wrapper can also be of type object<OCA\Circles\Model\Remote\RemoteWrapper>; however, OCA\Circles\Db\GSEventsRequest::create() does only seem to accept object<OCA\Circles\Model\GlobalScale\GSWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
185
		}
186
187
		$event->setWrapperToken($wrapper->getToken());
188
189
		$request = new NC21Request('', Request::TYPE_POST);
190
		$this->configService->configureRequest(
191
			$request, 'circles.GlobalScale.asyncBroadcast', ['token' => $wrapper->getToken()]
192
		);
193
194
		try {
195
			$this->doRequest($request);
196
		} catch (RequestNetworkException $e) {
197
			$this->e($e, ['wrapper' => $wrapper]);
198
		}
199
	}
200
201
202
	private function verifyViewer(RemoteEvent $event) {
203
		if (!$event->hasCircle() || !$event->getCircle()->hasViewer()) {
204
			return;
205
		}
206
207
		// TODO: Verify/Set Source of Viewer (check based on the source of the request)
208
//		if ($event->isLocal()) {
209
//		}
210
211
		$circle = $event->getCircle();
212
		$viewer = $circle->getViewer();
213
214
		try {
215
			$localCircle = $this->circleRequest->getCircle($circle->getId(), $viewer);
216
		} catch (CircleNotFoundException $e) {
217
			return;
218
		}
219
220
		if (!$this->compareMembers($viewer, $localCircle->getViewer())) {
221
			return;
222
		}
223
224
		$event->setVerifiedViewer(true);
225
		$event->setCircle($localCircle);
226
	}
227
228
229
	/**
230
	 * We check that the event can be managed/checked locally or if the owner of the circle belongs to
231
	 * an other instance of Nextcloud
232
	 *
233
	 * @param RemoteEvent $event
234
	 *
235
	 * @return bool
236
	 * @throws CircleNotFoundException
237
	 */
238
	private function isLocalEvent(RemoteEvent $event): bool {
239
		if ($event->isLocal()) {
240
			return true;
241
		}
242
243
		if (!$event->hasCircle()) {
244
			return false;
245
		}
246
247
		$circle = $event->getCircle();
248
		if (!$circle->hasOwner()) {
249
			// TODO: Check on circle with no owner (add getInstance() to Circle)
250
			return false;
251
		}
252
253
		if ($event->isVerifiedViewer()) {
254
			$localCircle = $event->getCircle();
255
		} else {
256
			$localCircle = $this->circleRequest->getCircle($circle->getId());
257
		}
258
259
		$owner = $localCircle->getOwner();
260
		if ($owner->getInstance() === ''
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return $owner->getInstan...$owner->getInstance());.
Loading history...
261
			|| $this->configService->isLocalInstance($owner->getInstance())) {
262
			return true;
263
		}
264
265
		return false;
266
	}
267
268
269
	/**
270
	 * @param RemoteEvent $event
271
	 *
272
	 * @return IRemoteEvent
273
	 * @throws RemoteEventException
274
	 */
275
	public function getRemoteEvent(RemoteEvent $event): IRemoteEvent {
276
		$class = $event->getClass();
277
		try {
278
			$test = new ReflectionClass($class);
279
		} catch (ReflectionException $e) {
280
			throw new RemoteEventException('ReflectionException with ' . $class . ': ' . $e->getMessage());
281
		}
282
283
		if (!in_array(IRemoteEvent::class, $test->getInterfaceNames())) {
284
			throw new RemoteEventException($class . ' does not implements IRemoteEvent');
285
		}
286
287
		$gs = OC::$server->get($class);
288
		if (!$gs instanceof IRemoteEvent) {
289
			throw new RemoteEventException($class . ' not an IRemoteEvent');
290
		}
291
292
		return $gs;
293
	}
294
295
296
	/**
297
	 * @param bool $all
298
	 *
299
	 * @return array
300
	 */
301
	private function getInstances(bool $all = false): array {
302
		$gsInstances = $this->getGlobalScaleInstances();
303
		$remoteInstances = $this->getRemoteInstances();
304
305
		$instances = array_merge([$this->configService->getLocalInstance()], $gsInstances, $remoteInstances);
306
		if ($all) {
307
			return $instances;
308
		}
309
310
		return array_values(array_diff($instances, $this->configService->getTrustedDomains()));
311
	}
312
313
314
	/**
315
	 * @return array
316
	 */
317
	private function getGlobalScaleInstances(): array {
318
		try {
319
			$lookup = $this->configService->getGSStatus(ConfigService::GS_LOOKUP);
320
			$request = new NC21Request(ConfigService::GS_LOOKUP_INSTANCES, Request::TYPE_POST);
321
			$this->configService->configureRequest($request);
322
			$request->basedOnUrl($lookup);
323
			$request->addData('authKey', $this->configService->getGSStatus(ConfigService::GS_KEY));
324
325
			try {
326
				return $this->retrieveJson($request);
327
			} catch (RequestNetworkException $e) {
328
				$this->e($e, ['request' => $request]);
329
			}
330
		} catch (GSStatusException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
331
		}
332
333
		return [];
334
	}
335
336
337
	/**
338
	 * @return array
339
	 */
340
	private function getRemoteInstances(): array {
341
		return [];
342
	}
343
344
345
	/**
346
	 * should be used to manage results from events, like sending mails on user creation
347
	 *
348
	 * @param string $token
349
	 */
350
	public function manageResults(string $token): void {
351
		try {
352
			$wrappers = $this->gsEventsRequest->getByToken($token);
353
		} catch (JsonException | ModelException $e) {
354
			return;
355
		}
356
357
		$event = null;
358
		$events = [];
359
		foreach ($wrappers as $wrapper) {
360
			if ($wrapper->getStatus() !== GSWrapper::STATUS_DONE) {
361
				return;
362
			}
363
364
			$events[$wrapper->getInstance()] = $event = $wrapper->getEvent();
365
		}
366
367
		if ($event === null) {
368
			return;
369
		}
370
371
		try {
372
			$gs = $this->getRemoteEvent($event);
0 ignored issues
show
Documentation introduced by
$event is of type object<OCA\Circles\Model\GlobalScale\GSEvent>, but the function expects a object<OCA\Circles\Model\Remote\RemoteEvent>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
373
			$gs->result($events);
374
		} catch (RemoteEventException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
375
		}
376
	}
377
378
379
	/**
380
	 * @param RemoteEvent $event
381
	 */
382
	private function confirmEvent(RemoteEvent $event): void {
383
//		$this->signEvent($event);
384
385
		$circle = $event->getCircle();
386
		$owner = $circle->getOwner();
387
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.event');
388
389
		$request = new NC21Request($path, Request::TYPE_POST);
390
		$this->configService->configureRequest($request);
391
		$request->basedOnUrl($owner->getInstance());
392
393
		$request->setDataSerialize($event);
394
395
		$result = $this->retrieveJson($request);
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
396
		$this->debug('confirming RemoteEvent', ['event' => $event, 'request' => $request]);
397
//
398
//		if ($this->getInt('status', $result) === 0) {
399
//			throw new GlobalScaleEventException($this->get('error', $result));
400
//		}
401
402
//		$updatedData = $this->getArray('event', $result);
403
//		$this->miscService->log('updatedEvent: ' . json_encode($updatedData), 0);
404
//		if (!empty($updatedData)) {
405
//			$updated = new GSEvent();
406
//			try {
407
//				$updated->import($updatedData);
408
//				$event = $updated;
409
//			} catch (Exception $e) {
410
//			}
411
//		}
412
	}
413
414
415
	/**
416
	 * @param Member $member1
417
	 * @param Member $member2
418
	 *
419
	 * @return bool
420
	 */
421
	private function compareMembers(Member $member1, Member $member2): bool {
422
//		if ($member1->getInstance() === '') {
423
//			$member1->setInstance($this->configService->getLocalInstance());
424
//		}
425
//
426
//		if ($member2->getInstance() === '') {
427
//			$member2->setInstance($this->configService->getLocalInstance());
428
//		}
429
430
		if ($member1->getCircleId() !== $member2->getCircleId()
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return !($member1->getCi...ember2->getInstance());.
Loading history...
431
			|| $member1->getUserId() !== $member2->getUserId()
432
			|| $member1->getUserType() <> $member2->getUserType()
433
			|| $member1->getLevel() <> $member2->getLevel()
434
			|| $member1->getStatus() !== $member2->getStatus()
435
			|| $member1->getInstance() !== $member2->getInstance()) {
436
			return false;
437
		}
438
439
		return true;
440
	}
441
442
}
443
444