Completed
Pull Request — master (#634)
by Maxence
04:45 queued 02:03
created

CircleService::generateSanitizedName()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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