Completed
Push — master ( 1c518a...24c6a1 )
by John
30:27 queued 07:45
created
apps/dav/lib/Connector/Sabre/Principal.php 1 patch
Indentation   +568 added lines, -568 removed lines patch added patch discarded remove patch
@@ -32,572 +32,572 @@
 block discarded – undo
32 32
 
33 33
 class Principal implements BackendInterface {
34 34
 
35
-	/** @var string */
36
-	private $principalPrefix;
37
-
38
-	/** @var bool */
39
-	private $hasGroups;
40
-
41
-	/** @var bool */
42
-	private $hasCircles;
43
-
44
-	/** @var ProxyMapper */
45
-	private $proxyMapper;
46
-
47
-	/** @var KnownUserService */
48
-	private $knownUserService;
49
-
50
-	public function __construct(
51
-		private IUserManager $userManager,
52
-		private IGroupManager $groupManager,
53
-		private IAccountManager $accountManager,
54
-		private IShareManager $shareManager,
55
-		private IUserSession $userSession,
56
-		private IAppManager $appManager,
57
-		ProxyMapper $proxyMapper,
58
-		KnownUserService $knownUserService,
59
-		private IConfig $config,
60
-		private IFactory $languageFactory,
61
-		string $principalPrefix = 'principals/users/',
62
-	) {
63
-		$this->principalPrefix = trim($principalPrefix, '/');
64
-		$this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
65
-		$this->proxyMapper = $proxyMapper;
66
-		$this->knownUserService = $knownUserService;
67
-	}
68
-
69
-	use PrincipalProxyTrait {
70
-		getGroupMembership as protected traitGetGroupMembership;
71
-	}
72
-
73
-	/**
74
-	 * Returns a list of principals based on a prefix.
75
-	 *
76
-	 * This prefix will often contain something like 'principals'. You are only
77
-	 * expected to return principals that are in this base path.
78
-	 *
79
-	 * You are expected to return at least a 'uri' for every user, you can
80
-	 * return any additional properties if you wish so. Common properties are:
81
-	 *   {DAV:}displayname
82
-	 *
83
-	 * @param string $prefixPath
84
-	 * @return string[]
85
-	 */
86
-	public function getPrincipalsByPrefix($prefixPath) {
87
-		$principals = [];
88
-
89
-		if ($prefixPath === $this->principalPrefix) {
90
-			foreach ($this->userManager->search('') as $user) {
91
-				$principals[] = $this->userToPrincipal($user);
92
-			}
93
-		}
94
-
95
-		return $principals;
96
-	}
97
-
98
-	/**
99
-	 * Returns a specific principal, specified by it's path.
100
-	 * The returned structure should be the exact same as from
101
-	 * getPrincipalsByPrefix.
102
-	 *
103
-	 * @param string $path
104
-	 * @return array
105
-	 */
106
-	public function getPrincipalByPath($path) {
107
-		[$prefix, $name] = \Sabre\Uri\split($path);
108
-		$decodedName = urldecode($name);
109
-
110
-		if ($name === 'calendar-proxy-write' || $name === 'calendar-proxy-read') {
111
-			[$prefix2, $name2] = \Sabre\Uri\split($prefix);
112
-
113
-			if ($prefix2 === $this->principalPrefix) {
114
-				$user = $this->userManager->get($name2);
115
-
116
-				if ($user !== null) {
117
-					return [
118
-						'uri' => 'principals/users/' . $user->getUID() . '/' . $name,
119
-					];
120
-				}
121
-				return null;
122
-			}
123
-		}
124
-
125
-		if ($prefix === $this->principalPrefix) {
126
-			// Depending on where it is called, it may happen that this function
127
-			// is called either with a urlencoded version of the name or with a non-urlencoded one.
128
-			// The urldecode function replaces %## and +, both of which are forbidden in usernames.
129
-			// Hence there can be no ambiguity here and it is safe to call urldecode on all usernames
130
-			$user = $this->userManager->get($decodedName);
131
-
132
-			if ($user !== null) {
133
-				return $this->userToPrincipal($user);
134
-			}
135
-		} elseif ($prefix === 'principals/circles') {
136
-			if ($this->userSession->getUser() !== null) {
137
-				// At the time of writing - 2021-01-19 — a mixed state is possible.
138
-				// The second condition can be removed when this is fixed.
139
-				return $this->circleToPrincipal($decodedName)
140
-					?: $this->circleToPrincipal($name);
141
-			}
142
-		} elseif ($prefix === 'principals/groups') {
143
-			// At the time of writing - 2021-01-19 — a mixed state is possible.
144
-			// The second condition can be removed when this is fixed.
145
-			$group = $this->groupManager->get($decodedName)
146
-				?: $this->groupManager->get($name);
147
-			if ($group instanceof IGroup) {
148
-				return [
149
-					'uri' => 'principals/groups/' . $name,
150
-					'{DAV:}displayname' => $group->getDisplayName(),
151
-				];
152
-			}
153
-		} elseif ($prefix === 'principals/system') {
154
-			return [
155
-				'uri' => 'principals/system/' . $name,
156
-				'{DAV:}displayname' => $this->languageFactory->get('dav')->t('Accounts'),
157
-			];
158
-		} elseif ($prefix === 'principals/shares') {
159
-			return [
160
-				'uri' => 'principals/shares/' . $name,
161
-				'{DAV:}displayname' => $name,
162
-			];
163
-		}
164
-		return null;
165
-	}
166
-
167
-	/**
168
-	 * Returns the list of groups a principal is a member of
169
-	 *
170
-	 * @param string $principal
171
-	 * @param bool $needGroups
172
-	 * @return array
173
-	 * @throws Exception
174
-	 */
175
-	public function getGroupMembership($principal, $needGroups = false) {
176
-		[$prefix, $name] = \Sabre\Uri\split($principal);
177
-
178
-		if ($prefix !== $this->principalPrefix) {
179
-			return [];
180
-		}
181
-
182
-		$user = $this->userManager->get($name);
183
-		if (!$user) {
184
-			throw new Exception('Principal not found');
185
-		}
186
-
187
-		$groups = [];
188
-
189
-		if ($this->hasGroups || $needGroups) {
190
-			$userGroups = $this->groupManager->getUserGroups($user);
191
-			foreach ($userGroups as $userGroup) {
192
-				$groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
193
-			}
194
-		}
195
-
196
-		$groups = array_unique(array_merge(
197
-			$groups,
198
-			$this->traitGetGroupMembership($principal, $needGroups)
199
-		));
200
-
201
-		return $groups;
202
-	}
203
-
204
-	/**
205
-	 * @param string $path
206
-	 * @param PropPatch $propPatch
207
-	 * @return int
208
-	 */
209
-	public function updatePrincipal($path, PropPatch $propPatch) {
210
-		// Updating schedule-default-calendar-URL is handled in CustomPropertiesBackend
211
-		return 0;
212
-	}
213
-
214
-	/**
215
-	 * Search user principals
216
-	 *
217
-	 * @param array $searchProperties
218
-	 * @param string $test
219
-	 * @return array
220
-	 */
221
-	protected function searchUserPrincipals(array $searchProperties, $test = 'allof') {
222
-		$results = [];
223
-
224
-		// If sharing is disabled, return the empty array
225
-		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
226
-		if (!$shareAPIEnabled) {
227
-			return [];
228
-		}
229
-
230
-		$allowEnumeration = $this->shareManager->allowEnumeration();
231
-		$limitEnumerationGroup = $this->shareManager->limitEnumerationToGroups();
232
-		$limitEnumerationPhone = $this->shareManager->limitEnumerationToPhone();
233
-		$allowEnumerationFullMatch = $this->shareManager->allowEnumerationFullMatch();
234
-		$ignoreSecondDisplayName = $this->shareManager->ignoreSecondDisplayName();
235
-		$matchEmail = $this->shareManager->matchEmail();
236
-
237
-		// If sharing is restricted to group members only,
238
-		// return only members that have groups in common
239
-		$restrictGroups = false;
240
-		$currentUser = $this->userSession->getUser();
241
-		if ($this->shareManager->shareWithGroupMembersOnly()) {
242
-			if (!$currentUser instanceof IUser) {
243
-				return [];
244
-			}
245
-
246
-			$restrictGroups = $this->groupManager->getUserGroupIds($currentUser);
247
-		}
248
-
249
-		$currentUserGroups = [];
250
-		if ($limitEnumerationGroup) {
251
-			if ($currentUser instanceof IUser) {
252
-				$currentUserGroups = $this->groupManager->getUserGroupIds($currentUser);
253
-			}
254
-		}
255
-
256
-		$searchLimit = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
257
-		if ($searchLimit <= 0) {
258
-			$searchLimit = null;
259
-		}
260
-		foreach ($searchProperties as $prop => $value) {
261
-			switch ($prop) {
262
-				case '{http://sabredav.org/ns}email-address':
263
-					if (!$allowEnumeration) {
264
-						if ($allowEnumerationFullMatch && $matchEmail) {
265
-							$users = $this->userManager->getByEmail($value);
266
-						} else {
267
-							$users = [];
268
-						}
269
-					} else {
270
-						$users = $this->userManager->getByEmail($value);
271
-						$users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
272
-							if ($allowEnumerationFullMatch && $user->getSystemEMailAddress() === $value) {
273
-								return true;
274
-							}
275
-
276
-							if ($limitEnumerationPhone
277
-								&& $currentUser instanceof IUser
278
-								&& $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
279
-								// Synced phonebook match
280
-								return true;
281
-							}
282
-
283
-							if (!$limitEnumerationGroup) {
284
-								// No limitation on enumeration, all allowed
285
-								return true;
286
-							}
287
-
288
-							return !empty($currentUserGroups) && !empty(array_intersect(
289
-								$this->groupManager->getUserGroupIds($user),
290
-								$currentUserGroups
291
-							));
292
-						});
293
-					}
294
-
295
-					$results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
296
-						// is sharing restricted to groups only?
297
-						if ($restrictGroups !== false) {
298
-							$userGroups = $this->groupManager->getUserGroupIds($user);
299
-							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
300
-								return $carry;
301
-							}
302
-						}
303
-
304
-						$carry[] = $this->principalPrefix . '/' . $user->getUID();
305
-						return $carry;
306
-					}, []);
307
-					break;
308
-
309
-				case '{DAV:}displayname':
310
-
311
-					if (!$allowEnumeration) {
312
-						if ($allowEnumerationFullMatch) {
313
-							$lowerSearch = strtolower($value);
314
-							$users = $this->userManager->searchDisplayName($value, $searchLimit);
315
-							$users = \array_filter($users, static function (IUser $user) use ($lowerSearch, $ignoreSecondDisplayName) {
316
-								$lowerDisplayName = strtolower($user->getDisplayName());
317
-								return $lowerDisplayName === $lowerSearch || ($ignoreSecondDisplayName && trim(preg_replace('/ \(.*\)$/', '', $lowerDisplayName)) === $lowerSearch);
318
-							});
319
-						} else {
320
-							$users = [];
321
-						}
322
-					} else {
323
-						$users = $this->userManager->searchDisplayName($value, $searchLimit);
324
-						$users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
325
-							if ($allowEnumerationFullMatch && $user->getDisplayName() === $value) {
326
-								return true;
327
-							}
328
-
329
-							if ($limitEnumerationPhone
330
-								&& $currentUser instanceof IUser
331
-								&& $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
332
-								// Synced phonebook match
333
-								return true;
334
-							}
335
-
336
-							if (!$limitEnumerationGroup) {
337
-								// No limitation on enumeration, all allowed
338
-								return true;
339
-							}
340
-
341
-							return !empty($currentUserGroups) && !empty(array_intersect(
342
-								$this->groupManager->getUserGroupIds($user),
343
-								$currentUserGroups
344
-							));
345
-						});
346
-					}
347
-
348
-					$results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
349
-						// is sharing restricted to groups only?
350
-						if ($restrictGroups !== false) {
351
-							$userGroups = $this->groupManager->getUserGroupIds($user);
352
-							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
353
-								return $carry;
354
-							}
355
-						}
356
-
357
-						$carry[] = $this->principalPrefix . '/' . $user->getUID();
358
-						return $carry;
359
-					}, []);
360
-					break;
361
-
362
-				case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
363
-					// If you add support for more search properties that qualify as a user-address,
364
-					// please also add them to the array below
365
-					$results[] = $this->searchUserPrincipals([
366
-						// In theory this should also search for principal:principals/users/...
367
-						// but that's used internally only anyway and i don't know of any client querying that
368
-						'{http://sabredav.org/ns}email-address' => $value,
369
-					], 'anyof');
370
-					break;
371
-
372
-				default:
373
-					$results[] = [];
374
-					break;
375
-			}
376
-		}
377
-
378
-		// results is an array of arrays, so this is not the first search result
379
-		// but the results of the first searchProperty
380
-		if (count($results) === 1) {
381
-			return $results[0];
382
-		}
383
-
384
-		switch ($test) {
385
-			case 'anyof':
386
-				return array_values(array_unique(array_merge(...$results)));
387
-
388
-			case 'allof':
389
-			default:
390
-				return array_values(array_intersect(...$results));
391
-		}
392
-	}
393
-
394
-	/**
395
-	 * @param string $prefixPath
396
-	 * @param array $searchProperties
397
-	 * @param string $test
398
-	 * @return array
399
-	 */
400
-	public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
401
-		if (count($searchProperties) === 0) {
402
-			return [];
403
-		}
404
-
405
-		switch ($prefixPath) {
406
-			case 'principals/users':
407
-				return $this->searchUserPrincipals($searchProperties, $test);
408
-
409
-			default:
410
-				return [];
411
-		}
412
-	}
413
-
414
-	/**
415
-	 * @param string $uri
416
-	 * @param string $principalPrefix
417
-	 * @return string
418
-	 */
419
-	public function findByUri($uri, $principalPrefix) {
420
-		// If sharing is disabled, return the empty array
421
-		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
422
-		if (!$shareAPIEnabled) {
423
-			return null;
424
-		}
425
-
426
-		// If sharing is restricted to group members only,
427
-		// return only members that have groups in common
428
-		$restrictGroups = false;
429
-		if ($this->shareManager->shareWithGroupMembersOnly()) {
430
-			$user = $this->userSession->getUser();
431
-			if (!$user) {
432
-				return null;
433
-			}
434
-
435
-			$restrictGroups = $this->groupManager->getUserGroupIds($user);
436
-		}
437
-
438
-		if (str_starts_with($uri, 'mailto:')) {
439
-			if ($principalPrefix === 'principals/users') {
440
-				$users = $this->userManager->getByEmail(substr($uri, 7));
441
-				if (count($users) !== 1) {
442
-					return null;
443
-				}
444
-				$user = $users[0];
445
-
446
-				if ($restrictGroups !== false) {
447
-					$userGroups = $this->groupManager->getUserGroupIds($user);
448
-					if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
449
-						return null;
450
-					}
451
-				}
452
-
453
-				return $this->principalPrefix . '/' . $user->getUID();
454
-			}
455
-		}
456
-		if (str_starts_with($uri, 'principal:')) {
457
-			$principal = substr($uri, 10);
458
-			$principal = $this->getPrincipalByPath($principal);
459
-			if ($principal !== null) {
460
-				return $principal['uri'];
461
-			}
462
-		}
463
-
464
-		return null;
465
-	}
466
-
467
-	/**
468
-	 * @param IUser $user
469
-	 * @return array
470
-	 * @throws PropertyDoesNotExistException
471
-	 */
472
-	protected function userToPrincipal($user) {
473
-		$userId = $user->getUID();
474
-		$displayName = $user->getDisplayName();
475
-		$principal = [
476
-			'uri' => $this->principalPrefix . '/' . $userId,
477
-			'{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
478
-			'{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
479
-			'{http://nextcloud.com/ns}language' => $this->languageFactory->getUserLanguage($user),
480
-		];
481
-
482
-		$account = $this->accountManager->getAccount($user);
483
-		$alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties());
484
-
485
-		$email = $user->getSystemEMailAddress();
486
-		if (!empty($email)) {
487
-			$principal['{http://sabredav.org/ns}email-address'] = $email;
488
-		}
489
-
490
-		if (!empty($alternativeEmails)) {
491
-			$principal['{DAV:}alternate-URI-set'] = $alternativeEmails;
492
-		}
493
-
494
-		return $principal;
495
-	}
496
-
497
-	public function getPrincipalPrefix() {
498
-		return $this->principalPrefix;
499
-	}
500
-
501
-	/**
502
-	 * @param string $circleUniqueId
503
-	 * @return array|null
504
-	 */
505
-	protected function circleToPrincipal($circleUniqueId) {
506
-		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
507
-			return null;
508
-		}
509
-
510
-		try {
511
-			$circle = Circles::detailsCircle($circleUniqueId, true);
512
-		} catch (QueryException $ex) {
513
-			return null;
514
-		} catch (CircleNotFoundException $ex) {
515
-			return null;
516
-		}
517
-
518
-		if (!$circle) {
519
-			return null;
520
-		}
521
-
522
-		$principal = [
523
-			'uri' => 'principals/circles/' . $circleUniqueId,
524
-			'{DAV:}displayname' => $circle->getDisplayName(),
525
-		];
526
-
527
-		return $principal;
528
-	}
529
-
530
-	/**
531
-	 * Returns the list of circles a principal is a member of
532
-	 *
533
-	 * @param string $principal
534
-	 * @return array
535
-	 * @throws Exception
536
-	 * @throws QueryException
537
-	 * @suppress PhanUndeclaredClassMethod
538
-	 */
539
-	public function getCircleMembership($principal):array {
540
-		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
541
-			return [];
542
-		}
543
-
544
-		[$prefix, $name] = \Sabre\Uri\split($principal);
545
-		if ($this->hasCircles && $prefix === $this->principalPrefix) {
546
-			$user = $this->userManager->get($name);
547
-			if (!$user) {
548
-				throw new Exception('Principal not found');
549
-			}
550
-
551
-			$circles = Circles::joinedCircles($name, true);
552
-
553
-			$circles = array_map(function ($circle) {
554
-				/** @var Circle $circle */
555
-				return 'principals/circles/' . urlencode($circle->getSingleId());
556
-			}, $circles);
557
-
558
-			return $circles;
559
-		}
560
-
561
-		return [];
562
-	}
563
-
564
-	/**
565
-	 * Get all email addresses associated to a principal.
566
-	 *
567
-	 * @param array $principal Data from getPrincipal*()
568
-	 * @return string[] All email addresses without the mailto: prefix
569
-	 */
570
-	public function getEmailAddressesOfPrincipal(array $principal): array {
571
-		$emailAddresses = [];
572
-
573
-		if (isset($principal['{http://sabredav.org/ns}email-address'])) {
574
-			$emailAddresses[] = $principal['{http://sabredav.org/ns}email-address'];
575
-		}
576
-
577
-		if (isset($principal['{DAV:}alternate-URI-set'])) {
578
-			foreach ($principal['{DAV:}alternate-URI-set'] as $address) {
579
-				if (str_starts_with($address, 'mailto:')) {
580
-					$emailAddresses[] = substr($address, 7);
581
-				}
582
-			}
583
-		}
584
-
585
-		if (isset($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'])) {
586
-			foreach ($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'] as $address) {
587
-				if (str_starts_with($address, 'mailto:')) {
588
-					$emailAddresses[] = substr($address, 7);
589
-				}
590
-			}
591
-		}
592
-
593
-		if (isset($principal['{http://calendarserver.org/ns/}email-address-set'])) {
594
-			foreach ($principal['{http://calendarserver.org/ns/}email-address-set'] as $address) {
595
-				if (str_starts_with($address, 'mailto:')) {
596
-					$emailAddresses[] = substr($address, 7);
597
-				}
598
-			}
599
-		}
600
-
601
-		return array_values(array_unique($emailAddresses));
602
-	}
35
+    /** @var string */
36
+    private $principalPrefix;
37
+
38
+    /** @var bool */
39
+    private $hasGroups;
40
+
41
+    /** @var bool */
42
+    private $hasCircles;
43
+
44
+    /** @var ProxyMapper */
45
+    private $proxyMapper;
46
+
47
+    /** @var KnownUserService */
48
+    private $knownUserService;
49
+
50
+    public function __construct(
51
+        private IUserManager $userManager,
52
+        private IGroupManager $groupManager,
53
+        private IAccountManager $accountManager,
54
+        private IShareManager $shareManager,
55
+        private IUserSession $userSession,
56
+        private IAppManager $appManager,
57
+        ProxyMapper $proxyMapper,
58
+        KnownUserService $knownUserService,
59
+        private IConfig $config,
60
+        private IFactory $languageFactory,
61
+        string $principalPrefix = 'principals/users/',
62
+    ) {
63
+        $this->principalPrefix = trim($principalPrefix, '/');
64
+        $this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
65
+        $this->proxyMapper = $proxyMapper;
66
+        $this->knownUserService = $knownUserService;
67
+    }
68
+
69
+    use PrincipalProxyTrait {
70
+        getGroupMembership as protected traitGetGroupMembership;
71
+    }
72
+
73
+    /**
74
+     * Returns a list of principals based on a prefix.
75
+     *
76
+     * This prefix will often contain something like 'principals'. You are only
77
+     * expected to return principals that are in this base path.
78
+     *
79
+     * You are expected to return at least a 'uri' for every user, you can
80
+     * return any additional properties if you wish so. Common properties are:
81
+     *   {DAV:}displayname
82
+     *
83
+     * @param string $prefixPath
84
+     * @return string[]
85
+     */
86
+    public function getPrincipalsByPrefix($prefixPath) {
87
+        $principals = [];
88
+
89
+        if ($prefixPath === $this->principalPrefix) {
90
+            foreach ($this->userManager->search('') as $user) {
91
+                $principals[] = $this->userToPrincipal($user);
92
+            }
93
+        }
94
+
95
+        return $principals;
96
+    }
97
+
98
+    /**
99
+     * Returns a specific principal, specified by it's path.
100
+     * The returned structure should be the exact same as from
101
+     * getPrincipalsByPrefix.
102
+     *
103
+     * @param string $path
104
+     * @return array
105
+     */
106
+    public function getPrincipalByPath($path) {
107
+        [$prefix, $name] = \Sabre\Uri\split($path);
108
+        $decodedName = urldecode($name);
109
+
110
+        if ($name === 'calendar-proxy-write' || $name === 'calendar-proxy-read') {
111
+            [$prefix2, $name2] = \Sabre\Uri\split($prefix);
112
+
113
+            if ($prefix2 === $this->principalPrefix) {
114
+                $user = $this->userManager->get($name2);
115
+
116
+                if ($user !== null) {
117
+                    return [
118
+                        'uri' => 'principals/users/' . $user->getUID() . '/' . $name,
119
+                    ];
120
+                }
121
+                return null;
122
+            }
123
+        }
124
+
125
+        if ($prefix === $this->principalPrefix) {
126
+            // Depending on where it is called, it may happen that this function
127
+            // is called either with a urlencoded version of the name or with a non-urlencoded one.
128
+            // The urldecode function replaces %## and +, both of which are forbidden in usernames.
129
+            // Hence there can be no ambiguity here and it is safe to call urldecode on all usernames
130
+            $user = $this->userManager->get($decodedName);
131
+
132
+            if ($user !== null) {
133
+                return $this->userToPrincipal($user);
134
+            }
135
+        } elseif ($prefix === 'principals/circles') {
136
+            if ($this->userSession->getUser() !== null) {
137
+                // At the time of writing - 2021-01-19 — a mixed state is possible.
138
+                // The second condition can be removed when this is fixed.
139
+                return $this->circleToPrincipal($decodedName)
140
+                    ?: $this->circleToPrincipal($name);
141
+            }
142
+        } elseif ($prefix === 'principals/groups') {
143
+            // At the time of writing - 2021-01-19 — a mixed state is possible.
144
+            // The second condition can be removed when this is fixed.
145
+            $group = $this->groupManager->get($decodedName)
146
+                ?: $this->groupManager->get($name);
147
+            if ($group instanceof IGroup) {
148
+                return [
149
+                    'uri' => 'principals/groups/' . $name,
150
+                    '{DAV:}displayname' => $group->getDisplayName(),
151
+                ];
152
+            }
153
+        } elseif ($prefix === 'principals/system') {
154
+            return [
155
+                'uri' => 'principals/system/' . $name,
156
+                '{DAV:}displayname' => $this->languageFactory->get('dav')->t('Accounts'),
157
+            ];
158
+        } elseif ($prefix === 'principals/shares') {
159
+            return [
160
+                'uri' => 'principals/shares/' . $name,
161
+                '{DAV:}displayname' => $name,
162
+            ];
163
+        }
164
+        return null;
165
+    }
166
+
167
+    /**
168
+     * Returns the list of groups a principal is a member of
169
+     *
170
+     * @param string $principal
171
+     * @param bool $needGroups
172
+     * @return array
173
+     * @throws Exception
174
+     */
175
+    public function getGroupMembership($principal, $needGroups = false) {
176
+        [$prefix, $name] = \Sabre\Uri\split($principal);
177
+
178
+        if ($prefix !== $this->principalPrefix) {
179
+            return [];
180
+        }
181
+
182
+        $user = $this->userManager->get($name);
183
+        if (!$user) {
184
+            throw new Exception('Principal not found');
185
+        }
186
+
187
+        $groups = [];
188
+
189
+        if ($this->hasGroups || $needGroups) {
190
+            $userGroups = $this->groupManager->getUserGroups($user);
191
+            foreach ($userGroups as $userGroup) {
192
+                $groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
193
+            }
194
+        }
195
+
196
+        $groups = array_unique(array_merge(
197
+            $groups,
198
+            $this->traitGetGroupMembership($principal, $needGroups)
199
+        ));
200
+
201
+        return $groups;
202
+    }
203
+
204
+    /**
205
+     * @param string $path
206
+     * @param PropPatch $propPatch
207
+     * @return int
208
+     */
209
+    public function updatePrincipal($path, PropPatch $propPatch) {
210
+        // Updating schedule-default-calendar-URL is handled in CustomPropertiesBackend
211
+        return 0;
212
+    }
213
+
214
+    /**
215
+     * Search user principals
216
+     *
217
+     * @param array $searchProperties
218
+     * @param string $test
219
+     * @return array
220
+     */
221
+    protected function searchUserPrincipals(array $searchProperties, $test = 'allof') {
222
+        $results = [];
223
+
224
+        // If sharing is disabled, return the empty array
225
+        $shareAPIEnabled = $this->shareManager->shareApiEnabled();
226
+        if (!$shareAPIEnabled) {
227
+            return [];
228
+        }
229
+
230
+        $allowEnumeration = $this->shareManager->allowEnumeration();
231
+        $limitEnumerationGroup = $this->shareManager->limitEnumerationToGroups();
232
+        $limitEnumerationPhone = $this->shareManager->limitEnumerationToPhone();
233
+        $allowEnumerationFullMatch = $this->shareManager->allowEnumerationFullMatch();
234
+        $ignoreSecondDisplayName = $this->shareManager->ignoreSecondDisplayName();
235
+        $matchEmail = $this->shareManager->matchEmail();
236
+
237
+        // If sharing is restricted to group members only,
238
+        // return only members that have groups in common
239
+        $restrictGroups = false;
240
+        $currentUser = $this->userSession->getUser();
241
+        if ($this->shareManager->shareWithGroupMembersOnly()) {
242
+            if (!$currentUser instanceof IUser) {
243
+                return [];
244
+            }
245
+
246
+            $restrictGroups = $this->groupManager->getUserGroupIds($currentUser);
247
+        }
248
+
249
+        $currentUserGroups = [];
250
+        if ($limitEnumerationGroup) {
251
+            if ($currentUser instanceof IUser) {
252
+                $currentUserGroups = $this->groupManager->getUserGroupIds($currentUser);
253
+            }
254
+        }
255
+
256
+        $searchLimit = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
257
+        if ($searchLimit <= 0) {
258
+            $searchLimit = null;
259
+        }
260
+        foreach ($searchProperties as $prop => $value) {
261
+            switch ($prop) {
262
+                case '{http://sabredav.org/ns}email-address':
263
+                    if (!$allowEnumeration) {
264
+                        if ($allowEnumerationFullMatch && $matchEmail) {
265
+                            $users = $this->userManager->getByEmail($value);
266
+                        } else {
267
+                            $users = [];
268
+                        }
269
+                    } else {
270
+                        $users = $this->userManager->getByEmail($value);
271
+                        $users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
272
+                            if ($allowEnumerationFullMatch && $user->getSystemEMailAddress() === $value) {
273
+                                return true;
274
+                            }
275
+
276
+                            if ($limitEnumerationPhone
277
+                                && $currentUser instanceof IUser
278
+                                && $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
279
+                                // Synced phonebook match
280
+                                return true;
281
+                            }
282
+
283
+                            if (!$limitEnumerationGroup) {
284
+                                // No limitation on enumeration, all allowed
285
+                                return true;
286
+                            }
287
+
288
+                            return !empty($currentUserGroups) && !empty(array_intersect(
289
+                                $this->groupManager->getUserGroupIds($user),
290
+                                $currentUserGroups
291
+                            ));
292
+                        });
293
+                    }
294
+
295
+                    $results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
296
+                        // is sharing restricted to groups only?
297
+                        if ($restrictGroups !== false) {
298
+                            $userGroups = $this->groupManager->getUserGroupIds($user);
299
+                            if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
300
+                                return $carry;
301
+                            }
302
+                        }
303
+
304
+                        $carry[] = $this->principalPrefix . '/' . $user->getUID();
305
+                        return $carry;
306
+                    }, []);
307
+                    break;
308
+
309
+                case '{DAV:}displayname':
310
+
311
+                    if (!$allowEnumeration) {
312
+                        if ($allowEnumerationFullMatch) {
313
+                            $lowerSearch = strtolower($value);
314
+                            $users = $this->userManager->searchDisplayName($value, $searchLimit);
315
+                            $users = \array_filter($users, static function (IUser $user) use ($lowerSearch, $ignoreSecondDisplayName) {
316
+                                $lowerDisplayName = strtolower($user->getDisplayName());
317
+                                return $lowerDisplayName === $lowerSearch || ($ignoreSecondDisplayName && trim(preg_replace('/ \(.*\)$/', '', $lowerDisplayName)) === $lowerSearch);
318
+                            });
319
+                        } else {
320
+                            $users = [];
321
+                        }
322
+                    } else {
323
+                        $users = $this->userManager->searchDisplayName($value, $searchLimit);
324
+                        $users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
325
+                            if ($allowEnumerationFullMatch && $user->getDisplayName() === $value) {
326
+                                return true;
327
+                            }
328
+
329
+                            if ($limitEnumerationPhone
330
+                                && $currentUser instanceof IUser
331
+                                && $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
332
+                                // Synced phonebook match
333
+                                return true;
334
+                            }
335
+
336
+                            if (!$limitEnumerationGroup) {
337
+                                // No limitation on enumeration, all allowed
338
+                                return true;
339
+                            }
340
+
341
+                            return !empty($currentUserGroups) && !empty(array_intersect(
342
+                                $this->groupManager->getUserGroupIds($user),
343
+                                $currentUserGroups
344
+                            ));
345
+                        });
346
+                    }
347
+
348
+                    $results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
349
+                        // is sharing restricted to groups only?
350
+                        if ($restrictGroups !== false) {
351
+                            $userGroups = $this->groupManager->getUserGroupIds($user);
352
+                            if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
353
+                                return $carry;
354
+                            }
355
+                        }
356
+
357
+                        $carry[] = $this->principalPrefix . '/' . $user->getUID();
358
+                        return $carry;
359
+                    }, []);
360
+                    break;
361
+
362
+                case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
363
+                    // If you add support for more search properties that qualify as a user-address,
364
+                    // please also add them to the array below
365
+                    $results[] = $this->searchUserPrincipals([
366
+                        // In theory this should also search for principal:principals/users/...
367
+                        // but that's used internally only anyway and i don't know of any client querying that
368
+                        '{http://sabredav.org/ns}email-address' => $value,
369
+                    ], 'anyof');
370
+                    break;
371
+
372
+                default:
373
+                    $results[] = [];
374
+                    break;
375
+            }
376
+        }
377
+
378
+        // results is an array of arrays, so this is not the first search result
379
+        // but the results of the first searchProperty
380
+        if (count($results) === 1) {
381
+            return $results[0];
382
+        }
383
+
384
+        switch ($test) {
385
+            case 'anyof':
386
+                return array_values(array_unique(array_merge(...$results)));
387
+
388
+            case 'allof':
389
+            default:
390
+                return array_values(array_intersect(...$results));
391
+        }
392
+    }
393
+
394
+    /**
395
+     * @param string $prefixPath
396
+     * @param array $searchProperties
397
+     * @param string $test
398
+     * @return array
399
+     */
400
+    public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
401
+        if (count($searchProperties) === 0) {
402
+            return [];
403
+        }
404
+
405
+        switch ($prefixPath) {
406
+            case 'principals/users':
407
+                return $this->searchUserPrincipals($searchProperties, $test);
408
+
409
+            default:
410
+                return [];
411
+        }
412
+    }
413
+
414
+    /**
415
+     * @param string $uri
416
+     * @param string $principalPrefix
417
+     * @return string
418
+     */
419
+    public function findByUri($uri, $principalPrefix) {
420
+        // If sharing is disabled, return the empty array
421
+        $shareAPIEnabled = $this->shareManager->shareApiEnabled();
422
+        if (!$shareAPIEnabled) {
423
+            return null;
424
+        }
425
+
426
+        // If sharing is restricted to group members only,
427
+        // return only members that have groups in common
428
+        $restrictGroups = false;
429
+        if ($this->shareManager->shareWithGroupMembersOnly()) {
430
+            $user = $this->userSession->getUser();
431
+            if (!$user) {
432
+                return null;
433
+            }
434
+
435
+            $restrictGroups = $this->groupManager->getUserGroupIds($user);
436
+        }
437
+
438
+        if (str_starts_with($uri, 'mailto:')) {
439
+            if ($principalPrefix === 'principals/users') {
440
+                $users = $this->userManager->getByEmail(substr($uri, 7));
441
+                if (count($users) !== 1) {
442
+                    return null;
443
+                }
444
+                $user = $users[0];
445
+
446
+                if ($restrictGroups !== false) {
447
+                    $userGroups = $this->groupManager->getUserGroupIds($user);
448
+                    if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
449
+                        return null;
450
+                    }
451
+                }
452
+
453
+                return $this->principalPrefix . '/' . $user->getUID();
454
+            }
455
+        }
456
+        if (str_starts_with($uri, 'principal:')) {
457
+            $principal = substr($uri, 10);
458
+            $principal = $this->getPrincipalByPath($principal);
459
+            if ($principal !== null) {
460
+                return $principal['uri'];
461
+            }
462
+        }
463
+
464
+        return null;
465
+    }
466
+
467
+    /**
468
+     * @param IUser $user
469
+     * @return array
470
+     * @throws PropertyDoesNotExistException
471
+     */
472
+    protected function userToPrincipal($user) {
473
+        $userId = $user->getUID();
474
+        $displayName = $user->getDisplayName();
475
+        $principal = [
476
+            'uri' => $this->principalPrefix . '/' . $userId,
477
+            '{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
478
+            '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
479
+            '{http://nextcloud.com/ns}language' => $this->languageFactory->getUserLanguage($user),
480
+        ];
481
+
482
+        $account = $this->accountManager->getAccount($user);
483
+        $alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties());
484
+
485
+        $email = $user->getSystemEMailAddress();
486
+        if (!empty($email)) {
487
+            $principal['{http://sabredav.org/ns}email-address'] = $email;
488
+        }
489
+
490
+        if (!empty($alternativeEmails)) {
491
+            $principal['{DAV:}alternate-URI-set'] = $alternativeEmails;
492
+        }
493
+
494
+        return $principal;
495
+    }
496
+
497
+    public function getPrincipalPrefix() {
498
+        return $this->principalPrefix;
499
+    }
500
+
501
+    /**
502
+     * @param string $circleUniqueId
503
+     * @return array|null
504
+     */
505
+    protected function circleToPrincipal($circleUniqueId) {
506
+        if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
507
+            return null;
508
+        }
509
+
510
+        try {
511
+            $circle = Circles::detailsCircle($circleUniqueId, true);
512
+        } catch (QueryException $ex) {
513
+            return null;
514
+        } catch (CircleNotFoundException $ex) {
515
+            return null;
516
+        }
517
+
518
+        if (!$circle) {
519
+            return null;
520
+        }
521
+
522
+        $principal = [
523
+            'uri' => 'principals/circles/' . $circleUniqueId,
524
+            '{DAV:}displayname' => $circle->getDisplayName(),
525
+        ];
526
+
527
+        return $principal;
528
+    }
529
+
530
+    /**
531
+     * Returns the list of circles a principal is a member of
532
+     *
533
+     * @param string $principal
534
+     * @return array
535
+     * @throws Exception
536
+     * @throws QueryException
537
+     * @suppress PhanUndeclaredClassMethod
538
+     */
539
+    public function getCircleMembership($principal):array {
540
+        if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
541
+            return [];
542
+        }
543
+
544
+        [$prefix, $name] = \Sabre\Uri\split($principal);
545
+        if ($this->hasCircles && $prefix === $this->principalPrefix) {
546
+            $user = $this->userManager->get($name);
547
+            if (!$user) {
548
+                throw new Exception('Principal not found');
549
+            }
550
+
551
+            $circles = Circles::joinedCircles($name, true);
552
+
553
+            $circles = array_map(function ($circle) {
554
+                /** @var Circle $circle */
555
+                return 'principals/circles/' . urlencode($circle->getSingleId());
556
+            }, $circles);
557
+
558
+            return $circles;
559
+        }
560
+
561
+        return [];
562
+    }
563
+
564
+    /**
565
+     * Get all email addresses associated to a principal.
566
+     *
567
+     * @param array $principal Data from getPrincipal*()
568
+     * @return string[] All email addresses without the mailto: prefix
569
+     */
570
+    public function getEmailAddressesOfPrincipal(array $principal): array {
571
+        $emailAddresses = [];
572
+
573
+        if (isset($principal['{http://sabredav.org/ns}email-address'])) {
574
+            $emailAddresses[] = $principal['{http://sabredav.org/ns}email-address'];
575
+        }
576
+
577
+        if (isset($principal['{DAV:}alternate-URI-set'])) {
578
+            foreach ($principal['{DAV:}alternate-URI-set'] as $address) {
579
+                if (str_starts_with($address, 'mailto:')) {
580
+                    $emailAddresses[] = substr($address, 7);
581
+                }
582
+            }
583
+        }
584
+
585
+        if (isset($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'])) {
586
+            foreach ($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'] as $address) {
587
+                if (str_starts_with($address, 'mailto:')) {
588
+                    $emailAddresses[] = substr($address, 7);
589
+                }
590
+            }
591
+        }
592
+
593
+        if (isset($principal['{http://calendarserver.org/ns/}email-address-set'])) {
594
+            foreach ($principal['{http://calendarserver.org/ns/}email-address-set'] as $address) {
595
+                if (str_starts_with($address, 'mailto:')) {
596
+                    $emailAddresses[] = substr($address, 7);
597
+                }
598
+            }
599
+        }
600
+
601
+        return array_values(array_unique($emailAddresses));
602
+    }
603 603
 }
Please login to merge, or discard this patch.
apps/dav/lib/Connector/Sabre/Directory.php 1 patch
Indentation   +449 added lines, -449 removed lines patch added patch discarded remove patch
@@ -40,453 +40,453 @@
 block discarded – undo
40 40
 use Sabre\DAV\INode;
41 41
 
42 42
 class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget {
43
-	/**
44
-	 * Cached directory content
45
-	 * @var FileInfo[]
46
-	 */
47
-	private ?array $dirContent = null;
48
-
49
-	/** Cached quota info */
50
-	private ?array $quotaInfo = null;
51
-
52
-	/**
53
-	 * Sets up the node, expects a full path name
54
-	 */
55
-	public function __construct(
56
-		View $view,
57
-		FileInfo $info,
58
-		private ?CachingTree $tree = null,
59
-		?IShareManager $shareManager = null,
60
-	) {
61
-		parent::__construct($view, $info, $shareManager);
62
-	}
63
-
64
-	/**
65
-	 * Creates a new file in the directory
66
-	 *
67
-	 * Data will either be supplied as a stream resource, or in certain cases
68
-	 * as a string. Keep in mind that you may have to support either.
69
-	 *
70
-	 * After successful creation of the file, you may choose to return the ETag
71
-	 * of the new file here.
72
-	 *
73
-	 * The returned ETag must be surrounded by double-quotes (The quotes should
74
-	 * be part of the actual string).
75
-	 *
76
-	 * If you cannot accurately determine the ETag, you should not return it.
77
-	 * If you don't store the file exactly as-is (you're transforming it
78
-	 * somehow) you should also not return an ETag.
79
-	 *
80
-	 * This means that if a subsequent GET to this new file does not exactly
81
-	 * return the same contents of what was submitted here, you are strongly
82
-	 * recommended to omit the ETag.
83
-	 *
84
-	 * @param string $name Name of the file
85
-	 * @param resource|string $data Initial payload
86
-	 * @return null|string
87
-	 * @throws Exception\EntityTooLarge
88
-	 * @throws Exception\UnsupportedMediaType
89
-	 * @throws FileLocked
90
-	 * @throws InvalidPath
91
-	 * @throws \Sabre\DAV\Exception
92
-	 * @throws \Sabre\DAV\Exception\BadRequest
93
-	 * @throws \Sabre\DAV\Exception\Forbidden
94
-	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
95
-	 */
96
-	public function createFile($name, $data = null) {
97
-		try {
98
-			if (!$this->fileView->isCreatable($this->path)) {
99
-				throw new \Sabre\DAV\Exception\Forbidden();
100
-			}
101
-
102
-			$this->fileView->verifyPath($this->path, $name);
103
-
104
-			$path = $this->fileView->getAbsolutePath($this->path) . '/' . $name;
105
-			// in case the file already exists/overwriting
106
-			$info = $this->fileView->getFileInfo($this->path . '/' . $name);
107
-			if (!$info) {
108
-				// use a dummy FileInfo which is acceptable here since it will be refreshed after the put is complete
109
-				$info = new \OC\Files\FileInfo($path, null, null, [
110
-					'type' => FileInfo::TYPE_FILE
111
-				], null);
112
-			}
113
-			$node = new File($this->fileView, $info);
114
-
115
-			// only allow 1 process to upload a file at once but still allow reading the file while writing the part file
116
-			$node->acquireLock(ILockingProvider::LOCK_SHARED);
117
-			$this->fileView->lockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
118
-
119
-			$result = $node->put($data);
120
-
121
-			$this->fileView->unlockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
122
-			$node->releaseLock(ILockingProvider::LOCK_SHARED);
123
-			return $result;
124
-		} catch (StorageNotAvailableException $e) {
125
-			throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
126
-		} catch (InvalidPathException $ex) {
127
-			throw new InvalidPath($ex->getMessage(), false, $ex);
128
-		} catch (ForbiddenException $ex) {
129
-			throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
130
-		} catch (LockedException $e) {
131
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
132
-		}
133
-	}
134
-
135
-	/**
136
-	 * Creates a new subdirectory
137
-	 *
138
-	 * @param string $name
139
-	 * @throws FileLocked
140
-	 * @throws InvalidPath
141
-	 * @throws \Sabre\DAV\Exception\Forbidden
142
-	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
143
-	 */
144
-	public function createDirectory($name) {
145
-		try {
146
-			if (!$this->info->isCreatable()) {
147
-				throw new \Sabre\DAV\Exception\Forbidden();
148
-			}
149
-
150
-			$this->fileView->verifyPath($this->path, $name);
151
-			$newPath = $this->path . '/' . $name;
152
-			if (!$this->fileView->mkdir($newPath)) {
153
-				throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
154
-			}
155
-		} catch (StorageNotAvailableException $e) {
156
-			throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
157
-		} catch (InvalidPathException $ex) {
158
-			throw new InvalidPath($ex->getMessage(), false, $ex);
159
-		} catch (ForbiddenException $ex) {
160
-			throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
161
-		} catch (LockedException $e) {
162
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
163
-		}
164
-	}
165
-
166
-	/**
167
-	 * Returns a specific child node, referenced by its name
168
-	 *
169
-	 * @param string $name
170
-	 * @param FileInfo $info
171
-	 * @return \Sabre\DAV\INode
172
-	 * @throws InvalidPath
173
-	 * @throws \Sabre\DAV\Exception\NotFound
174
-	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
175
-	 */
176
-	public function getChild($name, $info = null, ?IRequest $request = null, ?IL10N $l10n = null) {
177
-		$storage = $this->info->getStorage();
178
-		$allowDirectory = false;
179
-		if ($storage instanceof PublicShareWrapper) {
180
-			$share = $storage->getShare();
181
-			$allowDirectory =
182
-				// Only allow directories for file drops
183
-				($share->getPermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ &&
184
-				// And only allow it for directories which are a direct child of the share root
185
-				$this->info->getId() === $share->getNodeId();
186
-		}
187
-
188
-		// For file drop we need to be allowed to read the directory with the nickname
189
-		if (!$allowDirectory && !$this->info->isReadable()) {
190
-			// avoid detecting files through this way
191
-			throw new NotFound();
192
-		}
193
-
194
-		$path = $this->path . '/' . $name;
195
-		if (is_null($info)) {
196
-			try {
197
-				$this->fileView->verifyPath($this->path, $name, true);
198
-				$info = $this->fileView->getFileInfo($path);
199
-			} catch (StorageNotAvailableException $e) {
200
-				throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
201
-			} catch (InvalidPathException $ex) {
202
-				throw new InvalidPath($ex->getMessage(), false, $ex);
203
-			} catch (ForbiddenException $e) {
204
-				throw new \Sabre\DAV\Exception\Forbidden($e->getMessage(), $e->getCode(), $e);
205
-			}
206
-		}
207
-
208
-		if (!$info) {
209
-			throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
210
-		}
211
-
212
-		if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
213
-			$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
214
-		} else {
215
-			// In case reading a directory was allowed but it turns out the node was a not a directory, reject it now.
216
-			if (!$this->info->isReadable()) {
217
-				throw new NotFound();
218
-			}
219
-
220
-			$node = new File($this->fileView, $info, $this->shareManager, $request, $l10n);
221
-		}
222
-		if ($this->tree) {
223
-			$this->tree->cacheNode($node);
224
-		}
225
-		return $node;
226
-	}
227
-
228
-	/**
229
-	 * Returns an array with all the child nodes
230
-	 *
231
-	 * @return \Sabre\DAV\INode[]
232
-	 * @throws \Sabre\DAV\Exception\Locked
233
-	 * @throws Forbidden
234
-	 */
235
-	public function getChildren() {
236
-		if (!is_null($this->dirContent)) {
237
-			return $this->dirContent;
238
-		}
239
-		try {
240
-			if (!$this->info->isReadable()) {
241
-				// return 403 instead of 404 because a 404 would make
242
-				// the caller believe that the collection itself does not exist
243
-				if (Server::get(IAppManager::class)->isEnabledForAnyone('files_accesscontrol')) {
244
-					throw new Forbidden('No read permissions. This might be caused by files_accesscontrol, check your configured rules');
245
-				} else {
246
-					throw new Forbidden('No read permissions');
247
-				}
248
-			}
249
-			$folderContent = $this->getNode()->getDirectoryListing();
250
-		} catch (LockedException $e) {
251
-			throw new Locked();
252
-		}
253
-
254
-		$nodes = [];
255
-		$request = Server::get(IRequest::class);
256
-		$l10nFactory = Server::get(IFactory::class);
257
-		$l10n = $l10nFactory->get(Application::APP_ID);
258
-		foreach ($folderContent as $info) {
259
-			$node = $this->getChild($info->getName(), $info, $request, $l10n);
260
-			$nodes[] = $node;
261
-		}
262
-		$this->dirContent = $nodes;
263
-		return $this->dirContent;
264
-	}
265
-
266
-	/**
267
-	 * Checks if a child exists.
268
-	 *
269
-	 * @param string $name
270
-	 * @return bool
271
-	 */
272
-	public function childExists($name) {
273
-		// note: here we do NOT resolve the chunk file name to the real file name
274
-		// to make sure we return false when checking for file existence with a chunk
275
-		// file name.
276
-		// This is to make sure that "createFile" is still triggered
277
-		// (required old code) instead of "updateFile".
278
-		//
279
-		// TODO: resolve chunk file name here and implement "updateFile"
280
-		$path = $this->path . '/' . $name;
281
-		return $this->fileView->file_exists($path);
282
-	}
283
-
284
-	/**
285
-	 * Deletes all files in this directory, and then itself
286
-	 *
287
-	 * @return void
288
-	 * @throws FileLocked
289
-	 * @throws \Sabre\DAV\Exception\Forbidden
290
-	 */
291
-	public function delete() {
292
-		if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) {
293
-			throw new \Sabre\DAV\Exception\Forbidden();
294
-		}
295
-
296
-		try {
297
-			if (!$this->fileView->rmdir($this->path)) {
298
-				// assume it wasn't possible to remove due to permission issue
299
-				throw new \Sabre\DAV\Exception\Forbidden();
300
-			}
301
-		} catch (ForbiddenException $ex) {
302
-			throw new Forbidden($ex->getMessage(), $ex->getRetry());
303
-		} catch (LockedException $e) {
304
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
305
-		}
306
-	}
307
-
308
-	private function getLogger(): LoggerInterface {
309
-		return Server::get(LoggerInterface::class);
310
-	}
311
-
312
-	/**
313
-	 * Returns available diskspace information
314
-	 *
315
-	 * @return array
316
-	 */
317
-	public function getQuotaInfo() {
318
-		if ($this->quotaInfo) {
319
-			return $this->quotaInfo;
320
-		}
321
-		$relativePath = $this->fileView->getRelativePath($this->info->getPath());
322
-		if ($relativePath === null) {
323
-			$this->getLogger()->warning('error while getting quota as the relative path cannot be found');
324
-			return [0, 0];
325
-		}
326
-
327
-		try {
328
-			$storageInfo = \OC_Helper::getStorageInfo($relativePath, $this->info, false);
329
-			if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) {
330
-				$free = FileInfo::SPACE_UNLIMITED;
331
-			} else {
332
-				$free = $storageInfo['free'];
333
-			}
334
-			$this->quotaInfo = [
335
-				$storageInfo['used'],
336
-				$free
337
-			];
338
-			return $this->quotaInfo;
339
-		} catch (NotFoundException $e) {
340
-			$this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
341
-			return [0, 0];
342
-		} catch (StorageNotAvailableException $e) {
343
-			$this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
344
-			return [0, 0];
345
-		} catch (NotPermittedException $e) {
346
-			$this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
347
-			return [0, 0];
348
-		}
349
-	}
350
-
351
-	/**
352
-	 * Moves a node into this collection.
353
-	 *
354
-	 * It is up to the implementors to:
355
-	 *   1. Create the new resource.
356
-	 *   2. Remove the old resource.
357
-	 *   3. Transfer any properties or other data.
358
-	 *
359
-	 * Generally you should make very sure that your collection can easily move
360
-	 * the move.
361
-	 *
362
-	 * If you don't, just return false, which will trigger sabre/dav to handle
363
-	 * the move itself. If you return true from this function, the assumption
364
-	 * is that the move was successful.
365
-	 *
366
-	 * @param string $targetName New local file/collection name.
367
-	 * @param string $fullSourcePath Full path to source node
368
-	 * @param INode $sourceNode Source node itself
369
-	 * @return bool
370
-	 * @throws BadRequest
371
-	 * @throws ServiceUnavailable
372
-	 * @throws Forbidden
373
-	 * @throws FileLocked
374
-	 * @throws \Sabre\DAV\Exception\Forbidden
375
-	 */
376
-	public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
377
-		if (!$sourceNode instanceof Node) {
378
-			// it's a file of another kind, like FutureFile
379
-			if ($sourceNode instanceof IFile) {
380
-				// fallback to default copy+delete handling
381
-				return false;
382
-			}
383
-			throw new BadRequest('Incompatible node types');
384
-		}
385
-
386
-		$destinationPath = $this->getPath() . '/' . $targetName;
387
-
388
-
389
-		$targetNodeExists = $this->childExists($targetName);
390
-
391
-		// at getNodeForPath we also check the path for isForbiddenFileOrDir
392
-		// with that we have covered both source and destination
393
-		if ($sourceNode instanceof Directory && $targetNodeExists) {
394
-			throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
395
-		}
396
-
397
-		[$sourceDir,] = \Sabre\Uri\split($sourceNode->getPath());
398
-		$destinationDir = $this->getPath();
399
-
400
-		$sourcePath = $sourceNode->getPath();
401
-
402
-		$isMovableMount = false;
403
-		$sourceMount = Server::get(IMountManager::class)->find($this->fileView->getAbsolutePath($sourcePath));
404
-		$internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath));
405
-		if ($sourceMount instanceof MoveableMount && $internalPath === '') {
406
-			$isMovableMount = true;
407
-		}
408
-
409
-		try {
410
-			$sameFolder = ($sourceDir === $destinationDir);
411
-			// if we're overwriting or same folder
412
-			if ($targetNodeExists || $sameFolder) {
413
-				// note that renaming a share mount point is always allowed
414
-				if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) {
415
-					throw new \Sabre\DAV\Exception\Forbidden();
416
-				}
417
-			} else {
418
-				if (!$this->fileView->isCreatable($destinationDir)) {
419
-					throw new \Sabre\DAV\Exception\Forbidden();
420
-				}
421
-			}
422
-
423
-			if (!$sameFolder) {
424
-				// moving to a different folder, source will be gone, like a deletion
425
-				// note that moving a share mount point is always allowed
426
-				if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) {
427
-					throw new \Sabre\DAV\Exception\Forbidden();
428
-				}
429
-			}
430
-
431
-			$fileName = basename($destinationPath);
432
-			try {
433
-				$this->fileView->verifyPath($destinationDir, $fileName);
434
-			} catch (InvalidPathException $ex) {
435
-				throw new InvalidPath($ex->getMessage());
436
-			}
437
-
438
-			$renameOkay = $this->fileView->rename($sourcePath, $destinationPath);
439
-			if (!$renameOkay) {
440
-				throw new \Sabre\DAV\Exception\Forbidden('');
441
-			}
442
-		} catch (StorageNotAvailableException $e) {
443
-			throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
444
-		} catch (ForbiddenException $ex) {
445
-			throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
446
-		} catch (LockedException $e) {
447
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
448
-		}
449
-
450
-		return true;
451
-	}
452
-
453
-
454
-	public function copyInto($targetName, $sourcePath, INode $sourceNode) {
455
-		if ($sourceNode instanceof File || $sourceNode instanceof Directory) {
456
-			try {
457
-				$destinationPath = $this->getPath() . '/' . $targetName;
458
-				$sourcePath = $sourceNode->getPath();
459
-
460
-				if (!$this->fileView->isCreatable($this->getPath())) {
461
-					throw new \Sabre\DAV\Exception\Forbidden();
462
-				}
463
-
464
-				try {
465
-					$this->fileView->verifyPath($this->getPath(), $targetName);
466
-				} catch (InvalidPathException $ex) {
467
-					throw new InvalidPath($ex->getMessage());
468
-				}
469
-
470
-				$copyOkay = $this->fileView->copy($sourcePath, $destinationPath);
471
-
472
-				if (!$copyOkay) {
473
-					throw new \Sabre\DAV\Exception\Forbidden('Copy did not proceed');
474
-				}
475
-
476
-				return true;
477
-			} catch (StorageNotAvailableException $e) {
478
-				throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
479
-			} catch (ForbiddenException $ex) {
480
-				throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
481
-			} catch (LockedException $e) {
482
-				throw new FileLocked($e->getMessage(), $e->getCode(), $e);
483
-			}
484
-		}
485
-
486
-		return false;
487
-	}
488
-
489
-	public function getNode(): Folder {
490
-		return $this->node;
491
-	}
43
+    /**
44
+     * Cached directory content
45
+     * @var FileInfo[]
46
+     */
47
+    private ?array $dirContent = null;
48
+
49
+    /** Cached quota info */
50
+    private ?array $quotaInfo = null;
51
+
52
+    /**
53
+     * Sets up the node, expects a full path name
54
+     */
55
+    public function __construct(
56
+        View $view,
57
+        FileInfo $info,
58
+        private ?CachingTree $tree = null,
59
+        ?IShareManager $shareManager = null,
60
+    ) {
61
+        parent::__construct($view, $info, $shareManager);
62
+    }
63
+
64
+    /**
65
+     * Creates a new file in the directory
66
+     *
67
+     * Data will either be supplied as a stream resource, or in certain cases
68
+     * as a string. Keep in mind that you may have to support either.
69
+     *
70
+     * After successful creation of the file, you may choose to return the ETag
71
+     * of the new file here.
72
+     *
73
+     * The returned ETag must be surrounded by double-quotes (The quotes should
74
+     * be part of the actual string).
75
+     *
76
+     * If you cannot accurately determine the ETag, you should not return it.
77
+     * If you don't store the file exactly as-is (you're transforming it
78
+     * somehow) you should also not return an ETag.
79
+     *
80
+     * This means that if a subsequent GET to this new file does not exactly
81
+     * return the same contents of what was submitted here, you are strongly
82
+     * recommended to omit the ETag.
83
+     *
84
+     * @param string $name Name of the file
85
+     * @param resource|string $data Initial payload
86
+     * @return null|string
87
+     * @throws Exception\EntityTooLarge
88
+     * @throws Exception\UnsupportedMediaType
89
+     * @throws FileLocked
90
+     * @throws InvalidPath
91
+     * @throws \Sabre\DAV\Exception
92
+     * @throws \Sabre\DAV\Exception\BadRequest
93
+     * @throws \Sabre\DAV\Exception\Forbidden
94
+     * @throws \Sabre\DAV\Exception\ServiceUnavailable
95
+     */
96
+    public function createFile($name, $data = null) {
97
+        try {
98
+            if (!$this->fileView->isCreatable($this->path)) {
99
+                throw new \Sabre\DAV\Exception\Forbidden();
100
+            }
101
+
102
+            $this->fileView->verifyPath($this->path, $name);
103
+
104
+            $path = $this->fileView->getAbsolutePath($this->path) . '/' . $name;
105
+            // in case the file already exists/overwriting
106
+            $info = $this->fileView->getFileInfo($this->path . '/' . $name);
107
+            if (!$info) {
108
+                // use a dummy FileInfo which is acceptable here since it will be refreshed after the put is complete
109
+                $info = new \OC\Files\FileInfo($path, null, null, [
110
+                    'type' => FileInfo::TYPE_FILE
111
+                ], null);
112
+            }
113
+            $node = new File($this->fileView, $info);
114
+
115
+            // only allow 1 process to upload a file at once but still allow reading the file while writing the part file
116
+            $node->acquireLock(ILockingProvider::LOCK_SHARED);
117
+            $this->fileView->lockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
118
+
119
+            $result = $node->put($data);
120
+
121
+            $this->fileView->unlockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
122
+            $node->releaseLock(ILockingProvider::LOCK_SHARED);
123
+            return $result;
124
+        } catch (StorageNotAvailableException $e) {
125
+            throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
126
+        } catch (InvalidPathException $ex) {
127
+            throw new InvalidPath($ex->getMessage(), false, $ex);
128
+        } catch (ForbiddenException $ex) {
129
+            throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
130
+        } catch (LockedException $e) {
131
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
132
+        }
133
+    }
134
+
135
+    /**
136
+     * Creates a new subdirectory
137
+     *
138
+     * @param string $name
139
+     * @throws FileLocked
140
+     * @throws InvalidPath
141
+     * @throws \Sabre\DAV\Exception\Forbidden
142
+     * @throws \Sabre\DAV\Exception\ServiceUnavailable
143
+     */
144
+    public function createDirectory($name) {
145
+        try {
146
+            if (!$this->info->isCreatable()) {
147
+                throw new \Sabre\DAV\Exception\Forbidden();
148
+            }
149
+
150
+            $this->fileView->verifyPath($this->path, $name);
151
+            $newPath = $this->path . '/' . $name;
152
+            if (!$this->fileView->mkdir($newPath)) {
153
+                throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
154
+            }
155
+        } catch (StorageNotAvailableException $e) {
156
+            throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
157
+        } catch (InvalidPathException $ex) {
158
+            throw new InvalidPath($ex->getMessage(), false, $ex);
159
+        } catch (ForbiddenException $ex) {
160
+            throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
161
+        } catch (LockedException $e) {
162
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
163
+        }
164
+    }
165
+
166
+    /**
167
+     * Returns a specific child node, referenced by its name
168
+     *
169
+     * @param string $name
170
+     * @param FileInfo $info
171
+     * @return \Sabre\DAV\INode
172
+     * @throws InvalidPath
173
+     * @throws \Sabre\DAV\Exception\NotFound
174
+     * @throws \Sabre\DAV\Exception\ServiceUnavailable
175
+     */
176
+    public function getChild($name, $info = null, ?IRequest $request = null, ?IL10N $l10n = null) {
177
+        $storage = $this->info->getStorage();
178
+        $allowDirectory = false;
179
+        if ($storage instanceof PublicShareWrapper) {
180
+            $share = $storage->getShare();
181
+            $allowDirectory =
182
+                // Only allow directories for file drops
183
+                ($share->getPermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ &&
184
+                // And only allow it for directories which are a direct child of the share root
185
+                $this->info->getId() === $share->getNodeId();
186
+        }
187
+
188
+        // For file drop we need to be allowed to read the directory with the nickname
189
+        if (!$allowDirectory && !$this->info->isReadable()) {
190
+            // avoid detecting files through this way
191
+            throw new NotFound();
192
+        }
193
+
194
+        $path = $this->path . '/' . $name;
195
+        if (is_null($info)) {
196
+            try {
197
+                $this->fileView->verifyPath($this->path, $name, true);
198
+                $info = $this->fileView->getFileInfo($path);
199
+            } catch (StorageNotAvailableException $e) {
200
+                throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
201
+            } catch (InvalidPathException $ex) {
202
+                throw new InvalidPath($ex->getMessage(), false, $ex);
203
+            } catch (ForbiddenException $e) {
204
+                throw new \Sabre\DAV\Exception\Forbidden($e->getMessage(), $e->getCode(), $e);
205
+            }
206
+        }
207
+
208
+        if (!$info) {
209
+            throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
210
+        }
211
+
212
+        if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
213
+            $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
214
+        } else {
215
+            // In case reading a directory was allowed but it turns out the node was a not a directory, reject it now.
216
+            if (!$this->info->isReadable()) {
217
+                throw new NotFound();
218
+            }
219
+
220
+            $node = new File($this->fileView, $info, $this->shareManager, $request, $l10n);
221
+        }
222
+        if ($this->tree) {
223
+            $this->tree->cacheNode($node);
224
+        }
225
+        return $node;
226
+    }
227
+
228
+    /**
229
+     * Returns an array with all the child nodes
230
+     *
231
+     * @return \Sabre\DAV\INode[]
232
+     * @throws \Sabre\DAV\Exception\Locked
233
+     * @throws Forbidden
234
+     */
235
+    public function getChildren() {
236
+        if (!is_null($this->dirContent)) {
237
+            return $this->dirContent;
238
+        }
239
+        try {
240
+            if (!$this->info->isReadable()) {
241
+                // return 403 instead of 404 because a 404 would make
242
+                // the caller believe that the collection itself does not exist
243
+                if (Server::get(IAppManager::class)->isEnabledForAnyone('files_accesscontrol')) {
244
+                    throw new Forbidden('No read permissions. This might be caused by files_accesscontrol, check your configured rules');
245
+                } else {
246
+                    throw new Forbidden('No read permissions');
247
+                }
248
+            }
249
+            $folderContent = $this->getNode()->getDirectoryListing();
250
+        } catch (LockedException $e) {
251
+            throw new Locked();
252
+        }
253
+
254
+        $nodes = [];
255
+        $request = Server::get(IRequest::class);
256
+        $l10nFactory = Server::get(IFactory::class);
257
+        $l10n = $l10nFactory->get(Application::APP_ID);
258
+        foreach ($folderContent as $info) {
259
+            $node = $this->getChild($info->getName(), $info, $request, $l10n);
260
+            $nodes[] = $node;
261
+        }
262
+        $this->dirContent = $nodes;
263
+        return $this->dirContent;
264
+    }
265
+
266
+    /**
267
+     * Checks if a child exists.
268
+     *
269
+     * @param string $name
270
+     * @return bool
271
+     */
272
+    public function childExists($name) {
273
+        // note: here we do NOT resolve the chunk file name to the real file name
274
+        // to make sure we return false when checking for file existence with a chunk
275
+        // file name.
276
+        // This is to make sure that "createFile" is still triggered
277
+        // (required old code) instead of "updateFile".
278
+        //
279
+        // TODO: resolve chunk file name here and implement "updateFile"
280
+        $path = $this->path . '/' . $name;
281
+        return $this->fileView->file_exists($path);
282
+    }
283
+
284
+    /**
285
+     * Deletes all files in this directory, and then itself
286
+     *
287
+     * @return void
288
+     * @throws FileLocked
289
+     * @throws \Sabre\DAV\Exception\Forbidden
290
+     */
291
+    public function delete() {
292
+        if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) {
293
+            throw new \Sabre\DAV\Exception\Forbidden();
294
+        }
295
+
296
+        try {
297
+            if (!$this->fileView->rmdir($this->path)) {
298
+                // assume it wasn't possible to remove due to permission issue
299
+                throw new \Sabre\DAV\Exception\Forbidden();
300
+            }
301
+        } catch (ForbiddenException $ex) {
302
+            throw new Forbidden($ex->getMessage(), $ex->getRetry());
303
+        } catch (LockedException $e) {
304
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
305
+        }
306
+    }
307
+
308
+    private function getLogger(): LoggerInterface {
309
+        return Server::get(LoggerInterface::class);
310
+    }
311
+
312
+    /**
313
+     * Returns available diskspace information
314
+     *
315
+     * @return array
316
+     */
317
+    public function getQuotaInfo() {
318
+        if ($this->quotaInfo) {
319
+            return $this->quotaInfo;
320
+        }
321
+        $relativePath = $this->fileView->getRelativePath($this->info->getPath());
322
+        if ($relativePath === null) {
323
+            $this->getLogger()->warning('error while getting quota as the relative path cannot be found');
324
+            return [0, 0];
325
+        }
326
+
327
+        try {
328
+            $storageInfo = \OC_Helper::getStorageInfo($relativePath, $this->info, false);
329
+            if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) {
330
+                $free = FileInfo::SPACE_UNLIMITED;
331
+            } else {
332
+                $free = $storageInfo['free'];
333
+            }
334
+            $this->quotaInfo = [
335
+                $storageInfo['used'],
336
+                $free
337
+            ];
338
+            return $this->quotaInfo;
339
+        } catch (NotFoundException $e) {
340
+            $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
341
+            return [0, 0];
342
+        } catch (StorageNotAvailableException $e) {
343
+            $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
344
+            return [0, 0];
345
+        } catch (NotPermittedException $e) {
346
+            $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
347
+            return [0, 0];
348
+        }
349
+    }
350
+
351
+    /**
352
+     * Moves a node into this collection.
353
+     *
354
+     * It is up to the implementors to:
355
+     *   1. Create the new resource.
356
+     *   2. Remove the old resource.
357
+     *   3. Transfer any properties or other data.
358
+     *
359
+     * Generally you should make very sure that your collection can easily move
360
+     * the move.
361
+     *
362
+     * If you don't, just return false, which will trigger sabre/dav to handle
363
+     * the move itself. If you return true from this function, the assumption
364
+     * is that the move was successful.
365
+     *
366
+     * @param string $targetName New local file/collection name.
367
+     * @param string $fullSourcePath Full path to source node
368
+     * @param INode $sourceNode Source node itself
369
+     * @return bool
370
+     * @throws BadRequest
371
+     * @throws ServiceUnavailable
372
+     * @throws Forbidden
373
+     * @throws FileLocked
374
+     * @throws \Sabre\DAV\Exception\Forbidden
375
+     */
376
+    public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
377
+        if (!$sourceNode instanceof Node) {
378
+            // it's a file of another kind, like FutureFile
379
+            if ($sourceNode instanceof IFile) {
380
+                // fallback to default copy+delete handling
381
+                return false;
382
+            }
383
+            throw new BadRequest('Incompatible node types');
384
+        }
385
+
386
+        $destinationPath = $this->getPath() . '/' . $targetName;
387
+
388
+
389
+        $targetNodeExists = $this->childExists($targetName);
390
+
391
+        // at getNodeForPath we also check the path for isForbiddenFileOrDir
392
+        // with that we have covered both source and destination
393
+        if ($sourceNode instanceof Directory && $targetNodeExists) {
394
+            throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
395
+        }
396
+
397
+        [$sourceDir,] = \Sabre\Uri\split($sourceNode->getPath());
398
+        $destinationDir = $this->getPath();
399
+
400
+        $sourcePath = $sourceNode->getPath();
401
+
402
+        $isMovableMount = false;
403
+        $sourceMount = Server::get(IMountManager::class)->find($this->fileView->getAbsolutePath($sourcePath));
404
+        $internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath));
405
+        if ($sourceMount instanceof MoveableMount && $internalPath === '') {
406
+            $isMovableMount = true;
407
+        }
408
+
409
+        try {
410
+            $sameFolder = ($sourceDir === $destinationDir);
411
+            // if we're overwriting or same folder
412
+            if ($targetNodeExists || $sameFolder) {
413
+                // note that renaming a share mount point is always allowed
414
+                if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) {
415
+                    throw new \Sabre\DAV\Exception\Forbidden();
416
+                }
417
+            } else {
418
+                if (!$this->fileView->isCreatable($destinationDir)) {
419
+                    throw new \Sabre\DAV\Exception\Forbidden();
420
+                }
421
+            }
422
+
423
+            if (!$sameFolder) {
424
+                // moving to a different folder, source will be gone, like a deletion
425
+                // note that moving a share mount point is always allowed
426
+                if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) {
427
+                    throw new \Sabre\DAV\Exception\Forbidden();
428
+                }
429
+            }
430
+
431
+            $fileName = basename($destinationPath);
432
+            try {
433
+                $this->fileView->verifyPath($destinationDir, $fileName);
434
+            } catch (InvalidPathException $ex) {
435
+                throw new InvalidPath($ex->getMessage());
436
+            }
437
+
438
+            $renameOkay = $this->fileView->rename($sourcePath, $destinationPath);
439
+            if (!$renameOkay) {
440
+                throw new \Sabre\DAV\Exception\Forbidden('');
441
+            }
442
+        } catch (StorageNotAvailableException $e) {
443
+            throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
444
+        } catch (ForbiddenException $ex) {
445
+            throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
446
+        } catch (LockedException $e) {
447
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
448
+        }
449
+
450
+        return true;
451
+    }
452
+
453
+
454
+    public function copyInto($targetName, $sourcePath, INode $sourceNode) {
455
+        if ($sourceNode instanceof File || $sourceNode instanceof Directory) {
456
+            try {
457
+                $destinationPath = $this->getPath() . '/' . $targetName;
458
+                $sourcePath = $sourceNode->getPath();
459
+
460
+                if (!$this->fileView->isCreatable($this->getPath())) {
461
+                    throw new \Sabre\DAV\Exception\Forbidden();
462
+                }
463
+
464
+                try {
465
+                    $this->fileView->verifyPath($this->getPath(), $targetName);
466
+                } catch (InvalidPathException $ex) {
467
+                    throw new InvalidPath($ex->getMessage());
468
+                }
469
+
470
+                $copyOkay = $this->fileView->copy($sourcePath, $destinationPath);
471
+
472
+                if (!$copyOkay) {
473
+                    throw new \Sabre\DAV\Exception\Forbidden('Copy did not proceed');
474
+                }
475
+
476
+                return true;
477
+            } catch (StorageNotAvailableException $e) {
478
+                throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
479
+            } catch (ForbiddenException $ex) {
480
+                throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
481
+            } catch (LockedException $e) {
482
+                throw new FileLocked($e->getMessage(), $e->getCode(), $e);
483
+            }
484
+        }
485
+
486
+        return false;
487
+    }
488
+
489
+    public function getNode(): Folder {
490
+        return $this->node;
491
+    }
492 492
 }
Please login to merge, or discard this patch.
apps/dav/lib/Files/Sharing/FilesDropPlugin.php 1 patch
Indentation   +71 added lines, -71 removed lines patch added patch discarded remove patch
@@ -17,76 +17,76 @@
 block discarded – undo
17 17
  */
18 18
 class FilesDropPlugin extends ServerPlugin {
19 19
 
20
-	private ?View $view = null;
21
-	private ?IShare $share = null;
22
-	private bool $enabled = false;
23
-
24
-	public function setView(View $view): void {
25
-		$this->view = $view;
26
-	}
27
-
28
-	public function setShare(IShare $share): void {
29
-		$this->share = $share;
30
-	}
31
-
32
-	public function enable(): void {
33
-		$this->enabled = true;
34
-	}
35
-
36
-
37
-	/**
38
-	 * This initializes the plugin.
39
-	 *
40
-	 * @param \Sabre\DAV\Server $server Sabre server
41
-	 *
42
-	 * @return void
43
-	 * @throws MethodNotAllowed
44
-	 */
45
-	public function initialize(\Sabre\DAV\Server $server): void {
46
-		$server->on('beforeMethod:*', [$this, 'beforeMethod'], 999);
47
-		$this->enabled = false;
48
-	}
49
-
50
-	public function beforeMethod(RequestInterface $request, ResponseInterface $response): void {
51
-		if (!$this->enabled || $this->share === null || $this->view === null) {
52
-			return;
53
-		}
54
-
55
-		// Only allow file drop
56
-		if ($request->getMethod() !== 'PUT') {
57
-			throw new MethodNotAllowed('Only PUT is allowed on files drop');
58
-		}
59
-
60
-		// Always upload at the root level
61
-		$path = explode('/', $request->getPath());
62
-		$path = array_pop($path);
63
-
64
-		// Extract the attributes for the file request
65
-		$isFileRequest = false;
66
-		$attributes = $this->share->getAttributes();
67
-		$nickName = $request->hasHeader('X-NC-Nickname') ? urldecode($request->getHeader('X-NC-Nickname')) : null;
68
-		if ($attributes !== null) {
69
-			$isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true;
70
-		}
71
-
72
-		// We need a valid nickname for file requests
73
-		if ($isFileRequest && ($nickName == null || trim($nickName) === '')) {
74
-			throw new MethodNotAllowed('Nickname is required for file requests');
75
-		}
76
-
77
-		// If this is a file request we need to create a folder for the user
78
-		if ($isFileRequest) {
79
-			// Check if the folder already exists
80
-			if (!($this->view->file_exists($nickName) === true)) {
81
-				$this->view->mkdir($nickName);
82
-			}
83
-			// Put all files in the subfolder
84
-			$path = $nickName . '/' . $path;
85
-		}
86
-
87
-		$newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view);
88
-		$url = $request->getBaseUrl() . '/files/' . $this->share->getToken() . $newName;
89
-		$request->setUrl($url);
90
-	}
20
+    private ?View $view = null;
21
+    private ?IShare $share = null;
22
+    private bool $enabled = false;
23
+
24
+    public function setView(View $view): void {
25
+        $this->view = $view;
26
+    }
27
+
28
+    public function setShare(IShare $share): void {
29
+        $this->share = $share;
30
+    }
31
+
32
+    public function enable(): void {
33
+        $this->enabled = true;
34
+    }
35
+
36
+
37
+    /**
38
+     * This initializes the plugin.
39
+     *
40
+     * @param \Sabre\DAV\Server $server Sabre server
41
+     *
42
+     * @return void
43
+     * @throws MethodNotAllowed
44
+     */
45
+    public function initialize(\Sabre\DAV\Server $server): void {
46
+        $server->on('beforeMethod:*', [$this, 'beforeMethod'], 999);
47
+        $this->enabled = false;
48
+    }
49
+
50
+    public function beforeMethod(RequestInterface $request, ResponseInterface $response): void {
51
+        if (!$this->enabled || $this->share === null || $this->view === null) {
52
+            return;
53
+        }
54
+
55
+        // Only allow file drop
56
+        if ($request->getMethod() !== 'PUT') {
57
+            throw new MethodNotAllowed('Only PUT is allowed on files drop');
58
+        }
59
+
60
+        // Always upload at the root level
61
+        $path = explode('/', $request->getPath());
62
+        $path = array_pop($path);
63
+
64
+        // Extract the attributes for the file request
65
+        $isFileRequest = false;
66
+        $attributes = $this->share->getAttributes();
67
+        $nickName = $request->hasHeader('X-NC-Nickname') ? urldecode($request->getHeader('X-NC-Nickname')) : null;
68
+        if ($attributes !== null) {
69
+            $isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true;
70
+        }
71
+
72
+        // We need a valid nickname for file requests
73
+        if ($isFileRequest && ($nickName == null || trim($nickName) === '')) {
74
+            throw new MethodNotAllowed('Nickname is required for file requests');
75
+        }
76
+
77
+        // If this is a file request we need to create a folder for the user
78
+        if ($isFileRequest) {
79
+            // Check if the folder already exists
80
+            if (!($this->view->file_exists($nickName) === true)) {
81
+                $this->view->mkdir($nickName);
82
+            }
83
+            // Put all files in the subfolder
84
+            $path = $nickName . '/' . $path;
85
+        }
86
+
87
+        $newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view);
88
+        $url = $request->getBaseUrl() . '/files/' . $this->share->getToken() . $newName;
89
+        $request->setUrl($url);
90
+    }
91 91
 
92 92
 }
Please login to merge, or discard this patch.
apps/dav/lib/Files/Sharing/RootCollection.php 1 patch
Indentation   +13 added lines, -13 removed lines patch added patch discarded remove patch
@@ -14,19 +14,19 @@
 block discarded – undo
14 14
 use Sabre\DAVACL\PrincipalBackend\BackendInterface;
15 15
 
16 16
 class RootCollection extends AbstractPrincipalCollection {
17
-	public function __construct(
18
-		private INode $root,
19
-		BackendInterface $principalBackend,
20
-		string $principalPrefix = 'principals',
21
-	) {
22
-		parent::__construct($principalBackend, $principalPrefix);
23
-	}
17
+    public function __construct(
18
+        private INode $root,
19
+        BackendInterface $principalBackend,
20
+        string $principalPrefix = 'principals',
21
+    ) {
22
+        parent::__construct($principalBackend, $principalPrefix);
23
+    }
24 24
 
25
-	public function getChildForPrincipal(array $principalInfo): INode {
26
-		return $this->root;
27
-	}
25
+    public function getChildForPrincipal(array $principalInfo): INode {
26
+        return $this->root;
27
+    }
28 28
 
29
-	public function getName() {
30
-		return 'files';
31
-	}
29
+    public function getName() {
30
+        return 'files';
31
+    }
32 32
 }
Please login to merge, or discard this patch.
apps/dav/lib/Upload/UploadHome.php 1 patch
Indentation   +100 added lines, -100 removed lines patch added patch discarded remove patch
@@ -17,104 +17,104 @@
 block discarded – undo
17 17
 use Sabre\DAV\ICollection;
18 18
 
19 19
 class UploadHome implements ICollection {
20
-	private string $uid;
21
-	private ?Folder $uploadFolder = null;
22
-
23
-	public function __construct(
24
-		private readonly array $principalInfo,
25
-		private readonly CleanupService $cleanupService,
26
-		private readonly IRootFolder $rootFolder,
27
-		private readonly IUserSession $userSession,
28
-		private readonly \OCP\Share\IManager $shareManager,
29
-	) {
30
-		[$prefix, $name] = \Sabre\Uri\split($principalInfo['uri']);
31
-		if ($prefix === 'principals/shares') {
32
-			$this->uid = $this->shareManager->getShareByToken($name)->getShareOwner();
33
-		} else {
34
-			$user = $this->userSession->getUser();
35
-			if (!$user) {
36
-				throw new Forbidden('Not logged in');
37
-			}
38
-
39
-			$this->uid = $user->getUID();
40
-		}
41
-	}
42
-
43
-	public function createFile($name, $data = null) {
44
-		throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
45
-	}
46
-
47
-	public function createDirectory($name) {
48
-		$this->impl()->createDirectory($name);
49
-
50
-		// Add a cleanup job
51
-		$this->cleanupService->addJob($this->uid, $name);
52
-	}
53
-
54
-	public function getChild($name): UploadFolder {
55
-		return new UploadFolder(
56
-			$this->impl()->getChild($name),
57
-			$this->cleanupService,
58
-			$this->getStorage(),
59
-			$this->uid,
60
-		);
61
-	}
62
-
63
-	public function getChildren(): array {
64
-		return array_map(function ($node) {
65
-			return new UploadFolder(
66
-				$node,
67
-				$this->cleanupService,
68
-				$this->getStorage(),
69
-				$this->uid,
70
-			);
71
-		}, $this->impl()->getChildren());
72
-	}
73
-
74
-	public function childExists($name): bool {
75
-		return !is_null($this->getChild($name));
76
-	}
77
-
78
-	public function delete() {
79
-		$this->impl()->delete();
80
-	}
81
-
82
-	public function getName() {
83
-		[,$name] = \Sabre\Uri\split($this->principalInfo['uri']);
84
-		return $name;
85
-	}
86
-
87
-	public function setName($name) {
88
-		throw new Forbidden('Permission denied to rename this folder');
89
-	}
90
-
91
-	public function getLastModified() {
92
-		return $this->impl()->getLastModified();
93
-	}
94
-
95
-	private function getUploadFolder(): Folder {
96
-		if ($this->uploadFolder === null) {
97
-			$path = '/' . $this->uid . '/uploads';
98
-			try {
99
-				$folder = $this->rootFolder->get($path);
100
-				if (!$folder instanceof Folder) {
101
-					throw new \Exception('Upload folder is a file');
102
-				}
103
-				$this->uploadFolder = $folder;
104
-			} catch (NotFoundException $e) {
105
-				$this->uploadFolder = $this->rootFolder->newFolder($path);
106
-			}
107
-		}
108
-		return $this->uploadFolder;
109
-	}
110
-
111
-	private function impl(): Directory {
112
-		$folder = $this->getUploadFolder();
113
-		$view = new View($folder->getPath());
114
-		return new Directory($view, $folder);
115
-	}
116
-
117
-	private function getStorage() {
118
-		return $this->getUploadFolder()->getStorage();
119
-	}
20
+    private string $uid;
21
+    private ?Folder $uploadFolder = null;
22
+
23
+    public function __construct(
24
+        private readonly array $principalInfo,
25
+        private readonly CleanupService $cleanupService,
26
+        private readonly IRootFolder $rootFolder,
27
+        private readonly IUserSession $userSession,
28
+        private readonly \OCP\Share\IManager $shareManager,
29
+    ) {
30
+        [$prefix, $name] = \Sabre\Uri\split($principalInfo['uri']);
31
+        if ($prefix === 'principals/shares') {
32
+            $this->uid = $this->shareManager->getShareByToken($name)->getShareOwner();
33
+        } else {
34
+            $user = $this->userSession->getUser();
35
+            if (!$user) {
36
+                throw new Forbidden('Not logged in');
37
+            }
38
+
39
+            $this->uid = $user->getUID();
40
+        }
41
+    }
42
+
43
+    public function createFile($name, $data = null) {
44
+        throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
45
+    }
46
+
47
+    public function createDirectory($name) {
48
+        $this->impl()->createDirectory($name);
49
+
50
+        // Add a cleanup job
51
+        $this->cleanupService->addJob($this->uid, $name);
52
+    }
53
+
54
+    public function getChild($name): UploadFolder {
55
+        return new UploadFolder(
56
+            $this->impl()->getChild($name),
57
+            $this->cleanupService,
58
+            $this->getStorage(),
59
+            $this->uid,
60
+        );
61
+    }
62
+
63
+    public function getChildren(): array {
64
+        return array_map(function ($node) {
65
+            return new UploadFolder(
66
+                $node,
67
+                $this->cleanupService,
68
+                $this->getStorage(),
69
+                $this->uid,
70
+            );
71
+        }, $this->impl()->getChildren());
72
+    }
73
+
74
+    public function childExists($name): bool {
75
+        return !is_null($this->getChild($name));
76
+    }
77
+
78
+    public function delete() {
79
+        $this->impl()->delete();
80
+    }
81
+
82
+    public function getName() {
83
+        [,$name] = \Sabre\Uri\split($this->principalInfo['uri']);
84
+        return $name;
85
+    }
86
+
87
+    public function setName($name) {
88
+        throw new Forbidden('Permission denied to rename this folder');
89
+    }
90
+
91
+    public function getLastModified() {
92
+        return $this->impl()->getLastModified();
93
+    }
94
+
95
+    private function getUploadFolder(): Folder {
96
+        if ($this->uploadFolder === null) {
97
+            $path = '/' . $this->uid . '/uploads';
98
+            try {
99
+                $folder = $this->rootFolder->get($path);
100
+                if (!$folder instanceof Folder) {
101
+                    throw new \Exception('Upload folder is a file');
102
+                }
103
+                $this->uploadFolder = $folder;
104
+            } catch (NotFoundException $e) {
105
+                $this->uploadFolder = $this->rootFolder->newFolder($path);
106
+            }
107
+        }
108
+        return $this->uploadFolder;
109
+    }
110
+
111
+    private function impl(): Directory {
112
+        $folder = $this->getUploadFolder();
113
+        $view = new View($folder->getPath());
114
+        return new Directory($view, $folder);
115
+    }
116
+
117
+    private function getStorage() {
118
+        return $this->getUploadFolder()->getStorage();
119
+    }
120 120
 }
Please login to merge, or discard this patch.
apps/dav/lib/Upload/UploadFolder.php 1 patch
Indentation   +91 added lines, -91 removed lines patch added patch discarded remove patch
@@ -17,95 +17,95 @@
 block discarded – undo
17 17
 use Sabre\DAV\ICollection;
18 18
 
19 19
 class UploadFolder implements ICollection {
20
-	public function __construct(
21
-		private Directory $node,
22
-		private CleanupService $cleanupService,
23
-		private IStorage $storage,
24
-		private string $uid,
25
-	) {
26
-	}
27
-
28
-	public function createFile($name, $data = null) {
29
-		// TODO: verify name - should be a simple number
30
-		try {
31
-			$this->node->createFile($name, $data);
32
-		} catch (\Exception $e) {
33
-			if ($this->node->childExists($name)) {
34
-				$child = $this->node->getChild($name);
35
-				$child->delete();
36
-			}
37
-			throw $e;
38
-		}
39
-	}
40
-
41
-	public function createDirectory($name) {
42
-		throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
43
-	}
44
-
45
-	public function getChild($name) {
46
-		if ($name === '.file') {
47
-			return new FutureFile($this->node, '.file');
48
-		}
49
-		return new UploadFile($this->node->getChild($name));
50
-	}
51
-
52
-	public function getChildren() {
53
-		$tmpChildren = $this->node->getChildren();
54
-
55
-		$children = [];
56
-		$children[] = new FutureFile($this->node, '.file');
57
-
58
-		foreach ($tmpChildren as $child) {
59
-			$children[] = new UploadFile($child);
60
-		}
61
-
62
-		if ($this->storage->instanceOfStorage(ObjectStoreStorage::class)) {
63
-			/** @var ObjectStoreStorage $storage */
64
-			$objectStore = $this->storage->getObjectStore();
65
-			if ($objectStore instanceof IObjectStoreMultiPartUpload) {
66
-				$cache = Server::get(ICacheFactory::class)->createDistributed(ChunkingV2Plugin::CACHE_KEY);
67
-				$uploadSession = $cache->get($this->getName());
68
-				if ($uploadSession) {
69
-					$uploadId = $uploadSession[ChunkingV2Plugin::UPLOAD_ID];
70
-					$id = $uploadSession[ChunkingV2Plugin::UPLOAD_TARGET_ID];
71
-					$parts = $objectStore->getMultipartUploads($this->storage->getURN($id), $uploadId);
72
-					foreach ($parts as $part) {
73
-						$children[] = new PartFile($this->node, $part);
74
-					}
75
-				}
76
-			}
77
-		}
78
-
79
-		return $children;
80
-	}
81
-
82
-	public function childExists($name) {
83
-		if ($name === '.file') {
84
-			return true;
85
-		}
86
-		return $this->node->childExists($name);
87
-	}
88
-
89
-	public function delete() {
90
-		$this->node->delete();
91
-
92
-		// Background cleanup job is not needed anymore
93
-		$this->cleanupService->removeJob($this->uid, $this->getName());
94
-	}
95
-
96
-	public function getName() {
97
-		return $this->node->getName();
98
-	}
99
-
100
-	public function setName($name) {
101
-		throw new Forbidden('Permission denied to rename this folder');
102
-	}
103
-
104
-	public function getLastModified() {
105
-		return $this->node->getLastModified();
106
-	}
107
-
108
-	public function getStorage() {
109
-		return $this->storage;
110
-	}
20
+    public function __construct(
21
+        private Directory $node,
22
+        private CleanupService $cleanupService,
23
+        private IStorage $storage,
24
+        private string $uid,
25
+    ) {
26
+    }
27
+
28
+    public function createFile($name, $data = null) {
29
+        // TODO: verify name - should be a simple number
30
+        try {
31
+            $this->node->createFile($name, $data);
32
+        } catch (\Exception $e) {
33
+            if ($this->node->childExists($name)) {
34
+                $child = $this->node->getChild($name);
35
+                $child->delete();
36
+            }
37
+            throw $e;
38
+        }
39
+    }
40
+
41
+    public function createDirectory($name) {
42
+        throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
43
+    }
44
+
45
+    public function getChild($name) {
46
+        if ($name === '.file') {
47
+            return new FutureFile($this->node, '.file');
48
+        }
49
+        return new UploadFile($this->node->getChild($name));
50
+    }
51
+
52
+    public function getChildren() {
53
+        $tmpChildren = $this->node->getChildren();
54
+
55
+        $children = [];
56
+        $children[] = new FutureFile($this->node, '.file');
57
+
58
+        foreach ($tmpChildren as $child) {
59
+            $children[] = new UploadFile($child);
60
+        }
61
+
62
+        if ($this->storage->instanceOfStorage(ObjectStoreStorage::class)) {
63
+            /** @var ObjectStoreStorage $storage */
64
+            $objectStore = $this->storage->getObjectStore();
65
+            if ($objectStore instanceof IObjectStoreMultiPartUpload) {
66
+                $cache = Server::get(ICacheFactory::class)->createDistributed(ChunkingV2Plugin::CACHE_KEY);
67
+                $uploadSession = $cache->get($this->getName());
68
+                if ($uploadSession) {
69
+                    $uploadId = $uploadSession[ChunkingV2Plugin::UPLOAD_ID];
70
+                    $id = $uploadSession[ChunkingV2Plugin::UPLOAD_TARGET_ID];
71
+                    $parts = $objectStore->getMultipartUploads($this->storage->getURN($id), $uploadId);
72
+                    foreach ($parts as $part) {
73
+                        $children[] = new PartFile($this->node, $part);
74
+                    }
75
+                }
76
+            }
77
+        }
78
+
79
+        return $children;
80
+    }
81
+
82
+    public function childExists($name) {
83
+        if ($name === '.file') {
84
+            return true;
85
+        }
86
+        return $this->node->childExists($name);
87
+    }
88
+
89
+    public function delete() {
90
+        $this->node->delete();
91
+
92
+        // Background cleanup job is not needed anymore
93
+        $this->cleanupService->removeJob($this->uid, $this->getName());
94
+    }
95
+
96
+    public function getName() {
97
+        return $this->node->getName();
98
+    }
99
+
100
+    public function setName($name) {
101
+        throw new Forbidden('Permission denied to rename this folder');
102
+    }
103
+
104
+    public function getLastModified() {
105
+        return $this->node->getLastModified();
106
+    }
107
+
108
+    public function getStorage() {
109
+        return $this->storage;
110
+    }
111 111
 }
Please login to merge, or discard this patch.
apps/dav/lib/Upload/RootCollection.php 1 patch
Indentation   +28 added lines, -28 removed lines patch added patch discarded remove patch
@@ -17,34 +17,34 @@
 block discarded – undo
17 17
 
18 18
 class RootCollection extends AbstractPrincipalCollection {
19 19
 
20
-	public function __construct(
21
-		PrincipalBackend\BackendInterface $principalBackend,
22
-		string $principalPrefix,
23
-		private CleanupService $cleanupService,
24
-		private IRootFolder $rootFolder,
25
-		private IUserSession $userSession,
26
-		private IManager $shareManager,
27
-	) {
28
-		parent::__construct($principalBackend, $principalPrefix);
29
-	}
20
+    public function __construct(
21
+        PrincipalBackend\BackendInterface $principalBackend,
22
+        string $principalPrefix,
23
+        private CleanupService $cleanupService,
24
+        private IRootFolder $rootFolder,
25
+        private IUserSession $userSession,
26
+        private IManager $shareManager,
27
+    ) {
28
+        parent::__construct($principalBackend, $principalPrefix);
29
+    }
30 30
 
31
-	/**
32
-	 * @inheritdoc
33
-	 */
34
-	public function getChildForPrincipal(array $principalInfo): UploadHome {
35
-		return new UploadHome(
36
-			$principalInfo,
37
-			$this->cleanupService,
38
-			$this->rootFolder,
39
-			$this->userSession,
40
-			$this->shareManager,
41
-		);
42
-	}
31
+    /**
32
+     * @inheritdoc
33
+     */
34
+    public function getChildForPrincipal(array $principalInfo): UploadHome {
35
+        return new UploadHome(
36
+            $principalInfo,
37
+            $this->cleanupService,
38
+            $this->rootFolder,
39
+            $this->userSession,
40
+            $this->shareManager,
41
+        );
42
+    }
43 43
 
44
-	/**
45
-	 * @inheritdoc
46
-	 */
47
-	public function getName(): string {
48
-		return 'uploads';
49
-	}
44
+    /**
45
+     * @inheritdoc
46
+     */
47
+    public function getName(): string {
48
+        return 'uploads';
49
+    }
50 50
 }
Please login to merge, or discard this patch.
apps/dav/lib/Upload/CleanupService.php 1 patch
Indentation   +10 added lines, -10 removed lines patch added patch discarded remove patch
@@ -12,16 +12,16 @@
 block discarded – undo
12 12
 use OCP\BackgroundJob\IJobList;
13 13
 
14 14
 class CleanupService {
15
-	public function __construct(
16
-		private IJobList $jobList,
17
-	) {
18
-	}
15
+    public function __construct(
16
+        private IJobList $jobList,
17
+    ) {
18
+    }
19 19
 
20
-	public function addJob(string $uid, string $folder) {
21
-		$this->jobList->add(UploadCleanup::class, ['uid' => $uid, 'folder' => $folder]);
22
-	}
20
+    public function addJob(string $uid, string $folder) {
21
+        $this->jobList->add(UploadCleanup::class, ['uid' => $uid, 'folder' => $folder]);
22
+    }
23 23
 
24
-	public function removeJob(string $uid, string $folder) {
25
-		$this->jobList->remove(UploadCleanup::class, ['uid' => $uid, 'folder' => $folder]);
26
-	}
24
+    public function removeJob(string $uid, string $folder) {
25
+        $this->jobList->remove(UploadCleanup::class, ['uid' => $uid, 'folder' => $folder]);
26
+    }
27 27
 }
Please login to merge, or discard this patch.