Completed
Push — master ( 897add...cd91c1 )
by Maxence
02:34
created

CircleService::circleLeave()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
cc 1
nc 1
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
32
namespace OCA\Circles\Service;
33
34
35
use ArtificialOwl\MySmallPhpTools\Model\SimpleDataStore;
36
use ArtificialOwl\MySmallPhpTools\Traits\Nextcloud\nc22\TNC22Logger;
37
use ArtificialOwl\MySmallPhpTools\Traits\TArrayTools;
38
use ArtificialOwl\MySmallPhpTools\Traits\TStringTools;
39
use OCA\Circles\AppInfo\Application;
40
use OCA\Circles\Db\CircleRequest;
41
use OCA\Circles\Db\MemberRequest;
42
use OCA\Circles\Exceptions\CircleNameTooShortException;
43
use OCA\Circles\Exceptions\CircleNotFoundException;
44
use OCA\Circles\Exceptions\FederatedEventException;
45
use OCA\Circles\Exceptions\FederatedItemException;
46
use OCA\Circles\Exceptions\InitiatorNotConfirmedException;
47
use OCA\Circles\Exceptions\InitiatorNotFoundException;
48
use OCA\Circles\Exceptions\MembersLimitException;
49
use OCA\Circles\Exceptions\OwnerNotFoundException;
50
use OCA\Circles\Exceptions\RemoteInstanceException;
51
use OCA\Circles\Exceptions\RemoteNotFoundException;
52
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
53
use OCA\Circles\Exceptions\RequestBuilderException;
54
use OCA\Circles\Exceptions\UnknownRemoteException;
55
use OCA\Circles\FederatedItems\CircleConfig;
56
use OCA\Circles\FederatedItems\CircleCreate;
57
use OCA\Circles\FederatedItems\CircleDestroy;
58
use OCA\Circles\FederatedItems\CircleEdit;
59
use OCA\Circles\FederatedItems\CircleJoin;
60
use OCA\Circles\FederatedItems\CircleLeave;
61
use OCA\Circles\FederatedItems\CircleSettings;
62
use OCA\Circles\Model\Circle;
63
use OCA\Circles\Model\Federated\FederatedEvent;
64
use OCA\Circles\Model\FederatedUser;
65
use OCA\Circles\Model\ManagedModel;
66
use OCA\Circles\Model\Member;
67
use OCA\Circles\StatusCode;
68
69
70
/**
71
 * Class CircleService
72
 *
73
 * @package OCA\Circles\Service
74
 */
75
class CircleService {
76
77
78
	use TArrayTools;
79
	use TStringTools;
80
	use TNC22Logger;
81
82
83
	/** @var CircleRequest */
84
	private $circleRequest;
85
86
	/** @var MemberRequest */
87
	private $memberRequest;
88
89
	/** @var RemoteStreamService */
90
	private $remoteStreamService;
91
92
	/** @var FederatedUserService */
93
	private $federatedUserService;
94
95
	/** @var FederatedEventService */
96
	private $federatedEventService;
97
98
	/** @var MemberService */
99
	private $memberService;
100
101
	/** @var ConfigService */
102
	private $configService;
103
104
105
	/**
106
	 * CircleService constructor.
107
	 *
108
	 * @param CircleRequest $circleRequest
109
	 * @param MemberRequest $memberRequest
110
	 * @param RemoteStreamService $remoteStreamService
111
	 * @param FederatedUserService $federatedUserService
112
	 * @param FederatedEventService $federatedEventService
113
	 * @param MemberService $memberService
114
	 * @param ConfigService $configService
115
	 */
116
	public function __construct(
117
		CircleRequest $circleRequest,
118
		MemberRequest $memberRequest,
119
		RemoteStreamService $remoteStreamService,
120
		FederatedUserService $federatedUserService,
121
		FederatedEventService $federatedEventService,
122
		MemberService $memberService,
123
		ConfigService $configService
124
	) {
125
		$this->circleRequest = $circleRequest;
126
		$this->memberRequest = $memberRequest;
127
		$this->remoteStreamService = $remoteStreamService;
128
		$this->federatedUserService = $federatedUserService;
129
		$this->federatedEventService = $federatedEventService;
130
		$this->memberService = $memberService;
131
		$this->configService = $configService;
132
133
		$this->setup('app', Application::APP_ID);
134
	}
135
136
137
	/**
138
	 * @param string $name
139
	 * @param FederatedUser|null $owner
140
	 * @param bool $personal
141
	 * @param bool $local
142
	 *
143
	 * @return array
144
	 * @throws FederatedEventException
145
	 * @throws FederatedItemException
146
	 * @throws InitiatorNotConfirmedException
147
	 * @throws InitiatorNotFoundException
148
	 * @throws OwnerNotFoundException
149
	 * @throws RemoteInstanceException
150
	 * @throws RemoteNotFoundException
151
	 * @throws RemoteResourceNotFoundException
152
	 * @throws UnknownRemoteException
153
	 * @throws RequestBuilderException
154
	 * @throws CircleNameTooShortException
155
	 */
156
	public function create(
157
		string $name,
158
		?FederatedUser $owner = null,
159
		bool $personal = false,
160
		bool $local = false
161
	): array {
162
163
		$this->federatedUserService->mustHaveCurrentUser();
164
		if (is_null($owner)) {
165
			$owner = $this->federatedUserService->getCurrentUser();
166
		}
167
168
		if (is_null($owner)) {
169
			throw new OwnerNotFoundException('owner not defined');
170
		}
171
172
		$circle = new Circle();
173
		$circle->setName($this->cleanCircleName($name))
174
			   ->setSingleId($this->token(ManagedModel::ID_LENGTH))
175
			   ->setSource(Member::TYPE_CIRCLE);
176
177
		if (strlen($circle->getName()) < 3) {
178
			throw new CircleNameTooShortException('Circle name is too short');
179
		}
180
181
		if ($personal) {
182
			$circle->setConfig(Circle::CFG_PERSONAL);
183
		}
184
185
		if ($local) {
186
			$circle->addConfig(Circle::CFG_LOCAL);
187
		}
188
189
		$this->confirmName($circle);
190
191
		$member = new Member();
192
		$member->importFromIFederatedUser($owner);
193
		$member->setId($this->token(ManagedModel::ID_LENGTH))
194
			   ->setCircleId($circle->getSingleId())
195
			   ->setLevel(Member::LEVEL_OWNER)
196
			   ->setStatus(Member::STATUS_MEMBER);
197
198
		$this->federatedUserService->setMemberPatron($member);
199
200
		$circle->setOwner($member)
201
			   ->setInitiator($member);
202
203
		$event = new FederatedEvent(CircleCreate::class);
204
		$event->setCircle($circle);
205
		$this->federatedEventService->newEvent($event);
206
207
		return $event->getOutcome();
208
	}
209
210
211
	/**
212
	 * @param string $circleId
213
	 *
214
	 * @return array
215
	 * @throws CircleNotFoundException
216
	 * @throws FederatedEventException
217
	 * @throws FederatedItemException
218
	 * @throws InitiatorNotConfirmedException
219
	 * @throws InitiatorNotFoundException
220
	 * @throws OwnerNotFoundException
221
	 * @throws RemoteInstanceException
222
	 * @throws RemoteNotFoundException
223
	 * @throws RemoteResourceNotFoundException
224
	 * @throws RequestBuilderException
225
	 * @throws UnknownRemoteException
226
	 */
227
	public function destroy(string $circleId): array {
228
		$this->federatedUserService->mustHaveCurrentUser();
229
230
		$circle = $this->getCircle($circleId);
231
232
		$event = new FederatedEvent(CircleDestroy::class);
233
		$event->setCircle($circle);
234
		$this->federatedEventService->newEvent($event);
235
236
		return $event->getOutcome();
237
	}
238
239
240
	/**
241
	 * @param string $circleId
242
	 * @param int $config
243
	 *
244
	 * @return array
245
	 * @throws CircleNotFoundException
246
	 * @throws FederatedEventException
247
	 * @throws FederatedItemException
248
	 * @throws InitiatorNotConfirmedException
249
	 * @throws InitiatorNotFoundException
250
	 * @throws OwnerNotFoundException
251
	 * @throws RemoteInstanceException
252
	 * @throws RemoteNotFoundException
253
	 * @throws RemoteResourceNotFoundException
254
	 * @throws UnknownRemoteException
255
	 * @throws RequestBuilderException
256
	 */
257
	public function updateConfig(string $circleId, int $config): array {
258
		$circle = $this->getCircle($circleId);
259
260
		$event = new FederatedEvent(CircleConfig::class);
261
		$event->setCircle($circle);
262
		$event->setParams(new SimpleDataStore(['config' => $config]));
263
264
		$this->federatedEventService->newEvent($event);
265
266
		return $event->getOutcome();
267
	}
268
269
270
	/**
271
	 * @param string $circleId
272
	 * @param string $name
273
	 *
274
	 * @return array
275
	 * @throws CircleNotFoundException
276
	 * @throws FederatedEventException
277
	 * @throws FederatedItemException
278
	 * @throws InitiatorNotConfirmedException
279
	 * @throws InitiatorNotFoundException
280
	 * @throws OwnerNotFoundException
281
	 * @throws RemoteInstanceException
282
	 * @throws RemoteNotFoundException
283
	 * @throws RemoteResourceNotFoundException
284
	 * @throws RequestBuilderException
285
	 * @throws UnknownRemoteException
286
	 */
287
	public function updateName(string $circleId, string $name): array {
288
		$circle = $this->getCircle($circleId);
289
290
		$event = new FederatedEvent(CircleEdit::class);
291
		$event->setCircle($circle);
292
		$event->setParams(new SimpleDataStore(['name' => $name]));
293
294
		$this->federatedEventService->newEvent($event);
295
296
		return $event->getOutcome();
297
	}
298
299
	/**
300
	 * @param string $circleId
301
	 * @param string $description
302
	 *
303
	 * @return array
304
	 * @throws CircleNotFoundException
305
	 * @throws FederatedEventException
306
	 * @throws FederatedItemException
307
	 * @throws InitiatorNotConfirmedException
308
	 * @throws InitiatorNotFoundException
309
	 * @throws OwnerNotFoundException
310
	 * @throws RemoteInstanceException
311
	 * @throws RemoteNotFoundException
312
	 * @throws RemoteResourceNotFoundException
313
	 * @throws RequestBuilderException
314
	 * @throws UnknownRemoteException
315
	 */
316
	public function updateDescription(string $circleId, string $description): array {
317
		$circle = $this->getCircle($circleId);
318
319
		$event = new FederatedEvent(CircleEdit::class);
320
		$event->setCircle($circle);
321
		$event->setParams(new SimpleDataStore(['description' => $description]));
322
323
		$this->federatedEventService->newEvent($event);
324
325
		return $event->getOutcome();
326
	}
327
328
	/**
329
	 * @param string $circleId
330
	 * @param array $settings
331
	 *
332
	 * @return array
333
	 * @throws CircleNotFoundException
334
	 * @throws FederatedEventException
335
	 * @throws FederatedItemException
336
	 * @throws InitiatorNotConfirmedException
337
	 * @throws InitiatorNotFoundException
338
	 * @throws OwnerNotFoundException
339
	 * @throws RemoteInstanceException
340
	 * @throws RemoteNotFoundException
341
	 * @throws RemoteResourceNotFoundException
342
	 * @throws RequestBuilderException
343
	 * @throws UnknownRemoteException
344
	 */
345
	public function updateSettings(string $circleId, array $settings): array {
346
		$circle = $this->getCircle($circleId);
347
348
		$event = new FederatedEvent(CircleSettings::class);
349
		$event->setCircle($circle);
350
		$event->setParams(new SimpleDataStore(['settings' => $settings]));
351
352
		$this->federatedEventService->newEvent($event);
353
354
		return $event->getOutcome();
355
	}
356
357
358
	/**
359
	 * @param string $circleId
360
	 *
361
	 * @return array
362
	 * @throws CircleNotFoundException
363
	 * @throws FederatedEventException
364
	 * @throws FederatedItemException
365
	 * @throws InitiatorNotConfirmedException
366
	 * @throws InitiatorNotFoundException
367
	 * @throws OwnerNotFoundException
368
	 * @throws RemoteInstanceException
369
	 * @throws RemoteNotFoundException
370
	 * @throws RemoteResourceNotFoundException
371
	 * @throws UnknownRemoteException
372
	 * @throws RequestBuilderException
373
	 */
374
	public function circleJoin(string $circleId): array {
375
		$this->federatedUserService->mustHaveCurrentUser();
376
377
		$circle = $this->circleRequest->getCircle($circleId, $this->federatedUserService->getCurrentUser());
378
		if (!$circle->getInitiator()->hasInvitedBy()) {
379
			$this->federatedUserService->setMemberPatron($circle->getInitiator());
380
		}
381
382
		$event = new FederatedEvent(CircleJoin::class);
383
		$event->setCircle($circle);
384
385
		$this->federatedEventService->newEvent($event);
386
387
		return $event->getOutcome();
388
	}
389
390
391
	/**
392
	 * @param string $circleId
393
	 * @param bool $force
394
	 *
395
	 * @return array
396
	 * @throws CircleNotFoundException
397
	 * @throws FederatedEventException
398
	 * @throws FederatedItemException
399
	 * @throws InitiatorNotConfirmedException
400
	 * @throws InitiatorNotFoundException
401
	 * @throws OwnerNotFoundException
402
	 * @throws RemoteInstanceException
403
	 * @throws RemoteNotFoundException
404
	 * @throws RemoteResourceNotFoundException
405
	 * @throws RequestBuilderException
406
	 * @throws UnknownRemoteException
407
	 */
408
	public function circleLeave(string $circleId, bool $force = false): array {
409
		$this->federatedUserService->mustHaveCurrentUser();
410
411
		$circle = $this->circleRequest->getCircle($circleId, $this->federatedUserService->getCurrentUser());
412
413
		$event = new FederatedEvent(CircleLeave::class);
414
		$event->setCircle($circle);
415
		$event->getParams()->sBool('force', $force);
416
417
		$this->federatedEventService->newEvent($event);
418
419
		return $event->getOutcome();
420
	}
421
422
423
	/**
424
	 * @param string $circleId
425
	 * @param int $filter
426
	 *
427
	 * @return Circle
428
	 * @throws CircleNotFoundException
429
	 * @throws InitiatorNotFoundException
430
	 * @throws RequestBuilderException
431
	 */
432
	public function getCircle(
433
		string $circleId,
434
		int $filter = Circle::CFG_BACKEND | Circle::CFG_SINGLE | Circle::CFG_HIDDEN
435
	): Circle {
436
		$this->federatedUserService->mustHaveCurrentUser();
437
438
		return $this->circleRequest->getCircle(
439
			$circleId,
440
			$this->federatedUserService->getCurrentUser(),
441
			$this->federatedUserService->getRemoteInstance(),
442
			$filter
443
		);
444
	}
445
446
447
	/**
448
	 * @param Circle|null $circleFilter
449
	 * @param Member|null $memberFilter
450
	 * @param SimpleDataStore|null $params
451
	 *
452
	 * @return Circle[]
453
	 * @throws InitiatorNotFoundException
454
	 * @throws RequestBuilderException
455
	 */
456
	public function getCircles(
457
		?Circle $circleFilter = null,
458
		?Member $memberFilter = null,
459
		?SimpleDataStore $params = null
460
	): array {
461
		$this->federatedUserService->mustHaveCurrentUser();
462
463
		if ($params === null) {
464
			$params = new SimpleDataStore();
465
		}
466
		$params->default(
467
			[
468
				'limit'                  => -1,
469
				'offset'                 => 0,
470
				'mustBeMember'           => false,
471
				'includeHiddenCircles'   => false,
472
				'includeBackendCircles'  => false,
473
				'includeSystemCircles'   => false,
474
				'includePersonalCircles' => false
475
			]
476
		);
477
478
		return $this->circleRequest->getCircles(
479
			$circleFilter,
480
			$memberFilter,
481
			$this->federatedUserService->getCurrentUser(),
482
			$this->federatedUserService->getRemoteInstance(),
483
			$params
484
		);
485
	}
486
487
488
	/**
489
	 * @param Circle $circle
490
	 *
491
	 * @throws RequestBuilderException
492
	 */
493
	public function confirmName(Circle $circle): void {
494
		if ($circle->isConfig(Circle::CFG_SYSTEM)
495
			|| $circle->isConfig(Circle::CFG_SINGLE)) {
496
			return;
497
		}
498
499
		$this->confirmDisplayName($circle);
500
		$this->generateSanitizedName($circle);
501
	}
502
503
	/**
504
	 * @param Circle $circle
505
	 *
506
	 * @throws RequestBuilderException
507
	 */
508
	private function confirmDisplayName(Circle $circle) {
509
		$baseDisplayName = $circle->getName();
510
511
		$i = 1;
512
		while (true) {
513
			$testDisplayName = $baseDisplayName . (($i > 1) ? ' (' . $i . ')' : '');
514
			$test = new Circle();
515
			$test->setDisplayName($testDisplayName);
516
517
			try {
518
				$stored = $this->circleRequest->searchCircle($test);
519
				if ($stored->getSingleId() === $circle->getSingleId()) {
520
					throw new CircleNotFoundException();
521
				}
522
			} catch (CircleNotFoundException $e) {
523
				$circle->setDisplayName($testDisplayName);
524
525
				return;
526
			}
527
528
			$i++;
529
		}
530
	}
531
532
533
	/**
534
	 * @param Circle $circle
535
	 *
536
	 * @throws RequestBuilderException
537
	 */
538
	public function generateSanitizedName(Circle $circle) {
539
		$baseSanitizedName = $this->sanitizeName($circle->getName());
540
		if ($baseSanitizedName === '') {
541
			$baseSanitizedName = substr($circle->getSingleId(), 0, 3);
542
		}
543
544
		$i = 1;
545
		while (true) {
546
			$testSanitizedName = $baseSanitizedName . (($i > 1) ? ' (' . $i . ')' : '');
547
548
			$test = new Circle();
549
			$test->setSanitizedName($testSanitizedName);
550
551
			try {
552
				$stored = $this->circleRequest->searchCircle($test);
553
				if ($stored->getSingleId() === $circle->getSingleId()) {
554
					throw new CircleNotFoundException();
555
				}
556
			} catch (CircleNotFoundException $e) {
557
				$circle->setSanitizedName($testSanitizedName);
558
559
				return;
560
			}
561
562
			$i++;
563
		}
564
	}
565
566
	/**
567
	 * @param string $name
568
	 *
569
	 * @return string
570
	 */
571
	public function sanitizeName(string $name): string {
572
		// replace '/' with '-' to prevent directory traversal
573
		// replacing instead of stripping seems the better tradeoff here
574
		$sanitized = str_replace('/', '-', $name);
575
576
		// remove characters which are illegal on Windows (includes illegal characters on Unix/Linux)
577
		// see also \OC\Files\Storage\Common::verifyPosixPath(...)
578
		/** @noinspection CascadeStringReplacementInspection */
579
		$sanitized = str_replace(['*', '|', '\\', ':', '"', '<', '>', '?'], '', $sanitized);
580
581
		// remove leading+trailing spaces and dots to prevent hidden files
582
		return trim($sanitized, ' .');
583
	}
584
585
586
	/**
587
	 * @param Circle $circle
588
	 *
589
	 * @throws MembersLimitException
590
	 */
591
	public function confirmCircleNotFull(Circle $circle): void {
592
		if ($this->isCircleFull($circle)) {
593
			throw new MembersLimitException(StatusCode::$MEMBER_ADD[121], 121);
0 ignored issues
show
Bug introduced by
The property MEMBER_ADD cannot be accessed from this context as it is declared private in class OCA\Circles\StatusCode.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
594
		}
595
	}
596
597
598
	/**
599
	 * @param Circle $circle
600
	 *
601
	 * @return bool
602
	 * @throws RequestBuilderException
603
	 */
604
	public function isCircleFull(Circle $circle): bool {
605
		$filter = new Member();
606
		$filter->setLevel(Member::LEVEL_MEMBER);
607
		$members = $this->memberRequest->getMembers($circle->getSingleId(), null, null, $filter);
608
609
		$limit = $this->getInt('members_limit', $circle->getSettings());
610
		if ($limit === -1) {
611
			return false;
612
		}
613
		if ($limit === 0) {
614
			$limit = $this->configService->getAppValue(ConfigService::MEMBERS_LIMIT);
615
		}
616
617
		return (sizeof($members) >= $limit);
618
	}
619
620
621
	/**
622
	 * @param string $name
623
	 *
624
	 * @return string
625
	 */
626
	public function cleanCircleName(string $name): string {
627
		$name = preg_replace('/\s+/', ' ', $name);
628
629
		return trim($name);
630
	}
631
632
}
633
634