Completed
Push — master ( 752d90...32327c )
by
unknown
29:21
created
lib/public/Share/IShare.php 1 patch
Indentation   +633 added lines, -633 removed lines patch added patch discarded remove patch
@@ -22,637 +22,637 @@
 block discarded – undo
22 22
  * @since 9.0.0
23 23
  */
24 24
 interface IShare {
25
-	/**
26
-	 * @since 17.0.0
27
-	 */
28
-	public const TYPE_USER = 0;
29
-
30
-	/**
31
-	 * @since 17.0.0
32
-	 */
33
-	public const TYPE_GROUP = 1;
34
-
35
-	/**
36
-	 * @internal
37
-	 * @since 18.0.0
38
-	 */
39
-	public const TYPE_USERGROUP = 2;
40
-
41
-	/**
42
-	 * @since 17.0.0
43
-	 */
44
-	public const TYPE_LINK = 3;
45
-
46
-	/**
47
-	 * @since 17.0.0
48
-	 */
49
-	public const TYPE_EMAIL = 4;
50
-
51
-	/**
52
-	 * ToDo Check if it is still in use otherwise remove it
53
-	 * @since 17.0.0
54
-	 */
55
-	// public const TYPE_CONTACT = 5;
56
-
57
-	/**
58
-	 * @since 17.0.0
59
-	 */
60
-	public const TYPE_REMOTE = 6;
61
-
62
-	/**
63
-	 * @since 17.0.0
64
-	 */
65
-	public const TYPE_CIRCLE = 7;
66
-
67
-	/**
68
-	 * @since 17.0.0
69
-	 */
70
-	public const TYPE_GUEST = 8;
71
-
72
-	/**
73
-	 * @since 17.0.0
74
-	 */
75
-	public const TYPE_REMOTE_GROUP = 9;
76
-
77
-	/**
78
-	 * @since 17.0.0
79
-	 */
80
-	public const TYPE_ROOM = 10;
81
-
82
-	/**
83
-	 * Internal type used by RoomShareProvider
84
-	 * @since 17.0.0
85
-	 */
86
-	// const TYPE_USERROOM = 11;
87
-
88
-	/**
89
-	 * @since 21.0.0
90
-	 */
91
-	public const TYPE_DECK = 12;
92
-
93
-	/**
94
-	 * @internal
95
-	 * @since 21.0.0
96
-	 */
97
-	public const TYPE_DECK_USER = 13;
98
-
99
-	/**
100
-	 * @since 26.0.0
101
-	 * @deprecated 33.0.0 The app is abandonned.
102
-	 */
103
-	public const TYPE_SCIENCEMESH = 15;
104
-
105
-	/**
106
-	 * @since 18.0.0
107
-	 */
108
-	public const STATUS_PENDING = 0;
109
-
110
-	/**
111
-	 * @since 18.0.0
112
-	 */
113
-	public const STATUS_ACCEPTED = 1;
114
-
115
-	/**
116
-	 * @since 18.0.0
117
-	 */
118
-	public const STATUS_REJECTED = 2;
119
-
120
-	/**
121
-	 * Set the internal id of the share
122
-	 * It is only allowed to set the internal id of a share once.
123
-	 * Attempts to override the internal id will result in an IllegalIDChangeException
124
-	 *
125
-	 * @param string $id
126
-	 * @return \OCP\Share\IShare
127
-	 * @throws IllegalIDChangeException
128
-	 * @throws \InvalidArgumentException
129
-	 * @since 9.1.0
130
-	 */
131
-	public function setId($id);
132
-
133
-	/**
134
-	 * Get the internal id of the share.
135
-	 *
136
-	 * @return string
137
-	 * @since 9.0.0
138
-	 */
139
-	public function getId();
140
-
141
-	/**
142
-	 * Get the full share id. This is the <providerid>:<internalid>.
143
-	 * The full id is unique in the system.
144
-	 *
145
-	 * @return string
146
-	 * @since 9.0.0
147
-	 * @throws \UnexpectedValueException If the fullId could not be constructed
148
-	 */
149
-	public function getFullId();
150
-
151
-	/**
152
-	 * Set the provider id of the share
153
-	 * It is only allowed to set the provider id of a share once.
154
-	 * Attempts to override the provider id will result in an IllegalIDChangeException
155
-	 *
156
-	 * @param string $id
157
-	 * @return \OCP\Share\IShare
158
-	 * @throws IllegalIDChangeException
159
-	 * @throws \InvalidArgumentException
160
-	 * @since 9.1.0
161
-	 */
162
-	public function setProviderId($id);
163
-
164
-	/**
165
-	 * Set the node of the file/folder that is shared
166
-	 *
167
-	 * @param Node $node
168
-	 * @return \OCP\Share\IShare The modified object
169
-	 * @since 9.0.0
170
-	 */
171
-	public function setNode(Node $node);
172
-
173
-	/**
174
-	 * Get the node of the file/folder that is shared
175
-	 *
176
-	 * @return File|Folder
177
-	 * @since 9.0.0
178
-	 * @throws NotFoundException
179
-	 */
180
-	public function getNode();
181
-
182
-	/**
183
-	 * Set file id for lazy evaluation of the node
184
-	 * @param int $fileId
185
-	 * @return \OCP\Share\IShare The modified object
186
-	 * @since 9.0.0
187
-	 */
188
-	public function setNodeId($fileId);
189
-
190
-	/**
191
-	 * Get the fileid of the node of this share
192
-	 * @return int
193
-	 * @since 9.0.0
194
-	 * @throws NotFoundException
195
-	 */
196
-	public function getNodeId(): int;
197
-
198
-	/**
199
-	 * Set the type of node (file/folder)
200
-	 *
201
-	 * @param string $type
202
-	 * @return \OCP\Share\IShare The modified object
203
-	 * @since 9.0.0
204
-	 */
205
-	public function setNodeType($type);
206
-
207
-	/**
208
-	 * Get the type of node (file/folder)
209
-	 *
210
-	 * @return string
211
-	 * @since 9.0.0
212
-	 * @throws NotFoundException
213
-	 */
214
-	public function getNodeType();
215
-
216
-	/**
217
-	 * Set the shareType
218
-	 *
219
-	 * @param int $shareType
220
-	 * @return \OCP\Share\IShare The modified object
221
-	 * @since 9.0.0
222
-	 */
223
-	public function setShareType($shareType);
224
-
225
-	/**
226
-	 * Get the shareType
227
-	 *
228
-	 * @return int
229
-	 * @since 9.0.0
230
-	 */
231
-	public function getShareType();
232
-
233
-	/**
234
-	 * Set the receiver of this share.
235
-	 *
236
-	 * @param string $sharedWith
237
-	 * @return \OCP\Share\IShare The modified object
238
-	 * @since 9.0.0
239
-	 */
240
-	public function setSharedWith($sharedWith);
241
-
242
-	/**
243
-	 * Get the receiver of this share.
244
-	 *
245
-	 * @return string
246
-	 * @since 9.0.0
247
-	 */
248
-	public function getSharedWith();
249
-
250
-	/**
251
-	 * Set the display name of the receiver of this share.
252
-	 *
253
-	 * @param string $displayName
254
-	 * @return \OCP\Share\IShare The modified object
255
-	 * @since 14.0.0
256
-	 */
257
-	public function setSharedWithDisplayName($displayName);
258
-
259
-	/**
260
-	 * Get the display name of the receiver of this share.
261
-	 *
262
-	 * @return string
263
-	 * @since 14.0.0
264
-	 */
265
-	public function getSharedWithDisplayName();
266
-
267
-	/**
268
-	 * Set the avatar of the receiver of this share.
269
-	 *
270
-	 * @param string $src
271
-	 * @return \OCP\Share\IShare The modified object
272
-	 * @since 14.0.0
273
-	 */
274
-	public function setSharedWithAvatar($src);
275
-
276
-	/**
277
-	 * Get the avatar of the receiver of this share.
278
-	 *
279
-	 * @return string
280
-	 * @since 14.0.0
281
-	 */
282
-	public function getSharedWithAvatar();
283
-
284
-	/**
285
-	 * Set the permissions.
286
-	 * See \OCP\Constants::PERMISSION_*
287
-	 *
288
-	 * @param int $permissions
289
-	 * @return IShare The modified object
290
-	 * @since 9.0.0
291
-	 */
292
-	public function setPermissions($permissions);
293
-
294
-	/**
295
-	 * Get the share permissions
296
-	 * See \OCP\Constants::PERMISSION_*
297
-	 *
298
-	 * @return int
299
-	 * @since 9.0.0
300
-	 */
301
-	public function getPermissions();
302
-
303
-	/**
304
-	 * Create share attributes object
305
-	 *
306
-	 * @since 25.0.0
307
-	 * @return IAttributes
308
-	 */
309
-	public function newAttributes(): IAttributes;
310
-
311
-	/**
312
-	 * Set share attributes
313
-	 *
314
-	 * @param ?IAttributes $attributes
315
-	 * @since 25.0.0
316
-	 * @return IShare The modified object
317
-	 */
318
-	public function setAttributes(?IAttributes $attributes);
319
-
320
-	/**
321
-	 * Get share attributes
322
-	 *
323
-	 * @since 25.0.0
324
-	 * @return ?IAttributes
325
-	 */
326
-	public function getAttributes(): ?IAttributes;
327
-
328
-	/**
329
-	 * Set the accepted status
330
-	 * See self::STATUS_*
331
-	 *
332
-	 * @param int $status
333
-	 * @return IShare The modified object
334
-	 * @since 18.0.0
335
-	 */
336
-	public function setStatus(int $status): IShare;
337
-
338
-	/**
339
-	 * Get the accepted status
340
-	 * See self::STATUS_*
341
-	 *
342
-	 * @return int
343
-	 * @since 18.0.0
344
-	 */
345
-	public function getStatus(): int;
346
-
347
-	/**
348
-	 * Attach a note to a share
349
-	 *
350
-	 * @param string $note
351
-	 * @return \OCP\Share\IShare The modified object
352
-	 * @since 14.0.0
353
-	 */
354
-	public function setNote($note);
355
-
356
-	/**
357
-	 * Get note attached to a share
358
-	 *
359
-	 * @return string
360
-	 * @since 14.0.0
361
-	 */
362
-	public function getNote();
363
-
364
-
365
-	/**
366
-	 * Set the expiration date
367
-	 *
368
-	 * @param \DateTime|null $expireDate
369
-	 * @return \OCP\Share\IShare The modified object
370
-	 * @since 9.0.0
371
-	 */
372
-	public function setExpirationDate(?\DateTime $expireDate);
373
-
374
-	/**
375
-	 * Get the expiration date
376
-	 *
377
-	 * @return \DateTime|null
378
-	 * @since 9.0.0
379
-	 */
380
-	public function getExpirationDate();
381
-
382
-	/**
383
-	 * Set overwrite flag for falsy expiry date values
384
-	 *
385
-	 * @param bool $noExpirationDate
386
-	 * @return \OCP\Share\IShare The modified object
387
-	 * @since 30.0.0
388
-	 */
389
-	public function setNoExpirationDate(bool $noExpirationDate);
390
-
391
-
392
-	/**
393
-	 * Get value of overwrite falsy expiry date flag
394
-	 *
395
-	 * @return bool
396
-	 * @since 30.0.0
397
-	 */
398
-	public function getNoExpirationDate();
399
-
400
-	/**
401
-	 * Is the share expired ?
402
-	 *
403
-	 * @return boolean
404
-	 * @since 18.0.0
405
-	 */
406
-	public function isExpired();
407
-
408
-	/**
409
-	 * set a label for a share, some shares, e.g. public links can have a label
410
-	 *
411
-	 * @param string $label
412
-	 * @return \OCP\Share\IShare The modified object
413
-	 * @since 15.0.0
414
-	 */
415
-	public function setLabel($label);
416
-
417
-	/**
418
-	 * get label for the share, some shares, e.g. public links can have a label
419
-	 *
420
-	 * @return string
421
-	 * @since 15.0.0
422
-	 */
423
-	public function getLabel();
424
-
425
-	/**
426
-	 * Set the sharer of the path.
427
-	 *
428
-	 * @param string $sharedBy
429
-	 * @return \OCP\Share\IShare The modified object
430
-	 * @since 9.0.0
431
-	 */
432
-	public function setSharedBy($sharedBy);
433
-
434
-	/**
435
-	 * Get share sharer
436
-	 *
437
-	 * @return string
438
-	 * @since 9.0.0
439
-	 */
440
-	public function getSharedBy();
441
-
442
-	/**
443
-	 * Set the original share owner (who owns the path that is shared)
444
-	 *
445
-	 * @param string $shareOwner
446
-	 * @return \OCP\Share\IShare The modified object
447
-	 * @since 9.0.0
448
-	 */
449
-	public function setShareOwner($shareOwner);
450
-
451
-	/**
452
-	 * Get the original share owner (who owns the path that is shared)
453
-	 *
454
-	 * @return string
455
-	 * @since 9.0.0
456
-	 */
457
-	public function getShareOwner();
458
-
459
-	/**
460
-	 * Set the password for this share.
461
-	 * When the share is passed to the share manager to be created
462
-	 * or updated the password will be hashed.
463
-	 *
464
-	 * @param string|null $password
465
-	 * @return \OCP\Share\IShare The modified object
466
-	 * @since 9.0.0
467
-	 */
468
-	public function setPassword($password);
469
-
470
-	/**
471
-	 * Get the password of this share.
472
-	 * If this share is obtained via a shareprovider the password is
473
-	 * hashed.
474
-	 *
475
-	 * @return string|null
476
-	 * @since 9.0.0
477
-	 */
478
-	public function getPassword();
479
-
480
-	/**
481
-	 * Set the password's expiration time of this share.
482
-	 *
483
-	 * @return self The modified object
484
-	 * @since 24.0.0
485
-	 */
486
-	public function setPasswordExpirationTime(?\DateTimeInterface $passwordExpirationTime = null): IShare;
487
-
488
-	/**
489
-	 * Get the password's expiration time of this share.
490
-	 * @since 24.0.0
491
-	 */
492
-	public function getPasswordExpirationTime(): ?\DateTimeInterface;
493
-
494
-	/**
495
-	 * Set if the recipient can start a conversation with the owner to get the
496
-	 * password using Nextcloud Talk.
497
-	 *
498
-	 * @param bool $sendPasswordByTalk
499
-	 * @return \OCP\Share\IShare The modified object
500
-	 * @since 14.0.0
501
-	 */
502
-	public function setSendPasswordByTalk(bool $sendPasswordByTalk);
503
-
504
-	/**
505
-	 * Get if the recipient can start a conversation with the owner to get the
506
-	 * password using Nextcloud Talk.
507
-	 * The returned value does not take into account other factors, like Talk
508
-	 * being enabled for the owner of the share or not; it just cover whether
509
-	 * the option is enabled for the share itself or not.
510
-	 *
511
-	 * @return bool
512
-	 * @since 14.0.0
513
-	 */
514
-	public function getSendPasswordByTalk(): bool;
515
-
516
-	/**
517
-	 * Set the public link token.
518
-	 *
519
-	 * @param string $token
520
-	 * @return \OCP\Share\IShare The modified object
521
-	 * @since 9.0.0
522
-	 */
523
-	public function setToken($token);
524
-
525
-	/**
526
-	 * Get the public link token.
527
-	 *
528
-	 * @return string
529
-	 * @since 9.0.0
530
-	 */
531
-	public function getToken();
532
-
533
-	/**
534
-	 * Set the parent of this share
535
-	 *
536
-	 * @since 9.0.0
537
-	 */
538
-	public function setParent(int $parent): self;
539
-
540
-	/**
541
-	 * Get the parent of this share.
542
-	 *
543
-	 * @since 9.0.0
544
-	 */
545
-	public function getParent(): ?int;
546
-
547
-	/**
548
-	 * Set the target path of this share relative to the recipients user folder.
549
-	 *
550
-	 * @param string $target
551
-	 * @return \OCP\Share\IShare The modified object
552
-	 * @since 9.0.0
553
-	 */
554
-	public function setTarget($target);
555
-
556
-	/**
557
-	 * Get the target path of this share relative to the recipients user folder.
558
-	 *
559
-	 * @return string
560
-	 * @since 9.0.0
561
-	 */
562
-	public function getTarget();
563
-
564
-	/**
565
-	 * Set the time this share was created
566
-	 *
567
-	 * @param \DateTime $shareTime
568
-	 * @return \OCP\Share\IShare The modified object
569
-	 * @since 9.0.0
570
-	 */
571
-	public function setShareTime(\DateTime $shareTime);
572
-
573
-	/**
574
-	 * Get the timestamp this share was created
575
-	 *
576
-	 * @return \DateTime
577
-	 * @since 9.0.0
578
-	 */
579
-	public function getShareTime();
580
-
581
-	/**
582
-	 * Set if the recipient should be informed by mail about the share.
583
-	 *
584
-	 * @param bool $mailSend
585
-	 * @return \OCP\Share\IShare The modified object
586
-	 * @since 9.0.0
587
-	 */
588
-	public function setMailSend($mailSend);
589
-
590
-	/**
591
-	 * Get if the recipient should be informed by mail about the share.
592
-	 *
593
-	 * @return bool
594
-	 * @since 9.0.0
595
-	 */
596
-	public function getMailSend();
597
-
598
-	/**
599
-	 * Set the cache entry for the shared node
600
-	 *
601
-	 * @param ICacheEntry $entry
602
-	 * @return void
603
-	 * @since 11.0.0
604
-	 */
605
-	public function setNodeCacheEntry(ICacheEntry $entry);
606
-
607
-	/**
608
-	 * Get the cache entry for the shared node
609
-	 *
610
-	 * @return null|ICacheEntry
611
-	 * @since 11.0.0
612
-	 */
613
-	public function getNodeCacheEntry();
614
-
615
-	/**
616
-	 * Sets a shares hide download state
617
-	 * This is mainly for public shares. It will signal that the share page should
618
-	 * hide download buttons etc.
619
-	 *
620
-	 * @param bool $hide
621
-	 * @return IShare
622
-	 * @since 15.0.0
623
-	 */
624
-	public function setHideDownload(bool $hide): IShare;
625
-
626
-	/**
627
-	 * Gets a shares hide download state
628
-	 * This is mainly for public shares. It will signal that the share page should
629
-	 * hide download buttons etc.
630
-	 *
631
-	 * @return bool
632
-	 * @since 15.0.0
633
-	 */
634
-	public function getHideDownload(): bool;
635
-
636
-	/**
637
-	 * Sets a flag that stores whether a reminder via email has been sent
638
-	 *
639
-	 * @return self The modified object
640
-	 * @since 31.0.0
641
-	 */
642
-	public function setReminderSent(bool $reminderSent): IShare;
643
-
644
-	/**
645
-	 * Gets a flag that stores whether a reminder via email has been sent
646
-	 *
647
-	 * @return bool
648
-	 * @since 31.0.0
649
-	 */
650
-	public function getReminderSent(): bool;
651
-
652
-	/**
653
-	 * Check if the current user can see this share files contents.
654
-	 * This will check the download permissions as well as the global
655
-	 * admin setting to allow viewing files without downloading.
656
-	 */
657
-	public function canSeeContent(): bool;
25
+    /**
26
+     * @since 17.0.0
27
+     */
28
+    public const TYPE_USER = 0;
29
+
30
+    /**
31
+     * @since 17.0.0
32
+     */
33
+    public const TYPE_GROUP = 1;
34
+
35
+    /**
36
+     * @internal
37
+     * @since 18.0.0
38
+     */
39
+    public const TYPE_USERGROUP = 2;
40
+
41
+    /**
42
+     * @since 17.0.0
43
+     */
44
+    public const TYPE_LINK = 3;
45
+
46
+    /**
47
+     * @since 17.0.0
48
+     */
49
+    public const TYPE_EMAIL = 4;
50
+
51
+    /**
52
+     * ToDo Check if it is still in use otherwise remove it
53
+     * @since 17.0.0
54
+     */
55
+    // public const TYPE_CONTACT = 5;
56
+
57
+    /**
58
+     * @since 17.0.0
59
+     */
60
+    public const TYPE_REMOTE = 6;
61
+
62
+    /**
63
+     * @since 17.0.0
64
+     */
65
+    public const TYPE_CIRCLE = 7;
66
+
67
+    /**
68
+     * @since 17.0.0
69
+     */
70
+    public const TYPE_GUEST = 8;
71
+
72
+    /**
73
+     * @since 17.0.0
74
+     */
75
+    public const TYPE_REMOTE_GROUP = 9;
76
+
77
+    /**
78
+     * @since 17.0.0
79
+     */
80
+    public const TYPE_ROOM = 10;
81
+
82
+    /**
83
+     * Internal type used by RoomShareProvider
84
+     * @since 17.0.0
85
+     */
86
+    // const TYPE_USERROOM = 11;
87
+
88
+    /**
89
+     * @since 21.0.0
90
+     */
91
+    public const TYPE_DECK = 12;
92
+
93
+    /**
94
+     * @internal
95
+     * @since 21.0.0
96
+     */
97
+    public const TYPE_DECK_USER = 13;
98
+
99
+    /**
100
+     * @since 26.0.0
101
+     * @deprecated 33.0.0 The app is abandonned.
102
+     */
103
+    public const TYPE_SCIENCEMESH = 15;
104
+
105
+    /**
106
+     * @since 18.0.0
107
+     */
108
+    public const STATUS_PENDING = 0;
109
+
110
+    /**
111
+     * @since 18.0.0
112
+     */
113
+    public const STATUS_ACCEPTED = 1;
114
+
115
+    /**
116
+     * @since 18.0.0
117
+     */
118
+    public const STATUS_REJECTED = 2;
119
+
120
+    /**
121
+     * Set the internal id of the share
122
+     * It is only allowed to set the internal id of a share once.
123
+     * Attempts to override the internal id will result in an IllegalIDChangeException
124
+     *
125
+     * @param string $id
126
+     * @return \OCP\Share\IShare
127
+     * @throws IllegalIDChangeException
128
+     * @throws \InvalidArgumentException
129
+     * @since 9.1.0
130
+     */
131
+    public function setId($id);
132
+
133
+    /**
134
+     * Get the internal id of the share.
135
+     *
136
+     * @return string
137
+     * @since 9.0.0
138
+     */
139
+    public function getId();
140
+
141
+    /**
142
+     * Get the full share id. This is the <providerid>:<internalid>.
143
+     * The full id is unique in the system.
144
+     *
145
+     * @return string
146
+     * @since 9.0.0
147
+     * @throws \UnexpectedValueException If the fullId could not be constructed
148
+     */
149
+    public function getFullId();
150
+
151
+    /**
152
+     * Set the provider id of the share
153
+     * It is only allowed to set the provider id of a share once.
154
+     * Attempts to override the provider id will result in an IllegalIDChangeException
155
+     *
156
+     * @param string $id
157
+     * @return \OCP\Share\IShare
158
+     * @throws IllegalIDChangeException
159
+     * @throws \InvalidArgumentException
160
+     * @since 9.1.0
161
+     */
162
+    public function setProviderId($id);
163
+
164
+    /**
165
+     * Set the node of the file/folder that is shared
166
+     *
167
+     * @param Node $node
168
+     * @return \OCP\Share\IShare The modified object
169
+     * @since 9.0.0
170
+     */
171
+    public function setNode(Node $node);
172
+
173
+    /**
174
+     * Get the node of the file/folder that is shared
175
+     *
176
+     * @return File|Folder
177
+     * @since 9.0.0
178
+     * @throws NotFoundException
179
+     */
180
+    public function getNode();
181
+
182
+    /**
183
+     * Set file id for lazy evaluation of the node
184
+     * @param int $fileId
185
+     * @return \OCP\Share\IShare The modified object
186
+     * @since 9.0.0
187
+     */
188
+    public function setNodeId($fileId);
189
+
190
+    /**
191
+     * Get the fileid of the node of this share
192
+     * @return int
193
+     * @since 9.0.0
194
+     * @throws NotFoundException
195
+     */
196
+    public function getNodeId(): int;
197
+
198
+    /**
199
+     * Set the type of node (file/folder)
200
+     *
201
+     * @param string $type
202
+     * @return \OCP\Share\IShare The modified object
203
+     * @since 9.0.0
204
+     */
205
+    public function setNodeType($type);
206
+
207
+    /**
208
+     * Get the type of node (file/folder)
209
+     *
210
+     * @return string
211
+     * @since 9.0.0
212
+     * @throws NotFoundException
213
+     */
214
+    public function getNodeType();
215
+
216
+    /**
217
+     * Set the shareType
218
+     *
219
+     * @param int $shareType
220
+     * @return \OCP\Share\IShare The modified object
221
+     * @since 9.0.0
222
+     */
223
+    public function setShareType($shareType);
224
+
225
+    /**
226
+     * Get the shareType
227
+     *
228
+     * @return int
229
+     * @since 9.0.0
230
+     */
231
+    public function getShareType();
232
+
233
+    /**
234
+     * Set the receiver of this share.
235
+     *
236
+     * @param string $sharedWith
237
+     * @return \OCP\Share\IShare The modified object
238
+     * @since 9.0.0
239
+     */
240
+    public function setSharedWith($sharedWith);
241
+
242
+    /**
243
+     * Get the receiver of this share.
244
+     *
245
+     * @return string
246
+     * @since 9.0.0
247
+     */
248
+    public function getSharedWith();
249
+
250
+    /**
251
+     * Set the display name of the receiver of this share.
252
+     *
253
+     * @param string $displayName
254
+     * @return \OCP\Share\IShare The modified object
255
+     * @since 14.0.0
256
+     */
257
+    public function setSharedWithDisplayName($displayName);
258
+
259
+    /**
260
+     * Get the display name of the receiver of this share.
261
+     *
262
+     * @return string
263
+     * @since 14.0.0
264
+     */
265
+    public function getSharedWithDisplayName();
266
+
267
+    /**
268
+     * Set the avatar of the receiver of this share.
269
+     *
270
+     * @param string $src
271
+     * @return \OCP\Share\IShare The modified object
272
+     * @since 14.0.0
273
+     */
274
+    public function setSharedWithAvatar($src);
275
+
276
+    /**
277
+     * Get the avatar of the receiver of this share.
278
+     *
279
+     * @return string
280
+     * @since 14.0.0
281
+     */
282
+    public function getSharedWithAvatar();
283
+
284
+    /**
285
+     * Set the permissions.
286
+     * See \OCP\Constants::PERMISSION_*
287
+     *
288
+     * @param int $permissions
289
+     * @return IShare The modified object
290
+     * @since 9.0.0
291
+     */
292
+    public function setPermissions($permissions);
293
+
294
+    /**
295
+     * Get the share permissions
296
+     * See \OCP\Constants::PERMISSION_*
297
+     *
298
+     * @return int
299
+     * @since 9.0.0
300
+     */
301
+    public function getPermissions();
302
+
303
+    /**
304
+     * Create share attributes object
305
+     *
306
+     * @since 25.0.0
307
+     * @return IAttributes
308
+     */
309
+    public function newAttributes(): IAttributes;
310
+
311
+    /**
312
+     * Set share attributes
313
+     *
314
+     * @param ?IAttributes $attributes
315
+     * @since 25.0.0
316
+     * @return IShare The modified object
317
+     */
318
+    public function setAttributes(?IAttributes $attributes);
319
+
320
+    /**
321
+     * Get share attributes
322
+     *
323
+     * @since 25.0.0
324
+     * @return ?IAttributes
325
+     */
326
+    public function getAttributes(): ?IAttributes;
327
+
328
+    /**
329
+     * Set the accepted status
330
+     * See self::STATUS_*
331
+     *
332
+     * @param int $status
333
+     * @return IShare The modified object
334
+     * @since 18.0.0
335
+     */
336
+    public function setStatus(int $status): IShare;
337
+
338
+    /**
339
+     * Get the accepted status
340
+     * See self::STATUS_*
341
+     *
342
+     * @return int
343
+     * @since 18.0.0
344
+     */
345
+    public function getStatus(): int;
346
+
347
+    /**
348
+     * Attach a note to a share
349
+     *
350
+     * @param string $note
351
+     * @return \OCP\Share\IShare The modified object
352
+     * @since 14.0.0
353
+     */
354
+    public function setNote($note);
355
+
356
+    /**
357
+     * Get note attached to a share
358
+     *
359
+     * @return string
360
+     * @since 14.0.0
361
+     */
362
+    public function getNote();
363
+
364
+
365
+    /**
366
+     * Set the expiration date
367
+     *
368
+     * @param \DateTime|null $expireDate
369
+     * @return \OCP\Share\IShare The modified object
370
+     * @since 9.0.0
371
+     */
372
+    public function setExpirationDate(?\DateTime $expireDate);
373
+
374
+    /**
375
+     * Get the expiration date
376
+     *
377
+     * @return \DateTime|null
378
+     * @since 9.0.0
379
+     */
380
+    public function getExpirationDate();
381
+
382
+    /**
383
+     * Set overwrite flag for falsy expiry date values
384
+     *
385
+     * @param bool $noExpirationDate
386
+     * @return \OCP\Share\IShare The modified object
387
+     * @since 30.0.0
388
+     */
389
+    public function setNoExpirationDate(bool $noExpirationDate);
390
+
391
+
392
+    /**
393
+     * Get value of overwrite falsy expiry date flag
394
+     *
395
+     * @return bool
396
+     * @since 30.0.0
397
+     */
398
+    public function getNoExpirationDate();
399
+
400
+    /**
401
+     * Is the share expired ?
402
+     *
403
+     * @return boolean
404
+     * @since 18.0.0
405
+     */
406
+    public function isExpired();
407
+
408
+    /**
409
+     * set a label for a share, some shares, e.g. public links can have a label
410
+     *
411
+     * @param string $label
412
+     * @return \OCP\Share\IShare The modified object
413
+     * @since 15.0.0
414
+     */
415
+    public function setLabel($label);
416
+
417
+    /**
418
+     * get label for the share, some shares, e.g. public links can have a label
419
+     *
420
+     * @return string
421
+     * @since 15.0.0
422
+     */
423
+    public function getLabel();
424
+
425
+    /**
426
+     * Set the sharer of the path.
427
+     *
428
+     * @param string $sharedBy
429
+     * @return \OCP\Share\IShare The modified object
430
+     * @since 9.0.0
431
+     */
432
+    public function setSharedBy($sharedBy);
433
+
434
+    /**
435
+     * Get share sharer
436
+     *
437
+     * @return string
438
+     * @since 9.0.0
439
+     */
440
+    public function getSharedBy();
441
+
442
+    /**
443
+     * Set the original share owner (who owns the path that is shared)
444
+     *
445
+     * @param string $shareOwner
446
+     * @return \OCP\Share\IShare The modified object
447
+     * @since 9.0.0
448
+     */
449
+    public function setShareOwner($shareOwner);
450
+
451
+    /**
452
+     * Get the original share owner (who owns the path that is shared)
453
+     *
454
+     * @return string
455
+     * @since 9.0.0
456
+     */
457
+    public function getShareOwner();
458
+
459
+    /**
460
+     * Set the password for this share.
461
+     * When the share is passed to the share manager to be created
462
+     * or updated the password will be hashed.
463
+     *
464
+     * @param string|null $password
465
+     * @return \OCP\Share\IShare The modified object
466
+     * @since 9.0.0
467
+     */
468
+    public function setPassword($password);
469
+
470
+    /**
471
+     * Get the password of this share.
472
+     * If this share is obtained via a shareprovider the password is
473
+     * hashed.
474
+     *
475
+     * @return string|null
476
+     * @since 9.0.0
477
+     */
478
+    public function getPassword();
479
+
480
+    /**
481
+     * Set the password's expiration time of this share.
482
+     *
483
+     * @return self The modified object
484
+     * @since 24.0.0
485
+     */
486
+    public function setPasswordExpirationTime(?\DateTimeInterface $passwordExpirationTime = null): IShare;
487
+
488
+    /**
489
+     * Get the password's expiration time of this share.
490
+     * @since 24.0.0
491
+     */
492
+    public function getPasswordExpirationTime(): ?\DateTimeInterface;
493
+
494
+    /**
495
+     * Set if the recipient can start a conversation with the owner to get the
496
+     * password using Nextcloud Talk.
497
+     *
498
+     * @param bool $sendPasswordByTalk
499
+     * @return \OCP\Share\IShare The modified object
500
+     * @since 14.0.0
501
+     */
502
+    public function setSendPasswordByTalk(bool $sendPasswordByTalk);
503
+
504
+    /**
505
+     * Get if the recipient can start a conversation with the owner to get the
506
+     * password using Nextcloud Talk.
507
+     * The returned value does not take into account other factors, like Talk
508
+     * being enabled for the owner of the share or not; it just cover whether
509
+     * the option is enabled for the share itself or not.
510
+     *
511
+     * @return bool
512
+     * @since 14.0.0
513
+     */
514
+    public function getSendPasswordByTalk(): bool;
515
+
516
+    /**
517
+     * Set the public link token.
518
+     *
519
+     * @param string $token
520
+     * @return \OCP\Share\IShare The modified object
521
+     * @since 9.0.0
522
+     */
523
+    public function setToken($token);
524
+
525
+    /**
526
+     * Get the public link token.
527
+     *
528
+     * @return string
529
+     * @since 9.0.0
530
+     */
531
+    public function getToken();
532
+
533
+    /**
534
+     * Set the parent of this share
535
+     *
536
+     * @since 9.0.0
537
+     */
538
+    public function setParent(int $parent): self;
539
+
540
+    /**
541
+     * Get the parent of this share.
542
+     *
543
+     * @since 9.0.0
544
+     */
545
+    public function getParent(): ?int;
546
+
547
+    /**
548
+     * Set the target path of this share relative to the recipients user folder.
549
+     *
550
+     * @param string $target
551
+     * @return \OCP\Share\IShare The modified object
552
+     * @since 9.0.0
553
+     */
554
+    public function setTarget($target);
555
+
556
+    /**
557
+     * Get the target path of this share relative to the recipients user folder.
558
+     *
559
+     * @return string
560
+     * @since 9.0.0
561
+     */
562
+    public function getTarget();
563
+
564
+    /**
565
+     * Set the time this share was created
566
+     *
567
+     * @param \DateTime $shareTime
568
+     * @return \OCP\Share\IShare The modified object
569
+     * @since 9.0.0
570
+     */
571
+    public function setShareTime(\DateTime $shareTime);
572
+
573
+    /**
574
+     * Get the timestamp this share was created
575
+     *
576
+     * @return \DateTime
577
+     * @since 9.0.0
578
+     */
579
+    public function getShareTime();
580
+
581
+    /**
582
+     * Set if the recipient should be informed by mail about the share.
583
+     *
584
+     * @param bool $mailSend
585
+     * @return \OCP\Share\IShare The modified object
586
+     * @since 9.0.0
587
+     */
588
+    public function setMailSend($mailSend);
589
+
590
+    /**
591
+     * Get if the recipient should be informed by mail about the share.
592
+     *
593
+     * @return bool
594
+     * @since 9.0.0
595
+     */
596
+    public function getMailSend();
597
+
598
+    /**
599
+     * Set the cache entry for the shared node
600
+     *
601
+     * @param ICacheEntry $entry
602
+     * @return void
603
+     * @since 11.0.0
604
+     */
605
+    public function setNodeCacheEntry(ICacheEntry $entry);
606
+
607
+    /**
608
+     * Get the cache entry for the shared node
609
+     *
610
+     * @return null|ICacheEntry
611
+     * @since 11.0.0
612
+     */
613
+    public function getNodeCacheEntry();
614
+
615
+    /**
616
+     * Sets a shares hide download state
617
+     * This is mainly for public shares. It will signal that the share page should
618
+     * hide download buttons etc.
619
+     *
620
+     * @param bool $hide
621
+     * @return IShare
622
+     * @since 15.0.0
623
+     */
624
+    public function setHideDownload(bool $hide): IShare;
625
+
626
+    /**
627
+     * Gets a shares hide download state
628
+     * This is mainly for public shares. It will signal that the share page should
629
+     * hide download buttons etc.
630
+     *
631
+     * @return bool
632
+     * @since 15.0.0
633
+     */
634
+    public function getHideDownload(): bool;
635
+
636
+    /**
637
+     * Sets a flag that stores whether a reminder via email has been sent
638
+     *
639
+     * @return self The modified object
640
+     * @since 31.0.0
641
+     */
642
+    public function setReminderSent(bool $reminderSent): IShare;
643
+
644
+    /**
645
+     * Gets a flag that stores whether a reminder via email has been sent
646
+     *
647
+     * @return bool
648
+     * @since 31.0.0
649
+     */
650
+    public function getReminderSent(): bool;
651
+
652
+    /**
653
+     * Check if the current user can see this share files contents.
654
+     * This will check the download permissions as well as the global
655
+     * admin setting to allow viewing files without downloading.
656
+     */
657
+    public function canSeeContent(): bool;
658 658
 }
Please login to merge, or discard this patch.
lib/private/Files/View.php 1 patch
Indentation   +2227 added lines, -2227 removed lines patch added patch discarded remove patch
@@ -60,2231 +60,2231 @@
 block discarded – undo
60 60
  * @internal Since 33.0.0. use IRootFolder and the Folder/File/Node API instead in new code.
61 61
  */
62 62
 class View {
63
-	private string $fakeRoot = '';
64
-	private ILockingProvider $lockingProvider;
65
-	private bool $lockingEnabled;
66
-	private bool $updaterEnabled = true;
67
-	private UserManager $userManager;
68
-	private LoggerInterface $logger;
69
-
70
-	/**
71
-	 * @throws \Exception If $root contains an invalid path
72
-	 */
73
-	public function __construct(string $root = '') {
74
-		if (!Filesystem::isValidPath($root)) {
75
-			throw new \Exception();
76
-		}
77
-
78
-		$this->fakeRoot = $root;
79
-		$this->lockingProvider = \OC::$server->get(ILockingProvider::class);
80
-		$this->lockingEnabled = !($this->lockingProvider instanceof \OC\Lock\NoopLockingProvider);
81
-		$this->userManager = \OC::$server->getUserManager();
82
-		$this->logger = \OC::$server->get(LoggerInterface::class);
83
-	}
84
-
85
-	/**
86
-	 * @param ?string $path
87
-	 * @psalm-template S as string|null
88
-	 * @psalm-param S $path
89
-	 * @psalm-return (S is string ? string : null)
90
-	 */
91
-	public function getAbsolutePath($path = '/'): ?string {
92
-		if ($path === null) {
93
-			return null;
94
-		}
95
-		$this->assertPathLength($path);
96
-		return PathHelper::normalizePath($this->fakeRoot . '/' . $path);
97
-	}
98
-
99
-	/**
100
-	 * Change the root to a fake root
101
-	 *
102
-	 * @param string $fakeRoot
103
-	 */
104
-	public function chroot($fakeRoot): void {
105
-		if (!$fakeRoot == '') {
106
-			if ($fakeRoot[0] !== '/') {
107
-				$fakeRoot = '/' . $fakeRoot;
108
-			}
109
-		}
110
-		$this->fakeRoot = $fakeRoot;
111
-	}
112
-
113
-	/**
114
-	 * Get the fake root
115
-	 */
116
-	public function getRoot(): string {
117
-		return $this->fakeRoot;
118
-	}
119
-
120
-	/**
121
-	 * get path relative to the root of the view
122
-	 *
123
-	 * @param string $path
124
-	 */
125
-	public function getRelativePath($path): ?string {
126
-		$this->assertPathLength($path);
127
-		if ($this->fakeRoot == '') {
128
-			return $path;
129
-		}
130
-
131
-		if (rtrim($path, '/') === rtrim($this->fakeRoot, '/')) {
132
-			return '/';
133
-		}
134
-
135
-		// missing slashes can cause wrong matches!
136
-		$root = rtrim($this->fakeRoot, '/') . '/';
137
-
138
-		if (!str_starts_with($path, $root)) {
139
-			return null;
140
-		} else {
141
-			$path = substr($path, strlen($this->fakeRoot));
142
-			if (strlen($path) === 0) {
143
-				return '/';
144
-			} else {
145
-				return $path;
146
-			}
147
-		}
148
-	}
149
-
150
-	/**
151
-	 * Get the mountpoint of the storage object for a path
152
-	 * ( note: because a storage is not always mounted inside the fakeroot, the
153
-	 * returned mountpoint is relative to the absolute root of the filesystem
154
-	 * and does not take the chroot into account )
155
-	 *
156
-	 * @param string $path
157
-	 */
158
-	public function getMountPoint($path): string {
159
-		return Filesystem::getMountPoint($this->getAbsolutePath($path));
160
-	}
161
-
162
-	/**
163
-	 * Get the mountpoint of the storage object for a path
164
-	 * ( note: because a storage is not always mounted inside the fakeroot, the
165
-	 * returned mountpoint is relative to the absolute root of the filesystem
166
-	 * and does not take the chroot into account )
167
-	 *
168
-	 * @param string $path
169
-	 */
170
-	public function getMount($path): IMountPoint {
171
-		return Filesystem::getMountManager()->find($this->getAbsolutePath($path));
172
-	}
173
-
174
-	/**
175
-	 * Resolve a path to a storage and internal path
176
-	 *
177
-	 * @param string $path
178
-	 * @return array{?\OCP\Files\Storage\IStorage, string} an array consisting of the storage and the internal path
179
-	 */
180
-	public function resolvePath($path): array {
181
-		$a = $this->getAbsolutePath($path);
182
-		$p = Filesystem::normalizePath($a);
183
-		return Filesystem::resolvePath($p);
184
-	}
185
-
186
-	/**
187
-	 * Return the path to a local version of the file
188
-	 * we need this because we can't know if a file is stored local or not from
189
-	 * outside the filestorage and for some purposes a local file is needed
190
-	 *
191
-	 * @param string $path
192
-	 */
193
-	public function getLocalFile($path): string|false {
194
-		$parent = substr($path, 0, strrpos($path, '/') ?: 0);
195
-		$path = $this->getAbsolutePath($path);
196
-		[$storage, $internalPath] = Filesystem::resolvePath($path);
197
-		if (Filesystem::isValidPath($parent) && $storage) {
198
-			return $storage->getLocalFile($internalPath);
199
-		} else {
200
-			return false;
201
-		}
202
-	}
203
-
204
-	/**
205
-	 * the following functions operate with arguments and return values identical
206
-	 * to those of their PHP built-in equivalents. Mostly they are merely wrappers
207
-	 * for \OC\Files\Storage\Storage via basicOperation().
208
-	 */
209
-	public function mkdir($path) {
210
-		return $this->basicOperation('mkdir', $path, ['create', 'write']);
211
-	}
212
-
213
-	/**
214
-	 * remove mount point
215
-	 *
216
-	 * @param IMountPoint $mount
217
-	 * @param string $path relative to data/
218
-	 */
219
-	protected function removeMount($mount, $path): bool {
220
-		if ($mount instanceof MoveableMount) {
221
-			// cut of /user/files to get the relative path to data/user/files
222
-			$pathParts = explode('/', $path, 4);
223
-			$relPath = '/' . $pathParts[3];
224
-			$this->lockFile($relPath, ILockingProvider::LOCK_SHARED, true);
225
-			\OC_Hook::emit(
226
-				Filesystem::CLASSNAME, 'umount',
227
-				[Filesystem::signal_param_path => $relPath]
228
-			);
229
-			$this->changeLock($relPath, ILockingProvider::LOCK_EXCLUSIVE, true);
230
-			$result = $mount->removeMount();
231
-			$this->changeLock($relPath, ILockingProvider::LOCK_SHARED, true);
232
-			if ($result) {
233
-				\OC_Hook::emit(
234
-					Filesystem::CLASSNAME, 'post_umount',
235
-					[Filesystem::signal_param_path => $relPath]
236
-				);
237
-			}
238
-			$this->unlockFile($relPath, ILockingProvider::LOCK_SHARED, true);
239
-			return $result;
240
-		} else {
241
-			// do not allow deleting the storage's root / the mount point
242
-			// because for some storages it might delete the whole contents
243
-			// but isn't supposed to work that way
244
-			return false;
245
-		}
246
-	}
247
-
248
-	public function disableCacheUpdate(): void {
249
-		$this->updaterEnabled = false;
250
-	}
251
-
252
-	public function enableCacheUpdate(): void {
253
-		$this->updaterEnabled = true;
254
-	}
255
-
256
-	protected function writeUpdate(Storage $storage, string $internalPath, ?int $time = null, ?int $sizeDifference = null): void {
257
-		if ($this->updaterEnabled) {
258
-			if (is_null($time)) {
259
-				$time = time();
260
-			}
261
-			$storage->getUpdater()->update($internalPath, $time, $sizeDifference);
262
-		}
263
-	}
264
-
265
-	protected function removeUpdate(Storage $storage, string $internalPath): void {
266
-		if ($this->updaterEnabled) {
267
-			$storage->getUpdater()->remove($internalPath);
268
-		}
269
-	}
270
-
271
-	protected function renameUpdate(Storage $sourceStorage, Storage $targetStorage, string $sourceInternalPath, string $targetInternalPath): void {
272
-		if ($this->updaterEnabled) {
273
-			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
274
-		}
275
-	}
276
-
277
-	protected function copyUpdate(Storage $sourceStorage, Storage $targetStorage, string $sourceInternalPath, string $targetInternalPath): void {
278
-		if ($this->updaterEnabled) {
279
-			$targetStorage->getUpdater()->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
280
-		}
281
-	}
282
-
283
-	/**
284
-	 * @param string $path
285
-	 * @return bool|mixed
286
-	 */
287
-	public function rmdir($path) {
288
-		$absolutePath = $this->getAbsolutePath($path);
289
-		$mount = Filesystem::getMountManager()->find($absolutePath);
290
-		if ($mount->getInternalPath($absolutePath) === '') {
291
-			return $this->removeMount($mount, $absolutePath);
292
-		}
293
-		if ($this->is_dir($path)) {
294
-			$result = $this->basicOperation('rmdir', $path, ['delete']);
295
-		} else {
296
-			$result = false;
297
-		}
298
-
299
-		if (!$result && !$this->file_exists($path)) { //clear ghost files from the cache on delete
300
-			$storage = $mount->getStorage();
301
-			$internalPath = $mount->getInternalPath($absolutePath);
302
-			$storage->getUpdater()->remove($internalPath);
303
-		}
304
-		return $result;
305
-	}
306
-
307
-	/**
308
-	 * @param string $path
309
-	 * @return resource|false
310
-	 */
311
-	public function opendir($path) {
312
-		return $this->basicOperation('opendir', $path, ['read']);
313
-	}
314
-
315
-	/**
316
-	 * @param string $path
317
-	 * @return bool|mixed
318
-	 */
319
-	public function is_dir($path) {
320
-		if ($path == '/') {
321
-			return true;
322
-		}
323
-		return $this->basicOperation('is_dir', $path);
324
-	}
325
-
326
-	/**
327
-	 * @param string $path
328
-	 * @return bool|mixed
329
-	 */
330
-	public function is_file($path) {
331
-		if ($path == '/') {
332
-			return false;
333
-		}
334
-		return $this->basicOperation('is_file', $path);
335
-	}
336
-
337
-	/**
338
-	 * @param string $path
339
-	 * @return mixed
340
-	 */
341
-	public function stat($path) {
342
-		return $this->basicOperation('stat', $path);
343
-	}
344
-
345
-	/**
346
-	 * @param string $path
347
-	 * @return mixed
348
-	 */
349
-	public function filetype($path) {
350
-		return $this->basicOperation('filetype', $path);
351
-	}
352
-
353
-	/**
354
-	 * @param string $path
355
-	 * @return mixed
356
-	 */
357
-	public function filesize(string $path) {
358
-		return $this->basicOperation('filesize', $path);
359
-	}
360
-
361
-	/**
362
-	 * @param string $path
363
-	 * @return bool|mixed
364
-	 * @throws InvalidPathException
365
-	 */
366
-	public function readfile($path) {
367
-		$this->assertPathLength($path);
368
-		if (ob_get_level()) {
369
-			ob_end_clean();
370
-		}
371
-		$handle = $this->fopen($path, 'rb');
372
-		if ($handle) {
373
-			$chunkSize = 524288; // 512 kiB chunks
374
-			while (!feof($handle)) {
375
-				echo fread($handle, $chunkSize);
376
-				flush();
377
-				$this->checkConnectionStatus();
378
-			}
379
-			fclose($handle);
380
-			return $this->filesize($path);
381
-		}
382
-		return false;
383
-	}
384
-
385
-	/**
386
-	 * @param string $path
387
-	 * @param int $from
388
-	 * @param int $to
389
-	 * @return bool|mixed
390
-	 * @throws InvalidPathException
391
-	 * @throws \OCP\Files\UnseekableException
392
-	 */
393
-	public function readfilePart($path, $from, $to) {
394
-		$this->assertPathLength($path);
395
-		if (ob_get_level()) {
396
-			ob_end_clean();
397
-		}
398
-		$handle = $this->fopen($path, 'rb');
399
-		if ($handle) {
400
-			$chunkSize = 524288; // 512 kiB chunks
401
-			$startReading = true;
402
-
403
-			if ($from !== 0 && $from !== '0' && fseek($handle, $from) !== 0) {
404
-				// forward file handle via chunked fread because fseek seem to have failed
405
-
406
-				$end = $from + 1;
407
-				while (!feof($handle) && ftell($handle) < $end && ftell($handle) !== $from) {
408
-					$len = $from - ftell($handle);
409
-					if ($len > $chunkSize) {
410
-						$len = $chunkSize;
411
-					}
412
-					$result = fread($handle, $len);
413
-
414
-					if ($result === false) {
415
-						$startReading = false;
416
-						break;
417
-					}
418
-				}
419
-			}
420
-
421
-			if ($startReading) {
422
-				$end = $to + 1;
423
-				while (!feof($handle) && ftell($handle) < $end) {
424
-					$len = $end - ftell($handle);
425
-					if ($len > $chunkSize) {
426
-						$len = $chunkSize;
427
-					}
428
-					echo fread($handle, $len);
429
-					flush();
430
-					$this->checkConnectionStatus();
431
-				}
432
-				return ftell($handle) - $from;
433
-			}
434
-
435
-			throw new \OCP\Files\UnseekableException('fseek error');
436
-		}
437
-		return false;
438
-	}
439
-
440
-	private function checkConnectionStatus(): void {
441
-		$connectionStatus = \connection_status();
442
-		if ($connectionStatus !== CONNECTION_NORMAL) {
443
-			throw new ConnectionLostException("Connection lost. Status: $connectionStatus");
444
-		}
445
-	}
446
-
447
-	/**
448
-	 * @param string $path
449
-	 * @return mixed
450
-	 */
451
-	public function isCreatable($path) {
452
-		return $this->basicOperation('isCreatable', $path);
453
-	}
454
-
455
-	/**
456
-	 * @param string $path
457
-	 * @return mixed
458
-	 */
459
-	public function isReadable($path) {
460
-		return $this->basicOperation('isReadable', $path);
461
-	}
462
-
463
-	/**
464
-	 * @param string $path
465
-	 * @return mixed
466
-	 */
467
-	public function isUpdatable($path) {
468
-		return $this->basicOperation('isUpdatable', $path);
469
-	}
470
-
471
-	/**
472
-	 * @param string $path
473
-	 * @return bool|mixed
474
-	 */
475
-	public function isDeletable($path) {
476
-		$absolutePath = $this->getAbsolutePath($path);
477
-		$mount = Filesystem::getMountManager()->find($absolutePath);
478
-		if ($mount->getInternalPath($absolutePath) === '') {
479
-			return $mount instanceof MoveableMount;
480
-		}
481
-		return $this->basicOperation('isDeletable', $path);
482
-	}
483
-
484
-	/**
485
-	 * @param string $path
486
-	 * @return mixed
487
-	 */
488
-	public function isSharable($path) {
489
-		return $this->basicOperation('isSharable', $path);
490
-	}
491
-
492
-	/**
493
-	 * @param string $path
494
-	 * @return bool|mixed
495
-	 */
496
-	public function file_exists($path) {
497
-		if ($path == '/') {
498
-			return true;
499
-		}
500
-		return $this->basicOperation('file_exists', $path);
501
-	}
502
-
503
-	/**
504
-	 * @param string $path
505
-	 * @return mixed
506
-	 */
507
-	public function filemtime($path) {
508
-		return $this->basicOperation('filemtime', $path);
509
-	}
510
-
511
-	/**
512
-	 * @param string $path
513
-	 * @param int|string $mtime
514
-	 */
515
-	public function touch($path, $mtime = null): bool {
516
-		if (!is_null($mtime) && !is_numeric($mtime)) {
517
-			$mtime = strtotime($mtime);
518
-		}
519
-
520
-		$hooks = ['touch'];
521
-
522
-		if (!$this->file_exists($path)) {
523
-			$hooks[] = 'create';
524
-			$hooks[] = 'write';
525
-		}
526
-		try {
527
-			$result = $this->basicOperation('touch', $path, $hooks, $mtime);
528
-		} catch (\Exception $e) {
529
-			$this->logger->info('Error while setting modified time', ['app' => 'core', 'exception' => $e]);
530
-			$result = false;
531
-		}
532
-		if (!$result) {
533
-			// If create file fails because of permissions on external storage like SMB folders,
534
-			// check file exists and return false if not.
535
-			if (!$this->file_exists($path)) {
536
-				return false;
537
-			}
538
-			if (is_null($mtime)) {
539
-				$mtime = time();
540
-			}
541
-			//if native touch fails, we emulate it by changing the mtime in the cache
542
-			$this->putFileInfo($path, ['mtime' => floor($mtime)]);
543
-		}
544
-		return true;
545
-	}
546
-
547
-	/**
548
-	 * @param string $path
549
-	 * @return string|false
550
-	 * @throws LockedException
551
-	 */
552
-	public function file_get_contents($path) {
553
-		return $this->basicOperation('file_get_contents', $path, ['read']);
554
-	}
555
-
556
-	protected function emit_file_hooks_pre(bool $exists, string $path, bool &$run): void {
557
-		if (!$exists) {
558
-			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
559
-				Filesystem::signal_param_path => $this->getHookPath($path),
560
-				Filesystem::signal_param_run => &$run,
561
-			]);
562
-		} else {
563
-			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
564
-				Filesystem::signal_param_path => $this->getHookPath($path),
565
-				Filesystem::signal_param_run => &$run,
566
-			]);
567
-		}
568
-		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
569
-			Filesystem::signal_param_path => $this->getHookPath($path),
570
-			Filesystem::signal_param_run => &$run,
571
-		]);
572
-	}
573
-
574
-	protected function emit_file_hooks_post(bool $exists, string $path): void {
575
-		if (!$exists) {
576
-			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
577
-				Filesystem::signal_param_path => $this->getHookPath($path),
578
-			]);
579
-		} else {
580
-			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
581
-				Filesystem::signal_param_path => $this->getHookPath($path),
582
-			]);
583
-		}
584
-		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
585
-			Filesystem::signal_param_path => $this->getHookPath($path),
586
-		]);
587
-	}
588
-
589
-	/**
590
-	 * @param string $path
591
-	 * @param string|resource $data
592
-	 * @return bool|mixed
593
-	 * @throws LockedException
594
-	 */
595
-	public function file_put_contents($path, $data) {
596
-		if (is_resource($data)) { //not having to deal with streams in file_put_contents makes life easier
597
-			$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
598
-			if (Filesystem::isValidPath($path)
599
-				&& !Filesystem::isFileBlacklisted($path)
600
-			) {
601
-				$path = $this->getRelativePath($absolutePath);
602
-				if ($path === null) {
603
-					throw new InvalidPathException("Path $absolutePath is not in the expected root");
604
-				}
605
-
606
-				$this->lockFile($path, ILockingProvider::LOCK_SHARED);
607
-
608
-				$exists = $this->file_exists($path);
609
-				if ($this->shouldEmitHooks($path)) {
610
-					$run = true;
611
-					$this->emit_file_hooks_pre($exists, $path, $run);
612
-					if (!$run) {
613
-						$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
614
-						return false;
615
-					}
616
-				}
617
-
618
-				try {
619
-					$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
620
-				} catch (\Exception $e) {
621
-					// Release the shared lock before throwing.
622
-					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
623
-					throw $e;
624
-				}
625
-
626
-				/** @var Storage $storage */
627
-				[$storage, $internalPath] = $this->resolvePath($path);
628
-				$target = $storage->fopen($internalPath, 'w');
629
-				if ($target) {
630
-					[, $result] = Files::streamCopy($data, $target, true);
631
-					fclose($target);
632
-					fclose($data);
633
-
634
-					$this->writeUpdate($storage, $internalPath);
635
-
636
-					$this->changeLock($path, ILockingProvider::LOCK_SHARED);
637
-
638
-					if ($this->shouldEmitHooks($path) && $result !== false) {
639
-						$this->emit_file_hooks_post($exists, $path);
640
-					}
641
-					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
642
-					return $result;
643
-				} else {
644
-					$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
645
-					return false;
646
-				}
647
-			} else {
648
-				return false;
649
-			}
650
-		} else {
651
-			$hooks = $this->file_exists($path) ? ['update', 'write'] : ['create', 'write'];
652
-			return $this->basicOperation('file_put_contents', $path, $hooks, $data);
653
-		}
654
-	}
655
-
656
-	/**
657
-	 * @param string $path
658
-	 * @return bool|mixed
659
-	 */
660
-	public function unlink($path) {
661
-		if ($path === '' || $path === '/') {
662
-			// do not allow deleting the root
663
-			return false;
664
-		}
665
-		$postFix = (substr($path, -1) === '/') ? '/' : '';
666
-		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
667
-		$mount = Filesystem::getMountManager()->find($absolutePath . $postFix);
668
-		if ($mount->getInternalPath($absolutePath) === '') {
669
-			return $this->removeMount($mount, $absolutePath);
670
-		}
671
-		if ($this->is_dir($path)) {
672
-			$result = $this->basicOperation('rmdir', $path, ['delete']);
673
-		} else {
674
-			$result = $this->basicOperation('unlink', $path, ['delete']);
675
-		}
676
-		if (!$result && !$this->file_exists($path)) { //clear ghost files from the cache on delete
677
-			$storage = $mount->getStorage();
678
-			$internalPath = $mount->getInternalPath($absolutePath);
679
-			$storage->getUpdater()->remove($internalPath);
680
-			return true;
681
-		} else {
682
-			return $result;
683
-		}
684
-	}
685
-
686
-	/**
687
-	 * @param string $directory
688
-	 * @return bool|mixed
689
-	 */
690
-	public function deleteAll($directory) {
691
-		return $this->rmdir($directory);
692
-	}
693
-
694
-	/**
695
-	 * Rename/move a file or folder from the source path to target path.
696
-	 *
697
-	 * @param string $source source path
698
-	 * @param string $target target path
699
-	 * @param array $options
700
-	 *
701
-	 * @return bool|mixed
702
-	 * @throws LockedException
703
-	 */
704
-	public function rename($source, $target, array $options = []) {
705
-		$checkSubMounts = $options['checkSubMounts'] ?? true;
706
-
707
-		$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source));
708
-		$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
709
-
710
-		if (str_starts_with($absolutePath2, $absolutePath1 . '/')) {
711
-			throw new ForbiddenException('Moving a folder into a child folder is forbidden', false);
712
-		}
713
-
714
-		/** @var IMountManager $mountManager */
715
-		$mountManager = \OC::$server->get(IMountManager::class);
716
-
717
-		$targetParts = explode('/', $absolutePath2);
718
-		$targetUser = $targetParts[1] ?? null;
719
-		$result = false;
720
-		if (
721
-			Filesystem::isValidPath($target)
722
-			&& Filesystem::isValidPath($source)
723
-			&& !Filesystem::isFileBlacklisted($target)
724
-		) {
725
-			$source = $this->getRelativePath($absolutePath1);
726
-			$target = $this->getRelativePath($absolutePath2);
727
-			$exists = $this->file_exists($target);
728
-
729
-			if ($source == null || $target == null) {
730
-				return false;
731
-			}
732
-
733
-			try {
734
-				$this->verifyPath(dirname($target), basename($target));
735
-			} catch (InvalidPathException) {
736
-				return false;
737
-			}
738
-
739
-			$this->lockFile($source, ILockingProvider::LOCK_SHARED, true);
740
-			try {
741
-				$this->lockFile($target, ILockingProvider::LOCK_SHARED, true);
742
-
743
-				$run = true;
744
-				if ($this->shouldEmitHooks($source) && (Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target))) {
745
-					// if it was a rename from a part file to a regular file it was a write and not a rename operation
746
-					$this->emit_file_hooks_pre($exists, $target, $run);
747
-				} elseif ($this->shouldEmitHooks($source)) {
748
-					$sourcePath = $this->getHookPath($source);
749
-					$targetPath = $this->getHookPath($target);
750
-					if ($sourcePath !== null && $targetPath !== null) {
751
-						\OC_Hook::emit(
752
-							Filesystem::CLASSNAME, Filesystem::signal_rename,
753
-							[
754
-								Filesystem::signal_param_oldpath => $sourcePath,
755
-								Filesystem::signal_param_newpath => $targetPath,
756
-								Filesystem::signal_param_run => &$run
757
-							]
758
-						);
759
-					}
760
-				}
761
-				if ($run) {
762
-					$manager = Filesystem::getMountManager();
763
-					$mount1 = $this->getMount($source);
764
-					$mount2 = $this->getMount($target);
765
-					$storage1 = $mount1->getStorage();
766
-					$storage2 = $mount2->getStorage();
767
-					$internalPath1 = $mount1->getInternalPath($absolutePath1);
768
-					$internalPath2 = $mount2->getInternalPath($absolutePath2);
769
-
770
-					$this->changeLock($source, ILockingProvider::LOCK_EXCLUSIVE, true);
771
-					try {
772
-						$this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE, true);
773
-
774
-						if ($checkSubMounts) {
775
-							$movedMounts = $mountManager->findIn($this->getAbsolutePath($source));
776
-						} else {
777
-							$movedMounts = [];
778
-						}
779
-
780
-						if ($internalPath1 === '') {
781
-							$sourceParentMount = $this->getMount(dirname($source));
782
-							$movedMounts[] = $mount1;
783
-							$this->validateMountMove($movedMounts, $sourceParentMount, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2));
784
-							/**
785
-							 * @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
786
-							 */
787
-							$sourceMountPoint = $mount1->getMountPoint();
788
-							$result = $mount1->moveMount($absolutePath2);
789
-							$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
790
-
791
-							// moving a file/folder within the same mount point
792
-						} elseif ($storage1 === $storage2) {
793
-							if (count($movedMounts) > 0) {
794
-								$this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2));
795
-							}
796
-							if ($storage1) {
797
-								$result = $storage1->rename($internalPath1, $internalPath2);
798
-							} else {
799
-								$result = false;
800
-							}
801
-							// moving a file/folder between storages (from $storage1 to $storage2)
802
-						} else {
803
-							if (count($movedMounts) > 0) {
804
-								$this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2));
805
-							}
806
-							$result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
807
-						}
808
-
809
-						if ((Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target)) && $result !== false) {
810
-							// if it was a rename from a part file to a regular file it was a write and not a rename operation
811
-							$this->writeUpdate($storage2, $internalPath2);
812
-						} elseif ($result) {
813
-							if ($internalPath1 !== '') { // don't do a cache update for moved mounts
814
-								$this->renameUpdate($storage1, $storage2, $internalPath1, $internalPath2);
815
-							}
816
-						}
817
-					} catch (\Exception $e) {
818
-						throw $e;
819
-					} finally {
820
-						$this->changeLock($source, ILockingProvider::LOCK_SHARED, true);
821
-						$this->changeLock($target, ILockingProvider::LOCK_SHARED, true);
822
-					}
823
-
824
-					if ((Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target)) && $result !== false) {
825
-						if ($this->shouldEmitHooks()) {
826
-							$this->emit_file_hooks_post($exists, $target);
827
-						}
828
-					} elseif ($result) {
829
-						if ($this->shouldEmitHooks($source) && $this->shouldEmitHooks($target)) {
830
-							$sourcePath = $this->getHookPath($source);
831
-							$targetPath = $this->getHookPath($target);
832
-							if ($sourcePath !== null && $targetPath !== null) {
833
-								\OC_Hook::emit(
834
-									Filesystem::CLASSNAME,
835
-									Filesystem::signal_post_rename,
836
-									[
837
-										Filesystem::signal_param_oldpath => $sourcePath,
838
-										Filesystem::signal_param_newpath => $targetPath,
839
-									]
840
-								);
841
-							}
842
-						}
843
-					}
844
-				}
845
-			} catch (\Exception $e) {
846
-				throw $e;
847
-			} finally {
848
-				$this->unlockFile($source, ILockingProvider::LOCK_SHARED, true);
849
-				$this->unlockFile($target, ILockingProvider::LOCK_SHARED, true);
850
-			}
851
-		}
852
-		return $result;
853
-	}
854
-
855
-	/**
856
-	 * @throws ForbiddenException
857
-	 */
858
-	private function validateMountMove(array $mounts, IMountPoint $sourceMount, IMountPoint $targetMount, bool $targetIsShared): void {
859
-		$targetPath = $this->getRelativePath($targetMount->getMountPoint());
860
-		if ($targetPath) {
861
-			$targetPath = trim($targetPath, '/');
862
-		} else {
863
-			$targetPath = $targetMount->getMountPoint();
864
-		}
865
-
866
-		$l = \OC::$server->get(IFactory::class)->get('files');
867
-		foreach ($mounts as $mount) {
868
-			$sourcePath = $this->getRelativePath($mount->getMountPoint());
869
-			if ($sourcePath) {
870
-				$sourcePath = trim($sourcePath, '/');
871
-			} else {
872
-				$sourcePath = $mount->getMountPoint();
873
-			}
874
-
875
-			if (!$mount instanceof MoveableMount) {
876
-				throw new ForbiddenException($l->t('Storage %s cannot be moved', [$sourcePath]), false);
877
-			}
878
-
879
-			if ($targetIsShared) {
880
-				if ($sourceMount instanceof SharedMount) {
881
-					throw new ForbiddenException($l->t('Moving a share (%s) into a shared folder is not allowed', [$sourcePath]), false);
882
-				} else {
883
-					throw new ForbiddenException($l->t('Moving a storage (%s) into a shared folder is not allowed', [$sourcePath]), false);
884
-				}
885
-			}
886
-
887
-			if ($sourceMount !== $targetMount) {
888
-				if ($sourceMount instanceof SharedMount) {
889
-					if ($targetMount instanceof SharedMount) {
890
-						throw new ForbiddenException($l->t('Moving a share (%s) into another share (%s) is not allowed', [$sourcePath, $targetPath]), false);
891
-					} else {
892
-						throw new ForbiddenException($l->t('Moving a share (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false);
893
-					}
894
-				} else {
895
-					if ($targetMount instanceof SharedMount) {
896
-						throw new ForbiddenException($l->t('Moving a storage (%s) into a share (%s) is not allowed', [$sourcePath, $targetPath]), false);
897
-					} else {
898
-						throw new ForbiddenException($l->t('Moving a storage (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false);
899
-					}
900
-				}
901
-			}
902
-		}
903
-	}
904
-
905
-	/**
906
-	 * Copy a file/folder from the source path to target path
907
-	 *
908
-	 * @param string $source source path
909
-	 * @param string $target target path
910
-	 * @param bool $preserveMtime whether to preserve mtime on the copy
911
-	 *
912
-	 * @return bool|mixed
913
-	 */
914
-	public function copy($source, $target, $preserveMtime = false) {
915
-		$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source));
916
-		$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
917
-		$result = false;
918
-		if (
919
-			Filesystem::isValidPath($target)
920
-			&& Filesystem::isValidPath($source)
921
-			&& !Filesystem::isFileBlacklisted($target)
922
-		) {
923
-			$source = $this->getRelativePath($absolutePath1);
924
-			$target = $this->getRelativePath($absolutePath2);
925
-
926
-			if ($source == null || $target == null) {
927
-				return false;
928
-			}
929
-			$run = true;
930
-
931
-			$this->lockFile($target, ILockingProvider::LOCK_SHARED);
932
-			$this->lockFile($source, ILockingProvider::LOCK_SHARED);
933
-			$lockTypePath1 = ILockingProvider::LOCK_SHARED;
934
-			$lockTypePath2 = ILockingProvider::LOCK_SHARED;
935
-
936
-			try {
937
-				$exists = $this->file_exists($target);
938
-				if ($this->shouldEmitHooks($target)) {
939
-					\OC_Hook::emit(
940
-						Filesystem::CLASSNAME,
941
-						Filesystem::signal_copy,
942
-						[
943
-							Filesystem::signal_param_oldpath => $this->getHookPath($source),
944
-							Filesystem::signal_param_newpath => $this->getHookPath($target),
945
-							Filesystem::signal_param_run => &$run
946
-						]
947
-					);
948
-					$this->emit_file_hooks_pre($exists, $target, $run);
949
-				}
950
-				if ($run) {
951
-					$mount1 = $this->getMount($source);
952
-					$mount2 = $this->getMount($target);
953
-					$storage1 = $mount1->getStorage();
954
-					$internalPath1 = $mount1->getInternalPath($absolutePath1);
955
-					$storage2 = $mount2->getStorage();
956
-					$internalPath2 = $mount2->getInternalPath($absolutePath2);
957
-
958
-					$this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE);
959
-					$lockTypePath2 = ILockingProvider::LOCK_EXCLUSIVE;
960
-
961
-					if ($mount1->getMountPoint() == $mount2->getMountPoint()) {
962
-						if ($storage1) {
963
-							$result = $storage1->copy($internalPath1, $internalPath2);
964
-						} else {
965
-							$result = false;
966
-						}
967
-					} else {
968
-						$result = $storage2->copyFromStorage($storage1, $internalPath1, $internalPath2);
969
-					}
970
-
971
-					if ($result) {
972
-						$this->copyUpdate($storage1, $storage2, $internalPath1, $internalPath2);
973
-					}
974
-
975
-					$this->changeLock($target, ILockingProvider::LOCK_SHARED);
976
-					$lockTypePath2 = ILockingProvider::LOCK_SHARED;
977
-
978
-					if ($this->shouldEmitHooks($target) && $result !== false) {
979
-						\OC_Hook::emit(
980
-							Filesystem::CLASSNAME,
981
-							Filesystem::signal_post_copy,
982
-							[
983
-								Filesystem::signal_param_oldpath => $this->getHookPath($source),
984
-								Filesystem::signal_param_newpath => $this->getHookPath($target)
985
-							]
986
-						);
987
-						$this->emit_file_hooks_post($exists, $target);
988
-					}
989
-				}
990
-			} catch (\Exception $e) {
991
-				$this->unlockFile($target, $lockTypePath2);
992
-				$this->unlockFile($source, $lockTypePath1);
993
-				throw $e;
994
-			}
995
-
996
-			$this->unlockFile($target, $lockTypePath2);
997
-			$this->unlockFile($source, $lockTypePath1);
998
-		}
999
-		return $result;
1000
-	}
1001
-
1002
-	/**
1003
-	 * @param string $path
1004
-	 * @param string $mode 'r' or 'w'
1005
-	 * @return resource|false
1006
-	 * @throws LockedException
1007
-	 */
1008
-	public function fopen($path, $mode) {
1009
-		$mode = str_replace('b', '', $mode); // the binary flag is a windows only feature which we do not support
1010
-		$hooks = [];
1011
-		switch ($mode) {
1012
-			case 'r':
1013
-				$hooks[] = 'read';
1014
-				break;
1015
-			case 'r+':
1016
-			case 'w+':
1017
-			case 'x+':
1018
-			case 'a+':
1019
-				$hooks[] = 'read';
1020
-				$hooks[] = 'write';
1021
-				break;
1022
-			case 'w':
1023
-			case 'x':
1024
-			case 'a':
1025
-				$hooks[] = 'write';
1026
-				break;
1027
-			default:
1028
-				$this->logger->error('invalid mode (' . $mode . ') for ' . $path, ['app' => 'core']);
1029
-		}
1030
-
1031
-		if ($mode !== 'r' && $mode !== 'w') {
1032
-			$this->logger->info('Trying to open a file with a mode other than "r" or "w" can cause severe performance issues with some backends', ['app' => 'core']);
1033
-		}
1034
-
1035
-		$handle = $this->basicOperation('fopen', $path, $hooks, $mode);
1036
-		if (!is_resource($handle) && $mode === 'r') {
1037
-			// trying to read a file that isn't on disk, check if the cache is out of sync and rescan if needed
1038
-			$mount = $this->getMount($path);
1039
-			$internalPath = $mount->getInternalPath($this->getAbsolutePath($path));
1040
-			$storage = $mount->getStorage();
1041
-			if ($storage->getCache()->inCache($internalPath) && !$storage->file_exists($path)) {
1042
-				$this->writeUpdate($storage, $internalPath);
1043
-			}
1044
-		}
1045
-		return $handle;
1046
-	}
1047
-
1048
-	/**
1049
-	 * @param string $path
1050
-	 * @throws InvalidPathException
1051
-	 */
1052
-	public function toTmpFile($path): string|false {
1053
-		$this->assertPathLength($path);
1054
-		if (Filesystem::isValidPath($path)) {
1055
-			$source = $this->fopen($path, 'r');
1056
-			if ($source) {
1057
-				$extension = pathinfo($path, PATHINFO_EXTENSION);
1058
-				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($extension);
1059
-				file_put_contents($tmpFile, $source);
1060
-				return $tmpFile;
1061
-			} else {
1062
-				return false;
1063
-			}
1064
-		} else {
1065
-			return false;
1066
-		}
1067
-	}
1068
-
1069
-	/**
1070
-	 * @param string $tmpFile
1071
-	 * @param string $path
1072
-	 * @return bool|mixed
1073
-	 * @throws InvalidPathException
1074
-	 */
1075
-	public function fromTmpFile($tmpFile, $path) {
1076
-		$this->assertPathLength($path);
1077
-		if (Filesystem::isValidPath($path)) {
1078
-			// Get directory that the file is going into
1079
-			$filePath = dirname($path);
1080
-
1081
-			// Create the directories if any
1082
-			if (!$this->file_exists($filePath)) {
1083
-				$result = $this->createParentDirectories($filePath);
1084
-				if ($result === false) {
1085
-					return false;
1086
-				}
1087
-			}
1088
-
1089
-			$source = fopen($tmpFile, 'r');
1090
-			if ($source) {
1091
-				$result = $this->file_put_contents($path, $source);
1092
-				/**
1093
-				 * $this->file_put_contents() might have already closed
1094
-				 * the resource, so we check it, before trying to close it
1095
-				 * to avoid messages in the error log.
1096
-				 * @psalm-suppress RedundantCondition false-positive
1097
-				 */
1098
-				if (is_resource($source)) {
1099
-					fclose($source);
1100
-				}
1101
-				unlink($tmpFile);
1102
-				return $result;
1103
-			} else {
1104
-				return false;
1105
-			}
1106
-		} else {
1107
-			return false;
1108
-		}
1109
-	}
1110
-
1111
-
1112
-	/**
1113
-	 * @param string $path
1114
-	 * @return mixed
1115
-	 * @throws InvalidPathException
1116
-	 */
1117
-	public function getMimeType($path) {
1118
-		$this->assertPathLength($path);
1119
-		return $this->basicOperation('getMimeType', $path);
1120
-	}
1121
-
1122
-	/**
1123
-	 * @param string $type
1124
-	 * @param string $path
1125
-	 * @param bool $raw
1126
-	 */
1127
-	public function hash($type, $path, $raw = false): string|bool {
1128
-		$postFix = (substr($path, -1) === '/') ? '/' : '';
1129
-		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
1130
-		if (Filesystem::isValidPath($path)) {
1131
-			$path = $this->getRelativePath($absolutePath);
1132
-			if ($path == null) {
1133
-				return false;
1134
-			}
1135
-			if ($this->shouldEmitHooks($path)) {
1136
-				\OC_Hook::emit(
1137
-					Filesystem::CLASSNAME,
1138
-					Filesystem::signal_read,
1139
-					[Filesystem::signal_param_path => $this->getHookPath($path)]
1140
-				);
1141
-			}
1142
-			/** @var Storage|null $storage */
1143
-			[$storage, $internalPath] = Filesystem::resolvePath($absolutePath . $postFix);
1144
-			if ($storage) {
1145
-				return $storage->hash($type, $internalPath, $raw);
1146
-			}
1147
-		}
1148
-		return false;
1149
-	}
1150
-
1151
-	/**
1152
-	 * @param string $path
1153
-	 * @return mixed
1154
-	 * @throws InvalidPathException
1155
-	 */
1156
-	public function free_space($path = '/') {
1157
-		$this->assertPathLength($path);
1158
-		$result = $this->basicOperation('free_space', $path);
1159
-		if ($result === null) {
1160
-			throw new InvalidPathException();
1161
-		}
1162
-		return $result;
1163
-	}
1164
-
1165
-	/**
1166
-	 * abstraction layer for basic filesystem functions: wrapper for \OC\Files\Storage\Storage
1167
-	 *
1168
-	 * @param mixed $extraParam (optional)
1169
-	 * @return mixed
1170
-	 * @throws LockedException
1171
-	 *
1172
-	 * This method takes requests for basic filesystem functions (e.g. reading & writing
1173
-	 * files), processes hooks and proxies, sanitises paths, and finally passes them on to
1174
-	 * \OC\Files\Storage\Storage for delegation to a storage backend for execution
1175
-	 */
1176
-	private function basicOperation(string $operation, string $path, array $hooks = [], $extraParam = null) {
1177
-		$postFix = (substr($path, -1) === '/') ? '/' : '';
1178
-		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
1179
-		if (Filesystem::isValidPath($path)
1180
-			&& !Filesystem::isFileBlacklisted($path)
1181
-		) {
1182
-			$path = $this->getRelativePath($absolutePath);
1183
-			if ($path == null) {
1184
-				return false;
1185
-			}
1186
-
1187
-			if (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks)) {
1188
-				// always a shared lock during pre-hooks so the hook can read the file
1189
-				$this->lockFile($path, ILockingProvider::LOCK_SHARED);
1190
-			}
1191
-
1192
-			$run = $this->runHooks($hooks, $path);
1193
-			[$storage, $internalPath] = Filesystem::resolvePath($absolutePath . $postFix);
1194
-			if ($run && $storage) {
1195
-				/** @var Storage $storage */
1196
-				if (in_array('write', $hooks) || in_array('delete', $hooks)) {
1197
-					try {
1198
-						$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
1199
-					} catch (LockedException $e) {
1200
-						// release the shared lock we acquired before quitting
1201
-						$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1202
-						throw $e;
1203
-					}
1204
-				}
1205
-				try {
1206
-					if (!is_null($extraParam)) {
1207
-						$result = $storage->$operation($internalPath, $extraParam);
1208
-					} else {
1209
-						$result = $storage->$operation($internalPath);
1210
-					}
1211
-				} catch (\Exception $e) {
1212
-					if (in_array('write', $hooks) || in_array('delete', $hooks)) {
1213
-						$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
1214
-					} elseif (in_array('read', $hooks)) {
1215
-						$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1216
-					}
1217
-					throw $e;
1218
-				}
1219
-
1220
-				if ($result !== false && in_array('delete', $hooks)) {
1221
-					$this->removeUpdate($storage, $internalPath);
1222
-				}
1223
-				if ($result !== false && in_array('write', $hooks, true) && $operation !== 'fopen' && $operation !== 'touch') {
1224
-					$isCreateOperation = $operation === 'mkdir' || ($operation === 'file_put_contents' && in_array('create', $hooks, true));
1225
-					$sizeDifference = $operation === 'mkdir' ? 0 : $result;
1226
-					$this->writeUpdate($storage, $internalPath, null, $isCreateOperation ? $sizeDifference : null);
1227
-				}
1228
-				if ($result !== false && in_array('touch', $hooks)) {
1229
-					$this->writeUpdate($storage, $internalPath, $extraParam, 0);
1230
-				}
1231
-
1232
-				if ((in_array('write', $hooks) || in_array('delete', $hooks)) && ($operation !== 'fopen' || $result === false)) {
1233
-					$this->changeLock($path, ILockingProvider::LOCK_SHARED);
1234
-				}
1235
-
1236
-				$unlockLater = false;
1237
-				if ($this->lockingEnabled && $operation === 'fopen' && is_resource($result)) {
1238
-					$unlockLater = true;
1239
-					// make sure our unlocking callback will still be called if connection is aborted
1240
-					ignore_user_abort(true);
1241
-					$result = CallbackWrapper::wrap($result, null, null, function () use ($hooks, $path) {
1242
-						if (in_array('write', $hooks)) {
1243
-							$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
1244
-						} elseif (in_array('read', $hooks)) {
1245
-							$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1246
-						}
1247
-					});
1248
-				}
1249
-
1250
-				if ($this->shouldEmitHooks($path) && $result !== false) {
1251
-					if ($operation != 'fopen') { //no post hooks for fopen, the file stream is still open
1252
-						$this->runHooks($hooks, $path, true);
1253
-					}
1254
-				}
1255
-
1256
-				if (!$unlockLater
1257
-					&& (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks))
1258
-				) {
1259
-					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1260
-				}
1261
-				return $result;
1262
-			} else {
1263
-				$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1264
-			}
1265
-		}
1266
-		return null;
1267
-	}
1268
-
1269
-	/**
1270
-	 * get the path relative to the default root for hook usage
1271
-	 *
1272
-	 * @param string $path
1273
-	 * @return ?string
1274
-	 */
1275
-	private function getHookPath($path): ?string {
1276
-		$view = Filesystem::getView();
1277
-		if (!$view) {
1278
-			return $path;
1279
-		}
1280
-		return $view->getRelativePath($this->getAbsolutePath($path));
1281
-	}
1282
-
1283
-	private function shouldEmitHooks(string $path = ''): bool {
1284
-		if ($path && Cache\Scanner::isPartialFile($path)) {
1285
-			return false;
1286
-		}
1287
-		if (!Filesystem::$loaded) {
1288
-			return false;
1289
-		}
1290
-		$defaultRoot = Filesystem::getRoot();
1291
-		if ($defaultRoot === null) {
1292
-			return false;
1293
-		}
1294
-		if ($this->fakeRoot === $defaultRoot) {
1295
-			return true;
1296
-		}
1297
-		$fullPath = $this->getAbsolutePath($path);
1298
-
1299
-		if ($fullPath === $defaultRoot) {
1300
-			return true;
1301
-		}
1302
-
1303
-		return (strlen($fullPath) > strlen($defaultRoot)) && (substr($fullPath, 0, strlen($defaultRoot) + 1) === $defaultRoot . '/');
1304
-	}
1305
-
1306
-	/**
1307
-	 * @param string[] $hooks
1308
-	 * @param string $path
1309
-	 * @param bool $post
1310
-	 * @return bool
1311
-	 */
1312
-	private function runHooks($hooks, $path, $post = false) {
1313
-		$relativePath = $path;
1314
-		$path = $this->getHookPath($path);
1315
-		$prefix = $post ? 'post_' : '';
1316
-		$run = true;
1317
-		if ($this->shouldEmitHooks($relativePath)) {
1318
-			foreach ($hooks as $hook) {
1319
-				if ($hook != 'read') {
1320
-					\OC_Hook::emit(
1321
-						Filesystem::CLASSNAME,
1322
-						$prefix . $hook,
1323
-						[
1324
-							Filesystem::signal_param_run => &$run,
1325
-							Filesystem::signal_param_path => $path
1326
-						]
1327
-					);
1328
-				} elseif (!$post) {
1329
-					\OC_Hook::emit(
1330
-						Filesystem::CLASSNAME,
1331
-						$prefix . $hook,
1332
-						[
1333
-							Filesystem::signal_param_path => $path
1334
-						]
1335
-					);
1336
-				}
1337
-			}
1338
-		}
1339
-		return $run;
1340
-	}
1341
-
1342
-	/**
1343
-	 * check if a file or folder has been updated since $time
1344
-	 *
1345
-	 * @param string $path
1346
-	 * @param int $time
1347
-	 * @return bool
1348
-	 */
1349
-	public function hasUpdated($path, $time) {
1350
-		return $this->basicOperation('hasUpdated', $path, [], $time);
1351
-	}
1352
-
1353
-	/**
1354
-	 * @param string $ownerId
1355
-	 * @return IUser
1356
-	 */
1357
-	private function getUserObjectForOwner(string $ownerId) {
1358
-		return new LazyUser($ownerId, $this->userManager);
1359
-	}
1360
-
1361
-	/**
1362
-	 * Get file info from cache
1363
-	 *
1364
-	 * If the file is not in cached it will be scanned
1365
-	 * If the file has changed on storage the cache will be updated
1366
-	 *
1367
-	 * @param Storage $storage
1368
-	 * @param string $internalPath
1369
-	 * @param string $relativePath
1370
-	 * @return ICacheEntry|bool
1371
-	 */
1372
-	private function getCacheEntry($storage, $internalPath, $relativePath) {
1373
-		$cache = $storage->getCache($internalPath);
1374
-		$data = $cache->get($internalPath);
1375
-		$watcher = $storage->getWatcher($internalPath);
1376
-
1377
-		try {
1378
-			// if the file is not in the cache or needs to be updated, trigger the scanner and reload the data
1379
-			if (!$data || (isset($data['size']) && $data['size'] === -1)) {
1380
-				if (!$storage->file_exists($internalPath)) {
1381
-					return false;
1382
-				}
1383
-				// don't need to get a lock here since the scanner does it's own locking
1384
-				$scanner = $storage->getScanner($internalPath);
1385
-				$scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW);
1386
-				$data = $cache->get($internalPath);
1387
-			} elseif (!Cache\Scanner::isPartialFile($internalPath) && $watcher->needsUpdate($internalPath, $data)) {
1388
-				$this->lockFile($relativePath, ILockingProvider::LOCK_SHARED);
1389
-				$watcher->update($internalPath, $data);
1390
-				$storage->getPropagator()->propagateChange($internalPath, time());
1391
-				$data = $cache->get($internalPath);
1392
-				$this->unlockFile($relativePath, ILockingProvider::LOCK_SHARED);
1393
-			}
1394
-		} catch (LockedException $e) {
1395
-			// if the file is locked we just use the old cache info
1396
-		}
1397
-
1398
-		return $data;
1399
-	}
1400
-
1401
-	/**
1402
-	 * get the filesystem info
1403
-	 *
1404
-	 * @param string $path
1405
-	 * @param bool|string $includeMountPoints true to add mountpoint sizes,
1406
-	 *                                        'ext' to add only ext storage mount point sizes. Defaults to true.
1407
-	 * @return \OC\Files\FileInfo|false False if file does not exist
1408
-	 */
1409
-	public function getFileInfo($path, $includeMountPoints = true) {
1410
-		$this->assertPathLength($path);
1411
-		if (!Filesystem::isValidPath($path)) {
1412
-			return false;
1413
-		}
1414
-		$relativePath = $path;
1415
-		$path = Filesystem::normalizePath($this->fakeRoot . '/' . $path);
1416
-
1417
-		$mount = Filesystem::getMountManager()->find($path);
1418
-		$storage = $mount->getStorage();
1419
-		$internalPath = $mount->getInternalPath($path);
1420
-		if ($storage) {
1421
-			$data = $this->getCacheEntry($storage, $internalPath, $relativePath);
1422
-
1423
-			if (!$data instanceof ICacheEntry) {
1424
-				if (Cache\Scanner::isPartialFile($relativePath)) {
1425
-					return $this->getPartFileInfo($relativePath);
1426
-				}
1427
-
1428
-				return false;
1429
-			}
1430
-
1431
-			if ($mount instanceof MoveableMount && $internalPath === '') {
1432
-				$data['permissions'] |= \OCP\Constants::PERMISSION_DELETE;
1433
-			}
1434
-			if ($internalPath === '' && $data['name']) {
1435
-				$data['name'] = basename($path);
1436
-			}
1437
-
1438
-			$ownerId = $storage->getOwner($internalPath);
1439
-			$owner = null;
1440
-			if ($ownerId !== false) {
1441
-				// ownerId might be null if files are accessed with an access token without file system access
1442
-				$owner = $this->getUserObjectForOwner($ownerId);
1443
-			}
1444
-			$info = new FileInfo($path, $storage, $internalPath, $data, $mount, $owner);
1445
-
1446
-			if (isset($data['fileid'])) {
1447
-				if ($includeMountPoints && $data['mimetype'] === 'httpd/unix-directory') {
1448
-					//add the sizes of other mount points to the folder
1449
-					$extOnly = ($includeMountPoints === 'ext');
1450
-					$this->addSubMounts($info, $extOnly);
1451
-				}
1452
-			}
1453
-
1454
-			return $info;
1455
-		} else {
1456
-			$this->logger->warning('Storage not valid for mountpoint: ' . $mount->getMountPoint(), ['app' => 'core']);
1457
-		}
1458
-
1459
-		return false;
1460
-	}
1461
-
1462
-	/**
1463
-	 * Extend a FileInfo that was previously requested with `$includeMountPoints = false` to include the sub mounts
1464
-	 */
1465
-	public function addSubMounts(FileInfo $info, $extOnly = false): void {
1466
-		$mounts = Filesystem::getMountManager()->findIn($info->getPath());
1467
-		$info->setSubMounts(array_filter($mounts, function (IMountPoint $mount) use ($extOnly) {
1468
-			return !($extOnly && $mount instanceof SharedMount);
1469
-		}));
1470
-	}
1471
-
1472
-	/**
1473
-	 * get the content of a directory
1474
-	 *
1475
-	 * @param string $directory path under datadirectory
1476
-	 * @param string $mimetype_filter limit returned content to this mimetype or mimepart
1477
-	 * @return FileInfo[]
1478
-	 */
1479
-	public function getDirectoryContent($directory, $mimetype_filter = '', ?\OCP\Files\FileInfo $directoryInfo = null) {
1480
-		$this->assertPathLength($directory);
1481
-		if (!Filesystem::isValidPath($directory)) {
1482
-			return [];
1483
-		}
1484
-
1485
-		$path = $this->getAbsolutePath($directory);
1486
-		$path = Filesystem::normalizePath($path);
1487
-		$mount = $this->getMount($directory);
1488
-		$storage = $mount->getStorage();
1489
-		$internalPath = $mount->getInternalPath($path);
1490
-		if (!$storage) {
1491
-			return [];
1492
-		}
1493
-
1494
-		$cache = $storage->getCache($internalPath);
1495
-		$user = \OC_User::getUser();
1496
-
1497
-		if (!$directoryInfo) {
1498
-			$data = $this->getCacheEntry($storage, $internalPath, $directory);
1499
-			if (!$data instanceof ICacheEntry || !isset($data['fileid'])) {
1500
-				return [];
1501
-			}
1502
-		} else {
1503
-			$data = $directoryInfo;
1504
-		}
1505
-
1506
-		if (!($data->getPermissions() & Constants::PERMISSION_READ)) {
1507
-			return [];
1508
-		}
1509
-
1510
-		$folderId = $data->getId();
1511
-		$contents = $cache->getFolderContentsById($folderId); //TODO: mimetype_filter
1512
-
1513
-		$sharingDisabled = \OCP\Util::isSharingDisabledForUser();
1514
-
1515
-		$fileNames = array_map(function (ICacheEntry $content) {
1516
-			return $content->getName();
1517
-		}, $contents);
1518
-		/**
1519
-		 * @var \OC\Files\FileInfo[] $fileInfos
1520
-		 */
1521
-		$fileInfos = array_map(function (ICacheEntry $content) use ($path, $storage, $mount, $sharingDisabled) {
1522
-			if ($sharingDisabled) {
1523
-				$content['permissions'] = $content['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
1524
-			}
1525
-			$ownerId = $storage->getOwner($content['path']);
1526
-			if ($ownerId !== false) {
1527
-				$owner = $this->getUserObjectForOwner($ownerId);
1528
-			} else {
1529
-				$owner = null;
1530
-			}
1531
-			return new FileInfo($path . '/' . $content['name'], $storage, $content['path'], $content, $mount, $owner);
1532
-		}, $contents);
1533
-		$files = array_combine($fileNames, $fileInfos);
1534
-
1535
-		//add a folder for any mountpoint in this directory and add the sizes of other mountpoints to the folders
1536
-		$mounts = Filesystem::getMountManager()->findIn($path);
1537
-
1538
-		// make sure nested mounts are sorted after their parent mounts
1539
-		// otherwise doesn't propagate the etag across storage boundaries correctly
1540
-		usort($mounts, function (IMountPoint $a, IMountPoint $b) {
1541
-			return $a->getMountPoint() <=> $b->getMountPoint();
1542
-		});
1543
-
1544
-		$dirLength = strlen($path);
1545
-		foreach ($mounts as $mount) {
1546
-			$mountPoint = $mount->getMountPoint();
1547
-			$subStorage = $mount->getStorage();
1548
-			if ($subStorage) {
1549
-				$subCache = $subStorage->getCache('');
1550
-
1551
-				$rootEntry = $subCache->get('');
1552
-				if (!$rootEntry) {
1553
-					$subScanner = $subStorage->getScanner();
1554
-					try {
1555
-						$subScanner->scanFile('');
1556
-					} catch (\OCP\Files\StorageNotAvailableException $e) {
1557
-						continue;
1558
-					} catch (\OCP\Files\StorageInvalidException $e) {
1559
-						continue;
1560
-					} catch (\Exception $e) {
1561
-						// sometimes when the storage is not available it can be any exception
1562
-						$this->logger->error('Exception while scanning storage "' . $subStorage->getId() . '"', [
1563
-							'exception' => $e,
1564
-							'app' => 'core',
1565
-						]);
1566
-						continue;
1567
-					}
1568
-					$rootEntry = $subCache->get('');
1569
-				}
1570
-
1571
-				if ($rootEntry && ($rootEntry->getPermissions() & Constants::PERMISSION_READ)) {
1572
-					$relativePath = trim(substr($mountPoint, $dirLength), '/');
1573
-					if ($pos = strpos($relativePath, '/')) {
1574
-						//mountpoint inside subfolder add size to the correct folder
1575
-						$entryName = substr($relativePath, 0, $pos);
1576
-
1577
-						// Create parent folders if the mountpoint is inside a subfolder that doesn't exist yet
1578
-						if (!isset($files[$entryName])) {
1579
-							try {
1580
-								[$storage, ] = $this->resolvePath($path . '/' . $entryName);
1581
-								// make sure we can create the mountpoint folder, even if the user has a quota of 0
1582
-								if ($storage->instanceOfStorage(Quota::class)) {
1583
-									$storage->enableQuota(false);
1584
-								}
1585
-
1586
-								if ($this->mkdir($path . '/' . $entryName) !== false) {
1587
-									$info = $this->getFileInfo($path . '/' . $entryName);
1588
-									if ($info !== false) {
1589
-										$files[$entryName] = $info;
1590
-									}
1591
-								}
1592
-
1593
-								if ($storage->instanceOfStorage(Quota::class)) {
1594
-									$storage->enableQuota(true);
1595
-								}
1596
-							} catch (\Exception $e) {
1597
-								// Creating the parent folder might not be possible, for example due to a lack of permissions.
1598
-								$this->logger->debug('Failed to create non-existent parent', ['exception' => $e, 'path' => $path . '/' . $entryName]);
1599
-							}
1600
-						}
1601
-
1602
-						if (isset($files[$entryName])) {
1603
-							$files[$entryName]->addSubEntry($rootEntry, $mountPoint);
1604
-						}
1605
-					} else { //mountpoint in this folder, add an entry for it
1606
-						$rootEntry['name'] = $relativePath;
1607
-						$rootEntry['type'] = $rootEntry['mimetype'] === 'httpd/unix-directory' ? 'dir' : 'file';
1608
-						$permissions = $rootEntry['permissions'];
1609
-						// do not allow renaming/deleting the mount point if they are not shared files/folders
1610
-						// for shared files/folders we use the permissions given by the owner
1611
-						if ($mount instanceof MoveableMount) {
1612
-							$rootEntry['permissions'] = $permissions | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE;
1613
-						} else {
1614
-							$rootEntry['permissions'] = $permissions & (\OCP\Constants::PERMISSION_ALL - (\OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE));
1615
-						}
1616
-
1617
-						$rootEntry['path'] = substr(Filesystem::normalizePath($path . '/' . $rootEntry['name']), strlen($user) + 2); // full path without /$user/
1618
-
1619
-						// if sharing was disabled for the user we remove the share permissions
1620
-						if ($sharingDisabled) {
1621
-							$rootEntry['permissions'] = $rootEntry['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
1622
-						}
1623
-
1624
-						$ownerId = $subStorage->getOwner('');
1625
-						if ($ownerId !== false) {
1626
-							$owner = $this->getUserObjectForOwner($ownerId);
1627
-						} else {
1628
-							$owner = null;
1629
-						}
1630
-						$files[$rootEntry->getName()] = new FileInfo($path . '/' . $rootEntry['name'], $subStorage, '', $rootEntry, $mount, $owner);
1631
-					}
1632
-				}
1633
-			}
1634
-		}
1635
-
1636
-		if ($mimetype_filter) {
1637
-			$files = array_filter($files, function (FileInfo $file) use ($mimetype_filter) {
1638
-				if (strpos($mimetype_filter, '/')) {
1639
-					return $file->getMimetype() === $mimetype_filter;
1640
-				} else {
1641
-					return $file->getMimePart() === $mimetype_filter;
1642
-				}
1643
-			});
1644
-		}
1645
-
1646
-		return array_values($files);
1647
-	}
1648
-
1649
-	/**
1650
-	 * change file metadata
1651
-	 *
1652
-	 * @param string $path
1653
-	 * @param array|\OCP\Files\FileInfo $data
1654
-	 * @return int
1655
-	 *
1656
-	 * returns the fileid of the updated file
1657
-	 */
1658
-	public function putFileInfo($path, $data) {
1659
-		$this->assertPathLength($path);
1660
-		if ($data instanceof FileInfo) {
1661
-			$data = $data->getData();
1662
-		}
1663
-		$path = Filesystem::normalizePath($this->fakeRoot . '/' . $path);
1664
-		/**
1665
-		 * @var Storage $storage
1666
-		 * @var string $internalPath
1667
-		 */
1668
-		[$storage, $internalPath] = Filesystem::resolvePath($path);
1669
-		if ($storage) {
1670
-			$cache = $storage->getCache($path);
1671
-
1672
-			if (!$cache->inCache($internalPath)) {
1673
-				$scanner = $storage->getScanner($internalPath);
1674
-				$scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW);
1675
-			}
1676
-
1677
-			return $cache->put($internalPath, $data);
1678
-		} else {
1679
-			return -1;
1680
-		}
1681
-	}
1682
-
1683
-	/**
1684
-	 * search for files with the name matching $query
1685
-	 *
1686
-	 * @param string $query
1687
-	 * @return FileInfo[]
1688
-	 */
1689
-	public function search($query) {
1690
-		return $this->searchCommon('search', ['%' . $query . '%']);
1691
-	}
1692
-
1693
-	/**
1694
-	 * search for files with the name matching $query
1695
-	 *
1696
-	 * @param string $query
1697
-	 * @return FileInfo[]
1698
-	 */
1699
-	public function searchRaw($query) {
1700
-		return $this->searchCommon('search', [$query]);
1701
-	}
1702
-
1703
-	/**
1704
-	 * search for files by mimetype
1705
-	 *
1706
-	 * @param string $mimetype
1707
-	 * @return FileInfo[]
1708
-	 */
1709
-	public function searchByMime($mimetype) {
1710
-		return $this->searchCommon('searchByMime', [$mimetype]);
1711
-	}
1712
-
1713
-	/**
1714
-	 * search for files by tag
1715
-	 *
1716
-	 * @param string|int $tag name or tag id
1717
-	 * @param string $userId owner of the tags
1718
-	 * @return FileInfo[]
1719
-	 */
1720
-	public function searchByTag($tag, $userId) {
1721
-		return $this->searchCommon('searchByTag', [$tag, $userId]);
1722
-	}
1723
-
1724
-	/**
1725
-	 * @param string $method cache method
1726
-	 * @param array $args
1727
-	 * @return FileInfo[]
1728
-	 */
1729
-	private function searchCommon($method, $args) {
1730
-		$files = [];
1731
-		$rootLength = strlen($this->fakeRoot);
1732
-
1733
-		$mount = $this->getMount('');
1734
-		$mountPoint = $mount->getMountPoint();
1735
-		$storage = $mount->getStorage();
1736
-		$userManager = \OC::$server->getUserManager();
1737
-		if ($storage) {
1738
-			$cache = $storage->getCache('');
1739
-
1740
-			$results = call_user_func_array([$cache, $method], $args);
1741
-			foreach ($results as $result) {
1742
-				if (substr($mountPoint . $result['path'], 0, $rootLength + 1) === $this->fakeRoot . '/') {
1743
-					$internalPath = $result['path'];
1744
-					$path = $mountPoint . $result['path'];
1745
-					$result['path'] = substr($mountPoint . $result['path'], $rootLength);
1746
-					$ownerId = $storage->getOwner($internalPath);
1747
-					if ($ownerId !== false) {
1748
-						$owner = $userManager->get($ownerId);
1749
-					} else {
1750
-						$owner = null;
1751
-					}
1752
-					$files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner);
1753
-				}
1754
-			}
1755
-
1756
-			$mounts = Filesystem::getMountManager()->findIn($this->fakeRoot);
1757
-			foreach ($mounts as $mount) {
1758
-				$mountPoint = $mount->getMountPoint();
1759
-				$storage = $mount->getStorage();
1760
-				if ($storage) {
1761
-					$cache = $storage->getCache('');
1762
-
1763
-					$relativeMountPoint = substr($mountPoint, $rootLength);
1764
-					$results = call_user_func_array([$cache, $method], $args);
1765
-					if ($results) {
1766
-						foreach ($results as $result) {
1767
-							$internalPath = $result['path'];
1768
-							$result['path'] = rtrim($relativeMountPoint . $result['path'], '/');
1769
-							$path = rtrim($mountPoint . $internalPath, '/');
1770
-							$ownerId = $storage->getOwner($internalPath);
1771
-							if ($ownerId !== false) {
1772
-								$owner = $userManager->get($ownerId);
1773
-							} else {
1774
-								$owner = null;
1775
-							}
1776
-							$files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner);
1777
-						}
1778
-					}
1779
-				}
1780
-			}
1781
-		}
1782
-		return $files;
1783
-	}
1784
-
1785
-	/**
1786
-	 * Get the owner for a file or folder
1787
-	 *
1788
-	 * @throws NotFoundException
1789
-	 */
1790
-	public function getOwner(string $path): string {
1791
-		$info = $this->getFileInfo($path);
1792
-		if (!$info) {
1793
-			throw new NotFoundException($path . ' not found while trying to get owner');
1794
-		}
1795
-
1796
-		if ($info->getOwner() === null) {
1797
-			throw new NotFoundException($path . ' has no owner');
1798
-		}
1799
-
1800
-		return $info->getOwner()->getUID();
1801
-	}
1802
-
1803
-	/**
1804
-	 * get the ETag for a file or folder
1805
-	 *
1806
-	 * @param string $path
1807
-	 * @return string|false
1808
-	 */
1809
-	public function getETag($path) {
1810
-		[$storage, $internalPath] = $this->resolvePath($path);
1811
-		if ($storage) {
1812
-			return $storage->getETag($internalPath);
1813
-		} else {
1814
-			return false;
1815
-		}
1816
-	}
1817
-
1818
-	/**
1819
-	 * Get the path of a file by id, relative to the view
1820
-	 *
1821
-	 * Note that the resulting path is not guaranteed to be unique for the id, multiple paths can point to the same file
1822
-	 *
1823
-	 * @param int $id
1824
-	 * @param int|null $storageId
1825
-	 * @return string
1826
-	 * @throws NotFoundException
1827
-	 */
1828
-	public function getPath($id, ?int $storageId = null): string {
1829
-		$id = (int)$id;
1830
-		$rootFolder = Server::get(Files\IRootFolder::class);
1831
-
1832
-		$node = $rootFolder->getFirstNodeByIdInPath($id, $this->getRoot());
1833
-		if ($node) {
1834
-			if ($storageId === null || $storageId === $node->getStorage()->getCache()->getNumericStorageId()) {
1835
-				return $this->getRelativePath($node->getPath()) ?? '';
1836
-			}
1837
-		} else {
1838
-			throw new NotFoundException(sprintf('File with id "%s" has not been found.', $id));
1839
-		}
1840
-
1841
-		foreach ($rootFolder->getByIdInPath($id, $this->getRoot()) as $node) {
1842
-			if ($storageId === $node->getStorage()->getCache()->getNumericStorageId()) {
1843
-				return $this->getRelativePath($node->getPath()) ?? '';
1844
-			}
1845
-		}
1846
-
1847
-		throw new NotFoundException(sprintf('File with id "%s" has not been found.', $id));
1848
-	}
1849
-
1850
-	/**
1851
-	 * @param string $path
1852
-	 * @throws InvalidPathException
1853
-	 */
1854
-	private function assertPathLength($path): void {
1855
-		$maxLen = min(PHP_MAXPATHLEN, 4000);
1856
-		// Check for the string length - performed using isset() instead of strlen()
1857
-		// because isset() is about 5x-40x faster.
1858
-		if (isset($path[$maxLen])) {
1859
-			$pathLen = strlen($path);
1860
-			throw new InvalidPathException("Path length($pathLen) exceeds max path length($maxLen): $path");
1861
-		}
1862
-	}
1863
-
1864
-	/**
1865
-	 * check if it is allowed to move a mount point to a given target.
1866
-	 * It is not allowed to move a mount point into a different mount point or
1867
-	 * into an already shared folder
1868
-	 */
1869
-	private function targetIsNotShared(string $user, string $targetPath): bool {
1870
-		$providers = [
1871
-			IShare::TYPE_USER,
1872
-			IShare::TYPE_GROUP,
1873
-			IShare::TYPE_EMAIL,
1874
-			IShare::TYPE_CIRCLE,
1875
-			IShare::TYPE_ROOM,
1876
-			IShare::TYPE_DECK,
1877
-		];
1878
-		$shareManager = Server::get(IManager::class);
1879
-		/** @var IShare[] $shares */
1880
-		$shares = array_merge(...array_map(function (int $type) use ($shareManager, $user) {
1881
-			return $shareManager->getSharesBy($user, $type);
1882
-		}, $providers));
1883
-
1884
-		foreach ($shares as $share) {
1885
-			try {
1886
-				$sharedPath = $share->getNode()->getPath();
1887
-			} catch (NotFoundException $e) {
1888
-				// node is not found, ignoring
1889
-				$this->logger->debug(
1890
-					'Could not find the node linked to a share',
1891
-					['app' => 'files', 'exception' => $e]);
1892
-				continue;
1893
-			}
1894
-			if ($targetPath === $sharedPath || str_starts_with($targetPath, $sharedPath . '/')) {
1895
-				$this->logger->debug(
1896
-					'It is not allowed to move one mount point into a shared folder',
1897
-					['app' => 'files']);
1898
-				return false;
1899
-			}
1900
-		}
1901
-
1902
-		return true;
1903
-	}
1904
-
1905
-	/**
1906
-	 * Get a fileinfo object for files that are ignored in the cache (part files)
1907
-	 */
1908
-	private function getPartFileInfo(string $path): \OC\Files\FileInfo {
1909
-		$mount = $this->getMount($path);
1910
-		$storage = $mount->getStorage();
1911
-		$internalPath = $mount->getInternalPath($this->getAbsolutePath($path));
1912
-		$ownerId = $storage->getOwner($internalPath);
1913
-		if ($ownerId !== false) {
1914
-			$owner = Server::get(IUserManager::class)->get($ownerId);
1915
-		} else {
1916
-			$owner = null;
1917
-		}
1918
-		return new FileInfo(
1919
-			$this->getAbsolutePath($path),
1920
-			$storage,
1921
-			$internalPath,
1922
-			[
1923
-				'fileid' => null,
1924
-				'mimetype' => $storage->getMimeType($internalPath),
1925
-				'name' => basename($path),
1926
-				'etag' => null,
1927
-				'size' => $storage->filesize($internalPath),
1928
-				'mtime' => $storage->filemtime($internalPath),
1929
-				'encrypted' => false,
1930
-				'permissions' => \OCP\Constants::PERMISSION_ALL
1931
-			],
1932
-			$mount,
1933
-			$owner
1934
-		);
1935
-	}
1936
-
1937
-	/**
1938
-	 * @param string $path
1939
-	 * @param string $fileName
1940
-	 * @param bool $readonly Check only if the path is allowed for read-only access
1941
-	 * @throws InvalidPathException
1942
-	 */
1943
-	public function verifyPath($path, $fileName, $readonly = false): void {
1944
-		// All of the view's functions disallow '..' in the path so we can short cut if the path is invalid
1945
-		if (!Filesystem::isValidPath($path ?: '/')) {
1946
-			$l = \OCP\Util::getL10N('lib');
1947
-			throw new InvalidPathException($l->t('Path contains invalid segments'));
1948
-		}
1949
-
1950
-		// Short cut for read-only validation
1951
-		if ($readonly) {
1952
-			$validator = Server::get(FilenameValidator::class);
1953
-			if ($validator->isForbidden($fileName)) {
1954
-				$l = \OCP\Util::getL10N('lib');
1955
-				throw new InvalidPathException($l->t('Filename is a reserved word'));
1956
-			}
1957
-			return;
1958
-		}
1959
-
1960
-		try {
1961
-			/** @type \OCP\Files\Storage $storage */
1962
-			[$storage, $internalPath] = $this->resolvePath($path);
1963
-			$storage->verifyPath($internalPath, $fileName);
1964
-		} catch (ReservedWordException $ex) {
1965
-			$l = \OCP\Util::getL10N('lib');
1966
-			throw new InvalidPathException($ex->getMessage() ?: $l->t('Filename is a reserved word'));
1967
-		} catch (InvalidCharacterInPathException $ex) {
1968
-			$l = \OCP\Util::getL10N('lib');
1969
-			throw new InvalidPathException($ex->getMessage() ?: $l->t('Filename contains at least one invalid character'));
1970
-		} catch (FileNameTooLongException $ex) {
1971
-			$l = \OCP\Util::getL10N('lib');
1972
-			throw new InvalidPathException($l->t('Filename is too long'));
1973
-		} catch (InvalidDirectoryException $ex) {
1974
-			$l = \OCP\Util::getL10N('lib');
1975
-			throw new InvalidPathException($l->t('Dot files are not allowed'));
1976
-		} catch (EmptyFileNameException $ex) {
1977
-			$l = \OCP\Util::getL10N('lib');
1978
-			throw new InvalidPathException($l->t('Empty filename is not allowed'));
1979
-		}
1980
-	}
1981
-
1982
-	/**
1983
-	 * get all parent folders of $path
1984
-	 *
1985
-	 * @param string $path
1986
-	 * @return string[]
1987
-	 */
1988
-	private function getParents($path) {
1989
-		$path = trim($path, '/');
1990
-		if (!$path) {
1991
-			return [];
1992
-		}
1993
-
1994
-		$parts = explode('/', $path);
1995
-
1996
-		// remove the single file
1997
-		array_pop($parts);
1998
-		$result = ['/'];
1999
-		$resultPath = '';
2000
-		foreach ($parts as $part) {
2001
-			if ($part) {
2002
-				$resultPath .= '/' . $part;
2003
-				$result[] = $resultPath;
2004
-			}
2005
-		}
2006
-		return $result;
2007
-	}
2008
-
2009
-	/**
2010
-	 * Returns the mount point for which to lock
2011
-	 *
2012
-	 * @param string $absolutePath absolute path
2013
-	 * @param bool $useParentMount true to return parent mount instead of whatever
2014
-	 *                             is mounted directly on the given path, false otherwise
2015
-	 * @return IMountPoint mount point for which to apply locks
2016
-	 */
2017
-	private function getMountForLock(string $absolutePath, bool $useParentMount = false): IMountPoint {
2018
-		$mount = Filesystem::getMountManager()->find($absolutePath);
2019
-
2020
-		if ($useParentMount) {
2021
-			// find out if something is mounted directly on the path
2022
-			$internalPath = $mount->getInternalPath($absolutePath);
2023
-			if ($internalPath === '') {
2024
-				// resolve the parent mount instead
2025
-				$mount = Filesystem::getMountManager()->find(dirname($absolutePath));
2026
-			}
2027
-		}
2028
-
2029
-		return $mount;
2030
-	}
2031
-
2032
-	/**
2033
-	 * Lock the given path
2034
-	 *
2035
-	 * @param string $path the path of the file to lock, relative to the view
2036
-	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2037
-	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2038
-	 *
2039
-	 * @return bool False if the path is excluded from locking, true otherwise
2040
-	 * @throws LockedException if the path is already locked
2041
-	 */
2042
-	private function lockPath($path, $type, $lockMountPoint = false) {
2043
-		$absolutePath = $this->getAbsolutePath($path);
2044
-		$absolutePath = Filesystem::normalizePath($absolutePath);
2045
-		if (!$this->shouldLockFile($absolutePath)) {
2046
-			return false;
2047
-		}
2048
-
2049
-		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
2050
-		try {
2051
-			$storage = $mount->getStorage();
2052
-			if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
2053
-				$storage->acquireLock(
2054
-					$mount->getInternalPath($absolutePath),
2055
-					$type,
2056
-					$this->lockingProvider
2057
-				);
2058
-			}
2059
-		} catch (LockedException $e) {
2060
-			// rethrow with the human-readable path
2061
-			throw new LockedException(
2062
-				$path,
2063
-				$e,
2064
-				$e->getExistingLock()
2065
-			);
2066
-		}
2067
-
2068
-		return true;
2069
-	}
2070
-
2071
-	/**
2072
-	 * Change the lock type
2073
-	 *
2074
-	 * @param string $path the path of the file to lock, relative to the view
2075
-	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2076
-	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2077
-	 *
2078
-	 * @return bool False if the path is excluded from locking, true otherwise
2079
-	 * @throws LockedException if the path is already locked
2080
-	 */
2081
-	public function changeLock($path, $type, $lockMountPoint = false) {
2082
-		$path = Filesystem::normalizePath($path);
2083
-		$absolutePath = $this->getAbsolutePath($path);
2084
-		$absolutePath = Filesystem::normalizePath($absolutePath);
2085
-		if (!$this->shouldLockFile($absolutePath)) {
2086
-			return false;
2087
-		}
2088
-
2089
-		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
2090
-		try {
2091
-			$storage = $mount->getStorage();
2092
-			if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
2093
-				$storage->changeLock(
2094
-					$mount->getInternalPath($absolutePath),
2095
-					$type,
2096
-					$this->lockingProvider
2097
-				);
2098
-			}
2099
-		} catch (LockedException $e) {
2100
-			// rethrow with the a human-readable path
2101
-			throw new LockedException(
2102
-				$path,
2103
-				$e,
2104
-				$e->getExistingLock()
2105
-			);
2106
-		}
2107
-
2108
-		return true;
2109
-	}
2110
-
2111
-	/**
2112
-	 * Unlock the given path
2113
-	 *
2114
-	 * @param string $path the path of the file to unlock, relative to the view
2115
-	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2116
-	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2117
-	 *
2118
-	 * @return bool False if the path is excluded from locking, true otherwise
2119
-	 * @throws LockedException
2120
-	 */
2121
-	private function unlockPath($path, $type, $lockMountPoint = false) {
2122
-		$absolutePath = $this->getAbsolutePath($path);
2123
-		$absolutePath = Filesystem::normalizePath($absolutePath);
2124
-		if (!$this->shouldLockFile($absolutePath)) {
2125
-			return false;
2126
-		}
2127
-
2128
-		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
2129
-		$storage = $mount->getStorage();
2130
-		if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
2131
-			$storage->releaseLock(
2132
-				$mount->getInternalPath($absolutePath),
2133
-				$type,
2134
-				$this->lockingProvider
2135
-			);
2136
-		}
2137
-
2138
-		return true;
2139
-	}
2140
-
2141
-	/**
2142
-	 * Lock a path and all its parents up to the root of the view
2143
-	 *
2144
-	 * @param string $path the path of the file to lock relative to the view
2145
-	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2146
-	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2147
-	 *
2148
-	 * @return bool False if the path is excluded from locking, true otherwise
2149
-	 * @throws LockedException
2150
-	 */
2151
-	public function lockFile($path, $type, $lockMountPoint = false) {
2152
-		$absolutePath = $this->getAbsolutePath($path);
2153
-		$absolutePath = Filesystem::normalizePath($absolutePath);
2154
-		if (!$this->shouldLockFile($absolutePath)) {
2155
-			return false;
2156
-		}
2157
-
2158
-		$this->lockPath($path, $type, $lockMountPoint);
2159
-
2160
-		$parents = $this->getParents($path);
2161
-		foreach ($parents as $parent) {
2162
-			$this->lockPath($parent, ILockingProvider::LOCK_SHARED);
2163
-		}
2164
-
2165
-		return true;
2166
-	}
2167
-
2168
-	/**
2169
-	 * Unlock a path and all its parents up to the root of the view
2170
-	 *
2171
-	 * @param string $path the path of the file to lock relative to the view
2172
-	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2173
-	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2174
-	 *
2175
-	 * @return bool False if the path is excluded from locking, true otherwise
2176
-	 * @throws LockedException
2177
-	 */
2178
-	public function unlockFile($path, $type, $lockMountPoint = false) {
2179
-		$absolutePath = $this->getAbsolutePath($path);
2180
-		$absolutePath = Filesystem::normalizePath($absolutePath);
2181
-		if (!$this->shouldLockFile($absolutePath)) {
2182
-			return false;
2183
-		}
2184
-
2185
-		$this->unlockPath($path, $type, $lockMountPoint);
2186
-
2187
-		$parents = $this->getParents($path);
2188
-		foreach ($parents as $parent) {
2189
-			$this->unlockPath($parent, ILockingProvider::LOCK_SHARED);
2190
-		}
2191
-
2192
-		return true;
2193
-	}
2194
-
2195
-	/**
2196
-	 * Only lock files in data/user/files/
2197
-	 *
2198
-	 * @param string $path Absolute path to the file/folder we try to (un)lock
2199
-	 * @return bool
2200
-	 */
2201
-	protected function shouldLockFile($path) {
2202
-		$path = Filesystem::normalizePath($path);
2203
-
2204
-		$pathSegments = explode('/', $path);
2205
-		if (isset($pathSegments[2])) {
2206
-			// E.g.: /username/files/path-to-file
2207
-			return ($pathSegments[2] === 'files') && (count($pathSegments) > 3);
2208
-		}
2209
-
2210
-		return !str_starts_with($path, '/appdata_');
2211
-	}
2212
-
2213
-	/**
2214
-	 * Shortens the given absolute path to be relative to
2215
-	 * "$user/files".
2216
-	 *
2217
-	 * @param string $absolutePath absolute path which is under "files"
2218
-	 *
2219
-	 * @return string path relative to "files" with trimmed slashes or null
2220
-	 *                if the path was NOT relative to files
2221
-	 *
2222
-	 * @throws \InvalidArgumentException if the given path was not under "files"
2223
-	 * @since 8.1.0
2224
-	 */
2225
-	public function getPathRelativeToFiles($absolutePath) {
2226
-		$path = Filesystem::normalizePath($absolutePath);
2227
-		$parts = explode('/', trim($path, '/'), 3);
2228
-		// "$user", "files", "path/to/dir"
2229
-		if (!isset($parts[1]) || $parts[1] !== 'files') {
2230
-			$this->logger->error(
2231
-				'$absolutePath must be relative to "files", value is "{absolutePath}"',
2232
-				[
2233
-					'absolutePath' => $absolutePath,
2234
-				]
2235
-			);
2236
-			throw new \InvalidArgumentException('$absolutePath must be relative to "files"');
2237
-		}
2238
-		if (isset($parts[2])) {
2239
-			return $parts[2];
2240
-		}
2241
-		return '';
2242
-	}
2243
-
2244
-	/**
2245
-	 * @param string $filename
2246
-	 * @return array
2247
-	 * @throws \OC\User\NoUserException
2248
-	 * @throws NotFoundException
2249
-	 */
2250
-	public function getUidAndFilename($filename) {
2251
-		$info = $this->getFileInfo($filename);
2252
-		if (!$info instanceof \OCP\Files\FileInfo) {
2253
-			throw new NotFoundException($this->getAbsolutePath($filename) . ' not found');
2254
-		}
2255
-		$uid = $info->getOwner()->getUID();
2256
-		if ($uid != \OC_User::getUser()) {
2257
-			Filesystem::initMountPoints($uid);
2258
-			$ownerView = new View('/' . $uid . '/files');
2259
-			try {
2260
-				$filename = $ownerView->getPath($info['fileid']);
2261
-			} catch (NotFoundException $e) {
2262
-				throw new NotFoundException('File with id ' . $info['fileid'] . ' not found for user ' . $uid);
2263
-			}
2264
-		}
2265
-		return [$uid, $filename];
2266
-	}
2267
-
2268
-	/**
2269
-	 * Creates parent non-existing folders
2270
-	 *
2271
-	 * @param string $filePath
2272
-	 * @return bool
2273
-	 */
2274
-	private function createParentDirectories($filePath) {
2275
-		$directoryParts = explode('/', $filePath);
2276
-		$directoryParts = array_filter($directoryParts);
2277
-		foreach ($directoryParts as $key => $part) {
2278
-			$currentPathElements = array_slice($directoryParts, 0, $key);
2279
-			$currentPath = '/' . implode('/', $currentPathElements);
2280
-			if ($this->is_file($currentPath)) {
2281
-				return false;
2282
-			}
2283
-			if (!$this->file_exists($currentPath)) {
2284
-				$this->mkdir($currentPath);
2285
-			}
2286
-		}
2287
-
2288
-		return true;
2289
-	}
63
+    private string $fakeRoot = '';
64
+    private ILockingProvider $lockingProvider;
65
+    private bool $lockingEnabled;
66
+    private bool $updaterEnabled = true;
67
+    private UserManager $userManager;
68
+    private LoggerInterface $logger;
69
+
70
+    /**
71
+     * @throws \Exception If $root contains an invalid path
72
+     */
73
+    public function __construct(string $root = '') {
74
+        if (!Filesystem::isValidPath($root)) {
75
+            throw new \Exception();
76
+        }
77
+
78
+        $this->fakeRoot = $root;
79
+        $this->lockingProvider = \OC::$server->get(ILockingProvider::class);
80
+        $this->lockingEnabled = !($this->lockingProvider instanceof \OC\Lock\NoopLockingProvider);
81
+        $this->userManager = \OC::$server->getUserManager();
82
+        $this->logger = \OC::$server->get(LoggerInterface::class);
83
+    }
84
+
85
+    /**
86
+     * @param ?string $path
87
+     * @psalm-template S as string|null
88
+     * @psalm-param S $path
89
+     * @psalm-return (S is string ? string : null)
90
+     */
91
+    public function getAbsolutePath($path = '/'): ?string {
92
+        if ($path === null) {
93
+            return null;
94
+        }
95
+        $this->assertPathLength($path);
96
+        return PathHelper::normalizePath($this->fakeRoot . '/' . $path);
97
+    }
98
+
99
+    /**
100
+     * Change the root to a fake root
101
+     *
102
+     * @param string $fakeRoot
103
+     */
104
+    public function chroot($fakeRoot): void {
105
+        if (!$fakeRoot == '') {
106
+            if ($fakeRoot[0] !== '/') {
107
+                $fakeRoot = '/' . $fakeRoot;
108
+            }
109
+        }
110
+        $this->fakeRoot = $fakeRoot;
111
+    }
112
+
113
+    /**
114
+     * Get the fake root
115
+     */
116
+    public function getRoot(): string {
117
+        return $this->fakeRoot;
118
+    }
119
+
120
+    /**
121
+     * get path relative to the root of the view
122
+     *
123
+     * @param string $path
124
+     */
125
+    public function getRelativePath($path): ?string {
126
+        $this->assertPathLength($path);
127
+        if ($this->fakeRoot == '') {
128
+            return $path;
129
+        }
130
+
131
+        if (rtrim($path, '/') === rtrim($this->fakeRoot, '/')) {
132
+            return '/';
133
+        }
134
+
135
+        // missing slashes can cause wrong matches!
136
+        $root = rtrim($this->fakeRoot, '/') . '/';
137
+
138
+        if (!str_starts_with($path, $root)) {
139
+            return null;
140
+        } else {
141
+            $path = substr($path, strlen($this->fakeRoot));
142
+            if (strlen($path) === 0) {
143
+                return '/';
144
+            } else {
145
+                return $path;
146
+            }
147
+        }
148
+    }
149
+
150
+    /**
151
+     * Get the mountpoint of the storage object for a path
152
+     * ( note: because a storage is not always mounted inside the fakeroot, the
153
+     * returned mountpoint is relative to the absolute root of the filesystem
154
+     * and does not take the chroot into account )
155
+     *
156
+     * @param string $path
157
+     */
158
+    public function getMountPoint($path): string {
159
+        return Filesystem::getMountPoint($this->getAbsolutePath($path));
160
+    }
161
+
162
+    /**
163
+     * Get the mountpoint of the storage object for a path
164
+     * ( note: because a storage is not always mounted inside the fakeroot, the
165
+     * returned mountpoint is relative to the absolute root of the filesystem
166
+     * and does not take the chroot into account )
167
+     *
168
+     * @param string $path
169
+     */
170
+    public function getMount($path): IMountPoint {
171
+        return Filesystem::getMountManager()->find($this->getAbsolutePath($path));
172
+    }
173
+
174
+    /**
175
+     * Resolve a path to a storage and internal path
176
+     *
177
+     * @param string $path
178
+     * @return array{?\OCP\Files\Storage\IStorage, string} an array consisting of the storage and the internal path
179
+     */
180
+    public function resolvePath($path): array {
181
+        $a = $this->getAbsolutePath($path);
182
+        $p = Filesystem::normalizePath($a);
183
+        return Filesystem::resolvePath($p);
184
+    }
185
+
186
+    /**
187
+     * Return the path to a local version of the file
188
+     * we need this because we can't know if a file is stored local or not from
189
+     * outside the filestorage and for some purposes a local file is needed
190
+     *
191
+     * @param string $path
192
+     */
193
+    public function getLocalFile($path): string|false {
194
+        $parent = substr($path, 0, strrpos($path, '/') ?: 0);
195
+        $path = $this->getAbsolutePath($path);
196
+        [$storage, $internalPath] = Filesystem::resolvePath($path);
197
+        if (Filesystem::isValidPath($parent) && $storage) {
198
+            return $storage->getLocalFile($internalPath);
199
+        } else {
200
+            return false;
201
+        }
202
+    }
203
+
204
+    /**
205
+     * the following functions operate with arguments and return values identical
206
+     * to those of their PHP built-in equivalents. Mostly they are merely wrappers
207
+     * for \OC\Files\Storage\Storage via basicOperation().
208
+     */
209
+    public function mkdir($path) {
210
+        return $this->basicOperation('mkdir', $path, ['create', 'write']);
211
+    }
212
+
213
+    /**
214
+     * remove mount point
215
+     *
216
+     * @param IMountPoint $mount
217
+     * @param string $path relative to data/
218
+     */
219
+    protected function removeMount($mount, $path): bool {
220
+        if ($mount instanceof MoveableMount) {
221
+            // cut of /user/files to get the relative path to data/user/files
222
+            $pathParts = explode('/', $path, 4);
223
+            $relPath = '/' . $pathParts[3];
224
+            $this->lockFile($relPath, ILockingProvider::LOCK_SHARED, true);
225
+            \OC_Hook::emit(
226
+                Filesystem::CLASSNAME, 'umount',
227
+                [Filesystem::signal_param_path => $relPath]
228
+            );
229
+            $this->changeLock($relPath, ILockingProvider::LOCK_EXCLUSIVE, true);
230
+            $result = $mount->removeMount();
231
+            $this->changeLock($relPath, ILockingProvider::LOCK_SHARED, true);
232
+            if ($result) {
233
+                \OC_Hook::emit(
234
+                    Filesystem::CLASSNAME, 'post_umount',
235
+                    [Filesystem::signal_param_path => $relPath]
236
+                );
237
+            }
238
+            $this->unlockFile($relPath, ILockingProvider::LOCK_SHARED, true);
239
+            return $result;
240
+        } else {
241
+            // do not allow deleting the storage's root / the mount point
242
+            // because for some storages it might delete the whole contents
243
+            // but isn't supposed to work that way
244
+            return false;
245
+        }
246
+    }
247
+
248
+    public function disableCacheUpdate(): void {
249
+        $this->updaterEnabled = false;
250
+    }
251
+
252
+    public function enableCacheUpdate(): void {
253
+        $this->updaterEnabled = true;
254
+    }
255
+
256
+    protected function writeUpdate(Storage $storage, string $internalPath, ?int $time = null, ?int $sizeDifference = null): void {
257
+        if ($this->updaterEnabled) {
258
+            if (is_null($time)) {
259
+                $time = time();
260
+            }
261
+            $storage->getUpdater()->update($internalPath, $time, $sizeDifference);
262
+        }
263
+    }
264
+
265
+    protected function removeUpdate(Storage $storage, string $internalPath): void {
266
+        if ($this->updaterEnabled) {
267
+            $storage->getUpdater()->remove($internalPath);
268
+        }
269
+    }
270
+
271
+    protected function renameUpdate(Storage $sourceStorage, Storage $targetStorage, string $sourceInternalPath, string $targetInternalPath): void {
272
+        if ($this->updaterEnabled) {
273
+            $targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
274
+        }
275
+    }
276
+
277
+    protected function copyUpdate(Storage $sourceStorage, Storage $targetStorage, string $sourceInternalPath, string $targetInternalPath): void {
278
+        if ($this->updaterEnabled) {
279
+            $targetStorage->getUpdater()->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
280
+        }
281
+    }
282
+
283
+    /**
284
+     * @param string $path
285
+     * @return bool|mixed
286
+     */
287
+    public function rmdir($path) {
288
+        $absolutePath = $this->getAbsolutePath($path);
289
+        $mount = Filesystem::getMountManager()->find($absolutePath);
290
+        if ($mount->getInternalPath($absolutePath) === '') {
291
+            return $this->removeMount($mount, $absolutePath);
292
+        }
293
+        if ($this->is_dir($path)) {
294
+            $result = $this->basicOperation('rmdir', $path, ['delete']);
295
+        } else {
296
+            $result = false;
297
+        }
298
+
299
+        if (!$result && !$this->file_exists($path)) { //clear ghost files from the cache on delete
300
+            $storage = $mount->getStorage();
301
+            $internalPath = $mount->getInternalPath($absolutePath);
302
+            $storage->getUpdater()->remove($internalPath);
303
+        }
304
+        return $result;
305
+    }
306
+
307
+    /**
308
+     * @param string $path
309
+     * @return resource|false
310
+     */
311
+    public function opendir($path) {
312
+        return $this->basicOperation('opendir', $path, ['read']);
313
+    }
314
+
315
+    /**
316
+     * @param string $path
317
+     * @return bool|mixed
318
+     */
319
+    public function is_dir($path) {
320
+        if ($path == '/') {
321
+            return true;
322
+        }
323
+        return $this->basicOperation('is_dir', $path);
324
+    }
325
+
326
+    /**
327
+     * @param string $path
328
+     * @return bool|mixed
329
+     */
330
+    public function is_file($path) {
331
+        if ($path == '/') {
332
+            return false;
333
+        }
334
+        return $this->basicOperation('is_file', $path);
335
+    }
336
+
337
+    /**
338
+     * @param string $path
339
+     * @return mixed
340
+     */
341
+    public function stat($path) {
342
+        return $this->basicOperation('stat', $path);
343
+    }
344
+
345
+    /**
346
+     * @param string $path
347
+     * @return mixed
348
+     */
349
+    public function filetype($path) {
350
+        return $this->basicOperation('filetype', $path);
351
+    }
352
+
353
+    /**
354
+     * @param string $path
355
+     * @return mixed
356
+     */
357
+    public function filesize(string $path) {
358
+        return $this->basicOperation('filesize', $path);
359
+    }
360
+
361
+    /**
362
+     * @param string $path
363
+     * @return bool|mixed
364
+     * @throws InvalidPathException
365
+     */
366
+    public function readfile($path) {
367
+        $this->assertPathLength($path);
368
+        if (ob_get_level()) {
369
+            ob_end_clean();
370
+        }
371
+        $handle = $this->fopen($path, 'rb');
372
+        if ($handle) {
373
+            $chunkSize = 524288; // 512 kiB chunks
374
+            while (!feof($handle)) {
375
+                echo fread($handle, $chunkSize);
376
+                flush();
377
+                $this->checkConnectionStatus();
378
+            }
379
+            fclose($handle);
380
+            return $this->filesize($path);
381
+        }
382
+        return false;
383
+    }
384
+
385
+    /**
386
+     * @param string $path
387
+     * @param int $from
388
+     * @param int $to
389
+     * @return bool|mixed
390
+     * @throws InvalidPathException
391
+     * @throws \OCP\Files\UnseekableException
392
+     */
393
+    public function readfilePart($path, $from, $to) {
394
+        $this->assertPathLength($path);
395
+        if (ob_get_level()) {
396
+            ob_end_clean();
397
+        }
398
+        $handle = $this->fopen($path, 'rb');
399
+        if ($handle) {
400
+            $chunkSize = 524288; // 512 kiB chunks
401
+            $startReading = true;
402
+
403
+            if ($from !== 0 && $from !== '0' && fseek($handle, $from) !== 0) {
404
+                // forward file handle via chunked fread because fseek seem to have failed
405
+
406
+                $end = $from + 1;
407
+                while (!feof($handle) && ftell($handle) < $end && ftell($handle) !== $from) {
408
+                    $len = $from - ftell($handle);
409
+                    if ($len > $chunkSize) {
410
+                        $len = $chunkSize;
411
+                    }
412
+                    $result = fread($handle, $len);
413
+
414
+                    if ($result === false) {
415
+                        $startReading = false;
416
+                        break;
417
+                    }
418
+                }
419
+            }
420
+
421
+            if ($startReading) {
422
+                $end = $to + 1;
423
+                while (!feof($handle) && ftell($handle) < $end) {
424
+                    $len = $end - ftell($handle);
425
+                    if ($len > $chunkSize) {
426
+                        $len = $chunkSize;
427
+                    }
428
+                    echo fread($handle, $len);
429
+                    flush();
430
+                    $this->checkConnectionStatus();
431
+                }
432
+                return ftell($handle) - $from;
433
+            }
434
+
435
+            throw new \OCP\Files\UnseekableException('fseek error');
436
+        }
437
+        return false;
438
+    }
439
+
440
+    private function checkConnectionStatus(): void {
441
+        $connectionStatus = \connection_status();
442
+        if ($connectionStatus !== CONNECTION_NORMAL) {
443
+            throw new ConnectionLostException("Connection lost. Status: $connectionStatus");
444
+        }
445
+    }
446
+
447
+    /**
448
+     * @param string $path
449
+     * @return mixed
450
+     */
451
+    public function isCreatable($path) {
452
+        return $this->basicOperation('isCreatable', $path);
453
+    }
454
+
455
+    /**
456
+     * @param string $path
457
+     * @return mixed
458
+     */
459
+    public function isReadable($path) {
460
+        return $this->basicOperation('isReadable', $path);
461
+    }
462
+
463
+    /**
464
+     * @param string $path
465
+     * @return mixed
466
+     */
467
+    public function isUpdatable($path) {
468
+        return $this->basicOperation('isUpdatable', $path);
469
+    }
470
+
471
+    /**
472
+     * @param string $path
473
+     * @return bool|mixed
474
+     */
475
+    public function isDeletable($path) {
476
+        $absolutePath = $this->getAbsolutePath($path);
477
+        $mount = Filesystem::getMountManager()->find($absolutePath);
478
+        if ($mount->getInternalPath($absolutePath) === '') {
479
+            return $mount instanceof MoveableMount;
480
+        }
481
+        return $this->basicOperation('isDeletable', $path);
482
+    }
483
+
484
+    /**
485
+     * @param string $path
486
+     * @return mixed
487
+     */
488
+    public function isSharable($path) {
489
+        return $this->basicOperation('isSharable', $path);
490
+    }
491
+
492
+    /**
493
+     * @param string $path
494
+     * @return bool|mixed
495
+     */
496
+    public function file_exists($path) {
497
+        if ($path == '/') {
498
+            return true;
499
+        }
500
+        return $this->basicOperation('file_exists', $path);
501
+    }
502
+
503
+    /**
504
+     * @param string $path
505
+     * @return mixed
506
+     */
507
+    public function filemtime($path) {
508
+        return $this->basicOperation('filemtime', $path);
509
+    }
510
+
511
+    /**
512
+     * @param string $path
513
+     * @param int|string $mtime
514
+     */
515
+    public function touch($path, $mtime = null): bool {
516
+        if (!is_null($mtime) && !is_numeric($mtime)) {
517
+            $mtime = strtotime($mtime);
518
+        }
519
+
520
+        $hooks = ['touch'];
521
+
522
+        if (!$this->file_exists($path)) {
523
+            $hooks[] = 'create';
524
+            $hooks[] = 'write';
525
+        }
526
+        try {
527
+            $result = $this->basicOperation('touch', $path, $hooks, $mtime);
528
+        } catch (\Exception $e) {
529
+            $this->logger->info('Error while setting modified time', ['app' => 'core', 'exception' => $e]);
530
+            $result = false;
531
+        }
532
+        if (!$result) {
533
+            // If create file fails because of permissions on external storage like SMB folders,
534
+            // check file exists and return false if not.
535
+            if (!$this->file_exists($path)) {
536
+                return false;
537
+            }
538
+            if (is_null($mtime)) {
539
+                $mtime = time();
540
+            }
541
+            //if native touch fails, we emulate it by changing the mtime in the cache
542
+            $this->putFileInfo($path, ['mtime' => floor($mtime)]);
543
+        }
544
+        return true;
545
+    }
546
+
547
+    /**
548
+     * @param string $path
549
+     * @return string|false
550
+     * @throws LockedException
551
+     */
552
+    public function file_get_contents($path) {
553
+        return $this->basicOperation('file_get_contents', $path, ['read']);
554
+    }
555
+
556
+    protected function emit_file_hooks_pre(bool $exists, string $path, bool &$run): void {
557
+        if (!$exists) {
558
+            \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
559
+                Filesystem::signal_param_path => $this->getHookPath($path),
560
+                Filesystem::signal_param_run => &$run,
561
+            ]);
562
+        } else {
563
+            \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
564
+                Filesystem::signal_param_path => $this->getHookPath($path),
565
+                Filesystem::signal_param_run => &$run,
566
+            ]);
567
+        }
568
+        \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
569
+            Filesystem::signal_param_path => $this->getHookPath($path),
570
+            Filesystem::signal_param_run => &$run,
571
+        ]);
572
+    }
573
+
574
+    protected function emit_file_hooks_post(bool $exists, string $path): void {
575
+        if (!$exists) {
576
+            \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
577
+                Filesystem::signal_param_path => $this->getHookPath($path),
578
+            ]);
579
+        } else {
580
+            \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
581
+                Filesystem::signal_param_path => $this->getHookPath($path),
582
+            ]);
583
+        }
584
+        \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
585
+            Filesystem::signal_param_path => $this->getHookPath($path),
586
+        ]);
587
+    }
588
+
589
+    /**
590
+     * @param string $path
591
+     * @param string|resource $data
592
+     * @return bool|mixed
593
+     * @throws LockedException
594
+     */
595
+    public function file_put_contents($path, $data) {
596
+        if (is_resource($data)) { //not having to deal with streams in file_put_contents makes life easier
597
+            $absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
598
+            if (Filesystem::isValidPath($path)
599
+                && !Filesystem::isFileBlacklisted($path)
600
+            ) {
601
+                $path = $this->getRelativePath($absolutePath);
602
+                if ($path === null) {
603
+                    throw new InvalidPathException("Path $absolutePath is not in the expected root");
604
+                }
605
+
606
+                $this->lockFile($path, ILockingProvider::LOCK_SHARED);
607
+
608
+                $exists = $this->file_exists($path);
609
+                if ($this->shouldEmitHooks($path)) {
610
+                    $run = true;
611
+                    $this->emit_file_hooks_pre($exists, $path, $run);
612
+                    if (!$run) {
613
+                        $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
614
+                        return false;
615
+                    }
616
+                }
617
+
618
+                try {
619
+                    $this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
620
+                } catch (\Exception $e) {
621
+                    // Release the shared lock before throwing.
622
+                    $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
623
+                    throw $e;
624
+                }
625
+
626
+                /** @var Storage $storage */
627
+                [$storage, $internalPath] = $this->resolvePath($path);
628
+                $target = $storage->fopen($internalPath, 'w');
629
+                if ($target) {
630
+                    [, $result] = Files::streamCopy($data, $target, true);
631
+                    fclose($target);
632
+                    fclose($data);
633
+
634
+                    $this->writeUpdate($storage, $internalPath);
635
+
636
+                    $this->changeLock($path, ILockingProvider::LOCK_SHARED);
637
+
638
+                    if ($this->shouldEmitHooks($path) && $result !== false) {
639
+                        $this->emit_file_hooks_post($exists, $path);
640
+                    }
641
+                    $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
642
+                    return $result;
643
+                } else {
644
+                    $this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
645
+                    return false;
646
+                }
647
+            } else {
648
+                return false;
649
+            }
650
+        } else {
651
+            $hooks = $this->file_exists($path) ? ['update', 'write'] : ['create', 'write'];
652
+            return $this->basicOperation('file_put_contents', $path, $hooks, $data);
653
+        }
654
+    }
655
+
656
+    /**
657
+     * @param string $path
658
+     * @return bool|mixed
659
+     */
660
+    public function unlink($path) {
661
+        if ($path === '' || $path === '/') {
662
+            // do not allow deleting the root
663
+            return false;
664
+        }
665
+        $postFix = (substr($path, -1) === '/') ? '/' : '';
666
+        $absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
667
+        $mount = Filesystem::getMountManager()->find($absolutePath . $postFix);
668
+        if ($mount->getInternalPath($absolutePath) === '') {
669
+            return $this->removeMount($mount, $absolutePath);
670
+        }
671
+        if ($this->is_dir($path)) {
672
+            $result = $this->basicOperation('rmdir', $path, ['delete']);
673
+        } else {
674
+            $result = $this->basicOperation('unlink', $path, ['delete']);
675
+        }
676
+        if (!$result && !$this->file_exists($path)) { //clear ghost files from the cache on delete
677
+            $storage = $mount->getStorage();
678
+            $internalPath = $mount->getInternalPath($absolutePath);
679
+            $storage->getUpdater()->remove($internalPath);
680
+            return true;
681
+        } else {
682
+            return $result;
683
+        }
684
+    }
685
+
686
+    /**
687
+     * @param string $directory
688
+     * @return bool|mixed
689
+     */
690
+    public function deleteAll($directory) {
691
+        return $this->rmdir($directory);
692
+    }
693
+
694
+    /**
695
+     * Rename/move a file or folder from the source path to target path.
696
+     *
697
+     * @param string $source source path
698
+     * @param string $target target path
699
+     * @param array $options
700
+     *
701
+     * @return bool|mixed
702
+     * @throws LockedException
703
+     */
704
+    public function rename($source, $target, array $options = []) {
705
+        $checkSubMounts = $options['checkSubMounts'] ?? true;
706
+
707
+        $absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source));
708
+        $absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
709
+
710
+        if (str_starts_with($absolutePath2, $absolutePath1 . '/')) {
711
+            throw new ForbiddenException('Moving a folder into a child folder is forbidden', false);
712
+        }
713
+
714
+        /** @var IMountManager $mountManager */
715
+        $mountManager = \OC::$server->get(IMountManager::class);
716
+
717
+        $targetParts = explode('/', $absolutePath2);
718
+        $targetUser = $targetParts[1] ?? null;
719
+        $result = false;
720
+        if (
721
+            Filesystem::isValidPath($target)
722
+            && Filesystem::isValidPath($source)
723
+            && !Filesystem::isFileBlacklisted($target)
724
+        ) {
725
+            $source = $this->getRelativePath($absolutePath1);
726
+            $target = $this->getRelativePath($absolutePath2);
727
+            $exists = $this->file_exists($target);
728
+
729
+            if ($source == null || $target == null) {
730
+                return false;
731
+            }
732
+
733
+            try {
734
+                $this->verifyPath(dirname($target), basename($target));
735
+            } catch (InvalidPathException) {
736
+                return false;
737
+            }
738
+
739
+            $this->lockFile($source, ILockingProvider::LOCK_SHARED, true);
740
+            try {
741
+                $this->lockFile($target, ILockingProvider::LOCK_SHARED, true);
742
+
743
+                $run = true;
744
+                if ($this->shouldEmitHooks($source) && (Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target))) {
745
+                    // if it was a rename from a part file to a regular file it was a write and not a rename operation
746
+                    $this->emit_file_hooks_pre($exists, $target, $run);
747
+                } elseif ($this->shouldEmitHooks($source)) {
748
+                    $sourcePath = $this->getHookPath($source);
749
+                    $targetPath = $this->getHookPath($target);
750
+                    if ($sourcePath !== null && $targetPath !== null) {
751
+                        \OC_Hook::emit(
752
+                            Filesystem::CLASSNAME, Filesystem::signal_rename,
753
+                            [
754
+                                Filesystem::signal_param_oldpath => $sourcePath,
755
+                                Filesystem::signal_param_newpath => $targetPath,
756
+                                Filesystem::signal_param_run => &$run
757
+                            ]
758
+                        );
759
+                    }
760
+                }
761
+                if ($run) {
762
+                    $manager = Filesystem::getMountManager();
763
+                    $mount1 = $this->getMount($source);
764
+                    $mount2 = $this->getMount($target);
765
+                    $storage1 = $mount1->getStorage();
766
+                    $storage2 = $mount2->getStorage();
767
+                    $internalPath1 = $mount1->getInternalPath($absolutePath1);
768
+                    $internalPath2 = $mount2->getInternalPath($absolutePath2);
769
+
770
+                    $this->changeLock($source, ILockingProvider::LOCK_EXCLUSIVE, true);
771
+                    try {
772
+                        $this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE, true);
773
+
774
+                        if ($checkSubMounts) {
775
+                            $movedMounts = $mountManager->findIn($this->getAbsolutePath($source));
776
+                        } else {
777
+                            $movedMounts = [];
778
+                        }
779
+
780
+                        if ($internalPath1 === '') {
781
+                            $sourceParentMount = $this->getMount(dirname($source));
782
+                            $movedMounts[] = $mount1;
783
+                            $this->validateMountMove($movedMounts, $sourceParentMount, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2));
784
+                            /**
785
+                             * @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
786
+                             */
787
+                            $sourceMountPoint = $mount1->getMountPoint();
788
+                            $result = $mount1->moveMount($absolutePath2);
789
+                            $manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
790
+
791
+                            // moving a file/folder within the same mount point
792
+                        } elseif ($storage1 === $storage2) {
793
+                            if (count($movedMounts) > 0) {
794
+                                $this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2));
795
+                            }
796
+                            if ($storage1) {
797
+                                $result = $storage1->rename($internalPath1, $internalPath2);
798
+                            } else {
799
+                                $result = false;
800
+                            }
801
+                            // moving a file/folder between storages (from $storage1 to $storage2)
802
+                        } else {
803
+                            if (count($movedMounts) > 0) {
804
+                                $this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2));
805
+                            }
806
+                            $result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
807
+                        }
808
+
809
+                        if ((Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target)) && $result !== false) {
810
+                            // if it was a rename from a part file to a regular file it was a write and not a rename operation
811
+                            $this->writeUpdate($storage2, $internalPath2);
812
+                        } elseif ($result) {
813
+                            if ($internalPath1 !== '') { // don't do a cache update for moved mounts
814
+                                $this->renameUpdate($storage1, $storage2, $internalPath1, $internalPath2);
815
+                            }
816
+                        }
817
+                    } catch (\Exception $e) {
818
+                        throw $e;
819
+                    } finally {
820
+                        $this->changeLock($source, ILockingProvider::LOCK_SHARED, true);
821
+                        $this->changeLock($target, ILockingProvider::LOCK_SHARED, true);
822
+                    }
823
+
824
+                    if ((Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target)) && $result !== false) {
825
+                        if ($this->shouldEmitHooks()) {
826
+                            $this->emit_file_hooks_post($exists, $target);
827
+                        }
828
+                    } elseif ($result) {
829
+                        if ($this->shouldEmitHooks($source) && $this->shouldEmitHooks($target)) {
830
+                            $sourcePath = $this->getHookPath($source);
831
+                            $targetPath = $this->getHookPath($target);
832
+                            if ($sourcePath !== null && $targetPath !== null) {
833
+                                \OC_Hook::emit(
834
+                                    Filesystem::CLASSNAME,
835
+                                    Filesystem::signal_post_rename,
836
+                                    [
837
+                                        Filesystem::signal_param_oldpath => $sourcePath,
838
+                                        Filesystem::signal_param_newpath => $targetPath,
839
+                                    ]
840
+                                );
841
+                            }
842
+                        }
843
+                    }
844
+                }
845
+            } catch (\Exception $e) {
846
+                throw $e;
847
+            } finally {
848
+                $this->unlockFile($source, ILockingProvider::LOCK_SHARED, true);
849
+                $this->unlockFile($target, ILockingProvider::LOCK_SHARED, true);
850
+            }
851
+        }
852
+        return $result;
853
+    }
854
+
855
+    /**
856
+     * @throws ForbiddenException
857
+     */
858
+    private function validateMountMove(array $mounts, IMountPoint $sourceMount, IMountPoint $targetMount, bool $targetIsShared): void {
859
+        $targetPath = $this->getRelativePath($targetMount->getMountPoint());
860
+        if ($targetPath) {
861
+            $targetPath = trim($targetPath, '/');
862
+        } else {
863
+            $targetPath = $targetMount->getMountPoint();
864
+        }
865
+
866
+        $l = \OC::$server->get(IFactory::class)->get('files');
867
+        foreach ($mounts as $mount) {
868
+            $sourcePath = $this->getRelativePath($mount->getMountPoint());
869
+            if ($sourcePath) {
870
+                $sourcePath = trim($sourcePath, '/');
871
+            } else {
872
+                $sourcePath = $mount->getMountPoint();
873
+            }
874
+
875
+            if (!$mount instanceof MoveableMount) {
876
+                throw new ForbiddenException($l->t('Storage %s cannot be moved', [$sourcePath]), false);
877
+            }
878
+
879
+            if ($targetIsShared) {
880
+                if ($sourceMount instanceof SharedMount) {
881
+                    throw new ForbiddenException($l->t('Moving a share (%s) into a shared folder is not allowed', [$sourcePath]), false);
882
+                } else {
883
+                    throw new ForbiddenException($l->t('Moving a storage (%s) into a shared folder is not allowed', [$sourcePath]), false);
884
+                }
885
+            }
886
+
887
+            if ($sourceMount !== $targetMount) {
888
+                if ($sourceMount instanceof SharedMount) {
889
+                    if ($targetMount instanceof SharedMount) {
890
+                        throw new ForbiddenException($l->t('Moving a share (%s) into another share (%s) is not allowed', [$sourcePath, $targetPath]), false);
891
+                    } else {
892
+                        throw new ForbiddenException($l->t('Moving a share (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false);
893
+                    }
894
+                } else {
895
+                    if ($targetMount instanceof SharedMount) {
896
+                        throw new ForbiddenException($l->t('Moving a storage (%s) into a share (%s) is not allowed', [$sourcePath, $targetPath]), false);
897
+                    } else {
898
+                        throw new ForbiddenException($l->t('Moving a storage (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false);
899
+                    }
900
+                }
901
+            }
902
+        }
903
+    }
904
+
905
+    /**
906
+     * Copy a file/folder from the source path to target path
907
+     *
908
+     * @param string $source source path
909
+     * @param string $target target path
910
+     * @param bool $preserveMtime whether to preserve mtime on the copy
911
+     *
912
+     * @return bool|mixed
913
+     */
914
+    public function copy($source, $target, $preserveMtime = false) {
915
+        $absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source));
916
+        $absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
917
+        $result = false;
918
+        if (
919
+            Filesystem::isValidPath($target)
920
+            && Filesystem::isValidPath($source)
921
+            && !Filesystem::isFileBlacklisted($target)
922
+        ) {
923
+            $source = $this->getRelativePath($absolutePath1);
924
+            $target = $this->getRelativePath($absolutePath2);
925
+
926
+            if ($source == null || $target == null) {
927
+                return false;
928
+            }
929
+            $run = true;
930
+
931
+            $this->lockFile($target, ILockingProvider::LOCK_SHARED);
932
+            $this->lockFile($source, ILockingProvider::LOCK_SHARED);
933
+            $lockTypePath1 = ILockingProvider::LOCK_SHARED;
934
+            $lockTypePath2 = ILockingProvider::LOCK_SHARED;
935
+
936
+            try {
937
+                $exists = $this->file_exists($target);
938
+                if ($this->shouldEmitHooks($target)) {
939
+                    \OC_Hook::emit(
940
+                        Filesystem::CLASSNAME,
941
+                        Filesystem::signal_copy,
942
+                        [
943
+                            Filesystem::signal_param_oldpath => $this->getHookPath($source),
944
+                            Filesystem::signal_param_newpath => $this->getHookPath($target),
945
+                            Filesystem::signal_param_run => &$run
946
+                        ]
947
+                    );
948
+                    $this->emit_file_hooks_pre($exists, $target, $run);
949
+                }
950
+                if ($run) {
951
+                    $mount1 = $this->getMount($source);
952
+                    $mount2 = $this->getMount($target);
953
+                    $storage1 = $mount1->getStorage();
954
+                    $internalPath1 = $mount1->getInternalPath($absolutePath1);
955
+                    $storage2 = $mount2->getStorage();
956
+                    $internalPath2 = $mount2->getInternalPath($absolutePath2);
957
+
958
+                    $this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE);
959
+                    $lockTypePath2 = ILockingProvider::LOCK_EXCLUSIVE;
960
+
961
+                    if ($mount1->getMountPoint() == $mount2->getMountPoint()) {
962
+                        if ($storage1) {
963
+                            $result = $storage1->copy($internalPath1, $internalPath2);
964
+                        } else {
965
+                            $result = false;
966
+                        }
967
+                    } else {
968
+                        $result = $storage2->copyFromStorage($storage1, $internalPath1, $internalPath2);
969
+                    }
970
+
971
+                    if ($result) {
972
+                        $this->copyUpdate($storage1, $storage2, $internalPath1, $internalPath2);
973
+                    }
974
+
975
+                    $this->changeLock($target, ILockingProvider::LOCK_SHARED);
976
+                    $lockTypePath2 = ILockingProvider::LOCK_SHARED;
977
+
978
+                    if ($this->shouldEmitHooks($target) && $result !== false) {
979
+                        \OC_Hook::emit(
980
+                            Filesystem::CLASSNAME,
981
+                            Filesystem::signal_post_copy,
982
+                            [
983
+                                Filesystem::signal_param_oldpath => $this->getHookPath($source),
984
+                                Filesystem::signal_param_newpath => $this->getHookPath($target)
985
+                            ]
986
+                        );
987
+                        $this->emit_file_hooks_post($exists, $target);
988
+                    }
989
+                }
990
+            } catch (\Exception $e) {
991
+                $this->unlockFile($target, $lockTypePath2);
992
+                $this->unlockFile($source, $lockTypePath1);
993
+                throw $e;
994
+            }
995
+
996
+            $this->unlockFile($target, $lockTypePath2);
997
+            $this->unlockFile($source, $lockTypePath1);
998
+        }
999
+        return $result;
1000
+    }
1001
+
1002
+    /**
1003
+     * @param string $path
1004
+     * @param string $mode 'r' or 'w'
1005
+     * @return resource|false
1006
+     * @throws LockedException
1007
+     */
1008
+    public function fopen($path, $mode) {
1009
+        $mode = str_replace('b', '', $mode); // the binary flag is a windows only feature which we do not support
1010
+        $hooks = [];
1011
+        switch ($mode) {
1012
+            case 'r':
1013
+                $hooks[] = 'read';
1014
+                break;
1015
+            case 'r+':
1016
+            case 'w+':
1017
+            case 'x+':
1018
+            case 'a+':
1019
+                $hooks[] = 'read';
1020
+                $hooks[] = 'write';
1021
+                break;
1022
+            case 'w':
1023
+            case 'x':
1024
+            case 'a':
1025
+                $hooks[] = 'write';
1026
+                break;
1027
+            default:
1028
+                $this->logger->error('invalid mode (' . $mode . ') for ' . $path, ['app' => 'core']);
1029
+        }
1030
+
1031
+        if ($mode !== 'r' && $mode !== 'w') {
1032
+            $this->logger->info('Trying to open a file with a mode other than "r" or "w" can cause severe performance issues with some backends', ['app' => 'core']);
1033
+        }
1034
+
1035
+        $handle = $this->basicOperation('fopen', $path, $hooks, $mode);
1036
+        if (!is_resource($handle) && $mode === 'r') {
1037
+            // trying to read a file that isn't on disk, check if the cache is out of sync and rescan if needed
1038
+            $mount = $this->getMount($path);
1039
+            $internalPath = $mount->getInternalPath($this->getAbsolutePath($path));
1040
+            $storage = $mount->getStorage();
1041
+            if ($storage->getCache()->inCache($internalPath) && !$storage->file_exists($path)) {
1042
+                $this->writeUpdate($storage, $internalPath);
1043
+            }
1044
+        }
1045
+        return $handle;
1046
+    }
1047
+
1048
+    /**
1049
+     * @param string $path
1050
+     * @throws InvalidPathException
1051
+     */
1052
+    public function toTmpFile($path): string|false {
1053
+        $this->assertPathLength($path);
1054
+        if (Filesystem::isValidPath($path)) {
1055
+            $source = $this->fopen($path, 'r');
1056
+            if ($source) {
1057
+                $extension = pathinfo($path, PATHINFO_EXTENSION);
1058
+                $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($extension);
1059
+                file_put_contents($tmpFile, $source);
1060
+                return $tmpFile;
1061
+            } else {
1062
+                return false;
1063
+            }
1064
+        } else {
1065
+            return false;
1066
+        }
1067
+    }
1068
+
1069
+    /**
1070
+     * @param string $tmpFile
1071
+     * @param string $path
1072
+     * @return bool|mixed
1073
+     * @throws InvalidPathException
1074
+     */
1075
+    public function fromTmpFile($tmpFile, $path) {
1076
+        $this->assertPathLength($path);
1077
+        if (Filesystem::isValidPath($path)) {
1078
+            // Get directory that the file is going into
1079
+            $filePath = dirname($path);
1080
+
1081
+            // Create the directories if any
1082
+            if (!$this->file_exists($filePath)) {
1083
+                $result = $this->createParentDirectories($filePath);
1084
+                if ($result === false) {
1085
+                    return false;
1086
+                }
1087
+            }
1088
+
1089
+            $source = fopen($tmpFile, 'r');
1090
+            if ($source) {
1091
+                $result = $this->file_put_contents($path, $source);
1092
+                /**
1093
+                 * $this->file_put_contents() might have already closed
1094
+                 * the resource, so we check it, before trying to close it
1095
+                 * to avoid messages in the error log.
1096
+                 * @psalm-suppress RedundantCondition false-positive
1097
+                 */
1098
+                if (is_resource($source)) {
1099
+                    fclose($source);
1100
+                }
1101
+                unlink($tmpFile);
1102
+                return $result;
1103
+            } else {
1104
+                return false;
1105
+            }
1106
+        } else {
1107
+            return false;
1108
+        }
1109
+    }
1110
+
1111
+
1112
+    /**
1113
+     * @param string $path
1114
+     * @return mixed
1115
+     * @throws InvalidPathException
1116
+     */
1117
+    public function getMimeType($path) {
1118
+        $this->assertPathLength($path);
1119
+        return $this->basicOperation('getMimeType', $path);
1120
+    }
1121
+
1122
+    /**
1123
+     * @param string $type
1124
+     * @param string $path
1125
+     * @param bool $raw
1126
+     */
1127
+    public function hash($type, $path, $raw = false): string|bool {
1128
+        $postFix = (substr($path, -1) === '/') ? '/' : '';
1129
+        $absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
1130
+        if (Filesystem::isValidPath($path)) {
1131
+            $path = $this->getRelativePath($absolutePath);
1132
+            if ($path == null) {
1133
+                return false;
1134
+            }
1135
+            if ($this->shouldEmitHooks($path)) {
1136
+                \OC_Hook::emit(
1137
+                    Filesystem::CLASSNAME,
1138
+                    Filesystem::signal_read,
1139
+                    [Filesystem::signal_param_path => $this->getHookPath($path)]
1140
+                );
1141
+            }
1142
+            /** @var Storage|null $storage */
1143
+            [$storage, $internalPath] = Filesystem::resolvePath($absolutePath . $postFix);
1144
+            if ($storage) {
1145
+                return $storage->hash($type, $internalPath, $raw);
1146
+            }
1147
+        }
1148
+        return false;
1149
+    }
1150
+
1151
+    /**
1152
+     * @param string $path
1153
+     * @return mixed
1154
+     * @throws InvalidPathException
1155
+     */
1156
+    public function free_space($path = '/') {
1157
+        $this->assertPathLength($path);
1158
+        $result = $this->basicOperation('free_space', $path);
1159
+        if ($result === null) {
1160
+            throw new InvalidPathException();
1161
+        }
1162
+        return $result;
1163
+    }
1164
+
1165
+    /**
1166
+     * abstraction layer for basic filesystem functions: wrapper for \OC\Files\Storage\Storage
1167
+     *
1168
+     * @param mixed $extraParam (optional)
1169
+     * @return mixed
1170
+     * @throws LockedException
1171
+     *
1172
+     * This method takes requests for basic filesystem functions (e.g. reading & writing
1173
+     * files), processes hooks and proxies, sanitises paths, and finally passes them on to
1174
+     * \OC\Files\Storage\Storage for delegation to a storage backend for execution
1175
+     */
1176
+    private function basicOperation(string $operation, string $path, array $hooks = [], $extraParam = null) {
1177
+        $postFix = (substr($path, -1) === '/') ? '/' : '';
1178
+        $absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
1179
+        if (Filesystem::isValidPath($path)
1180
+            && !Filesystem::isFileBlacklisted($path)
1181
+        ) {
1182
+            $path = $this->getRelativePath($absolutePath);
1183
+            if ($path == null) {
1184
+                return false;
1185
+            }
1186
+
1187
+            if (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks)) {
1188
+                // always a shared lock during pre-hooks so the hook can read the file
1189
+                $this->lockFile($path, ILockingProvider::LOCK_SHARED);
1190
+            }
1191
+
1192
+            $run = $this->runHooks($hooks, $path);
1193
+            [$storage, $internalPath] = Filesystem::resolvePath($absolutePath . $postFix);
1194
+            if ($run && $storage) {
1195
+                /** @var Storage $storage */
1196
+                if (in_array('write', $hooks) || in_array('delete', $hooks)) {
1197
+                    try {
1198
+                        $this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
1199
+                    } catch (LockedException $e) {
1200
+                        // release the shared lock we acquired before quitting
1201
+                        $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1202
+                        throw $e;
1203
+                    }
1204
+                }
1205
+                try {
1206
+                    if (!is_null($extraParam)) {
1207
+                        $result = $storage->$operation($internalPath, $extraParam);
1208
+                    } else {
1209
+                        $result = $storage->$operation($internalPath);
1210
+                    }
1211
+                } catch (\Exception $e) {
1212
+                    if (in_array('write', $hooks) || in_array('delete', $hooks)) {
1213
+                        $this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
1214
+                    } elseif (in_array('read', $hooks)) {
1215
+                        $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1216
+                    }
1217
+                    throw $e;
1218
+                }
1219
+
1220
+                if ($result !== false && in_array('delete', $hooks)) {
1221
+                    $this->removeUpdate($storage, $internalPath);
1222
+                }
1223
+                if ($result !== false && in_array('write', $hooks, true) && $operation !== 'fopen' && $operation !== 'touch') {
1224
+                    $isCreateOperation = $operation === 'mkdir' || ($operation === 'file_put_contents' && in_array('create', $hooks, true));
1225
+                    $sizeDifference = $operation === 'mkdir' ? 0 : $result;
1226
+                    $this->writeUpdate($storage, $internalPath, null, $isCreateOperation ? $sizeDifference : null);
1227
+                }
1228
+                if ($result !== false && in_array('touch', $hooks)) {
1229
+                    $this->writeUpdate($storage, $internalPath, $extraParam, 0);
1230
+                }
1231
+
1232
+                if ((in_array('write', $hooks) || in_array('delete', $hooks)) && ($operation !== 'fopen' || $result === false)) {
1233
+                    $this->changeLock($path, ILockingProvider::LOCK_SHARED);
1234
+                }
1235
+
1236
+                $unlockLater = false;
1237
+                if ($this->lockingEnabled && $operation === 'fopen' && is_resource($result)) {
1238
+                    $unlockLater = true;
1239
+                    // make sure our unlocking callback will still be called if connection is aborted
1240
+                    ignore_user_abort(true);
1241
+                    $result = CallbackWrapper::wrap($result, null, null, function () use ($hooks, $path) {
1242
+                        if (in_array('write', $hooks)) {
1243
+                            $this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
1244
+                        } elseif (in_array('read', $hooks)) {
1245
+                            $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1246
+                        }
1247
+                    });
1248
+                }
1249
+
1250
+                if ($this->shouldEmitHooks($path) && $result !== false) {
1251
+                    if ($operation != 'fopen') { //no post hooks for fopen, the file stream is still open
1252
+                        $this->runHooks($hooks, $path, true);
1253
+                    }
1254
+                }
1255
+
1256
+                if (!$unlockLater
1257
+                    && (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks))
1258
+                ) {
1259
+                    $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1260
+                }
1261
+                return $result;
1262
+            } else {
1263
+                $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1264
+            }
1265
+        }
1266
+        return null;
1267
+    }
1268
+
1269
+    /**
1270
+     * get the path relative to the default root for hook usage
1271
+     *
1272
+     * @param string $path
1273
+     * @return ?string
1274
+     */
1275
+    private function getHookPath($path): ?string {
1276
+        $view = Filesystem::getView();
1277
+        if (!$view) {
1278
+            return $path;
1279
+        }
1280
+        return $view->getRelativePath($this->getAbsolutePath($path));
1281
+    }
1282
+
1283
+    private function shouldEmitHooks(string $path = ''): bool {
1284
+        if ($path && Cache\Scanner::isPartialFile($path)) {
1285
+            return false;
1286
+        }
1287
+        if (!Filesystem::$loaded) {
1288
+            return false;
1289
+        }
1290
+        $defaultRoot = Filesystem::getRoot();
1291
+        if ($defaultRoot === null) {
1292
+            return false;
1293
+        }
1294
+        if ($this->fakeRoot === $defaultRoot) {
1295
+            return true;
1296
+        }
1297
+        $fullPath = $this->getAbsolutePath($path);
1298
+
1299
+        if ($fullPath === $defaultRoot) {
1300
+            return true;
1301
+        }
1302
+
1303
+        return (strlen($fullPath) > strlen($defaultRoot)) && (substr($fullPath, 0, strlen($defaultRoot) + 1) === $defaultRoot . '/');
1304
+    }
1305
+
1306
+    /**
1307
+     * @param string[] $hooks
1308
+     * @param string $path
1309
+     * @param bool $post
1310
+     * @return bool
1311
+     */
1312
+    private function runHooks($hooks, $path, $post = false) {
1313
+        $relativePath = $path;
1314
+        $path = $this->getHookPath($path);
1315
+        $prefix = $post ? 'post_' : '';
1316
+        $run = true;
1317
+        if ($this->shouldEmitHooks($relativePath)) {
1318
+            foreach ($hooks as $hook) {
1319
+                if ($hook != 'read') {
1320
+                    \OC_Hook::emit(
1321
+                        Filesystem::CLASSNAME,
1322
+                        $prefix . $hook,
1323
+                        [
1324
+                            Filesystem::signal_param_run => &$run,
1325
+                            Filesystem::signal_param_path => $path
1326
+                        ]
1327
+                    );
1328
+                } elseif (!$post) {
1329
+                    \OC_Hook::emit(
1330
+                        Filesystem::CLASSNAME,
1331
+                        $prefix . $hook,
1332
+                        [
1333
+                            Filesystem::signal_param_path => $path
1334
+                        ]
1335
+                    );
1336
+                }
1337
+            }
1338
+        }
1339
+        return $run;
1340
+    }
1341
+
1342
+    /**
1343
+     * check if a file or folder has been updated since $time
1344
+     *
1345
+     * @param string $path
1346
+     * @param int $time
1347
+     * @return bool
1348
+     */
1349
+    public function hasUpdated($path, $time) {
1350
+        return $this->basicOperation('hasUpdated', $path, [], $time);
1351
+    }
1352
+
1353
+    /**
1354
+     * @param string $ownerId
1355
+     * @return IUser
1356
+     */
1357
+    private function getUserObjectForOwner(string $ownerId) {
1358
+        return new LazyUser($ownerId, $this->userManager);
1359
+    }
1360
+
1361
+    /**
1362
+     * Get file info from cache
1363
+     *
1364
+     * If the file is not in cached it will be scanned
1365
+     * If the file has changed on storage the cache will be updated
1366
+     *
1367
+     * @param Storage $storage
1368
+     * @param string $internalPath
1369
+     * @param string $relativePath
1370
+     * @return ICacheEntry|bool
1371
+     */
1372
+    private function getCacheEntry($storage, $internalPath, $relativePath) {
1373
+        $cache = $storage->getCache($internalPath);
1374
+        $data = $cache->get($internalPath);
1375
+        $watcher = $storage->getWatcher($internalPath);
1376
+
1377
+        try {
1378
+            // if the file is not in the cache or needs to be updated, trigger the scanner and reload the data
1379
+            if (!$data || (isset($data['size']) && $data['size'] === -1)) {
1380
+                if (!$storage->file_exists($internalPath)) {
1381
+                    return false;
1382
+                }
1383
+                // don't need to get a lock here since the scanner does it's own locking
1384
+                $scanner = $storage->getScanner($internalPath);
1385
+                $scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW);
1386
+                $data = $cache->get($internalPath);
1387
+            } elseif (!Cache\Scanner::isPartialFile($internalPath) && $watcher->needsUpdate($internalPath, $data)) {
1388
+                $this->lockFile($relativePath, ILockingProvider::LOCK_SHARED);
1389
+                $watcher->update($internalPath, $data);
1390
+                $storage->getPropagator()->propagateChange($internalPath, time());
1391
+                $data = $cache->get($internalPath);
1392
+                $this->unlockFile($relativePath, ILockingProvider::LOCK_SHARED);
1393
+            }
1394
+        } catch (LockedException $e) {
1395
+            // if the file is locked we just use the old cache info
1396
+        }
1397
+
1398
+        return $data;
1399
+    }
1400
+
1401
+    /**
1402
+     * get the filesystem info
1403
+     *
1404
+     * @param string $path
1405
+     * @param bool|string $includeMountPoints true to add mountpoint sizes,
1406
+     *                                        'ext' to add only ext storage mount point sizes. Defaults to true.
1407
+     * @return \OC\Files\FileInfo|false False if file does not exist
1408
+     */
1409
+    public function getFileInfo($path, $includeMountPoints = true) {
1410
+        $this->assertPathLength($path);
1411
+        if (!Filesystem::isValidPath($path)) {
1412
+            return false;
1413
+        }
1414
+        $relativePath = $path;
1415
+        $path = Filesystem::normalizePath($this->fakeRoot . '/' . $path);
1416
+
1417
+        $mount = Filesystem::getMountManager()->find($path);
1418
+        $storage = $mount->getStorage();
1419
+        $internalPath = $mount->getInternalPath($path);
1420
+        if ($storage) {
1421
+            $data = $this->getCacheEntry($storage, $internalPath, $relativePath);
1422
+
1423
+            if (!$data instanceof ICacheEntry) {
1424
+                if (Cache\Scanner::isPartialFile($relativePath)) {
1425
+                    return $this->getPartFileInfo($relativePath);
1426
+                }
1427
+
1428
+                return false;
1429
+            }
1430
+
1431
+            if ($mount instanceof MoveableMount && $internalPath === '') {
1432
+                $data['permissions'] |= \OCP\Constants::PERMISSION_DELETE;
1433
+            }
1434
+            if ($internalPath === '' && $data['name']) {
1435
+                $data['name'] = basename($path);
1436
+            }
1437
+
1438
+            $ownerId = $storage->getOwner($internalPath);
1439
+            $owner = null;
1440
+            if ($ownerId !== false) {
1441
+                // ownerId might be null if files are accessed with an access token without file system access
1442
+                $owner = $this->getUserObjectForOwner($ownerId);
1443
+            }
1444
+            $info = new FileInfo($path, $storage, $internalPath, $data, $mount, $owner);
1445
+
1446
+            if (isset($data['fileid'])) {
1447
+                if ($includeMountPoints && $data['mimetype'] === 'httpd/unix-directory') {
1448
+                    //add the sizes of other mount points to the folder
1449
+                    $extOnly = ($includeMountPoints === 'ext');
1450
+                    $this->addSubMounts($info, $extOnly);
1451
+                }
1452
+            }
1453
+
1454
+            return $info;
1455
+        } else {
1456
+            $this->logger->warning('Storage not valid for mountpoint: ' . $mount->getMountPoint(), ['app' => 'core']);
1457
+        }
1458
+
1459
+        return false;
1460
+    }
1461
+
1462
+    /**
1463
+     * Extend a FileInfo that was previously requested with `$includeMountPoints = false` to include the sub mounts
1464
+     */
1465
+    public function addSubMounts(FileInfo $info, $extOnly = false): void {
1466
+        $mounts = Filesystem::getMountManager()->findIn($info->getPath());
1467
+        $info->setSubMounts(array_filter($mounts, function (IMountPoint $mount) use ($extOnly) {
1468
+            return !($extOnly && $mount instanceof SharedMount);
1469
+        }));
1470
+    }
1471
+
1472
+    /**
1473
+     * get the content of a directory
1474
+     *
1475
+     * @param string $directory path under datadirectory
1476
+     * @param string $mimetype_filter limit returned content to this mimetype or mimepart
1477
+     * @return FileInfo[]
1478
+     */
1479
+    public function getDirectoryContent($directory, $mimetype_filter = '', ?\OCP\Files\FileInfo $directoryInfo = null) {
1480
+        $this->assertPathLength($directory);
1481
+        if (!Filesystem::isValidPath($directory)) {
1482
+            return [];
1483
+        }
1484
+
1485
+        $path = $this->getAbsolutePath($directory);
1486
+        $path = Filesystem::normalizePath($path);
1487
+        $mount = $this->getMount($directory);
1488
+        $storage = $mount->getStorage();
1489
+        $internalPath = $mount->getInternalPath($path);
1490
+        if (!$storage) {
1491
+            return [];
1492
+        }
1493
+
1494
+        $cache = $storage->getCache($internalPath);
1495
+        $user = \OC_User::getUser();
1496
+
1497
+        if (!$directoryInfo) {
1498
+            $data = $this->getCacheEntry($storage, $internalPath, $directory);
1499
+            if (!$data instanceof ICacheEntry || !isset($data['fileid'])) {
1500
+                return [];
1501
+            }
1502
+        } else {
1503
+            $data = $directoryInfo;
1504
+        }
1505
+
1506
+        if (!($data->getPermissions() & Constants::PERMISSION_READ)) {
1507
+            return [];
1508
+        }
1509
+
1510
+        $folderId = $data->getId();
1511
+        $contents = $cache->getFolderContentsById($folderId); //TODO: mimetype_filter
1512
+
1513
+        $sharingDisabled = \OCP\Util::isSharingDisabledForUser();
1514
+
1515
+        $fileNames = array_map(function (ICacheEntry $content) {
1516
+            return $content->getName();
1517
+        }, $contents);
1518
+        /**
1519
+         * @var \OC\Files\FileInfo[] $fileInfos
1520
+         */
1521
+        $fileInfos = array_map(function (ICacheEntry $content) use ($path, $storage, $mount, $sharingDisabled) {
1522
+            if ($sharingDisabled) {
1523
+                $content['permissions'] = $content['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
1524
+            }
1525
+            $ownerId = $storage->getOwner($content['path']);
1526
+            if ($ownerId !== false) {
1527
+                $owner = $this->getUserObjectForOwner($ownerId);
1528
+            } else {
1529
+                $owner = null;
1530
+            }
1531
+            return new FileInfo($path . '/' . $content['name'], $storage, $content['path'], $content, $mount, $owner);
1532
+        }, $contents);
1533
+        $files = array_combine($fileNames, $fileInfos);
1534
+
1535
+        //add a folder for any mountpoint in this directory and add the sizes of other mountpoints to the folders
1536
+        $mounts = Filesystem::getMountManager()->findIn($path);
1537
+
1538
+        // make sure nested mounts are sorted after their parent mounts
1539
+        // otherwise doesn't propagate the etag across storage boundaries correctly
1540
+        usort($mounts, function (IMountPoint $a, IMountPoint $b) {
1541
+            return $a->getMountPoint() <=> $b->getMountPoint();
1542
+        });
1543
+
1544
+        $dirLength = strlen($path);
1545
+        foreach ($mounts as $mount) {
1546
+            $mountPoint = $mount->getMountPoint();
1547
+            $subStorage = $mount->getStorage();
1548
+            if ($subStorage) {
1549
+                $subCache = $subStorage->getCache('');
1550
+
1551
+                $rootEntry = $subCache->get('');
1552
+                if (!$rootEntry) {
1553
+                    $subScanner = $subStorage->getScanner();
1554
+                    try {
1555
+                        $subScanner->scanFile('');
1556
+                    } catch (\OCP\Files\StorageNotAvailableException $e) {
1557
+                        continue;
1558
+                    } catch (\OCP\Files\StorageInvalidException $e) {
1559
+                        continue;
1560
+                    } catch (\Exception $e) {
1561
+                        // sometimes when the storage is not available it can be any exception
1562
+                        $this->logger->error('Exception while scanning storage "' . $subStorage->getId() . '"', [
1563
+                            'exception' => $e,
1564
+                            'app' => 'core',
1565
+                        ]);
1566
+                        continue;
1567
+                    }
1568
+                    $rootEntry = $subCache->get('');
1569
+                }
1570
+
1571
+                if ($rootEntry && ($rootEntry->getPermissions() & Constants::PERMISSION_READ)) {
1572
+                    $relativePath = trim(substr($mountPoint, $dirLength), '/');
1573
+                    if ($pos = strpos($relativePath, '/')) {
1574
+                        //mountpoint inside subfolder add size to the correct folder
1575
+                        $entryName = substr($relativePath, 0, $pos);
1576
+
1577
+                        // Create parent folders if the mountpoint is inside a subfolder that doesn't exist yet
1578
+                        if (!isset($files[$entryName])) {
1579
+                            try {
1580
+                                [$storage, ] = $this->resolvePath($path . '/' . $entryName);
1581
+                                // make sure we can create the mountpoint folder, even if the user has a quota of 0
1582
+                                if ($storage->instanceOfStorage(Quota::class)) {
1583
+                                    $storage->enableQuota(false);
1584
+                                }
1585
+
1586
+                                if ($this->mkdir($path . '/' . $entryName) !== false) {
1587
+                                    $info = $this->getFileInfo($path . '/' . $entryName);
1588
+                                    if ($info !== false) {
1589
+                                        $files[$entryName] = $info;
1590
+                                    }
1591
+                                }
1592
+
1593
+                                if ($storage->instanceOfStorage(Quota::class)) {
1594
+                                    $storage->enableQuota(true);
1595
+                                }
1596
+                            } catch (\Exception $e) {
1597
+                                // Creating the parent folder might not be possible, for example due to a lack of permissions.
1598
+                                $this->logger->debug('Failed to create non-existent parent', ['exception' => $e, 'path' => $path . '/' . $entryName]);
1599
+                            }
1600
+                        }
1601
+
1602
+                        if (isset($files[$entryName])) {
1603
+                            $files[$entryName]->addSubEntry($rootEntry, $mountPoint);
1604
+                        }
1605
+                    } else { //mountpoint in this folder, add an entry for it
1606
+                        $rootEntry['name'] = $relativePath;
1607
+                        $rootEntry['type'] = $rootEntry['mimetype'] === 'httpd/unix-directory' ? 'dir' : 'file';
1608
+                        $permissions = $rootEntry['permissions'];
1609
+                        // do not allow renaming/deleting the mount point if they are not shared files/folders
1610
+                        // for shared files/folders we use the permissions given by the owner
1611
+                        if ($mount instanceof MoveableMount) {
1612
+                            $rootEntry['permissions'] = $permissions | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE;
1613
+                        } else {
1614
+                            $rootEntry['permissions'] = $permissions & (\OCP\Constants::PERMISSION_ALL - (\OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE));
1615
+                        }
1616
+
1617
+                        $rootEntry['path'] = substr(Filesystem::normalizePath($path . '/' . $rootEntry['name']), strlen($user) + 2); // full path without /$user/
1618
+
1619
+                        // if sharing was disabled for the user we remove the share permissions
1620
+                        if ($sharingDisabled) {
1621
+                            $rootEntry['permissions'] = $rootEntry['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
1622
+                        }
1623
+
1624
+                        $ownerId = $subStorage->getOwner('');
1625
+                        if ($ownerId !== false) {
1626
+                            $owner = $this->getUserObjectForOwner($ownerId);
1627
+                        } else {
1628
+                            $owner = null;
1629
+                        }
1630
+                        $files[$rootEntry->getName()] = new FileInfo($path . '/' . $rootEntry['name'], $subStorage, '', $rootEntry, $mount, $owner);
1631
+                    }
1632
+                }
1633
+            }
1634
+        }
1635
+
1636
+        if ($mimetype_filter) {
1637
+            $files = array_filter($files, function (FileInfo $file) use ($mimetype_filter) {
1638
+                if (strpos($mimetype_filter, '/')) {
1639
+                    return $file->getMimetype() === $mimetype_filter;
1640
+                } else {
1641
+                    return $file->getMimePart() === $mimetype_filter;
1642
+                }
1643
+            });
1644
+        }
1645
+
1646
+        return array_values($files);
1647
+    }
1648
+
1649
+    /**
1650
+     * change file metadata
1651
+     *
1652
+     * @param string $path
1653
+     * @param array|\OCP\Files\FileInfo $data
1654
+     * @return int
1655
+     *
1656
+     * returns the fileid of the updated file
1657
+     */
1658
+    public function putFileInfo($path, $data) {
1659
+        $this->assertPathLength($path);
1660
+        if ($data instanceof FileInfo) {
1661
+            $data = $data->getData();
1662
+        }
1663
+        $path = Filesystem::normalizePath($this->fakeRoot . '/' . $path);
1664
+        /**
1665
+         * @var Storage $storage
1666
+         * @var string $internalPath
1667
+         */
1668
+        [$storage, $internalPath] = Filesystem::resolvePath($path);
1669
+        if ($storage) {
1670
+            $cache = $storage->getCache($path);
1671
+
1672
+            if (!$cache->inCache($internalPath)) {
1673
+                $scanner = $storage->getScanner($internalPath);
1674
+                $scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW);
1675
+            }
1676
+
1677
+            return $cache->put($internalPath, $data);
1678
+        } else {
1679
+            return -1;
1680
+        }
1681
+    }
1682
+
1683
+    /**
1684
+     * search for files with the name matching $query
1685
+     *
1686
+     * @param string $query
1687
+     * @return FileInfo[]
1688
+     */
1689
+    public function search($query) {
1690
+        return $this->searchCommon('search', ['%' . $query . '%']);
1691
+    }
1692
+
1693
+    /**
1694
+     * search for files with the name matching $query
1695
+     *
1696
+     * @param string $query
1697
+     * @return FileInfo[]
1698
+     */
1699
+    public function searchRaw($query) {
1700
+        return $this->searchCommon('search', [$query]);
1701
+    }
1702
+
1703
+    /**
1704
+     * search for files by mimetype
1705
+     *
1706
+     * @param string $mimetype
1707
+     * @return FileInfo[]
1708
+     */
1709
+    public function searchByMime($mimetype) {
1710
+        return $this->searchCommon('searchByMime', [$mimetype]);
1711
+    }
1712
+
1713
+    /**
1714
+     * search for files by tag
1715
+     *
1716
+     * @param string|int $tag name or tag id
1717
+     * @param string $userId owner of the tags
1718
+     * @return FileInfo[]
1719
+     */
1720
+    public function searchByTag($tag, $userId) {
1721
+        return $this->searchCommon('searchByTag', [$tag, $userId]);
1722
+    }
1723
+
1724
+    /**
1725
+     * @param string $method cache method
1726
+     * @param array $args
1727
+     * @return FileInfo[]
1728
+     */
1729
+    private function searchCommon($method, $args) {
1730
+        $files = [];
1731
+        $rootLength = strlen($this->fakeRoot);
1732
+
1733
+        $mount = $this->getMount('');
1734
+        $mountPoint = $mount->getMountPoint();
1735
+        $storage = $mount->getStorage();
1736
+        $userManager = \OC::$server->getUserManager();
1737
+        if ($storage) {
1738
+            $cache = $storage->getCache('');
1739
+
1740
+            $results = call_user_func_array([$cache, $method], $args);
1741
+            foreach ($results as $result) {
1742
+                if (substr($mountPoint . $result['path'], 0, $rootLength + 1) === $this->fakeRoot . '/') {
1743
+                    $internalPath = $result['path'];
1744
+                    $path = $mountPoint . $result['path'];
1745
+                    $result['path'] = substr($mountPoint . $result['path'], $rootLength);
1746
+                    $ownerId = $storage->getOwner($internalPath);
1747
+                    if ($ownerId !== false) {
1748
+                        $owner = $userManager->get($ownerId);
1749
+                    } else {
1750
+                        $owner = null;
1751
+                    }
1752
+                    $files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner);
1753
+                }
1754
+            }
1755
+
1756
+            $mounts = Filesystem::getMountManager()->findIn($this->fakeRoot);
1757
+            foreach ($mounts as $mount) {
1758
+                $mountPoint = $mount->getMountPoint();
1759
+                $storage = $mount->getStorage();
1760
+                if ($storage) {
1761
+                    $cache = $storage->getCache('');
1762
+
1763
+                    $relativeMountPoint = substr($mountPoint, $rootLength);
1764
+                    $results = call_user_func_array([$cache, $method], $args);
1765
+                    if ($results) {
1766
+                        foreach ($results as $result) {
1767
+                            $internalPath = $result['path'];
1768
+                            $result['path'] = rtrim($relativeMountPoint . $result['path'], '/');
1769
+                            $path = rtrim($mountPoint . $internalPath, '/');
1770
+                            $ownerId = $storage->getOwner($internalPath);
1771
+                            if ($ownerId !== false) {
1772
+                                $owner = $userManager->get($ownerId);
1773
+                            } else {
1774
+                                $owner = null;
1775
+                            }
1776
+                            $files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner);
1777
+                        }
1778
+                    }
1779
+                }
1780
+            }
1781
+        }
1782
+        return $files;
1783
+    }
1784
+
1785
+    /**
1786
+     * Get the owner for a file or folder
1787
+     *
1788
+     * @throws NotFoundException
1789
+     */
1790
+    public function getOwner(string $path): string {
1791
+        $info = $this->getFileInfo($path);
1792
+        if (!$info) {
1793
+            throw new NotFoundException($path . ' not found while trying to get owner');
1794
+        }
1795
+
1796
+        if ($info->getOwner() === null) {
1797
+            throw new NotFoundException($path . ' has no owner');
1798
+        }
1799
+
1800
+        return $info->getOwner()->getUID();
1801
+    }
1802
+
1803
+    /**
1804
+     * get the ETag for a file or folder
1805
+     *
1806
+     * @param string $path
1807
+     * @return string|false
1808
+     */
1809
+    public function getETag($path) {
1810
+        [$storage, $internalPath] = $this->resolvePath($path);
1811
+        if ($storage) {
1812
+            return $storage->getETag($internalPath);
1813
+        } else {
1814
+            return false;
1815
+        }
1816
+    }
1817
+
1818
+    /**
1819
+     * Get the path of a file by id, relative to the view
1820
+     *
1821
+     * Note that the resulting path is not guaranteed to be unique for the id, multiple paths can point to the same file
1822
+     *
1823
+     * @param int $id
1824
+     * @param int|null $storageId
1825
+     * @return string
1826
+     * @throws NotFoundException
1827
+     */
1828
+    public function getPath($id, ?int $storageId = null): string {
1829
+        $id = (int)$id;
1830
+        $rootFolder = Server::get(Files\IRootFolder::class);
1831
+
1832
+        $node = $rootFolder->getFirstNodeByIdInPath($id, $this->getRoot());
1833
+        if ($node) {
1834
+            if ($storageId === null || $storageId === $node->getStorage()->getCache()->getNumericStorageId()) {
1835
+                return $this->getRelativePath($node->getPath()) ?? '';
1836
+            }
1837
+        } else {
1838
+            throw new NotFoundException(sprintf('File with id "%s" has not been found.', $id));
1839
+        }
1840
+
1841
+        foreach ($rootFolder->getByIdInPath($id, $this->getRoot()) as $node) {
1842
+            if ($storageId === $node->getStorage()->getCache()->getNumericStorageId()) {
1843
+                return $this->getRelativePath($node->getPath()) ?? '';
1844
+            }
1845
+        }
1846
+
1847
+        throw new NotFoundException(sprintf('File with id "%s" has not been found.', $id));
1848
+    }
1849
+
1850
+    /**
1851
+     * @param string $path
1852
+     * @throws InvalidPathException
1853
+     */
1854
+    private function assertPathLength($path): void {
1855
+        $maxLen = min(PHP_MAXPATHLEN, 4000);
1856
+        // Check for the string length - performed using isset() instead of strlen()
1857
+        // because isset() is about 5x-40x faster.
1858
+        if (isset($path[$maxLen])) {
1859
+            $pathLen = strlen($path);
1860
+            throw new InvalidPathException("Path length($pathLen) exceeds max path length($maxLen): $path");
1861
+        }
1862
+    }
1863
+
1864
+    /**
1865
+     * check if it is allowed to move a mount point to a given target.
1866
+     * It is not allowed to move a mount point into a different mount point or
1867
+     * into an already shared folder
1868
+     */
1869
+    private function targetIsNotShared(string $user, string $targetPath): bool {
1870
+        $providers = [
1871
+            IShare::TYPE_USER,
1872
+            IShare::TYPE_GROUP,
1873
+            IShare::TYPE_EMAIL,
1874
+            IShare::TYPE_CIRCLE,
1875
+            IShare::TYPE_ROOM,
1876
+            IShare::TYPE_DECK,
1877
+        ];
1878
+        $shareManager = Server::get(IManager::class);
1879
+        /** @var IShare[] $shares */
1880
+        $shares = array_merge(...array_map(function (int $type) use ($shareManager, $user) {
1881
+            return $shareManager->getSharesBy($user, $type);
1882
+        }, $providers));
1883
+
1884
+        foreach ($shares as $share) {
1885
+            try {
1886
+                $sharedPath = $share->getNode()->getPath();
1887
+            } catch (NotFoundException $e) {
1888
+                // node is not found, ignoring
1889
+                $this->logger->debug(
1890
+                    'Could not find the node linked to a share',
1891
+                    ['app' => 'files', 'exception' => $e]);
1892
+                continue;
1893
+            }
1894
+            if ($targetPath === $sharedPath || str_starts_with($targetPath, $sharedPath . '/')) {
1895
+                $this->logger->debug(
1896
+                    'It is not allowed to move one mount point into a shared folder',
1897
+                    ['app' => 'files']);
1898
+                return false;
1899
+            }
1900
+        }
1901
+
1902
+        return true;
1903
+    }
1904
+
1905
+    /**
1906
+     * Get a fileinfo object for files that are ignored in the cache (part files)
1907
+     */
1908
+    private function getPartFileInfo(string $path): \OC\Files\FileInfo {
1909
+        $mount = $this->getMount($path);
1910
+        $storage = $mount->getStorage();
1911
+        $internalPath = $mount->getInternalPath($this->getAbsolutePath($path));
1912
+        $ownerId = $storage->getOwner($internalPath);
1913
+        if ($ownerId !== false) {
1914
+            $owner = Server::get(IUserManager::class)->get($ownerId);
1915
+        } else {
1916
+            $owner = null;
1917
+        }
1918
+        return new FileInfo(
1919
+            $this->getAbsolutePath($path),
1920
+            $storage,
1921
+            $internalPath,
1922
+            [
1923
+                'fileid' => null,
1924
+                'mimetype' => $storage->getMimeType($internalPath),
1925
+                'name' => basename($path),
1926
+                'etag' => null,
1927
+                'size' => $storage->filesize($internalPath),
1928
+                'mtime' => $storage->filemtime($internalPath),
1929
+                'encrypted' => false,
1930
+                'permissions' => \OCP\Constants::PERMISSION_ALL
1931
+            ],
1932
+            $mount,
1933
+            $owner
1934
+        );
1935
+    }
1936
+
1937
+    /**
1938
+     * @param string $path
1939
+     * @param string $fileName
1940
+     * @param bool $readonly Check only if the path is allowed for read-only access
1941
+     * @throws InvalidPathException
1942
+     */
1943
+    public function verifyPath($path, $fileName, $readonly = false): void {
1944
+        // All of the view's functions disallow '..' in the path so we can short cut if the path is invalid
1945
+        if (!Filesystem::isValidPath($path ?: '/')) {
1946
+            $l = \OCP\Util::getL10N('lib');
1947
+            throw new InvalidPathException($l->t('Path contains invalid segments'));
1948
+        }
1949
+
1950
+        // Short cut for read-only validation
1951
+        if ($readonly) {
1952
+            $validator = Server::get(FilenameValidator::class);
1953
+            if ($validator->isForbidden($fileName)) {
1954
+                $l = \OCP\Util::getL10N('lib');
1955
+                throw new InvalidPathException($l->t('Filename is a reserved word'));
1956
+            }
1957
+            return;
1958
+        }
1959
+
1960
+        try {
1961
+            /** @type \OCP\Files\Storage $storage */
1962
+            [$storage, $internalPath] = $this->resolvePath($path);
1963
+            $storage->verifyPath($internalPath, $fileName);
1964
+        } catch (ReservedWordException $ex) {
1965
+            $l = \OCP\Util::getL10N('lib');
1966
+            throw new InvalidPathException($ex->getMessage() ?: $l->t('Filename is a reserved word'));
1967
+        } catch (InvalidCharacterInPathException $ex) {
1968
+            $l = \OCP\Util::getL10N('lib');
1969
+            throw new InvalidPathException($ex->getMessage() ?: $l->t('Filename contains at least one invalid character'));
1970
+        } catch (FileNameTooLongException $ex) {
1971
+            $l = \OCP\Util::getL10N('lib');
1972
+            throw new InvalidPathException($l->t('Filename is too long'));
1973
+        } catch (InvalidDirectoryException $ex) {
1974
+            $l = \OCP\Util::getL10N('lib');
1975
+            throw new InvalidPathException($l->t('Dot files are not allowed'));
1976
+        } catch (EmptyFileNameException $ex) {
1977
+            $l = \OCP\Util::getL10N('lib');
1978
+            throw new InvalidPathException($l->t('Empty filename is not allowed'));
1979
+        }
1980
+    }
1981
+
1982
+    /**
1983
+     * get all parent folders of $path
1984
+     *
1985
+     * @param string $path
1986
+     * @return string[]
1987
+     */
1988
+    private function getParents($path) {
1989
+        $path = trim($path, '/');
1990
+        if (!$path) {
1991
+            return [];
1992
+        }
1993
+
1994
+        $parts = explode('/', $path);
1995
+
1996
+        // remove the single file
1997
+        array_pop($parts);
1998
+        $result = ['/'];
1999
+        $resultPath = '';
2000
+        foreach ($parts as $part) {
2001
+            if ($part) {
2002
+                $resultPath .= '/' . $part;
2003
+                $result[] = $resultPath;
2004
+            }
2005
+        }
2006
+        return $result;
2007
+    }
2008
+
2009
+    /**
2010
+     * Returns the mount point for which to lock
2011
+     *
2012
+     * @param string $absolutePath absolute path
2013
+     * @param bool $useParentMount true to return parent mount instead of whatever
2014
+     *                             is mounted directly on the given path, false otherwise
2015
+     * @return IMountPoint mount point for which to apply locks
2016
+     */
2017
+    private function getMountForLock(string $absolutePath, bool $useParentMount = false): IMountPoint {
2018
+        $mount = Filesystem::getMountManager()->find($absolutePath);
2019
+
2020
+        if ($useParentMount) {
2021
+            // find out if something is mounted directly on the path
2022
+            $internalPath = $mount->getInternalPath($absolutePath);
2023
+            if ($internalPath === '') {
2024
+                // resolve the parent mount instead
2025
+                $mount = Filesystem::getMountManager()->find(dirname($absolutePath));
2026
+            }
2027
+        }
2028
+
2029
+        return $mount;
2030
+    }
2031
+
2032
+    /**
2033
+     * Lock the given path
2034
+     *
2035
+     * @param string $path the path of the file to lock, relative to the view
2036
+     * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2037
+     * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2038
+     *
2039
+     * @return bool False if the path is excluded from locking, true otherwise
2040
+     * @throws LockedException if the path is already locked
2041
+     */
2042
+    private function lockPath($path, $type, $lockMountPoint = false) {
2043
+        $absolutePath = $this->getAbsolutePath($path);
2044
+        $absolutePath = Filesystem::normalizePath($absolutePath);
2045
+        if (!$this->shouldLockFile($absolutePath)) {
2046
+            return false;
2047
+        }
2048
+
2049
+        $mount = $this->getMountForLock($absolutePath, $lockMountPoint);
2050
+        try {
2051
+            $storage = $mount->getStorage();
2052
+            if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
2053
+                $storage->acquireLock(
2054
+                    $mount->getInternalPath($absolutePath),
2055
+                    $type,
2056
+                    $this->lockingProvider
2057
+                );
2058
+            }
2059
+        } catch (LockedException $e) {
2060
+            // rethrow with the human-readable path
2061
+            throw new LockedException(
2062
+                $path,
2063
+                $e,
2064
+                $e->getExistingLock()
2065
+            );
2066
+        }
2067
+
2068
+        return true;
2069
+    }
2070
+
2071
+    /**
2072
+     * Change the lock type
2073
+     *
2074
+     * @param string $path the path of the file to lock, relative to the view
2075
+     * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2076
+     * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2077
+     *
2078
+     * @return bool False if the path is excluded from locking, true otherwise
2079
+     * @throws LockedException if the path is already locked
2080
+     */
2081
+    public function changeLock($path, $type, $lockMountPoint = false) {
2082
+        $path = Filesystem::normalizePath($path);
2083
+        $absolutePath = $this->getAbsolutePath($path);
2084
+        $absolutePath = Filesystem::normalizePath($absolutePath);
2085
+        if (!$this->shouldLockFile($absolutePath)) {
2086
+            return false;
2087
+        }
2088
+
2089
+        $mount = $this->getMountForLock($absolutePath, $lockMountPoint);
2090
+        try {
2091
+            $storage = $mount->getStorage();
2092
+            if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
2093
+                $storage->changeLock(
2094
+                    $mount->getInternalPath($absolutePath),
2095
+                    $type,
2096
+                    $this->lockingProvider
2097
+                );
2098
+            }
2099
+        } catch (LockedException $e) {
2100
+            // rethrow with the a human-readable path
2101
+            throw new LockedException(
2102
+                $path,
2103
+                $e,
2104
+                $e->getExistingLock()
2105
+            );
2106
+        }
2107
+
2108
+        return true;
2109
+    }
2110
+
2111
+    /**
2112
+     * Unlock the given path
2113
+     *
2114
+     * @param string $path the path of the file to unlock, relative to the view
2115
+     * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2116
+     * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2117
+     *
2118
+     * @return bool False if the path is excluded from locking, true otherwise
2119
+     * @throws LockedException
2120
+     */
2121
+    private function unlockPath($path, $type, $lockMountPoint = false) {
2122
+        $absolutePath = $this->getAbsolutePath($path);
2123
+        $absolutePath = Filesystem::normalizePath($absolutePath);
2124
+        if (!$this->shouldLockFile($absolutePath)) {
2125
+            return false;
2126
+        }
2127
+
2128
+        $mount = $this->getMountForLock($absolutePath, $lockMountPoint);
2129
+        $storage = $mount->getStorage();
2130
+        if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
2131
+            $storage->releaseLock(
2132
+                $mount->getInternalPath($absolutePath),
2133
+                $type,
2134
+                $this->lockingProvider
2135
+            );
2136
+        }
2137
+
2138
+        return true;
2139
+    }
2140
+
2141
+    /**
2142
+     * Lock a path and all its parents up to the root of the view
2143
+     *
2144
+     * @param string $path the path of the file to lock relative to the view
2145
+     * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2146
+     * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2147
+     *
2148
+     * @return bool False if the path is excluded from locking, true otherwise
2149
+     * @throws LockedException
2150
+     */
2151
+    public function lockFile($path, $type, $lockMountPoint = false) {
2152
+        $absolutePath = $this->getAbsolutePath($path);
2153
+        $absolutePath = Filesystem::normalizePath($absolutePath);
2154
+        if (!$this->shouldLockFile($absolutePath)) {
2155
+            return false;
2156
+        }
2157
+
2158
+        $this->lockPath($path, $type, $lockMountPoint);
2159
+
2160
+        $parents = $this->getParents($path);
2161
+        foreach ($parents as $parent) {
2162
+            $this->lockPath($parent, ILockingProvider::LOCK_SHARED);
2163
+        }
2164
+
2165
+        return true;
2166
+    }
2167
+
2168
+    /**
2169
+     * Unlock a path and all its parents up to the root of the view
2170
+     *
2171
+     * @param string $path the path of the file to lock relative to the view
2172
+     * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2173
+     * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2174
+     *
2175
+     * @return bool False if the path is excluded from locking, true otherwise
2176
+     * @throws LockedException
2177
+     */
2178
+    public function unlockFile($path, $type, $lockMountPoint = false) {
2179
+        $absolutePath = $this->getAbsolutePath($path);
2180
+        $absolutePath = Filesystem::normalizePath($absolutePath);
2181
+        if (!$this->shouldLockFile($absolutePath)) {
2182
+            return false;
2183
+        }
2184
+
2185
+        $this->unlockPath($path, $type, $lockMountPoint);
2186
+
2187
+        $parents = $this->getParents($path);
2188
+        foreach ($parents as $parent) {
2189
+            $this->unlockPath($parent, ILockingProvider::LOCK_SHARED);
2190
+        }
2191
+
2192
+        return true;
2193
+    }
2194
+
2195
+    /**
2196
+     * Only lock files in data/user/files/
2197
+     *
2198
+     * @param string $path Absolute path to the file/folder we try to (un)lock
2199
+     * @return bool
2200
+     */
2201
+    protected function shouldLockFile($path) {
2202
+        $path = Filesystem::normalizePath($path);
2203
+
2204
+        $pathSegments = explode('/', $path);
2205
+        if (isset($pathSegments[2])) {
2206
+            // E.g.: /username/files/path-to-file
2207
+            return ($pathSegments[2] === 'files') && (count($pathSegments) > 3);
2208
+        }
2209
+
2210
+        return !str_starts_with($path, '/appdata_');
2211
+    }
2212
+
2213
+    /**
2214
+     * Shortens the given absolute path to be relative to
2215
+     * "$user/files".
2216
+     *
2217
+     * @param string $absolutePath absolute path which is under "files"
2218
+     *
2219
+     * @return string path relative to "files" with trimmed slashes or null
2220
+     *                if the path was NOT relative to files
2221
+     *
2222
+     * @throws \InvalidArgumentException if the given path was not under "files"
2223
+     * @since 8.1.0
2224
+     */
2225
+    public function getPathRelativeToFiles($absolutePath) {
2226
+        $path = Filesystem::normalizePath($absolutePath);
2227
+        $parts = explode('/', trim($path, '/'), 3);
2228
+        // "$user", "files", "path/to/dir"
2229
+        if (!isset($parts[1]) || $parts[1] !== 'files') {
2230
+            $this->logger->error(
2231
+                '$absolutePath must be relative to "files", value is "{absolutePath}"',
2232
+                [
2233
+                    'absolutePath' => $absolutePath,
2234
+                ]
2235
+            );
2236
+            throw new \InvalidArgumentException('$absolutePath must be relative to "files"');
2237
+        }
2238
+        if (isset($parts[2])) {
2239
+            return $parts[2];
2240
+        }
2241
+        return '';
2242
+    }
2243
+
2244
+    /**
2245
+     * @param string $filename
2246
+     * @return array
2247
+     * @throws \OC\User\NoUserException
2248
+     * @throws NotFoundException
2249
+     */
2250
+    public function getUidAndFilename($filename) {
2251
+        $info = $this->getFileInfo($filename);
2252
+        if (!$info instanceof \OCP\Files\FileInfo) {
2253
+            throw new NotFoundException($this->getAbsolutePath($filename) . ' not found');
2254
+        }
2255
+        $uid = $info->getOwner()->getUID();
2256
+        if ($uid != \OC_User::getUser()) {
2257
+            Filesystem::initMountPoints($uid);
2258
+            $ownerView = new View('/' . $uid . '/files');
2259
+            try {
2260
+                $filename = $ownerView->getPath($info['fileid']);
2261
+            } catch (NotFoundException $e) {
2262
+                throw new NotFoundException('File with id ' . $info['fileid'] . ' not found for user ' . $uid);
2263
+            }
2264
+        }
2265
+        return [$uid, $filename];
2266
+    }
2267
+
2268
+    /**
2269
+     * Creates parent non-existing folders
2270
+     *
2271
+     * @param string $filePath
2272
+     * @return bool
2273
+     */
2274
+    private function createParentDirectories($filePath) {
2275
+        $directoryParts = explode('/', $filePath);
2276
+        $directoryParts = array_filter($directoryParts);
2277
+        foreach ($directoryParts as $key => $part) {
2278
+            $currentPathElements = array_slice($directoryParts, 0, $key);
2279
+            $currentPath = '/' . implode('/', $currentPathElements);
2280
+            if ($this->is_file($currentPath)) {
2281
+                return false;
2282
+            }
2283
+            if (!$this->file_exists($currentPath)) {
2284
+                $this->mkdir($currentPath);
2285
+            }
2286
+        }
2287
+
2288
+        return true;
2289
+    }
2290 2290
 }
Please login to merge, or discard this patch.
lib/private/Share20/ProviderFactory.php 1 patch
Indentation   +199 added lines, -199 removed lines patch added patch discarded remove patch
@@ -27,209 +27,209 @@
 block discarded – undo
27 27
  * @package OC\Share20
28 28
  */
29 29
 class ProviderFactory implements IProviderFactory {
30
-	private ?DefaultShareProvider $defaultProvider = null;
31
-	private ?FederatedShareProvider $federatedProvider = null;
32
-	private ?ShareByMailProvider $shareByMailProvider = null;
33
-	/**
34
-	 * @psalm-suppress UndefinedDocblockClass
35
-	 * @var ?RoomShareProvider
36
-	 */
37
-	private $roomShareProvider = null;
38
-
39
-	private array $registeredShareProviders = [];
40
-
41
-	private array $shareProviders = [];
42
-
43
-	public function __construct(
44
-		protected IAppManager $appManager,
45
-		protected LoggerInterface $logger,
46
-	) {
47
-	}
48
-
49
-	public function registerProvider(string $shareProviderClass): void {
50
-		$this->registeredShareProviders[] = $shareProviderClass;
51
-	}
52
-
53
-	/**
54
-	 * Create the default share provider.
55
-	 */
56
-	protected function defaultShareProvider(): DefaultShareProvider {
57
-		return Server::get(DefaultShareProvider::class);
58
-	}
59
-
60
-	/**
61
-	 * Create the federated share provider
62
-	 */
63
-	protected function federatedShareProvider(): ?FederatedShareProvider {
64
-		if ($this->federatedProvider === null) {
65
-			/*
30
+    private ?DefaultShareProvider $defaultProvider = null;
31
+    private ?FederatedShareProvider $federatedProvider = null;
32
+    private ?ShareByMailProvider $shareByMailProvider = null;
33
+    /**
34
+     * @psalm-suppress UndefinedDocblockClass
35
+     * @var ?RoomShareProvider
36
+     */
37
+    private $roomShareProvider = null;
38
+
39
+    private array $registeredShareProviders = [];
40
+
41
+    private array $shareProviders = [];
42
+
43
+    public function __construct(
44
+        protected IAppManager $appManager,
45
+        protected LoggerInterface $logger,
46
+    ) {
47
+    }
48
+
49
+    public function registerProvider(string $shareProviderClass): void {
50
+        $this->registeredShareProviders[] = $shareProviderClass;
51
+    }
52
+
53
+    /**
54
+     * Create the default share provider.
55
+     */
56
+    protected function defaultShareProvider(): DefaultShareProvider {
57
+        return Server::get(DefaultShareProvider::class);
58
+    }
59
+
60
+    /**
61
+     * Create the federated share provider
62
+     */
63
+    protected function federatedShareProvider(): ?FederatedShareProvider {
64
+        if ($this->federatedProvider === null) {
65
+            /*
66 66
 			 * Check if the app is enabled
67 67
 			 */
68
-			if (!$this->appManager->isEnabledForUser('federatedfilesharing')) {
69
-				return null;
70
-			}
71
-
72
-			$this->federatedProvider = Server::get(FederatedShareProvider::class);
73
-		}
74
-
75
-		return $this->federatedProvider;
76
-	}
77
-
78
-	/**
79
-	 * Create the federated share provider
80
-	 */
81
-	protected function getShareByMailProvider(): ?ShareByMailProvider {
82
-		if ($this->shareByMailProvider === null) {
83
-			/*
68
+            if (!$this->appManager->isEnabledForUser('federatedfilesharing')) {
69
+                return null;
70
+            }
71
+
72
+            $this->federatedProvider = Server::get(FederatedShareProvider::class);
73
+        }
74
+
75
+        return $this->federatedProvider;
76
+    }
77
+
78
+    /**
79
+     * Create the federated share provider
80
+     */
81
+    protected function getShareByMailProvider(): ?ShareByMailProvider {
82
+        if ($this->shareByMailProvider === null) {
83
+            /*
84 84
 			 * Check if the app is enabled
85 85
 			 */
86
-			if (!$this->appManager->isEnabledForUser('sharebymail')) {
87
-				return null;
88
-			}
89
-
90
-			$this->shareByMailProvider = Server::get(ShareByMailProvider::class);
91
-		}
92
-
93
-		return $this->shareByMailProvider;
94
-	}
95
-
96
-	/**
97
-	 * Create the room share provider
98
-	 *
99
-	 * @psalm-suppress UndefinedDocblockClass
100
-	 * @return ?RoomShareProvider
101
-	 */
102
-	protected function getRoomShareProvider() {
103
-		if ($this->roomShareProvider === null) {
104
-			/*
86
+            if (!$this->appManager->isEnabledForUser('sharebymail')) {
87
+                return null;
88
+            }
89
+
90
+            $this->shareByMailProvider = Server::get(ShareByMailProvider::class);
91
+        }
92
+
93
+        return $this->shareByMailProvider;
94
+    }
95
+
96
+    /**
97
+     * Create the room share provider
98
+     *
99
+     * @psalm-suppress UndefinedDocblockClass
100
+     * @return ?RoomShareProvider
101
+     */
102
+    protected function getRoomShareProvider() {
103
+        if ($this->roomShareProvider === null) {
104
+            /*
105 105
 			 * Check if the app is enabled
106 106
 			 */
107
-			if (!$this->appManager->isEnabledForUser('spreed')) {
108
-				return null;
109
-			}
110
-
111
-			try {
112
-				/**
113
-				 * @psalm-suppress UndefinedClass
114
-				 */
115
-				$this->roomShareProvider = Server::get(RoomShareProvider::class);
116
-			} catch (\Throwable $e) {
117
-				$this->logger->error(
118
-					$e->getMessage(),
119
-					['exception' => $e]
120
-				);
121
-				return null;
122
-			}
123
-		}
124
-
125
-		return $this->roomShareProvider;
126
-	}
127
-
128
-	/**
129
-	 * @inheritdoc
130
-	 */
131
-	public function getProvider($id) {
132
-		$provider = null;
133
-		if (isset($this->shareProviders[$id])) {
134
-			return $this->shareProviders[$id];
135
-		}
136
-
137
-		if ($id === 'ocinternal') {
138
-			$provider = $this->defaultShareProvider();
139
-		} elseif ($id === 'ocFederatedSharing') {
140
-			$provider = $this->federatedShareProvider();
141
-		} elseif ($id === 'ocMailShare') {
142
-			$provider = $this->getShareByMailProvider();
143
-		} elseif ($id === 'ocRoomShare') {
144
-			$provider = $this->getRoomShareProvider();
145
-		}
146
-
147
-		foreach ($this->registeredShareProviders as $shareProvider) {
148
-			try {
149
-				/** @var IShareProvider $instance */
150
-				$instance = Server::get($shareProvider);
151
-				$this->shareProviders[$instance->identifier()] = $instance;
152
-			} catch (\Throwable $e) {
153
-				$this->logger->error(
154
-					$e->getMessage(),
155
-					['exception' => $e]
156
-				);
157
-			}
158
-		}
159
-
160
-		if (isset($this->shareProviders[$id])) {
161
-			$provider = $this->shareProviders[$id];
162
-		}
163
-
164
-		if ($provider === null) {
165
-			throw new ProviderException('No provider with id .' . $id . ' found.');
166
-		}
167
-
168
-		return $provider;
169
-	}
170
-
171
-	/**
172
-	 * @inheritdoc
173
-	 */
174
-	public function getProviderForType($shareType) {
175
-		$provider = null;
176
-
177
-		if ($shareType === IShare::TYPE_USER
178
-			|| $shareType === IShare::TYPE_GROUP
179
-			|| $shareType === IShare::TYPE_LINK
180
-		) {
181
-			$provider = $this->defaultShareProvider();
182
-		} elseif ($shareType === IShare::TYPE_REMOTE || $shareType === IShare::TYPE_REMOTE_GROUP) {
183
-			$provider = $this->federatedShareProvider();
184
-		} elseif ($shareType === IShare::TYPE_EMAIL) {
185
-			$provider = $this->getShareByMailProvider();
186
-		} elseif ($shareType === IShare::TYPE_CIRCLE) {
187
-			$provider = $this->getProvider('ocCircleShare');
188
-		} elseif ($shareType === IShare::TYPE_ROOM) {
189
-			$provider = $this->getRoomShareProvider();
190
-		} elseif ($shareType === IShare::TYPE_DECK) {
191
-			$provider = $this->getProvider('deck');
192
-		}
193
-
194
-
195
-		if ($provider === null) {
196
-			throw new ProviderException('No share provider for share type ' . $shareType);
197
-		}
198
-
199
-		return $provider;
200
-	}
201
-
202
-	public function getAllProviders() {
203
-		$shares = [$this->defaultShareProvider(), $this->federatedShareProvider()];
204
-		$shareByMail = $this->getShareByMailProvider();
205
-		if ($shareByMail !== null) {
206
-			$shares[] = $shareByMail;
207
-		}
208
-		$roomShare = $this->getRoomShareProvider();
209
-		if ($roomShare !== null) {
210
-			$shares[] = $roomShare;
211
-		}
212
-
213
-		foreach ($this->registeredShareProviders as $shareProvider) {
214
-			try {
215
-				/** @var IShareProvider $instance */
216
-				$instance = Server::get($shareProvider);
217
-			} catch (\Throwable $e) {
218
-				$this->logger->error(
219
-					$e->getMessage(),
220
-					['exception' => $e]
221
-				);
222
-				continue;
223
-			}
224
-
225
-			if (!isset($this->shareProviders[$instance->identifier()])) {
226
-				$this->shareProviders[$instance->identifier()] = $instance;
227
-			}
228
-			$shares[] = $this->shareProviders[$instance->identifier()];
229
-		}
230
-
231
-
232
-
233
-		return $shares;
234
-	}
107
+            if (!$this->appManager->isEnabledForUser('spreed')) {
108
+                return null;
109
+            }
110
+
111
+            try {
112
+                /**
113
+                 * @psalm-suppress UndefinedClass
114
+                 */
115
+                $this->roomShareProvider = Server::get(RoomShareProvider::class);
116
+            } catch (\Throwable $e) {
117
+                $this->logger->error(
118
+                    $e->getMessage(),
119
+                    ['exception' => $e]
120
+                );
121
+                return null;
122
+            }
123
+        }
124
+
125
+        return $this->roomShareProvider;
126
+    }
127
+
128
+    /**
129
+     * @inheritdoc
130
+     */
131
+    public function getProvider($id) {
132
+        $provider = null;
133
+        if (isset($this->shareProviders[$id])) {
134
+            return $this->shareProviders[$id];
135
+        }
136
+
137
+        if ($id === 'ocinternal') {
138
+            $provider = $this->defaultShareProvider();
139
+        } elseif ($id === 'ocFederatedSharing') {
140
+            $provider = $this->federatedShareProvider();
141
+        } elseif ($id === 'ocMailShare') {
142
+            $provider = $this->getShareByMailProvider();
143
+        } elseif ($id === 'ocRoomShare') {
144
+            $provider = $this->getRoomShareProvider();
145
+        }
146
+
147
+        foreach ($this->registeredShareProviders as $shareProvider) {
148
+            try {
149
+                /** @var IShareProvider $instance */
150
+                $instance = Server::get($shareProvider);
151
+                $this->shareProviders[$instance->identifier()] = $instance;
152
+            } catch (\Throwable $e) {
153
+                $this->logger->error(
154
+                    $e->getMessage(),
155
+                    ['exception' => $e]
156
+                );
157
+            }
158
+        }
159
+
160
+        if (isset($this->shareProviders[$id])) {
161
+            $provider = $this->shareProviders[$id];
162
+        }
163
+
164
+        if ($provider === null) {
165
+            throw new ProviderException('No provider with id .' . $id . ' found.');
166
+        }
167
+
168
+        return $provider;
169
+    }
170
+
171
+    /**
172
+     * @inheritdoc
173
+     */
174
+    public function getProviderForType($shareType) {
175
+        $provider = null;
176
+
177
+        if ($shareType === IShare::TYPE_USER
178
+            || $shareType === IShare::TYPE_GROUP
179
+            || $shareType === IShare::TYPE_LINK
180
+        ) {
181
+            $provider = $this->defaultShareProvider();
182
+        } elseif ($shareType === IShare::TYPE_REMOTE || $shareType === IShare::TYPE_REMOTE_GROUP) {
183
+            $provider = $this->federatedShareProvider();
184
+        } elseif ($shareType === IShare::TYPE_EMAIL) {
185
+            $provider = $this->getShareByMailProvider();
186
+        } elseif ($shareType === IShare::TYPE_CIRCLE) {
187
+            $provider = $this->getProvider('ocCircleShare');
188
+        } elseif ($shareType === IShare::TYPE_ROOM) {
189
+            $provider = $this->getRoomShareProvider();
190
+        } elseif ($shareType === IShare::TYPE_DECK) {
191
+            $provider = $this->getProvider('deck');
192
+        }
193
+
194
+
195
+        if ($provider === null) {
196
+            throw new ProviderException('No share provider for share type ' . $shareType);
197
+        }
198
+
199
+        return $provider;
200
+    }
201
+
202
+    public function getAllProviders() {
203
+        $shares = [$this->defaultShareProvider(), $this->federatedShareProvider()];
204
+        $shareByMail = $this->getShareByMailProvider();
205
+        if ($shareByMail !== null) {
206
+            $shares[] = $shareByMail;
207
+        }
208
+        $roomShare = $this->getRoomShareProvider();
209
+        if ($roomShare !== null) {
210
+            $shares[] = $roomShare;
211
+        }
212
+
213
+        foreach ($this->registeredShareProviders as $shareProvider) {
214
+            try {
215
+                /** @var IShareProvider $instance */
216
+                $instance = Server::get($shareProvider);
217
+            } catch (\Throwable $e) {
218
+                $this->logger->error(
219
+                    $e->getMessage(),
220
+                    ['exception' => $e]
221
+                );
222
+                continue;
223
+            }
224
+
225
+            if (!isset($this->shareProviders[$instance->identifier()])) {
226
+                $this->shareProviders[$instance->identifier()] = $instance;
227
+            }
228
+            $shares[] = $this->shareProviders[$instance->identifier()];
229
+        }
230
+
231
+
232
+
233
+        return $shares;
234
+    }
235 235
 }
Please login to merge, or discard this patch.
lib/private/Share20/Manager.php 1 patch
Indentation   +1801 added lines, -1801 removed lines patch added patch discarded remove patch
@@ -63,1822 +63,1822 @@
 block discarded – undo
63 63
  */
64 64
 class Manager implements IManager {
65 65
 
66
-	private ?IL10N $l;
67
-	private LegacyHooks $legacyHooks;
68
-
69
-	public function __construct(
70
-		private LoggerInterface $logger,
71
-		private IConfig $config,
72
-		private ISecureRandom $secureRandom,
73
-		private IHasher $hasher,
74
-		private IMountManager $mountManager,
75
-		private IGroupManager $groupManager,
76
-		private IFactory $l10nFactory,
77
-		private IProviderFactory $factory,
78
-		private IUserManager $userManager,
79
-		private IRootFolder $rootFolder,
80
-		private IEventDispatcher $dispatcher,
81
-		private IUserSession $userSession,
82
-		private KnownUserService $knownUserService,
83
-		private ShareDisableChecker $shareDisableChecker,
84
-		private IDateTimeZone $dateTimeZone,
85
-		private IAppConfig $appConfig,
86
-	) {
87
-		$this->l = $this->l10nFactory->get('lib');
88
-		// The constructor of LegacyHooks registers the listeners of share events
89
-		// do not remove if those are not properly migrated
90
-		$this->legacyHooks = new LegacyHooks($this->dispatcher);
91
-	}
92
-
93
-	/**
94
-	 * Convert from a full share id to a tuple (providerId, shareId)
95
-	 *
96
-	 * @return string[]
97
-	 */
98
-	private function splitFullId(string $id): array {
99
-		return explode(':', $id, 2);
100
-	}
101
-
102
-	/**
103
-	 * Verify if a password meets all requirements
104
-	 *
105
-	 * @throws HintException
106
-	 */
107
-	protected function verifyPassword(?string $password): void {
108
-		if ($password === null) {
109
-			// No password is set, check if this is allowed.
110
-			if ($this->shareApiLinkEnforcePassword()) {
111
-				throw new \InvalidArgumentException($this->l->t('Passwords are enforced for link and mail shares'));
112
-			}
113
-
114
-			return;
115
-		}
116
-
117
-		// Let others verify the password
118
-		try {
119
-			$event = new ValidatePasswordPolicyEvent($password, PasswordContext::SHARING);
120
-			$this->dispatcher->dispatchTyped($event);
121
-		} catch (HintException $e) {
122
-			/* Wrap in a 400 bad request error */
123
-			throw new HintException($e->getMessage(), $e->getHint(), 400, $e);
124
-		}
125
-	}
126
-
127
-	/**
128
-	 * Check for generic requirements before creating a share
129
-	 *
130
-	 * @param IShare $share
131
-	 * @throws \InvalidArgumentException
132
-	 * @throws GenericShareException
133
-	 *
134
-	 * @suppress PhanUndeclaredClassMethod
135
-	 */
136
-	protected function generalCreateChecks(IShare $share, bool $isUpdate = false): void {
137
-		if ($share->getShareType() === IShare::TYPE_USER) {
138
-			// We expect a valid user as sharedWith for user shares
139
-			if (!$this->userManager->userExists($share->getSharedWith())) {
140
-				throw new \InvalidArgumentException($this->l->t('Share recipient is not a valid user'));
141
-			}
142
-		} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
143
-			// We expect a valid group as sharedWith for group shares
144
-			if (!$this->groupManager->groupExists($share->getSharedWith())) {
145
-				throw new \InvalidArgumentException($this->l->t('Share recipient is not a valid group'));
146
-			}
147
-		} elseif ($share->getShareType() === IShare::TYPE_LINK) {
148
-			// No check for TYPE_EMAIL here as we have a recipient for them
149
-			if ($share->getSharedWith() !== null) {
150
-				throw new \InvalidArgumentException($this->l->t('Share recipient should be empty'));
151
-			}
152
-		} elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
153
-			if ($share->getSharedWith() === null) {
154
-				throw new \InvalidArgumentException($this->l->t('Share recipient should not be empty'));
155
-			}
156
-		} elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
157
-			if ($share->getSharedWith() === null) {
158
-				throw new \InvalidArgumentException($this->l->t('Share recipient should not be empty'));
159
-			}
160
-		} elseif ($share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
161
-			if ($share->getSharedWith() === null) {
162
-				throw new \InvalidArgumentException($this->l->t('Share recipient should not be empty'));
163
-			}
164
-		} elseif ($share->getShareType() === IShare::TYPE_CIRCLE) {
165
-			$circle = \OCA\Circles\Api\v1\Circles::detailsCircle($share->getSharedWith());
166
-			if ($circle === null) {
167
-				throw new \InvalidArgumentException($this->l->t('Share recipient is not a valid circle'));
168
-			}
169
-		} elseif ($share->getShareType() !== IShare::TYPE_ROOM && $share->getShareType() !== IShare::TYPE_DECK) {
170
-			// We cannot handle other types yet
171
-			throw new \InvalidArgumentException($this->l->t('Unknown share type'));
172
-		}
173
-
174
-		// Verify the initiator of the share is set
175
-		if ($share->getSharedBy() === null) {
176
-			throw new \InvalidArgumentException($this->l->t('Share initiator must be set'));
177
-		}
178
-
179
-		// Cannot share with yourself
180
-		if ($share->getShareType() === IShare::TYPE_USER
181
-			&& $share->getSharedWith() === $share->getSharedBy()) {
182
-			throw new \InvalidArgumentException($this->l->t('Cannot share with yourself'));
183
-		}
184
-
185
-		// The path should be set
186
-		if ($share->getNode() === null) {
187
-			throw new \InvalidArgumentException($this->l->t('Shared path must be set'));
188
-		}
189
-
190
-		// And it should be a file or a folder
191
-		if (!($share->getNode() instanceof \OCP\Files\File)
192
-			&& !($share->getNode() instanceof \OCP\Files\Folder)) {
193
-			throw new \InvalidArgumentException($this->l->t('Shared path must be either a file or a folder'));
194
-		}
195
-
196
-		// And you cannot share your rootfolder
197
-		if ($this->userManager->userExists($share->getSharedBy())) {
198
-			$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
199
-		} else {
200
-			$userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
201
-		}
202
-		if ($userFolder->getId() === $share->getNode()->getId()) {
203
-			throw new \InvalidArgumentException($this->l->t('You cannot share your root folder'));
204
-		}
205
-
206
-		// Check if we actually have share permissions
207
-		if (!$share->getNode()->isShareable()) {
208
-			throw new GenericShareException($this->l->t('You are not allowed to share %s', [$share->getNode()->getName()]), code: 404);
209
-		}
210
-
211
-		// Permissions should be set
212
-		if ($share->getPermissions() === null) {
213
-			throw new \InvalidArgumentException($this->l->t('Valid permissions are required for sharing'));
214
-		}
215
-
216
-		// Permissions must be valid
217
-		if ($share->getPermissions() < 0 || $share->getPermissions() > \OCP\Constants::PERMISSION_ALL) {
218
-			throw new \InvalidArgumentException($this->l->t('Valid permissions are required for sharing'));
219
-		}
220
-
221
-		// Single file shares should never have delete or create permissions
222
-		if (($share->getNode() instanceof File)
223
-			&& (($share->getPermissions() & (\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_DELETE)) !== 0)) {
224
-			throw new \InvalidArgumentException($this->l->t('File shares cannot have create or delete permissions'));
225
-		}
226
-
227
-		$permissions = 0;
228
-		$nodesForUser = $userFolder->getById($share->getNodeId());
229
-		foreach ($nodesForUser as $node) {
230
-			if ($node->getInternalPath() === '' && !$node->getMountPoint() instanceof MoveableMount) {
231
-				// for the root of non-movable mount, the permissions we see if limited by the mount itself,
232
-				// so we instead use the "raw" permissions from the storage
233
-				$permissions |= $node->getStorage()->getPermissions('');
234
-			} else {
235
-				$permissions |= $node->getPermissions();
236
-			}
237
-		}
238
-
239
-		// Check that we do not share with more permissions than we have
240
-		if ($share->getPermissions() & ~$permissions) {
241
-			$path = $userFolder->getRelativePath($share->getNode()->getPath());
242
-			throw new GenericShareException($this->l->t('Cannot increase permissions of %s', [$path]), code: 404);
243
-		}
244
-
245
-		// Check that read permissions are always set
246
-		// Link shares are allowed to have no read permissions to allow upload to hidden folders
247
-		$noReadPermissionRequired = $share->getShareType() === IShare::TYPE_LINK
248
-			|| $share->getShareType() === IShare::TYPE_EMAIL;
249
-		if (!$noReadPermissionRequired
250
-			&& ($share->getPermissions() & \OCP\Constants::PERMISSION_READ) === 0) {
251
-			throw new \InvalidArgumentException($this->l->t('Shares need at least read permissions'));
252
-		}
253
-
254
-		if ($share->getNode() instanceof \OCP\Files\File) {
255
-			if ($share->getPermissions() & \OCP\Constants::PERMISSION_DELETE) {
256
-				throw new GenericShareException($this->l->t('Files cannot be shared with delete permissions'));
257
-			}
258
-			if ($share->getPermissions() & \OCP\Constants::PERMISSION_CREATE) {
259
-				throw new GenericShareException($this->l->t('Files cannot be shared with create permissions'));
260
-			}
261
-		}
262
-	}
263
-
264
-	/**
265
-	 * Validate if the expiration date fits the system settings
266
-	 *
267
-	 * @param IShare $share The share to validate the expiration date of
268
-	 * @return IShare The modified share object
269
-	 * @throws GenericShareException
270
-	 * @throws \InvalidArgumentException
271
-	 * @throws \Exception
272
-	 */
273
-	protected function validateExpirationDateInternal(IShare $share): IShare {
274
-		$isRemote = $share->getShareType() === IShare::TYPE_REMOTE || $share->getShareType() === IShare::TYPE_REMOTE_GROUP;
275
-
276
-		$expirationDate = $share->getExpirationDate();
277
-
278
-		if ($isRemote) {
279
-			$defaultExpireDate = $this->shareApiRemoteDefaultExpireDate();
280
-			$defaultExpireDays = $this->shareApiRemoteDefaultExpireDays();
281
-			$configProp = 'remote_defaultExpDays';
282
-			$isEnforced = $this->shareApiRemoteDefaultExpireDateEnforced();
283
-		} else {
284
-			$defaultExpireDate = $this->shareApiInternalDefaultExpireDate();
285
-			$defaultExpireDays = $this->shareApiInternalDefaultExpireDays();
286
-			$configProp = 'internal_defaultExpDays';
287
-			$isEnforced = $this->shareApiInternalDefaultExpireDateEnforced();
288
-		}
289
-
290
-		// If $expirationDate is falsy, noExpirationDate is true and expiration not enforced
291
-		// Then skip expiration date validation as null is accepted
292
-		if (!$share->getNoExpirationDate() || $isEnforced) {
293
-			if ($expirationDate !== null) {
294
-				$expirationDate->setTimezone($this->dateTimeZone->getTimeZone());
295
-				$expirationDate->setTime(0, 0, 0);
296
-
297
-				$date = new \DateTime('now', $this->dateTimeZone->getTimeZone());
298
-				$date->setTime(0, 0, 0);
299
-				if ($date >= $expirationDate) {
300
-					throw new GenericShareException($this->l->t('Expiration date is in the past'), code: 404);
301
-				}
302
-			}
303
-
304
-			// If expiredate is empty set a default one if there is a default
305
-			$fullId = null;
306
-			try {
307
-				$fullId = $share->getFullId();
308
-			} catch (\UnexpectedValueException $e) {
309
-				// This is a new share
310
-			}
311
-
312
-			if ($fullId === null && $expirationDate === null && $defaultExpireDate) {
313
-				$expirationDate = new \DateTime('now', $this->dateTimeZone->getTimeZone());
314
-				$expirationDate->setTime(0, 0, 0);
315
-				$days = (int)$this->config->getAppValue('core', $configProp, (string)$defaultExpireDays);
316
-				if ($days > $defaultExpireDays) {
317
-					$days = $defaultExpireDays;
318
-				}
319
-				$expirationDate->add(new \DateInterval('P' . $days . 'D'));
320
-			}
321
-
322
-			// If we enforce the expiration date check that is does not exceed
323
-			if ($isEnforced) {
324
-				if (empty($expirationDate)) {
325
-					throw new \InvalidArgumentException($this->l->t('Expiration date is enforced'));
326
-				}
327
-
328
-				$date = new \DateTime('now', $this->dateTimeZone->getTimeZone());
329
-				$date->setTime(0, 0, 0);
330
-				$date->add(new \DateInterval('P' . $defaultExpireDays . 'D'));
331
-				if ($date < $expirationDate) {
332
-					throw new GenericShareException($this->l->n('Cannot set expiration date more than %n day in the future', 'Cannot set expiration date more than %n days in the future', $defaultExpireDays), code: 404);
333
-				}
334
-			}
335
-		}
336
-
337
-		$accepted = true;
338
-		$message = '';
339
-		\OCP\Util::emitHook('\OC\Share', 'verifyExpirationDate', [
340
-			'expirationDate' => &$expirationDate,
341
-			'accepted' => &$accepted,
342
-			'message' => &$message,
343
-			'passwordSet' => $share->getPassword() !== null,
344
-		]);
345
-
346
-		if (!$accepted) {
347
-			throw new \Exception($message);
348
-		}
349
-
350
-		$share->setExpirationDate($expirationDate);
351
-
352
-		return $share;
353
-	}
354
-
355
-	/**
356
-	 * Validate if the expiration date fits the system settings
357
-	 *
358
-	 * @param IShare $share The share to validate the expiration date of
359
-	 * @return IShare The modified share object
360
-	 * @throws GenericShareException
361
-	 * @throws \InvalidArgumentException
362
-	 * @throws \Exception
363
-	 */
364
-	protected function validateExpirationDateLink(IShare $share): IShare {
365
-		$expirationDate = $share->getExpirationDate();
366
-		$isEnforced = $this->shareApiLinkDefaultExpireDateEnforced();
367
-
368
-		// If $expirationDate is falsy, noExpirationDate is true and expiration not enforced
369
-		// Then skip expiration date validation as null is accepted
370
-		if (!($share->getNoExpirationDate() && !$isEnforced)) {
371
-			if ($expirationDate !== null) {
372
-				$expirationDate->setTimezone($this->dateTimeZone->getTimeZone());
373
-				$expirationDate->setTime(0, 0, 0);
374
-
375
-				$date = new \DateTime('now', $this->dateTimeZone->getTimeZone());
376
-				$date->setTime(0, 0, 0);
377
-				if ($date >= $expirationDate) {
378
-					throw new GenericShareException($this->l->t('Expiration date is in the past'), code: 404);
379
-				}
380
-			}
381
-
382
-			// If expiredate is empty set a default one if there is a default
383
-			$fullId = null;
384
-			try {
385
-				$fullId = $share->getFullId();
386
-			} catch (\UnexpectedValueException $e) {
387
-				// This is a new share
388
-			}
389
-
390
-			if ($fullId === null && $expirationDate === null && $this->shareApiLinkDefaultExpireDate()) {
391
-				$expirationDate = new \DateTime('now', $this->dateTimeZone->getTimeZone());
392
-				$expirationDate->setTime(0, 0, 0);
393
-
394
-				$days = (int)$this->config->getAppValue('core', 'link_defaultExpDays', (string)$this->shareApiLinkDefaultExpireDays());
395
-				if ($days > $this->shareApiLinkDefaultExpireDays()) {
396
-					$days = $this->shareApiLinkDefaultExpireDays();
397
-				}
398
-				$expirationDate->add(new \DateInterval('P' . $days . 'D'));
399
-			}
400
-
401
-			// If we enforce the expiration date check that is does not exceed
402
-			if ($isEnforced) {
403
-				if (empty($expirationDate)) {
404
-					throw new \InvalidArgumentException($this->l->t('Expiration date is enforced'));
405
-				}
406
-
407
-				$date = new \DateTime('now', $this->dateTimeZone->getTimeZone());
408
-				$date->setTime(0, 0, 0);
409
-				$date->add(new \DateInterval('P' . $this->shareApiLinkDefaultExpireDays() . 'D'));
410
-				if ($date < $expirationDate) {
411
-					throw new GenericShareException(
412
-						$this->l->n('Cannot set expiration date more than %n day in the future', 'Cannot set expiration date more than %n days in the future', $this->shareApiLinkDefaultExpireDays()),
413
-						code: 404,
414
-					);
415
-				}
416
-			}
417
-
418
-		}
419
-
420
-		$accepted = true;
421
-		$message = '';
422
-		\OCP\Util::emitHook('\OC\Share', 'verifyExpirationDate', [
423
-			'expirationDate' => &$expirationDate,
424
-			'accepted' => &$accepted,
425
-			'message' => &$message,
426
-			'passwordSet' => $share->getPassword() !== null,
427
-		]);
428
-
429
-		if (!$accepted) {
430
-			throw new \Exception($message);
431
-		}
432
-
433
-		$share->setExpirationDate($expirationDate);
434
-
435
-		return $share;
436
-	}
437
-
438
-	/**
439
-	 * Check for pre share requirements for user shares
440
-	 *
441
-	 * @throws \Exception
442
-	 */
443
-	protected function userCreateChecks(IShare $share): void {
444
-		// Check if we can share with group members only
445
-		if ($this->shareWithGroupMembersOnly()) {
446
-			$sharedBy = $this->userManager->get($share->getSharedBy());
447
-			$sharedWith = $this->userManager->get($share->getSharedWith());
448
-			// Verify we can share with this user
449
-			$groups = array_intersect(
450
-				$this->groupManager->getUserGroupIds($sharedBy),
451
-				$this->groupManager->getUserGroupIds($sharedWith)
452
-			);
453
-
454
-			// optional excluded groups
455
-			$excludedGroups = $this->shareWithGroupMembersOnlyExcludeGroupsList();
456
-			$groups = array_diff($groups, $excludedGroups);
457
-
458
-			if (empty($groups)) {
459
-				throw new \Exception($this->l->t('Sharing is only allowed with group members'));
460
-			}
461
-		}
462
-
463
-		/*
66
+    private ?IL10N $l;
67
+    private LegacyHooks $legacyHooks;
68
+
69
+    public function __construct(
70
+        private LoggerInterface $logger,
71
+        private IConfig $config,
72
+        private ISecureRandom $secureRandom,
73
+        private IHasher $hasher,
74
+        private IMountManager $mountManager,
75
+        private IGroupManager $groupManager,
76
+        private IFactory $l10nFactory,
77
+        private IProviderFactory $factory,
78
+        private IUserManager $userManager,
79
+        private IRootFolder $rootFolder,
80
+        private IEventDispatcher $dispatcher,
81
+        private IUserSession $userSession,
82
+        private KnownUserService $knownUserService,
83
+        private ShareDisableChecker $shareDisableChecker,
84
+        private IDateTimeZone $dateTimeZone,
85
+        private IAppConfig $appConfig,
86
+    ) {
87
+        $this->l = $this->l10nFactory->get('lib');
88
+        // The constructor of LegacyHooks registers the listeners of share events
89
+        // do not remove if those are not properly migrated
90
+        $this->legacyHooks = new LegacyHooks($this->dispatcher);
91
+    }
92
+
93
+    /**
94
+     * Convert from a full share id to a tuple (providerId, shareId)
95
+     *
96
+     * @return string[]
97
+     */
98
+    private function splitFullId(string $id): array {
99
+        return explode(':', $id, 2);
100
+    }
101
+
102
+    /**
103
+     * Verify if a password meets all requirements
104
+     *
105
+     * @throws HintException
106
+     */
107
+    protected function verifyPassword(?string $password): void {
108
+        if ($password === null) {
109
+            // No password is set, check if this is allowed.
110
+            if ($this->shareApiLinkEnforcePassword()) {
111
+                throw new \InvalidArgumentException($this->l->t('Passwords are enforced for link and mail shares'));
112
+            }
113
+
114
+            return;
115
+        }
116
+
117
+        // Let others verify the password
118
+        try {
119
+            $event = new ValidatePasswordPolicyEvent($password, PasswordContext::SHARING);
120
+            $this->dispatcher->dispatchTyped($event);
121
+        } catch (HintException $e) {
122
+            /* Wrap in a 400 bad request error */
123
+            throw new HintException($e->getMessage(), $e->getHint(), 400, $e);
124
+        }
125
+    }
126
+
127
+    /**
128
+     * Check for generic requirements before creating a share
129
+     *
130
+     * @param IShare $share
131
+     * @throws \InvalidArgumentException
132
+     * @throws GenericShareException
133
+     *
134
+     * @suppress PhanUndeclaredClassMethod
135
+     */
136
+    protected function generalCreateChecks(IShare $share, bool $isUpdate = false): void {
137
+        if ($share->getShareType() === IShare::TYPE_USER) {
138
+            // We expect a valid user as sharedWith for user shares
139
+            if (!$this->userManager->userExists($share->getSharedWith())) {
140
+                throw new \InvalidArgumentException($this->l->t('Share recipient is not a valid user'));
141
+            }
142
+        } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
143
+            // We expect a valid group as sharedWith for group shares
144
+            if (!$this->groupManager->groupExists($share->getSharedWith())) {
145
+                throw new \InvalidArgumentException($this->l->t('Share recipient is not a valid group'));
146
+            }
147
+        } elseif ($share->getShareType() === IShare::TYPE_LINK) {
148
+            // No check for TYPE_EMAIL here as we have a recipient for them
149
+            if ($share->getSharedWith() !== null) {
150
+                throw new \InvalidArgumentException($this->l->t('Share recipient should be empty'));
151
+            }
152
+        } elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
153
+            if ($share->getSharedWith() === null) {
154
+                throw new \InvalidArgumentException($this->l->t('Share recipient should not be empty'));
155
+            }
156
+        } elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
157
+            if ($share->getSharedWith() === null) {
158
+                throw new \InvalidArgumentException($this->l->t('Share recipient should not be empty'));
159
+            }
160
+        } elseif ($share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
161
+            if ($share->getSharedWith() === null) {
162
+                throw new \InvalidArgumentException($this->l->t('Share recipient should not be empty'));
163
+            }
164
+        } elseif ($share->getShareType() === IShare::TYPE_CIRCLE) {
165
+            $circle = \OCA\Circles\Api\v1\Circles::detailsCircle($share->getSharedWith());
166
+            if ($circle === null) {
167
+                throw new \InvalidArgumentException($this->l->t('Share recipient is not a valid circle'));
168
+            }
169
+        } elseif ($share->getShareType() !== IShare::TYPE_ROOM && $share->getShareType() !== IShare::TYPE_DECK) {
170
+            // We cannot handle other types yet
171
+            throw new \InvalidArgumentException($this->l->t('Unknown share type'));
172
+        }
173
+
174
+        // Verify the initiator of the share is set
175
+        if ($share->getSharedBy() === null) {
176
+            throw new \InvalidArgumentException($this->l->t('Share initiator must be set'));
177
+        }
178
+
179
+        // Cannot share with yourself
180
+        if ($share->getShareType() === IShare::TYPE_USER
181
+            && $share->getSharedWith() === $share->getSharedBy()) {
182
+            throw new \InvalidArgumentException($this->l->t('Cannot share with yourself'));
183
+        }
184
+
185
+        // The path should be set
186
+        if ($share->getNode() === null) {
187
+            throw new \InvalidArgumentException($this->l->t('Shared path must be set'));
188
+        }
189
+
190
+        // And it should be a file or a folder
191
+        if (!($share->getNode() instanceof \OCP\Files\File)
192
+            && !($share->getNode() instanceof \OCP\Files\Folder)) {
193
+            throw new \InvalidArgumentException($this->l->t('Shared path must be either a file or a folder'));
194
+        }
195
+
196
+        // And you cannot share your rootfolder
197
+        if ($this->userManager->userExists($share->getSharedBy())) {
198
+            $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
199
+        } else {
200
+            $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
201
+        }
202
+        if ($userFolder->getId() === $share->getNode()->getId()) {
203
+            throw new \InvalidArgumentException($this->l->t('You cannot share your root folder'));
204
+        }
205
+
206
+        // Check if we actually have share permissions
207
+        if (!$share->getNode()->isShareable()) {
208
+            throw new GenericShareException($this->l->t('You are not allowed to share %s', [$share->getNode()->getName()]), code: 404);
209
+        }
210
+
211
+        // Permissions should be set
212
+        if ($share->getPermissions() === null) {
213
+            throw new \InvalidArgumentException($this->l->t('Valid permissions are required for sharing'));
214
+        }
215
+
216
+        // Permissions must be valid
217
+        if ($share->getPermissions() < 0 || $share->getPermissions() > \OCP\Constants::PERMISSION_ALL) {
218
+            throw new \InvalidArgumentException($this->l->t('Valid permissions are required for sharing'));
219
+        }
220
+
221
+        // Single file shares should never have delete or create permissions
222
+        if (($share->getNode() instanceof File)
223
+            && (($share->getPermissions() & (\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_DELETE)) !== 0)) {
224
+            throw new \InvalidArgumentException($this->l->t('File shares cannot have create or delete permissions'));
225
+        }
226
+
227
+        $permissions = 0;
228
+        $nodesForUser = $userFolder->getById($share->getNodeId());
229
+        foreach ($nodesForUser as $node) {
230
+            if ($node->getInternalPath() === '' && !$node->getMountPoint() instanceof MoveableMount) {
231
+                // for the root of non-movable mount, the permissions we see if limited by the mount itself,
232
+                // so we instead use the "raw" permissions from the storage
233
+                $permissions |= $node->getStorage()->getPermissions('');
234
+            } else {
235
+                $permissions |= $node->getPermissions();
236
+            }
237
+        }
238
+
239
+        // Check that we do not share with more permissions than we have
240
+        if ($share->getPermissions() & ~$permissions) {
241
+            $path = $userFolder->getRelativePath($share->getNode()->getPath());
242
+            throw new GenericShareException($this->l->t('Cannot increase permissions of %s', [$path]), code: 404);
243
+        }
244
+
245
+        // Check that read permissions are always set
246
+        // Link shares are allowed to have no read permissions to allow upload to hidden folders
247
+        $noReadPermissionRequired = $share->getShareType() === IShare::TYPE_LINK
248
+            || $share->getShareType() === IShare::TYPE_EMAIL;
249
+        if (!$noReadPermissionRequired
250
+            && ($share->getPermissions() & \OCP\Constants::PERMISSION_READ) === 0) {
251
+            throw new \InvalidArgumentException($this->l->t('Shares need at least read permissions'));
252
+        }
253
+
254
+        if ($share->getNode() instanceof \OCP\Files\File) {
255
+            if ($share->getPermissions() & \OCP\Constants::PERMISSION_DELETE) {
256
+                throw new GenericShareException($this->l->t('Files cannot be shared with delete permissions'));
257
+            }
258
+            if ($share->getPermissions() & \OCP\Constants::PERMISSION_CREATE) {
259
+                throw new GenericShareException($this->l->t('Files cannot be shared with create permissions'));
260
+            }
261
+        }
262
+    }
263
+
264
+    /**
265
+     * Validate if the expiration date fits the system settings
266
+     *
267
+     * @param IShare $share The share to validate the expiration date of
268
+     * @return IShare The modified share object
269
+     * @throws GenericShareException
270
+     * @throws \InvalidArgumentException
271
+     * @throws \Exception
272
+     */
273
+    protected function validateExpirationDateInternal(IShare $share): IShare {
274
+        $isRemote = $share->getShareType() === IShare::TYPE_REMOTE || $share->getShareType() === IShare::TYPE_REMOTE_GROUP;
275
+
276
+        $expirationDate = $share->getExpirationDate();
277
+
278
+        if ($isRemote) {
279
+            $defaultExpireDate = $this->shareApiRemoteDefaultExpireDate();
280
+            $defaultExpireDays = $this->shareApiRemoteDefaultExpireDays();
281
+            $configProp = 'remote_defaultExpDays';
282
+            $isEnforced = $this->shareApiRemoteDefaultExpireDateEnforced();
283
+        } else {
284
+            $defaultExpireDate = $this->shareApiInternalDefaultExpireDate();
285
+            $defaultExpireDays = $this->shareApiInternalDefaultExpireDays();
286
+            $configProp = 'internal_defaultExpDays';
287
+            $isEnforced = $this->shareApiInternalDefaultExpireDateEnforced();
288
+        }
289
+
290
+        // If $expirationDate is falsy, noExpirationDate is true and expiration not enforced
291
+        // Then skip expiration date validation as null is accepted
292
+        if (!$share->getNoExpirationDate() || $isEnforced) {
293
+            if ($expirationDate !== null) {
294
+                $expirationDate->setTimezone($this->dateTimeZone->getTimeZone());
295
+                $expirationDate->setTime(0, 0, 0);
296
+
297
+                $date = new \DateTime('now', $this->dateTimeZone->getTimeZone());
298
+                $date->setTime(0, 0, 0);
299
+                if ($date >= $expirationDate) {
300
+                    throw new GenericShareException($this->l->t('Expiration date is in the past'), code: 404);
301
+                }
302
+            }
303
+
304
+            // If expiredate is empty set a default one if there is a default
305
+            $fullId = null;
306
+            try {
307
+                $fullId = $share->getFullId();
308
+            } catch (\UnexpectedValueException $e) {
309
+                // This is a new share
310
+            }
311
+
312
+            if ($fullId === null && $expirationDate === null && $defaultExpireDate) {
313
+                $expirationDate = new \DateTime('now', $this->dateTimeZone->getTimeZone());
314
+                $expirationDate->setTime(0, 0, 0);
315
+                $days = (int)$this->config->getAppValue('core', $configProp, (string)$defaultExpireDays);
316
+                if ($days > $defaultExpireDays) {
317
+                    $days = $defaultExpireDays;
318
+                }
319
+                $expirationDate->add(new \DateInterval('P' . $days . 'D'));
320
+            }
321
+
322
+            // If we enforce the expiration date check that is does not exceed
323
+            if ($isEnforced) {
324
+                if (empty($expirationDate)) {
325
+                    throw new \InvalidArgumentException($this->l->t('Expiration date is enforced'));
326
+                }
327
+
328
+                $date = new \DateTime('now', $this->dateTimeZone->getTimeZone());
329
+                $date->setTime(0, 0, 0);
330
+                $date->add(new \DateInterval('P' . $defaultExpireDays . 'D'));
331
+                if ($date < $expirationDate) {
332
+                    throw new GenericShareException($this->l->n('Cannot set expiration date more than %n day in the future', 'Cannot set expiration date more than %n days in the future', $defaultExpireDays), code: 404);
333
+                }
334
+            }
335
+        }
336
+
337
+        $accepted = true;
338
+        $message = '';
339
+        \OCP\Util::emitHook('\OC\Share', 'verifyExpirationDate', [
340
+            'expirationDate' => &$expirationDate,
341
+            'accepted' => &$accepted,
342
+            'message' => &$message,
343
+            'passwordSet' => $share->getPassword() !== null,
344
+        ]);
345
+
346
+        if (!$accepted) {
347
+            throw new \Exception($message);
348
+        }
349
+
350
+        $share->setExpirationDate($expirationDate);
351
+
352
+        return $share;
353
+    }
354
+
355
+    /**
356
+     * Validate if the expiration date fits the system settings
357
+     *
358
+     * @param IShare $share The share to validate the expiration date of
359
+     * @return IShare The modified share object
360
+     * @throws GenericShareException
361
+     * @throws \InvalidArgumentException
362
+     * @throws \Exception
363
+     */
364
+    protected function validateExpirationDateLink(IShare $share): IShare {
365
+        $expirationDate = $share->getExpirationDate();
366
+        $isEnforced = $this->shareApiLinkDefaultExpireDateEnforced();
367
+
368
+        // If $expirationDate is falsy, noExpirationDate is true and expiration not enforced
369
+        // Then skip expiration date validation as null is accepted
370
+        if (!($share->getNoExpirationDate() && !$isEnforced)) {
371
+            if ($expirationDate !== null) {
372
+                $expirationDate->setTimezone($this->dateTimeZone->getTimeZone());
373
+                $expirationDate->setTime(0, 0, 0);
374
+
375
+                $date = new \DateTime('now', $this->dateTimeZone->getTimeZone());
376
+                $date->setTime(0, 0, 0);
377
+                if ($date >= $expirationDate) {
378
+                    throw new GenericShareException($this->l->t('Expiration date is in the past'), code: 404);
379
+                }
380
+            }
381
+
382
+            // If expiredate is empty set a default one if there is a default
383
+            $fullId = null;
384
+            try {
385
+                $fullId = $share->getFullId();
386
+            } catch (\UnexpectedValueException $e) {
387
+                // This is a new share
388
+            }
389
+
390
+            if ($fullId === null && $expirationDate === null && $this->shareApiLinkDefaultExpireDate()) {
391
+                $expirationDate = new \DateTime('now', $this->dateTimeZone->getTimeZone());
392
+                $expirationDate->setTime(0, 0, 0);
393
+
394
+                $days = (int)$this->config->getAppValue('core', 'link_defaultExpDays', (string)$this->shareApiLinkDefaultExpireDays());
395
+                if ($days > $this->shareApiLinkDefaultExpireDays()) {
396
+                    $days = $this->shareApiLinkDefaultExpireDays();
397
+                }
398
+                $expirationDate->add(new \DateInterval('P' . $days . 'D'));
399
+            }
400
+
401
+            // If we enforce the expiration date check that is does not exceed
402
+            if ($isEnforced) {
403
+                if (empty($expirationDate)) {
404
+                    throw new \InvalidArgumentException($this->l->t('Expiration date is enforced'));
405
+                }
406
+
407
+                $date = new \DateTime('now', $this->dateTimeZone->getTimeZone());
408
+                $date->setTime(0, 0, 0);
409
+                $date->add(new \DateInterval('P' . $this->shareApiLinkDefaultExpireDays() . 'D'));
410
+                if ($date < $expirationDate) {
411
+                    throw new GenericShareException(
412
+                        $this->l->n('Cannot set expiration date more than %n day in the future', 'Cannot set expiration date more than %n days in the future', $this->shareApiLinkDefaultExpireDays()),
413
+                        code: 404,
414
+                    );
415
+                }
416
+            }
417
+
418
+        }
419
+
420
+        $accepted = true;
421
+        $message = '';
422
+        \OCP\Util::emitHook('\OC\Share', 'verifyExpirationDate', [
423
+            'expirationDate' => &$expirationDate,
424
+            'accepted' => &$accepted,
425
+            'message' => &$message,
426
+            'passwordSet' => $share->getPassword() !== null,
427
+        ]);
428
+
429
+        if (!$accepted) {
430
+            throw new \Exception($message);
431
+        }
432
+
433
+        $share->setExpirationDate($expirationDate);
434
+
435
+        return $share;
436
+    }
437
+
438
+    /**
439
+     * Check for pre share requirements for user shares
440
+     *
441
+     * @throws \Exception
442
+     */
443
+    protected function userCreateChecks(IShare $share): void {
444
+        // Check if we can share with group members only
445
+        if ($this->shareWithGroupMembersOnly()) {
446
+            $sharedBy = $this->userManager->get($share->getSharedBy());
447
+            $sharedWith = $this->userManager->get($share->getSharedWith());
448
+            // Verify we can share with this user
449
+            $groups = array_intersect(
450
+                $this->groupManager->getUserGroupIds($sharedBy),
451
+                $this->groupManager->getUserGroupIds($sharedWith)
452
+            );
453
+
454
+            // optional excluded groups
455
+            $excludedGroups = $this->shareWithGroupMembersOnlyExcludeGroupsList();
456
+            $groups = array_diff($groups, $excludedGroups);
457
+
458
+            if (empty($groups)) {
459
+                throw new \Exception($this->l->t('Sharing is only allowed with group members'));
460
+            }
461
+        }
462
+
463
+        /*
464 464
 		 * TODO: Could be costly, fix
465 465
 		 *
466 466
 		 * Also this is not what we want in the future.. then we want to squash identical shares.
467 467
 		 */
468
-		$provider = $this->factory->getProviderForType(IShare::TYPE_USER);
469
-		$existingShares = $provider->getSharesByPath($share->getNode());
470
-		foreach ($existingShares as $existingShare) {
471
-			// Ignore if it is the same share
472
-			try {
473
-				if ($existingShare->getFullId() === $share->getFullId()) {
474
-					continue;
475
-				}
476
-			} catch (\UnexpectedValueException $e) {
477
-				//Shares are not identical
478
-			}
479
-
480
-			// Identical share already exists
481
-			if ($existingShare->getSharedWith() === $share->getSharedWith() && $existingShare->getShareType() === $share->getShareType()) {
482
-				throw new AlreadySharedException($this->l->t('Sharing %s failed, because this item is already shared with the account %s', [$share->getNode()->getName(), $share->getSharedWithDisplayName()]), $existingShare);
483
-			}
484
-
485
-			// The share is already shared with this user via a group share
486
-			if ($existingShare->getShareType() === IShare::TYPE_GROUP) {
487
-				$group = $this->groupManager->get($existingShare->getSharedWith());
488
-				if (!is_null($group)) {
489
-					$user = $this->userManager->get($share->getSharedWith());
490
-
491
-					if ($group->inGroup($user) && $existingShare->getShareOwner() !== $share->getShareOwner()) {
492
-						throw new AlreadySharedException($this->l->t('Sharing %s failed, because this item is already shared with the account %s', [$share->getNode()->getName(), $share->getSharedWithDisplayName()]), $existingShare);
493
-					}
494
-				}
495
-			}
496
-		}
497
-	}
498
-
499
-	/**
500
-	 * Check for pre share requirements for group shares
501
-	 *
502
-	 * @throws \Exception
503
-	 */
504
-	protected function groupCreateChecks(IShare $share): void {
505
-		// Verify group shares are allowed
506
-		if (!$this->allowGroupSharing()) {
507
-			throw new \Exception($this->l->t('Group sharing is now allowed'));
508
-		}
509
-
510
-		// Verify if the user can share with this group
511
-		if ($this->shareWithGroupMembersOnly()) {
512
-			$sharedBy = $this->userManager->get($share->getSharedBy());
513
-			$sharedWith = $this->groupManager->get($share->getSharedWith());
514
-
515
-			// optional excluded groups
516
-			$excludedGroups = $this->shareWithGroupMembersOnlyExcludeGroupsList();
517
-			if (is_null($sharedWith) || in_array($share->getSharedWith(), $excludedGroups) || !$sharedWith->inGroup($sharedBy)) {
518
-				throw new \Exception($this->l->t('Sharing is only allowed within your own groups'));
519
-			}
520
-		}
521
-
522
-		/*
468
+        $provider = $this->factory->getProviderForType(IShare::TYPE_USER);
469
+        $existingShares = $provider->getSharesByPath($share->getNode());
470
+        foreach ($existingShares as $existingShare) {
471
+            // Ignore if it is the same share
472
+            try {
473
+                if ($existingShare->getFullId() === $share->getFullId()) {
474
+                    continue;
475
+                }
476
+            } catch (\UnexpectedValueException $e) {
477
+                //Shares are not identical
478
+            }
479
+
480
+            // Identical share already exists
481
+            if ($existingShare->getSharedWith() === $share->getSharedWith() && $existingShare->getShareType() === $share->getShareType()) {
482
+                throw new AlreadySharedException($this->l->t('Sharing %s failed, because this item is already shared with the account %s', [$share->getNode()->getName(), $share->getSharedWithDisplayName()]), $existingShare);
483
+            }
484
+
485
+            // The share is already shared with this user via a group share
486
+            if ($existingShare->getShareType() === IShare::TYPE_GROUP) {
487
+                $group = $this->groupManager->get($existingShare->getSharedWith());
488
+                if (!is_null($group)) {
489
+                    $user = $this->userManager->get($share->getSharedWith());
490
+
491
+                    if ($group->inGroup($user) && $existingShare->getShareOwner() !== $share->getShareOwner()) {
492
+                        throw new AlreadySharedException($this->l->t('Sharing %s failed, because this item is already shared with the account %s', [$share->getNode()->getName(), $share->getSharedWithDisplayName()]), $existingShare);
493
+                    }
494
+                }
495
+            }
496
+        }
497
+    }
498
+
499
+    /**
500
+     * Check for pre share requirements for group shares
501
+     *
502
+     * @throws \Exception
503
+     */
504
+    protected function groupCreateChecks(IShare $share): void {
505
+        // Verify group shares are allowed
506
+        if (!$this->allowGroupSharing()) {
507
+            throw new \Exception($this->l->t('Group sharing is now allowed'));
508
+        }
509
+
510
+        // Verify if the user can share with this group
511
+        if ($this->shareWithGroupMembersOnly()) {
512
+            $sharedBy = $this->userManager->get($share->getSharedBy());
513
+            $sharedWith = $this->groupManager->get($share->getSharedWith());
514
+
515
+            // optional excluded groups
516
+            $excludedGroups = $this->shareWithGroupMembersOnlyExcludeGroupsList();
517
+            if (is_null($sharedWith) || in_array($share->getSharedWith(), $excludedGroups) || !$sharedWith->inGroup($sharedBy)) {
518
+                throw new \Exception($this->l->t('Sharing is only allowed within your own groups'));
519
+            }
520
+        }
521
+
522
+        /*
523 523
 		 * TODO: Could be costly, fix
524 524
 		 *
525 525
 		 * Also this is not what we want in the future.. then we want to squash identical shares.
526 526
 		 */
527
-		$provider = $this->factory->getProviderForType(IShare::TYPE_GROUP);
528
-		$existingShares = $provider->getSharesByPath($share->getNode());
529
-		foreach ($existingShares as $existingShare) {
530
-			try {
531
-				if ($existingShare->getFullId() === $share->getFullId()) {
532
-					continue;
533
-				}
534
-			} catch (\UnexpectedValueException $e) {
535
-				//It is a new share so just continue
536
-			}
537
-
538
-			if ($existingShare->getSharedWith() === $share->getSharedWith() && $existingShare->getShareType() === $share->getShareType()) {
539
-				throw new AlreadySharedException($this->l->t('Path is already shared with this group'), $existingShare);
540
-			}
541
-		}
542
-	}
543
-
544
-	/**
545
-	 * Check for pre share requirements for link shares
546
-	 *
547
-	 * @throws \Exception
548
-	 */
549
-	protected function linkCreateChecks(IShare $share): void {
550
-		// Are link shares allowed?
551
-		if (!$this->shareApiAllowLinks()) {
552
-			throw new \Exception($this->l->t('Link sharing is not allowed'));
553
-		}
554
-
555
-		// Check if public upload is allowed
556
-		if ($share->getNodeType() === 'folder' && !$this->shareApiLinkAllowPublicUpload()
557
-			&& ($share->getPermissions() & (\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE))) {
558
-			throw new \InvalidArgumentException($this->l->t('Public upload is not allowed'));
559
-		}
560
-	}
561
-
562
-	/**
563
-	 * To make sure we don't get invisible link shares we set the parent
564
-	 * of a link if it is a reshare. This is a quick word around
565
-	 * until we can properly display multiple link shares in the UI
566
-	 *
567
-	 * See: https://github.com/owncloud/core/issues/22295
568
-	 *
569
-	 * FIXME: Remove once multiple link shares can be properly displayed
570
-	 */
571
-	protected function setLinkParent(IShare $share): void {
572
-		$storage = $share->getNode()->getStorage();
573
-		if ($storage->instanceOfStorage(SharedStorage::class)) {
574
-			/** @var \OCA\Files_Sharing\SharedStorage $storage */
575
-			$share->setParent((int)$storage->getShareId());
576
-		}
577
-	}
578
-
579
-	protected function pathCreateChecks(Node $path): void {
580
-		// Make sure that we do not share a path that contains a shared mountpoint
581
-		if ($path instanceof \OCP\Files\Folder) {
582
-			$mounts = $this->mountManager->findIn($path->getPath());
583
-			foreach ($mounts as $mount) {
584
-				if ($mount->getStorage()->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) {
585
-					// Using a flat sharing model ensures the file owner can always see who has access.
586
-					// Allowing parent folder sharing would require tracking inherited access, which adds complexity
587
-					// and hurts performance/scalability.
588
-					// So we forbid sharing a parent folder of a share you received.
589
-					throw new \InvalidArgumentException($this->l->t('You cannot share a folder that contains other shares'));
590
-				}
591
-			}
592
-		}
593
-	}
594
-
595
-	/**
596
-	 * Check if the user that is sharing can actually share
597
-	 *
598
-	 * @throws \Exception
599
-	 */
600
-	protected function canShare(IShare $share): void {
601
-		if (!$this->shareApiEnabled()) {
602
-			throw new \Exception($this->l->t('Sharing is disabled'));
603
-		}
604
-
605
-		if ($this->sharingDisabledForUser($share->getSharedBy())) {
606
-			throw new \Exception($this->l->t('Sharing is disabled for you'));
607
-		}
608
-	}
609
-
610
-	#[Override]
611
-	public function createShare(IShare $share): IShare {
612
-		// TODO: handle link share permissions or check them
613
-		$this->canShare($share);
614
-
615
-		$this->generalCreateChecks($share);
616
-
617
-		// Verify if there are any issues with the path
618
-		$this->pathCreateChecks($share->getNode());
619
-
620
-		/*
527
+        $provider = $this->factory->getProviderForType(IShare::TYPE_GROUP);
528
+        $existingShares = $provider->getSharesByPath($share->getNode());
529
+        foreach ($existingShares as $existingShare) {
530
+            try {
531
+                if ($existingShare->getFullId() === $share->getFullId()) {
532
+                    continue;
533
+                }
534
+            } catch (\UnexpectedValueException $e) {
535
+                //It is a new share so just continue
536
+            }
537
+
538
+            if ($existingShare->getSharedWith() === $share->getSharedWith() && $existingShare->getShareType() === $share->getShareType()) {
539
+                throw new AlreadySharedException($this->l->t('Path is already shared with this group'), $existingShare);
540
+            }
541
+        }
542
+    }
543
+
544
+    /**
545
+     * Check for pre share requirements for link shares
546
+     *
547
+     * @throws \Exception
548
+     */
549
+    protected function linkCreateChecks(IShare $share): void {
550
+        // Are link shares allowed?
551
+        if (!$this->shareApiAllowLinks()) {
552
+            throw new \Exception($this->l->t('Link sharing is not allowed'));
553
+        }
554
+
555
+        // Check if public upload is allowed
556
+        if ($share->getNodeType() === 'folder' && !$this->shareApiLinkAllowPublicUpload()
557
+            && ($share->getPermissions() & (\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE))) {
558
+            throw new \InvalidArgumentException($this->l->t('Public upload is not allowed'));
559
+        }
560
+    }
561
+
562
+    /**
563
+     * To make sure we don't get invisible link shares we set the parent
564
+     * of a link if it is a reshare. This is a quick word around
565
+     * until we can properly display multiple link shares in the UI
566
+     *
567
+     * See: https://github.com/owncloud/core/issues/22295
568
+     *
569
+     * FIXME: Remove once multiple link shares can be properly displayed
570
+     */
571
+    protected function setLinkParent(IShare $share): void {
572
+        $storage = $share->getNode()->getStorage();
573
+        if ($storage->instanceOfStorage(SharedStorage::class)) {
574
+            /** @var \OCA\Files_Sharing\SharedStorage $storage */
575
+            $share->setParent((int)$storage->getShareId());
576
+        }
577
+    }
578
+
579
+    protected function pathCreateChecks(Node $path): void {
580
+        // Make sure that we do not share a path that contains a shared mountpoint
581
+        if ($path instanceof \OCP\Files\Folder) {
582
+            $mounts = $this->mountManager->findIn($path->getPath());
583
+            foreach ($mounts as $mount) {
584
+                if ($mount->getStorage()->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) {
585
+                    // Using a flat sharing model ensures the file owner can always see who has access.
586
+                    // Allowing parent folder sharing would require tracking inherited access, which adds complexity
587
+                    // and hurts performance/scalability.
588
+                    // So we forbid sharing a parent folder of a share you received.
589
+                    throw new \InvalidArgumentException($this->l->t('You cannot share a folder that contains other shares'));
590
+                }
591
+            }
592
+        }
593
+    }
594
+
595
+    /**
596
+     * Check if the user that is sharing can actually share
597
+     *
598
+     * @throws \Exception
599
+     */
600
+    protected function canShare(IShare $share): void {
601
+        if (!$this->shareApiEnabled()) {
602
+            throw new \Exception($this->l->t('Sharing is disabled'));
603
+        }
604
+
605
+        if ($this->sharingDisabledForUser($share->getSharedBy())) {
606
+            throw new \Exception($this->l->t('Sharing is disabled for you'));
607
+        }
608
+    }
609
+
610
+    #[Override]
611
+    public function createShare(IShare $share): IShare {
612
+        // TODO: handle link share permissions or check them
613
+        $this->canShare($share);
614
+
615
+        $this->generalCreateChecks($share);
616
+
617
+        // Verify if there are any issues with the path
618
+        $this->pathCreateChecks($share->getNode());
619
+
620
+        /*
621 621
 		 * On creation of a share the owner is always the owner of the path
622 622
 		 * Except for mounted federated shares.
623 623
 		 */
624
-		$storage = $share->getNode()->getStorage();
625
-		if ($storage->instanceOfStorage('OCA\Files_Sharing\External\Storage')) {
626
-			$parent = $share->getNode()->getParent();
627
-			while ($parent->getStorage()->instanceOfStorage('OCA\Files_Sharing\External\Storage')) {
628
-				$parent = $parent->getParent();
629
-			}
630
-			$share->setShareOwner($parent->getOwner()->getUID());
631
-		} else {
632
-			if ($share->getNode()->getOwner()) {
633
-				$share->setShareOwner($share->getNode()->getOwner()->getUID());
634
-			} else {
635
-				$share->setShareOwner($share->getSharedBy());
636
-			}
637
-		}
638
-
639
-		try {
640
-			// Verify share type
641
-			if ($share->getShareType() === IShare::TYPE_USER) {
642
-				$this->userCreateChecks($share);
643
-
644
-				// Verify the expiration date
645
-				$share = $this->validateExpirationDateInternal($share);
646
-			} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
647
-				$this->groupCreateChecks($share);
648
-
649
-				// Verify the expiration date
650
-				$share = $this->validateExpirationDateInternal($share);
651
-			} elseif ($share->getShareType() === IShare::TYPE_REMOTE || $share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
652
-				// Verify the expiration date
653
-				$share = $this->validateExpirationDateInternal($share);
654
-			} elseif ($share->getShareType() === IShare::TYPE_LINK
655
-				|| $share->getShareType() === IShare::TYPE_EMAIL) {
656
-				$this->linkCreateChecks($share);
657
-				$this->setLinkParent($share);
658
-
659
-				$token = $this->generateToken();
660
-				// Set the unique token
661
-				$share->setToken($token);
662
-
663
-				// Verify the expiration date
664
-				$share = $this->validateExpirationDateLink($share);
665
-
666
-				// Verify the password
667
-				$this->verifyPassword($share->getPassword());
668
-
669
-				// If a password is set. Hash it!
670
-				if ($share->getShareType() === IShare::TYPE_LINK
671
-					&& $share->getPassword() !== null) {
672
-					$share->setPassword($this->hasher->hash($share->getPassword()));
673
-				}
674
-			}
675
-
676
-			// Cannot share with the owner
677
-			if ($share->getShareType() === IShare::TYPE_USER
678
-				&& $share->getSharedWith() === $share->getShareOwner()) {
679
-				throw new \InvalidArgumentException($this->l->t('Cannot share with the share owner'));
680
-			}
681
-
682
-			// Generate the target
683
-			$shareFolder = $this->config->getSystemValue('share_folder', '/');
684
-			if ($share->getShareType() === IShare::TYPE_USER) {
685
-				$allowCustomShareFolder = $this->config->getSystemValueBool('sharing.allow_custom_share_folder', true);
686
-				if ($allowCustomShareFolder) {
687
-					$shareFolder = $this->config->getUserValue($share->getSharedWith(), Application::APP_ID, 'share_folder', $shareFolder);
688
-				}
689
-			}
690
-
691
-			$target = $shareFolder . '/' . $share->getNode()->getName();
692
-			$target = \OC\Files\Filesystem::normalizePath($target);
693
-			$share->setTarget($target);
694
-
695
-			// Pre share event
696
-			$event = new Share\Events\BeforeShareCreatedEvent($share);
697
-			$this->dispatchEvent($event, 'before share created');
698
-			if ($event->isPropagationStopped() && $event->getError()) {
699
-				throw new \Exception($event->getError());
700
-			}
701
-
702
-			$oldShare = $share;
703
-			$provider = $this->factory->getProviderForType($share->getShareType());
704
-			$share = $provider->create($share);
705
-
706
-			// Reuse the node we already have
707
-			$share->setNode($oldShare->getNode());
708
-
709
-			// Reset the target if it is null for the new share
710
-			if ($share->getTarget() === '') {
711
-				$share->setTarget($target);
712
-			}
713
-		} catch (AlreadySharedException $e) {
714
-			// If a share for the same target already exists, dont create a new one,
715
-			// but do trigger the hooks and notifications again
716
-			$oldShare = $share;
717
-
718
-			// Reuse the node we already have
719
-			$share = $e->getExistingShare();
720
-			$share->setNode($oldShare->getNode());
721
-		}
722
-
723
-		// Post share event
724
-		$this->dispatchEvent(new ShareCreatedEvent($share), 'share created');
725
-
726
-		// Send email if needed
727
-		if ($this->config->getSystemValueBool('sharing.enable_share_mail', true)) {
728
-			if ($share->getMailSend()) {
729
-				$provider = $this->factory->getProviderForType($share->getShareType());
730
-				if ($provider instanceof IShareProviderWithNotification) {
731
-					$provider->sendMailNotification($share);
732
-				} else {
733
-					$this->logger->debug('Share notification not sent because the provider does not support it.', ['app' => 'share']);
734
-				}
735
-			} else {
736
-				$this->logger->debug('Share notification not sent because mailsend is false.', ['app' => 'share']);
737
-			}
738
-		} else {
739
-			$this->logger->debug('Share notification not sent because sharing notification emails is disabled.', ['app' => 'share']);
740
-		}
741
-
742
-		return $share;
743
-	}
744
-
745
-	#[Override]
746
-	public function updateShare(IShare $share, bool $onlyValid = true): IShare {
747
-		$expirationDateUpdated = false;
748
-
749
-		$this->canShare($share);
750
-
751
-		try {
752
-			$originalShare = $this->getShareById($share->getFullId(), onlyValid: $onlyValid);
753
-		} catch (\UnexpectedValueException $e) {
754
-			throw new \InvalidArgumentException($this->l->t('Share does not have a full ID'));
755
-		}
756
-
757
-		// We cannot change the share type!
758
-		if ($share->getShareType() !== $originalShare->getShareType()) {
759
-			throw new \InvalidArgumentException($this->l->t('Cannot change share type'));
760
-		}
761
-
762
-		// We can only change the recipient on user shares
763
-		if ($share->getSharedWith() !== $originalShare->getSharedWith()
764
-			&& $share->getShareType() !== IShare::TYPE_USER) {
765
-			throw new \InvalidArgumentException($this->l->t('Can only update recipient on user shares'));
766
-		}
767
-
768
-		// Cannot share with the owner
769
-		if ($share->getShareType() === IShare::TYPE_USER
770
-			&& $share->getSharedWith() === $share->getShareOwner()) {
771
-			throw new \InvalidArgumentException($this->l->t('Cannot share with the share owner'));
772
-		}
773
-
774
-		$this->generalCreateChecks($share, true);
775
-
776
-		if ($share->getShareType() === IShare::TYPE_USER) {
777
-			$this->userCreateChecks($share);
778
-
779
-			if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
780
-				// Verify the expiration date
781
-				$this->validateExpirationDateInternal($share);
782
-				$expirationDateUpdated = true;
783
-			}
784
-		} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
785
-			$this->groupCreateChecks($share);
786
-
787
-			if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
788
-				// Verify the expiration date
789
-				$this->validateExpirationDateInternal($share);
790
-				$expirationDateUpdated = true;
791
-			}
792
-		} elseif ($share->getShareType() === IShare::TYPE_LINK
793
-			|| $share->getShareType() === IShare::TYPE_EMAIL) {
794
-			$this->linkCreateChecks($share);
795
-
796
-			// The new password is not set again if it is the same as the old
797
-			// one, unless when switching from sending by Talk to sending by
798
-			// mail.
799
-			$plainTextPassword = $share->getPassword();
800
-			$updatedPassword = $this->updateSharePasswordIfNeeded($share, $originalShare);
801
-
802
-			/**
803
-			 * Cannot enable the getSendPasswordByTalk if there is no password set
804
-			 */
805
-			if (empty($plainTextPassword) && $share->getSendPasswordByTalk()) {
806
-				throw new \InvalidArgumentException($this->l->t('Cannot enable sending the password by Talk with an empty password'));
807
-			}
808
-
809
-			/**
810
-			 * If we're in a mail share, we need to force a password change
811
-			 * as either the user is not aware of the password or is already (received by mail)
812
-			 * Thus the SendPasswordByTalk feature would not make sense
813
-			 */
814
-			if (!$updatedPassword && $share->getShareType() === IShare::TYPE_EMAIL) {
815
-				if (!$originalShare->getSendPasswordByTalk() && $share->getSendPasswordByTalk()) {
816
-					throw new \InvalidArgumentException($this->l->t('Cannot enable sending the password by Talk without setting a new password'));
817
-				}
818
-				if ($originalShare->getSendPasswordByTalk() && !$share->getSendPasswordByTalk()) {
819
-					throw new \InvalidArgumentException($this->l->t('Cannot disable sending the password by Talk without setting a new password'));
820
-				}
821
-			}
822
-
823
-			if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
824
-				// Verify the expiration date
825
-				$this->validateExpirationDateLink($share);
826
-				$expirationDateUpdated = true;
827
-			}
828
-		} elseif ($share->getShareType() === IShare::TYPE_REMOTE || $share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
829
-			if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
830
-				// Verify the expiration date
831
-				$this->validateExpirationDateInternal($share);
832
-				$expirationDateUpdated = true;
833
-			}
834
-		}
835
-
836
-		$this->pathCreateChecks($share->getNode());
837
-
838
-		// Now update the share!
839
-		$provider = $this->factory->getProviderForType($share->getShareType());
840
-		if ($share->getShareType() === IShare::TYPE_EMAIL) {
841
-			/** @var ShareByMailProvider $provider */
842
-			$share = $provider->update($share, $plainTextPassword);
843
-		} else {
844
-			$share = $provider->update($share);
845
-		}
846
-
847
-		if ($expirationDateUpdated === true) {
848
-			\OC_Hook::emit(Share::class, 'post_set_expiration_date', [
849
-				'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
850
-				'itemSource' => $share->getNode()->getId(),
851
-				'date' => $share->getExpirationDate(),
852
-				'uidOwner' => $share->getSharedBy(),
853
-			]);
854
-		}
855
-
856
-		if ($share->getPassword() !== $originalShare->getPassword()) {
857
-			\OC_Hook::emit(Share::class, 'post_update_password', [
858
-				'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
859
-				'itemSource' => $share->getNode()->getId(),
860
-				'uidOwner' => $share->getSharedBy(),
861
-				'token' => $share->getToken(),
862
-				'disabled' => is_null($share->getPassword()),
863
-			]);
864
-		}
865
-
866
-		if ($share->getPermissions() !== $originalShare->getPermissions()) {
867
-			if ($this->userManager->userExists($share->getShareOwner())) {
868
-				$userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
869
-			} else {
870
-				$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
871
-			}
872
-			\OC_Hook::emit(Share::class, 'post_update_permissions', [
873
-				'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
874
-				'itemSource' => $share->getNode()->getId(),
875
-				'shareType' => $share->getShareType(),
876
-				'shareWith' => $share->getSharedWith(),
877
-				'uidOwner' => $share->getSharedBy(),
878
-				'permissions' => $share->getPermissions(),
879
-				'attributes' => $share->getAttributes() !== null ? $share->getAttributes()->toArray() : null,
880
-				'path' => $userFolder->getRelativePath($share->getNode()->getPath()),
881
-			]);
882
-		}
883
-
884
-		return $share;
885
-	}
886
-
887
-	#[Override]
888
-	public function acceptShare(IShare $share, string $recipientId): IShare {
889
-		[$providerId,] = $this->splitFullId($share->getFullId());
890
-		$provider = $this->factory->getProvider($providerId);
891
-
892
-		if (!($provider instanceof IShareProviderSupportsAccept)) {
893
-			throw new \InvalidArgumentException($this->l->t('Share provider does not support accepting'));
894
-		}
895
-		/** @var IShareProvider&IShareProviderSupportsAccept $provider */
896
-		$provider->acceptShare($share, $recipientId);
897
-
898
-		$event = new ShareAcceptedEvent($share);
899
-		$this->dispatchEvent($event, 'share accepted');
900
-
901
-		return $share;
902
-	}
903
-
904
-	/**
905
-	 * Updates the password of the given share if it is not the same as the
906
-	 * password of the original share.
907
-	 *
908
-	 * @param IShare $share the share to update its password.
909
-	 * @param IShare $originalShare the original share to compare its
910
-	 *                              password with.
911
-	 * @return bool whether the password was updated or not.
912
-	 */
913
-	private function updateSharePasswordIfNeeded(IShare $share, IShare $originalShare): bool {
914
-		$passwordsAreDifferent = ($share->getPassword() !== $originalShare->getPassword())
915
-			&& (($share->getPassword() !== null && $originalShare->getPassword() === null)
916
-				|| ($share->getPassword() === null && $originalShare->getPassword() !== null)
917
-				|| ($share->getPassword() !== null && $originalShare->getPassword() !== null
918
-					&& !$this->hasher->verify($share->getPassword(), $originalShare->getPassword())));
919
-
920
-		// Password updated.
921
-		if ($passwordsAreDifferent) {
922
-			// Verify the password
923
-			$this->verifyPassword($share->getPassword());
924
-
925
-			// If a password is set. Hash it!
926
-			if (!empty($share->getPassword())) {
927
-				$share->setPassword($this->hasher->hash($share->getPassword()));
928
-				if ($share->getShareType() === IShare::TYPE_EMAIL) {
929
-					// Shares shared by email have temporary passwords
930
-					$this->setSharePasswordExpirationTime($share);
931
-				}
932
-
933
-				return true;
934
-			} else {
935
-				// Empty string and null are seen as NOT password protected
936
-				$share->setPassword(null);
937
-				if ($share->getShareType() === IShare::TYPE_EMAIL) {
938
-					$share->setPasswordExpirationTime(null);
939
-				}
940
-				return true;
941
-			}
942
-		} else {
943
-			// Reset the password to the original one, as it is either the same
944
-			// as the "new" password or a hashed version of it.
945
-			$share->setPassword($originalShare->getPassword());
946
-		}
947
-
948
-		return false;
949
-	}
950
-
951
-	/**
952
-	 * Set the share's password expiration time
953
-	 */
954
-	private function setSharePasswordExpirationTime(IShare $share): void {
955
-		if (!$this->config->getSystemValueBool('sharing.enable_mail_link_password_expiration', false)) {
956
-			// Sets password expiration date to NULL
957
-			$share->setPasswordExpirationTime();
958
-			return;
959
-		}
960
-		// Sets password expiration date
961
-		$expirationTime = null;
962
-		$now = new \DateTime();
963
-		$expirationInterval = $this->config->getSystemValue('sharing.mail_link_password_expiration_interval', 3600);
964
-		$expirationTime = $now->add(new \DateInterval('PT' . $expirationInterval . 'S'));
965
-		$share->setPasswordExpirationTime($expirationTime);
966
-	}
967
-
968
-
969
-	/**
970
-	 * Delete all the children of this share
971
-	 *
972
-	 * @param IShare $share
973
-	 * @return list<IShare> List of deleted shares
974
-	 */
975
-	protected function deleteChildren(IShare $share): array {
976
-		$deletedShares = [];
977
-
978
-		$provider = $this->factory->getProviderForType($share->getShareType());
979
-
980
-		foreach ($provider->getChildren($share) as $child) {
981
-			$this->dispatchEvent(new BeforeShareDeletedEvent($child), 'before share deleted');
982
-
983
-			$deletedChildren = $this->deleteChildren($child);
984
-			$deletedShares = array_merge($deletedShares, $deletedChildren);
985
-
986
-			$provider->delete($child);
987
-			$this->dispatchEvent(new ShareDeletedEvent($child), 'share deleted');
988
-			$deletedShares[] = $child;
989
-		}
990
-
991
-		return $deletedShares;
992
-	}
993
-
994
-	/** Promote re-shares into direct shares so that target user keeps access */
995
-	protected function promoteReshares(IShare $share): void {
996
-		try {
997
-			$node = $share->getNode();
998
-		} catch (NotFoundException) {
999
-			/* Skip if node not found */
1000
-			return;
1001
-		}
1002
-
1003
-		$userIds = [];
1004
-
1005
-		if ($share->getShareType() === IShare::TYPE_USER) {
1006
-			$userIds[] = $share->getSharedWith();
1007
-		} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
1008
-			$group = $this->groupManager->get($share->getSharedWith());
1009
-			$users = $group?->getUsers() ?? [];
1010
-
1011
-			foreach ($users as $user) {
1012
-				/* Skip share owner */
1013
-				if ($user->getUID() === $share->getShareOwner() || $user->getUID() === $share->getSharedBy()) {
1014
-					continue;
1015
-				}
1016
-				$userIds[] = $user->getUID();
1017
-			}
1018
-		} else {
1019
-			/* We only support user and group shares */
1020
-			return;
1021
-		}
1022
-
1023
-		$reshareRecords = [];
1024
-		$shareTypes = [
1025
-			IShare::TYPE_GROUP,
1026
-			IShare::TYPE_USER,
1027
-			IShare::TYPE_LINK,
1028
-			IShare::TYPE_REMOTE,
1029
-			IShare::TYPE_EMAIL,
1030
-		];
1031
-
1032
-		foreach ($userIds as $userId) {
1033
-			foreach ($shareTypes as $shareType) {
1034
-				try {
1035
-					$provider = $this->factory->getProviderForType($shareType);
1036
-				} catch (ProviderException $e) {
1037
-					continue;
1038
-				}
1039
-
1040
-				if ($node instanceof Folder) {
1041
-					/* We need to get all shares by this user to get subshares */
1042
-					$shares = $provider->getSharesBy($userId, $shareType, null, false, -1, 0);
1043
-
1044
-					foreach ($shares as $share) {
1045
-						try {
1046
-							$path = $share->getNode()->getPath();
1047
-						} catch (NotFoundException) {
1048
-							/* Ignore share of non-existing node */
1049
-							continue;
1050
-						}
1051
-						if ($node->getRelativePath($path) !== null) {
1052
-							/* If relative path is not null it means the shared node is the same or in a subfolder */
1053
-							$reshareRecords[] = $share;
1054
-						}
1055
-					}
1056
-				} else {
1057
-					$shares = $provider->getSharesBy($userId, $shareType, $node, false, -1, 0);
1058
-					foreach ($shares as $child) {
1059
-						$reshareRecords[] = $child;
1060
-					}
1061
-				}
1062
-			}
1063
-		}
1064
-
1065
-		foreach ($reshareRecords as $child) {
1066
-			try {
1067
-				/* Check if the share is still valid (means the resharer still has access to the file through another mean) */
1068
-				$this->generalCreateChecks($child);
1069
-			} catch (GenericShareException $e) {
1070
-				/* The check is invalid, promote it to a direct share from the sharer of parent share */
1071
-				$this->logger->debug('Promote reshare because of exception ' . $e->getMessage(), ['exception' => $e, 'fullId' => $child->getFullId()]);
1072
-				try {
1073
-					$child->setSharedBy($share->getSharedBy());
1074
-					$this->updateShare($child);
1075
-				} catch (GenericShareException|\InvalidArgumentException $e) {
1076
-					$this->logger->warning('Failed to promote reshare because of exception ' . $e->getMessage(), ['exception' => $e, 'fullId' => $child->getFullId()]);
1077
-				}
1078
-			}
1079
-		}
1080
-	}
1081
-
1082
-	#[Override]
1083
-	public function deleteShare(IShare $share): void {
1084
-		try {
1085
-			$share->getFullId();
1086
-		} catch (\UnexpectedValueException $e) {
1087
-			throw new \InvalidArgumentException($this->l->t('Share does not have a full ID'));
1088
-		}
1089
-
1090
-		$this->dispatchEvent(new BeforeShareDeletedEvent($share), 'before share deleted');
1091
-
1092
-		// Get all children and delete them as well
1093
-		$this->deleteChildren($share);
1094
-
1095
-		// Do the actual delete
1096
-		$provider = $this->factory->getProviderForType($share->getShareType());
1097
-		$provider->delete($share);
1098
-
1099
-		$this->dispatchEvent(new ShareDeletedEvent($share), 'share deleted');
1100
-
1101
-		// Promote reshares of the deleted share
1102
-		$this->promoteReshares($share);
1103
-	}
1104
-
1105
-	#[Override]
1106
-	public function deleteFromSelf(IShare $share, string $recipientId): void {
1107
-		[$providerId,] = $this->splitFullId($share->getFullId());
1108
-		$provider = $this->factory->getProvider($providerId);
1109
-
1110
-		$provider->deleteFromSelf($share, $recipientId);
1111
-		$event = new ShareDeletedFromSelfEvent($share);
1112
-		$this->dispatchEvent($event, 'leave share');
1113
-	}
1114
-
1115
-	#[Override]
1116
-	public function restoreShare(IShare $share, string $recipientId): IShare {
1117
-		[$providerId,] = $this->splitFullId($share->getFullId());
1118
-		$provider = $this->factory->getProvider($providerId);
1119
-
1120
-		return $provider->restore($share, $recipientId);
1121
-	}
1122
-
1123
-	#[Override]
1124
-	public function moveShare(IShare $share, string $recipientId): IShare {
1125
-		if ($share->getShareType() === IShare::TYPE_LINK
1126
-			|| $share->getShareType() === IShare::TYPE_EMAIL) {
1127
-			throw new \InvalidArgumentException($this->l->t('Cannot change target of link share'));
1128
-		}
1129
-
1130
-		if ($share->getShareType() === IShare::TYPE_USER && $share->getSharedWith() !== $recipientId) {
1131
-			throw new \InvalidArgumentException($this->l->t('Invalid share recipient'));
1132
-		}
1133
-
1134
-		if ($share->getShareType() === IShare::TYPE_GROUP) {
1135
-			$sharedWith = $this->groupManager->get($share->getSharedWith());
1136
-			if (is_null($sharedWith)) {
1137
-				throw new \InvalidArgumentException($this->l->t('Group "%s" does not exist', [$share->getSharedWith()]));
1138
-			}
1139
-			$recipient = $this->userManager->get($recipientId);
1140
-			if (!$sharedWith->inGroup($recipient)) {
1141
-				throw new \InvalidArgumentException($this->l->t('Invalid share recipient'));
1142
-			}
1143
-		}
1144
-
1145
-		[$providerId,] = $this->splitFullId($share->getFullId());
1146
-		$provider = $this->factory->getProvider($providerId);
1147
-
1148
-		return $provider->move($share, $recipientId);
1149
-	}
1150
-
1151
-	#[Override]
1152
-	public function getSharesInFolder($userId, Folder $node, bool $reshares = false, bool $shallow = true): array {
1153
-		$providers = $this->factory->getAllProviders();
1154
-		if (!$shallow) {
1155
-			throw new \Exception('non-shallow getSharesInFolder is no longer supported');
1156
-		}
1157
-
1158
-		$isOwnerless = $node->getMountPoint() instanceof IShareOwnerlessMount;
1159
-
1160
-		$shares = [];
1161
-		foreach ($providers as $provider) {
1162
-			if ($isOwnerless) {
1163
-				// If the provider does not implement the additional interface,
1164
-				// we lack a performant way of querying all shares and therefore ignore the provider.
1165
-				if ($provider instanceof IShareProviderSupportsAllSharesInFolder) {
1166
-					foreach ($provider->getAllSharesInFolder($node) as $fid => $data) {
1167
-						$shares[$fid] ??= [];
1168
-						$shares[$fid] = array_merge($shares[$fid], $data);
1169
-					}
1170
-				}
1171
-			} else {
1172
-				foreach ($provider->getSharesInFolder($userId, $node, $reshares) as $fid => $data) {
1173
-					$shares[$fid] ??= [];
1174
-					$shares[$fid] = array_merge($shares[$fid], $data);
1175
-				}
1176
-			}
1177
-		}
1178
-
1179
-		return $shares;
1180
-	}
1181
-
1182
-	#[Override]
1183
-	public function getSharesBy(string $userId, int $shareType, ?Node $path = null, bool $reshares = false, int $limit = 50, int $offset = 0, bool $onlyValid = true): array {
1184
-		if ($path !== null
1185
-			&& !($path instanceof \OCP\Files\File)
1186
-			&& !($path instanceof \OCP\Files\Folder)) {
1187
-			throw new \InvalidArgumentException($this->l->t('Invalid path'));
1188
-		}
1189
-
1190
-		try {
1191
-			$provider = $this->factory->getProviderForType($shareType);
1192
-		} catch (ProviderException $e) {
1193
-			return [];
1194
-		}
1195
-
1196
-		if ($path?->getMountPoint() instanceof IShareOwnerlessMount) {
1197
-			$shares = array_filter($provider->getSharesByPath($path), static fn (IShare $share) => $share->getShareType() === $shareType);
1198
-		} else {
1199
-			$shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
1200
-		}
1201
-
1202
-		/*
624
+        $storage = $share->getNode()->getStorage();
625
+        if ($storage->instanceOfStorage('OCA\Files_Sharing\External\Storage')) {
626
+            $parent = $share->getNode()->getParent();
627
+            while ($parent->getStorage()->instanceOfStorage('OCA\Files_Sharing\External\Storage')) {
628
+                $parent = $parent->getParent();
629
+            }
630
+            $share->setShareOwner($parent->getOwner()->getUID());
631
+        } else {
632
+            if ($share->getNode()->getOwner()) {
633
+                $share->setShareOwner($share->getNode()->getOwner()->getUID());
634
+            } else {
635
+                $share->setShareOwner($share->getSharedBy());
636
+            }
637
+        }
638
+
639
+        try {
640
+            // Verify share type
641
+            if ($share->getShareType() === IShare::TYPE_USER) {
642
+                $this->userCreateChecks($share);
643
+
644
+                // Verify the expiration date
645
+                $share = $this->validateExpirationDateInternal($share);
646
+            } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
647
+                $this->groupCreateChecks($share);
648
+
649
+                // Verify the expiration date
650
+                $share = $this->validateExpirationDateInternal($share);
651
+            } elseif ($share->getShareType() === IShare::TYPE_REMOTE || $share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
652
+                // Verify the expiration date
653
+                $share = $this->validateExpirationDateInternal($share);
654
+            } elseif ($share->getShareType() === IShare::TYPE_LINK
655
+                || $share->getShareType() === IShare::TYPE_EMAIL) {
656
+                $this->linkCreateChecks($share);
657
+                $this->setLinkParent($share);
658
+
659
+                $token = $this->generateToken();
660
+                // Set the unique token
661
+                $share->setToken($token);
662
+
663
+                // Verify the expiration date
664
+                $share = $this->validateExpirationDateLink($share);
665
+
666
+                // Verify the password
667
+                $this->verifyPassword($share->getPassword());
668
+
669
+                // If a password is set. Hash it!
670
+                if ($share->getShareType() === IShare::TYPE_LINK
671
+                    && $share->getPassword() !== null) {
672
+                    $share->setPassword($this->hasher->hash($share->getPassword()));
673
+                }
674
+            }
675
+
676
+            // Cannot share with the owner
677
+            if ($share->getShareType() === IShare::TYPE_USER
678
+                && $share->getSharedWith() === $share->getShareOwner()) {
679
+                throw new \InvalidArgumentException($this->l->t('Cannot share with the share owner'));
680
+            }
681
+
682
+            // Generate the target
683
+            $shareFolder = $this->config->getSystemValue('share_folder', '/');
684
+            if ($share->getShareType() === IShare::TYPE_USER) {
685
+                $allowCustomShareFolder = $this->config->getSystemValueBool('sharing.allow_custom_share_folder', true);
686
+                if ($allowCustomShareFolder) {
687
+                    $shareFolder = $this->config->getUserValue($share->getSharedWith(), Application::APP_ID, 'share_folder', $shareFolder);
688
+                }
689
+            }
690
+
691
+            $target = $shareFolder . '/' . $share->getNode()->getName();
692
+            $target = \OC\Files\Filesystem::normalizePath($target);
693
+            $share->setTarget($target);
694
+
695
+            // Pre share event
696
+            $event = new Share\Events\BeforeShareCreatedEvent($share);
697
+            $this->dispatchEvent($event, 'before share created');
698
+            if ($event->isPropagationStopped() && $event->getError()) {
699
+                throw new \Exception($event->getError());
700
+            }
701
+
702
+            $oldShare = $share;
703
+            $provider = $this->factory->getProviderForType($share->getShareType());
704
+            $share = $provider->create($share);
705
+
706
+            // Reuse the node we already have
707
+            $share->setNode($oldShare->getNode());
708
+
709
+            // Reset the target if it is null for the new share
710
+            if ($share->getTarget() === '') {
711
+                $share->setTarget($target);
712
+            }
713
+        } catch (AlreadySharedException $e) {
714
+            // If a share for the same target already exists, dont create a new one,
715
+            // but do trigger the hooks and notifications again
716
+            $oldShare = $share;
717
+
718
+            // Reuse the node we already have
719
+            $share = $e->getExistingShare();
720
+            $share->setNode($oldShare->getNode());
721
+        }
722
+
723
+        // Post share event
724
+        $this->dispatchEvent(new ShareCreatedEvent($share), 'share created');
725
+
726
+        // Send email if needed
727
+        if ($this->config->getSystemValueBool('sharing.enable_share_mail', true)) {
728
+            if ($share->getMailSend()) {
729
+                $provider = $this->factory->getProviderForType($share->getShareType());
730
+                if ($provider instanceof IShareProviderWithNotification) {
731
+                    $provider->sendMailNotification($share);
732
+                } else {
733
+                    $this->logger->debug('Share notification not sent because the provider does not support it.', ['app' => 'share']);
734
+                }
735
+            } else {
736
+                $this->logger->debug('Share notification not sent because mailsend is false.', ['app' => 'share']);
737
+            }
738
+        } else {
739
+            $this->logger->debug('Share notification not sent because sharing notification emails is disabled.', ['app' => 'share']);
740
+        }
741
+
742
+        return $share;
743
+    }
744
+
745
+    #[Override]
746
+    public function updateShare(IShare $share, bool $onlyValid = true): IShare {
747
+        $expirationDateUpdated = false;
748
+
749
+        $this->canShare($share);
750
+
751
+        try {
752
+            $originalShare = $this->getShareById($share->getFullId(), onlyValid: $onlyValid);
753
+        } catch (\UnexpectedValueException $e) {
754
+            throw new \InvalidArgumentException($this->l->t('Share does not have a full ID'));
755
+        }
756
+
757
+        // We cannot change the share type!
758
+        if ($share->getShareType() !== $originalShare->getShareType()) {
759
+            throw new \InvalidArgumentException($this->l->t('Cannot change share type'));
760
+        }
761
+
762
+        // We can only change the recipient on user shares
763
+        if ($share->getSharedWith() !== $originalShare->getSharedWith()
764
+            && $share->getShareType() !== IShare::TYPE_USER) {
765
+            throw new \InvalidArgumentException($this->l->t('Can only update recipient on user shares'));
766
+        }
767
+
768
+        // Cannot share with the owner
769
+        if ($share->getShareType() === IShare::TYPE_USER
770
+            && $share->getSharedWith() === $share->getShareOwner()) {
771
+            throw new \InvalidArgumentException($this->l->t('Cannot share with the share owner'));
772
+        }
773
+
774
+        $this->generalCreateChecks($share, true);
775
+
776
+        if ($share->getShareType() === IShare::TYPE_USER) {
777
+            $this->userCreateChecks($share);
778
+
779
+            if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
780
+                // Verify the expiration date
781
+                $this->validateExpirationDateInternal($share);
782
+                $expirationDateUpdated = true;
783
+            }
784
+        } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
785
+            $this->groupCreateChecks($share);
786
+
787
+            if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
788
+                // Verify the expiration date
789
+                $this->validateExpirationDateInternal($share);
790
+                $expirationDateUpdated = true;
791
+            }
792
+        } elseif ($share->getShareType() === IShare::TYPE_LINK
793
+            || $share->getShareType() === IShare::TYPE_EMAIL) {
794
+            $this->linkCreateChecks($share);
795
+
796
+            // The new password is not set again if it is the same as the old
797
+            // one, unless when switching from sending by Talk to sending by
798
+            // mail.
799
+            $plainTextPassword = $share->getPassword();
800
+            $updatedPassword = $this->updateSharePasswordIfNeeded($share, $originalShare);
801
+
802
+            /**
803
+             * Cannot enable the getSendPasswordByTalk if there is no password set
804
+             */
805
+            if (empty($plainTextPassword) && $share->getSendPasswordByTalk()) {
806
+                throw new \InvalidArgumentException($this->l->t('Cannot enable sending the password by Talk with an empty password'));
807
+            }
808
+
809
+            /**
810
+             * If we're in a mail share, we need to force a password change
811
+             * as either the user is not aware of the password or is already (received by mail)
812
+             * Thus the SendPasswordByTalk feature would not make sense
813
+             */
814
+            if (!$updatedPassword && $share->getShareType() === IShare::TYPE_EMAIL) {
815
+                if (!$originalShare->getSendPasswordByTalk() && $share->getSendPasswordByTalk()) {
816
+                    throw new \InvalidArgumentException($this->l->t('Cannot enable sending the password by Talk without setting a new password'));
817
+                }
818
+                if ($originalShare->getSendPasswordByTalk() && !$share->getSendPasswordByTalk()) {
819
+                    throw new \InvalidArgumentException($this->l->t('Cannot disable sending the password by Talk without setting a new password'));
820
+                }
821
+            }
822
+
823
+            if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
824
+                // Verify the expiration date
825
+                $this->validateExpirationDateLink($share);
826
+                $expirationDateUpdated = true;
827
+            }
828
+        } elseif ($share->getShareType() === IShare::TYPE_REMOTE || $share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
829
+            if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
830
+                // Verify the expiration date
831
+                $this->validateExpirationDateInternal($share);
832
+                $expirationDateUpdated = true;
833
+            }
834
+        }
835
+
836
+        $this->pathCreateChecks($share->getNode());
837
+
838
+        // Now update the share!
839
+        $provider = $this->factory->getProviderForType($share->getShareType());
840
+        if ($share->getShareType() === IShare::TYPE_EMAIL) {
841
+            /** @var ShareByMailProvider $provider */
842
+            $share = $provider->update($share, $plainTextPassword);
843
+        } else {
844
+            $share = $provider->update($share);
845
+        }
846
+
847
+        if ($expirationDateUpdated === true) {
848
+            \OC_Hook::emit(Share::class, 'post_set_expiration_date', [
849
+                'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
850
+                'itemSource' => $share->getNode()->getId(),
851
+                'date' => $share->getExpirationDate(),
852
+                'uidOwner' => $share->getSharedBy(),
853
+            ]);
854
+        }
855
+
856
+        if ($share->getPassword() !== $originalShare->getPassword()) {
857
+            \OC_Hook::emit(Share::class, 'post_update_password', [
858
+                'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
859
+                'itemSource' => $share->getNode()->getId(),
860
+                'uidOwner' => $share->getSharedBy(),
861
+                'token' => $share->getToken(),
862
+                'disabled' => is_null($share->getPassword()),
863
+            ]);
864
+        }
865
+
866
+        if ($share->getPermissions() !== $originalShare->getPermissions()) {
867
+            if ($this->userManager->userExists($share->getShareOwner())) {
868
+                $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
869
+            } else {
870
+                $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
871
+            }
872
+            \OC_Hook::emit(Share::class, 'post_update_permissions', [
873
+                'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
874
+                'itemSource' => $share->getNode()->getId(),
875
+                'shareType' => $share->getShareType(),
876
+                'shareWith' => $share->getSharedWith(),
877
+                'uidOwner' => $share->getSharedBy(),
878
+                'permissions' => $share->getPermissions(),
879
+                'attributes' => $share->getAttributes() !== null ? $share->getAttributes()->toArray() : null,
880
+                'path' => $userFolder->getRelativePath($share->getNode()->getPath()),
881
+            ]);
882
+        }
883
+
884
+        return $share;
885
+    }
886
+
887
+    #[Override]
888
+    public function acceptShare(IShare $share, string $recipientId): IShare {
889
+        [$providerId,] = $this->splitFullId($share->getFullId());
890
+        $provider = $this->factory->getProvider($providerId);
891
+
892
+        if (!($provider instanceof IShareProviderSupportsAccept)) {
893
+            throw new \InvalidArgumentException($this->l->t('Share provider does not support accepting'));
894
+        }
895
+        /** @var IShareProvider&IShareProviderSupportsAccept $provider */
896
+        $provider->acceptShare($share, $recipientId);
897
+
898
+        $event = new ShareAcceptedEvent($share);
899
+        $this->dispatchEvent($event, 'share accepted');
900
+
901
+        return $share;
902
+    }
903
+
904
+    /**
905
+     * Updates the password of the given share if it is not the same as the
906
+     * password of the original share.
907
+     *
908
+     * @param IShare $share the share to update its password.
909
+     * @param IShare $originalShare the original share to compare its
910
+     *                              password with.
911
+     * @return bool whether the password was updated or not.
912
+     */
913
+    private function updateSharePasswordIfNeeded(IShare $share, IShare $originalShare): bool {
914
+        $passwordsAreDifferent = ($share->getPassword() !== $originalShare->getPassword())
915
+            && (($share->getPassword() !== null && $originalShare->getPassword() === null)
916
+                || ($share->getPassword() === null && $originalShare->getPassword() !== null)
917
+                || ($share->getPassword() !== null && $originalShare->getPassword() !== null
918
+                    && !$this->hasher->verify($share->getPassword(), $originalShare->getPassword())));
919
+
920
+        // Password updated.
921
+        if ($passwordsAreDifferent) {
922
+            // Verify the password
923
+            $this->verifyPassword($share->getPassword());
924
+
925
+            // If a password is set. Hash it!
926
+            if (!empty($share->getPassword())) {
927
+                $share->setPassword($this->hasher->hash($share->getPassword()));
928
+                if ($share->getShareType() === IShare::TYPE_EMAIL) {
929
+                    // Shares shared by email have temporary passwords
930
+                    $this->setSharePasswordExpirationTime($share);
931
+                }
932
+
933
+                return true;
934
+            } else {
935
+                // Empty string and null are seen as NOT password protected
936
+                $share->setPassword(null);
937
+                if ($share->getShareType() === IShare::TYPE_EMAIL) {
938
+                    $share->setPasswordExpirationTime(null);
939
+                }
940
+                return true;
941
+            }
942
+        } else {
943
+            // Reset the password to the original one, as it is either the same
944
+            // as the "new" password or a hashed version of it.
945
+            $share->setPassword($originalShare->getPassword());
946
+        }
947
+
948
+        return false;
949
+    }
950
+
951
+    /**
952
+     * Set the share's password expiration time
953
+     */
954
+    private function setSharePasswordExpirationTime(IShare $share): void {
955
+        if (!$this->config->getSystemValueBool('sharing.enable_mail_link_password_expiration', false)) {
956
+            // Sets password expiration date to NULL
957
+            $share->setPasswordExpirationTime();
958
+            return;
959
+        }
960
+        // Sets password expiration date
961
+        $expirationTime = null;
962
+        $now = new \DateTime();
963
+        $expirationInterval = $this->config->getSystemValue('sharing.mail_link_password_expiration_interval', 3600);
964
+        $expirationTime = $now->add(new \DateInterval('PT' . $expirationInterval . 'S'));
965
+        $share->setPasswordExpirationTime($expirationTime);
966
+    }
967
+
968
+
969
+    /**
970
+     * Delete all the children of this share
971
+     *
972
+     * @param IShare $share
973
+     * @return list<IShare> List of deleted shares
974
+     */
975
+    protected function deleteChildren(IShare $share): array {
976
+        $deletedShares = [];
977
+
978
+        $provider = $this->factory->getProviderForType($share->getShareType());
979
+
980
+        foreach ($provider->getChildren($share) as $child) {
981
+            $this->dispatchEvent(new BeforeShareDeletedEvent($child), 'before share deleted');
982
+
983
+            $deletedChildren = $this->deleteChildren($child);
984
+            $deletedShares = array_merge($deletedShares, $deletedChildren);
985
+
986
+            $provider->delete($child);
987
+            $this->dispatchEvent(new ShareDeletedEvent($child), 'share deleted');
988
+            $deletedShares[] = $child;
989
+        }
990
+
991
+        return $deletedShares;
992
+    }
993
+
994
+    /** Promote re-shares into direct shares so that target user keeps access */
995
+    protected function promoteReshares(IShare $share): void {
996
+        try {
997
+            $node = $share->getNode();
998
+        } catch (NotFoundException) {
999
+            /* Skip if node not found */
1000
+            return;
1001
+        }
1002
+
1003
+        $userIds = [];
1004
+
1005
+        if ($share->getShareType() === IShare::TYPE_USER) {
1006
+            $userIds[] = $share->getSharedWith();
1007
+        } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
1008
+            $group = $this->groupManager->get($share->getSharedWith());
1009
+            $users = $group?->getUsers() ?? [];
1010
+
1011
+            foreach ($users as $user) {
1012
+                /* Skip share owner */
1013
+                if ($user->getUID() === $share->getShareOwner() || $user->getUID() === $share->getSharedBy()) {
1014
+                    continue;
1015
+                }
1016
+                $userIds[] = $user->getUID();
1017
+            }
1018
+        } else {
1019
+            /* We only support user and group shares */
1020
+            return;
1021
+        }
1022
+
1023
+        $reshareRecords = [];
1024
+        $shareTypes = [
1025
+            IShare::TYPE_GROUP,
1026
+            IShare::TYPE_USER,
1027
+            IShare::TYPE_LINK,
1028
+            IShare::TYPE_REMOTE,
1029
+            IShare::TYPE_EMAIL,
1030
+        ];
1031
+
1032
+        foreach ($userIds as $userId) {
1033
+            foreach ($shareTypes as $shareType) {
1034
+                try {
1035
+                    $provider = $this->factory->getProviderForType($shareType);
1036
+                } catch (ProviderException $e) {
1037
+                    continue;
1038
+                }
1039
+
1040
+                if ($node instanceof Folder) {
1041
+                    /* We need to get all shares by this user to get subshares */
1042
+                    $shares = $provider->getSharesBy($userId, $shareType, null, false, -1, 0);
1043
+
1044
+                    foreach ($shares as $share) {
1045
+                        try {
1046
+                            $path = $share->getNode()->getPath();
1047
+                        } catch (NotFoundException) {
1048
+                            /* Ignore share of non-existing node */
1049
+                            continue;
1050
+                        }
1051
+                        if ($node->getRelativePath($path) !== null) {
1052
+                            /* If relative path is not null it means the shared node is the same or in a subfolder */
1053
+                            $reshareRecords[] = $share;
1054
+                        }
1055
+                    }
1056
+                } else {
1057
+                    $shares = $provider->getSharesBy($userId, $shareType, $node, false, -1, 0);
1058
+                    foreach ($shares as $child) {
1059
+                        $reshareRecords[] = $child;
1060
+                    }
1061
+                }
1062
+            }
1063
+        }
1064
+
1065
+        foreach ($reshareRecords as $child) {
1066
+            try {
1067
+                /* Check if the share is still valid (means the resharer still has access to the file through another mean) */
1068
+                $this->generalCreateChecks($child);
1069
+            } catch (GenericShareException $e) {
1070
+                /* The check is invalid, promote it to a direct share from the sharer of parent share */
1071
+                $this->logger->debug('Promote reshare because of exception ' . $e->getMessage(), ['exception' => $e, 'fullId' => $child->getFullId()]);
1072
+                try {
1073
+                    $child->setSharedBy($share->getSharedBy());
1074
+                    $this->updateShare($child);
1075
+                } catch (GenericShareException|\InvalidArgumentException $e) {
1076
+                    $this->logger->warning('Failed to promote reshare because of exception ' . $e->getMessage(), ['exception' => $e, 'fullId' => $child->getFullId()]);
1077
+                }
1078
+            }
1079
+        }
1080
+    }
1081
+
1082
+    #[Override]
1083
+    public function deleteShare(IShare $share): void {
1084
+        try {
1085
+            $share->getFullId();
1086
+        } catch (\UnexpectedValueException $e) {
1087
+            throw new \InvalidArgumentException($this->l->t('Share does not have a full ID'));
1088
+        }
1089
+
1090
+        $this->dispatchEvent(new BeforeShareDeletedEvent($share), 'before share deleted');
1091
+
1092
+        // Get all children and delete them as well
1093
+        $this->deleteChildren($share);
1094
+
1095
+        // Do the actual delete
1096
+        $provider = $this->factory->getProviderForType($share->getShareType());
1097
+        $provider->delete($share);
1098
+
1099
+        $this->dispatchEvent(new ShareDeletedEvent($share), 'share deleted');
1100
+
1101
+        // Promote reshares of the deleted share
1102
+        $this->promoteReshares($share);
1103
+    }
1104
+
1105
+    #[Override]
1106
+    public function deleteFromSelf(IShare $share, string $recipientId): void {
1107
+        [$providerId,] = $this->splitFullId($share->getFullId());
1108
+        $provider = $this->factory->getProvider($providerId);
1109
+
1110
+        $provider->deleteFromSelf($share, $recipientId);
1111
+        $event = new ShareDeletedFromSelfEvent($share);
1112
+        $this->dispatchEvent($event, 'leave share');
1113
+    }
1114
+
1115
+    #[Override]
1116
+    public function restoreShare(IShare $share, string $recipientId): IShare {
1117
+        [$providerId,] = $this->splitFullId($share->getFullId());
1118
+        $provider = $this->factory->getProvider($providerId);
1119
+
1120
+        return $provider->restore($share, $recipientId);
1121
+    }
1122
+
1123
+    #[Override]
1124
+    public function moveShare(IShare $share, string $recipientId): IShare {
1125
+        if ($share->getShareType() === IShare::TYPE_LINK
1126
+            || $share->getShareType() === IShare::TYPE_EMAIL) {
1127
+            throw new \InvalidArgumentException($this->l->t('Cannot change target of link share'));
1128
+        }
1129
+
1130
+        if ($share->getShareType() === IShare::TYPE_USER && $share->getSharedWith() !== $recipientId) {
1131
+            throw new \InvalidArgumentException($this->l->t('Invalid share recipient'));
1132
+        }
1133
+
1134
+        if ($share->getShareType() === IShare::TYPE_GROUP) {
1135
+            $sharedWith = $this->groupManager->get($share->getSharedWith());
1136
+            if (is_null($sharedWith)) {
1137
+                throw new \InvalidArgumentException($this->l->t('Group "%s" does not exist', [$share->getSharedWith()]));
1138
+            }
1139
+            $recipient = $this->userManager->get($recipientId);
1140
+            if (!$sharedWith->inGroup($recipient)) {
1141
+                throw new \InvalidArgumentException($this->l->t('Invalid share recipient'));
1142
+            }
1143
+        }
1144
+
1145
+        [$providerId,] = $this->splitFullId($share->getFullId());
1146
+        $provider = $this->factory->getProvider($providerId);
1147
+
1148
+        return $provider->move($share, $recipientId);
1149
+    }
1150
+
1151
+    #[Override]
1152
+    public function getSharesInFolder($userId, Folder $node, bool $reshares = false, bool $shallow = true): array {
1153
+        $providers = $this->factory->getAllProviders();
1154
+        if (!$shallow) {
1155
+            throw new \Exception('non-shallow getSharesInFolder is no longer supported');
1156
+        }
1157
+
1158
+        $isOwnerless = $node->getMountPoint() instanceof IShareOwnerlessMount;
1159
+
1160
+        $shares = [];
1161
+        foreach ($providers as $provider) {
1162
+            if ($isOwnerless) {
1163
+                // If the provider does not implement the additional interface,
1164
+                // we lack a performant way of querying all shares and therefore ignore the provider.
1165
+                if ($provider instanceof IShareProviderSupportsAllSharesInFolder) {
1166
+                    foreach ($provider->getAllSharesInFolder($node) as $fid => $data) {
1167
+                        $shares[$fid] ??= [];
1168
+                        $shares[$fid] = array_merge($shares[$fid], $data);
1169
+                    }
1170
+                }
1171
+            } else {
1172
+                foreach ($provider->getSharesInFolder($userId, $node, $reshares) as $fid => $data) {
1173
+                    $shares[$fid] ??= [];
1174
+                    $shares[$fid] = array_merge($shares[$fid], $data);
1175
+                }
1176
+            }
1177
+        }
1178
+
1179
+        return $shares;
1180
+    }
1181
+
1182
+    #[Override]
1183
+    public function getSharesBy(string $userId, int $shareType, ?Node $path = null, bool $reshares = false, int $limit = 50, int $offset = 0, bool $onlyValid = true): array {
1184
+        if ($path !== null
1185
+            && !($path instanceof \OCP\Files\File)
1186
+            && !($path instanceof \OCP\Files\Folder)) {
1187
+            throw new \InvalidArgumentException($this->l->t('Invalid path'));
1188
+        }
1189
+
1190
+        try {
1191
+            $provider = $this->factory->getProviderForType($shareType);
1192
+        } catch (ProviderException $e) {
1193
+            return [];
1194
+        }
1195
+
1196
+        if ($path?->getMountPoint() instanceof IShareOwnerlessMount) {
1197
+            $shares = array_filter($provider->getSharesByPath($path), static fn (IShare $share) => $share->getShareType() === $shareType);
1198
+        } else {
1199
+            $shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
1200
+        }
1201
+
1202
+        /*
1203 1203
 		 * Work around so we don't return expired shares but still follow
1204 1204
 		 * proper pagination.
1205 1205
 		 */
1206 1206
 
1207
-		$shares2 = [];
1208
-
1209
-		while (true) {
1210
-			$added = 0;
1211
-			foreach ($shares as $share) {
1212
-				$added++;
1213
-				if ($onlyValid) {
1214
-					try {
1215
-						$this->checkShare($share, $added);
1216
-					} catch (ShareNotFound $e) {
1217
-						// Ignore since this basically means the share is deleted
1218
-						continue;
1219
-					}
1220
-				}
1221
-
1222
-				$shares2[] = $share;
1223
-
1224
-				if (count($shares2) === $limit) {
1225
-					break;
1226
-				}
1227
-			}
1228
-
1229
-			// If we did not fetch more shares than the limit then there are no more shares
1230
-			if (count($shares) < $limit) {
1231
-				break;
1232
-			}
1233
-
1234
-			if (count($shares2) === $limit) {
1235
-				break;
1236
-			}
1237
-
1238
-			// If there was no limit on the select we are done
1239
-			if ($limit === -1) {
1240
-				break;
1241
-			}
1242
-
1243
-			$offset += $added;
1244
-
1245
-			// Fetch again $limit shares
1246
-			if ($path?->getMountPoint() instanceof IShareOwnerlessMount) {
1247
-				// We already fetched all shares, so end here
1248
-				$shares = [];
1249
-			} else {
1250
-				$shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
1251
-			}
1252
-
1253
-			// No more shares means we are done
1254
-			if (empty($shares)) {
1255
-				break;
1256
-			}
1257
-		}
1258
-
1259
-		$shares = $shares2;
1260
-
1261
-		return $shares;
1262
-	}
1263
-
1264
-	#[Override]
1265
-	public function getSharedWith(string $userId, int $shareType, ?Node $node = null, int $limit = 50, int $offset = 0): array {
1266
-		try {
1267
-			$provider = $this->factory->getProviderForType($shareType);
1268
-		} catch (ProviderException $e) {
1269
-			return [];
1270
-		}
1271
-
1272
-		$shares = $provider->getSharedWith($userId, $shareType, $node, $limit, $offset);
1273
-
1274
-		// remove all shares which are already expired
1275
-		foreach ($shares as $key => $share) {
1276
-			try {
1277
-				$this->checkShare($share);
1278
-			} catch (ShareNotFound $e) {
1279
-				unset($shares[$key]);
1280
-			}
1281
-		}
1282
-
1283
-		return $shares;
1284
-	}
1285
-
1286
-	#[Override]
1287
-	public function getDeletedSharedWith(string $userId, int $shareType, ?Node $node = null, int $limit = 50, int $offset = 0): array {
1288
-		$shares = $this->getSharedWith($userId, $shareType, $node, $limit, $offset);
1289
-
1290
-		// Only get shares deleted shares and where the owner still exists
1291
-		return array_filter($shares, fn (IShare $share): bool => $share->getPermissions() === 0
1292
-			&& $this->userManager->userExists($share->getShareOwner()));
1293
-	}
1294
-
1295
-	#[Override]
1296
-	public function getShareById($id, $recipient = null, bool $onlyValid = true): IShare {
1297
-		if ($id === null) {
1298
-			throw new ShareNotFound();
1299
-		}
1300
-
1301
-		[$providerId, $id] = $this->splitFullId($id);
1302
-
1303
-		try {
1304
-			$provider = $this->factory->getProvider($providerId);
1305
-		} catch (ProviderException $e) {
1306
-			throw new ShareNotFound();
1307
-		}
1308
-
1309
-		$share = $provider->getShareById($id, $recipient);
1310
-
1311
-		if ($onlyValid) {
1312
-			$this->checkShare($share);
1313
-		}
1314
-
1315
-		return $share;
1316
-	}
1317
-
1318
-	#[Override]
1319
-	public function getShareByToken(string $token): IShare {
1320
-		// tokens cannot be valid local usernames
1321
-		if ($this->userManager->userExists($token)) {
1322
-			throw new ShareNotFound();
1323
-		}
1324
-		$share = null;
1325
-		try {
1326
-			if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') === 'yes') {
1327
-				$provider = $this->factory->getProviderForType(IShare::TYPE_LINK);
1328
-				$share = $provider->getShareByToken($token);
1329
-			}
1330
-		} catch (ProviderException|ShareNotFound) {
1331
-		}
1332
-
1333
-
1334
-		// If it is not a link share try to fetch a federated share by token
1335
-		if ($share === null) {
1336
-			try {
1337
-				$provider = $this->factory->getProviderForType(IShare::TYPE_REMOTE);
1338
-				$share = $provider->getShareByToken($token);
1339
-			} catch (ProviderException|ShareNotFound) {
1340
-			}
1341
-		}
1342
-
1343
-		// If it is not a link share try to fetch a mail share by token
1344
-		if ($share === null && $this->shareProviderExists(IShare::TYPE_EMAIL)) {
1345
-			try {
1346
-				$provider = $this->factory->getProviderForType(IShare::TYPE_EMAIL);
1347
-				$share = $provider->getShareByToken($token);
1348
-			} catch (ProviderException|ShareNotFound) {
1349
-			}
1350
-		}
1351
-
1352
-		if ($share === null && $this->shareProviderExists(IShare::TYPE_CIRCLE)) {
1353
-			try {
1354
-				$provider = $this->factory->getProviderForType(IShare::TYPE_CIRCLE);
1355
-				$share = $provider->getShareByToken($token);
1356
-			} catch (ProviderException|ShareNotFound) {
1357
-			}
1358
-		}
1359
-
1360
-		if ($share === null && $this->shareProviderExists(IShare::TYPE_ROOM)) {
1361
-			try {
1362
-				$provider = $this->factory->getProviderForType(IShare::TYPE_ROOM);
1363
-				$share = $provider->getShareByToken($token);
1364
-			} catch (ProviderException|ShareNotFound) {
1365
-			}
1366
-		}
1367
-
1368
-		if ($share === null) {
1369
-			throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
1370
-		}
1371
-
1372
-		$this->checkShare($share);
1373
-
1374
-		/*
1207
+        $shares2 = [];
1208
+
1209
+        while (true) {
1210
+            $added = 0;
1211
+            foreach ($shares as $share) {
1212
+                $added++;
1213
+                if ($onlyValid) {
1214
+                    try {
1215
+                        $this->checkShare($share, $added);
1216
+                    } catch (ShareNotFound $e) {
1217
+                        // Ignore since this basically means the share is deleted
1218
+                        continue;
1219
+                    }
1220
+                }
1221
+
1222
+                $shares2[] = $share;
1223
+
1224
+                if (count($shares2) === $limit) {
1225
+                    break;
1226
+                }
1227
+            }
1228
+
1229
+            // If we did not fetch more shares than the limit then there are no more shares
1230
+            if (count($shares) < $limit) {
1231
+                break;
1232
+            }
1233
+
1234
+            if (count($shares2) === $limit) {
1235
+                break;
1236
+            }
1237
+
1238
+            // If there was no limit on the select we are done
1239
+            if ($limit === -1) {
1240
+                break;
1241
+            }
1242
+
1243
+            $offset += $added;
1244
+
1245
+            // Fetch again $limit shares
1246
+            if ($path?->getMountPoint() instanceof IShareOwnerlessMount) {
1247
+                // We already fetched all shares, so end here
1248
+                $shares = [];
1249
+            } else {
1250
+                $shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
1251
+            }
1252
+
1253
+            // No more shares means we are done
1254
+            if (empty($shares)) {
1255
+                break;
1256
+            }
1257
+        }
1258
+
1259
+        $shares = $shares2;
1260
+
1261
+        return $shares;
1262
+    }
1263
+
1264
+    #[Override]
1265
+    public function getSharedWith(string $userId, int $shareType, ?Node $node = null, int $limit = 50, int $offset = 0): array {
1266
+        try {
1267
+            $provider = $this->factory->getProviderForType($shareType);
1268
+        } catch (ProviderException $e) {
1269
+            return [];
1270
+        }
1271
+
1272
+        $shares = $provider->getSharedWith($userId, $shareType, $node, $limit, $offset);
1273
+
1274
+        // remove all shares which are already expired
1275
+        foreach ($shares as $key => $share) {
1276
+            try {
1277
+                $this->checkShare($share);
1278
+            } catch (ShareNotFound $e) {
1279
+                unset($shares[$key]);
1280
+            }
1281
+        }
1282
+
1283
+        return $shares;
1284
+    }
1285
+
1286
+    #[Override]
1287
+    public function getDeletedSharedWith(string $userId, int $shareType, ?Node $node = null, int $limit = 50, int $offset = 0): array {
1288
+        $shares = $this->getSharedWith($userId, $shareType, $node, $limit, $offset);
1289
+
1290
+        // Only get shares deleted shares and where the owner still exists
1291
+        return array_filter($shares, fn (IShare $share): bool => $share->getPermissions() === 0
1292
+            && $this->userManager->userExists($share->getShareOwner()));
1293
+    }
1294
+
1295
+    #[Override]
1296
+    public function getShareById($id, $recipient = null, bool $onlyValid = true): IShare {
1297
+        if ($id === null) {
1298
+            throw new ShareNotFound();
1299
+        }
1300
+
1301
+        [$providerId, $id] = $this->splitFullId($id);
1302
+
1303
+        try {
1304
+            $provider = $this->factory->getProvider($providerId);
1305
+        } catch (ProviderException $e) {
1306
+            throw new ShareNotFound();
1307
+        }
1308
+
1309
+        $share = $provider->getShareById($id, $recipient);
1310
+
1311
+        if ($onlyValid) {
1312
+            $this->checkShare($share);
1313
+        }
1314
+
1315
+        return $share;
1316
+    }
1317
+
1318
+    #[Override]
1319
+    public function getShareByToken(string $token): IShare {
1320
+        // tokens cannot be valid local usernames
1321
+        if ($this->userManager->userExists($token)) {
1322
+            throw new ShareNotFound();
1323
+        }
1324
+        $share = null;
1325
+        try {
1326
+            if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') === 'yes') {
1327
+                $provider = $this->factory->getProviderForType(IShare::TYPE_LINK);
1328
+                $share = $provider->getShareByToken($token);
1329
+            }
1330
+        } catch (ProviderException|ShareNotFound) {
1331
+        }
1332
+
1333
+
1334
+        // If it is not a link share try to fetch a federated share by token
1335
+        if ($share === null) {
1336
+            try {
1337
+                $provider = $this->factory->getProviderForType(IShare::TYPE_REMOTE);
1338
+                $share = $provider->getShareByToken($token);
1339
+            } catch (ProviderException|ShareNotFound) {
1340
+            }
1341
+        }
1342
+
1343
+        // If it is not a link share try to fetch a mail share by token
1344
+        if ($share === null && $this->shareProviderExists(IShare::TYPE_EMAIL)) {
1345
+            try {
1346
+                $provider = $this->factory->getProviderForType(IShare::TYPE_EMAIL);
1347
+                $share = $provider->getShareByToken($token);
1348
+            } catch (ProviderException|ShareNotFound) {
1349
+            }
1350
+        }
1351
+
1352
+        if ($share === null && $this->shareProviderExists(IShare::TYPE_CIRCLE)) {
1353
+            try {
1354
+                $provider = $this->factory->getProviderForType(IShare::TYPE_CIRCLE);
1355
+                $share = $provider->getShareByToken($token);
1356
+            } catch (ProviderException|ShareNotFound) {
1357
+            }
1358
+        }
1359
+
1360
+        if ($share === null && $this->shareProviderExists(IShare::TYPE_ROOM)) {
1361
+            try {
1362
+                $provider = $this->factory->getProviderForType(IShare::TYPE_ROOM);
1363
+                $share = $provider->getShareByToken($token);
1364
+            } catch (ProviderException|ShareNotFound) {
1365
+            }
1366
+        }
1367
+
1368
+        if ($share === null) {
1369
+            throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
1370
+        }
1371
+
1372
+        $this->checkShare($share);
1373
+
1374
+        /*
1375 1375
 		 * Reduce the permissions for link or email shares if public upload is not enabled
1376 1376
 		 */
1377
-		if (($share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL)
1378
-			&& $share->getNodeType() === 'folder' && !$this->shareApiLinkAllowPublicUpload()) {
1379
-			$share->setPermissions($share->getPermissions() & ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE));
1380
-		}
1381
-
1382
-		return $share;
1383
-	}
1384
-
1385
-	/**
1386
-	 * Check expire date and disabled owner
1387
-	 *
1388
-	 * @param int &$added If given, will be decremented if the share is deleted
1389
-	 * @throws ShareNotFound
1390
-	 */
1391
-	private function checkShare(IShare $share, int &$added = 1): void {
1392
-		if ($share->isExpired()) {
1393
-			$this->deleteShare($share);
1394
-			// Remove 1 to added, because this share was deleted
1395
-			$added--;
1396
-			throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
1397
-		}
1398
-		if ($this->config->getAppValue('files_sharing', 'hide_disabled_user_shares', 'no') === 'yes') {
1399
-			$uids = array_unique([$share->getShareOwner(),$share->getSharedBy()]);
1400
-			foreach ($uids as $uid) {
1401
-				$user = $this->userManager->get($uid);
1402
-				if ($user?->isEnabled() === false) {
1403
-					throw new ShareNotFound($this->l->t('The requested share comes from a disabled user'));
1404
-				}
1405
-			}
1406
-		}
1407
-
1408
-		// For link and email shares, verify the share owner can still create such shares
1409
-		if ($share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL) {
1410
-			$shareOwner = $this->userManager->get($share->getShareOwner());
1411
-			if ($shareOwner === null) {
1412
-				throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
1413
-			}
1414
-			if (!$this->userCanCreateLinkShares($shareOwner)) {
1415
-				throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
1416
-			}
1417
-		}
1418
-	}
1419
-
1420
-	#[Override]
1421
-	public function checkPassword(IShare $share, ?string $password): bool {
1422
-
1423
-		// if there is no password on the share object / passsword is null, there is nothing to check
1424
-		if ($password === null || $share->getPassword() === null) {
1425
-			return false;
1426
-		}
1427
-
1428
-		// Makes sure password hasn't expired
1429
-		$expirationTime = $share->getPasswordExpirationTime();
1430
-		if ($expirationTime !== null && $expirationTime < new \DateTime()) {
1431
-			return false;
1432
-		}
1433
-
1434
-		$newHash = '';
1435
-		if (!$this->hasher->verify($password, $share->getPassword(), $newHash)) {
1436
-			return false;
1437
-		}
1438
-
1439
-		if (!empty($newHash)) {
1440
-			$share->setPassword($newHash);
1441
-			$provider = $this->factory->getProviderForType($share->getShareType());
1442
-			$provider->update($share);
1443
-		}
1444
-
1445
-		return true;
1446
-	}
1447
-
1448
-	#[Override]
1449
-	public function userDeleted(string $uid): void {
1450
-		$types = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK, IShare::TYPE_REMOTE, IShare::TYPE_EMAIL];
1451
-
1452
-		foreach ($types as $type) {
1453
-			try {
1454
-				$provider = $this->factory->getProviderForType($type);
1455
-			} catch (ProviderException $e) {
1456
-				continue;
1457
-			}
1458
-			$provider->userDeleted($uid, $type);
1459
-		}
1460
-	}
1461
-
1462
-	#[Override]
1463
-	public function groupDeleted(string $gid): void {
1464
-		foreach ([IShare::TYPE_GROUP, IShare::TYPE_REMOTE_GROUP] as $type) {
1465
-			try {
1466
-				$provider = $this->factory->getProviderForType($type);
1467
-			} catch (ProviderException $e) {
1468
-				continue;
1469
-			}
1470
-			$provider->groupDeleted($gid);
1471
-		}
1472
-
1473
-		$excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', '');
1474
-		if ($excludedGroups === '') {
1475
-			return;
1476
-		}
1477
-
1478
-		$excludedGroups = json_decode($excludedGroups, true);
1479
-		if (json_last_error() !== JSON_ERROR_NONE) {
1480
-			return;
1481
-		}
1482
-
1483
-		$excludedGroups = array_diff($excludedGroups, [$gid]);
1484
-		$this->config->setAppValue('core', 'shareapi_exclude_groups_list', json_encode($excludedGroups));
1485
-	}
1486
-
1487
-	#[Override]
1488
-	public function userDeletedFromGroup(string $uid, string $gid): void {
1489
-		foreach ([IShare::TYPE_GROUP, IShare::TYPE_REMOTE_GROUP] as $type) {
1490
-			try {
1491
-				$provider = $this->factory->getProviderForType($type);
1492
-			} catch (ProviderException $e) {
1493
-				continue;
1494
-			}
1495
-			$provider->userDeletedFromGroup($uid, $gid);
1496
-		}
1497
-	}
1498
-
1499
-	#[\Override]
1500
-	public function getAccessList(\OCP\Files\Node $path, $recursive = true, $currentAccess = false): array {
1501
-		$owner = $path->getOwner();
1502
-
1503
-		if ($owner === null) {
1504
-			return [];
1505
-		}
1506
-
1507
-		$owner = $owner->getUID();
1508
-
1509
-		if ($currentAccess) {
1510
-			$al = ['users' => [], 'remote' => [], 'public' => false, 'mail' => []];
1511
-		} else {
1512
-			$al = ['users' => [], 'remote' => false, 'public' => false, 'mail' => []];
1513
-		}
1514
-		if (!$this->userManager->userExists($owner)) {
1515
-			return $al;
1516
-		}
1517
-
1518
-		//Get node for the owner and correct the owner in case of external storage
1519
-		$userFolder = $this->rootFolder->getUserFolder($owner);
1520
-		if ($path->getId() !== $userFolder->getId() && !$userFolder->isSubNode($path)) {
1521
-			$path = $userFolder->getFirstNodeById($path->getId());
1522
-			if ($path === null || $path->getOwner() === null) {
1523
-				return [];
1524
-			}
1525
-			$owner = $path->getOwner()->getUID();
1526
-		}
1527
-
1528
-		$providers = $this->factory->getAllProviders();
1529
-
1530
-		/** @var Node[] $nodes */
1531
-		$nodes = [];
1532
-
1533
-
1534
-		if ($currentAccess) {
1535
-			$ownerPath = $path->getPath();
1536
-			$ownerPath = explode('/', $ownerPath, 4);
1537
-			if (count($ownerPath) < 4) {
1538
-				$ownerPath = '';
1539
-			} else {
1540
-				$ownerPath = $ownerPath[3];
1541
-			}
1542
-			$al['users'][$owner] = [
1543
-				'node_id' => $path->getId(),
1544
-				'node_path' => '/' . $ownerPath,
1545
-			];
1546
-		} else {
1547
-			$al['users'][] = $owner;
1548
-		}
1549
-
1550
-		// Collect all the shares
1551
-		while ($path->getPath() !== $userFolder->getPath()) {
1552
-			$nodes[] = $path;
1553
-			if (!$recursive) {
1554
-				break;
1555
-			}
1556
-			$path = $path->getParent();
1557
-		}
1558
-
1559
-		foreach ($providers as $provider) {
1560
-			$tmp = $provider->getAccessList($nodes, $currentAccess);
1561
-
1562
-			foreach ($tmp as $k => $v) {
1563
-				if (isset($al[$k])) {
1564
-					if (is_array($al[$k])) {
1565
-						if ($currentAccess) {
1566
-							$al[$k] += $v;
1567
-						} else {
1568
-							$al[$k] = array_merge($al[$k], $v);
1569
-							$al[$k] = array_unique($al[$k]);
1570
-							$al[$k] = array_values($al[$k]);
1571
-						}
1572
-					} else {
1573
-						$al[$k] = $al[$k] || $v;
1574
-					}
1575
-				} else {
1576
-					$al[$k] = $v;
1577
-				}
1578
-			}
1579
-		}
1580
-
1581
-		return $al;
1582
-	}
1583
-
1584
-	#[Override]
1585
-	public function newShare(): IShare {
1586
-		return new \OC\Share20\Share($this->rootFolder, $this->userManager);
1587
-	}
1588
-
1589
-	#[Override]
1590
-	public function shareApiEnabled(): bool {
1591
-		return $this->config->getAppValue('core', 'shareapi_enabled', 'yes') === 'yes';
1592
-	}
1593
-
1594
-	#[Override]
1595
-	public function shareApiAllowLinks(?IUser $user = null): bool {
1596
-		if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') {
1597
-			return false;
1598
-		}
1599
-
1600
-		$user = $user ?? $this->userSession->getUser();
1601
-		if ($user) {
1602
-			$excludedGroups = json_decode($this->config->getAppValue('core', 'shareapi_allow_links_exclude_groups', '[]'));
1603
-			if ($excludedGroups) {
1604
-				$userGroups = $this->groupManager->getUserGroupIds($user);
1605
-				return !(bool)array_intersect($excludedGroups, $userGroups);
1606
-			}
1607
-		}
1608
-
1609
-		return true;
1610
-	}
1611
-
1612
-	/**
1613
-	 * Check if a specific user can create link shares
1614
-	 *
1615
-	 * @param IUser $user The user to check
1616
-	 * @return bool
1617
-	 */
1618
-	protected function userCanCreateLinkShares(IUser $user): bool {
1619
-		return $this->shareApiAllowLinks($user);
1620
-	}
1621
-
1622
-	#[Override]
1623
-	public function shareApiLinkEnforcePassword(bool $checkGroupMembership = true): bool {
1624
-		$excludedGroups = $this->config->getAppValue('core', 'shareapi_enforce_links_password_excluded_groups', '');
1625
-		if ($excludedGroups !== '' && $checkGroupMembership) {
1626
-			$excludedGroups = json_decode($excludedGroups);
1627
-			$user = $this->userSession->getUser();
1628
-			if ($user) {
1629
-				$userGroups = $this->groupManager->getUserGroupIds($user);
1630
-				if ((bool)array_intersect($excludedGroups, $userGroups)) {
1631
-					return false;
1632
-				}
1633
-			}
1634
-		}
1635
-		return $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_PASSWORD_ENFORCED);
1636
-	}
1637
-
1638
-	#[Override]
1639
-	public function shareApiLinkDefaultExpireDate(): bool {
1640
-		return $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_EXPIRE_DATE_DEFAULT);
1641
-	}
1642
-
1643
-	#[Override]
1644
-	public function shareApiLinkDefaultExpireDateEnforced(): bool {
1645
-		return $this->shareApiLinkDefaultExpireDate()
1646
-			&& $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_EXPIRE_DATE_ENFORCED);
1647
-	}
1648
-
1649
-	#[Override]
1650
-	public function shareApiLinkDefaultExpireDays(): int {
1651
-		return (int)$this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7');
1652
-	}
1653
-
1654
-	#[Override]
1655
-	public function shareApiInternalDefaultExpireDate(): bool {
1656
-		return $this->config->getAppValue('core', 'shareapi_default_internal_expire_date', 'no') === 'yes';
1657
-	}
1658
-
1659
-	#[Override]
1660
-	public function shareApiRemoteDefaultExpireDate(): bool {
1661
-		return $this->config->getAppValue('core', 'shareapi_default_remote_expire_date', 'no') === 'yes';
1662
-	}
1663
-
1664
-	#[Override]
1665
-	public function shareApiInternalDefaultExpireDateEnforced(): bool {
1666
-		return $this->shareApiInternalDefaultExpireDate()
1667
-			&& $this->config->getAppValue('core', 'shareapi_enforce_internal_expire_date', 'no') === 'yes';
1668
-	}
1669
-
1670
-	#[Override]
1671
-	public function shareApiRemoteDefaultExpireDateEnforced(): bool {
1672
-		return $this->shareApiRemoteDefaultExpireDate()
1673
-			&& $this->config->getAppValue('core', 'shareapi_enforce_remote_expire_date', 'no') === 'yes';
1674
-	}
1675
-
1676
-	#[Override]
1677
-	public function shareApiInternalDefaultExpireDays(): int {
1678
-		return (int)$this->config->getAppValue('core', 'shareapi_internal_expire_after_n_days', '7');
1679
-	}
1680
-
1681
-	#[Override]
1682
-	public function shareApiRemoteDefaultExpireDays(): int {
1683
-		return (int)$this->config->getAppValue('core', 'shareapi_remote_expire_after_n_days', '7');
1684
-	}
1685
-
1686
-	#[Override]
1687
-	public function shareApiLinkAllowPublicUpload(): bool {
1688
-		return $this->config->getAppValue('core', 'shareapi_allow_public_upload', 'yes') === 'yes';
1689
-	}
1690
-
1691
-	#[Override]
1692
-	public function shareWithGroupMembersOnly(): bool {
1693
-		return $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
1694
-	}
1695
-
1696
-	#[Override]
1697
-	public function shareWithGroupMembersOnlyExcludeGroupsList(): array {
1698
-		if (!$this->shareWithGroupMembersOnly()) {
1699
-			return [];
1700
-		}
1701
-		$excludeGroups = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', '');
1702
-		return json_decode($excludeGroups, true) ?? [];
1703
-	}
1704
-
1705
-	#[Override]
1706
-	public function allowGroupSharing(): bool {
1707
-		return $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'yes';
1708
-	}
1709
-
1710
-	#[Override]
1711
-	public function allowEnumeration(): bool {
1712
-		return $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
1713
-	}
1714
-
1715
-	#[Override]
1716
-	public function limitEnumerationToGroups(): bool {
1717
-		return $this->allowEnumeration()
1718
-			&& $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
1719
-	}
1720
-
1721
-	#[Override]
1722
-	public function limitEnumerationToPhone(): bool {
1723
-		return $this->allowEnumeration()
1724
-			&& $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
1725
-	}
1726
-
1727
-	#[Override]
1728
-	public function allowEnumerationFullMatch(): bool {
1729
-		return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
1730
-	}
1731
-
1732
-	#[Override]
1733
-	public function matchEmail(): bool {
1734
-		return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes';
1735
-	}
1736
-
1737
-	#[Override]
1738
-	public function matchUserId(): bool {
1739
-		return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_user_id', 'yes') === 'yes';
1740
-	}
1741
-
1742
-	public function matchDisplayName(): bool {
1743
-		return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_displayname', 'yes') === 'yes';
1744
-	}
1745
-
1746
-	#[Override]
1747
-	public function ignoreSecondDisplayName(): bool {
1748
-		return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn', 'no') === 'yes';
1749
-	}
1750
-
1751
-	#[Override]
1752
-	public function allowCustomTokens(): bool {
1753
-		return $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_CUSTOM_TOKEN);
1754
-	}
1755
-
1756
-	#[Override]
1757
-	public function allowViewWithoutDownload(): bool {
1758
-		return $this->appConfig->getValueBool('core', 'shareapi_allow_view_without_download', true);
1759
-	}
1760
-
1761
-	#[Override]
1762
-	public function currentUserCanEnumerateTargetUser(?IUser $currentUser, IUser $targetUser): bool {
1763
-		if ($this->allowEnumerationFullMatch()) {
1764
-			return true;
1765
-		}
1766
-
1767
-		if (!$this->allowEnumeration()) {
1768
-			return false;
1769
-		}
1770
-
1771
-		if (!$this->limitEnumerationToPhone() && !$this->limitEnumerationToGroups()) {
1772
-			// Enumeration is enabled and not restricted: OK
1773
-			return true;
1774
-		}
1775
-
1776
-		if (!$currentUser instanceof IUser) {
1777
-			// Enumeration restrictions require an account
1778
-			return false;
1779
-		}
1780
-
1781
-		// Enumeration is limited to phone match
1782
-		if ($this->limitEnumerationToPhone() && $this->knownUserService->isKnownToUser($currentUser->getUID(), $targetUser->getUID())) {
1783
-			return true;
1784
-		}
1785
-
1786
-		// Enumeration is limited to groups
1787
-		if ($this->limitEnumerationToGroups()) {
1788
-			$currentUserGroupIds = $this->groupManager->getUserGroupIds($currentUser);
1789
-			$targetUserGroupIds = $this->groupManager->getUserGroupIds($targetUser);
1790
-			if (!empty(array_intersect($currentUserGroupIds, $targetUserGroupIds))) {
1791
-				return true;
1792
-			}
1793
-		}
1794
-
1795
-		return false;
1796
-	}
1797
-
1798
-	#[Override]
1799
-	public function sharingDisabledForUser(?string $userId): bool {
1800
-		return $this->shareDisableChecker->sharingDisabledForUser($userId);
1801
-	}
1802
-
1803
-	#[Override]
1804
-	public function outgoingServer2ServerSharesAllowed(): bool {
1805
-		return $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'yes';
1806
-	}
1807
-
1808
-	#[Override]
1809
-	public function outgoingServer2ServerGroupSharesAllowed(): bool {
1810
-		return $this->config->getAppValue('files_sharing', 'outgoing_server2server_group_share_enabled', 'no') === 'yes';
1811
-	}
1812
-
1813
-	#[Override]
1814
-	public function shareProviderExists(int $shareType): bool {
1815
-		try {
1816
-			$this->factory->getProviderForType($shareType);
1817
-		} catch (ProviderException $e) {
1818
-			return false;
1819
-		}
1820
-
1821
-		return true;
1822
-	}
1823
-
1824
-	public function registerShareProvider(string $shareProviderClass): void {
1825
-		$this->factory->registerProvider($shareProviderClass);
1826
-	}
1827
-
1828
-	#[Override]
1829
-	public function getAllShares(): iterable {
1830
-		$providers = $this->factory->getAllProviders();
1831
-
1832
-		foreach ($providers as $provider) {
1833
-			yield from $provider->getAllShares();
1834
-		}
1835
-	}
1836
-
1837
-	#[Override]
1838
-	public function generateToken(): string {
1839
-		// Initial token length
1840
-		$tokenLength = Helper::getTokenLength();
1841
-
1842
-		do {
1843
-			$tokenExists = false;
1844
-
1845
-			for ($i = 0; $i <= 2; $i++) {
1846
-				// Generate a new token
1847
-				$token = $this->secureRandom->generate(
1848
-					$tokenLength,
1849
-					ISecureRandom::CHAR_HUMAN_READABLE,
1850
-				);
1851
-
1852
-				try {
1853
-					// Try to fetch a share with the generated token
1854
-					$this->getShareByToken($token);
1855
-					$tokenExists = true; // Token exists, we need to try again
1856
-				} catch (ShareNotFound $e) {
1857
-					// Token is unique, exit the loop
1858
-					$tokenExists = false;
1859
-					break;
1860
-				}
1861
-			}
1862
-
1863
-			// If we've reached the maximum attempts and the token still exists, increase the token length
1864
-			if ($tokenExists) {
1865
-				$tokenLength++;
1866
-
1867
-				// Check if the token length exceeds the maximum allowed length
1868
-				if ($tokenLength > \OC\Share\Constants::MAX_TOKEN_LENGTH) {
1869
-					throw new ShareTokenException('Unable to generate a unique share token. Maximum token length exceeded.');
1870
-				}
1871
-			}
1872
-		} while ($tokenExists);
1873
-
1874
-		return $token;
1875
-	}
1876
-
1877
-	private function dispatchEvent(Event $event, string $name): void {
1878
-		try {
1879
-			$this->dispatcher->dispatchTyped($event);
1880
-		} catch (\Exception $e) {
1881
-			$this->logger->error("Error while sending ' . $name . ' event", ['exception' => $e]);
1882
-		}
1883
-	}
1377
+        if (($share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL)
1378
+            && $share->getNodeType() === 'folder' && !$this->shareApiLinkAllowPublicUpload()) {
1379
+            $share->setPermissions($share->getPermissions() & ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE));
1380
+        }
1381
+
1382
+        return $share;
1383
+    }
1384
+
1385
+    /**
1386
+     * Check expire date and disabled owner
1387
+     *
1388
+     * @param int &$added If given, will be decremented if the share is deleted
1389
+     * @throws ShareNotFound
1390
+     */
1391
+    private function checkShare(IShare $share, int &$added = 1): void {
1392
+        if ($share->isExpired()) {
1393
+            $this->deleteShare($share);
1394
+            // Remove 1 to added, because this share was deleted
1395
+            $added--;
1396
+            throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
1397
+        }
1398
+        if ($this->config->getAppValue('files_sharing', 'hide_disabled_user_shares', 'no') === 'yes') {
1399
+            $uids = array_unique([$share->getShareOwner(),$share->getSharedBy()]);
1400
+            foreach ($uids as $uid) {
1401
+                $user = $this->userManager->get($uid);
1402
+                if ($user?->isEnabled() === false) {
1403
+                    throw new ShareNotFound($this->l->t('The requested share comes from a disabled user'));
1404
+                }
1405
+            }
1406
+        }
1407
+
1408
+        // For link and email shares, verify the share owner can still create such shares
1409
+        if ($share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL) {
1410
+            $shareOwner = $this->userManager->get($share->getShareOwner());
1411
+            if ($shareOwner === null) {
1412
+                throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
1413
+            }
1414
+            if (!$this->userCanCreateLinkShares($shareOwner)) {
1415
+                throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
1416
+            }
1417
+        }
1418
+    }
1419
+
1420
+    #[Override]
1421
+    public function checkPassword(IShare $share, ?string $password): bool {
1422
+
1423
+        // if there is no password on the share object / passsword is null, there is nothing to check
1424
+        if ($password === null || $share->getPassword() === null) {
1425
+            return false;
1426
+        }
1427
+
1428
+        // Makes sure password hasn't expired
1429
+        $expirationTime = $share->getPasswordExpirationTime();
1430
+        if ($expirationTime !== null && $expirationTime < new \DateTime()) {
1431
+            return false;
1432
+        }
1433
+
1434
+        $newHash = '';
1435
+        if (!$this->hasher->verify($password, $share->getPassword(), $newHash)) {
1436
+            return false;
1437
+        }
1438
+
1439
+        if (!empty($newHash)) {
1440
+            $share->setPassword($newHash);
1441
+            $provider = $this->factory->getProviderForType($share->getShareType());
1442
+            $provider->update($share);
1443
+        }
1444
+
1445
+        return true;
1446
+    }
1447
+
1448
+    #[Override]
1449
+    public function userDeleted(string $uid): void {
1450
+        $types = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK, IShare::TYPE_REMOTE, IShare::TYPE_EMAIL];
1451
+
1452
+        foreach ($types as $type) {
1453
+            try {
1454
+                $provider = $this->factory->getProviderForType($type);
1455
+            } catch (ProviderException $e) {
1456
+                continue;
1457
+            }
1458
+            $provider->userDeleted($uid, $type);
1459
+        }
1460
+    }
1461
+
1462
+    #[Override]
1463
+    public function groupDeleted(string $gid): void {
1464
+        foreach ([IShare::TYPE_GROUP, IShare::TYPE_REMOTE_GROUP] as $type) {
1465
+            try {
1466
+                $provider = $this->factory->getProviderForType($type);
1467
+            } catch (ProviderException $e) {
1468
+                continue;
1469
+            }
1470
+            $provider->groupDeleted($gid);
1471
+        }
1472
+
1473
+        $excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', '');
1474
+        if ($excludedGroups === '') {
1475
+            return;
1476
+        }
1477
+
1478
+        $excludedGroups = json_decode($excludedGroups, true);
1479
+        if (json_last_error() !== JSON_ERROR_NONE) {
1480
+            return;
1481
+        }
1482
+
1483
+        $excludedGroups = array_diff($excludedGroups, [$gid]);
1484
+        $this->config->setAppValue('core', 'shareapi_exclude_groups_list', json_encode($excludedGroups));
1485
+    }
1486
+
1487
+    #[Override]
1488
+    public function userDeletedFromGroup(string $uid, string $gid): void {
1489
+        foreach ([IShare::TYPE_GROUP, IShare::TYPE_REMOTE_GROUP] as $type) {
1490
+            try {
1491
+                $provider = $this->factory->getProviderForType($type);
1492
+            } catch (ProviderException $e) {
1493
+                continue;
1494
+            }
1495
+            $provider->userDeletedFromGroup($uid, $gid);
1496
+        }
1497
+    }
1498
+
1499
+    #[\Override]
1500
+    public function getAccessList(\OCP\Files\Node $path, $recursive = true, $currentAccess = false): array {
1501
+        $owner = $path->getOwner();
1502
+
1503
+        if ($owner === null) {
1504
+            return [];
1505
+        }
1506
+
1507
+        $owner = $owner->getUID();
1508
+
1509
+        if ($currentAccess) {
1510
+            $al = ['users' => [], 'remote' => [], 'public' => false, 'mail' => []];
1511
+        } else {
1512
+            $al = ['users' => [], 'remote' => false, 'public' => false, 'mail' => []];
1513
+        }
1514
+        if (!$this->userManager->userExists($owner)) {
1515
+            return $al;
1516
+        }
1517
+
1518
+        //Get node for the owner and correct the owner in case of external storage
1519
+        $userFolder = $this->rootFolder->getUserFolder($owner);
1520
+        if ($path->getId() !== $userFolder->getId() && !$userFolder->isSubNode($path)) {
1521
+            $path = $userFolder->getFirstNodeById($path->getId());
1522
+            if ($path === null || $path->getOwner() === null) {
1523
+                return [];
1524
+            }
1525
+            $owner = $path->getOwner()->getUID();
1526
+        }
1527
+
1528
+        $providers = $this->factory->getAllProviders();
1529
+
1530
+        /** @var Node[] $nodes */
1531
+        $nodes = [];
1532
+
1533
+
1534
+        if ($currentAccess) {
1535
+            $ownerPath = $path->getPath();
1536
+            $ownerPath = explode('/', $ownerPath, 4);
1537
+            if (count($ownerPath) < 4) {
1538
+                $ownerPath = '';
1539
+            } else {
1540
+                $ownerPath = $ownerPath[3];
1541
+            }
1542
+            $al['users'][$owner] = [
1543
+                'node_id' => $path->getId(),
1544
+                'node_path' => '/' . $ownerPath,
1545
+            ];
1546
+        } else {
1547
+            $al['users'][] = $owner;
1548
+        }
1549
+
1550
+        // Collect all the shares
1551
+        while ($path->getPath() !== $userFolder->getPath()) {
1552
+            $nodes[] = $path;
1553
+            if (!$recursive) {
1554
+                break;
1555
+            }
1556
+            $path = $path->getParent();
1557
+        }
1558
+
1559
+        foreach ($providers as $provider) {
1560
+            $tmp = $provider->getAccessList($nodes, $currentAccess);
1561
+
1562
+            foreach ($tmp as $k => $v) {
1563
+                if (isset($al[$k])) {
1564
+                    if (is_array($al[$k])) {
1565
+                        if ($currentAccess) {
1566
+                            $al[$k] += $v;
1567
+                        } else {
1568
+                            $al[$k] = array_merge($al[$k], $v);
1569
+                            $al[$k] = array_unique($al[$k]);
1570
+                            $al[$k] = array_values($al[$k]);
1571
+                        }
1572
+                    } else {
1573
+                        $al[$k] = $al[$k] || $v;
1574
+                    }
1575
+                } else {
1576
+                    $al[$k] = $v;
1577
+                }
1578
+            }
1579
+        }
1580
+
1581
+        return $al;
1582
+    }
1583
+
1584
+    #[Override]
1585
+    public function newShare(): IShare {
1586
+        return new \OC\Share20\Share($this->rootFolder, $this->userManager);
1587
+    }
1588
+
1589
+    #[Override]
1590
+    public function shareApiEnabled(): bool {
1591
+        return $this->config->getAppValue('core', 'shareapi_enabled', 'yes') === 'yes';
1592
+    }
1593
+
1594
+    #[Override]
1595
+    public function shareApiAllowLinks(?IUser $user = null): bool {
1596
+        if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') {
1597
+            return false;
1598
+        }
1599
+
1600
+        $user = $user ?? $this->userSession->getUser();
1601
+        if ($user) {
1602
+            $excludedGroups = json_decode($this->config->getAppValue('core', 'shareapi_allow_links_exclude_groups', '[]'));
1603
+            if ($excludedGroups) {
1604
+                $userGroups = $this->groupManager->getUserGroupIds($user);
1605
+                return !(bool)array_intersect($excludedGroups, $userGroups);
1606
+            }
1607
+        }
1608
+
1609
+        return true;
1610
+    }
1611
+
1612
+    /**
1613
+     * Check if a specific user can create link shares
1614
+     *
1615
+     * @param IUser $user The user to check
1616
+     * @return bool
1617
+     */
1618
+    protected function userCanCreateLinkShares(IUser $user): bool {
1619
+        return $this->shareApiAllowLinks($user);
1620
+    }
1621
+
1622
+    #[Override]
1623
+    public function shareApiLinkEnforcePassword(bool $checkGroupMembership = true): bool {
1624
+        $excludedGroups = $this->config->getAppValue('core', 'shareapi_enforce_links_password_excluded_groups', '');
1625
+        if ($excludedGroups !== '' && $checkGroupMembership) {
1626
+            $excludedGroups = json_decode($excludedGroups);
1627
+            $user = $this->userSession->getUser();
1628
+            if ($user) {
1629
+                $userGroups = $this->groupManager->getUserGroupIds($user);
1630
+                if ((bool)array_intersect($excludedGroups, $userGroups)) {
1631
+                    return false;
1632
+                }
1633
+            }
1634
+        }
1635
+        return $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_PASSWORD_ENFORCED);
1636
+    }
1637
+
1638
+    #[Override]
1639
+    public function shareApiLinkDefaultExpireDate(): bool {
1640
+        return $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_EXPIRE_DATE_DEFAULT);
1641
+    }
1642
+
1643
+    #[Override]
1644
+    public function shareApiLinkDefaultExpireDateEnforced(): bool {
1645
+        return $this->shareApiLinkDefaultExpireDate()
1646
+            && $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_EXPIRE_DATE_ENFORCED);
1647
+    }
1648
+
1649
+    #[Override]
1650
+    public function shareApiLinkDefaultExpireDays(): int {
1651
+        return (int)$this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7');
1652
+    }
1653
+
1654
+    #[Override]
1655
+    public function shareApiInternalDefaultExpireDate(): bool {
1656
+        return $this->config->getAppValue('core', 'shareapi_default_internal_expire_date', 'no') === 'yes';
1657
+    }
1658
+
1659
+    #[Override]
1660
+    public function shareApiRemoteDefaultExpireDate(): bool {
1661
+        return $this->config->getAppValue('core', 'shareapi_default_remote_expire_date', 'no') === 'yes';
1662
+    }
1663
+
1664
+    #[Override]
1665
+    public function shareApiInternalDefaultExpireDateEnforced(): bool {
1666
+        return $this->shareApiInternalDefaultExpireDate()
1667
+            && $this->config->getAppValue('core', 'shareapi_enforce_internal_expire_date', 'no') === 'yes';
1668
+    }
1669
+
1670
+    #[Override]
1671
+    public function shareApiRemoteDefaultExpireDateEnforced(): bool {
1672
+        return $this->shareApiRemoteDefaultExpireDate()
1673
+            && $this->config->getAppValue('core', 'shareapi_enforce_remote_expire_date', 'no') === 'yes';
1674
+    }
1675
+
1676
+    #[Override]
1677
+    public function shareApiInternalDefaultExpireDays(): int {
1678
+        return (int)$this->config->getAppValue('core', 'shareapi_internal_expire_after_n_days', '7');
1679
+    }
1680
+
1681
+    #[Override]
1682
+    public function shareApiRemoteDefaultExpireDays(): int {
1683
+        return (int)$this->config->getAppValue('core', 'shareapi_remote_expire_after_n_days', '7');
1684
+    }
1685
+
1686
+    #[Override]
1687
+    public function shareApiLinkAllowPublicUpload(): bool {
1688
+        return $this->config->getAppValue('core', 'shareapi_allow_public_upload', 'yes') === 'yes';
1689
+    }
1690
+
1691
+    #[Override]
1692
+    public function shareWithGroupMembersOnly(): bool {
1693
+        return $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
1694
+    }
1695
+
1696
+    #[Override]
1697
+    public function shareWithGroupMembersOnlyExcludeGroupsList(): array {
1698
+        if (!$this->shareWithGroupMembersOnly()) {
1699
+            return [];
1700
+        }
1701
+        $excludeGroups = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', '');
1702
+        return json_decode($excludeGroups, true) ?? [];
1703
+    }
1704
+
1705
+    #[Override]
1706
+    public function allowGroupSharing(): bool {
1707
+        return $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'yes';
1708
+    }
1709
+
1710
+    #[Override]
1711
+    public function allowEnumeration(): bool {
1712
+        return $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
1713
+    }
1714
+
1715
+    #[Override]
1716
+    public function limitEnumerationToGroups(): bool {
1717
+        return $this->allowEnumeration()
1718
+            && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
1719
+    }
1720
+
1721
+    #[Override]
1722
+    public function limitEnumerationToPhone(): bool {
1723
+        return $this->allowEnumeration()
1724
+            && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
1725
+    }
1726
+
1727
+    #[Override]
1728
+    public function allowEnumerationFullMatch(): bool {
1729
+        return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
1730
+    }
1731
+
1732
+    #[Override]
1733
+    public function matchEmail(): bool {
1734
+        return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes';
1735
+    }
1736
+
1737
+    #[Override]
1738
+    public function matchUserId(): bool {
1739
+        return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_user_id', 'yes') === 'yes';
1740
+    }
1741
+
1742
+    public function matchDisplayName(): bool {
1743
+        return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_displayname', 'yes') === 'yes';
1744
+    }
1745
+
1746
+    #[Override]
1747
+    public function ignoreSecondDisplayName(): bool {
1748
+        return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn', 'no') === 'yes';
1749
+    }
1750
+
1751
+    #[Override]
1752
+    public function allowCustomTokens(): bool {
1753
+        return $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_CUSTOM_TOKEN);
1754
+    }
1755
+
1756
+    #[Override]
1757
+    public function allowViewWithoutDownload(): bool {
1758
+        return $this->appConfig->getValueBool('core', 'shareapi_allow_view_without_download', true);
1759
+    }
1760
+
1761
+    #[Override]
1762
+    public function currentUserCanEnumerateTargetUser(?IUser $currentUser, IUser $targetUser): bool {
1763
+        if ($this->allowEnumerationFullMatch()) {
1764
+            return true;
1765
+        }
1766
+
1767
+        if (!$this->allowEnumeration()) {
1768
+            return false;
1769
+        }
1770
+
1771
+        if (!$this->limitEnumerationToPhone() && !$this->limitEnumerationToGroups()) {
1772
+            // Enumeration is enabled and not restricted: OK
1773
+            return true;
1774
+        }
1775
+
1776
+        if (!$currentUser instanceof IUser) {
1777
+            // Enumeration restrictions require an account
1778
+            return false;
1779
+        }
1780
+
1781
+        // Enumeration is limited to phone match
1782
+        if ($this->limitEnumerationToPhone() && $this->knownUserService->isKnownToUser($currentUser->getUID(), $targetUser->getUID())) {
1783
+            return true;
1784
+        }
1785
+
1786
+        // Enumeration is limited to groups
1787
+        if ($this->limitEnumerationToGroups()) {
1788
+            $currentUserGroupIds = $this->groupManager->getUserGroupIds($currentUser);
1789
+            $targetUserGroupIds = $this->groupManager->getUserGroupIds($targetUser);
1790
+            if (!empty(array_intersect($currentUserGroupIds, $targetUserGroupIds))) {
1791
+                return true;
1792
+            }
1793
+        }
1794
+
1795
+        return false;
1796
+    }
1797
+
1798
+    #[Override]
1799
+    public function sharingDisabledForUser(?string $userId): bool {
1800
+        return $this->shareDisableChecker->sharingDisabledForUser($userId);
1801
+    }
1802
+
1803
+    #[Override]
1804
+    public function outgoingServer2ServerSharesAllowed(): bool {
1805
+        return $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'yes';
1806
+    }
1807
+
1808
+    #[Override]
1809
+    public function outgoingServer2ServerGroupSharesAllowed(): bool {
1810
+        return $this->config->getAppValue('files_sharing', 'outgoing_server2server_group_share_enabled', 'no') === 'yes';
1811
+    }
1812
+
1813
+    #[Override]
1814
+    public function shareProviderExists(int $shareType): bool {
1815
+        try {
1816
+            $this->factory->getProviderForType($shareType);
1817
+        } catch (ProviderException $e) {
1818
+            return false;
1819
+        }
1820
+
1821
+        return true;
1822
+    }
1823
+
1824
+    public function registerShareProvider(string $shareProviderClass): void {
1825
+        $this->factory->registerProvider($shareProviderClass);
1826
+    }
1827
+
1828
+    #[Override]
1829
+    public function getAllShares(): iterable {
1830
+        $providers = $this->factory->getAllProviders();
1831
+
1832
+        foreach ($providers as $provider) {
1833
+            yield from $provider->getAllShares();
1834
+        }
1835
+    }
1836
+
1837
+    #[Override]
1838
+    public function generateToken(): string {
1839
+        // Initial token length
1840
+        $tokenLength = Helper::getTokenLength();
1841
+
1842
+        do {
1843
+            $tokenExists = false;
1844
+
1845
+            for ($i = 0; $i <= 2; $i++) {
1846
+                // Generate a new token
1847
+                $token = $this->secureRandom->generate(
1848
+                    $tokenLength,
1849
+                    ISecureRandom::CHAR_HUMAN_READABLE,
1850
+                );
1851
+
1852
+                try {
1853
+                    // Try to fetch a share with the generated token
1854
+                    $this->getShareByToken($token);
1855
+                    $tokenExists = true; // Token exists, we need to try again
1856
+                } catch (ShareNotFound $e) {
1857
+                    // Token is unique, exit the loop
1858
+                    $tokenExists = false;
1859
+                    break;
1860
+                }
1861
+            }
1862
+
1863
+            // If we've reached the maximum attempts and the token still exists, increase the token length
1864
+            if ($tokenExists) {
1865
+                $tokenLength++;
1866
+
1867
+                // Check if the token length exceeds the maximum allowed length
1868
+                if ($tokenLength > \OC\Share\Constants::MAX_TOKEN_LENGTH) {
1869
+                    throw new ShareTokenException('Unable to generate a unique share token. Maximum token length exceeded.');
1870
+                }
1871
+            }
1872
+        } while ($tokenExists);
1873
+
1874
+        return $token;
1875
+    }
1876
+
1877
+    private function dispatchEvent(Event $event, string $name): void {
1878
+        try {
1879
+            $this->dispatcher->dispatchTyped($event);
1880
+        } catch (\Exception $e) {
1881
+            $this->logger->error("Error while sending ' . $name . ' event", ['exception' => $e]);
1882
+        }
1883
+    }
1884 1884
 }
Please login to merge, or discard this patch.
apps/files/lib/Service/OwnershipTransferService.php 1 patch
Indentation   +585 added lines, -585 removed lines patch added patch discarded remove patch
@@ -44,589 +44,589 @@
 block discarded – undo
44 44
 
45 45
 class OwnershipTransferService {
46 46
 
47
-	public function __construct(
48
-		private IEncryptionManager $encryptionManager,
49
-		private IShareManager $shareManager,
50
-		private IMountManager $mountManager,
51
-		private IUserMountCache $userMountCache,
52
-		private IUserManager $userManager,
53
-		private IFactory $l10nFactory,
54
-		private IRootFolder $rootFolder,
55
-	) {
56
-	}
57
-
58
-	/**
59
-	 * @param IUser $sourceUser
60
-	 * @param IUser $destinationUser
61
-	 * @param string $path
62
-	 *
63
-	 * @param OutputInterface|null $output
64
-	 * @param bool $move
65
-	 * @throws TransferOwnershipException
66
-	 * @throws NoUserException
67
-	 */
68
-	public function transfer(
69
-		IUser $sourceUser,
70
-		IUser $destinationUser,
71
-		string $path,
72
-		?OutputInterface $output = null,
73
-		bool $move = false,
74
-		bool $firstLogin = false,
75
-		bool $includeExternalStorage = false,
76
-		bool $useUserId = false,
77
-	): void {
78
-		$output = $output ?? new NullOutput();
79
-		$sourceUid = $sourceUser->getUID();
80
-		$destinationUid = $destinationUser->getUID();
81
-		$sourcePath = rtrim($sourceUid . '/files/' . $path, '/');
82
-
83
-		// If encryption is on we have to ensure the user has logged in before and that all encryption modules are ready
84
-		if (($this->encryptionManager->isEnabled() && $destinationUser->getLastLogin() === 0)
85
-			|| !$this->encryptionManager->isReadyForUser($destinationUid)) {
86
-			throw new TransferOwnershipException('The target user is not ready to accept files. The user has at least to have logged in once.', 2);
87
-		}
88
-
89
-		// setup filesystem
90
-		// Requesting the user folder will set it up if the user hasn't logged in before
91
-		// We need a setupFS for the full filesystem setup before as otherwise we will just return
92
-		// a lazy root folder which does not create the destination users folder
93
-		\OC_Util::setupFS($sourceUser->getUID());
94
-		\OC_Util::setupFS($destinationUser->getUID());
95
-		$this->rootFolder->getUserFolder($sourceUser->getUID());
96
-		$this->rootFolder->getUserFolder($destinationUser->getUID());
97
-		Filesystem::initMountPoints($sourceUid);
98
-		Filesystem::initMountPoints($destinationUid);
99
-
100
-		$view = new View();
101
-
102
-		if ($move) {
103
-			$finalTarget = "$destinationUid/files/";
104
-		} else {
105
-			$l = $this->l10nFactory->get('files', $this->l10nFactory->getUserLanguage($destinationUser));
106
-			$date = date('Y-m-d H-i-s');
107
-
108
-			if ($useUserId) {
109
-				$cleanUserName = $sourceUid;
110
-			} else {
111
-				$cleanUserName = $this->sanitizeFolderName($sourceUser->getDisplayName());
112
-				if ($cleanUserName === '') {
113
-					$cleanUserName = $sourceUid;
114
-				}
115
-			}
116
-
117
-			$finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$cleanUserName, $date]));
118
-			try {
119
-				$view->verifyPath(dirname($finalTarget), basename($finalTarget));
120
-			} catch (InvalidPathException $e) {
121
-				$finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$sourceUid, $date]));
122
-			}
123
-		}
124
-
125
-		if (!($view->is_dir($sourcePath) || $view->is_file($sourcePath))) {
126
-			throw new TransferOwnershipException("Unknown path provided: $path", 1);
127
-		}
128
-
129
-		if ($move && !$view->is_dir($finalTarget)) {
130
-			// Initialize storage
131
-			\OC_Util::setupFS($destinationUser->getUID());
132
-		}
133
-
134
-		if ($move && !$firstLogin && count($view->getDirectoryContent($finalTarget)) > 0) {
135
-			throw new TransferOwnershipException('Destination path does not exists or is not empty', 1);
136
-		}
137
-
138
-
139
-		// analyse source folder
140
-		$this->analyse(
141
-			$sourceUid,
142
-			$destinationUid,
143
-			$sourcePath,
144
-			$view,
145
-			$output
146
-		);
147
-
148
-		// collect all the shares
149
-		$shares = $this->collectUsersShares(
150
-			$sourceUid,
151
-			$output,
152
-			$view,
153
-			$sourcePath
154
-		);
155
-
156
-		$sourceSize = $view->getFileInfo($sourcePath)->getSize();
157
-
158
-		// transfer the files
159
-		$this->transferFiles(
160
-			$sourceUid,
161
-			$sourcePath,
162
-			$finalTarget,
163
-			$view,
164
-			$output,
165
-			$includeExternalStorage,
166
-		);
167
-		$sizeDifference = $sourceSize - $view->getFileInfo($finalTarget)->getSize();
168
-
169
-		// transfer the incoming shares
170
-		$sourceShares = $this->collectIncomingShares(
171
-			$sourceUid,
172
-			$output,
173
-			$sourcePath,
174
-		);
175
-		$destinationShares = $this->collectIncomingShares(
176
-			$destinationUid,
177
-			$output,
178
-			null,
179
-		);
180
-		$this->transferIncomingShares(
181
-			$sourceUid,
182
-			$destinationUid,
183
-			$sourceShares,
184
-			$destinationShares,
185
-			$output,
186
-			$path,
187
-			$finalTarget,
188
-			$move
189
-		);
190
-
191
-		$destinationPath = $finalTarget . '/' . $path;
192
-		// restore the shares
193
-		$this->restoreShares(
194
-			$sourceUid,
195
-			$destinationUid,
196
-			$destinationPath,
197
-			$shares,
198
-			$output
199
-		);
200
-		if ($sizeDifference !== 0) {
201
-			$output->writeln("Transferred folder have a size difference of: $sizeDifference Bytes which means the transfer may be incomplete. Please check the logs if there was any issue during the transfer operation.");
202
-		}
203
-	}
204
-
205
-	private function sanitizeFolderName(string $name): string {
206
-		// Remove some characters which are prone to cause errors
207
-		$name = str_replace(['\\', '/', ':', '.', '?', '#', '\'', '"'], '-', $name);
208
-		// Replace multiple dashes with one dash
209
-		return preg_replace('/-{2,}/s', '-', $name);
210
-	}
211
-
212
-	private function walkFiles(View $view, $path, Closure $callBack) {
213
-		foreach ($view->getDirectoryContent($path) as $fileInfo) {
214
-			if (!$callBack($fileInfo)) {
215
-				return;
216
-			}
217
-			if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
218
-				$this->walkFiles($view, $fileInfo->getPath(), $callBack);
219
-			}
220
-		}
221
-	}
222
-
223
-	/**
224
-	 * @param OutputInterface $output
225
-	 *
226
-	 * @throws TransferOwnershipException
227
-	 */
228
-	protected function analyse(
229
-		string $sourceUid,
230
-		string $destinationUid,
231
-		string $sourcePath,
232
-		View $view,
233
-		OutputInterface $output,
234
-		bool $includeExternalStorage = false,
235
-	): void {
236
-		$output->writeln('Validating quota');
237
-		$sourceFileInfo = $view->getFileInfo($sourcePath, false);
238
-		if ($sourceFileInfo === false) {
239
-			throw new TransferOwnershipException("Unknown path provided: $sourcePath", 1);
240
-		}
241
-		$size = $sourceFileInfo->getSize(false);
242
-		$freeSpace = $view->free_space($destinationUid . '/files/');
243
-		if ($size > $freeSpace && $freeSpace !== FileInfo::SPACE_UNKNOWN) {
244
-			throw new TransferOwnershipException('Target user does not have enough free space available.', 1);
245
-		}
246
-
247
-		$output->writeln("Analysing files of $sourceUid ...");
248
-		$progress = new ProgressBar($output);
249
-		$progress->start();
250
-
251
-		if ($this->encryptionManager->isEnabled()) {
252
-			$masterKeyEnabled = Server::get(Util::class)->isMasterKeyEnabled();
253
-		} else {
254
-			$masterKeyEnabled = false;
255
-		}
256
-		$encryptedFiles = [];
257
-		if ($sourceFileInfo->getType() === FileInfo::TYPE_FOLDER) {
258
-			if ($sourceFileInfo->isEncrypted()) {
259
-				/* Encrypted folder means e2ee encrypted */
260
-				$encryptedFiles[] = $sourceFileInfo;
261
-			} else {
262
-				$this->walkFiles($view, $sourcePath,
263
-					function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles, $includeExternalStorage) {
264
-						if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
265
-							$mount = $fileInfo->getMountPoint();
266
-							// only analyze into folders from main storage,
267
-							if (
268
-								$mount->getMountProvider() instanceof IHomeMountProvider
269
-								|| ($includeExternalStorage && $mount->getMountProvider() instanceof ConfigAdapter)
270
-							) {
271
-								if ($fileInfo->isEncrypted()) {
272
-									/* Encrypted folder means e2ee encrypted, we cannot transfer it */
273
-									$encryptedFiles[] = $fileInfo;
274
-								}
275
-								return true;
276
-							} else {
277
-								return false;
278
-							}
279
-						}
280
-						$progress->advance();
281
-						if ($fileInfo->isEncrypted() && !$masterKeyEnabled) {
282
-							/* Encrypted file means SSE, we can only transfer it if master key is enabled */
283
-							$encryptedFiles[] = $fileInfo;
284
-						}
285
-						return true;
286
-					});
287
-			}
288
-		} elseif ($sourceFileInfo->isEncrypted() && !$masterKeyEnabled) {
289
-			/* Encrypted file means SSE, we can only transfer it if master key is enabled */
290
-			$encryptedFiles[] = $sourceFileInfo;
291
-		}
292
-		$progress->finish();
293
-		$output->writeln('');
294
-
295
-		// no file is allowed to be encrypted
296
-		if (!empty($encryptedFiles)) {
297
-			$output->writeln('<error>Some files are encrypted - please decrypt them first.</error>');
298
-			foreach ($encryptedFiles as $encryptedFile) {
299
-				/** @var FileInfo $encryptedFile */
300
-				$output->writeln('  ' . $encryptedFile->getPath());
301
-			}
302
-			throw new TransferOwnershipException('Some files are encrypted - please decrypt them first.', 1);
303
-		}
304
-	}
305
-
306
-	/**
307
-	 * @return array<array{share: IShare, suffix: string}>
308
-	 */
309
-	private function collectUsersShares(
310
-		string $sourceUid,
311
-		OutputInterface $output,
312
-		View $view,
313
-		string $path,
314
-	): array {
315
-		$output->writeln("Collecting all share information for files and folders of $sourceUid ...");
316
-
317
-		$shares = [];
318
-		$progress = new ProgressBar($output);
319
-
320
-		$normalizedPath = Filesystem::normalizePath($path);
321
-
322
-		$supportedShareTypes = [
323
-			IShare::TYPE_GROUP,
324
-			IShare::TYPE_USER,
325
-			IShare::TYPE_LINK,
326
-			IShare::TYPE_REMOTE,
327
-			IShare::TYPE_ROOM,
328
-			IShare::TYPE_EMAIL,
329
-			IShare::TYPE_CIRCLE,
330
-			IShare::TYPE_DECK,
331
-		];
332
-
333
-		foreach ($supportedShareTypes as $shareType) {
334
-			$offset = 0;
335
-			while (true) {
336
-				$sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset, onlyValid: false);
337
-				$progress->advance(count($sharePage));
338
-				if (empty($sharePage)) {
339
-					break;
340
-				}
341
-				if ($path !== "$sourceUid/files") {
342
-					$sharePage = array_filter($sharePage, function (IShare $share) use ($view, $normalizedPath) {
343
-						try {
344
-							$sourceNode = $share->getNode();
345
-							$relativePath = $view->getRelativePath($sourceNode->getPath());
346
-
347
-							return str_starts_with($relativePath . '/', $normalizedPath . '/');
348
-						} catch (Exception $e) {
349
-							return false;
350
-						}
351
-					});
352
-				}
353
-				$shares = array_merge($shares, $sharePage);
354
-				$offset += 50;
355
-			}
356
-		}
357
-
358
-		$progress->finish();
359
-		$output->writeln('');
360
-
361
-		return array_values(array_filter(array_map(function (IShare $share) use ($view, $normalizedPath, $output, $sourceUid) {
362
-			try {
363
-				$nodePath = $view->getRelativePath($share->getNode()->getPath());
364
-			} catch (NotFoundException $e) {
365
-				$output->writeln("<error>Failed to find path for shared file {$share->getNodeId()} for user $sourceUid, skipping</error>");
366
-				return null;
367
-			}
368
-
369
-			return [
370
-				'share' => $share,
371
-				'suffix' => substr(Filesystem::normalizePath($nodePath), strlen($normalizedPath)),
372
-			];
373
-		}, $shares)));
374
-	}
375
-
376
-	private function collectIncomingShares(
377
-		string $sourceUid,
378
-		OutputInterface $output,
379
-		?string $path,
380
-	): array {
381
-		$output->writeln("Collecting all incoming share information for files and folders of $sourceUid ...");
382
-
383
-		$shares = [];
384
-		$progress = new ProgressBar($output);
385
-		$normalizedPath = Filesystem::normalizePath($path);
386
-
387
-		$offset = 0;
388
-		while (true) {
389
-			$sharePage = $this->shareManager->getSharedWith($sourceUid, IShare::TYPE_USER, null, 50, $offset);
390
-			$progress->advance(count($sharePage));
391
-			if (empty($sharePage)) {
392
-				break;
393
-			}
394
-
395
-			if ($path !== null && $path !== "$sourceUid/files") {
396
-				$sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath) {
397
-					try {
398
-						return str_starts_with(Filesystem::normalizePath($sourceUid . '/files' . $share->getTarget() . '/', false), $normalizedPath . '/');
399
-					} catch (Exception) {
400
-						return false;
401
-					}
402
-				});
403
-			}
404
-
405
-			foreach ($sharePage as $share) {
406
-				$shares[$share->getNodeId()] = $share;
407
-			}
408
-
409
-			$offset += 50;
410
-		}
411
-
412
-
413
-		$progress->finish();
414
-		$output->writeln('');
415
-		return $shares;
416
-	}
417
-
418
-	/**
419
-	 * @throws TransferOwnershipException
420
-	 */
421
-	protected function transferFiles(
422
-		string $sourceUid,
423
-		string $sourcePath,
424
-		string $finalTarget,
425
-		View $view,
426
-		OutputInterface $output,
427
-		bool $includeExternalStorage,
428
-	): void {
429
-		$output->writeln("Transferring files to $finalTarget ...");
430
-
431
-		// This change will help user to transfer the folder specified using --path option.
432
-		// Else only the content inside folder is transferred which is not correct.
433
-		if ($sourcePath !== "$sourceUid/files") {
434
-			$view->mkdir($finalTarget);
435
-			$finalTarget = $finalTarget . '/' . basename($sourcePath);
436
-		}
437
-		$sourceInfo = $view->getFileInfo($sourcePath);
438
-
439
-		/// handle the external storages mounted at the root, or the admin specifying an external storage with --path
440
-		if ($sourceInfo->getInternalPath() === '' && $includeExternalStorage) {
441
-			$this->moveMountContents($view, $sourcePath, $finalTarget);
442
-		} else {
443
-			if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) {
444
-				throw new TransferOwnershipException('Could not transfer files.', 1);
445
-			}
446
-		}
447
-
448
-		if ($includeExternalStorage) {
449
-			$nestedMounts = $this->mountManager->findIn($sourcePath);
450
-			foreach ($nestedMounts as $mount) {
451
-				if ($mount->getMountProvider() === ConfigAdapter::class) {
452
-					$relativePath = substr(trim($mount->getMountPoint(), '/'), strlen($sourcePath));
453
-					$this->moveMountContents($view, $mount->getMountPoint(), $finalTarget . $relativePath);
454
-				}
455
-			}
456
-		}
457
-
458
-		if (!is_dir("$sourceUid/files")) {
459
-			// because the files folder is moved away we need to recreate it
460
-			$view->mkdir("$sourceUid/files");
461
-		}
462
-	}
463
-
464
-	private function moveMountContents(View $rootView, string $source, string $target) {
465
-		if ($rootView->copy($source, $target)) {
466
-			// just doing `rmdir` on the mountpoint would cause it to try and unmount the storage
467
-			// we need to empty the contents instead
468
-			$content = $rootView->getDirectoryContent($source);
469
-			foreach ($content as $item) {
470
-				if ($item->getType() === FileInfo::TYPE_FOLDER) {
471
-					$rootView->rmdir($item->getPath());
472
-				} else {
473
-					$rootView->unlink($item->getPath());
474
-				}
475
-			}
476
-		} else {
477
-			throw new TransferOwnershipException("Could not transfer $source to $target");
478
-		}
479
-	}
480
-
481
-	/**
482
-	 * @param string $targetLocation New location of the transfered node
483
-	 * @param array<array{share: IShare, suffix: string}> $shares previously collected share information
484
-	 */
485
-	private function restoreShares(
486
-		string $sourceUid,
487
-		string $destinationUid,
488
-		string $targetLocation,
489
-		array $shares,
490
-		OutputInterface $output,
491
-	):void {
492
-		$output->writeln('Restoring shares ...');
493
-		$progress = new ProgressBar($output, count($shares));
494
-
495
-		foreach ($shares as ['share' => $share, 'suffix' => $suffix]) {
496
-			try {
497
-				$output->writeln('Transfering share ' . $share->getId() . ' of type ' . $share->getShareType(), OutputInterface::VERBOSITY_VERBOSE);
498
-				if ($share->getShareType() === IShare::TYPE_USER
499
-					&& $share->getSharedWith() === $destinationUid) {
500
-					// Unmount the shares before deleting, so we don't try to get the storage later on.
501
-					$shareMountPoint = $this->mountManager->find('/' . $destinationUid . '/files' . $share->getTarget());
502
-					if ($shareMountPoint) {
503
-						$this->mountManager->removeMount($shareMountPoint->getMountPoint());
504
-					}
505
-					$this->shareManager->deleteShare($share);
506
-				} else {
507
-					if ($share->getShareOwner() === $sourceUid) {
508
-						$share->setShareOwner($destinationUid);
509
-					}
510
-					if ($share->getSharedBy() === $sourceUid) {
511
-						$share->setSharedBy($destinationUid);
512
-					}
513
-
514
-					if ($share->getShareType() === IShare::TYPE_USER
515
-						&& !$this->userManager->userExists($share->getSharedWith())) {
516
-						// stray share with deleted user
517
-						$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted user "' . $share->getSharedWith() . '", deleting</error>');
518
-						$this->shareManager->deleteShare($share);
519
-						continue;
520
-					} else {
521
-						// trigger refetching of the node so that the new owner and mountpoint are taken into account
522
-						// otherwise the checks on the share update will fail due to the original node not being available in the new user scope
523
-						$this->userMountCache->clear();
524
-
525
-						try {
526
-							// Try to get the "old" id.
527
-							// Normally the ID is preserved,
528
-							// but for transferes between different storages the ID might change
529
-							$newNodeId = $share->getNode()->getId();
530
-						} catch (NotFoundException) {
531
-							// ID has changed due to transfer between different storages
532
-							// Try to get the new ID from the target path and suffix of the share
533
-							$node = $this->rootFolder->get(Filesystem::normalizePath($targetLocation . '/' . $suffix));
534
-							$newNodeId = $node->getId();
535
-							$output->writeln('Had to change node id to ' . $newNodeId, OutputInterface::VERBOSITY_VERY_VERBOSE);
536
-						}
537
-						$share->setNodeId($newNodeId);
538
-
539
-						$this->shareManager->updateShare($share, onlyValid: false);
540
-					}
541
-				}
542
-			} catch (NotFoundException $e) {
543
-				$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
544
-			} catch (\Throwable $e) {
545
-				$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getMessage() . ' : ' . $e->getTraceAsString() . '</error>');
546
-			}
547
-			$progress->advance();
548
-		}
549
-		$progress->finish();
550
-		$output->writeln('');
551
-	}
552
-
553
-	private function transferIncomingShares(string $sourceUid,
554
-		string $destinationUid,
555
-		array $sourceShares,
556
-		array $destinationShares,
557
-		OutputInterface $output,
558
-		string $path,
559
-		string $finalTarget,
560
-		bool $move): void {
561
-		$output->writeln('Restoring incoming shares ...');
562
-		$progress = new ProgressBar($output, count($sourceShares));
563
-		$prefix = "$destinationUid/files";
564
-		$finalShareTarget = '';
565
-		if (str_starts_with($finalTarget, $prefix)) {
566
-			$finalShareTarget = substr($finalTarget, strlen($prefix));
567
-		}
568
-		foreach ($sourceShares as $share) {
569
-			try {
570
-				// Only restore if share is in given path.
571
-				$pathToCheck = '/';
572
-				if (trim($path, '/') !== '') {
573
-					$pathToCheck = '/' . trim($path) . '/';
574
-				}
575
-				if (!str_starts_with($share->getTarget(), $pathToCheck)) {
576
-					continue;
577
-				}
578
-				$shareTarget = $share->getTarget();
579
-				$shareTarget = $finalShareTarget . $shareTarget;
580
-				if ($share->getShareType() === IShare::TYPE_USER
581
-					&& $share->getSharedBy() === $destinationUid) {
582
-					$this->shareManager->deleteShare($share);
583
-				} elseif (isset($destinationShares[$share->getNodeId()])) {
584
-					$destinationShare = $destinationShares[$share->getNodeId()];
585
-					// Keep the share which has the most permissions and discard the other one.
586
-					if ($destinationShare->getPermissions() < $share->getPermissions()) {
587
-						$this->shareManager->deleteShare($destinationShare);
588
-						$share->setSharedWith($destinationUid);
589
-						// trigger refetching of the node so that the new owner and mountpoint are taken into account
590
-						// otherwise the checks on the share update will fail due to the original node not being available in the new user scope
591
-						$this->userMountCache->clear();
592
-						$share->setNodeId($share->getNode()->getId());
593
-						$this->shareManager->updateShare($share);
594
-						// The share is already transferred.
595
-						$progress->advance();
596
-						if ($move) {
597
-							continue;
598
-						}
599
-						$share->setTarget($shareTarget);
600
-						$this->shareManager->moveShare($share, $destinationUid);
601
-						continue;
602
-					}
603
-					$this->shareManager->deleteShare($share);
604
-				} elseif ($share->getShareOwner() === $destinationUid) {
605
-					$this->shareManager->deleteShare($share);
606
-				} else {
607
-					$share->setSharedWith($destinationUid);
608
-					$share->setNodeId($share->getNode()->getId());
609
-					$this->shareManager->updateShare($share);
610
-					// trigger refetching of the node so that the new owner and mountpoint are taken into account
611
-					// otherwise the checks on the share update will fail due to the original node not being available in the new user scope
612
-					$this->userMountCache->clear();
613
-					// The share is already transferred.
614
-					$progress->advance();
615
-					if ($move) {
616
-						continue;
617
-					}
618
-					$share->setTarget($shareTarget);
619
-					$this->shareManager->moveShare($share, $destinationUid);
620
-					continue;
621
-				}
622
-			} catch (NotFoundException $e) {
623
-				$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
624
-			} catch (\Throwable $e) {
625
-				$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '</error>');
626
-			}
627
-			$progress->advance();
628
-		}
629
-		$progress->finish();
630
-		$output->writeln('');
631
-	}
47
+    public function __construct(
48
+        private IEncryptionManager $encryptionManager,
49
+        private IShareManager $shareManager,
50
+        private IMountManager $mountManager,
51
+        private IUserMountCache $userMountCache,
52
+        private IUserManager $userManager,
53
+        private IFactory $l10nFactory,
54
+        private IRootFolder $rootFolder,
55
+    ) {
56
+    }
57
+
58
+    /**
59
+     * @param IUser $sourceUser
60
+     * @param IUser $destinationUser
61
+     * @param string $path
62
+     *
63
+     * @param OutputInterface|null $output
64
+     * @param bool $move
65
+     * @throws TransferOwnershipException
66
+     * @throws NoUserException
67
+     */
68
+    public function transfer(
69
+        IUser $sourceUser,
70
+        IUser $destinationUser,
71
+        string $path,
72
+        ?OutputInterface $output = null,
73
+        bool $move = false,
74
+        bool $firstLogin = false,
75
+        bool $includeExternalStorage = false,
76
+        bool $useUserId = false,
77
+    ): void {
78
+        $output = $output ?? new NullOutput();
79
+        $sourceUid = $sourceUser->getUID();
80
+        $destinationUid = $destinationUser->getUID();
81
+        $sourcePath = rtrim($sourceUid . '/files/' . $path, '/');
82
+
83
+        // If encryption is on we have to ensure the user has logged in before and that all encryption modules are ready
84
+        if (($this->encryptionManager->isEnabled() && $destinationUser->getLastLogin() === 0)
85
+            || !$this->encryptionManager->isReadyForUser($destinationUid)) {
86
+            throw new TransferOwnershipException('The target user is not ready to accept files. The user has at least to have logged in once.', 2);
87
+        }
88
+
89
+        // setup filesystem
90
+        // Requesting the user folder will set it up if the user hasn't logged in before
91
+        // We need a setupFS for the full filesystem setup before as otherwise we will just return
92
+        // a lazy root folder which does not create the destination users folder
93
+        \OC_Util::setupFS($sourceUser->getUID());
94
+        \OC_Util::setupFS($destinationUser->getUID());
95
+        $this->rootFolder->getUserFolder($sourceUser->getUID());
96
+        $this->rootFolder->getUserFolder($destinationUser->getUID());
97
+        Filesystem::initMountPoints($sourceUid);
98
+        Filesystem::initMountPoints($destinationUid);
99
+
100
+        $view = new View();
101
+
102
+        if ($move) {
103
+            $finalTarget = "$destinationUid/files/";
104
+        } else {
105
+            $l = $this->l10nFactory->get('files', $this->l10nFactory->getUserLanguage($destinationUser));
106
+            $date = date('Y-m-d H-i-s');
107
+
108
+            if ($useUserId) {
109
+                $cleanUserName = $sourceUid;
110
+            } else {
111
+                $cleanUserName = $this->sanitizeFolderName($sourceUser->getDisplayName());
112
+                if ($cleanUserName === '') {
113
+                    $cleanUserName = $sourceUid;
114
+                }
115
+            }
116
+
117
+            $finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$cleanUserName, $date]));
118
+            try {
119
+                $view->verifyPath(dirname($finalTarget), basename($finalTarget));
120
+            } catch (InvalidPathException $e) {
121
+                $finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$sourceUid, $date]));
122
+            }
123
+        }
124
+
125
+        if (!($view->is_dir($sourcePath) || $view->is_file($sourcePath))) {
126
+            throw new TransferOwnershipException("Unknown path provided: $path", 1);
127
+        }
128
+
129
+        if ($move && !$view->is_dir($finalTarget)) {
130
+            // Initialize storage
131
+            \OC_Util::setupFS($destinationUser->getUID());
132
+        }
133
+
134
+        if ($move && !$firstLogin && count($view->getDirectoryContent($finalTarget)) > 0) {
135
+            throw new TransferOwnershipException('Destination path does not exists or is not empty', 1);
136
+        }
137
+
138
+
139
+        // analyse source folder
140
+        $this->analyse(
141
+            $sourceUid,
142
+            $destinationUid,
143
+            $sourcePath,
144
+            $view,
145
+            $output
146
+        );
147
+
148
+        // collect all the shares
149
+        $shares = $this->collectUsersShares(
150
+            $sourceUid,
151
+            $output,
152
+            $view,
153
+            $sourcePath
154
+        );
155
+
156
+        $sourceSize = $view->getFileInfo($sourcePath)->getSize();
157
+
158
+        // transfer the files
159
+        $this->transferFiles(
160
+            $sourceUid,
161
+            $sourcePath,
162
+            $finalTarget,
163
+            $view,
164
+            $output,
165
+            $includeExternalStorage,
166
+        );
167
+        $sizeDifference = $sourceSize - $view->getFileInfo($finalTarget)->getSize();
168
+
169
+        // transfer the incoming shares
170
+        $sourceShares = $this->collectIncomingShares(
171
+            $sourceUid,
172
+            $output,
173
+            $sourcePath,
174
+        );
175
+        $destinationShares = $this->collectIncomingShares(
176
+            $destinationUid,
177
+            $output,
178
+            null,
179
+        );
180
+        $this->transferIncomingShares(
181
+            $sourceUid,
182
+            $destinationUid,
183
+            $sourceShares,
184
+            $destinationShares,
185
+            $output,
186
+            $path,
187
+            $finalTarget,
188
+            $move
189
+        );
190
+
191
+        $destinationPath = $finalTarget . '/' . $path;
192
+        // restore the shares
193
+        $this->restoreShares(
194
+            $sourceUid,
195
+            $destinationUid,
196
+            $destinationPath,
197
+            $shares,
198
+            $output
199
+        );
200
+        if ($sizeDifference !== 0) {
201
+            $output->writeln("Transferred folder have a size difference of: $sizeDifference Bytes which means the transfer may be incomplete. Please check the logs if there was any issue during the transfer operation.");
202
+        }
203
+    }
204
+
205
+    private function sanitizeFolderName(string $name): string {
206
+        // Remove some characters which are prone to cause errors
207
+        $name = str_replace(['\\', '/', ':', '.', '?', '#', '\'', '"'], '-', $name);
208
+        // Replace multiple dashes with one dash
209
+        return preg_replace('/-{2,}/s', '-', $name);
210
+    }
211
+
212
+    private function walkFiles(View $view, $path, Closure $callBack) {
213
+        foreach ($view->getDirectoryContent($path) as $fileInfo) {
214
+            if (!$callBack($fileInfo)) {
215
+                return;
216
+            }
217
+            if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
218
+                $this->walkFiles($view, $fileInfo->getPath(), $callBack);
219
+            }
220
+        }
221
+    }
222
+
223
+    /**
224
+     * @param OutputInterface $output
225
+     *
226
+     * @throws TransferOwnershipException
227
+     */
228
+    protected function analyse(
229
+        string $sourceUid,
230
+        string $destinationUid,
231
+        string $sourcePath,
232
+        View $view,
233
+        OutputInterface $output,
234
+        bool $includeExternalStorage = false,
235
+    ): void {
236
+        $output->writeln('Validating quota');
237
+        $sourceFileInfo = $view->getFileInfo($sourcePath, false);
238
+        if ($sourceFileInfo === false) {
239
+            throw new TransferOwnershipException("Unknown path provided: $sourcePath", 1);
240
+        }
241
+        $size = $sourceFileInfo->getSize(false);
242
+        $freeSpace = $view->free_space($destinationUid . '/files/');
243
+        if ($size > $freeSpace && $freeSpace !== FileInfo::SPACE_UNKNOWN) {
244
+            throw new TransferOwnershipException('Target user does not have enough free space available.', 1);
245
+        }
246
+
247
+        $output->writeln("Analysing files of $sourceUid ...");
248
+        $progress = new ProgressBar($output);
249
+        $progress->start();
250
+
251
+        if ($this->encryptionManager->isEnabled()) {
252
+            $masterKeyEnabled = Server::get(Util::class)->isMasterKeyEnabled();
253
+        } else {
254
+            $masterKeyEnabled = false;
255
+        }
256
+        $encryptedFiles = [];
257
+        if ($sourceFileInfo->getType() === FileInfo::TYPE_FOLDER) {
258
+            if ($sourceFileInfo->isEncrypted()) {
259
+                /* Encrypted folder means e2ee encrypted */
260
+                $encryptedFiles[] = $sourceFileInfo;
261
+            } else {
262
+                $this->walkFiles($view, $sourcePath,
263
+                    function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles, $includeExternalStorage) {
264
+                        if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
265
+                            $mount = $fileInfo->getMountPoint();
266
+                            // only analyze into folders from main storage,
267
+                            if (
268
+                                $mount->getMountProvider() instanceof IHomeMountProvider
269
+                                || ($includeExternalStorage && $mount->getMountProvider() instanceof ConfigAdapter)
270
+                            ) {
271
+                                if ($fileInfo->isEncrypted()) {
272
+                                    /* Encrypted folder means e2ee encrypted, we cannot transfer it */
273
+                                    $encryptedFiles[] = $fileInfo;
274
+                                }
275
+                                return true;
276
+                            } else {
277
+                                return false;
278
+                            }
279
+                        }
280
+                        $progress->advance();
281
+                        if ($fileInfo->isEncrypted() && !$masterKeyEnabled) {
282
+                            /* Encrypted file means SSE, we can only transfer it if master key is enabled */
283
+                            $encryptedFiles[] = $fileInfo;
284
+                        }
285
+                        return true;
286
+                    });
287
+            }
288
+        } elseif ($sourceFileInfo->isEncrypted() && !$masterKeyEnabled) {
289
+            /* Encrypted file means SSE, we can only transfer it if master key is enabled */
290
+            $encryptedFiles[] = $sourceFileInfo;
291
+        }
292
+        $progress->finish();
293
+        $output->writeln('');
294
+
295
+        // no file is allowed to be encrypted
296
+        if (!empty($encryptedFiles)) {
297
+            $output->writeln('<error>Some files are encrypted - please decrypt them first.</error>');
298
+            foreach ($encryptedFiles as $encryptedFile) {
299
+                /** @var FileInfo $encryptedFile */
300
+                $output->writeln('  ' . $encryptedFile->getPath());
301
+            }
302
+            throw new TransferOwnershipException('Some files are encrypted - please decrypt them first.', 1);
303
+        }
304
+    }
305
+
306
+    /**
307
+     * @return array<array{share: IShare, suffix: string}>
308
+     */
309
+    private function collectUsersShares(
310
+        string $sourceUid,
311
+        OutputInterface $output,
312
+        View $view,
313
+        string $path,
314
+    ): array {
315
+        $output->writeln("Collecting all share information for files and folders of $sourceUid ...");
316
+
317
+        $shares = [];
318
+        $progress = new ProgressBar($output);
319
+
320
+        $normalizedPath = Filesystem::normalizePath($path);
321
+
322
+        $supportedShareTypes = [
323
+            IShare::TYPE_GROUP,
324
+            IShare::TYPE_USER,
325
+            IShare::TYPE_LINK,
326
+            IShare::TYPE_REMOTE,
327
+            IShare::TYPE_ROOM,
328
+            IShare::TYPE_EMAIL,
329
+            IShare::TYPE_CIRCLE,
330
+            IShare::TYPE_DECK,
331
+        ];
332
+
333
+        foreach ($supportedShareTypes as $shareType) {
334
+            $offset = 0;
335
+            while (true) {
336
+                $sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset, onlyValid: false);
337
+                $progress->advance(count($sharePage));
338
+                if (empty($sharePage)) {
339
+                    break;
340
+                }
341
+                if ($path !== "$sourceUid/files") {
342
+                    $sharePage = array_filter($sharePage, function (IShare $share) use ($view, $normalizedPath) {
343
+                        try {
344
+                            $sourceNode = $share->getNode();
345
+                            $relativePath = $view->getRelativePath($sourceNode->getPath());
346
+
347
+                            return str_starts_with($relativePath . '/', $normalizedPath . '/');
348
+                        } catch (Exception $e) {
349
+                            return false;
350
+                        }
351
+                    });
352
+                }
353
+                $shares = array_merge($shares, $sharePage);
354
+                $offset += 50;
355
+            }
356
+        }
357
+
358
+        $progress->finish();
359
+        $output->writeln('');
360
+
361
+        return array_values(array_filter(array_map(function (IShare $share) use ($view, $normalizedPath, $output, $sourceUid) {
362
+            try {
363
+                $nodePath = $view->getRelativePath($share->getNode()->getPath());
364
+            } catch (NotFoundException $e) {
365
+                $output->writeln("<error>Failed to find path for shared file {$share->getNodeId()} for user $sourceUid, skipping</error>");
366
+                return null;
367
+            }
368
+
369
+            return [
370
+                'share' => $share,
371
+                'suffix' => substr(Filesystem::normalizePath($nodePath), strlen($normalizedPath)),
372
+            ];
373
+        }, $shares)));
374
+    }
375
+
376
+    private function collectIncomingShares(
377
+        string $sourceUid,
378
+        OutputInterface $output,
379
+        ?string $path,
380
+    ): array {
381
+        $output->writeln("Collecting all incoming share information for files and folders of $sourceUid ...");
382
+
383
+        $shares = [];
384
+        $progress = new ProgressBar($output);
385
+        $normalizedPath = Filesystem::normalizePath($path);
386
+
387
+        $offset = 0;
388
+        while (true) {
389
+            $sharePage = $this->shareManager->getSharedWith($sourceUid, IShare::TYPE_USER, null, 50, $offset);
390
+            $progress->advance(count($sharePage));
391
+            if (empty($sharePage)) {
392
+                break;
393
+            }
394
+
395
+            if ($path !== null && $path !== "$sourceUid/files") {
396
+                $sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath) {
397
+                    try {
398
+                        return str_starts_with(Filesystem::normalizePath($sourceUid . '/files' . $share->getTarget() . '/', false), $normalizedPath . '/');
399
+                    } catch (Exception) {
400
+                        return false;
401
+                    }
402
+                });
403
+            }
404
+
405
+            foreach ($sharePage as $share) {
406
+                $shares[$share->getNodeId()] = $share;
407
+            }
408
+
409
+            $offset += 50;
410
+        }
411
+
412
+
413
+        $progress->finish();
414
+        $output->writeln('');
415
+        return $shares;
416
+    }
417
+
418
+    /**
419
+     * @throws TransferOwnershipException
420
+     */
421
+    protected function transferFiles(
422
+        string $sourceUid,
423
+        string $sourcePath,
424
+        string $finalTarget,
425
+        View $view,
426
+        OutputInterface $output,
427
+        bool $includeExternalStorage,
428
+    ): void {
429
+        $output->writeln("Transferring files to $finalTarget ...");
430
+
431
+        // This change will help user to transfer the folder specified using --path option.
432
+        // Else only the content inside folder is transferred which is not correct.
433
+        if ($sourcePath !== "$sourceUid/files") {
434
+            $view->mkdir($finalTarget);
435
+            $finalTarget = $finalTarget . '/' . basename($sourcePath);
436
+        }
437
+        $sourceInfo = $view->getFileInfo($sourcePath);
438
+
439
+        /// handle the external storages mounted at the root, or the admin specifying an external storage with --path
440
+        if ($sourceInfo->getInternalPath() === '' && $includeExternalStorage) {
441
+            $this->moveMountContents($view, $sourcePath, $finalTarget);
442
+        } else {
443
+            if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) {
444
+                throw new TransferOwnershipException('Could not transfer files.', 1);
445
+            }
446
+        }
447
+
448
+        if ($includeExternalStorage) {
449
+            $nestedMounts = $this->mountManager->findIn($sourcePath);
450
+            foreach ($nestedMounts as $mount) {
451
+                if ($mount->getMountProvider() === ConfigAdapter::class) {
452
+                    $relativePath = substr(trim($mount->getMountPoint(), '/'), strlen($sourcePath));
453
+                    $this->moveMountContents($view, $mount->getMountPoint(), $finalTarget . $relativePath);
454
+                }
455
+            }
456
+        }
457
+
458
+        if (!is_dir("$sourceUid/files")) {
459
+            // because the files folder is moved away we need to recreate it
460
+            $view->mkdir("$sourceUid/files");
461
+        }
462
+    }
463
+
464
+    private function moveMountContents(View $rootView, string $source, string $target) {
465
+        if ($rootView->copy($source, $target)) {
466
+            // just doing `rmdir` on the mountpoint would cause it to try and unmount the storage
467
+            // we need to empty the contents instead
468
+            $content = $rootView->getDirectoryContent($source);
469
+            foreach ($content as $item) {
470
+                if ($item->getType() === FileInfo::TYPE_FOLDER) {
471
+                    $rootView->rmdir($item->getPath());
472
+                } else {
473
+                    $rootView->unlink($item->getPath());
474
+                }
475
+            }
476
+        } else {
477
+            throw new TransferOwnershipException("Could not transfer $source to $target");
478
+        }
479
+    }
480
+
481
+    /**
482
+     * @param string $targetLocation New location of the transfered node
483
+     * @param array<array{share: IShare, suffix: string}> $shares previously collected share information
484
+     */
485
+    private function restoreShares(
486
+        string $sourceUid,
487
+        string $destinationUid,
488
+        string $targetLocation,
489
+        array $shares,
490
+        OutputInterface $output,
491
+    ):void {
492
+        $output->writeln('Restoring shares ...');
493
+        $progress = new ProgressBar($output, count($shares));
494
+
495
+        foreach ($shares as ['share' => $share, 'suffix' => $suffix]) {
496
+            try {
497
+                $output->writeln('Transfering share ' . $share->getId() . ' of type ' . $share->getShareType(), OutputInterface::VERBOSITY_VERBOSE);
498
+                if ($share->getShareType() === IShare::TYPE_USER
499
+                    && $share->getSharedWith() === $destinationUid) {
500
+                    // Unmount the shares before deleting, so we don't try to get the storage later on.
501
+                    $shareMountPoint = $this->mountManager->find('/' . $destinationUid . '/files' . $share->getTarget());
502
+                    if ($shareMountPoint) {
503
+                        $this->mountManager->removeMount($shareMountPoint->getMountPoint());
504
+                    }
505
+                    $this->shareManager->deleteShare($share);
506
+                } else {
507
+                    if ($share->getShareOwner() === $sourceUid) {
508
+                        $share->setShareOwner($destinationUid);
509
+                    }
510
+                    if ($share->getSharedBy() === $sourceUid) {
511
+                        $share->setSharedBy($destinationUid);
512
+                    }
513
+
514
+                    if ($share->getShareType() === IShare::TYPE_USER
515
+                        && !$this->userManager->userExists($share->getSharedWith())) {
516
+                        // stray share with deleted user
517
+                        $output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted user "' . $share->getSharedWith() . '", deleting</error>');
518
+                        $this->shareManager->deleteShare($share);
519
+                        continue;
520
+                    } else {
521
+                        // trigger refetching of the node so that the new owner and mountpoint are taken into account
522
+                        // otherwise the checks on the share update will fail due to the original node not being available in the new user scope
523
+                        $this->userMountCache->clear();
524
+
525
+                        try {
526
+                            // Try to get the "old" id.
527
+                            // Normally the ID is preserved,
528
+                            // but for transferes between different storages the ID might change
529
+                            $newNodeId = $share->getNode()->getId();
530
+                        } catch (NotFoundException) {
531
+                            // ID has changed due to transfer between different storages
532
+                            // Try to get the new ID from the target path and suffix of the share
533
+                            $node = $this->rootFolder->get(Filesystem::normalizePath($targetLocation . '/' . $suffix));
534
+                            $newNodeId = $node->getId();
535
+                            $output->writeln('Had to change node id to ' . $newNodeId, OutputInterface::VERBOSITY_VERY_VERBOSE);
536
+                        }
537
+                        $share->setNodeId($newNodeId);
538
+
539
+                        $this->shareManager->updateShare($share, onlyValid: false);
540
+                    }
541
+                }
542
+            } catch (NotFoundException $e) {
543
+                $output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
544
+            } catch (\Throwable $e) {
545
+                $output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getMessage() . ' : ' . $e->getTraceAsString() . '</error>');
546
+            }
547
+            $progress->advance();
548
+        }
549
+        $progress->finish();
550
+        $output->writeln('');
551
+    }
552
+
553
+    private function transferIncomingShares(string $sourceUid,
554
+        string $destinationUid,
555
+        array $sourceShares,
556
+        array $destinationShares,
557
+        OutputInterface $output,
558
+        string $path,
559
+        string $finalTarget,
560
+        bool $move): void {
561
+        $output->writeln('Restoring incoming shares ...');
562
+        $progress = new ProgressBar($output, count($sourceShares));
563
+        $prefix = "$destinationUid/files";
564
+        $finalShareTarget = '';
565
+        if (str_starts_with($finalTarget, $prefix)) {
566
+            $finalShareTarget = substr($finalTarget, strlen($prefix));
567
+        }
568
+        foreach ($sourceShares as $share) {
569
+            try {
570
+                // Only restore if share is in given path.
571
+                $pathToCheck = '/';
572
+                if (trim($path, '/') !== '') {
573
+                    $pathToCheck = '/' . trim($path) . '/';
574
+                }
575
+                if (!str_starts_with($share->getTarget(), $pathToCheck)) {
576
+                    continue;
577
+                }
578
+                $shareTarget = $share->getTarget();
579
+                $shareTarget = $finalShareTarget . $shareTarget;
580
+                if ($share->getShareType() === IShare::TYPE_USER
581
+                    && $share->getSharedBy() === $destinationUid) {
582
+                    $this->shareManager->deleteShare($share);
583
+                } elseif (isset($destinationShares[$share->getNodeId()])) {
584
+                    $destinationShare = $destinationShares[$share->getNodeId()];
585
+                    // Keep the share which has the most permissions and discard the other one.
586
+                    if ($destinationShare->getPermissions() < $share->getPermissions()) {
587
+                        $this->shareManager->deleteShare($destinationShare);
588
+                        $share->setSharedWith($destinationUid);
589
+                        // trigger refetching of the node so that the new owner and mountpoint are taken into account
590
+                        // otherwise the checks on the share update will fail due to the original node not being available in the new user scope
591
+                        $this->userMountCache->clear();
592
+                        $share->setNodeId($share->getNode()->getId());
593
+                        $this->shareManager->updateShare($share);
594
+                        // The share is already transferred.
595
+                        $progress->advance();
596
+                        if ($move) {
597
+                            continue;
598
+                        }
599
+                        $share->setTarget($shareTarget);
600
+                        $this->shareManager->moveShare($share, $destinationUid);
601
+                        continue;
602
+                    }
603
+                    $this->shareManager->deleteShare($share);
604
+                } elseif ($share->getShareOwner() === $destinationUid) {
605
+                    $this->shareManager->deleteShare($share);
606
+                } else {
607
+                    $share->setSharedWith($destinationUid);
608
+                    $share->setNodeId($share->getNode()->getId());
609
+                    $this->shareManager->updateShare($share);
610
+                    // trigger refetching of the node so that the new owner and mountpoint are taken into account
611
+                    // otherwise the checks on the share update will fail due to the original node not being available in the new user scope
612
+                    $this->userMountCache->clear();
613
+                    // The share is already transferred.
614
+                    $progress->advance();
615
+                    if ($move) {
616
+                        continue;
617
+                    }
618
+                    $share->setTarget($shareTarget);
619
+                    $this->shareManager->moveShare($share, $destinationUid);
620
+                    continue;
621
+                }
622
+            } catch (NotFoundException $e) {
623
+                $output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
624
+            } catch (\Throwable $e) {
625
+                $output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '</error>');
626
+            }
627
+            $progress->advance();
628
+        }
629
+        $progress->finish();
630
+        $output->writeln('');
631
+    }
632 632
 }
Please login to merge, or discard this patch.
apps/files/lib/Controller/ApiController.php 1 patch
Indentation   +405 added lines, -405 removed lines patch added patch discarded remove patch
@@ -53,409 +53,409 @@
 block discarded – undo
53 53
  * @package OCA\Files\Controller
54 54
  */
55 55
 class ApiController extends Controller {
56
-	public function __construct(
57
-		string $appName,
58
-		IRequest $request,
59
-		private IUserSession $userSession,
60
-		private TagService $tagService,
61
-		private IPreview $previewManager,
62
-		private IManager $shareManager,
63
-		private IConfig $config,
64
-		private ?Folder $userFolder,
65
-		private UserConfig $userConfig,
66
-		private ViewConfig $viewConfig,
67
-		private IL10N $l10n,
68
-		private IRootFolder $rootFolder,
69
-		private LoggerInterface $logger,
70
-	) {
71
-		parent::__construct($appName, $request);
72
-	}
73
-
74
-	/**
75
-	 * Gets a thumbnail of the specified file
76
-	 *
77
-	 * @since API version 1.0
78
-	 * @deprecated 32.0.0 Use the preview endpoint provided by core instead
79
-	 *
80
-	 * @param int $x Width of the thumbnail
81
-	 * @param int $y Height of the thumbnail
82
-	 * @param string $file URL-encoded filename
83
-	 * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{message?: string}, array{}>
84
-	 *
85
-	 * 200: Thumbnail returned
86
-	 * 400: Getting thumbnail is not possible
87
-	 * 404: File not found
88
-	 */
89
-	#[NoAdminRequired]
90
-	#[NoCSRFRequired]
91
-	#[StrictCookiesRequired]
92
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
93
-	public function getThumbnail($x, $y, $file) {
94
-		if ($x < 1 || $y < 1) {
95
-			return new DataResponse(['message' => 'Requested size must be numeric and a positive value.'], Http::STATUS_BAD_REQUEST);
96
-		}
97
-
98
-		try {
99
-			$file = $this->userFolder?->get($file);
100
-			if ($file === null
101
-				|| !($file instanceof File)
102
-				|| ($file->getId() <= 0)
103
-			) {
104
-				throw new NotFoundException();
105
-			}
106
-
107
-			// Validate the user is allowed to download the file (preview is some kind of download)
108
-			/** @var ISharedStorage $storage */
109
-			$storage = $file->getStorage();
110
-			if ($storage->instanceOfStorage(ISharedStorage::class)) {
111
-				/** @var IShare $share */
112
-				$share = $storage->getShare();
113
-				if (!$share->canSeeContent()) {
114
-					throw new NotFoundException();
115
-				}
116
-			}
117
-
118
-			$preview = $this->previewManager->getPreview($file, $x, $y, true);
119
-
120
-			return new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => $preview->getMimeType()]);
121
-		} catch (NotFoundException|NotPermittedException|InvalidPathException) {
122
-			return new DataResponse(['message' => 'File not found.'], Http::STATUS_NOT_FOUND);
123
-		} catch (\Exception $e) {
124
-			return new DataResponse([], Http::STATUS_BAD_REQUEST);
125
-		}
126
-	}
127
-
128
-	/**
129
-	 * Updates the info of the specified file path
130
-	 * The passed tags are absolute, which means they will
131
-	 * replace the actual tag selection.
132
-	 *
133
-	 * @param string $path path
134
-	 * @param array|string $tags array of tags
135
-	 * @return DataResponse
136
-	 */
137
-	#[NoAdminRequired]
138
-	public function updateFileTags($path, $tags = null) {
139
-		$result = [];
140
-		// if tags specified or empty array, update tags
141
-		if (!is_null($tags)) {
142
-			try {
143
-				$this->tagService->updateFileTags($path, $tags);
144
-			} catch (NotFoundException $e) {
145
-				return new DataResponse([
146
-					'message' => $e->getMessage()
147
-				], Http::STATUS_NOT_FOUND);
148
-			} catch (StorageNotAvailableException $e) {
149
-				return new DataResponse([
150
-					'message' => $e->getMessage()
151
-				], Http::STATUS_SERVICE_UNAVAILABLE);
152
-			} catch (\Exception $e) {
153
-				return new DataResponse([
154
-					'message' => $e->getMessage()
155
-				], Http::STATUS_NOT_FOUND);
156
-			}
157
-			$result['tags'] = $tags;
158
-		}
159
-		return new DataResponse($result);
160
-	}
161
-
162
-	/**
163
-	 * @param \OCP\Files\Node[] $nodes
164
-	 * @return array
165
-	 */
166
-	private function formatNodes(array $nodes) {
167
-		$shareTypesForNodes = $this->getShareTypesForNodes($nodes);
168
-		return array_values(array_map(function (Node $node) use ($shareTypesForNodes) {
169
-			$shareTypes = $shareTypesForNodes[$node->getId()] ?? [];
170
-			$file = Helper::formatFileInfo($node->getFileInfo());
171
-			$file['hasPreview'] = $this->previewManager->isAvailable($node);
172
-			$parts = explode('/', dirname($node->getPath()), 4);
173
-			if (isset($parts[3])) {
174
-				$file['path'] = '/' . $parts[3];
175
-			} else {
176
-				$file['path'] = '/';
177
-			}
178
-			if (!empty($shareTypes)) {
179
-				$file['shareTypes'] = $shareTypes;
180
-			}
181
-			return $file;
182
-		}, $nodes));
183
-	}
184
-
185
-	/**
186
-	 * Get the share types for each node
187
-	 *
188
-	 * @param \OCP\Files\Node[] $nodes
189
-	 * @return array<int, int[]> list of share types for each fileid
190
-	 */
191
-	private function getShareTypesForNodes(array $nodes): array {
192
-		$userId = $this->userSession->getUser()->getUID();
193
-		$requestedShareTypes = [
194
-			IShare::TYPE_USER,
195
-			IShare::TYPE_GROUP,
196
-			IShare::TYPE_LINK,
197
-			IShare::TYPE_REMOTE,
198
-			IShare::TYPE_EMAIL,
199
-			IShare::TYPE_ROOM,
200
-			IShare::TYPE_DECK,
201
-		];
202
-		$shareTypes = [];
203
-
204
-		$nodeIds = array_map(function (Node $node) {
205
-			return $node->getId();
206
-		}, $nodes);
207
-
208
-		foreach ($requestedShareTypes as $shareType) {
209
-			$nodesLeft = array_combine($nodeIds, array_fill(0, count($nodeIds), true));
210
-			$offset = 0;
211
-
212
-			// fetch shares until we've either found shares for all nodes or there are no more shares left
213
-			while (count($nodesLeft) > 0) {
214
-				$shares = $this->shareManager->getSharesBy($userId, $shareType, null, false, 100, $offset);
215
-				foreach ($shares as $share) {
216
-					$fileId = $share->getNodeId();
217
-					if (isset($nodesLeft[$fileId])) {
218
-						if (!isset($shareTypes[$fileId])) {
219
-							$shareTypes[$fileId] = [];
220
-						}
221
-						$shareTypes[$fileId][] = $shareType;
222
-						unset($nodesLeft[$fileId]);
223
-					}
224
-				}
225
-
226
-				if (count($shares) < 100) {
227
-					break;
228
-				} else {
229
-					$offset += count($shares);
230
-				}
231
-			}
232
-		}
233
-		return $shareTypes;
234
-	}
235
-
236
-	/**
237
-	 * Returns a list of recently modified files.
238
-	 *
239
-	 * @return DataResponse
240
-	 */
241
-	#[NoAdminRequired]
242
-	public function getRecentFiles() {
243
-		$nodes = $this->userFolder->getRecent(100);
244
-		$files = $this->formatNodes($nodes);
245
-		return new DataResponse(['files' => $files]);
246
-	}
247
-
248
-	/**
249
-	 * @param \OCP\Files\Node[] $nodes
250
-	 * @param int $depth The depth to traverse into the contents of each node
251
-	 */
252
-	private function getChildren(array $nodes, int $depth = 1, int $currentDepth = 0): array {
253
-		if ($currentDepth >= $depth) {
254
-			return [];
255
-		}
256
-
257
-		$children = [];
258
-		foreach ($nodes as $node) {
259
-			if (!($node instanceof Folder)) {
260
-				continue;
261
-			}
262
-
263
-			$basename = basename($node->getPath());
264
-			$entry = [
265
-				'id' => $node->getId(),
266
-				'basename' => $basename,
267
-				'children' => $this->getChildren($node->getDirectoryListing(), $depth, $currentDepth + 1),
268
-			];
269
-			$displayName = $node->getName();
270
-			if ($basename !== $displayName) {
271
-				$entry['displayName'] = $displayName;
272
-			}
273
-			$children[] = $entry;
274
-		}
275
-		return $children;
276
-	}
277
-
278
-	/**
279
-	 * Returns the folder tree of the user
280
-	 *
281
-	 * @param string $path The path relative to the user folder
282
-	 * @param int $depth The depth of the tree
283
-	 *
284
-	 * @return JSONResponse<Http::STATUS_OK, FilesFolderTree, array{}>|JSONResponse<Http::STATUS_UNAUTHORIZED|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
285
-	 *
286
-	 * 200: Folder tree returned successfully
287
-	 * 400: Invalid folder path
288
-	 * 401: Unauthorized
289
-	 * 404: Folder not found
290
-	 */
291
-	#[NoAdminRequired]
292
-	#[ApiRoute(verb: 'GET', url: '/api/v1/folder-tree')]
293
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
294
-	public function getFolderTree(string $path = '/', int $depth = 1): JSONResponse {
295
-		$user = $this->userSession->getUser();
296
-		if (!($user instanceof IUser)) {
297
-			return new JSONResponse([
298
-				'message' => $this->l10n->t('Failed to authorize'),
299
-			], Http::STATUS_UNAUTHORIZED);
300
-		}
301
-		try {
302
-			$userFolder = $this->rootFolder->getUserFolder($user->getUID());
303
-			$userFolderPath = $userFolder->getPath();
304
-			$fullPath = implode('/', [$userFolderPath, trim($path, '/')]);
305
-			$node = $this->rootFolder->get($fullPath);
306
-			if (!($node instanceof Folder)) {
307
-				return new JSONResponse([
308
-					'message' => $this->l10n->t('Invalid folder path'),
309
-				], Http::STATUS_BAD_REQUEST);
310
-			}
311
-			$nodes = $node->getDirectoryListing();
312
-			$tree = $this->getChildren($nodes, $depth);
313
-		} catch (NotFoundException $e) {
314
-			return new JSONResponse([
315
-				'message' => $this->l10n->t('Folder not found'),
316
-			], Http::STATUS_NOT_FOUND);
317
-		} catch (Throwable $th) {
318
-			$this->logger->error($th->getMessage(), ['exception' => $th]);
319
-			$tree = [];
320
-		}
321
-		return new JSONResponse($tree);
322
-	}
323
-
324
-	/**
325
-	 * Returns the current logged-in user's storage stats.
326
-	 *
327
-	 * @param ?string $dir the directory to get the storage stats from
328
-	 * @return JSONResponse
329
-	 */
330
-	#[NoAdminRequired]
331
-	public function getStorageStats($dir = '/'): JSONResponse {
332
-		$storageInfo = \OC_Helper::getStorageInfo($dir ?: '/');
333
-		$response = new JSONResponse(['message' => 'ok', 'data' => $storageInfo]);
334
-		$response->cacheFor(5 * 60);
335
-		return $response;
336
-	}
337
-
338
-	/**
339
-	 * Set a user view config
340
-	 *
341
-	 * @param string $view
342
-	 * @param string $key
343
-	 * @param string|bool $value
344
-	 * @return JSONResponse
345
-	 */
346
-	#[NoAdminRequired]
347
-	public function setViewConfig(string $view, string $key, $value): JSONResponse {
348
-		try {
349
-			$this->viewConfig->setConfig($view, $key, (string)$value);
350
-		} catch (\InvalidArgumentException $e) {
351
-			return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
352
-		}
353
-
354
-		return new JSONResponse(['message' => 'ok', 'data' => $this->viewConfig->getConfig($view)]);
355
-	}
356
-
357
-
358
-	/**
359
-	 * Get the user view config
360
-	 *
361
-	 * @return JSONResponse
362
-	 */
363
-	#[NoAdminRequired]
364
-	public function getViewConfigs(): JSONResponse {
365
-		return new JSONResponse(['message' => 'ok', 'data' => $this->viewConfig->getConfigs()]);
366
-	}
367
-
368
-	/**
369
-	 * Set a user config
370
-	 *
371
-	 * @param string $key
372
-	 * @param string|bool $value
373
-	 * @return JSONResponse
374
-	 */
375
-	#[NoAdminRequired]
376
-	public function setConfig(string $key, $value): JSONResponse {
377
-		try {
378
-			$this->userConfig->setConfig($key, (string)$value);
379
-		} catch (\InvalidArgumentException $e) {
380
-			return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
381
-		}
382
-
383
-		return new JSONResponse(['message' => 'ok', 'data' => ['key' => $key, 'value' => $value]]);
384
-	}
385
-
386
-
387
-	/**
388
-	 * Get the user config
389
-	 *
390
-	 * @return JSONResponse
391
-	 */
392
-	#[NoAdminRequired]
393
-	public function getConfigs(): JSONResponse {
394
-		return new JSONResponse(['message' => 'ok', 'data' => $this->userConfig->getConfigs()]);
395
-	}
396
-
397
-	/**
398
-	 * Toggle default for showing/hiding hidden files
399
-	 *
400
-	 * @param bool $value
401
-	 * @return Response
402
-	 * @throws PreConditionNotMetException
403
-	 */
404
-	#[NoAdminRequired]
405
-	public function showHiddenFiles(bool $value): Response {
406
-		$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', $value ? '1' : '0');
407
-		return new Response();
408
-	}
409
-
410
-	/**
411
-	 * Toggle default for cropping preview images
412
-	 *
413
-	 * @param bool $value
414
-	 * @return Response
415
-	 * @throws PreConditionNotMetException
416
-	 */
417
-	#[NoAdminRequired]
418
-	public function cropImagePreviews(bool $value): Response {
419
-		$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'crop_image_previews', $value ? '1' : '0');
420
-		return new Response();
421
-	}
422
-
423
-	/**
424
-	 * Toggle default for files grid view
425
-	 *
426
-	 * @param bool $show
427
-	 * @return Response
428
-	 * @throws PreConditionNotMetException
429
-	 */
430
-	#[NoAdminRequired]
431
-	public function showGridView(bool $show): Response {
432
-		$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_grid', $show ? '1' : '0');
433
-		return new Response();
434
-	}
435
-
436
-	/**
437
-	 * Get default settings for the grid view
438
-	 */
439
-	#[NoAdminRequired]
440
-	public function getGridView() {
441
-		$status = $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'show_grid', '0') === '1';
442
-		return new JSONResponse(['gridview' => $status]);
443
-	}
444
-
445
-	#[PublicPage]
446
-	#[NoCSRFRequired]
447
-	#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
448
-	public function serviceWorker(): StreamResponse {
449
-		$response = new StreamResponse(__DIR__ . '/../../../../dist/preview-service-worker.js');
450
-		$response->setHeaders([
451
-			'Content-Type' => 'application/javascript',
452
-			'Service-Worker-Allowed' => '/'
453
-		]);
454
-		$policy = new ContentSecurityPolicy();
455
-		$policy->addAllowedWorkerSrcDomain("'self'");
456
-		$policy->addAllowedScriptDomain("'self'");
457
-		$policy->addAllowedConnectDomain("'self'");
458
-		$response->setContentSecurityPolicy($policy);
459
-		return $response;
460
-	}
56
+    public function __construct(
57
+        string $appName,
58
+        IRequest $request,
59
+        private IUserSession $userSession,
60
+        private TagService $tagService,
61
+        private IPreview $previewManager,
62
+        private IManager $shareManager,
63
+        private IConfig $config,
64
+        private ?Folder $userFolder,
65
+        private UserConfig $userConfig,
66
+        private ViewConfig $viewConfig,
67
+        private IL10N $l10n,
68
+        private IRootFolder $rootFolder,
69
+        private LoggerInterface $logger,
70
+    ) {
71
+        parent::__construct($appName, $request);
72
+    }
73
+
74
+    /**
75
+     * Gets a thumbnail of the specified file
76
+     *
77
+     * @since API version 1.0
78
+     * @deprecated 32.0.0 Use the preview endpoint provided by core instead
79
+     *
80
+     * @param int $x Width of the thumbnail
81
+     * @param int $y Height of the thumbnail
82
+     * @param string $file URL-encoded filename
83
+     * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{message?: string}, array{}>
84
+     *
85
+     * 200: Thumbnail returned
86
+     * 400: Getting thumbnail is not possible
87
+     * 404: File not found
88
+     */
89
+    #[NoAdminRequired]
90
+    #[NoCSRFRequired]
91
+    #[StrictCookiesRequired]
92
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
93
+    public function getThumbnail($x, $y, $file) {
94
+        if ($x < 1 || $y < 1) {
95
+            return new DataResponse(['message' => 'Requested size must be numeric and a positive value.'], Http::STATUS_BAD_REQUEST);
96
+        }
97
+
98
+        try {
99
+            $file = $this->userFolder?->get($file);
100
+            if ($file === null
101
+                || !($file instanceof File)
102
+                || ($file->getId() <= 0)
103
+            ) {
104
+                throw new NotFoundException();
105
+            }
106
+
107
+            // Validate the user is allowed to download the file (preview is some kind of download)
108
+            /** @var ISharedStorage $storage */
109
+            $storage = $file->getStorage();
110
+            if ($storage->instanceOfStorage(ISharedStorage::class)) {
111
+                /** @var IShare $share */
112
+                $share = $storage->getShare();
113
+                if (!$share->canSeeContent()) {
114
+                    throw new NotFoundException();
115
+                }
116
+            }
117
+
118
+            $preview = $this->previewManager->getPreview($file, $x, $y, true);
119
+
120
+            return new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => $preview->getMimeType()]);
121
+        } catch (NotFoundException|NotPermittedException|InvalidPathException) {
122
+            return new DataResponse(['message' => 'File not found.'], Http::STATUS_NOT_FOUND);
123
+        } catch (\Exception $e) {
124
+            return new DataResponse([], Http::STATUS_BAD_REQUEST);
125
+        }
126
+    }
127
+
128
+    /**
129
+     * Updates the info of the specified file path
130
+     * The passed tags are absolute, which means they will
131
+     * replace the actual tag selection.
132
+     *
133
+     * @param string $path path
134
+     * @param array|string $tags array of tags
135
+     * @return DataResponse
136
+     */
137
+    #[NoAdminRequired]
138
+    public function updateFileTags($path, $tags = null) {
139
+        $result = [];
140
+        // if tags specified or empty array, update tags
141
+        if (!is_null($tags)) {
142
+            try {
143
+                $this->tagService->updateFileTags($path, $tags);
144
+            } catch (NotFoundException $e) {
145
+                return new DataResponse([
146
+                    'message' => $e->getMessage()
147
+                ], Http::STATUS_NOT_FOUND);
148
+            } catch (StorageNotAvailableException $e) {
149
+                return new DataResponse([
150
+                    'message' => $e->getMessage()
151
+                ], Http::STATUS_SERVICE_UNAVAILABLE);
152
+            } catch (\Exception $e) {
153
+                return new DataResponse([
154
+                    'message' => $e->getMessage()
155
+                ], Http::STATUS_NOT_FOUND);
156
+            }
157
+            $result['tags'] = $tags;
158
+        }
159
+        return new DataResponse($result);
160
+    }
161
+
162
+    /**
163
+     * @param \OCP\Files\Node[] $nodes
164
+     * @return array
165
+     */
166
+    private function formatNodes(array $nodes) {
167
+        $shareTypesForNodes = $this->getShareTypesForNodes($nodes);
168
+        return array_values(array_map(function (Node $node) use ($shareTypesForNodes) {
169
+            $shareTypes = $shareTypesForNodes[$node->getId()] ?? [];
170
+            $file = Helper::formatFileInfo($node->getFileInfo());
171
+            $file['hasPreview'] = $this->previewManager->isAvailable($node);
172
+            $parts = explode('/', dirname($node->getPath()), 4);
173
+            if (isset($parts[3])) {
174
+                $file['path'] = '/' . $parts[3];
175
+            } else {
176
+                $file['path'] = '/';
177
+            }
178
+            if (!empty($shareTypes)) {
179
+                $file['shareTypes'] = $shareTypes;
180
+            }
181
+            return $file;
182
+        }, $nodes));
183
+    }
184
+
185
+    /**
186
+     * Get the share types for each node
187
+     *
188
+     * @param \OCP\Files\Node[] $nodes
189
+     * @return array<int, int[]> list of share types for each fileid
190
+     */
191
+    private function getShareTypesForNodes(array $nodes): array {
192
+        $userId = $this->userSession->getUser()->getUID();
193
+        $requestedShareTypes = [
194
+            IShare::TYPE_USER,
195
+            IShare::TYPE_GROUP,
196
+            IShare::TYPE_LINK,
197
+            IShare::TYPE_REMOTE,
198
+            IShare::TYPE_EMAIL,
199
+            IShare::TYPE_ROOM,
200
+            IShare::TYPE_DECK,
201
+        ];
202
+        $shareTypes = [];
203
+
204
+        $nodeIds = array_map(function (Node $node) {
205
+            return $node->getId();
206
+        }, $nodes);
207
+
208
+        foreach ($requestedShareTypes as $shareType) {
209
+            $nodesLeft = array_combine($nodeIds, array_fill(0, count($nodeIds), true));
210
+            $offset = 0;
211
+
212
+            // fetch shares until we've either found shares for all nodes or there are no more shares left
213
+            while (count($nodesLeft) > 0) {
214
+                $shares = $this->shareManager->getSharesBy($userId, $shareType, null, false, 100, $offset);
215
+                foreach ($shares as $share) {
216
+                    $fileId = $share->getNodeId();
217
+                    if (isset($nodesLeft[$fileId])) {
218
+                        if (!isset($shareTypes[$fileId])) {
219
+                            $shareTypes[$fileId] = [];
220
+                        }
221
+                        $shareTypes[$fileId][] = $shareType;
222
+                        unset($nodesLeft[$fileId]);
223
+                    }
224
+                }
225
+
226
+                if (count($shares) < 100) {
227
+                    break;
228
+                } else {
229
+                    $offset += count($shares);
230
+                }
231
+            }
232
+        }
233
+        return $shareTypes;
234
+    }
235
+
236
+    /**
237
+     * Returns a list of recently modified files.
238
+     *
239
+     * @return DataResponse
240
+     */
241
+    #[NoAdminRequired]
242
+    public function getRecentFiles() {
243
+        $nodes = $this->userFolder->getRecent(100);
244
+        $files = $this->formatNodes($nodes);
245
+        return new DataResponse(['files' => $files]);
246
+    }
247
+
248
+    /**
249
+     * @param \OCP\Files\Node[] $nodes
250
+     * @param int $depth The depth to traverse into the contents of each node
251
+     */
252
+    private function getChildren(array $nodes, int $depth = 1, int $currentDepth = 0): array {
253
+        if ($currentDepth >= $depth) {
254
+            return [];
255
+        }
256
+
257
+        $children = [];
258
+        foreach ($nodes as $node) {
259
+            if (!($node instanceof Folder)) {
260
+                continue;
261
+            }
262
+
263
+            $basename = basename($node->getPath());
264
+            $entry = [
265
+                'id' => $node->getId(),
266
+                'basename' => $basename,
267
+                'children' => $this->getChildren($node->getDirectoryListing(), $depth, $currentDepth + 1),
268
+            ];
269
+            $displayName = $node->getName();
270
+            if ($basename !== $displayName) {
271
+                $entry['displayName'] = $displayName;
272
+            }
273
+            $children[] = $entry;
274
+        }
275
+        return $children;
276
+    }
277
+
278
+    /**
279
+     * Returns the folder tree of the user
280
+     *
281
+     * @param string $path The path relative to the user folder
282
+     * @param int $depth The depth of the tree
283
+     *
284
+     * @return JSONResponse<Http::STATUS_OK, FilesFolderTree, array{}>|JSONResponse<Http::STATUS_UNAUTHORIZED|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
285
+     *
286
+     * 200: Folder tree returned successfully
287
+     * 400: Invalid folder path
288
+     * 401: Unauthorized
289
+     * 404: Folder not found
290
+     */
291
+    #[NoAdminRequired]
292
+    #[ApiRoute(verb: 'GET', url: '/api/v1/folder-tree')]
293
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
294
+    public function getFolderTree(string $path = '/', int $depth = 1): JSONResponse {
295
+        $user = $this->userSession->getUser();
296
+        if (!($user instanceof IUser)) {
297
+            return new JSONResponse([
298
+                'message' => $this->l10n->t('Failed to authorize'),
299
+            ], Http::STATUS_UNAUTHORIZED);
300
+        }
301
+        try {
302
+            $userFolder = $this->rootFolder->getUserFolder($user->getUID());
303
+            $userFolderPath = $userFolder->getPath();
304
+            $fullPath = implode('/', [$userFolderPath, trim($path, '/')]);
305
+            $node = $this->rootFolder->get($fullPath);
306
+            if (!($node instanceof Folder)) {
307
+                return new JSONResponse([
308
+                    'message' => $this->l10n->t('Invalid folder path'),
309
+                ], Http::STATUS_BAD_REQUEST);
310
+            }
311
+            $nodes = $node->getDirectoryListing();
312
+            $tree = $this->getChildren($nodes, $depth);
313
+        } catch (NotFoundException $e) {
314
+            return new JSONResponse([
315
+                'message' => $this->l10n->t('Folder not found'),
316
+            ], Http::STATUS_NOT_FOUND);
317
+        } catch (Throwable $th) {
318
+            $this->logger->error($th->getMessage(), ['exception' => $th]);
319
+            $tree = [];
320
+        }
321
+        return new JSONResponse($tree);
322
+    }
323
+
324
+    /**
325
+     * Returns the current logged-in user's storage stats.
326
+     *
327
+     * @param ?string $dir the directory to get the storage stats from
328
+     * @return JSONResponse
329
+     */
330
+    #[NoAdminRequired]
331
+    public function getStorageStats($dir = '/'): JSONResponse {
332
+        $storageInfo = \OC_Helper::getStorageInfo($dir ?: '/');
333
+        $response = new JSONResponse(['message' => 'ok', 'data' => $storageInfo]);
334
+        $response->cacheFor(5 * 60);
335
+        return $response;
336
+    }
337
+
338
+    /**
339
+     * Set a user view config
340
+     *
341
+     * @param string $view
342
+     * @param string $key
343
+     * @param string|bool $value
344
+     * @return JSONResponse
345
+     */
346
+    #[NoAdminRequired]
347
+    public function setViewConfig(string $view, string $key, $value): JSONResponse {
348
+        try {
349
+            $this->viewConfig->setConfig($view, $key, (string)$value);
350
+        } catch (\InvalidArgumentException $e) {
351
+            return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
352
+        }
353
+
354
+        return new JSONResponse(['message' => 'ok', 'data' => $this->viewConfig->getConfig($view)]);
355
+    }
356
+
357
+
358
+    /**
359
+     * Get the user view config
360
+     *
361
+     * @return JSONResponse
362
+     */
363
+    #[NoAdminRequired]
364
+    public function getViewConfigs(): JSONResponse {
365
+        return new JSONResponse(['message' => 'ok', 'data' => $this->viewConfig->getConfigs()]);
366
+    }
367
+
368
+    /**
369
+     * Set a user config
370
+     *
371
+     * @param string $key
372
+     * @param string|bool $value
373
+     * @return JSONResponse
374
+     */
375
+    #[NoAdminRequired]
376
+    public function setConfig(string $key, $value): JSONResponse {
377
+        try {
378
+            $this->userConfig->setConfig($key, (string)$value);
379
+        } catch (\InvalidArgumentException $e) {
380
+            return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
381
+        }
382
+
383
+        return new JSONResponse(['message' => 'ok', 'data' => ['key' => $key, 'value' => $value]]);
384
+    }
385
+
386
+
387
+    /**
388
+     * Get the user config
389
+     *
390
+     * @return JSONResponse
391
+     */
392
+    #[NoAdminRequired]
393
+    public function getConfigs(): JSONResponse {
394
+        return new JSONResponse(['message' => 'ok', 'data' => $this->userConfig->getConfigs()]);
395
+    }
396
+
397
+    /**
398
+     * Toggle default for showing/hiding hidden files
399
+     *
400
+     * @param bool $value
401
+     * @return Response
402
+     * @throws PreConditionNotMetException
403
+     */
404
+    #[NoAdminRequired]
405
+    public function showHiddenFiles(bool $value): Response {
406
+        $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', $value ? '1' : '0');
407
+        return new Response();
408
+    }
409
+
410
+    /**
411
+     * Toggle default for cropping preview images
412
+     *
413
+     * @param bool $value
414
+     * @return Response
415
+     * @throws PreConditionNotMetException
416
+     */
417
+    #[NoAdminRequired]
418
+    public function cropImagePreviews(bool $value): Response {
419
+        $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'crop_image_previews', $value ? '1' : '0');
420
+        return new Response();
421
+    }
422
+
423
+    /**
424
+     * Toggle default for files grid view
425
+     *
426
+     * @param bool $show
427
+     * @return Response
428
+     * @throws PreConditionNotMetException
429
+     */
430
+    #[NoAdminRequired]
431
+    public function showGridView(bool $show): Response {
432
+        $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_grid', $show ? '1' : '0');
433
+        return new Response();
434
+    }
435
+
436
+    /**
437
+     * Get default settings for the grid view
438
+     */
439
+    #[NoAdminRequired]
440
+    public function getGridView() {
441
+        $status = $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'show_grid', '0') === '1';
442
+        return new JSONResponse(['gridview' => $status]);
443
+    }
444
+
445
+    #[PublicPage]
446
+    #[NoCSRFRequired]
447
+    #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
448
+    public function serviceWorker(): StreamResponse {
449
+        $response = new StreamResponse(__DIR__ . '/../../../../dist/preview-service-worker.js');
450
+        $response->setHeaders([
451
+            'Content-Type' => 'application/javascript',
452
+            'Service-Worker-Allowed' => '/'
453
+        ]);
454
+        $policy = new ContentSecurityPolicy();
455
+        $policy->addAllowedWorkerSrcDomain("'self'");
456
+        $policy->addAllowedScriptDomain("'self'");
457
+        $policy->addAllowedConnectDomain("'self'");
458
+        $response->setContentSecurityPolicy($policy);
459
+        return $response;
460
+    }
461 461
 }
Please login to merge, or discard this patch.
apps/files_sharing/lib/MountProvider.php 1 patch
Indentation   +326 added lines, -326 removed lines patch added patch discarded remove patch
@@ -27,330 +27,330 @@
 block discarded – undo
27 27
 use function count;
28 28
 
29 29
 class MountProvider implements IMountProvider {
30
-	/**
31
-	 * @param IConfig $config
32
-	 * @param IManager $shareManager
33
-	 * @param LoggerInterface $logger
34
-	 */
35
-	public function __construct(
36
-		protected IConfig $config,
37
-		protected IManager $shareManager,
38
-		protected LoggerInterface $logger,
39
-		protected IEventDispatcher $eventDispatcher,
40
-		protected ICacheFactory $cacheFactory,
41
-		protected IMountManager $mountManager,
42
-	) {
43
-	}
44
-
45
-	/**
46
-	 * Get all mountpoints applicable for the user and check for shares where we need to update the etags
47
-	 *
48
-	 * @param IUser $user
49
-	 * @param IStorageFactory $loader
50
-	 * @return IMountPoint[]
51
-	 */
52
-	public function getMountsForUser(IUser $user, IStorageFactory $loader) {
53
-		$userId = $user->getUID();
54
-		$shares = array_merge(
55
-			$this->shareManager->getSharedWith($userId, IShare::TYPE_USER, null, -1),
56
-			$this->shareManager->getSharedWith($userId, IShare::TYPE_GROUP, null, -1),
57
-			$this->shareManager->getSharedWith($userId, IShare::TYPE_CIRCLE, null, -1),
58
-			$this->shareManager->getSharedWith($userId, IShare::TYPE_ROOM, null, -1),
59
-			$this->shareManager->getSharedWith($userId, IShare::TYPE_DECK, null, -1),
60
-		);
61
-
62
-		$shares = $this->filterShares($shares, $userId);
63
-		$superShares = $this->buildSuperShares($shares, $user);
64
-
65
-		return $this->getMountsFromSuperShares($userId, $superShares, $loader, $user);
66
-	}
67
-
68
-	/**
69
-	 * Groups shares by path (nodeId) and target path
70
-	 *
71
-	 * @param IShare[] $shares
72
-	 * @return IShare[][] array of grouped shares, each element in the
73
-	 *                    array is a group which itself is an array of shares
74
-	 */
75
-	private function groupShares(array $shares) {
76
-		$tmp = [];
77
-
78
-		foreach ($shares as $share) {
79
-			$nodeId = $share->getNodeId();
80
-			if (!isset($tmp[$nodeId])) {
81
-				$tmp[$nodeId] = [];
82
-			}
83
-			$tmp[$nodeId][] = $share;
84
-		}
85
-
86
-		$result = [];
87
-		// sort by stime, the super share will be based on the least recent share
88
-		foreach ($tmp as &$tmp2) {
89
-			@usort($tmp2, function ($a, $b) {
90
-				$aTime = $a->getShareTime()->getTimestamp();
91
-				$bTime = $b->getShareTime()->getTimestamp();
92
-				if ($aTime === $bTime) {
93
-					return $a->getId() < $b->getId() ? -1 : 1;
94
-				}
95
-				return $aTime < $bTime ? -1 : 1;
96
-			});
97
-			$result[] = $tmp2;
98
-		}
99
-
100
-		return $result;
101
-	}
102
-
103
-	/**
104
-	 * Groups shares by node ID and builds a new share object (super share)
105
-	 * which represents a summarized version of all the shares in the group.
106
-	 *
107
-	 * The permissions and attributes of the super share are accumulated from
108
-	 * the shares in the group, forming the most permissive combination
109
-	 * possible.
110
-	 *
111
-	 * @param IShare[] $allShares
112
-	 * @param IUser $user user
113
-	 * @return list<array{IShare, array<IShare>}> Tuple of [superShare, groupedShares]
114
-	 */
115
-	private function buildSuperShares(array $allShares, IUser $user) {
116
-		$result = [];
117
-
118
-		$groupedShares = $this->groupShares($allShares);
119
-
120
-		foreach ($groupedShares as $shares) {
121
-			if (count($shares) === 0) {
122
-				continue;
123
-			}
124
-
125
-			$superShare = $this->shareManager->newShare();
126
-
127
-			// compute super share based on first entry of the group
128
-			$superShare->setId($shares[0]->getId())
129
-				->setShareOwner($shares[0]->getShareOwner())
130
-				->setNodeId($shares[0]->getNodeId())
131
-				->setShareType($shares[0]->getShareType())
132
-				->setTarget($shares[0]->getTarget());
133
-
134
-			$this->combineNotes($shares, $superShare);
135
-
136
-			// use most permissive permissions
137
-			// this covers the case where there are multiple shares for the same
138
-			// file e.g. from different groups and different permissions
139
-			$superPermissions = 0;
140
-			$superAttributes = $this->shareManager->newShare()->newAttributes();
141
-			$status = IShare::STATUS_PENDING;
142
-			foreach ($shares as $share) {
143
-				$status = max($status, $share->getStatus());
144
-				// update permissions
145
-				$superPermissions |= $share->getPermissions();
146
-
147
-				// update share permission attributes
148
-				$attributes = $share->getAttributes();
149
-				if ($attributes !== null) {
150
-					$this->mergeAttributes($attributes, $superAttributes);
151
-				}
152
-
153
-				$this->adjustTarget($share, $superShare, $user);
154
-				if ($share->getNodeCacheEntry() !== null) {
155
-					$superShare->setNodeCacheEntry($share->getNodeCacheEntry());
156
-				}
157
-			}
158
-
159
-			$superShare->setPermissions($superPermissions);
160
-			$superShare->setStatus($status);
161
-			$superShare->setAttributes($superAttributes);
162
-
163
-			$result[] = [$superShare, $shares];
164
-		}
165
-
166
-		return $result;
167
-	}
168
-
169
-	/**
170
-	 * Combines $attributes into the most permissive set of attributes and
171
-	 * sets them in $superAttributes.
172
-	 */
173
-	private function mergeAttributes(
174
-		IAttributes $attributes,
175
-		IAttributes $superAttributes,
176
-	): void {
177
-		foreach ($attributes->toArray() as $attribute) {
178
-			if ($superAttributes->getAttribute(
179
-				$attribute['scope'],
180
-				$attribute['key']
181
-			) === true) {
182
-				// if super share attribute is already enabled, it is most permissive
183
-				continue;
184
-			}
185
-			// update super share attributes with subshare attribute
186
-			$superAttributes->setAttribute(
187
-				$attribute['scope'],
188
-				$attribute['key'],
189
-				$attribute['value']
190
-			);
191
-		}
192
-	}
193
-
194
-	/**
195
-	 * Gather notes from all the shares. Since these are readily available
196
-	 * here, storing them enables the DAV FilesPlugin to avoid executing many
197
-	 * DB queries to retrieve the same information.
198
-	 *
199
-	 * @param array<IShare> $shares
200
-	 * @param IShare $superShare
201
-	 * @return void
202
-	 */
203
-	private function combineNotes(
204
-		array &$shares,
205
-		IShare $superShare,
206
-	): void {
207
-		$allNotes = implode(
208
-			"\n",
209
-			array_map(static fn ($sh) => $sh->getNote(), $shares)
210
-		);
211
-		$superShare->setNote($allNotes);
212
-	}
213
-
214
-	/**
215
-	 * Adjusts the target in $share for DB consistency, if needed.
216
-	 */
217
-	private function adjustTarget(
218
-		IShare $share,
219
-		IShare $superShare,
220
-		IUser $user,
221
-	): void {
222
-		if ($share->getTarget() === $superShare->getTarget()) {
223
-			return;
224
-		}
225
-
226
-		$share->setTarget($superShare->getTarget());
227
-		try {
228
-			$this->shareManager->moveShare($share, $user->getUID());
229
-		} catch (InvalidArgumentException $e) {
230
-			// ignore as it is not important and we don't want to
231
-			// block FS setup
232
-
233
-			// the subsequent code anyway only uses the target of the
234
-			// super share
235
-
236
-			// such issue can usually happen when dealing with
237
-			// null groups which usually appear with group backend
238
-			// caching inconsistencies
239
-			$this->logger->debug(
240
-				'Could not adjust share target for share ' . $share->getId(
241
-				) . ' to make it consistent: ' . $e->getMessage(),
242
-				['app' => 'files_sharing']
243
-			);
244
-		}
245
-	}
246
-	/**
247
-	 * @param string $userId
248
-	 * @param array $superShares
249
-	 * @param IStorageFactory $loader
250
-	 * @param IUser $user
251
-	 * @return array
252
-	 * @throws Exception
253
-	 */
254
-	private function getMountsFromSuperShares(
255
-		string $userId,
256
-		array $superShares,
257
-		IStorageFactory $loader,
258
-		IUser $user,
259
-	): array {
260
-		$allMounts = $this->mountManager->getAll();
261
-		$mounts = [];
262
-		$view = new View('/' . $userId . '/files');
263
-		$ownerViews = [];
264
-		$sharingDisabledForUser
265
-			= $this->shareManager->sharingDisabledForUser($userId);
266
-		/** @var CappedMemoryCache<bool> $folderExistCache */
267
-		$foldersExistCache = new CappedMemoryCache();
268
-
269
-		$validShareCache
270
-			= $this->cacheFactory->createLocal('share-valid-mountpoint-max');
271
-		$maxValidatedShare = $validShareCache->get($userId) ?? 0;
272
-		$newMaxValidatedShare = $maxValidatedShare;
273
-
274
-		foreach ($superShares as $share) {
275
-			[$parentShare, $groupedShares] = $share;
276
-			try {
277
-				if ($parentShare->getStatus() !== IShare::STATUS_ACCEPTED
278
-					&& ($parentShare->getShareType() === IShare::TYPE_GROUP
279
-						|| $parentShare->getShareType() === IShare::TYPE_USERGROUP
280
-						|| $parentShare->getShareType() === IShare::TYPE_USER)
281
-				) {
282
-					continue;
283
-				}
284
-
285
-				$owner = $parentShare->getShareOwner();
286
-				if (!isset($ownerViews[$owner])) {
287
-					$ownerViews[$owner] = new View('/' . $owner . '/files');
288
-				}
289
-				$shareId = (int)$parentShare->getId();
290
-				$mount = new SharedMount(
291
-					'\OCA\Files_Sharing\SharedStorage',
292
-					$allMounts,
293
-					[
294
-						'user' => $userId,
295
-						// parent share
296
-						'superShare' => $parentShare,
297
-						// children/component of the superShare
298
-						'groupedShares' => $groupedShares,
299
-						'ownerView' => $ownerViews[$owner],
300
-						'sharingDisabledForUser' => $sharingDisabledForUser
301
-					],
302
-					$loader,
303
-					$view,
304
-					$foldersExistCache,
305
-					$this->eventDispatcher,
306
-					$user,
307
-					$shareId <= $maxValidatedShare,
308
-				);
309
-
310
-				$newMaxValidatedShare = max($shareId, $newMaxValidatedShare);
311
-
312
-				$event = new ShareMountedEvent($mount);
313
-				$this->eventDispatcher->dispatchTyped($event);
314
-
315
-				$mounts[$mount->getMountPoint()]
316
-				= $allMounts[$mount->getMountPoint()] = $mount;
317
-				foreach ($event->getAdditionalMounts() as $additionalMount) {
318
-					$allMounts[$additionalMount->getMountPoint()]
319
-					= $mounts[$additionalMount->getMountPoint()]
320
-						= $additionalMount;
321
-				}
322
-			} catch (Exception $e) {
323
-				$this->logger->error(
324
-					'Error while trying to create shared mount',
325
-					[
326
-						'app' => 'files_sharing',
327
-						'exception' => $e,
328
-					],
329
-				);
330
-			}
331
-		}
332
-
333
-		$validShareCache->set($userId, $newMaxValidatedShare, 24 * 60 * 60);
334
-
335
-		// array_filter removes the null values from the array
336
-		return array_values(array_filter($mounts));
337
-	}
338
-
339
-	/**
340
-	 * Filters out shares owned or shared by the user and ones for which the
341
-	 * user has no permissions.
342
-	 *
343
-	 * @param IShare[] $shares
344
-	 * @return IShare[]
345
-	 */
346
-	private function filterShares(array $shares, string $userId): array {
347
-		return array_filter(
348
-			$shares,
349
-			static function (IShare $share) use ($userId) {
350
-				return $share->getPermissions() > 0
351
-					&& $share->getShareOwner() !== $userId
352
-					&& $share->getSharedBy() !== $userId;
353
-			}
354
-		);
355
-	}
30
+    /**
31
+     * @param IConfig $config
32
+     * @param IManager $shareManager
33
+     * @param LoggerInterface $logger
34
+     */
35
+    public function __construct(
36
+        protected IConfig $config,
37
+        protected IManager $shareManager,
38
+        protected LoggerInterface $logger,
39
+        protected IEventDispatcher $eventDispatcher,
40
+        protected ICacheFactory $cacheFactory,
41
+        protected IMountManager $mountManager,
42
+    ) {
43
+    }
44
+
45
+    /**
46
+     * Get all mountpoints applicable for the user and check for shares where we need to update the etags
47
+     *
48
+     * @param IUser $user
49
+     * @param IStorageFactory $loader
50
+     * @return IMountPoint[]
51
+     */
52
+    public function getMountsForUser(IUser $user, IStorageFactory $loader) {
53
+        $userId = $user->getUID();
54
+        $shares = array_merge(
55
+            $this->shareManager->getSharedWith($userId, IShare::TYPE_USER, null, -1),
56
+            $this->shareManager->getSharedWith($userId, IShare::TYPE_GROUP, null, -1),
57
+            $this->shareManager->getSharedWith($userId, IShare::TYPE_CIRCLE, null, -1),
58
+            $this->shareManager->getSharedWith($userId, IShare::TYPE_ROOM, null, -1),
59
+            $this->shareManager->getSharedWith($userId, IShare::TYPE_DECK, null, -1),
60
+        );
61
+
62
+        $shares = $this->filterShares($shares, $userId);
63
+        $superShares = $this->buildSuperShares($shares, $user);
64
+
65
+        return $this->getMountsFromSuperShares($userId, $superShares, $loader, $user);
66
+    }
67
+
68
+    /**
69
+     * Groups shares by path (nodeId) and target path
70
+     *
71
+     * @param IShare[] $shares
72
+     * @return IShare[][] array of grouped shares, each element in the
73
+     *                    array is a group which itself is an array of shares
74
+     */
75
+    private function groupShares(array $shares) {
76
+        $tmp = [];
77
+
78
+        foreach ($shares as $share) {
79
+            $nodeId = $share->getNodeId();
80
+            if (!isset($tmp[$nodeId])) {
81
+                $tmp[$nodeId] = [];
82
+            }
83
+            $tmp[$nodeId][] = $share;
84
+        }
85
+
86
+        $result = [];
87
+        // sort by stime, the super share will be based on the least recent share
88
+        foreach ($tmp as &$tmp2) {
89
+            @usort($tmp2, function ($a, $b) {
90
+                $aTime = $a->getShareTime()->getTimestamp();
91
+                $bTime = $b->getShareTime()->getTimestamp();
92
+                if ($aTime === $bTime) {
93
+                    return $a->getId() < $b->getId() ? -1 : 1;
94
+                }
95
+                return $aTime < $bTime ? -1 : 1;
96
+            });
97
+            $result[] = $tmp2;
98
+        }
99
+
100
+        return $result;
101
+    }
102
+
103
+    /**
104
+     * Groups shares by node ID and builds a new share object (super share)
105
+     * which represents a summarized version of all the shares in the group.
106
+     *
107
+     * The permissions and attributes of the super share are accumulated from
108
+     * the shares in the group, forming the most permissive combination
109
+     * possible.
110
+     *
111
+     * @param IShare[] $allShares
112
+     * @param IUser $user user
113
+     * @return list<array{IShare, array<IShare>}> Tuple of [superShare, groupedShares]
114
+     */
115
+    private function buildSuperShares(array $allShares, IUser $user) {
116
+        $result = [];
117
+
118
+        $groupedShares = $this->groupShares($allShares);
119
+
120
+        foreach ($groupedShares as $shares) {
121
+            if (count($shares) === 0) {
122
+                continue;
123
+            }
124
+
125
+            $superShare = $this->shareManager->newShare();
126
+
127
+            // compute super share based on first entry of the group
128
+            $superShare->setId($shares[0]->getId())
129
+                ->setShareOwner($shares[0]->getShareOwner())
130
+                ->setNodeId($shares[0]->getNodeId())
131
+                ->setShareType($shares[0]->getShareType())
132
+                ->setTarget($shares[0]->getTarget());
133
+
134
+            $this->combineNotes($shares, $superShare);
135
+
136
+            // use most permissive permissions
137
+            // this covers the case where there are multiple shares for the same
138
+            // file e.g. from different groups and different permissions
139
+            $superPermissions = 0;
140
+            $superAttributes = $this->shareManager->newShare()->newAttributes();
141
+            $status = IShare::STATUS_PENDING;
142
+            foreach ($shares as $share) {
143
+                $status = max($status, $share->getStatus());
144
+                // update permissions
145
+                $superPermissions |= $share->getPermissions();
146
+
147
+                // update share permission attributes
148
+                $attributes = $share->getAttributes();
149
+                if ($attributes !== null) {
150
+                    $this->mergeAttributes($attributes, $superAttributes);
151
+                }
152
+
153
+                $this->adjustTarget($share, $superShare, $user);
154
+                if ($share->getNodeCacheEntry() !== null) {
155
+                    $superShare->setNodeCacheEntry($share->getNodeCacheEntry());
156
+                }
157
+            }
158
+
159
+            $superShare->setPermissions($superPermissions);
160
+            $superShare->setStatus($status);
161
+            $superShare->setAttributes($superAttributes);
162
+
163
+            $result[] = [$superShare, $shares];
164
+        }
165
+
166
+        return $result;
167
+    }
168
+
169
+    /**
170
+     * Combines $attributes into the most permissive set of attributes and
171
+     * sets them in $superAttributes.
172
+     */
173
+    private function mergeAttributes(
174
+        IAttributes $attributes,
175
+        IAttributes $superAttributes,
176
+    ): void {
177
+        foreach ($attributes->toArray() as $attribute) {
178
+            if ($superAttributes->getAttribute(
179
+                $attribute['scope'],
180
+                $attribute['key']
181
+            ) === true) {
182
+                // if super share attribute is already enabled, it is most permissive
183
+                continue;
184
+            }
185
+            // update super share attributes with subshare attribute
186
+            $superAttributes->setAttribute(
187
+                $attribute['scope'],
188
+                $attribute['key'],
189
+                $attribute['value']
190
+            );
191
+        }
192
+    }
193
+
194
+    /**
195
+     * Gather notes from all the shares. Since these are readily available
196
+     * here, storing them enables the DAV FilesPlugin to avoid executing many
197
+     * DB queries to retrieve the same information.
198
+     *
199
+     * @param array<IShare> $shares
200
+     * @param IShare $superShare
201
+     * @return void
202
+     */
203
+    private function combineNotes(
204
+        array &$shares,
205
+        IShare $superShare,
206
+    ): void {
207
+        $allNotes = implode(
208
+            "\n",
209
+            array_map(static fn ($sh) => $sh->getNote(), $shares)
210
+        );
211
+        $superShare->setNote($allNotes);
212
+    }
213
+
214
+    /**
215
+     * Adjusts the target in $share for DB consistency, if needed.
216
+     */
217
+    private function adjustTarget(
218
+        IShare $share,
219
+        IShare $superShare,
220
+        IUser $user,
221
+    ): void {
222
+        if ($share->getTarget() === $superShare->getTarget()) {
223
+            return;
224
+        }
225
+
226
+        $share->setTarget($superShare->getTarget());
227
+        try {
228
+            $this->shareManager->moveShare($share, $user->getUID());
229
+        } catch (InvalidArgumentException $e) {
230
+            // ignore as it is not important and we don't want to
231
+            // block FS setup
232
+
233
+            // the subsequent code anyway only uses the target of the
234
+            // super share
235
+
236
+            // such issue can usually happen when dealing with
237
+            // null groups which usually appear with group backend
238
+            // caching inconsistencies
239
+            $this->logger->debug(
240
+                'Could not adjust share target for share ' . $share->getId(
241
+                ) . ' to make it consistent: ' . $e->getMessage(),
242
+                ['app' => 'files_sharing']
243
+            );
244
+        }
245
+    }
246
+    /**
247
+     * @param string $userId
248
+     * @param array $superShares
249
+     * @param IStorageFactory $loader
250
+     * @param IUser $user
251
+     * @return array
252
+     * @throws Exception
253
+     */
254
+    private function getMountsFromSuperShares(
255
+        string $userId,
256
+        array $superShares,
257
+        IStorageFactory $loader,
258
+        IUser $user,
259
+    ): array {
260
+        $allMounts = $this->mountManager->getAll();
261
+        $mounts = [];
262
+        $view = new View('/' . $userId . '/files');
263
+        $ownerViews = [];
264
+        $sharingDisabledForUser
265
+            = $this->shareManager->sharingDisabledForUser($userId);
266
+        /** @var CappedMemoryCache<bool> $folderExistCache */
267
+        $foldersExistCache = new CappedMemoryCache();
268
+
269
+        $validShareCache
270
+            = $this->cacheFactory->createLocal('share-valid-mountpoint-max');
271
+        $maxValidatedShare = $validShareCache->get($userId) ?? 0;
272
+        $newMaxValidatedShare = $maxValidatedShare;
273
+
274
+        foreach ($superShares as $share) {
275
+            [$parentShare, $groupedShares] = $share;
276
+            try {
277
+                if ($parentShare->getStatus() !== IShare::STATUS_ACCEPTED
278
+                    && ($parentShare->getShareType() === IShare::TYPE_GROUP
279
+                        || $parentShare->getShareType() === IShare::TYPE_USERGROUP
280
+                        || $parentShare->getShareType() === IShare::TYPE_USER)
281
+                ) {
282
+                    continue;
283
+                }
284
+
285
+                $owner = $parentShare->getShareOwner();
286
+                if (!isset($ownerViews[$owner])) {
287
+                    $ownerViews[$owner] = new View('/' . $owner . '/files');
288
+                }
289
+                $shareId = (int)$parentShare->getId();
290
+                $mount = new SharedMount(
291
+                    '\OCA\Files_Sharing\SharedStorage',
292
+                    $allMounts,
293
+                    [
294
+                        'user' => $userId,
295
+                        // parent share
296
+                        'superShare' => $parentShare,
297
+                        // children/component of the superShare
298
+                        'groupedShares' => $groupedShares,
299
+                        'ownerView' => $ownerViews[$owner],
300
+                        'sharingDisabledForUser' => $sharingDisabledForUser
301
+                    ],
302
+                    $loader,
303
+                    $view,
304
+                    $foldersExistCache,
305
+                    $this->eventDispatcher,
306
+                    $user,
307
+                    $shareId <= $maxValidatedShare,
308
+                );
309
+
310
+                $newMaxValidatedShare = max($shareId, $newMaxValidatedShare);
311
+
312
+                $event = new ShareMountedEvent($mount);
313
+                $this->eventDispatcher->dispatchTyped($event);
314
+
315
+                $mounts[$mount->getMountPoint()]
316
+                = $allMounts[$mount->getMountPoint()] = $mount;
317
+                foreach ($event->getAdditionalMounts() as $additionalMount) {
318
+                    $allMounts[$additionalMount->getMountPoint()]
319
+                    = $mounts[$additionalMount->getMountPoint()]
320
+                        = $additionalMount;
321
+                }
322
+            } catch (Exception $e) {
323
+                $this->logger->error(
324
+                    'Error while trying to create shared mount',
325
+                    [
326
+                        'app' => 'files_sharing',
327
+                        'exception' => $e,
328
+                    ],
329
+                );
330
+            }
331
+        }
332
+
333
+        $validShareCache->set($userId, $newMaxValidatedShare, 24 * 60 * 60);
334
+
335
+        // array_filter removes the null values from the array
336
+        return array_values(array_filter($mounts));
337
+    }
338
+
339
+    /**
340
+     * Filters out shares owned or shared by the user and ones for which the
341
+     * user has no permissions.
342
+     *
343
+     * @param IShare[] $shares
344
+     * @return IShare[]
345
+     */
346
+    private function filterShares(array $shares, string $userId): array {
347
+        return array_filter(
348
+            $shares,
349
+            static function (IShare $share) use ($userId) {
350
+                return $share->getPermissions() > 0
351
+                    && $share->getShareOwner() !== $userId
352
+                    && $share->getSharedBy() !== $userId;
353
+            }
354
+        );
355
+    }
356 356
 }
Please login to merge, or discard this patch.
apps/files_sharing/lib/Controller/ShareAPIController.php 2 patches
Indentation   +2172 added lines, -2172 removed lines patch added patch discarded remove patch
@@ -77,2179 +77,2179 @@
 block discarded – undo
77 77
  */
78 78
 class ShareAPIController extends OCSController {
79 79
 
80
-	private ?Node $lockedNode = null;
81
-	private array $trustedServerCache = [];
82
-
83
-	/**
84
-	 * Share20OCS constructor.
85
-	 */
86
-	public function __construct(
87
-		string $appName,
88
-		IRequest $request,
89
-		private IManager $shareManager,
90
-		private IGroupManager $groupManager,
91
-		private IUserManager $userManager,
92
-		private IRootFolder $rootFolder,
93
-		private IURLGenerator $urlGenerator,
94
-		private IL10N $l,
95
-		private IConfig $config,
96
-		private IAppConfig $appConfig,
97
-		private IAppManager $appManager,
98
-		private ContainerInterface $serverContainer,
99
-		private IUserStatusManager $userStatusManager,
100
-		private IPreview $previewManager,
101
-		private IDateTimeZone $dateTimeZone,
102
-		private LoggerInterface $logger,
103
-		private IProviderFactory $factory,
104
-		private IMailer $mailer,
105
-		private ITagManager $tagManager,
106
-		private IEmailValidator $emailValidator,
107
-		private ?TrustedServers $trustedServers,
108
-		private ?string $userId = null,
109
-	) {
110
-		parent::__construct($appName, $request);
111
-	}
112
-
113
-	/**
114
-	 * Convert an IShare to an array for OCS output
115
-	 *
116
-	 * @param IShare $share
117
-	 * @param Node|null $recipientNode
118
-	 * @return Files_SharingShare
119
-	 * @throws NotFoundException In case the node can't be resolved.
120
-	 *
121
-	 * @suppress PhanUndeclaredClassMethod
122
-	 */
123
-	protected function formatShare(IShare $share, ?Node $recipientNode = null): array {
124
-		$sharedBy = $this->userManager->get($share->getSharedBy());
125
-		$shareOwner = $this->userManager->get($share->getShareOwner());
126
-
127
-		$isOwnShare = false;
128
-		if ($shareOwner !== null) {
129
-			$isOwnShare = $shareOwner->getUID() === $this->userId;
130
-		}
131
-
132
-		$result = [
133
-			'id' => $share->getId(),
134
-			'share_type' => $share->getShareType(),
135
-			'uid_owner' => $share->getSharedBy(),
136
-			'displayname_owner' => $sharedBy !== null ? $sharedBy->getDisplayName() : $share->getSharedBy(),
137
-			// recipient permissions
138
-			'permissions' => $share->getPermissions(),
139
-			// current user permissions on this share
140
-			'can_edit' => $this->canEditShare($share),
141
-			'can_delete' => $this->canDeleteShare($share),
142
-			'stime' => $share->getShareTime()->getTimestamp(),
143
-			'parent' => null,
144
-			'expiration' => null,
145
-			'token' => null,
146
-			'uid_file_owner' => $share->getShareOwner(),
147
-			'note' => $share->getNote(),
148
-			'label' => $share->getLabel(),
149
-			'displayname_file_owner' => $shareOwner !== null ? $shareOwner->getDisplayName() : $share->getShareOwner(),
150
-		];
151
-
152
-		$userFolder = $this->rootFolder->getUserFolder($this->userId);
153
-		if ($recipientNode) {
154
-			$node = $recipientNode;
155
-		} else {
156
-			$node = $userFolder->getFirstNodeById($share->getNodeId());
157
-			if (!$node) {
158
-				// fallback to guessing the path
159
-				$node = $userFolder->get($share->getTarget());
160
-				if ($node === null || $share->getTarget() === '') {
161
-					throw new NotFoundException();
162
-				}
163
-			}
164
-		}
165
-
166
-		$result['path'] = $userFolder->getRelativePath($node->getPath());
167
-		if ($node instanceof Folder) {
168
-			$result['item_type'] = 'folder';
169
-		} else {
170
-			$result['item_type'] = 'file';
171
-		}
172
-
173
-		// Get the original node permission if the share owner is the current user
174
-		if ($isOwnShare) {
175
-			$result['item_permissions'] = $node->getPermissions();
176
-		}
177
-
178
-		// If we're on the recipient side, the node permissions
179
-		// are bound to the share permissions. So we need to
180
-		// adjust the permissions to the share permissions if necessary.
181
-		if (!$isOwnShare) {
182
-			$result['item_permissions'] = $share->getPermissions();
183
-
184
-			// For some reason, single files share are forbidden to have the delete permission
185
-			// since we have custom methods to check those, let's adjust straight away.
186
-			// DAV permissions does not have that issue though.
187
-			if ($this->canDeleteShare($share) || $this->canDeleteShareFromSelf($share)) {
188
-				$result['item_permissions'] |= Constants::PERMISSION_DELETE;
189
-			}
190
-			if ($this->canEditShare($share)) {
191
-				$result['item_permissions'] |= Constants::PERMISSION_UPDATE;
192
-			}
193
-		}
194
-
195
-		// See MOUNT_ROOT_PROPERTYNAME dav property
196
-		$result['is-mount-root'] = $node->getInternalPath() === '';
197
-		$result['mount-type'] = $node->getMountPoint()->getMountType();
198
-
199
-		$result['mimetype'] = $node->getMimetype();
200
-		$result['has_preview'] = $this->previewManager->isAvailable($node);
201
-		$result['storage_id'] = $node->getStorage()->getId();
202
-		$result['storage'] = $node->getStorage()->getCache()->getNumericStorageId();
203
-		$result['item_source'] = $node->getId();
204
-		$result['file_source'] = $node->getId();
205
-		$result['file_parent'] = $node->getParent()->getId();
206
-		$result['file_target'] = $share->getTarget();
207
-		$result['item_size'] = $node->getSize();
208
-		$result['item_mtime'] = $node->getMTime();
209
-
210
-		if ($this->trustedServers !== null && in_array($share->getShareType(), [IShare::TYPE_REMOTE, IShare::TYPE_REMOTE_GROUP], true)) {
211
-			$result['is_trusted_server'] = false;
212
-			$sharedWith = $share->getSharedWith();
213
-			$remoteIdentifier = is_string($sharedWith) ? strrchr($sharedWith, '@') : false;
214
-			if ($remoteIdentifier !== false) {
215
-				$remote = substr($remoteIdentifier, 1);
216
-
217
-				if (isset($this->trustedServerCache[$remote])) {
218
-					$result['is_trusted_server'] = $this->trustedServerCache[$remote];
219
-				} else {
220
-					try {
221
-						$isTrusted = $this->trustedServers->isTrustedServer($remote);
222
-						$this->trustedServerCache[$remote] = $isTrusted;
223
-						$result['is_trusted_server'] = $isTrusted;
224
-					} catch (\Exception $e) {
225
-						// Server not found or other issue, we consider it not trusted
226
-						$this->trustedServerCache[$remote] = false;
227
-						$this->logger->error(
228
-							'Error checking if remote server is trusted (treating as untrusted): ' . $e->getMessage(),
229
-							['exception' => $e]
230
-						);
231
-					}
232
-				}
233
-			}
234
-		}
235
-
236
-		$expiration = $share->getExpirationDate();
237
-		if ($expiration !== null) {
238
-			$expiration->setTimezone($this->dateTimeZone->getTimeZone());
239
-			$result['expiration'] = $expiration->format('Y-m-d 00:00:00');
240
-		}
241
-
242
-		if ($share->getShareType() === IShare::TYPE_USER) {
243
-			$sharedWith = $this->userManager->get($share->getSharedWith());
244
-			$result['share_with'] = $share->getSharedWith();
245
-			$result['share_with_displayname'] = $sharedWith !== null ? $sharedWith->getDisplayName() : $share->getSharedWith();
246
-			$result['share_with_displayname_unique'] = $sharedWith !== null ? (
247
-				!empty($sharedWith->getSystemEMailAddress()) ? $sharedWith->getSystemEMailAddress() : $sharedWith->getUID()
248
-			) : $share->getSharedWith();
249
-
250
-			$userStatuses = $this->userStatusManager->getUserStatuses([$share->getSharedWith()]);
251
-			$userStatus = array_shift($userStatuses);
252
-			if ($userStatus) {
253
-				$result['status'] = [
254
-					'status' => $userStatus->getStatus(),
255
-					'message' => $userStatus->getMessage(),
256
-					'icon' => $userStatus->getIcon(),
257
-					'clearAt' => $userStatus->getClearAt()
258
-						? (int)$userStatus->getClearAt()->format('U')
259
-						: null,
260
-				];
261
-			}
262
-		} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
263
-			$group = $this->groupManager->get($share->getSharedWith());
264
-			$result['share_with'] = $share->getSharedWith();
265
-			$result['share_with_displayname'] = $group !== null ? $group->getDisplayName() : $share->getSharedWith();
266
-		} elseif ($share->getShareType() === IShare::TYPE_LINK) {
267
-
268
-			// "share_with" and "share_with_displayname" for passwords of link
269
-			// shares was deprecated in Nextcloud 15, use "password" instead.
270
-			$result['share_with'] = $share->getPassword();
271
-			$result['share_with_displayname'] = '(' . $this->l->t('Shared link') . ')';
272
-
273
-			$result['password'] = $share->getPassword();
274
-
275
-			$result['send_password_by_talk'] = $share->getSendPasswordByTalk();
276
-
277
-			$result['token'] = $share->getToken();
278
-			$result['url'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $share->getToken()]);
279
-		} elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
280
-			$result['share_with'] = $share->getSharedWith();
281
-			$result['share_with_displayname'] = $this->getCachedFederatedDisplayName($share->getSharedWith());
282
-			$result['token'] = $share->getToken();
283
-		} elseif ($share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
284
-			$result['share_with'] = $share->getSharedWith();
285
-			$result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'CLOUD');
286
-			$result['token'] = $share->getToken();
287
-		} elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
288
-			$result['share_with'] = $share->getSharedWith();
289
-			$result['password'] = $share->getPassword();
290
-			$result['password_expiration_time'] = $share->getPasswordExpirationTime() !== null ? $share->getPasswordExpirationTime()->format(\DateTime::ATOM) : null;
291
-			$result['send_password_by_talk'] = $share->getSendPasswordByTalk();
292
-			$result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'EMAIL');
293
-			$result['token'] = $share->getToken();
294
-		} elseif ($share->getShareType() === IShare::TYPE_CIRCLE) {
295
-			// getSharedWith() returns either "name (type, owner)" or
296
-			// "name (type, owner) [id]", depending on the Teams app version.
297
-			$hasCircleId = (substr($share->getSharedWith(), -1) === ']');
298
-
299
-			$result['share_with_displayname'] = $share->getSharedWithDisplayName();
300
-			if (empty($result['share_with_displayname'])) {
301
-				$displayNameLength = ($hasCircleId ? strrpos($share->getSharedWith(), ' ') : strlen($share->getSharedWith()));
302
-				$result['share_with_displayname'] = substr($share->getSharedWith(), 0, $displayNameLength);
303
-			}
304
-
305
-			$result['share_with_avatar'] = $share->getSharedWithAvatar();
306
-
307
-			$shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0);
308
-			$shareWithLength = ($hasCircleId ? -1 : strpos($share->getSharedWith(), ' '));
309
-			if ($shareWithLength === false) {
310
-				$result['share_with'] = substr($share->getSharedWith(), $shareWithStart);
311
-			} else {
312
-				$result['share_with'] = substr($share->getSharedWith(), $shareWithStart, $shareWithLength);
313
-			}
314
-		} elseif ($share->getShareType() === IShare::TYPE_ROOM) {
315
-			$result['share_with'] = $share->getSharedWith();
316
-			$result['share_with_displayname'] = '';
317
-
318
-			try {
319
-				/** @var array{share_with_displayname: string, share_with_link: string, share_with?: string, token?: string} $roomShare */
320
-				$roomShare = $this->getRoomShareHelper()->formatShare($share);
321
-				$result = array_merge($result, $roomShare);
322
-			} catch (ContainerExceptionInterface $e) {
323
-			}
324
-		} elseif ($share->getShareType() === IShare::TYPE_DECK) {
325
-			$result['share_with'] = $share->getSharedWith();
326
-			$result['share_with_displayname'] = '';
327
-
328
-			try {
329
-				/** @var array{share_with: string, share_with_displayname: string, share_with_link: string} $deckShare */
330
-				$deckShare = $this->getDeckShareHelper()->formatShare($share);
331
-				$result = array_merge($result, $deckShare);
332
-			} catch (ContainerExceptionInterface $e) {
333
-			}
334
-		}
335
-
336
-
337
-		$result['mail_send'] = $share->getMailSend() ? 1 : 0;
338
-		$result['hide_download'] = $share->getHideDownload() ? 1 : 0;
339
-
340
-		$result['attributes'] = null;
341
-		if ($attributes = $share->getAttributes()) {
342
-			$result['attributes'] = (string)\json_encode($attributes->toArray());
343
-		}
344
-
345
-		return $result;
346
-	}
347
-
348
-	/**
349
-	 * Check if one of the users address books knows the exact property, if
350
-	 * not we return the full name.
351
-	 *
352
-	 * @param string $query
353
-	 * @param string $property
354
-	 * @return string
355
-	 */
356
-	private function getDisplayNameFromAddressBook(string $query, string $property): string {
357
-		// FIXME: If we inject the contacts manager it gets initialized before any address books are registered
358
-		try {
359
-			$result = Server::get(\OCP\Contacts\IManager::class)->search($query, [$property], [
360
-				'limit' => 1,
361
-				'enumeration' => false,
362
-				'strict_search' => true,
363
-			]);
364
-		} catch (Exception $e) {
365
-			$this->logger->error(
366
-				$e->getMessage(),
367
-				['exception' => $e]
368
-			);
369
-			return $query;
370
-		}
371
-
372
-		foreach ($result as $r) {
373
-			foreach ($r[$property] as $value) {
374
-				if ($value === $query && $r['FN']) {
375
-					return $r['FN'];
376
-				}
377
-			}
378
-		}
379
-
380
-		return $query;
381
-	}
382
-
383
-
384
-	/**
385
-	 * @param list<Files_SharingShare> $shares
386
-	 * @param array<string, string>|null $updatedDisplayName
387
-	 *
388
-	 * @return list<Files_SharingShare>
389
-	 */
390
-	private function fixMissingDisplayName(array $shares, ?array $updatedDisplayName = null): array {
391
-		$userIds = $updated = [];
392
-		foreach ($shares as $share) {
393
-			// share is federated and share have no display name yet
394
-			if ($share['share_type'] === IShare::TYPE_REMOTE
395
-				&& ($share['share_with'] ?? '') !== ''
396
-				&& ($share['share_with_displayname'] ?? '') === '') {
397
-				$userIds[] = $userId = $share['share_with'];
398
-
399
-				if ($updatedDisplayName !== null && array_key_exists($userId, $updatedDisplayName)) {
400
-					$share['share_with_displayname'] = $updatedDisplayName[$userId];
401
-				}
402
-			}
403
-
404
-			// prepping userIds with displayName to be updated
405
-			$updated[] = $share;
406
-		}
407
-
408
-		// if $updatedDisplayName is not null, it means we should have already fixed displayNames of the shares
409
-		if ($updatedDisplayName !== null) {
410
-			return $updated;
411
-		}
412
-
413
-		// get displayName for the generated list of userId with no displayName
414
-		$displayNames = $this->retrieveFederatedDisplayName($userIds);
415
-
416
-		// if no displayName are updated, we exit
417
-		if (empty($displayNames)) {
418
-			return $updated;
419
-		}
420
-
421
-		// let's fix missing display name and returns all shares
422
-		return $this->fixMissingDisplayName($shares, $displayNames);
423
-	}
424
-
425
-
426
-	/**
427
-	 * get displayName of a list of userIds from the lookup-server; through the globalsiteselector app.
428
-	 * returns an array with userIds as keys and displayName as values.
429
-	 *
430
-	 * @param array $userIds
431
-	 * @param bool $cacheOnly - do not reach LUS, get data from cache.
432
-	 *
433
-	 * @return array
434
-	 * @throws ContainerExceptionInterface
435
-	 */
436
-	private function retrieveFederatedDisplayName(array $userIds, bool $cacheOnly = false): array {
437
-		// check if gss is enabled and available
438
-		if (count($userIds) === 0
439
-			|| !$this->appManager->isEnabledForAnyone('globalsiteselector')
440
-			|| !class_exists('\OCA\GlobalSiteSelector\Service\SlaveService')) {
441
-			return [];
442
-		}
443
-
444
-		try {
445
-			$slaveService = Server::get(SlaveService::class);
446
-		} catch (\Throwable $e) {
447
-			$this->logger->error(
448
-				$e->getMessage(),
449
-				['exception' => $e]
450
-			);
451
-			return [];
452
-		}
453
-
454
-		return $slaveService->getUsersDisplayName($userIds, $cacheOnly);
455
-	}
456
-
457
-
458
-	/**
459
-	 * retrieve displayName from cache if available (should be used on federated shares)
460
-	 * if not available in cache/lus, try for get from address-book, else returns empty string.
461
-	 *
462
-	 * @param string $userId
463
-	 * @param bool $cacheOnly if true will not reach the lus but will only get data from cache
464
-	 *
465
-	 * @return string
466
-	 */
467
-	private function getCachedFederatedDisplayName(string $userId, bool $cacheOnly = true): string {
468
-		$details = $this->retrieveFederatedDisplayName([$userId], $cacheOnly);
469
-		if (array_key_exists($userId, $details)) {
470
-			return $details[$userId];
471
-		}
472
-
473
-		$displayName = $this->getDisplayNameFromAddressBook($userId, 'CLOUD');
474
-		return ($displayName === $userId) ? '' : $displayName;
475
-	}
476
-
477
-
478
-
479
-	/**
480
-	 * Get a specific share by id
481
-	 *
482
-	 * @param string $id ID of the share
483
-	 * @param bool $include_tags Include tags in the share
484
-	 * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
485
-	 * @throws OCSNotFoundException Share not found
486
-	 *
487
-	 * 200: Share returned
488
-	 */
489
-	#[NoAdminRequired]
490
-	public function getShare(string $id, bool $include_tags = false): DataResponse {
491
-		try {
492
-			$share = $this->getShareById($id);
493
-		} catch (ShareNotFound $e) {
494
-			throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
495
-		}
496
-
497
-		try {
498
-			if ($this->canAccessShare($share)) {
499
-				$share = $this->formatShare($share);
500
-
501
-				if ($include_tags) {
502
-					$share = $this->populateTags([$share]);
503
-				} else {
504
-					$share = [$share];
505
-				}
506
-
507
-				return new DataResponse($share);
508
-			}
509
-		} catch (NotFoundException $e) {
510
-			// Fall through
511
-		}
512
-
513
-		throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
514
-	}
515
-
516
-	/**
517
-	 * Delete a share
518
-	 *
519
-	 * @param string $id ID of the share
520
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
521
-	 * @throws OCSNotFoundException Share not found
522
-	 * @throws OCSForbiddenException Missing permissions to delete the share
523
-	 *
524
-	 * 200: Share deleted successfully
525
-	 */
526
-	#[NoAdminRequired]
527
-	public function deleteShare(string $id): DataResponse {
528
-		try {
529
-			$share = $this->getShareById($id);
530
-		} catch (ShareNotFound $e) {
531
-			throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
532
-		}
533
-
534
-		try {
535
-			$this->lock($share->getNode());
536
-		} catch (LockedException $e) {
537
-			throw new OCSNotFoundException($this->l->t('Could not delete share'));
538
-		} catch (NotFoundException $e) {
539
-			$this->logger->debug('File of to be deleted share was not found, skip locking', ['exception' => $e]);
540
-		}
541
-
542
-		if (!$this->canAccessShare($share)) {
543
-			throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
544
-		}
545
-
546
-		// if it's a group share or a room share
547
-		// we don't delete the share, but only the
548
-		// mount point. Allowing it to be restored
549
-		// from the deleted shares
550
-		if ($this->canDeleteShareFromSelf($share)) {
551
-			$this->shareManager->deleteFromSelf($share, $this->userId);
552
-		} else {
553
-			if (!$this->canDeleteShare($share)) {
554
-				throw new OCSForbiddenException($this->l->t('Could not delete share'));
555
-			}
556
-
557
-			$this->shareManager->deleteShare($share);
558
-		}
559
-
560
-		return new DataResponse();
561
-	}
562
-
563
-	/**
564
-	 * Create a share
565
-	 *
566
-	 * @param string|null $path Path of the share
567
-	 * @param int|null $permissions Permissions for the share
568
-	 * @param int $shareType Type of the share
569
-	 * @param ?string $shareWith The entity this should be shared with
570
-	 * @param 'true'|'false'|null $publicUpload If public uploading is allowed (deprecated)
571
-	 * @param string $password Password for the share
572
-	 * @param string|null $sendPasswordByTalk Send the password for the share over Talk
573
-	 * @param ?string $expireDate The expiry date of the share in the user's timezone at 00:00.
574
-	 *                            If $expireDate is not supplied or set to `null`, the system default will be used.
575
-	 * @param string $note Note for the share
576
-	 * @param string $label Label for the share (only used in link and email)
577
-	 * @param string|null $attributes Additional attributes for the share
578
-	 * @param 'false'|'true'|null $sendMail Send a mail to the recipient
579
-	 *
580
-	 * @return DataResponse<Http::STATUS_OK, Files_SharingShare, array{}>
581
-	 * @throws OCSBadRequestException Unknown share type
582
-	 * @throws OCSException
583
-	 * @throws OCSForbiddenException Creating the share is not allowed
584
-	 * @throws OCSNotFoundException Creating the share failed
585
-	 * @suppress PhanUndeclaredClassMethod
586
-	 *
587
-	 * 200: Share created
588
-	 */
589
-	#[NoAdminRequired]
590
-	#[UserRateLimit(limit: 20, period: 600)]
591
-	public function createShare(
592
-		?string $path = null,
593
-		?int $permissions = null,
594
-		int $shareType = -1,
595
-		?string $shareWith = null,
596
-		?string $publicUpload = null,
597
-		string $password = '',
598
-		?string $sendPasswordByTalk = null,
599
-		?string $expireDate = null,
600
-		string $note = '',
601
-		string $label = '',
602
-		?string $attributes = null,
603
-		?string $sendMail = null,
604
-	): DataResponse {
605
-		assert($this->userId !== null);
606
-
607
-		$share = $this->shareManager->newShare();
608
-		$hasPublicUpload = $this->getLegacyPublicUpload($publicUpload);
609
-
610
-		// Verify path
611
-		if ($path === null) {
612
-			throw new OCSNotFoundException($this->l->t('Please specify a file or folder path'));
613
-		}
614
-
615
-		$userFolder = $this->rootFolder->getUserFolder($this->userId);
616
-		try {
617
-			/** @var \OC\Files\Node\Node $node */
618
-			$node = $userFolder->get($path);
619
-		} catch (NotFoundException $e) {
620
-			throw new OCSNotFoundException($this->l->t('Wrong path, file/folder does not exist'));
621
-		}
622
-
623
-		// a user can have access to a file through different paths, with differing permissions
624
-		// combine all permissions to determine if the user can share this file
625
-		$nodes = $userFolder->getById($node->getId());
626
-		foreach ($nodes as $nodeById) {
627
-			/** @var FileInfo $fileInfo */
628
-			$fileInfo = $node->getFileInfo();
629
-			$fileInfo['permissions'] |= $nodeById->getPermissions();
630
-		}
631
-
632
-		$share->setNode($node);
633
-
634
-		try {
635
-			$this->lock($share->getNode());
636
-		} catch (LockedException $e) {
637
-			throw new OCSNotFoundException($this->l->t('Could not create share'));
638
-		}
639
-
640
-		// Set permissions
641
-		if ($shareType === IShare::TYPE_LINK || $shareType === IShare::TYPE_EMAIL) {
642
-			$permissions = $this->getLinkSharePermissions($permissions, $hasPublicUpload);
643
-			$this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload);
644
-		} else {
645
-			// Use default permissions only for non-link shares to keep legacy behavior
646
-			if ($permissions === null) {
647
-				$permissions = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL);
648
-			}
649
-			// Non-link shares always require read permissions (link shares could be file drop)
650
-			$permissions |= Constants::PERMISSION_READ;
651
-		}
652
-
653
-		// For legacy reasons the API allows to pass PERMISSIONS_ALL even for single file shares (I look at you Talk)
654
-		if ($node instanceof File) {
655
-			// if this is a single file share we remove the DELETE and CREATE permissions
656
-			$permissions = $permissions & ~(Constants::PERMISSION_DELETE | Constants::PERMISSION_CREATE);
657
-		}
658
-
659
-		/**
660
-		 * Hack for https://github.com/owncloud/core/issues/22587
661
-		 * We check the permissions via webdav. But the permissions of the mount point
662
-		 * do not equal the share permissions. Here we fix that for federated mounts.
663
-		 */
664
-		if ($node->getStorage()->instanceOfStorage(Storage::class)) {
665
-			$permissions &= ~($permissions & ~$node->getPermissions());
666
-		}
667
-
668
-		if ($attributes !== null) {
669
-			$share = $this->setShareAttributes($share, $attributes);
670
-		}
671
-
672
-		// Expire date checks
673
-		// Normally, null means no expiration date but we still set the default for backwards compatibility
674
-		// If the client sends an empty string, we set noExpirationDate to true
675
-		if ($expireDate !== null) {
676
-			if ($expireDate !== '') {
677
-				try {
678
-					$expireDateTime = $this->parseDate($expireDate);
679
-					$share->setExpirationDate($expireDateTime);
680
-				} catch (\Exception $e) {
681
-					throw new OCSNotFoundException($e->getMessage(), $e);
682
-				}
683
-			} else {
684
-				// Client sent empty string for expire date.
685
-				// Set noExpirationDate to true so overwrite is prevented.
686
-				$share->setNoExpirationDate(true);
687
-			}
688
-		}
689
-
690
-		$share->setSharedBy($this->userId);
691
-
692
-		// Handle mail send
693
-		if (is_null($sendMail)) {
694
-			$allowSendMail = $this->config->getSystemValueBool('sharing.enable_share_mail', true);
695
-			if ($allowSendMail !== true || $shareType === IShare::TYPE_EMAIL) {
696
-				// Define a default behavior when sendMail is not provided
697
-				// For email shares with a valid recipient, the default is to send the mail
698
-				// For all other share types, the default is to not send the mail
699
-				$allowSendMail = ($shareType === IShare::TYPE_EMAIL && $shareWith !== null && $shareWith !== '');
700
-			}
701
-			$share->setMailSend($allowSendMail);
702
-		} else {
703
-			$share->setMailSend($sendMail === 'true');
704
-		}
705
-
706
-		if ($shareType === IShare::TYPE_USER) {
707
-			// Valid user is required to share
708
-			if ($shareWith === null || !$this->userManager->userExists($shareWith)) {
709
-				throw new OCSNotFoundException($this->l->t('Please specify a valid account to share with'));
710
-			}
711
-			$share->setSharedWith($shareWith);
712
-			$share->setPermissions($permissions);
713
-		} elseif ($shareType === IShare::TYPE_GROUP) {
714
-			if (!$this->shareManager->allowGroupSharing()) {
715
-				throw new OCSNotFoundException($this->l->t('Group sharing is disabled by the administrator'));
716
-			}
717
-
718
-			// Valid group is required to share
719
-			if ($shareWith === null || !$this->groupManager->groupExists($shareWith)) {
720
-				throw new OCSNotFoundException($this->l->t('Please specify a valid group'));
721
-			}
722
-			$share->setSharedWith($shareWith);
723
-			$share->setPermissions($permissions);
724
-		} elseif ($shareType === IShare::TYPE_LINK
725
-			|| $shareType === IShare::TYPE_EMAIL) {
726
-
727
-			// Can we even share links?
728
-			if (!$this->shareManager->shareApiAllowLinks()) {
729
-				throw new OCSNotFoundException($this->l->t('Public link sharing is disabled by the administrator'));
730
-			}
731
-
732
-			$this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload);
733
-			$share->setPermissions($permissions);
734
-
735
-			// Set password
736
-			if ($password !== '') {
737
-				$share->setPassword($password);
738
-			}
739
-
740
-			// Only share by mail have a recipient
741
-			if (is_string($shareWith) && $shareType === IShare::TYPE_EMAIL) {
742
-				// If sending a mail have been requested, validate the mail address
743
-				if ($share->getMailSend() && !$this->emailValidator->isValid($shareWith)) {
744
-					throw new OCSNotFoundException($this->l->t('Please specify a valid email address'));
745
-				}
746
-				$share->setSharedWith($shareWith);
747
-			}
748
-
749
-			// If we have a label, use it
750
-			if ($label !== '') {
751
-				if (strlen($label) > 255) {
752
-					throw new OCSBadRequestException('Maximum label length is 255');
753
-				}
754
-				$share->setLabel($label);
755
-			}
756
-
757
-			if ($sendPasswordByTalk === 'true') {
758
-				if (!$this->appManager->isEnabledForUser('spreed')) {
759
-					throw new OCSForbiddenException($this->l->t('Sharing %s sending the password by Nextcloud Talk failed because Nextcloud Talk is not enabled', [$node->getPath()]));
760
-				}
761
-
762
-				$share->setSendPasswordByTalk(true);
763
-			}
764
-		} elseif ($shareType === IShare::TYPE_REMOTE) {
765
-			if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) {
766
-				throw new OCSForbiddenException($this->l->t('Sharing %1$s failed because the back end does not allow shares from type %2$s', [$node->getPath(), $shareType]));
767
-			}
768
-
769
-			if ($shareWith === null) {
770
-				throw new OCSNotFoundException($this->l->t('Please specify a valid federated account ID'));
771
-			}
772
-
773
-			$share->setSharedWith($shareWith);
774
-			$share->setPermissions($permissions);
775
-			$share->setSharedWithDisplayName($this->getCachedFederatedDisplayName($shareWith, false));
776
-		} elseif ($shareType === IShare::TYPE_REMOTE_GROUP) {
777
-			if (!$this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
778
-				throw new OCSForbiddenException($this->l->t('Sharing %1$s failed because the back end does not allow shares from type %2$s', [$node->getPath(), $shareType]));
779
-			}
780
-
781
-			if ($shareWith === null) {
782
-				throw new OCSNotFoundException($this->l->t('Please specify a valid federated group ID'));
783
-			}
784
-
785
-			$share->setSharedWith($shareWith);
786
-			$share->setPermissions($permissions);
787
-		} elseif ($shareType === IShare::TYPE_CIRCLE) {
788
-			if (!Server::get(IAppManager::class)->isEnabledForUser('circles') || !class_exists('\OCA\Circles\ShareByCircleProvider')) {
789
-				throw new OCSNotFoundException($this->l->t('You cannot share to a Team if the app is not enabled'));
790
-			}
791
-
792
-			$circle = Circles::detailsCircle($shareWith);
793
-
794
-			// Valid team is required to share
795
-			if ($circle === null) {
796
-				throw new OCSNotFoundException($this->l->t('Please specify a valid team'));
797
-			}
798
-			$share->setSharedWith($shareWith);
799
-			$share->setPermissions($permissions);
800
-		} elseif ($shareType === IShare::TYPE_ROOM) {
801
-			try {
802
-				$this->getRoomShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? '');
803
-			} catch (ContainerExceptionInterface $e) {
804
-				throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support room shares', [$node->getPath()]));
805
-			}
806
-		} elseif ($shareType === IShare::TYPE_DECK) {
807
-			try {
808
-				$this->getDeckShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? '');
809
-			} catch (ContainerExceptionInterface $e) {
810
-				throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support room shares', [$node->getPath()]));
811
-			}
812
-		} else {
813
-			throw new OCSBadRequestException($this->l->t('Unknown share type'));
814
-		}
815
-
816
-		$share->setShareType($shareType);
817
-		$this->checkInheritedAttributes($share);
818
-
819
-		if ($note !== '') {
820
-			$share->setNote($note);
821
-		}
822
-
823
-		try {
824
-			$share = $this->shareManager->createShare($share);
825
-		} catch (HintException $e) {
826
-			$code = $e->getCode() === 0 ? 403 : $e->getCode();
827
-			throw new OCSException($e->getHint(), $code);
828
-		} catch (GenericShareException|\InvalidArgumentException $e) {
829
-			$this->logger->error($e->getMessage(), ['exception' => $e]);
830
-			throw new OCSForbiddenException($e->getMessage(), $e);
831
-		} catch (\Exception $e) {
832
-			$this->logger->error($e->getMessage(), ['exception' => $e]);
833
-			throw new OCSForbiddenException('Failed to create share.', $e);
834
-		}
835
-
836
-		$output = $this->formatShare($share);
837
-
838
-		return new DataResponse($output);
839
-	}
840
-
841
-	/**
842
-	 * @param null|Node $node
843
-	 * @param boolean $includeTags
844
-	 *
845
-	 * @return list<Files_SharingShare>
846
-	 */
847
-	private function getSharedWithMe($node, bool $includeTags): array {
848
-		$userShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_USER, $node, -1, 0);
849
-		$groupShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_GROUP, $node, -1, 0);
850
-		$circleShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_CIRCLE, $node, -1, 0);
851
-		$roomShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_ROOM, $node, -1, 0);
852
-		$deckShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_DECK, $node, -1, 0);
853
-
854
-		$shares = array_merge($userShares, $groupShares, $circleShares, $roomShares, $deckShares);
855
-
856
-		$filteredShares = array_filter($shares, function (IShare $share) {
857
-			return $share->getShareOwner() !== $this->userId && $share->getSharedBy() !== $this->userId;
858
-		});
859
-
860
-		$formatted = [];
861
-		foreach ($filteredShares as $share) {
862
-			if ($this->canAccessShare($share)) {
863
-				try {
864
-					$formatted[] = $this->formatShare($share);
865
-				} catch (NotFoundException $e) {
866
-					// Ignore this share
867
-				}
868
-			}
869
-		}
870
-
871
-		if ($includeTags) {
872
-			$formatted = $this->populateTags($formatted);
873
-		}
874
-
875
-		return $formatted;
876
-	}
877
-
878
-	/**
879
-	 * @param Node $folder
880
-	 *
881
-	 * @return list<Files_SharingShare>
882
-	 * @throws OCSBadRequestException
883
-	 * @throws NotFoundException
884
-	 */
885
-	private function getSharesInDir(Node $folder): array {
886
-		if (!($folder instanceof Folder)) {
887
-			throw new OCSBadRequestException($this->l->t('Not a directory'));
888
-		}
889
-
890
-		$nodes = $folder->getDirectoryListing();
891
-
892
-		/** @var IShare[] $shares */
893
-		$shares = array_reduce($nodes, function ($carry, $node) {
894
-			$carry = array_merge($carry, $this->getAllShares($node, true));
895
-			return $carry;
896
-		}, []);
897
-
898
-		// filter out duplicate shares
899
-		$known = [];
900
-
901
-		$formatted = $miniFormatted = [];
902
-		$resharingRight = false;
903
-		$known = [];
904
-		foreach ($shares as $share) {
905
-			if (in_array($share->getId(), $known) || $share->getSharedWith() === $this->userId) {
906
-				continue;
907
-			}
908
-
909
-			try {
910
-				$format = $this->formatShare($share);
911
-
912
-				$known[] = $share->getId();
913
-				$formatted[] = $format;
914
-				if ($share->getSharedBy() === $this->userId) {
915
-					$miniFormatted[] = $format;
916
-				}
917
-				if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $folder)) {
918
-					$resharingRight = true;
919
-				}
920
-			} catch (\Exception $e) {
921
-				//Ignore this share
922
-			}
923
-		}
924
-
925
-		if (!$resharingRight) {
926
-			$formatted = $miniFormatted;
927
-		}
928
-
929
-		return $formatted;
930
-	}
931
-
932
-	/**
933
-	 * Get shares of the current user
934
-	 *
935
-	 * @param string $shared_with_me Only get shares with the current user
936
-	 * @param string $reshares Only get shares by the current user and reshares
937
-	 * @param string $subfiles Only get all shares in a folder
938
-	 * @param string $path Get shares for a specific path
939
-	 * @param string $include_tags Include tags in the share
940
-	 *
941
-	 * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
942
-	 * @throws OCSNotFoundException The folder was not found or is inaccessible
943
-	 *
944
-	 * 200: Shares returned
945
-	 */
946
-	#[NoAdminRequired]
947
-	public function getShares(
948
-		string $shared_with_me = 'false',
949
-		string $reshares = 'false',
950
-		string $subfiles = 'false',
951
-		string $path = '',
952
-		string $include_tags = 'false',
953
-	): DataResponse {
954
-		$node = null;
955
-		if ($path !== '') {
956
-			$userFolder = $this->rootFolder->getUserFolder($this->userId);
957
-			try {
958
-				$node = $userFolder->get($path);
959
-				$this->lock($node);
960
-			} catch (NotFoundException $e) {
961
-				throw new OCSNotFoundException(
962
-					$this->l->t('Wrong path, file/folder does not exist')
963
-				);
964
-			} catch (LockedException $e) {
965
-				throw new OCSNotFoundException($this->l->t('Could not lock node'));
966
-			}
967
-		}
968
-
969
-		$shares = $this->getFormattedShares(
970
-			$this->userId,
971
-			$node,
972
-			($shared_with_me === 'true'),
973
-			($reshares === 'true'),
974
-			($subfiles === 'true'),
975
-			($include_tags === 'true')
976
-		);
977
-
978
-		return new DataResponse($shares);
979
-	}
980
-
981
-	private function getLinkSharePermissions(?int $permissions, ?bool $legacyPublicUpload): int {
982
-		$permissions = $permissions ?? Constants::PERMISSION_READ;
983
-
984
-		// Legacy option handling
985
-		if ($legacyPublicUpload !== null) {
986
-			$permissions = $legacyPublicUpload
987
-				? (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE)
988
-				: Constants::PERMISSION_READ;
989
-		}
990
-
991
-		if ($this->hasPermission($permissions, Constants::PERMISSION_READ)
992
-			&& $this->shareManager->outgoingServer2ServerSharesAllowed()
993
-			&& $this->appConfig->getValueBool('core', ConfigLexicon::SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES)) {
994
-			$permissions |= Constants::PERMISSION_SHARE;
995
-		}
996
-
997
-		return $permissions;
998
-	}
999
-
1000
-	/**
1001
-	 * Helper to check for legacy "publicUpload" handling.
1002
-	 * If the value is set to `true` or `false` then true or false are returned.
1003
-	 * Otherwise null is returned to indicate that the option was not (or wrong) set.
1004
-	 *
1005
-	 * @param null|string $legacyPublicUpload The value of `publicUpload`
1006
-	 */
1007
-	private function getLegacyPublicUpload(?string $legacyPublicUpload): ?bool {
1008
-		if ($legacyPublicUpload === 'true') {
1009
-			return true;
1010
-		} elseif ($legacyPublicUpload === 'false') {
1011
-			return false;
1012
-		}
1013
-		// Not set at all
1014
-		return null;
1015
-	}
1016
-
1017
-	/**
1018
-	 * For link and email shares validate that only allowed combinations are set.
1019
-	 *
1020
-	 * @throw OCSBadRequestException If permission combination is invalid.
1021
-	 * @throw OCSForbiddenException If public upload was forbidden by the administrator.
1022
-	 */
1023
-	private function validateLinkSharePermissions(Node $node, int $permissions, ?bool $legacyPublicUpload): void {
1024
-		if ($legacyPublicUpload && ($node instanceof File)) {
1025
-			throw new OCSBadRequestException($this->l->t('Public upload is only possible for publicly shared folders'));
1026
-		}
1027
-
1028
-		// We need at least READ or CREATE (file drop)
1029
-		if (!$this->hasPermission($permissions, Constants::PERMISSION_READ)
1030
-			&& !$this->hasPermission($permissions, Constants::PERMISSION_CREATE)) {
1031
-			throw new OCSBadRequestException($this->l->t('Share must at least have READ or CREATE permissions'));
1032
-		}
1033
-
1034
-		// UPDATE and DELETE require a READ permission
1035
-		if (!$this->hasPermission($permissions, Constants::PERMISSION_READ)
1036
-			&& ($this->hasPermission($permissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($permissions, Constants::PERMISSION_DELETE))) {
1037
-			throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set'));
1038
-		}
1039
-
1040
-		// Check if public uploading was disabled
1041
-		if ($this->hasPermission($permissions, Constants::PERMISSION_CREATE)
1042
-			&& !$this->shareManager->shareApiLinkAllowPublicUpload()) {
1043
-			throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator'));
1044
-		}
1045
-	}
1046
-
1047
-	/**
1048
-	 * @param string $viewer
1049
-	 * @param Node $node
1050
-	 * @param bool $sharedWithMe
1051
-	 * @param bool $reShares
1052
-	 * @param bool $subFiles
1053
-	 * @param bool $includeTags
1054
-	 *
1055
-	 * @return list<Files_SharingShare>
1056
-	 * @throws NotFoundException
1057
-	 * @throws OCSBadRequestException
1058
-	 */
1059
-	private function getFormattedShares(
1060
-		string $viewer,
1061
-		$node = null,
1062
-		bool $sharedWithMe = false,
1063
-		bool $reShares = false,
1064
-		bool $subFiles = false,
1065
-		bool $includeTags = false,
1066
-	): array {
1067
-		if ($sharedWithMe) {
1068
-			return $this->getSharedWithMe($node, $includeTags);
1069
-		}
1070
-
1071
-		if ($subFiles) {
1072
-			return $this->getSharesInDir($node);
1073
-		}
1074
-
1075
-		$shares = $this->getSharesFromNode($viewer, $node, $reShares);
1076
-
1077
-		$known = $formatted = $miniFormatted = [];
1078
-		$resharingRight = false;
1079
-		foreach ($shares as $share) {
1080
-			try {
1081
-				$share->getNode();
1082
-			} catch (NotFoundException $e) {
1083
-				/*
80
+    private ?Node $lockedNode = null;
81
+    private array $trustedServerCache = [];
82
+
83
+    /**
84
+     * Share20OCS constructor.
85
+     */
86
+    public function __construct(
87
+        string $appName,
88
+        IRequest $request,
89
+        private IManager $shareManager,
90
+        private IGroupManager $groupManager,
91
+        private IUserManager $userManager,
92
+        private IRootFolder $rootFolder,
93
+        private IURLGenerator $urlGenerator,
94
+        private IL10N $l,
95
+        private IConfig $config,
96
+        private IAppConfig $appConfig,
97
+        private IAppManager $appManager,
98
+        private ContainerInterface $serverContainer,
99
+        private IUserStatusManager $userStatusManager,
100
+        private IPreview $previewManager,
101
+        private IDateTimeZone $dateTimeZone,
102
+        private LoggerInterface $logger,
103
+        private IProviderFactory $factory,
104
+        private IMailer $mailer,
105
+        private ITagManager $tagManager,
106
+        private IEmailValidator $emailValidator,
107
+        private ?TrustedServers $trustedServers,
108
+        private ?string $userId = null,
109
+    ) {
110
+        parent::__construct($appName, $request);
111
+    }
112
+
113
+    /**
114
+     * Convert an IShare to an array for OCS output
115
+     *
116
+     * @param IShare $share
117
+     * @param Node|null $recipientNode
118
+     * @return Files_SharingShare
119
+     * @throws NotFoundException In case the node can't be resolved.
120
+     *
121
+     * @suppress PhanUndeclaredClassMethod
122
+     */
123
+    protected function formatShare(IShare $share, ?Node $recipientNode = null): array {
124
+        $sharedBy = $this->userManager->get($share->getSharedBy());
125
+        $shareOwner = $this->userManager->get($share->getShareOwner());
126
+
127
+        $isOwnShare = false;
128
+        if ($shareOwner !== null) {
129
+            $isOwnShare = $shareOwner->getUID() === $this->userId;
130
+        }
131
+
132
+        $result = [
133
+            'id' => $share->getId(),
134
+            'share_type' => $share->getShareType(),
135
+            'uid_owner' => $share->getSharedBy(),
136
+            'displayname_owner' => $sharedBy !== null ? $sharedBy->getDisplayName() : $share->getSharedBy(),
137
+            // recipient permissions
138
+            'permissions' => $share->getPermissions(),
139
+            // current user permissions on this share
140
+            'can_edit' => $this->canEditShare($share),
141
+            'can_delete' => $this->canDeleteShare($share),
142
+            'stime' => $share->getShareTime()->getTimestamp(),
143
+            'parent' => null,
144
+            'expiration' => null,
145
+            'token' => null,
146
+            'uid_file_owner' => $share->getShareOwner(),
147
+            'note' => $share->getNote(),
148
+            'label' => $share->getLabel(),
149
+            'displayname_file_owner' => $shareOwner !== null ? $shareOwner->getDisplayName() : $share->getShareOwner(),
150
+        ];
151
+
152
+        $userFolder = $this->rootFolder->getUserFolder($this->userId);
153
+        if ($recipientNode) {
154
+            $node = $recipientNode;
155
+        } else {
156
+            $node = $userFolder->getFirstNodeById($share->getNodeId());
157
+            if (!$node) {
158
+                // fallback to guessing the path
159
+                $node = $userFolder->get($share->getTarget());
160
+                if ($node === null || $share->getTarget() === '') {
161
+                    throw new NotFoundException();
162
+                }
163
+            }
164
+        }
165
+
166
+        $result['path'] = $userFolder->getRelativePath($node->getPath());
167
+        if ($node instanceof Folder) {
168
+            $result['item_type'] = 'folder';
169
+        } else {
170
+            $result['item_type'] = 'file';
171
+        }
172
+
173
+        // Get the original node permission if the share owner is the current user
174
+        if ($isOwnShare) {
175
+            $result['item_permissions'] = $node->getPermissions();
176
+        }
177
+
178
+        // If we're on the recipient side, the node permissions
179
+        // are bound to the share permissions. So we need to
180
+        // adjust the permissions to the share permissions if necessary.
181
+        if (!$isOwnShare) {
182
+            $result['item_permissions'] = $share->getPermissions();
183
+
184
+            // For some reason, single files share are forbidden to have the delete permission
185
+            // since we have custom methods to check those, let's adjust straight away.
186
+            // DAV permissions does not have that issue though.
187
+            if ($this->canDeleteShare($share) || $this->canDeleteShareFromSelf($share)) {
188
+                $result['item_permissions'] |= Constants::PERMISSION_DELETE;
189
+            }
190
+            if ($this->canEditShare($share)) {
191
+                $result['item_permissions'] |= Constants::PERMISSION_UPDATE;
192
+            }
193
+        }
194
+
195
+        // See MOUNT_ROOT_PROPERTYNAME dav property
196
+        $result['is-mount-root'] = $node->getInternalPath() === '';
197
+        $result['mount-type'] = $node->getMountPoint()->getMountType();
198
+
199
+        $result['mimetype'] = $node->getMimetype();
200
+        $result['has_preview'] = $this->previewManager->isAvailable($node);
201
+        $result['storage_id'] = $node->getStorage()->getId();
202
+        $result['storage'] = $node->getStorage()->getCache()->getNumericStorageId();
203
+        $result['item_source'] = $node->getId();
204
+        $result['file_source'] = $node->getId();
205
+        $result['file_parent'] = $node->getParent()->getId();
206
+        $result['file_target'] = $share->getTarget();
207
+        $result['item_size'] = $node->getSize();
208
+        $result['item_mtime'] = $node->getMTime();
209
+
210
+        if ($this->trustedServers !== null && in_array($share->getShareType(), [IShare::TYPE_REMOTE, IShare::TYPE_REMOTE_GROUP], true)) {
211
+            $result['is_trusted_server'] = false;
212
+            $sharedWith = $share->getSharedWith();
213
+            $remoteIdentifier = is_string($sharedWith) ? strrchr($sharedWith, '@') : false;
214
+            if ($remoteIdentifier !== false) {
215
+                $remote = substr($remoteIdentifier, 1);
216
+
217
+                if (isset($this->trustedServerCache[$remote])) {
218
+                    $result['is_trusted_server'] = $this->trustedServerCache[$remote];
219
+                } else {
220
+                    try {
221
+                        $isTrusted = $this->trustedServers->isTrustedServer($remote);
222
+                        $this->trustedServerCache[$remote] = $isTrusted;
223
+                        $result['is_trusted_server'] = $isTrusted;
224
+                    } catch (\Exception $e) {
225
+                        // Server not found or other issue, we consider it not trusted
226
+                        $this->trustedServerCache[$remote] = false;
227
+                        $this->logger->error(
228
+                            'Error checking if remote server is trusted (treating as untrusted): ' . $e->getMessage(),
229
+                            ['exception' => $e]
230
+                        );
231
+                    }
232
+                }
233
+            }
234
+        }
235
+
236
+        $expiration = $share->getExpirationDate();
237
+        if ($expiration !== null) {
238
+            $expiration->setTimezone($this->dateTimeZone->getTimeZone());
239
+            $result['expiration'] = $expiration->format('Y-m-d 00:00:00');
240
+        }
241
+
242
+        if ($share->getShareType() === IShare::TYPE_USER) {
243
+            $sharedWith = $this->userManager->get($share->getSharedWith());
244
+            $result['share_with'] = $share->getSharedWith();
245
+            $result['share_with_displayname'] = $sharedWith !== null ? $sharedWith->getDisplayName() : $share->getSharedWith();
246
+            $result['share_with_displayname_unique'] = $sharedWith !== null ? (
247
+                !empty($sharedWith->getSystemEMailAddress()) ? $sharedWith->getSystemEMailAddress() : $sharedWith->getUID()
248
+            ) : $share->getSharedWith();
249
+
250
+            $userStatuses = $this->userStatusManager->getUserStatuses([$share->getSharedWith()]);
251
+            $userStatus = array_shift($userStatuses);
252
+            if ($userStatus) {
253
+                $result['status'] = [
254
+                    'status' => $userStatus->getStatus(),
255
+                    'message' => $userStatus->getMessage(),
256
+                    'icon' => $userStatus->getIcon(),
257
+                    'clearAt' => $userStatus->getClearAt()
258
+                        ? (int)$userStatus->getClearAt()->format('U')
259
+                        : null,
260
+                ];
261
+            }
262
+        } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
263
+            $group = $this->groupManager->get($share->getSharedWith());
264
+            $result['share_with'] = $share->getSharedWith();
265
+            $result['share_with_displayname'] = $group !== null ? $group->getDisplayName() : $share->getSharedWith();
266
+        } elseif ($share->getShareType() === IShare::TYPE_LINK) {
267
+
268
+            // "share_with" and "share_with_displayname" for passwords of link
269
+            // shares was deprecated in Nextcloud 15, use "password" instead.
270
+            $result['share_with'] = $share->getPassword();
271
+            $result['share_with_displayname'] = '(' . $this->l->t('Shared link') . ')';
272
+
273
+            $result['password'] = $share->getPassword();
274
+
275
+            $result['send_password_by_talk'] = $share->getSendPasswordByTalk();
276
+
277
+            $result['token'] = $share->getToken();
278
+            $result['url'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $share->getToken()]);
279
+        } elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
280
+            $result['share_with'] = $share->getSharedWith();
281
+            $result['share_with_displayname'] = $this->getCachedFederatedDisplayName($share->getSharedWith());
282
+            $result['token'] = $share->getToken();
283
+        } elseif ($share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
284
+            $result['share_with'] = $share->getSharedWith();
285
+            $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'CLOUD');
286
+            $result['token'] = $share->getToken();
287
+        } elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
288
+            $result['share_with'] = $share->getSharedWith();
289
+            $result['password'] = $share->getPassword();
290
+            $result['password_expiration_time'] = $share->getPasswordExpirationTime() !== null ? $share->getPasswordExpirationTime()->format(\DateTime::ATOM) : null;
291
+            $result['send_password_by_talk'] = $share->getSendPasswordByTalk();
292
+            $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'EMAIL');
293
+            $result['token'] = $share->getToken();
294
+        } elseif ($share->getShareType() === IShare::TYPE_CIRCLE) {
295
+            // getSharedWith() returns either "name (type, owner)" or
296
+            // "name (type, owner) [id]", depending on the Teams app version.
297
+            $hasCircleId = (substr($share->getSharedWith(), -1) === ']');
298
+
299
+            $result['share_with_displayname'] = $share->getSharedWithDisplayName();
300
+            if (empty($result['share_with_displayname'])) {
301
+                $displayNameLength = ($hasCircleId ? strrpos($share->getSharedWith(), ' ') : strlen($share->getSharedWith()));
302
+                $result['share_with_displayname'] = substr($share->getSharedWith(), 0, $displayNameLength);
303
+            }
304
+
305
+            $result['share_with_avatar'] = $share->getSharedWithAvatar();
306
+
307
+            $shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0);
308
+            $shareWithLength = ($hasCircleId ? -1 : strpos($share->getSharedWith(), ' '));
309
+            if ($shareWithLength === false) {
310
+                $result['share_with'] = substr($share->getSharedWith(), $shareWithStart);
311
+            } else {
312
+                $result['share_with'] = substr($share->getSharedWith(), $shareWithStart, $shareWithLength);
313
+            }
314
+        } elseif ($share->getShareType() === IShare::TYPE_ROOM) {
315
+            $result['share_with'] = $share->getSharedWith();
316
+            $result['share_with_displayname'] = '';
317
+
318
+            try {
319
+                /** @var array{share_with_displayname: string, share_with_link: string, share_with?: string, token?: string} $roomShare */
320
+                $roomShare = $this->getRoomShareHelper()->formatShare($share);
321
+                $result = array_merge($result, $roomShare);
322
+            } catch (ContainerExceptionInterface $e) {
323
+            }
324
+        } elseif ($share->getShareType() === IShare::TYPE_DECK) {
325
+            $result['share_with'] = $share->getSharedWith();
326
+            $result['share_with_displayname'] = '';
327
+
328
+            try {
329
+                /** @var array{share_with: string, share_with_displayname: string, share_with_link: string} $deckShare */
330
+                $deckShare = $this->getDeckShareHelper()->formatShare($share);
331
+                $result = array_merge($result, $deckShare);
332
+            } catch (ContainerExceptionInterface $e) {
333
+            }
334
+        }
335
+
336
+
337
+        $result['mail_send'] = $share->getMailSend() ? 1 : 0;
338
+        $result['hide_download'] = $share->getHideDownload() ? 1 : 0;
339
+
340
+        $result['attributes'] = null;
341
+        if ($attributes = $share->getAttributes()) {
342
+            $result['attributes'] = (string)\json_encode($attributes->toArray());
343
+        }
344
+
345
+        return $result;
346
+    }
347
+
348
+    /**
349
+     * Check if one of the users address books knows the exact property, if
350
+     * not we return the full name.
351
+     *
352
+     * @param string $query
353
+     * @param string $property
354
+     * @return string
355
+     */
356
+    private function getDisplayNameFromAddressBook(string $query, string $property): string {
357
+        // FIXME: If we inject the contacts manager it gets initialized before any address books are registered
358
+        try {
359
+            $result = Server::get(\OCP\Contacts\IManager::class)->search($query, [$property], [
360
+                'limit' => 1,
361
+                'enumeration' => false,
362
+                'strict_search' => true,
363
+            ]);
364
+        } catch (Exception $e) {
365
+            $this->logger->error(
366
+                $e->getMessage(),
367
+                ['exception' => $e]
368
+            );
369
+            return $query;
370
+        }
371
+
372
+        foreach ($result as $r) {
373
+            foreach ($r[$property] as $value) {
374
+                if ($value === $query && $r['FN']) {
375
+                    return $r['FN'];
376
+                }
377
+            }
378
+        }
379
+
380
+        return $query;
381
+    }
382
+
383
+
384
+    /**
385
+     * @param list<Files_SharingShare> $shares
386
+     * @param array<string, string>|null $updatedDisplayName
387
+     *
388
+     * @return list<Files_SharingShare>
389
+     */
390
+    private function fixMissingDisplayName(array $shares, ?array $updatedDisplayName = null): array {
391
+        $userIds = $updated = [];
392
+        foreach ($shares as $share) {
393
+            // share is federated and share have no display name yet
394
+            if ($share['share_type'] === IShare::TYPE_REMOTE
395
+                && ($share['share_with'] ?? '') !== ''
396
+                && ($share['share_with_displayname'] ?? '') === '') {
397
+                $userIds[] = $userId = $share['share_with'];
398
+
399
+                if ($updatedDisplayName !== null && array_key_exists($userId, $updatedDisplayName)) {
400
+                    $share['share_with_displayname'] = $updatedDisplayName[$userId];
401
+                }
402
+            }
403
+
404
+            // prepping userIds with displayName to be updated
405
+            $updated[] = $share;
406
+        }
407
+
408
+        // if $updatedDisplayName is not null, it means we should have already fixed displayNames of the shares
409
+        if ($updatedDisplayName !== null) {
410
+            return $updated;
411
+        }
412
+
413
+        // get displayName for the generated list of userId with no displayName
414
+        $displayNames = $this->retrieveFederatedDisplayName($userIds);
415
+
416
+        // if no displayName are updated, we exit
417
+        if (empty($displayNames)) {
418
+            return $updated;
419
+        }
420
+
421
+        // let's fix missing display name and returns all shares
422
+        return $this->fixMissingDisplayName($shares, $displayNames);
423
+    }
424
+
425
+
426
+    /**
427
+     * get displayName of a list of userIds from the lookup-server; through the globalsiteselector app.
428
+     * returns an array with userIds as keys and displayName as values.
429
+     *
430
+     * @param array $userIds
431
+     * @param bool $cacheOnly - do not reach LUS, get data from cache.
432
+     *
433
+     * @return array
434
+     * @throws ContainerExceptionInterface
435
+     */
436
+    private function retrieveFederatedDisplayName(array $userIds, bool $cacheOnly = false): array {
437
+        // check if gss is enabled and available
438
+        if (count($userIds) === 0
439
+            || !$this->appManager->isEnabledForAnyone('globalsiteselector')
440
+            || !class_exists('\OCA\GlobalSiteSelector\Service\SlaveService')) {
441
+            return [];
442
+        }
443
+
444
+        try {
445
+            $slaveService = Server::get(SlaveService::class);
446
+        } catch (\Throwable $e) {
447
+            $this->logger->error(
448
+                $e->getMessage(),
449
+                ['exception' => $e]
450
+            );
451
+            return [];
452
+        }
453
+
454
+        return $slaveService->getUsersDisplayName($userIds, $cacheOnly);
455
+    }
456
+
457
+
458
+    /**
459
+     * retrieve displayName from cache if available (should be used on federated shares)
460
+     * if not available in cache/lus, try for get from address-book, else returns empty string.
461
+     *
462
+     * @param string $userId
463
+     * @param bool $cacheOnly if true will not reach the lus but will only get data from cache
464
+     *
465
+     * @return string
466
+     */
467
+    private function getCachedFederatedDisplayName(string $userId, bool $cacheOnly = true): string {
468
+        $details = $this->retrieveFederatedDisplayName([$userId], $cacheOnly);
469
+        if (array_key_exists($userId, $details)) {
470
+            return $details[$userId];
471
+        }
472
+
473
+        $displayName = $this->getDisplayNameFromAddressBook($userId, 'CLOUD');
474
+        return ($displayName === $userId) ? '' : $displayName;
475
+    }
476
+
477
+
478
+
479
+    /**
480
+     * Get a specific share by id
481
+     *
482
+     * @param string $id ID of the share
483
+     * @param bool $include_tags Include tags in the share
484
+     * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
485
+     * @throws OCSNotFoundException Share not found
486
+     *
487
+     * 200: Share returned
488
+     */
489
+    #[NoAdminRequired]
490
+    public function getShare(string $id, bool $include_tags = false): DataResponse {
491
+        try {
492
+            $share = $this->getShareById($id);
493
+        } catch (ShareNotFound $e) {
494
+            throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
495
+        }
496
+
497
+        try {
498
+            if ($this->canAccessShare($share)) {
499
+                $share = $this->formatShare($share);
500
+
501
+                if ($include_tags) {
502
+                    $share = $this->populateTags([$share]);
503
+                } else {
504
+                    $share = [$share];
505
+                }
506
+
507
+                return new DataResponse($share);
508
+            }
509
+        } catch (NotFoundException $e) {
510
+            // Fall through
511
+        }
512
+
513
+        throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
514
+    }
515
+
516
+    /**
517
+     * Delete a share
518
+     *
519
+     * @param string $id ID of the share
520
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
521
+     * @throws OCSNotFoundException Share not found
522
+     * @throws OCSForbiddenException Missing permissions to delete the share
523
+     *
524
+     * 200: Share deleted successfully
525
+     */
526
+    #[NoAdminRequired]
527
+    public function deleteShare(string $id): DataResponse {
528
+        try {
529
+            $share = $this->getShareById($id);
530
+        } catch (ShareNotFound $e) {
531
+            throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
532
+        }
533
+
534
+        try {
535
+            $this->lock($share->getNode());
536
+        } catch (LockedException $e) {
537
+            throw new OCSNotFoundException($this->l->t('Could not delete share'));
538
+        } catch (NotFoundException $e) {
539
+            $this->logger->debug('File of to be deleted share was not found, skip locking', ['exception' => $e]);
540
+        }
541
+
542
+        if (!$this->canAccessShare($share)) {
543
+            throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
544
+        }
545
+
546
+        // if it's a group share or a room share
547
+        // we don't delete the share, but only the
548
+        // mount point. Allowing it to be restored
549
+        // from the deleted shares
550
+        if ($this->canDeleteShareFromSelf($share)) {
551
+            $this->shareManager->deleteFromSelf($share, $this->userId);
552
+        } else {
553
+            if (!$this->canDeleteShare($share)) {
554
+                throw new OCSForbiddenException($this->l->t('Could not delete share'));
555
+            }
556
+
557
+            $this->shareManager->deleteShare($share);
558
+        }
559
+
560
+        return new DataResponse();
561
+    }
562
+
563
+    /**
564
+     * Create a share
565
+     *
566
+     * @param string|null $path Path of the share
567
+     * @param int|null $permissions Permissions for the share
568
+     * @param int $shareType Type of the share
569
+     * @param ?string $shareWith The entity this should be shared with
570
+     * @param 'true'|'false'|null $publicUpload If public uploading is allowed (deprecated)
571
+     * @param string $password Password for the share
572
+     * @param string|null $sendPasswordByTalk Send the password for the share over Talk
573
+     * @param ?string $expireDate The expiry date of the share in the user's timezone at 00:00.
574
+     *                            If $expireDate is not supplied or set to `null`, the system default will be used.
575
+     * @param string $note Note for the share
576
+     * @param string $label Label for the share (only used in link and email)
577
+     * @param string|null $attributes Additional attributes for the share
578
+     * @param 'false'|'true'|null $sendMail Send a mail to the recipient
579
+     *
580
+     * @return DataResponse<Http::STATUS_OK, Files_SharingShare, array{}>
581
+     * @throws OCSBadRequestException Unknown share type
582
+     * @throws OCSException
583
+     * @throws OCSForbiddenException Creating the share is not allowed
584
+     * @throws OCSNotFoundException Creating the share failed
585
+     * @suppress PhanUndeclaredClassMethod
586
+     *
587
+     * 200: Share created
588
+     */
589
+    #[NoAdminRequired]
590
+    #[UserRateLimit(limit: 20, period: 600)]
591
+    public function createShare(
592
+        ?string $path = null,
593
+        ?int $permissions = null,
594
+        int $shareType = -1,
595
+        ?string $shareWith = null,
596
+        ?string $publicUpload = null,
597
+        string $password = '',
598
+        ?string $sendPasswordByTalk = null,
599
+        ?string $expireDate = null,
600
+        string $note = '',
601
+        string $label = '',
602
+        ?string $attributes = null,
603
+        ?string $sendMail = null,
604
+    ): DataResponse {
605
+        assert($this->userId !== null);
606
+
607
+        $share = $this->shareManager->newShare();
608
+        $hasPublicUpload = $this->getLegacyPublicUpload($publicUpload);
609
+
610
+        // Verify path
611
+        if ($path === null) {
612
+            throw new OCSNotFoundException($this->l->t('Please specify a file or folder path'));
613
+        }
614
+
615
+        $userFolder = $this->rootFolder->getUserFolder($this->userId);
616
+        try {
617
+            /** @var \OC\Files\Node\Node $node */
618
+            $node = $userFolder->get($path);
619
+        } catch (NotFoundException $e) {
620
+            throw new OCSNotFoundException($this->l->t('Wrong path, file/folder does not exist'));
621
+        }
622
+
623
+        // a user can have access to a file through different paths, with differing permissions
624
+        // combine all permissions to determine if the user can share this file
625
+        $nodes = $userFolder->getById($node->getId());
626
+        foreach ($nodes as $nodeById) {
627
+            /** @var FileInfo $fileInfo */
628
+            $fileInfo = $node->getFileInfo();
629
+            $fileInfo['permissions'] |= $nodeById->getPermissions();
630
+        }
631
+
632
+        $share->setNode($node);
633
+
634
+        try {
635
+            $this->lock($share->getNode());
636
+        } catch (LockedException $e) {
637
+            throw new OCSNotFoundException($this->l->t('Could not create share'));
638
+        }
639
+
640
+        // Set permissions
641
+        if ($shareType === IShare::TYPE_LINK || $shareType === IShare::TYPE_EMAIL) {
642
+            $permissions = $this->getLinkSharePermissions($permissions, $hasPublicUpload);
643
+            $this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload);
644
+        } else {
645
+            // Use default permissions only for non-link shares to keep legacy behavior
646
+            if ($permissions === null) {
647
+                $permissions = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL);
648
+            }
649
+            // Non-link shares always require read permissions (link shares could be file drop)
650
+            $permissions |= Constants::PERMISSION_READ;
651
+        }
652
+
653
+        // For legacy reasons the API allows to pass PERMISSIONS_ALL even for single file shares (I look at you Talk)
654
+        if ($node instanceof File) {
655
+            // if this is a single file share we remove the DELETE and CREATE permissions
656
+            $permissions = $permissions & ~(Constants::PERMISSION_DELETE | Constants::PERMISSION_CREATE);
657
+        }
658
+
659
+        /**
660
+         * Hack for https://github.com/owncloud/core/issues/22587
661
+         * We check the permissions via webdav. But the permissions of the mount point
662
+         * do not equal the share permissions. Here we fix that for federated mounts.
663
+         */
664
+        if ($node->getStorage()->instanceOfStorage(Storage::class)) {
665
+            $permissions &= ~($permissions & ~$node->getPermissions());
666
+        }
667
+
668
+        if ($attributes !== null) {
669
+            $share = $this->setShareAttributes($share, $attributes);
670
+        }
671
+
672
+        // Expire date checks
673
+        // Normally, null means no expiration date but we still set the default for backwards compatibility
674
+        // If the client sends an empty string, we set noExpirationDate to true
675
+        if ($expireDate !== null) {
676
+            if ($expireDate !== '') {
677
+                try {
678
+                    $expireDateTime = $this->parseDate($expireDate);
679
+                    $share->setExpirationDate($expireDateTime);
680
+                } catch (\Exception $e) {
681
+                    throw new OCSNotFoundException($e->getMessage(), $e);
682
+                }
683
+            } else {
684
+                // Client sent empty string for expire date.
685
+                // Set noExpirationDate to true so overwrite is prevented.
686
+                $share->setNoExpirationDate(true);
687
+            }
688
+        }
689
+
690
+        $share->setSharedBy($this->userId);
691
+
692
+        // Handle mail send
693
+        if (is_null($sendMail)) {
694
+            $allowSendMail = $this->config->getSystemValueBool('sharing.enable_share_mail', true);
695
+            if ($allowSendMail !== true || $shareType === IShare::TYPE_EMAIL) {
696
+                // Define a default behavior when sendMail is not provided
697
+                // For email shares with a valid recipient, the default is to send the mail
698
+                // For all other share types, the default is to not send the mail
699
+                $allowSendMail = ($shareType === IShare::TYPE_EMAIL && $shareWith !== null && $shareWith !== '');
700
+            }
701
+            $share->setMailSend($allowSendMail);
702
+        } else {
703
+            $share->setMailSend($sendMail === 'true');
704
+        }
705
+
706
+        if ($shareType === IShare::TYPE_USER) {
707
+            // Valid user is required to share
708
+            if ($shareWith === null || !$this->userManager->userExists($shareWith)) {
709
+                throw new OCSNotFoundException($this->l->t('Please specify a valid account to share with'));
710
+            }
711
+            $share->setSharedWith($shareWith);
712
+            $share->setPermissions($permissions);
713
+        } elseif ($shareType === IShare::TYPE_GROUP) {
714
+            if (!$this->shareManager->allowGroupSharing()) {
715
+                throw new OCSNotFoundException($this->l->t('Group sharing is disabled by the administrator'));
716
+            }
717
+
718
+            // Valid group is required to share
719
+            if ($shareWith === null || !$this->groupManager->groupExists($shareWith)) {
720
+                throw new OCSNotFoundException($this->l->t('Please specify a valid group'));
721
+            }
722
+            $share->setSharedWith($shareWith);
723
+            $share->setPermissions($permissions);
724
+        } elseif ($shareType === IShare::TYPE_LINK
725
+            || $shareType === IShare::TYPE_EMAIL) {
726
+
727
+            // Can we even share links?
728
+            if (!$this->shareManager->shareApiAllowLinks()) {
729
+                throw new OCSNotFoundException($this->l->t('Public link sharing is disabled by the administrator'));
730
+            }
731
+
732
+            $this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload);
733
+            $share->setPermissions($permissions);
734
+
735
+            // Set password
736
+            if ($password !== '') {
737
+                $share->setPassword($password);
738
+            }
739
+
740
+            // Only share by mail have a recipient
741
+            if (is_string($shareWith) && $shareType === IShare::TYPE_EMAIL) {
742
+                // If sending a mail have been requested, validate the mail address
743
+                if ($share->getMailSend() && !$this->emailValidator->isValid($shareWith)) {
744
+                    throw new OCSNotFoundException($this->l->t('Please specify a valid email address'));
745
+                }
746
+                $share->setSharedWith($shareWith);
747
+            }
748
+
749
+            // If we have a label, use it
750
+            if ($label !== '') {
751
+                if (strlen($label) > 255) {
752
+                    throw new OCSBadRequestException('Maximum label length is 255');
753
+                }
754
+                $share->setLabel($label);
755
+            }
756
+
757
+            if ($sendPasswordByTalk === 'true') {
758
+                if (!$this->appManager->isEnabledForUser('spreed')) {
759
+                    throw new OCSForbiddenException($this->l->t('Sharing %s sending the password by Nextcloud Talk failed because Nextcloud Talk is not enabled', [$node->getPath()]));
760
+                }
761
+
762
+                $share->setSendPasswordByTalk(true);
763
+            }
764
+        } elseif ($shareType === IShare::TYPE_REMOTE) {
765
+            if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) {
766
+                throw new OCSForbiddenException($this->l->t('Sharing %1$s failed because the back end does not allow shares from type %2$s', [$node->getPath(), $shareType]));
767
+            }
768
+
769
+            if ($shareWith === null) {
770
+                throw new OCSNotFoundException($this->l->t('Please specify a valid federated account ID'));
771
+            }
772
+
773
+            $share->setSharedWith($shareWith);
774
+            $share->setPermissions($permissions);
775
+            $share->setSharedWithDisplayName($this->getCachedFederatedDisplayName($shareWith, false));
776
+        } elseif ($shareType === IShare::TYPE_REMOTE_GROUP) {
777
+            if (!$this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
778
+                throw new OCSForbiddenException($this->l->t('Sharing %1$s failed because the back end does not allow shares from type %2$s', [$node->getPath(), $shareType]));
779
+            }
780
+
781
+            if ($shareWith === null) {
782
+                throw new OCSNotFoundException($this->l->t('Please specify a valid federated group ID'));
783
+            }
784
+
785
+            $share->setSharedWith($shareWith);
786
+            $share->setPermissions($permissions);
787
+        } elseif ($shareType === IShare::TYPE_CIRCLE) {
788
+            if (!Server::get(IAppManager::class)->isEnabledForUser('circles') || !class_exists('\OCA\Circles\ShareByCircleProvider')) {
789
+                throw new OCSNotFoundException($this->l->t('You cannot share to a Team if the app is not enabled'));
790
+            }
791
+
792
+            $circle = Circles::detailsCircle($shareWith);
793
+
794
+            // Valid team is required to share
795
+            if ($circle === null) {
796
+                throw new OCSNotFoundException($this->l->t('Please specify a valid team'));
797
+            }
798
+            $share->setSharedWith($shareWith);
799
+            $share->setPermissions($permissions);
800
+        } elseif ($shareType === IShare::TYPE_ROOM) {
801
+            try {
802
+                $this->getRoomShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? '');
803
+            } catch (ContainerExceptionInterface $e) {
804
+                throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support room shares', [$node->getPath()]));
805
+            }
806
+        } elseif ($shareType === IShare::TYPE_DECK) {
807
+            try {
808
+                $this->getDeckShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? '');
809
+            } catch (ContainerExceptionInterface $e) {
810
+                throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support room shares', [$node->getPath()]));
811
+            }
812
+        } else {
813
+            throw new OCSBadRequestException($this->l->t('Unknown share type'));
814
+        }
815
+
816
+        $share->setShareType($shareType);
817
+        $this->checkInheritedAttributes($share);
818
+
819
+        if ($note !== '') {
820
+            $share->setNote($note);
821
+        }
822
+
823
+        try {
824
+            $share = $this->shareManager->createShare($share);
825
+        } catch (HintException $e) {
826
+            $code = $e->getCode() === 0 ? 403 : $e->getCode();
827
+            throw new OCSException($e->getHint(), $code);
828
+        } catch (GenericShareException|\InvalidArgumentException $e) {
829
+            $this->logger->error($e->getMessage(), ['exception' => $e]);
830
+            throw new OCSForbiddenException($e->getMessage(), $e);
831
+        } catch (\Exception $e) {
832
+            $this->logger->error($e->getMessage(), ['exception' => $e]);
833
+            throw new OCSForbiddenException('Failed to create share.', $e);
834
+        }
835
+
836
+        $output = $this->formatShare($share);
837
+
838
+        return new DataResponse($output);
839
+    }
840
+
841
+    /**
842
+     * @param null|Node $node
843
+     * @param boolean $includeTags
844
+     *
845
+     * @return list<Files_SharingShare>
846
+     */
847
+    private function getSharedWithMe($node, bool $includeTags): array {
848
+        $userShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_USER, $node, -1, 0);
849
+        $groupShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_GROUP, $node, -1, 0);
850
+        $circleShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_CIRCLE, $node, -1, 0);
851
+        $roomShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_ROOM, $node, -1, 0);
852
+        $deckShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_DECK, $node, -1, 0);
853
+
854
+        $shares = array_merge($userShares, $groupShares, $circleShares, $roomShares, $deckShares);
855
+
856
+        $filteredShares = array_filter($shares, function (IShare $share) {
857
+            return $share->getShareOwner() !== $this->userId && $share->getSharedBy() !== $this->userId;
858
+        });
859
+
860
+        $formatted = [];
861
+        foreach ($filteredShares as $share) {
862
+            if ($this->canAccessShare($share)) {
863
+                try {
864
+                    $formatted[] = $this->formatShare($share);
865
+                } catch (NotFoundException $e) {
866
+                    // Ignore this share
867
+                }
868
+            }
869
+        }
870
+
871
+        if ($includeTags) {
872
+            $formatted = $this->populateTags($formatted);
873
+        }
874
+
875
+        return $formatted;
876
+    }
877
+
878
+    /**
879
+     * @param Node $folder
880
+     *
881
+     * @return list<Files_SharingShare>
882
+     * @throws OCSBadRequestException
883
+     * @throws NotFoundException
884
+     */
885
+    private function getSharesInDir(Node $folder): array {
886
+        if (!($folder instanceof Folder)) {
887
+            throw new OCSBadRequestException($this->l->t('Not a directory'));
888
+        }
889
+
890
+        $nodes = $folder->getDirectoryListing();
891
+
892
+        /** @var IShare[] $shares */
893
+        $shares = array_reduce($nodes, function ($carry, $node) {
894
+            $carry = array_merge($carry, $this->getAllShares($node, true));
895
+            return $carry;
896
+        }, []);
897
+
898
+        // filter out duplicate shares
899
+        $known = [];
900
+
901
+        $formatted = $miniFormatted = [];
902
+        $resharingRight = false;
903
+        $known = [];
904
+        foreach ($shares as $share) {
905
+            if (in_array($share->getId(), $known) || $share->getSharedWith() === $this->userId) {
906
+                continue;
907
+            }
908
+
909
+            try {
910
+                $format = $this->formatShare($share);
911
+
912
+                $known[] = $share->getId();
913
+                $formatted[] = $format;
914
+                if ($share->getSharedBy() === $this->userId) {
915
+                    $miniFormatted[] = $format;
916
+                }
917
+                if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $folder)) {
918
+                    $resharingRight = true;
919
+                }
920
+            } catch (\Exception $e) {
921
+                //Ignore this share
922
+            }
923
+        }
924
+
925
+        if (!$resharingRight) {
926
+            $formatted = $miniFormatted;
927
+        }
928
+
929
+        return $formatted;
930
+    }
931
+
932
+    /**
933
+     * Get shares of the current user
934
+     *
935
+     * @param string $shared_with_me Only get shares with the current user
936
+     * @param string $reshares Only get shares by the current user and reshares
937
+     * @param string $subfiles Only get all shares in a folder
938
+     * @param string $path Get shares for a specific path
939
+     * @param string $include_tags Include tags in the share
940
+     *
941
+     * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
942
+     * @throws OCSNotFoundException The folder was not found or is inaccessible
943
+     *
944
+     * 200: Shares returned
945
+     */
946
+    #[NoAdminRequired]
947
+    public function getShares(
948
+        string $shared_with_me = 'false',
949
+        string $reshares = 'false',
950
+        string $subfiles = 'false',
951
+        string $path = '',
952
+        string $include_tags = 'false',
953
+    ): DataResponse {
954
+        $node = null;
955
+        if ($path !== '') {
956
+            $userFolder = $this->rootFolder->getUserFolder($this->userId);
957
+            try {
958
+                $node = $userFolder->get($path);
959
+                $this->lock($node);
960
+            } catch (NotFoundException $e) {
961
+                throw new OCSNotFoundException(
962
+                    $this->l->t('Wrong path, file/folder does not exist')
963
+                );
964
+            } catch (LockedException $e) {
965
+                throw new OCSNotFoundException($this->l->t('Could not lock node'));
966
+            }
967
+        }
968
+
969
+        $shares = $this->getFormattedShares(
970
+            $this->userId,
971
+            $node,
972
+            ($shared_with_me === 'true'),
973
+            ($reshares === 'true'),
974
+            ($subfiles === 'true'),
975
+            ($include_tags === 'true')
976
+        );
977
+
978
+        return new DataResponse($shares);
979
+    }
980
+
981
+    private function getLinkSharePermissions(?int $permissions, ?bool $legacyPublicUpload): int {
982
+        $permissions = $permissions ?? Constants::PERMISSION_READ;
983
+
984
+        // Legacy option handling
985
+        if ($legacyPublicUpload !== null) {
986
+            $permissions = $legacyPublicUpload
987
+                ? (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE)
988
+                : Constants::PERMISSION_READ;
989
+        }
990
+
991
+        if ($this->hasPermission($permissions, Constants::PERMISSION_READ)
992
+            && $this->shareManager->outgoingServer2ServerSharesAllowed()
993
+            && $this->appConfig->getValueBool('core', ConfigLexicon::SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES)) {
994
+            $permissions |= Constants::PERMISSION_SHARE;
995
+        }
996
+
997
+        return $permissions;
998
+    }
999
+
1000
+    /**
1001
+     * Helper to check for legacy "publicUpload" handling.
1002
+     * If the value is set to `true` or `false` then true or false are returned.
1003
+     * Otherwise null is returned to indicate that the option was not (or wrong) set.
1004
+     *
1005
+     * @param null|string $legacyPublicUpload The value of `publicUpload`
1006
+     */
1007
+    private function getLegacyPublicUpload(?string $legacyPublicUpload): ?bool {
1008
+        if ($legacyPublicUpload === 'true') {
1009
+            return true;
1010
+        } elseif ($legacyPublicUpload === 'false') {
1011
+            return false;
1012
+        }
1013
+        // Not set at all
1014
+        return null;
1015
+    }
1016
+
1017
+    /**
1018
+     * For link and email shares validate that only allowed combinations are set.
1019
+     *
1020
+     * @throw OCSBadRequestException If permission combination is invalid.
1021
+     * @throw OCSForbiddenException If public upload was forbidden by the administrator.
1022
+     */
1023
+    private function validateLinkSharePermissions(Node $node, int $permissions, ?bool $legacyPublicUpload): void {
1024
+        if ($legacyPublicUpload && ($node instanceof File)) {
1025
+            throw new OCSBadRequestException($this->l->t('Public upload is only possible for publicly shared folders'));
1026
+        }
1027
+
1028
+        // We need at least READ or CREATE (file drop)
1029
+        if (!$this->hasPermission($permissions, Constants::PERMISSION_READ)
1030
+            && !$this->hasPermission($permissions, Constants::PERMISSION_CREATE)) {
1031
+            throw new OCSBadRequestException($this->l->t('Share must at least have READ or CREATE permissions'));
1032
+        }
1033
+
1034
+        // UPDATE and DELETE require a READ permission
1035
+        if (!$this->hasPermission($permissions, Constants::PERMISSION_READ)
1036
+            && ($this->hasPermission($permissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($permissions, Constants::PERMISSION_DELETE))) {
1037
+            throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set'));
1038
+        }
1039
+
1040
+        // Check if public uploading was disabled
1041
+        if ($this->hasPermission($permissions, Constants::PERMISSION_CREATE)
1042
+            && !$this->shareManager->shareApiLinkAllowPublicUpload()) {
1043
+            throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator'));
1044
+        }
1045
+    }
1046
+
1047
+    /**
1048
+     * @param string $viewer
1049
+     * @param Node $node
1050
+     * @param bool $sharedWithMe
1051
+     * @param bool $reShares
1052
+     * @param bool $subFiles
1053
+     * @param bool $includeTags
1054
+     *
1055
+     * @return list<Files_SharingShare>
1056
+     * @throws NotFoundException
1057
+     * @throws OCSBadRequestException
1058
+     */
1059
+    private function getFormattedShares(
1060
+        string $viewer,
1061
+        $node = null,
1062
+        bool $sharedWithMe = false,
1063
+        bool $reShares = false,
1064
+        bool $subFiles = false,
1065
+        bool $includeTags = false,
1066
+    ): array {
1067
+        if ($sharedWithMe) {
1068
+            return $this->getSharedWithMe($node, $includeTags);
1069
+        }
1070
+
1071
+        if ($subFiles) {
1072
+            return $this->getSharesInDir($node);
1073
+        }
1074
+
1075
+        $shares = $this->getSharesFromNode($viewer, $node, $reShares);
1076
+
1077
+        $known = $formatted = $miniFormatted = [];
1078
+        $resharingRight = false;
1079
+        foreach ($shares as $share) {
1080
+            try {
1081
+                $share->getNode();
1082
+            } catch (NotFoundException $e) {
1083
+                /*
1084 1084
 				 * Ignore shares where we can't get the node
1085 1085
 				 * For example deleted shares
1086 1086
 				 */
1087
-				continue;
1088
-			}
1089
-
1090
-			if (in_array($share->getId(), $known)
1091
-				|| ($share->getSharedWith() === $this->userId && $share->getShareType() === IShare::TYPE_USER)) {
1092
-				continue;
1093
-			}
1094
-
1095
-			$known[] = $share->getId();
1096
-			try {
1097
-				/** @var IShare $share */
1098
-				$format = $this->formatShare($share, $node);
1099
-				$formatted[] = $format;
1100
-
1101
-				// let's also build a list of shares created
1102
-				// by the current user only, in case
1103
-				// there is no resharing rights
1104
-				if ($share->getSharedBy() === $this->userId) {
1105
-					$miniFormatted[] = $format;
1106
-				}
1107
-
1108
-				// check if one of those share is shared with me
1109
-				// and if I have resharing rights on it
1110
-				if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $node)) {
1111
-					$resharingRight = true;
1112
-				}
1113
-			} catch (InvalidPathException|NotFoundException $e) {
1114
-			}
1115
-		}
1116
-
1117
-		if (!$resharingRight) {
1118
-			$formatted = $miniFormatted;
1119
-		}
1120
-
1121
-		// fix eventual missing display name from federated shares
1122
-		$formatted = $this->fixMissingDisplayName($formatted);
1123
-
1124
-		if ($includeTags) {
1125
-			$formatted = $this->populateTags($formatted);
1126
-		}
1127
-
1128
-		return $formatted;
1129
-	}
1130
-
1131
-
1132
-	/**
1133
-	 * Get all shares relative to a file, including parent folders shares rights
1134
-	 *
1135
-	 * @param string $path Path all shares will be relative to
1136
-	 *
1137
-	 * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
1138
-	 * @throws InvalidPathException
1139
-	 * @throws NotFoundException
1140
-	 * @throws OCSNotFoundException The given path is invalid
1141
-	 * @throws SharingRightsException
1142
-	 *
1143
-	 * 200: Shares returned
1144
-	 */
1145
-	#[NoAdminRequired]
1146
-	public function getInheritedShares(string $path): DataResponse {
1147
-		// get Node from (string) path.
1148
-		$userFolder = $this->rootFolder->getUserFolder($this->userId);
1149
-		try {
1150
-			$node = $userFolder->get($path);
1151
-			$this->lock($node);
1152
-		} catch (NotFoundException $e) {
1153
-			throw new OCSNotFoundException($this->l->t('Wrong path, file/folder does not exist'));
1154
-		} catch (LockedException $e) {
1155
-			throw new OCSNotFoundException($this->l->t('Could not lock path'));
1156
-		}
1157
-
1158
-		if (!($node->getPermissions() & Constants::PERMISSION_SHARE)) {
1159
-			throw new SharingRightsException($this->l->t('no sharing rights on this item'));
1160
-		}
1161
-
1162
-		// The current top parent we have access to
1163
-		$parent = $node;
1164
-
1165
-		// initiate real owner.
1166
-		$owner = $node->getOwner()
1167
-			->getUID();
1168
-		if (!$this->userManager->userExists($owner)) {
1169
-			return new DataResponse([]);
1170
-		}
1171
-
1172
-		// get node based on the owner, fix owner in case of external storage
1173
-		$userFolder = $this->rootFolder->getUserFolder($owner);
1174
-		if ($node->getId() !== $userFolder->getId() && !$userFolder->isSubNode($node)) {
1175
-			$owner = $node->getOwner()
1176
-				->getUID();
1177
-			$userFolder = $this->rootFolder->getUserFolder($owner);
1178
-			$node = $userFolder->getFirstNodeById($node->getId());
1179
-		}
1180
-		$basePath = $userFolder->getPath();
1181
-
1182
-		// generate node list for each parent folders
1183
-		/** @var Node[] $nodes */
1184
-		$nodes = [];
1185
-		while (true) {
1186
-			$node = $node->getParent();
1187
-			if ($node->getPath() === $basePath) {
1188
-				break;
1189
-			}
1190
-			$nodes[] = $node;
1191
-		}
1192
-
1193
-		// The user that is requesting this list
1194
-		$currentUserFolder = $this->rootFolder->getUserFolder($this->userId);
1195
-
1196
-		// for each nodes, retrieve shares.
1197
-		$shares = [];
1198
-
1199
-		foreach ($nodes as $node) {
1200
-			$getShares = $this->getFormattedShares($owner, $node, false, true);
1201
-
1202
-			$currentUserNode = $currentUserFolder->getFirstNodeById($node->getId());
1203
-			if ($currentUserNode) {
1204
-				$parent = $currentUserNode;
1205
-			}
1206
-
1207
-			$subPath = $currentUserFolder->getRelativePath($parent->getPath());
1208
-			foreach ($getShares as &$share) {
1209
-				$share['via_fileid'] = $parent->getId();
1210
-				$share['via_path'] = $subPath;
1211
-			}
1212
-			$this->mergeFormattedShares($shares, $getShares);
1213
-		}
1214
-
1215
-		return new DataResponse(array_values($shares));
1216
-	}
1217
-
1218
-	/**
1219
-	 * Check whether a set of permissions contains the permissions to check.
1220
-	 */
1221
-	private function hasPermission(int $permissionsSet, int $permissionsToCheck): bool {
1222
-		return ($permissionsSet & $permissionsToCheck) === $permissionsToCheck;
1223
-	}
1224
-
1225
-	/**
1226
-	 * Update a share
1227
-	 *
1228
-	 * @param string $id ID of the share
1229
-	 * @param int|null $permissions New permissions
1230
-	 * @param string|null $password New password
1231
-	 * @param string|null $sendPasswordByTalk New condition if the password should be send over Talk
1232
-	 * @param string|null $publicUpload New condition if public uploading is allowed
1233
-	 * @param string|null $expireDate New expiry date
1234
-	 * @param string|null $note New note
1235
-	 * @param string|null $label New label
1236
-	 * @param string|null $hideDownload New condition if the download should be hidden
1237
-	 * @param string|null $attributes New additional attributes
1238
-	 * @param string|null $sendMail if the share should be send by mail.
1239
-	 *                              Considering the share already exists, no mail will be send after the share is updated.
1240
-	 *                              You will have to use the sendMail action to send the mail.
1241
-	 * @param string|null $shareWith New recipient for email shares
1242
-	 * @param string|null $token New token
1243
-	 * @return DataResponse<Http::STATUS_OK, Files_SharingShare, array{}>
1244
-	 * @throws OCSBadRequestException Share could not be updated because the requested changes are invalid
1245
-	 * @throws OCSForbiddenException Missing permissions to update the share
1246
-	 * @throws OCSNotFoundException Share not found
1247
-	 *
1248
-	 * 200: Share updated successfully
1249
-	 */
1250
-	#[NoAdminRequired]
1251
-	public function updateShare(
1252
-		string $id,
1253
-		?int $permissions = null,
1254
-		?string $password = null,
1255
-		?string $sendPasswordByTalk = null,
1256
-		?string $publicUpload = null,
1257
-		?string $expireDate = null,
1258
-		?string $note = null,
1259
-		?string $label = null,
1260
-		?string $hideDownload = null,
1261
-		?string $attributes = null,
1262
-		?string $sendMail = null,
1263
-		?string $token = null,
1264
-	): DataResponse {
1265
-		try {
1266
-			$share = $this->getShareById($id);
1267
-		} catch (ShareNotFound $e) {
1268
-			throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
1269
-		}
1270
-
1271
-		$this->lock($share->getNode());
1272
-
1273
-		if (!$this->canAccessShare($share, false)) {
1274
-			throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
1275
-		}
1276
-
1277
-		if (!$this->canEditShare($share)) {
1278
-			throw new OCSForbiddenException($this->l->t('You are not allowed to edit incoming shares'));
1279
-		}
1280
-
1281
-		if (
1282
-			$permissions === null
1283
-			&& $password === null
1284
-			&& $sendPasswordByTalk === null
1285
-			&& $publicUpload === null
1286
-			&& $expireDate === null
1287
-			&& $note === null
1288
-			&& $label === null
1289
-			&& $hideDownload === null
1290
-			&& $attributes === null
1291
-			&& $sendMail === null
1292
-			&& $token === null
1293
-		) {
1294
-			throw new OCSBadRequestException($this->l->t('Wrong or no update parameter given'));
1295
-		}
1296
-
1297
-		if ($note !== null) {
1298
-			$share->setNote($note);
1299
-		}
1300
-
1301
-		if ($attributes !== null) {
1302
-			$share = $this->setShareAttributes($share, $attributes);
1303
-		}
1304
-
1305
-		// Handle mail send
1306
-		if ($sendMail === 'true' || $sendMail === 'false') {
1307
-			$share->setMailSend($sendMail === 'true');
1308
-		}
1309
-
1310
-		/**
1311
-		 * expiration date, password and publicUpload only make sense for link shares
1312
-		 */
1313
-		if ($share->getShareType() === IShare::TYPE_LINK
1314
-			|| $share->getShareType() === IShare::TYPE_EMAIL) {
1315
-
1316
-			// Update hide download state
1317
-			if ($hideDownload === 'true') {
1318
-				$share->setHideDownload(true);
1319
-			} elseif ($hideDownload === 'false') {
1320
-				$share->setHideDownload(false);
1321
-			}
1322
-
1323
-			// If either manual permissions are specified or publicUpload
1324
-			// then we need to also update the permissions of the share
1325
-			if ($permissions !== null || $publicUpload !== null) {
1326
-				$hasPublicUpload = $this->getLegacyPublicUpload($publicUpload);
1327
-				$permissions = $this->getLinkSharePermissions($permissions ?? Constants::PERMISSION_READ, $hasPublicUpload);
1328
-				$this->validateLinkSharePermissions($share->getNode(), $permissions, $hasPublicUpload);
1329
-				$share->setPermissions($permissions);
1330
-			}
1331
-
1332
-			$passwordParamSent = $password !== null;
1333
-			if ($passwordParamSent) {
1334
-				if ($password === '') {
1335
-					$share->setPassword(null);
1336
-				} else {
1337
-					$share->setPassword($password);
1338
-				}
1339
-			}
1340
-
1341
-			if ($label !== null) {
1342
-				if (strlen($label) > 255) {
1343
-					throw new OCSBadRequestException('Maximum label length is 255');
1344
-				}
1345
-				$share->setLabel($label);
1346
-			}
1347
-
1348
-			if ($sendPasswordByTalk === 'true') {
1349
-				if (!$this->appManager->isEnabledForUser('spreed')) {
1350
-					throw new OCSForbiddenException($this->l->t('"Sending the password by Nextcloud Talk" for sharing a file or folder failed because Nextcloud Talk is not enabled.'));
1351
-				}
1352
-
1353
-				$share->setSendPasswordByTalk(true);
1354
-			} elseif ($sendPasswordByTalk !== null) {
1355
-				$share->setSendPasswordByTalk(false);
1356
-			}
1357
-
1358
-			if ($token !== null) {
1359
-				if (!$this->shareManager->allowCustomTokens()) {
1360
-					throw new OCSForbiddenException($this->l->t('Custom share link tokens have been disabled by the administrator'));
1361
-				}
1362
-				if (!$this->validateToken($token)) {
1363
-					throw new OCSBadRequestException($this->l->t('Tokens must contain at least 1 character and may only contain letters, numbers, or a hyphen'));
1364
-				}
1365
-				$share->setToken($token);
1366
-			}
1367
-		}
1368
-
1369
-		// NOT A LINK SHARE
1370
-		else {
1371
-			if ($permissions !== null) {
1372
-				$share->setPermissions($permissions);
1373
-			}
1374
-		}
1375
-
1376
-		if ($expireDate === '') {
1377
-			$share->setExpirationDate(null);
1378
-		} elseif ($expireDate !== null) {
1379
-			try {
1380
-				$expireDateTime = $this->parseDate($expireDate);
1381
-				$share->setExpirationDate($expireDateTime);
1382
-			} catch (\Exception $e) {
1383
-				throw new OCSBadRequestException($e->getMessage(), $e);
1384
-			}
1385
-		}
1386
-
1387
-		try {
1388
-			$this->checkInheritedAttributes($share);
1389
-			$share = $this->shareManager->updateShare($share);
1390
-		} catch (HintException $e) {
1391
-			$code = $e->getCode() === 0 ? 403 : $e->getCode();
1392
-			throw new OCSException($e->getHint(), (int)$code);
1393
-		} catch (\Exception $e) {
1394
-			$this->logger->error($e->getMessage(), ['exception' => $e]);
1395
-			throw new OCSBadRequestException('Failed to update share.', $e);
1396
-		}
1397
-
1398
-		return new DataResponse($this->formatShare($share));
1399
-	}
1400
-
1401
-	private function validateToken(string $token): bool {
1402
-		if (mb_strlen($token) === 0) {
1403
-			return false;
1404
-		}
1405
-		if (!preg_match('/^[a-z0-9-]+$/i', $token)) {
1406
-			return false;
1407
-		}
1408
-		return true;
1409
-	}
1410
-
1411
-	/**
1412
-	 * Get all shares that are still pending
1413
-	 *
1414
-	 * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
1415
-	 *
1416
-	 * 200: Pending shares returned
1417
-	 */
1418
-	#[NoAdminRequired]
1419
-	public function pendingShares(): DataResponse {
1420
-		$pendingShares = [];
1421
-
1422
-		$shareTypes = [
1423
-			IShare::TYPE_USER,
1424
-			IShare::TYPE_GROUP
1425
-		];
1426
-
1427
-		foreach ($shareTypes as $shareType) {
1428
-			$shares = $this->shareManager->getSharedWith($this->userId, $shareType, null, -1, 0);
1429
-
1430
-			foreach ($shares as $share) {
1431
-				if ($share->getStatus() === IShare::STATUS_PENDING || $share->getStatus() === IShare::STATUS_REJECTED) {
1432
-					$pendingShares[] = $share;
1433
-				}
1434
-			}
1435
-		}
1436
-
1437
-		$result = array_values(array_filter(array_map(function (IShare $share) {
1438
-			$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
1439
-			$node = $userFolder->getFirstNodeById($share->getNodeId());
1440
-			if (!$node) {
1441
-				// fallback to guessing the path
1442
-				$node = $userFolder->get($share->getTarget());
1443
-				if ($node === null || $share->getTarget() === '') {
1444
-					return null;
1445
-				}
1446
-			}
1447
-
1448
-			try {
1449
-				$formattedShare = $this->formatShare($share, $node);
1450
-				$formattedShare['path'] = '/' . $share->getNode()->getName();
1451
-				$formattedShare['permissions'] = 0;
1452
-				return $formattedShare;
1453
-			} catch (NotFoundException $e) {
1454
-				return null;
1455
-			}
1456
-		}, $pendingShares), function ($entry) {
1457
-			return $entry !== null;
1458
-		}));
1459
-
1460
-		return new DataResponse($result);
1461
-	}
1462
-
1463
-	/**
1464
-	 * Accept a share
1465
-	 *
1466
-	 * @param string $id ID of the share
1467
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1468
-	 * @throws OCSNotFoundException Share not found
1469
-	 * @throws OCSException
1470
-	 * @throws OCSBadRequestException Share could not be accepted
1471
-	 *
1472
-	 * 200: Share accepted successfully
1473
-	 */
1474
-	#[NoAdminRequired]
1475
-	public function acceptShare(string $id): DataResponse {
1476
-		try {
1477
-			$share = $this->getShareById($id);
1478
-		} catch (ShareNotFound $e) {
1479
-			throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
1480
-		}
1481
-
1482
-		if (!$this->canAccessShare($share)) {
1483
-			throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
1484
-		}
1485
-
1486
-		try {
1487
-			$this->shareManager->acceptShare($share, $this->userId);
1488
-		} catch (HintException $e) {
1489
-			$code = $e->getCode() === 0 ? 403 : $e->getCode();
1490
-			throw new OCSException($e->getHint(), (int)$code);
1491
-		} catch (\Exception $e) {
1492
-			$this->logger->error($e->getMessage(), ['exception' => $e]);
1493
-			throw new OCSBadRequestException('Failed to accept share.', $e);
1494
-		}
1495
-
1496
-		return new DataResponse();
1497
-	}
1498
-
1499
-	/**
1500
-	 * Does the user have read permission on the share
1501
-	 *
1502
-	 * @param IShare $share the share to check
1503
-	 * @param boolean $checkGroups check groups as well?
1504
-	 * @return boolean
1505
-	 * @throws NotFoundException
1506
-	 *
1507
-	 * @suppress PhanUndeclaredClassMethod
1508
-	 */
1509
-	protected function canAccessShare(IShare $share, bool $checkGroups = true): bool {
1510
-		// A file with permissions 0 can't be accessed by us. So Don't show it
1511
-		if ($share->getPermissions() === 0) {
1512
-			return false;
1513
-		}
1514
-
1515
-		// Owner of the file and the sharer of the file can always get share
1516
-		if ($share->getShareOwner() === $this->userId
1517
-			|| $share->getSharedBy() === $this->userId) {
1518
-			return true;
1519
-		}
1520
-
1521
-		// If the share is shared with you, you can access it!
1522
-		if ($share->getShareType() === IShare::TYPE_USER
1523
-			&& $share->getSharedWith() === $this->userId) {
1524
-			return true;
1525
-		}
1526
-
1527
-		// Have reshare rights on the shared file/folder ?
1528
-		// Does the currentUser have access to the shared file?
1529
-		$userFolder = $this->rootFolder->getUserFolder($this->userId);
1530
-		$file = $userFolder->getFirstNodeById($share->getNodeId());
1531
-		if ($file && $this->shareProviderResharingRights($this->userId, $share, $file)) {
1532
-			return true;
1533
-		}
1534
-
1535
-		// If in the recipient group, you can see the share
1536
-		if ($checkGroups && $share->getShareType() === IShare::TYPE_GROUP) {
1537
-			$sharedWith = $this->groupManager->get($share->getSharedWith());
1538
-			$user = $this->userManager->get($this->userId);
1539
-			if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) {
1540
-				return true;
1541
-			}
1542
-		}
1543
-
1544
-		if ($share->getShareType() === IShare::TYPE_CIRCLE) {
1545
-			// TODO: have a sanity check like above?
1546
-			return true;
1547
-		}
1548
-
1549
-		if ($share->getShareType() === IShare::TYPE_ROOM) {
1550
-			try {
1551
-				return $this->getRoomShareHelper()->canAccessShare($share, $this->userId);
1552
-			} catch (ContainerExceptionInterface $e) {
1553
-				return false;
1554
-			}
1555
-		}
1556
-
1557
-		if ($share->getShareType() === IShare::TYPE_DECK) {
1558
-			try {
1559
-				return $this->getDeckShareHelper()->canAccessShare($share, $this->userId);
1560
-			} catch (ContainerExceptionInterface $e) {
1561
-				return false;
1562
-			}
1563
-		}
1564
-
1565
-		return false;
1566
-	}
1567
-
1568
-	/**
1569
-	 * Does the user have edit permission on the share
1570
-	 *
1571
-	 * @param IShare $share the share to check
1572
-	 * @return boolean
1573
-	 */
1574
-	protected function canEditShare(IShare $share): bool {
1575
-		// A file with permissions 0 can't be accessed by us. So Don't show it
1576
-		if ($share->getPermissions() === 0) {
1577
-			return false;
1578
-		}
1579
-
1580
-		// The owner of the file and the creator of the share
1581
-		// can always edit the share
1582
-		if ($share->getShareOwner() === $this->userId
1583
-			|| $share->getSharedBy() === $this->userId
1584
-		) {
1585
-			return true;
1586
-		}
1587
-
1588
-		$userFolder = $this->rootFolder->getUserFolder($this->userId);
1589
-		$file = $userFolder->getFirstNodeById($share->getNodeId());
1590
-		if ($file?->getMountPoint() instanceof IShareOwnerlessMount && $this->shareProviderResharingRights($this->userId, $share, $file)) {
1591
-			return true;
1592
-		}
1593
-
1594
-		//! we do NOT support some kind of `admin` in groups.
1595
-		//! You cannot edit shares shared to a group you're
1596
-		//! a member of if you're not the share owner or the file owner!
1597
-
1598
-		return false;
1599
-	}
1600
-
1601
-	/**
1602
-	 * Does the user have delete permission on the share
1603
-	 *
1604
-	 * @param IShare $share the share to check
1605
-	 * @return boolean
1606
-	 */
1607
-	protected function canDeleteShare(IShare $share): bool {
1608
-		// A file with permissions 0 can't be accessed by us. So Don't show it
1609
-		if ($share->getPermissions() === 0) {
1610
-			return false;
1611
-		}
1612
-
1613
-		// if the user is the recipient, i can unshare
1614
-		// the share with self
1615
-		if ($share->getShareType() === IShare::TYPE_USER
1616
-			&& $share->getSharedWith() === $this->userId
1617
-		) {
1618
-			return true;
1619
-		}
1620
-
1621
-		// The owner of the file and the creator of the share
1622
-		// can always delete the share
1623
-		if ($share->getShareOwner() === $this->userId
1624
-			|| $share->getSharedBy() === $this->userId
1625
-		) {
1626
-			return true;
1627
-		}
1628
-
1629
-		$userFolder = $this->rootFolder->getUserFolder($this->userId);
1630
-		$file = $userFolder->getFirstNodeById($share->getNodeId());
1631
-		if ($file?->getMountPoint() instanceof IShareOwnerlessMount && $this->shareProviderResharingRights($this->userId, $share, $file)) {
1632
-			return true;
1633
-		}
1634
-
1635
-		return false;
1636
-	}
1637
-
1638
-	/**
1639
-	 * Does the user have delete permission on the share
1640
-	 * This differs from the canDeleteShare function as it only
1641
-	 * remove the share for the current user. It does NOT
1642
-	 * completely delete the share but only the mount point.
1643
-	 * It can then be restored from the deleted shares section.
1644
-	 *
1645
-	 * @param IShare $share the share to check
1646
-	 * @return boolean
1647
-	 *
1648
-	 * @suppress PhanUndeclaredClassMethod
1649
-	 */
1650
-	protected function canDeleteShareFromSelf(IShare $share): bool {
1651
-		if ($share->getShareType() !== IShare::TYPE_GROUP
1652
-			&& $share->getShareType() !== IShare::TYPE_ROOM
1653
-			&& $share->getShareType() !== IShare::TYPE_DECK
1654
-		) {
1655
-			return false;
1656
-		}
1657
-
1658
-		if ($share->getShareOwner() === $this->userId
1659
-			|| $share->getSharedBy() === $this->userId
1660
-		) {
1661
-			// Delete the whole share, not just for self
1662
-			return false;
1663
-		}
1664
-
1665
-		// If in the recipient group, you can delete the share from self
1666
-		if ($share->getShareType() === IShare::TYPE_GROUP) {
1667
-			$sharedWith = $this->groupManager->get($share->getSharedWith());
1668
-			$user = $this->userManager->get($this->userId);
1669
-			if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) {
1670
-				return true;
1671
-			}
1672
-		}
1673
-
1674
-		if ($share->getShareType() === IShare::TYPE_ROOM) {
1675
-			try {
1676
-				return $this->getRoomShareHelper()->canAccessShare($share, $this->userId);
1677
-			} catch (ContainerExceptionInterface $e) {
1678
-				return false;
1679
-			}
1680
-		}
1681
-
1682
-		if ($share->getShareType() === IShare::TYPE_DECK) {
1683
-			try {
1684
-				return $this->getDeckShareHelper()->canAccessShare($share, $this->userId);
1685
-			} catch (ContainerExceptionInterface $e) {
1686
-				return false;
1687
-			}
1688
-		}
1689
-
1690
-		return false;
1691
-	}
1692
-
1693
-	/**
1694
-	 * Make sure that the passed date is valid ISO 8601
1695
-	 * So YYYY-MM-DD
1696
-	 * If not throw an exception
1697
-	 *
1698
-	 * @param string $expireDate
1699
-	 *
1700
-	 * @throws \Exception
1701
-	 * @return \DateTime
1702
-	 */
1703
-	private function parseDate(string $expireDate): \DateTime {
1704
-		try {
1705
-			$date = new \DateTime(trim($expireDate, '"'), $this->dateTimeZone->getTimeZone());
1706
-			// Make sure it expires at midnight in owner timezone
1707
-			$date->setTime(0, 0, 0);
1708
-		} catch (\Exception $e) {
1709
-			throw new \Exception($this->l->t('Invalid date. Format must be YYYY-MM-DD'));
1710
-		}
1711
-
1712
-		return $date;
1713
-	}
1714
-
1715
-	/**
1716
-	 * Since we have multiple providers but the OCS Share API v1 does
1717
-	 * not support this we need to check all backends.
1718
-	 *
1719
-	 * @param string $id
1720
-	 * @return IShare
1721
-	 * @throws ShareNotFound
1722
-	 */
1723
-	private function getShareById(string $id): IShare {
1724
-		$providers = [
1725
-			'ocinternal' => null, // No type check needed
1726
-			'ocCircleShare' => IShare::TYPE_CIRCLE,
1727
-			'ocMailShare' => IShare::TYPE_EMAIL,
1728
-			'ocRoomShare' => null,
1729
-			'deck' => IShare::TYPE_DECK,
1730
-		];
1731
-
1732
-		// Add federated sharing as a provider only if it's allowed
1733
-		if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
1734
-			$providers['ocFederatedSharing'] = null; // No type check needed
1735
-		}
1736
-
1737
-		foreach ($providers as $prefix => $type) {
1738
-			try {
1739
-				if ($type === null || $this->shareManager->shareProviderExists($type)) {
1740
-					return $this->shareManager->getShareById($prefix . ':' . $id, $this->userId);
1741
-				}
1742
-			} catch (ShareNotFound $e) {
1743
-				// Do nothing, continue to next provider
1744
-			} catch (\Exception $e) {
1745
-				$this->logger->warning('Unexpected error in share provider', [
1746
-					'shareId' => $id,
1747
-					'provider' => $prefix,
1748
-					'exception' => $e,
1749
-				]);
1750
-			}
1751
-		}
1752
-		throw new ShareNotFound();
1753
-	}
1754
-
1755
-	/**
1756
-	 * Lock a Node
1757
-	 *
1758
-	 * @param Node $node
1759
-	 * @throws LockedException
1760
-	 */
1761
-	private function lock(Node $node) {
1762
-		$node->lock(ILockingProvider::LOCK_SHARED);
1763
-		$this->lockedNode = $node;
1764
-	}
1765
-
1766
-	/**
1767
-	 * Cleanup the remaining locks
1768
-	 * @throws LockedException
1769
-	 */
1770
-	public function cleanup() {
1771
-		if ($this->lockedNode !== null) {
1772
-			$this->lockedNode->unlock(ILockingProvider::LOCK_SHARED);
1773
-		}
1774
-	}
1775
-
1776
-	/**
1777
-	 * Returns the helper of ShareAPIController for room shares.
1778
-	 *
1779
-	 * If the Talk application is not enabled or the helper is not available
1780
-	 * a ContainerExceptionInterface is thrown instead.
1781
-	 *
1782
-	 * @return \OCA\Talk\Share\Helper\ShareAPIController
1783
-	 * @throws ContainerExceptionInterface
1784
-	 */
1785
-	private function getRoomShareHelper() {
1786
-		if (!$this->appManager->isEnabledForUser('spreed')) {
1787
-			throw new QueryException();
1788
-		}
1789
-
1790
-		return $this->serverContainer->get('\OCA\Talk\Share\Helper\ShareAPIController');
1791
-	}
1792
-
1793
-	/**
1794
-	 * Returns the helper of ShareAPIHelper for deck shares.
1795
-	 *
1796
-	 * If the Deck application is not enabled or the helper is not available
1797
-	 * a ContainerExceptionInterface is thrown instead.
1798
-	 *
1799
-	 * @return ShareAPIHelper
1800
-	 * @throws ContainerExceptionInterface
1801
-	 */
1802
-	private function getDeckShareHelper() {
1803
-		if (!$this->appManager->isEnabledForUser('deck')) {
1804
-			throw new QueryException();
1805
-		}
1806
-
1807
-		return $this->serverContainer->get('\OCA\Deck\Sharing\ShareAPIHelper');
1808
-	}
1809
-
1810
-	/**
1811
-	 * Returns the helper of ShareAPIHelper for sciencemesh shares.
1812
-	 *
1813
-	 * If the sciencemesh application is not enabled or the helper is not available
1814
-	 * a ContainerExceptionInterface is thrown instead.
1815
-	 *
1816
-	 * @return ShareAPIHelper
1817
-	 * @throws ContainerExceptionInterface
1818
-	 */
1819
-	private function getSciencemeshShareHelper() {
1820
-		if (!$this->appManager->isEnabledForUser('sciencemesh')) {
1821
-			throw new QueryException();
1822
-		}
1823
-
1824
-		return $this->serverContainer->get('\OCA\ScienceMesh\Sharing\ShareAPIHelper');
1825
-	}
1826
-
1827
-	/**
1828
-	 * @param string $viewer
1829
-	 * @param Node $node
1830
-	 * @param bool $reShares
1831
-	 *
1832
-	 * @return IShare[]
1833
-	 */
1834
-	private function getSharesFromNode(string $viewer, $node, bool $reShares): array {
1835
-		$providers = [
1836
-			IShare::TYPE_USER,
1837
-			IShare::TYPE_GROUP,
1838
-			IShare::TYPE_LINK,
1839
-			IShare::TYPE_EMAIL,
1840
-			IShare::TYPE_CIRCLE,
1841
-			IShare::TYPE_ROOM,
1842
-			IShare::TYPE_DECK,
1843
-		];
1844
-
1845
-		// Should we assume that the (currentUser) viewer is the owner of the node !?
1846
-		$shares = [];
1847
-		foreach ($providers as $provider) {
1848
-			if (!$this->shareManager->shareProviderExists($provider)) {
1849
-				continue;
1850
-			}
1851
-
1852
-			$providerShares
1853
-				= $this->shareManager->getSharesBy($viewer, $provider, $node, $reShares, -1, 0);
1854
-			$shares = array_merge($shares, $providerShares);
1855
-		}
1856
-
1857
-		if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
1858
-			$federatedShares = $this->shareManager->getSharesBy(
1859
-				$this->userId, IShare::TYPE_REMOTE, $node, $reShares, -1, 0
1860
-			);
1861
-			$shares = array_merge($shares, $federatedShares);
1862
-		}
1863
-
1864
-		if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
1865
-			$federatedShares = $this->shareManager->getSharesBy(
1866
-				$this->userId, IShare::TYPE_REMOTE_GROUP, $node, $reShares, -1, 0
1867
-			);
1868
-			$shares = array_merge($shares, $federatedShares);
1869
-		}
1870
-
1871
-		return $shares;
1872
-	}
1873
-
1874
-
1875
-	/**
1876
-	 * @param Node $node
1877
-	 *
1878
-	 * @throws SharingRightsException
1879
-	 */
1880
-	private function confirmSharingRights(Node $node): void {
1881
-		if (!$this->hasResharingRights($this->userId, $node)) {
1882
-			throw new SharingRightsException($this->l->t('No sharing rights on this item'));
1883
-		}
1884
-	}
1885
-
1886
-
1887
-	/**
1888
-	 * @param string $viewer
1889
-	 * @param Node $node
1890
-	 *
1891
-	 * @return bool
1892
-	 */
1893
-	private function hasResharingRights($viewer, $node): bool {
1894
-		if ($viewer === $node->getOwner()->getUID()) {
1895
-			return true;
1896
-		}
1897
-
1898
-		foreach ([$node, $node->getParent()] as $node) {
1899
-			$shares = $this->getSharesFromNode($viewer, $node, true);
1900
-			foreach ($shares as $share) {
1901
-				try {
1902
-					if ($this->shareProviderResharingRights($viewer, $share, $node)) {
1903
-						return true;
1904
-					}
1905
-				} catch (InvalidPathException|NotFoundException $e) {
1906
-				}
1907
-			}
1908
-		}
1909
-
1910
-		return false;
1911
-	}
1912
-
1913
-
1914
-	/**
1915
-	 * Returns if we can find resharing rights in an IShare object for a specific user.
1916
-	 *
1917
-	 * @suppress PhanUndeclaredClassMethod
1918
-	 *
1919
-	 * @param string $userId
1920
-	 * @param IShare $share
1921
-	 * @param Node $node
1922
-	 *
1923
-	 * @return bool
1924
-	 * @throws NotFoundException
1925
-	 * @throws InvalidPathException
1926
-	 */
1927
-	private function shareProviderResharingRights(string $userId, IShare $share, $node): bool {
1928
-		if ($share->getShareOwner() === $userId) {
1929
-			return true;
1930
-		}
1931
-
1932
-		// we check that current user have parent resharing rights on the current file
1933
-		if ($node !== null && ($node->getPermissions() & Constants::PERMISSION_SHARE) !== 0) {
1934
-			return true;
1935
-		}
1936
-
1937
-		if ((Constants::PERMISSION_SHARE & $share->getPermissions()) === 0) {
1938
-			return false;
1939
-		}
1940
-
1941
-		if ($share->getShareType() === IShare::TYPE_USER && $share->getSharedWith() === $userId) {
1942
-			return true;
1943
-		}
1944
-
1945
-		if ($share->getShareType() === IShare::TYPE_GROUP && $this->groupManager->isInGroup($userId, $share->getSharedWith())) {
1946
-			return true;
1947
-		}
1948
-
1949
-		if ($share->getShareType() === IShare::TYPE_CIRCLE && Server::get(IAppManager::class)->isEnabledForUser('circles')
1950
-			&& class_exists('\OCA\Circles\Api\v1\Circles')) {
1951
-			$hasCircleId = (str_ends_with($share->getSharedWith(), ']'));
1952
-			$shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0);
1953
-			$shareWithLength = ($hasCircleId ? -1 : strpos($share->getSharedWith(), ' '));
1954
-			if ($shareWithLength === false) {
1955
-				$sharedWith = substr($share->getSharedWith(), $shareWithStart);
1956
-			} else {
1957
-				$sharedWith = substr($share->getSharedWith(), $shareWithStart, $shareWithLength);
1958
-			}
1959
-			try {
1960
-				$member = Circles::getMember($sharedWith, $userId, 1);
1961
-				if ($member->getLevel() >= 4) {
1962
-					return true;
1963
-				}
1964
-				return false;
1965
-			} catch (ContainerExceptionInterface $e) {
1966
-				return false;
1967
-			}
1968
-		}
1969
-
1970
-		return false;
1971
-	}
1972
-
1973
-	/**
1974
-	 * Get all the shares for the current user
1975
-	 *
1976
-	 * @param Node|null $path
1977
-	 * @param boolean $reshares
1978
-	 * @return IShare[]
1979
-	 */
1980
-	private function getAllShares(?Node $path = null, bool $reshares = false) {
1981
-		// Get all shares
1982
-		$userShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_USER, $path, $reshares, -1, 0);
1983
-		$groupShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_GROUP, $path, $reshares, -1, 0);
1984
-		$linkShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_LINK, $path, $reshares, -1, 0);
1985
-
1986
-		// EMAIL SHARES
1987
-		$mailShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_EMAIL, $path, $reshares, -1, 0);
1988
-
1989
-		// TEAM SHARES
1990
-		$circleShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_CIRCLE, $path, $reshares, -1, 0);
1991
-
1992
-		// TALK SHARES
1993
-		$roomShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_ROOM, $path, $reshares, -1, 0);
1994
-
1995
-		// DECK SHARES
1996
-		$deckShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_DECK, $path, $reshares, -1, 0);
1997
-
1998
-		// FEDERATION
1999
-		if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
2000
-			$federatedShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_REMOTE, $path, $reshares, -1, 0);
2001
-		} else {
2002
-			$federatedShares = [];
2003
-		}
2004
-		if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
2005
-			$federatedGroupShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_REMOTE_GROUP, $path, $reshares, -1, 0);
2006
-		} else {
2007
-			$federatedGroupShares = [];
2008
-		}
2009
-
2010
-		return array_merge($userShares, $groupShares, $linkShares, $mailShares, $circleShares, $roomShares, $deckShares, $federatedShares, $federatedGroupShares);
2011
-	}
2012
-
2013
-
2014
-	/**
2015
-	 * merging already formatted shares.
2016
-	 * We'll make an associative array to easily detect duplicate Ids.
2017
-	 * Keys _needs_ to be removed after all shares are retrieved and merged.
2018
-	 *
2019
-	 * @param array $shares
2020
-	 * @param array $newShares
2021
-	 */
2022
-	private function mergeFormattedShares(array &$shares, array $newShares) {
2023
-		foreach ($newShares as $newShare) {
2024
-			if (!array_key_exists($newShare['id'], $shares)) {
2025
-				$shares[$newShare['id']] = $newShare;
2026
-			}
2027
-		}
2028
-	}
2029
-
2030
-	/**
2031
-	 * @param IShare $share
2032
-	 * @param string|null $attributesString
2033
-	 * @return IShare modified share
2034
-	 */
2035
-	private function setShareAttributes(IShare $share, ?string $attributesString) {
2036
-		$newShareAttributes = null;
2037
-		if ($attributesString !== null) {
2038
-			$newShareAttributes = $this->shareManager->newShare()->newAttributes();
2039
-			$formattedShareAttributes = \json_decode($attributesString, true);
2040
-			if (is_array($formattedShareAttributes)) {
2041
-				foreach ($formattedShareAttributes as $formattedAttr) {
2042
-					$newShareAttributes->setAttribute(
2043
-						$formattedAttr['scope'],
2044
-						$formattedAttr['key'],
2045
-						$formattedAttr['value'],
2046
-					);
2047
-				}
2048
-			} else {
2049
-				throw new OCSBadRequestException($this->l->t('Invalid share attributes provided: "%s"', [$attributesString]));
2050
-			}
2051
-		}
2052
-		$share->setAttributes($newShareAttributes);
2053
-
2054
-		return $share;
2055
-	}
2056
-
2057
-	private function checkInheritedAttributes(IShare $share): void {
2058
-		if (!$share->getSharedBy()) {
2059
-			return; // Probably in a test
2060
-		}
2061
-
2062
-		$canDownload = false;
2063
-		$hideDownload = true;
2064
-		$userExplicitlySetHideDownload = $share->getHideDownload(); // Capture user's explicit choice
2065
-
2066
-		$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
2067
-		$nodes = $userFolder->getById($share->getNodeId());
2068
-		foreach ($nodes as $node) {
2069
-			// Owner always can download it - so allow it, but respect their explicit choice about hiding downloads
2070
-			if ($node->getOwner()?->getUID() === $share->getSharedBy()) {
2071
-				$canDownload = true;
2072
-				$hideDownload = $userExplicitlySetHideDownload;
2073
-				break;
2074
-			}
2075
-
2076
-			if ($node->getStorage()->instanceOfStorage(SharedStorage::class)) {
2077
-				$storage = $node->getStorage();
2078
-				if ($storage instanceof Wrapper) {
2079
-					$storage = $storage->getInstanceOfStorage(SharedStorage::class);
2080
-					if ($storage === null) {
2081
-						throw new \RuntimeException('Should not happen, instanceOfStorage but getInstanceOfStorage return null');
2082
-					}
2083
-				} else {
2084
-					throw new \RuntimeException('Should not happen, instanceOfStorage but not a wrapper');
2085
-				}
2086
-
2087
-				/** @var SharedStorage $storage */
2088
-				$originalShare = $storage->getShare();
2089
-				$inheritedAttributes = $originalShare->getAttributes();
2090
-
2091
-				// For federated shares: users can only be MORE restrictive, never LESS restrictive
2092
-				// If parent has hideDownload=true, child MUST have hideDownload=true
2093
-				$parentHidesDownload = $originalShare->getHideDownload();
2094
-
2095
-				// Check if download permission is available from parent
2096
-				$parentAllowsDownload = $inheritedAttributes === null || $inheritedAttributes->getAttribute('permissions', 'download') !== false;
2097
-
2098
-				// Apply inheritance rules:
2099
-				// 1. If parent hides download, child must hide download
2100
-				// 2. If parent allows download, child can choose to hide or allow
2101
-				// 3. If parent forbids download, child cannot allow download
2102
-				$hideDownload = $parentHidesDownload || $userExplicitlySetHideDownload;
2103
-
2104
-				$canDownload = $canDownload || $parentAllowsDownload;
2105
-
2106
-			} elseif ($node->getStorage()->instanceOfStorage(Storage::class)) {
2107
-				$canDownload = true; // in case of federation storage, we can expect the download to be activated by default
2108
-				// For external federation storage, respect user's choice if downloads are available
2109
-				$hideDownload = $userExplicitlySetHideDownload;
2110
-			}
2111
-		}
2112
-
2113
-		// Apply the final restrictions:
2114
-		// 1. If parent doesn't allow downloads at all, force hide and disable download attribute
2115
-		// 2. If parent allows downloads, respect user's hideDownload choice
2116
-		if (!$canDownload) {
2117
-			// Parent completely forbids downloads - must enforce this restriction
2118
-			$share->setHideDownload(true);
2119
-			$attributes = $share->getAttributes() ?? $share->newAttributes();
2120
-			$attributes->setAttribute('permissions', 'download', false);
2121
-			$share->setAttributes($attributes);
2122
-		} elseif ($hideDownload) {
2123
-			// Either parent forces hide, or user chooses to hide - respect this
2124
-			$share->setHideDownload(true);
2125
-		} else {
2126
-			// User explicitly wants to allow downloads and parent permits it
2127
-			$share->setHideDownload(false);
2128
-		}
2129
-	}
2130
-
2131
-	/**
2132
-	 * Send a mail notification again for a share.
2133
-	 * The mail_send option must be enabled for the given share.
2134
-	 * @param string $id the share ID
2135
-	 * @param string $password the password to check against. Necessary for password protected shares.
2136
-	 * @throws OCSNotFoundException Share not found
2137
-	 * @throws OCSForbiddenException You are not allowed to send mail notifications
2138
-	 * @throws OCSBadRequestException Invalid request or wrong password
2139
-	 * @throws OCSException Error while sending mail notification
2140
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
2141
-	 *
2142
-	 * 200: The email notification was sent successfully
2143
-	 */
2144
-	#[NoAdminRequired]
2145
-	#[UserRateLimit(limit: 10, period: 600)]
2146
-	public function sendShareEmail(string $id, $password = ''): DataResponse {
2147
-		try {
2148
-			$share = $this->getShareById($id);
2149
-
2150
-			if (!$this->canAccessShare($share, false)) {
2151
-				throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
2152
-			}
2153
-
2154
-			if (!$this->canEditShare($share)) {
2155
-				throw new OCSForbiddenException($this->l->t('You are not allowed to send mail notifications'));
2156
-			}
2157
-
2158
-			// For mail and link shares, the user must be
2159
-			// the owner of the share, not only the file owner.
2160
-			if ($share->getShareType() === IShare::TYPE_EMAIL
2161
-				|| $share->getShareType() === IShare::TYPE_LINK) {
2162
-				if ($share->getSharedBy() !== $this->userId) {
2163
-					throw new OCSForbiddenException($this->l->t('You are not allowed to send mail notifications'));
2164
-				}
2165
-			}
2166
-
2167
-			try {
2168
-				$provider = $this->factory->getProviderForType($share->getShareType());
2169
-				if (!($provider instanceof IShareProviderWithNotification)) {
2170
-					throw new OCSBadRequestException($this->l->t('No mail notification configured for this share type'));
2171
-				}
2172
-
2173
-				// Circumvent the password encrypted data by
2174
-				// setting the password clear. We're not storing
2175
-				// the password clear, it is just a temporary
2176
-				// object manipulation. The password will stay
2177
-				// encrypted in the database.
2178
-				if ($share->getPassword() !== null && $share->getPassword() !== $password) {
2179
-					if (!$this->shareManager->checkPassword($share, $password)) {
2180
-						throw new OCSBadRequestException($this->l->t('Wrong password'));
2181
-					}
2182
-					$share = $share->setPassword($password);
2183
-				}
2184
-
2185
-				$provider->sendMailNotification($share);
2186
-				return new DataResponse();
2187
-			} catch (Exception $e) {
2188
-				$this->logger->error($e->getMessage(), ['exception' => $e]);
2189
-				throw new OCSException($this->l->t('Error while sending mail notification'));
2190
-			}
2191
-
2192
-		} catch (ShareNotFound $e) {
2193
-			throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
2194
-		}
2195
-	}
2196
-
2197
-	/**
2198
-	 * Get a unique share token
2199
-	 *
2200
-	 * @throws OCSException Failed to generate a unique token
2201
-	 *
2202
-	 * @return DataResponse<Http::STATUS_OK, array{token: string}, array{}>
2203
-	 *
2204
-	 * 200: Token generated successfully
2205
-	 */
2206
-	#[ApiRoute(verb: 'GET', url: '/api/v1/token')]
2207
-	#[NoAdminRequired]
2208
-	public function generateToken(): DataResponse {
2209
-		try {
2210
-			$token = $this->shareManager->generateToken();
2211
-			return new DataResponse([
2212
-				'token' => $token,
2213
-			]);
2214
-		} catch (ShareTokenException $e) {
2215
-			throw new OCSException($this->l->t('Failed to generate a unique token'));
2216
-		}
2217
-	}
2218
-
2219
-	/**
2220
-	 * Populate the result set with file tags
2221
-	 *
2222
-	 * @psalm-template T of array{tags?: list<string>, file_source: int, ...array<string, mixed>}
2223
-	 * @param list<T> $fileList
2224
-	 * @return list<T> file list populated with tags
2225
-	 */
2226
-	private function populateTags(array $fileList): array {
2227
-		$tagger = $this->tagManager->load('files');
2228
-		$tags = $tagger->getTagsForObjects(array_map(static fn (array $fileData) => $fileData['file_source'], $fileList));
2229
-
2230
-		if (!is_array($tags)) {
2231
-			throw new \UnexpectedValueException('$tags must be an array');
2232
-		}
2233
-
2234
-		// Set empty tag array
2235
-		foreach ($fileList as &$fileData) {
2236
-			$fileData['tags'] = [];
2237
-		}
2238
-		unset($fileData);
2239
-
2240
-		if (!empty($tags)) {
2241
-			foreach ($tags as $fileId => $fileTags) {
2242
-				foreach ($fileList as &$fileData) {
2243
-					if ($fileId !== $fileData['file_source']) {
2244
-						continue;
2245
-					}
2246
-
2247
-					$fileData['tags'] = $fileTags;
2248
-				}
2249
-				unset($fileData);
2250
-			}
2251
-		}
2252
-
2253
-		return $fileList;
2254
-	}
1087
+                continue;
1088
+            }
1089
+
1090
+            if (in_array($share->getId(), $known)
1091
+                || ($share->getSharedWith() === $this->userId && $share->getShareType() === IShare::TYPE_USER)) {
1092
+                continue;
1093
+            }
1094
+
1095
+            $known[] = $share->getId();
1096
+            try {
1097
+                /** @var IShare $share */
1098
+                $format = $this->formatShare($share, $node);
1099
+                $formatted[] = $format;
1100
+
1101
+                // let's also build a list of shares created
1102
+                // by the current user only, in case
1103
+                // there is no resharing rights
1104
+                if ($share->getSharedBy() === $this->userId) {
1105
+                    $miniFormatted[] = $format;
1106
+                }
1107
+
1108
+                // check if one of those share is shared with me
1109
+                // and if I have resharing rights on it
1110
+                if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $node)) {
1111
+                    $resharingRight = true;
1112
+                }
1113
+            } catch (InvalidPathException|NotFoundException $e) {
1114
+            }
1115
+        }
1116
+
1117
+        if (!$resharingRight) {
1118
+            $formatted = $miniFormatted;
1119
+        }
1120
+
1121
+        // fix eventual missing display name from federated shares
1122
+        $formatted = $this->fixMissingDisplayName($formatted);
1123
+
1124
+        if ($includeTags) {
1125
+            $formatted = $this->populateTags($formatted);
1126
+        }
1127
+
1128
+        return $formatted;
1129
+    }
1130
+
1131
+
1132
+    /**
1133
+     * Get all shares relative to a file, including parent folders shares rights
1134
+     *
1135
+     * @param string $path Path all shares will be relative to
1136
+     *
1137
+     * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
1138
+     * @throws InvalidPathException
1139
+     * @throws NotFoundException
1140
+     * @throws OCSNotFoundException The given path is invalid
1141
+     * @throws SharingRightsException
1142
+     *
1143
+     * 200: Shares returned
1144
+     */
1145
+    #[NoAdminRequired]
1146
+    public function getInheritedShares(string $path): DataResponse {
1147
+        // get Node from (string) path.
1148
+        $userFolder = $this->rootFolder->getUserFolder($this->userId);
1149
+        try {
1150
+            $node = $userFolder->get($path);
1151
+            $this->lock($node);
1152
+        } catch (NotFoundException $e) {
1153
+            throw new OCSNotFoundException($this->l->t('Wrong path, file/folder does not exist'));
1154
+        } catch (LockedException $e) {
1155
+            throw new OCSNotFoundException($this->l->t('Could not lock path'));
1156
+        }
1157
+
1158
+        if (!($node->getPermissions() & Constants::PERMISSION_SHARE)) {
1159
+            throw new SharingRightsException($this->l->t('no sharing rights on this item'));
1160
+        }
1161
+
1162
+        // The current top parent we have access to
1163
+        $parent = $node;
1164
+
1165
+        // initiate real owner.
1166
+        $owner = $node->getOwner()
1167
+            ->getUID();
1168
+        if (!$this->userManager->userExists($owner)) {
1169
+            return new DataResponse([]);
1170
+        }
1171
+
1172
+        // get node based on the owner, fix owner in case of external storage
1173
+        $userFolder = $this->rootFolder->getUserFolder($owner);
1174
+        if ($node->getId() !== $userFolder->getId() && !$userFolder->isSubNode($node)) {
1175
+            $owner = $node->getOwner()
1176
+                ->getUID();
1177
+            $userFolder = $this->rootFolder->getUserFolder($owner);
1178
+            $node = $userFolder->getFirstNodeById($node->getId());
1179
+        }
1180
+        $basePath = $userFolder->getPath();
1181
+
1182
+        // generate node list for each parent folders
1183
+        /** @var Node[] $nodes */
1184
+        $nodes = [];
1185
+        while (true) {
1186
+            $node = $node->getParent();
1187
+            if ($node->getPath() === $basePath) {
1188
+                break;
1189
+            }
1190
+            $nodes[] = $node;
1191
+        }
1192
+
1193
+        // The user that is requesting this list
1194
+        $currentUserFolder = $this->rootFolder->getUserFolder($this->userId);
1195
+
1196
+        // for each nodes, retrieve shares.
1197
+        $shares = [];
1198
+
1199
+        foreach ($nodes as $node) {
1200
+            $getShares = $this->getFormattedShares($owner, $node, false, true);
1201
+
1202
+            $currentUserNode = $currentUserFolder->getFirstNodeById($node->getId());
1203
+            if ($currentUserNode) {
1204
+                $parent = $currentUserNode;
1205
+            }
1206
+
1207
+            $subPath = $currentUserFolder->getRelativePath($parent->getPath());
1208
+            foreach ($getShares as &$share) {
1209
+                $share['via_fileid'] = $parent->getId();
1210
+                $share['via_path'] = $subPath;
1211
+            }
1212
+            $this->mergeFormattedShares($shares, $getShares);
1213
+        }
1214
+
1215
+        return new DataResponse(array_values($shares));
1216
+    }
1217
+
1218
+    /**
1219
+     * Check whether a set of permissions contains the permissions to check.
1220
+     */
1221
+    private function hasPermission(int $permissionsSet, int $permissionsToCheck): bool {
1222
+        return ($permissionsSet & $permissionsToCheck) === $permissionsToCheck;
1223
+    }
1224
+
1225
+    /**
1226
+     * Update a share
1227
+     *
1228
+     * @param string $id ID of the share
1229
+     * @param int|null $permissions New permissions
1230
+     * @param string|null $password New password
1231
+     * @param string|null $sendPasswordByTalk New condition if the password should be send over Talk
1232
+     * @param string|null $publicUpload New condition if public uploading is allowed
1233
+     * @param string|null $expireDate New expiry date
1234
+     * @param string|null $note New note
1235
+     * @param string|null $label New label
1236
+     * @param string|null $hideDownload New condition if the download should be hidden
1237
+     * @param string|null $attributes New additional attributes
1238
+     * @param string|null $sendMail if the share should be send by mail.
1239
+     *                              Considering the share already exists, no mail will be send after the share is updated.
1240
+     *                              You will have to use the sendMail action to send the mail.
1241
+     * @param string|null $shareWith New recipient for email shares
1242
+     * @param string|null $token New token
1243
+     * @return DataResponse<Http::STATUS_OK, Files_SharingShare, array{}>
1244
+     * @throws OCSBadRequestException Share could not be updated because the requested changes are invalid
1245
+     * @throws OCSForbiddenException Missing permissions to update the share
1246
+     * @throws OCSNotFoundException Share not found
1247
+     *
1248
+     * 200: Share updated successfully
1249
+     */
1250
+    #[NoAdminRequired]
1251
+    public function updateShare(
1252
+        string $id,
1253
+        ?int $permissions = null,
1254
+        ?string $password = null,
1255
+        ?string $sendPasswordByTalk = null,
1256
+        ?string $publicUpload = null,
1257
+        ?string $expireDate = null,
1258
+        ?string $note = null,
1259
+        ?string $label = null,
1260
+        ?string $hideDownload = null,
1261
+        ?string $attributes = null,
1262
+        ?string $sendMail = null,
1263
+        ?string $token = null,
1264
+    ): DataResponse {
1265
+        try {
1266
+            $share = $this->getShareById($id);
1267
+        } catch (ShareNotFound $e) {
1268
+            throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
1269
+        }
1270
+
1271
+        $this->lock($share->getNode());
1272
+
1273
+        if (!$this->canAccessShare($share, false)) {
1274
+            throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
1275
+        }
1276
+
1277
+        if (!$this->canEditShare($share)) {
1278
+            throw new OCSForbiddenException($this->l->t('You are not allowed to edit incoming shares'));
1279
+        }
1280
+
1281
+        if (
1282
+            $permissions === null
1283
+            && $password === null
1284
+            && $sendPasswordByTalk === null
1285
+            && $publicUpload === null
1286
+            && $expireDate === null
1287
+            && $note === null
1288
+            && $label === null
1289
+            && $hideDownload === null
1290
+            && $attributes === null
1291
+            && $sendMail === null
1292
+            && $token === null
1293
+        ) {
1294
+            throw new OCSBadRequestException($this->l->t('Wrong or no update parameter given'));
1295
+        }
1296
+
1297
+        if ($note !== null) {
1298
+            $share->setNote($note);
1299
+        }
1300
+
1301
+        if ($attributes !== null) {
1302
+            $share = $this->setShareAttributes($share, $attributes);
1303
+        }
1304
+
1305
+        // Handle mail send
1306
+        if ($sendMail === 'true' || $sendMail === 'false') {
1307
+            $share->setMailSend($sendMail === 'true');
1308
+        }
1309
+
1310
+        /**
1311
+         * expiration date, password and publicUpload only make sense for link shares
1312
+         */
1313
+        if ($share->getShareType() === IShare::TYPE_LINK
1314
+            || $share->getShareType() === IShare::TYPE_EMAIL) {
1315
+
1316
+            // Update hide download state
1317
+            if ($hideDownload === 'true') {
1318
+                $share->setHideDownload(true);
1319
+            } elseif ($hideDownload === 'false') {
1320
+                $share->setHideDownload(false);
1321
+            }
1322
+
1323
+            // If either manual permissions are specified or publicUpload
1324
+            // then we need to also update the permissions of the share
1325
+            if ($permissions !== null || $publicUpload !== null) {
1326
+                $hasPublicUpload = $this->getLegacyPublicUpload($publicUpload);
1327
+                $permissions = $this->getLinkSharePermissions($permissions ?? Constants::PERMISSION_READ, $hasPublicUpload);
1328
+                $this->validateLinkSharePermissions($share->getNode(), $permissions, $hasPublicUpload);
1329
+                $share->setPermissions($permissions);
1330
+            }
1331
+
1332
+            $passwordParamSent = $password !== null;
1333
+            if ($passwordParamSent) {
1334
+                if ($password === '') {
1335
+                    $share->setPassword(null);
1336
+                } else {
1337
+                    $share->setPassword($password);
1338
+                }
1339
+            }
1340
+
1341
+            if ($label !== null) {
1342
+                if (strlen($label) > 255) {
1343
+                    throw new OCSBadRequestException('Maximum label length is 255');
1344
+                }
1345
+                $share->setLabel($label);
1346
+            }
1347
+
1348
+            if ($sendPasswordByTalk === 'true') {
1349
+                if (!$this->appManager->isEnabledForUser('spreed')) {
1350
+                    throw new OCSForbiddenException($this->l->t('"Sending the password by Nextcloud Talk" for sharing a file or folder failed because Nextcloud Talk is not enabled.'));
1351
+                }
1352
+
1353
+                $share->setSendPasswordByTalk(true);
1354
+            } elseif ($sendPasswordByTalk !== null) {
1355
+                $share->setSendPasswordByTalk(false);
1356
+            }
1357
+
1358
+            if ($token !== null) {
1359
+                if (!$this->shareManager->allowCustomTokens()) {
1360
+                    throw new OCSForbiddenException($this->l->t('Custom share link tokens have been disabled by the administrator'));
1361
+                }
1362
+                if (!$this->validateToken($token)) {
1363
+                    throw new OCSBadRequestException($this->l->t('Tokens must contain at least 1 character and may only contain letters, numbers, or a hyphen'));
1364
+                }
1365
+                $share->setToken($token);
1366
+            }
1367
+        }
1368
+
1369
+        // NOT A LINK SHARE
1370
+        else {
1371
+            if ($permissions !== null) {
1372
+                $share->setPermissions($permissions);
1373
+            }
1374
+        }
1375
+
1376
+        if ($expireDate === '') {
1377
+            $share->setExpirationDate(null);
1378
+        } elseif ($expireDate !== null) {
1379
+            try {
1380
+                $expireDateTime = $this->parseDate($expireDate);
1381
+                $share->setExpirationDate($expireDateTime);
1382
+            } catch (\Exception $e) {
1383
+                throw new OCSBadRequestException($e->getMessage(), $e);
1384
+            }
1385
+        }
1386
+
1387
+        try {
1388
+            $this->checkInheritedAttributes($share);
1389
+            $share = $this->shareManager->updateShare($share);
1390
+        } catch (HintException $e) {
1391
+            $code = $e->getCode() === 0 ? 403 : $e->getCode();
1392
+            throw new OCSException($e->getHint(), (int)$code);
1393
+        } catch (\Exception $e) {
1394
+            $this->logger->error($e->getMessage(), ['exception' => $e]);
1395
+            throw new OCSBadRequestException('Failed to update share.', $e);
1396
+        }
1397
+
1398
+        return new DataResponse($this->formatShare($share));
1399
+    }
1400
+
1401
+    private function validateToken(string $token): bool {
1402
+        if (mb_strlen($token) === 0) {
1403
+            return false;
1404
+        }
1405
+        if (!preg_match('/^[a-z0-9-]+$/i', $token)) {
1406
+            return false;
1407
+        }
1408
+        return true;
1409
+    }
1410
+
1411
+    /**
1412
+     * Get all shares that are still pending
1413
+     *
1414
+     * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
1415
+     *
1416
+     * 200: Pending shares returned
1417
+     */
1418
+    #[NoAdminRequired]
1419
+    public function pendingShares(): DataResponse {
1420
+        $pendingShares = [];
1421
+
1422
+        $shareTypes = [
1423
+            IShare::TYPE_USER,
1424
+            IShare::TYPE_GROUP
1425
+        ];
1426
+
1427
+        foreach ($shareTypes as $shareType) {
1428
+            $shares = $this->shareManager->getSharedWith($this->userId, $shareType, null, -1, 0);
1429
+
1430
+            foreach ($shares as $share) {
1431
+                if ($share->getStatus() === IShare::STATUS_PENDING || $share->getStatus() === IShare::STATUS_REJECTED) {
1432
+                    $pendingShares[] = $share;
1433
+                }
1434
+            }
1435
+        }
1436
+
1437
+        $result = array_values(array_filter(array_map(function (IShare $share) {
1438
+            $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
1439
+            $node = $userFolder->getFirstNodeById($share->getNodeId());
1440
+            if (!$node) {
1441
+                // fallback to guessing the path
1442
+                $node = $userFolder->get($share->getTarget());
1443
+                if ($node === null || $share->getTarget() === '') {
1444
+                    return null;
1445
+                }
1446
+            }
1447
+
1448
+            try {
1449
+                $formattedShare = $this->formatShare($share, $node);
1450
+                $formattedShare['path'] = '/' . $share->getNode()->getName();
1451
+                $formattedShare['permissions'] = 0;
1452
+                return $formattedShare;
1453
+            } catch (NotFoundException $e) {
1454
+                return null;
1455
+            }
1456
+        }, $pendingShares), function ($entry) {
1457
+            return $entry !== null;
1458
+        }));
1459
+
1460
+        return new DataResponse($result);
1461
+    }
1462
+
1463
+    /**
1464
+     * Accept a share
1465
+     *
1466
+     * @param string $id ID of the share
1467
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1468
+     * @throws OCSNotFoundException Share not found
1469
+     * @throws OCSException
1470
+     * @throws OCSBadRequestException Share could not be accepted
1471
+     *
1472
+     * 200: Share accepted successfully
1473
+     */
1474
+    #[NoAdminRequired]
1475
+    public function acceptShare(string $id): DataResponse {
1476
+        try {
1477
+            $share = $this->getShareById($id);
1478
+        } catch (ShareNotFound $e) {
1479
+            throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
1480
+        }
1481
+
1482
+        if (!$this->canAccessShare($share)) {
1483
+            throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
1484
+        }
1485
+
1486
+        try {
1487
+            $this->shareManager->acceptShare($share, $this->userId);
1488
+        } catch (HintException $e) {
1489
+            $code = $e->getCode() === 0 ? 403 : $e->getCode();
1490
+            throw new OCSException($e->getHint(), (int)$code);
1491
+        } catch (\Exception $e) {
1492
+            $this->logger->error($e->getMessage(), ['exception' => $e]);
1493
+            throw new OCSBadRequestException('Failed to accept share.', $e);
1494
+        }
1495
+
1496
+        return new DataResponse();
1497
+    }
1498
+
1499
+    /**
1500
+     * Does the user have read permission on the share
1501
+     *
1502
+     * @param IShare $share the share to check
1503
+     * @param boolean $checkGroups check groups as well?
1504
+     * @return boolean
1505
+     * @throws NotFoundException
1506
+     *
1507
+     * @suppress PhanUndeclaredClassMethod
1508
+     */
1509
+    protected function canAccessShare(IShare $share, bool $checkGroups = true): bool {
1510
+        // A file with permissions 0 can't be accessed by us. So Don't show it
1511
+        if ($share->getPermissions() === 0) {
1512
+            return false;
1513
+        }
1514
+
1515
+        // Owner of the file and the sharer of the file can always get share
1516
+        if ($share->getShareOwner() === $this->userId
1517
+            || $share->getSharedBy() === $this->userId) {
1518
+            return true;
1519
+        }
1520
+
1521
+        // If the share is shared with you, you can access it!
1522
+        if ($share->getShareType() === IShare::TYPE_USER
1523
+            && $share->getSharedWith() === $this->userId) {
1524
+            return true;
1525
+        }
1526
+
1527
+        // Have reshare rights on the shared file/folder ?
1528
+        // Does the currentUser have access to the shared file?
1529
+        $userFolder = $this->rootFolder->getUserFolder($this->userId);
1530
+        $file = $userFolder->getFirstNodeById($share->getNodeId());
1531
+        if ($file && $this->shareProviderResharingRights($this->userId, $share, $file)) {
1532
+            return true;
1533
+        }
1534
+
1535
+        // If in the recipient group, you can see the share
1536
+        if ($checkGroups && $share->getShareType() === IShare::TYPE_GROUP) {
1537
+            $sharedWith = $this->groupManager->get($share->getSharedWith());
1538
+            $user = $this->userManager->get($this->userId);
1539
+            if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) {
1540
+                return true;
1541
+            }
1542
+        }
1543
+
1544
+        if ($share->getShareType() === IShare::TYPE_CIRCLE) {
1545
+            // TODO: have a sanity check like above?
1546
+            return true;
1547
+        }
1548
+
1549
+        if ($share->getShareType() === IShare::TYPE_ROOM) {
1550
+            try {
1551
+                return $this->getRoomShareHelper()->canAccessShare($share, $this->userId);
1552
+            } catch (ContainerExceptionInterface $e) {
1553
+                return false;
1554
+            }
1555
+        }
1556
+
1557
+        if ($share->getShareType() === IShare::TYPE_DECK) {
1558
+            try {
1559
+                return $this->getDeckShareHelper()->canAccessShare($share, $this->userId);
1560
+            } catch (ContainerExceptionInterface $e) {
1561
+                return false;
1562
+            }
1563
+        }
1564
+
1565
+        return false;
1566
+    }
1567
+
1568
+    /**
1569
+     * Does the user have edit permission on the share
1570
+     *
1571
+     * @param IShare $share the share to check
1572
+     * @return boolean
1573
+     */
1574
+    protected function canEditShare(IShare $share): bool {
1575
+        // A file with permissions 0 can't be accessed by us. So Don't show it
1576
+        if ($share->getPermissions() === 0) {
1577
+            return false;
1578
+        }
1579
+
1580
+        // The owner of the file and the creator of the share
1581
+        // can always edit the share
1582
+        if ($share->getShareOwner() === $this->userId
1583
+            || $share->getSharedBy() === $this->userId
1584
+        ) {
1585
+            return true;
1586
+        }
1587
+
1588
+        $userFolder = $this->rootFolder->getUserFolder($this->userId);
1589
+        $file = $userFolder->getFirstNodeById($share->getNodeId());
1590
+        if ($file?->getMountPoint() instanceof IShareOwnerlessMount && $this->shareProviderResharingRights($this->userId, $share, $file)) {
1591
+            return true;
1592
+        }
1593
+
1594
+        //! we do NOT support some kind of `admin` in groups.
1595
+        //! You cannot edit shares shared to a group you're
1596
+        //! a member of if you're not the share owner or the file owner!
1597
+
1598
+        return false;
1599
+    }
1600
+
1601
+    /**
1602
+     * Does the user have delete permission on the share
1603
+     *
1604
+     * @param IShare $share the share to check
1605
+     * @return boolean
1606
+     */
1607
+    protected function canDeleteShare(IShare $share): bool {
1608
+        // A file with permissions 0 can't be accessed by us. So Don't show it
1609
+        if ($share->getPermissions() === 0) {
1610
+            return false;
1611
+        }
1612
+
1613
+        // if the user is the recipient, i can unshare
1614
+        // the share with self
1615
+        if ($share->getShareType() === IShare::TYPE_USER
1616
+            && $share->getSharedWith() === $this->userId
1617
+        ) {
1618
+            return true;
1619
+        }
1620
+
1621
+        // The owner of the file and the creator of the share
1622
+        // can always delete the share
1623
+        if ($share->getShareOwner() === $this->userId
1624
+            || $share->getSharedBy() === $this->userId
1625
+        ) {
1626
+            return true;
1627
+        }
1628
+
1629
+        $userFolder = $this->rootFolder->getUserFolder($this->userId);
1630
+        $file = $userFolder->getFirstNodeById($share->getNodeId());
1631
+        if ($file?->getMountPoint() instanceof IShareOwnerlessMount && $this->shareProviderResharingRights($this->userId, $share, $file)) {
1632
+            return true;
1633
+        }
1634
+
1635
+        return false;
1636
+    }
1637
+
1638
+    /**
1639
+     * Does the user have delete permission on the share
1640
+     * This differs from the canDeleteShare function as it only
1641
+     * remove the share for the current user. It does NOT
1642
+     * completely delete the share but only the mount point.
1643
+     * It can then be restored from the deleted shares section.
1644
+     *
1645
+     * @param IShare $share the share to check
1646
+     * @return boolean
1647
+     *
1648
+     * @suppress PhanUndeclaredClassMethod
1649
+     */
1650
+    protected function canDeleteShareFromSelf(IShare $share): bool {
1651
+        if ($share->getShareType() !== IShare::TYPE_GROUP
1652
+            && $share->getShareType() !== IShare::TYPE_ROOM
1653
+            && $share->getShareType() !== IShare::TYPE_DECK
1654
+        ) {
1655
+            return false;
1656
+        }
1657
+
1658
+        if ($share->getShareOwner() === $this->userId
1659
+            || $share->getSharedBy() === $this->userId
1660
+        ) {
1661
+            // Delete the whole share, not just for self
1662
+            return false;
1663
+        }
1664
+
1665
+        // If in the recipient group, you can delete the share from self
1666
+        if ($share->getShareType() === IShare::TYPE_GROUP) {
1667
+            $sharedWith = $this->groupManager->get($share->getSharedWith());
1668
+            $user = $this->userManager->get($this->userId);
1669
+            if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) {
1670
+                return true;
1671
+            }
1672
+        }
1673
+
1674
+        if ($share->getShareType() === IShare::TYPE_ROOM) {
1675
+            try {
1676
+                return $this->getRoomShareHelper()->canAccessShare($share, $this->userId);
1677
+            } catch (ContainerExceptionInterface $e) {
1678
+                return false;
1679
+            }
1680
+        }
1681
+
1682
+        if ($share->getShareType() === IShare::TYPE_DECK) {
1683
+            try {
1684
+                return $this->getDeckShareHelper()->canAccessShare($share, $this->userId);
1685
+            } catch (ContainerExceptionInterface $e) {
1686
+                return false;
1687
+            }
1688
+        }
1689
+
1690
+        return false;
1691
+    }
1692
+
1693
+    /**
1694
+     * Make sure that the passed date is valid ISO 8601
1695
+     * So YYYY-MM-DD
1696
+     * If not throw an exception
1697
+     *
1698
+     * @param string $expireDate
1699
+     *
1700
+     * @throws \Exception
1701
+     * @return \DateTime
1702
+     */
1703
+    private function parseDate(string $expireDate): \DateTime {
1704
+        try {
1705
+            $date = new \DateTime(trim($expireDate, '"'), $this->dateTimeZone->getTimeZone());
1706
+            // Make sure it expires at midnight in owner timezone
1707
+            $date->setTime(0, 0, 0);
1708
+        } catch (\Exception $e) {
1709
+            throw new \Exception($this->l->t('Invalid date. Format must be YYYY-MM-DD'));
1710
+        }
1711
+
1712
+        return $date;
1713
+    }
1714
+
1715
+    /**
1716
+     * Since we have multiple providers but the OCS Share API v1 does
1717
+     * not support this we need to check all backends.
1718
+     *
1719
+     * @param string $id
1720
+     * @return IShare
1721
+     * @throws ShareNotFound
1722
+     */
1723
+    private function getShareById(string $id): IShare {
1724
+        $providers = [
1725
+            'ocinternal' => null, // No type check needed
1726
+            'ocCircleShare' => IShare::TYPE_CIRCLE,
1727
+            'ocMailShare' => IShare::TYPE_EMAIL,
1728
+            'ocRoomShare' => null,
1729
+            'deck' => IShare::TYPE_DECK,
1730
+        ];
1731
+
1732
+        // Add federated sharing as a provider only if it's allowed
1733
+        if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
1734
+            $providers['ocFederatedSharing'] = null; // No type check needed
1735
+        }
1736
+
1737
+        foreach ($providers as $prefix => $type) {
1738
+            try {
1739
+                if ($type === null || $this->shareManager->shareProviderExists($type)) {
1740
+                    return $this->shareManager->getShareById($prefix . ':' . $id, $this->userId);
1741
+                }
1742
+            } catch (ShareNotFound $e) {
1743
+                // Do nothing, continue to next provider
1744
+            } catch (\Exception $e) {
1745
+                $this->logger->warning('Unexpected error in share provider', [
1746
+                    'shareId' => $id,
1747
+                    'provider' => $prefix,
1748
+                    'exception' => $e,
1749
+                ]);
1750
+            }
1751
+        }
1752
+        throw new ShareNotFound();
1753
+    }
1754
+
1755
+    /**
1756
+     * Lock a Node
1757
+     *
1758
+     * @param Node $node
1759
+     * @throws LockedException
1760
+     */
1761
+    private function lock(Node $node) {
1762
+        $node->lock(ILockingProvider::LOCK_SHARED);
1763
+        $this->lockedNode = $node;
1764
+    }
1765
+
1766
+    /**
1767
+     * Cleanup the remaining locks
1768
+     * @throws LockedException
1769
+     */
1770
+    public function cleanup() {
1771
+        if ($this->lockedNode !== null) {
1772
+            $this->lockedNode->unlock(ILockingProvider::LOCK_SHARED);
1773
+        }
1774
+    }
1775
+
1776
+    /**
1777
+     * Returns the helper of ShareAPIController for room shares.
1778
+     *
1779
+     * If the Talk application is not enabled or the helper is not available
1780
+     * a ContainerExceptionInterface is thrown instead.
1781
+     *
1782
+     * @return \OCA\Talk\Share\Helper\ShareAPIController
1783
+     * @throws ContainerExceptionInterface
1784
+     */
1785
+    private function getRoomShareHelper() {
1786
+        if (!$this->appManager->isEnabledForUser('spreed')) {
1787
+            throw new QueryException();
1788
+        }
1789
+
1790
+        return $this->serverContainer->get('\OCA\Talk\Share\Helper\ShareAPIController');
1791
+    }
1792
+
1793
+    /**
1794
+     * Returns the helper of ShareAPIHelper for deck shares.
1795
+     *
1796
+     * If the Deck application is not enabled or the helper is not available
1797
+     * a ContainerExceptionInterface is thrown instead.
1798
+     *
1799
+     * @return ShareAPIHelper
1800
+     * @throws ContainerExceptionInterface
1801
+     */
1802
+    private function getDeckShareHelper() {
1803
+        if (!$this->appManager->isEnabledForUser('deck')) {
1804
+            throw new QueryException();
1805
+        }
1806
+
1807
+        return $this->serverContainer->get('\OCA\Deck\Sharing\ShareAPIHelper');
1808
+    }
1809
+
1810
+    /**
1811
+     * Returns the helper of ShareAPIHelper for sciencemesh shares.
1812
+     *
1813
+     * If the sciencemesh application is not enabled or the helper is not available
1814
+     * a ContainerExceptionInterface is thrown instead.
1815
+     *
1816
+     * @return ShareAPIHelper
1817
+     * @throws ContainerExceptionInterface
1818
+     */
1819
+    private function getSciencemeshShareHelper() {
1820
+        if (!$this->appManager->isEnabledForUser('sciencemesh')) {
1821
+            throw new QueryException();
1822
+        }
1823
+
1824
+        return $this->serverContainer->get('\OCA\ScienceMesh\Sharing\ShareAPIHelper');
1825
+    }
1826
+
1827
+    /**
1828
+     * @param string $viewer
1829
+     * @param Node $node
1830
+     * @param bool $reShares
1831
+     *
1832
+     * @return IShare[]
1833
+     */
1834
+    private function getSharesFromNode(string $viewer, $node, bool $reShares): array {
1835
+        $providers = [
1836
+            IShare::TYPE_USER,
1837
+            IShare::TYPE_GROUP,
1838
+            IShare::TYPE_LINK,
1839
+            IShare::TYPE_EMAIL,
1840
+            IShare::TYPE_CIRCLE,
1841
+            IShare::TYPE_ROOM,
1842
+            IShare::TYPE_DECK,
1843
+        ];
1844
+
1845
+        // Should we assume that the (currentUser) viewer is the owner of the node !?
1846
+        $shares = [];
1847
+        foreach ($providers as $provider) {
1848
+            if (!$this->shareManager->shareProviderExists($provider)) {
1849
+                continue;
1850
+            }
1851
+
1852
+            $providerShares
1853
+                = $this->shareManager->getSharesBy($viewer, $provider, $node, $reShares, -1, 0);
1854
+            $shares = array_merge($shares, $providerShares);
1855
+        }
1856
+
1857
+        if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
1858
+            $federatedShares = $this->shareManager->getSharesBy(
1859
+                $this->userId, IShare::TYPE_REMOTE, $node, $reShares, -1, 0
1860
+            );
1861
+            $shares = array_merge($shares, $federatedShares);
1862
+        }
1863
+
1864
+        if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
1865
+            $federatedShares = $this->shareManager->getSharesBy(
1866
+                $this->userId, IShare::TYPE_REMOTE_GROUP, $node, $reShares, -1, 0
1867
+            );
1868
+            $shares = array_merge($shares, $federatedShares);
1869
+        }
1870
+
1871
+        return $shares;
1872
+    }
1873
+
1874
+
1875
+    /**
1876
+     * @param Node $node
1877
+     *
1878
+     * @throws SharingRightsException
1879
+     */
1880
+    private function confirmSharingRights(Node $node): void {
1881
+        if (!$this->hasResharingRights($this->userId, $node)) {
1882
+            throw new SharingRightsException($this->l->t('No sharing rights on this item'));
1883
+        }
1884
+    }
1885
+
1886
+
1887
+    /**
1888
+     * @param string $viewer
1889
+     * @param Node $node
1890
+     *
1891
+     * @return bool
1892
+     */
1893
+    private function hasResharingRights($viewer, $node): bool {
1894
+        if ($viewer === $node->getOwner()->getUID()) {
1895
+            return true;
1896
+        }
1897
+
1898
+        foreach ([$node, $node->getParent()] as $node) {
1899
+            $shares = $this->getSharesFromNode($viewer, $node, true);
1900
+            foreach ($shares as $share) {
1901
+                try {
1902
+                    if ($this->shareProviderResharingRights($viewer, $share, $node)) {
1903
+                        return true;
1904
+                    }
1905
+                } catch (InvalidPathException|NotFoundException $e) {
1906
+                }
1907
+            }
1908
+        }
1909
+
1910
+        return false;
1911
+    }
1912
+
1913
+
1914
+    /**
1915
+     * Returns if we can find resharing rights in an IShare object for a specific user.
1916
+     *
1917
+     * @suppress PhanUndeclaredClassMethod
1918
+     *
1919
+     * @param string $userId
1920
+     * @param IShare $share
1921
+     * @param Node $node
1922
+     *
1923
+     * @return bool
1924
+     * @throws NotFoundException
1925
+     * @throws InvalidPathException
1926
+     */
1927
+    private function shareProviderResharingRights(string $userId, IShare $share, $node): bool {
1928
+        if ($share->getShareOwner() === $userId) {
1929
+            return true;
1930
+        }
1931
+
1932
+        // we check that current user have parent resharing rights on the current file
1933
+        if ($node !== null && ($node->getPermissions() & Constants::PERMISSION_SHARE) !== 0) {
1934
+            return true;
1935
+        }
1936
+
1937
+        if ((Constants::PERMISSION_SHARE & $share->getPermissions()) === 0) {
1938
+            return false;
1939
+        }
1940
+
1941
+        if ($share->getShareType() === IShare::TYPE_USER && $share->getSharedWith() === $userId) {
1942
+            return true;
1943
+        }
1944
+
1945
+        if ($share->getShareType() === IShare::TYPE_GROUP && $this->groupManager->isInGroup($userId, $share->getSharedWith())) {
1946
+            return true;
1947
+        }
1948
+
1949
+        if ($share->getShareType() === IShare::TYPE_CIRCLE && Server::get(IAppManager::class)->isEnabledForUser('circles')
1950
+            && class_exists('\OCA\Circles\Api\v1\Circles')) {
1951
+            $hasCircleId = (str_ends_with($share->getSharedWith(), ']'));
1952
+            $shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0);
1953
+            $shareWithLength = ($hasCircleId ? -1 : strpos($share->getSharedWith(), ' '));
1954
+            if ($shareWithLength === false) {
1955
+                $sharedWith = substr($share->getSharedWith(), $shareWithStart);
1956
+            } else {
1957
+                $sharedWith = substr($share->getSharedWith(), $shareWithStart, $shareWithLength);
1958
+            }
1959
+            try {
1960
+                $member = Circles::getMember($sharedWith, $userId, 1);
1961
+                if ($member->getLevel() >= 4) {
1962
+                    return true;
1963
+                }
1964
+                return false;
1965
+            } catch (ContainerExceptionInterface $e) {
1966
+                return false;
1967
+            }
1968
+        }
1969
+
1970
+        return false;
1971
+    }
1972
+
1973
+    /**
1974
+     * Get all the shares for the current user
1975
+     *
1976
+     * @param Node|null $path
1977
+     * @param boolean $reshares
1978
+     * @return IShare[]
1979
+     */
1980
+    private function getAllShares(?Node $path = null, bool $reshares = false) {
1981
+        // Get all shares
1982
+        $userShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_USER, $path, $reshares, -1, 0);
1983
+        $groupShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_GROUP, $path, $reshares, -1, 0);
1984
+        $linkShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_LINK, $path, $reshares, -1, 0);
1985
+
1986
+        // EMAIL SHARES
1987
+        $mailShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_EMAIL, $path, $reshares, -1, 0);
1988
+
1989
+        // TEAM SHARES
1990
+        $circleShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_CIRCLE, $path, $reshares, -1, 0);
1991
+
1992
+        // TALK SHARES
1993
+        $roomShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_ROOM, $path, $reshares, -1, 0);
1994
+
1995
+        // DECK SHARES
1996
+        $deckShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_DECK, $path, $reshares, -1, 0);
1997
+
1998
+        // FEDERATION
1999
+        if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
2000
+            $federatedShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_REMOTE, $path, $reshares, -1, 0);
2001
+        } else {
2002
+            $federatedShares = [];
2003
+        }
2004
+        if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
2005
+            $federatedGroupShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_REMOTE_GROUP, $path, $reshares, -1, 0);
2006
+        } else {
2007
+            $federatedGroupShares = [];
2008
+        }
2009
+
2010
+        return array_merge($userShares, $groupShares, $linkShares, $mailShares, $circleShares, $roomShares, $deckShares, $federatedShares, $federatedGroupShares);
2011
+    }
2012
+
2013
+
2014
+    /**
2015
+     * merging already formatted shares.
2016
+     * We'll make an associative array to easily detect duplicate Ids.
2017
+     * Keys _needs_ to be removed after all shares are retrieved and merged.
2018
+     *
2019
+     * @param array $shares
2020
+     * @param array $newShares
2021
+     */
2022
+    private function mergeFormattedShares(array &$shares, array $newShares) {
2023
+        foreach ($newShares as $newShare) {
2024
+            if (!array_key_exists($newShare['id'], $shares)) {
2025
+                $shares[$newShare['id']] = $newShare;
2026
+            }
2027
+        }
2028
+    }
2029
+
2030
+    /**
2031
+     * @param IShare $share
2032
+     * @param string|null $attributesString
2033
+     * @return IShare modified share
2034
+     */
2035
+    private function setShareAttributes(IShare $share, ?string $attributesString) {
2036
+        $newShareAttributes = null;
2037
+        if ($attributesString !== null) {
2038
+            $newShareAttributes = $this->shareManager->newShare()->newAttributes();
2039
+            $formattedShareAttributes = \json_decode($attributesString, true);
2040
+            if (is_array($formattedShareAttributes)) {
2041
+                foreach ($formattedShareAttributes as $formattedAttr) {
2042
+                    $newShareAttributes->setAttribute(
2043
+                        $formattedAttr['scope'],
2044
+                        $formattedAttr['key'],
2045
+                        $formattedAttr['value'],
2046
+                    );
2047
+                }
2048
+            } else {
2049
+                throw new OCSBadRequestException($this->l->t('Invalid share attributes provided: "%s"', [$attributesString]));
2050
+            }
2051
+        }
2052
+        $share->setAttributes($newShareAttributes);
2053
+
2054
+        return $share;
2055
+    }
2056
+
2057
+    private function checkInheritedAttributes(IShare $share): void {
2058
+        if (!$share->getSharedBy()) {
2059
+            return; // Probably in a test
2060
+        }
2061
+
2062
+        $canDownload = false;
2063
+        $hideDownload = true;
2064
+        $userExplicitlySetHideDownload = $share->getHideDownload(); // Capture user's explicit choice
2065
+
2066
+        $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
2067
+        $nodes = $userFolder->getById($share->getNodeId());
2068
+        foreach ($nodes as $node) {
2069
+            // Owner always can download it - so allow it, but respect their explicit choice about hiding downloads
2070
+            if ($node->getOwner()?->getUID() === $share->getSharedBy()) {
2071
+                $canDownload = true;
2072
+                $hideDownload = $userExplicitlySetHideDownload;
2073
+                break;
2074
+            }
2075
+
2076
+            if ($node->getStorage()->instanceOfStorage(SharedStorage::class)) {
2077
+                $storage = $node->getStorage();
2078
+                if ($storage instanceof Wrapper) {
2079
+                    $storage = $storage->getInstanceOfStorage(SharedStorage::class);
2080
+                    if ($storage === null) {
2081
+                        throw new \RuntimeException('Should not happen, instanceOfStorage but getInstanceOfStorage return null');
2082
+                    }
2083
+                } else {
2084
+                    throw new \RuntimeException('Should not happen, instanceOfStorage but not a wrapper');
2085
+                }
2086
+
2087
+                /** @var SharedStorage $storage */
2088
+                $originalShare = $storage->getShare();
2089
+                $inheritedAttributes = $originalShare->getAttributes();
2090
+
2091
+                // For federated shares: users can only be MORE restrictive, never LESS restrictive
2092
+                // If parent has hideDownload=true, child MUST have hideDownload=true
2093
+                $parentHidesDownload = $originalShare->getHideDownload();
2094
+
2095
+                // Check if download permission is available from parent
2096
+                $parentAllowsDownload = $inheritedAttributes === null || $inheritedAttributes->getAttribute('permissions', 'download') !== false;
2097
+
2098
+                // Apply inheritance rules:
2099
+                // 1. If parent hides download, child must hide download
2100
+                // 2. If parent allows download, child can choose to hide or allow
2101
+                // 3. If parent forbids download, child cannot allow download
2102
+                $hideDownload = $parentHidesDownload || $userExplicitlySetHideDownload;
2103
+
2104
+                $canDownload = $canDownload || $parentAllowsDownload;
2105
+
2106
+            } elseif ($node->getStorage()->instanceOfStorage(Storage::class)) {
2107
+                $canDownload = true; // in case of federation storage, we can expect the download to be activated by default
2108
+                // For external federation storage, respect user's choice if downloads are available
2109
+                $hideDownload = $userExplicitlySetHideDownload;
2110
+            }
2111
+        }
2112
+
2113
+        // Apply the final restrictions:
2114
+        // 1. If parent doesn't allow downloads at all, force hide and disable download attribute
2115
+        // 2. If parent allows downloads, respect user's hideDownload choice
2116
+        if (!$canDownload) {
2117
+            // Parent completely forbids downloads - must enforce this restriction
2118
+            $share->setHideDownload(true);
2119
+            $attributes = $share->getAttributes() ?? $share->newAttributes();
2120
+            $attributes->setAttribute('permissions', 'download', false);
2121
+            $share->setAttributes($attributes);
2122
+        } elseif ($hideDownload) {
2123
+            // Either parent forces hide, or user chooses to hide - respect this
2124
+            $share->setHideDownload(true);
2125
+        } else {
2126
+            // User explicitly wants to allow downloads and parent permits it
2127
+            $share->setHideDownload(false);
2128
+        }
2129
+    }
2130
+
2131
+    /**
2132
+     * Send a mail notification again for a share.
2133
+     * The mail_send option must be enabled for the given share.
2134
+     * @param string $id the share ID
2135
+     * @param string $password the password to check against. Necessary for password protected shares.
2136
+     * @throws OCSNotFoundException Share not found
2137
+     * @throws OCSForbiddenException You are not allowed to send mail notifications
2138
+     * @throws OCSBadRequestException Invalid request or wrong password
2139
+     * @throws OCSException Error while sending mail notification
2140
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
2141
+     *
2142
+     * 200: The email notification was sent successfully
2143
+     */
2144
+    #[NoAdminRequired]
2145
+    #[UserRateLimit(limit: 10, period: 600)]
2146
+    public function sendShareEmail(string $id, $password = ''): DataResponse {
2147
+        try {
2148
+            $share = $this->getShareById($id);
2149
+
2150
+            if (!$this->canAccessShare($share, false)) {
2151
+                throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
2152
+            }
2153
+
2154
+            if (!$this->canEditShare($share)) {
2155
+                throw new OCSForbiddenException($this->l->t('You are not allowed to send mail notifications'));
2156
+            }
2157
+
2158
+            // For mail and link shares, the user must be
2159
+            // the owner of the share, not only the file owner.
2160
+            if ($share->getShareType() === IShare::TYPE_EMAIL
2161
+                || $share->getShareType() === IShare::TYPE_LINK) {
2162
+                if ($share->getSharedBy() !== $this->userId) {
2163
+                    throw new OCSForbiddenException($this->l->t('You are not allowed to send mail notifications'));
2164
+                }
2165
+            }
2166
+
2167
+            try {
2168
+                $provider = $this->factory->getProviderForType($share->getShareType());
2169
+                if (!($provider instanceof IShareProviderWithNotification)) {
2170
+                    throw new OCSBadRequestException($this->l->t('No mail notification configured for this share type'));
2171
+                }
2172
+
2173
+                // Circumvent the password encrypted data by
2174
+                // setting the password clear. We're not storing
2175
+                // the password clear, it is just a temporary
2176
+                // object manipulation. The password will stay
2177
+                // encrypted in the database.
2178
+                if ($share->getPassword() !== null && $share->getPassword() !== $password) {
2179
+                    if (!$this->shareManager->checkPassword($share, $password)) {
2180
+                        throw new OCSBadRequestException($this->l->t('Wrong password'));
2181
+                    }
2182
+                    $share = $share->setPassword($password);
2183
+                }
2184
+
2185
+                $provider->sendMailNotification($share);
2186
+                return new DataResponse();
2187
+            } catch (Exception $e) {
2188
+                $this->logger->error($e->getMessage(), ['exception' => $e]);
2189
+                throw new OCSException($this->l->t('Error while sending mail notification'));
2190
+            }
2191
+
2192
+        } catch (ShareNotFound $e) {
2193
+            throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
2194
+        }
2195
+    }
2196
+
2197
+    /**
2198
+     * Get a unique share token
2199
+     *
2200
+     * @throws OCSException Failed to generate a unique token
2201
+     *
2202
+     * @return DataResponse<Http::STATUS_OK, array{token: string}, array{}>
2203
+     *
2204
+     * 200: Token generated successfully
2205
+     */
2206
+    #[ApiRoute(verb: 'GET', url: '/api/v1/token')]
2207
+    #[NoAdminRequired]
2208
+    public function generateToken(): DataResponse {
2209
+        try {
2210
+            $token = $this->shareManager->generateToken();
2211
+            return new DataResponse([
2212
+                'token' => $token,
2213
+            ]);
2214
+        } catch (ShareTokenException $e) {
2215
+            throw new OCSException($this->l->t('Failed to generate a unique token'));
2216
+        }
2217
+    }
2218
+
2219
+    /**
2220
+     * Populate the result set with file tags
2221
+     *
2222
+     * @psalm-template T of array{tags?: list<string>, file_source: int, ...array<string, mixed>}
2223
+     * @param list<T> $fileList
2224
+     * @return list<T> file list populated with tags
2225
+     */
2226
+    private function populateTags(array $fileList): array {
2227
+        $tagger = $this->tagManager->load('files');
2228
+        $tags = $tagger->getTagsForObjects(array_map(static fn (array $fileData) => $fileData['file_source'], $fileList));
2229
+
2230
+        if (!is_array($tags)) {
2231
+            throw new \UnexpectedValueException('$tags must be an array');
2232
+        }
2233
+
2234
+        // Set empty tag array
2235
+        foreach ($fileList as &$fileData) {
2236
+            $fileData['tags'] = [];
2237
+        }
2238
+        unset($fileData);
2239
+
2240
+        if (!empty($tags)) {
2241
+            foreach ($tags as $fileId => $fileTags) {
2242
+                foreach ($fileList as &$fileData) {
2243
+                    if ($fileId !== $fileData['file_source']) {
2244
+                        continue;
2245
+                    }
2246
+
2247
+                    $fileData['tags'] = $fileTags;
2248
+                }
2249
+                unset($fileData);
2250
+            }
2251
+        }
2252
+
2253
+        return $fileList;
2254
+    }
2255 2255
 }
Please login to merge, or discard this patch.
Spacing   +16 added lines, -16 removed lines patch added patch discarded remove patch
@@ -225,7 +225,7 @@  discard block
 block discarded – undo
225 225
 						// Server not found or other issue, we consider it not trusted
226 226
 						$this->trustedServerCache[$remote] = false;
227 227
 						$this->logger->error(
228
-							'Error checking if remote server is trusted (treating as untrusted): ' . $e->getMessage(),
228
+							'Error checking if remote server is trusted (treating as untrusted): '.$e->getMessage(),
229 229
 							['exception' => $e]
230 230
 						);
231 231
 					}
@@ -255,7 +255,7 @@  discard block
 block discarded – undo
255 255
 					'message' => $userStatus->getMessage(),
256 256
 					'icon' => $userStatus->getIcon(),
257 257
 					'clearAt' => $userStatus->getClearAt()
258
-						? (int)$userStatus->getClearAt()->format('U')
258
+						? (int) $userStatus->getClearAt()->format('U')
259 259
 						: null,
260 260
 				];
261 261
 			}
@@ -268,7 +268,7 @@  discard block
 block discarded – undo
268 268
 			// "share_with" and "share_with_displayname" for passwords of link
269 269
 			// shares was deprecated in Nextcloud 15, use "password" instead.
270 270
 			$result['share_with'] = $share->getPassword();
271
-			$result['share_with_displayname'] = '(' . $this->l->t('Shared link') . ')';
271
+			$result['share_with_displayname'] = '('.$this->l->t('Shared link').')';
272 272
 
273 273
 			$result['password'] = $share->getPassword();
274 274
 
@@ -339,7 +339,7 @@  discard block
 block discarded – undo
339 339
 
340 340
 		$result['attributes'] = null;
341 341
 		if ($attributes = $share->getAttributes()) {
342
-			$result['attributes'] = (string)\json_encode($attributes->toArray());
342
+			$result['attributes'] = (string) \json_encode($attributes->toArray());
343 343
 		}
344 344
 
345 345
 		return $result;
@@ -644,7 +644,7 @@  discard block
 block discarded – undo
644 644
 		} else {
645 645
 			// Use default permissions only for non-link shares to keep legacy behavior
646 646
 			if ($permissions === null) {
647
-				$permissions = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL);
647
+				$permissions = (int) $this->config->getAppValue('core', 'shareapi_default_permissions', (string) Constants::PERMISSION_ALL);
648 648
 			}
649 649
 			// Non-link shares always require read permissions (link shares could be file drop)
650 650
 			$permissions |= Constants::PERMISSION_READ;
@@ -825,7 +825,7 @@  discard block
 block discarded – undo
825 825
 		} catch (HintException $e) {
826 826
 			$code = $e->getCode() === 0 ? 403 : $e->getCode();
827 827
 			throw new OCSException($e->getHint(), $code);
828
-		} catch (GenericShareException|\InvalidArgumentException $e) {
828
+		} catch (GenericShareException | \InvalidArgumentException $e) {
829 829
 			$this->logger->error($e->getMessage(), ['exception' => $e]);
830 830
 			throw new OCSForbiddenException($e->getMessage(), $e);
831 831
 		} catch (\Exception $e) {
@@ -853,7 +853,7 @@  discard block
 block discarded – undo
853 853
 
854 854
 		$shares = array_merge($userShares, $groupShares, $circleShares, $roomShares, $deckShares);
855 855
 
856
-		$filteredShares = array_filter($shares, function (IShare $share) {
856
+		$filteredShares = array_filter($shares, function(IShare $share) {
857 857
 			return $share->getShareOwner() !== $this->userId && $share->getSharedBy() !== $this->userId;
858 858
 		});
859 859
 
@@ -890,7 +890,7 @@  discard block
 block discarded – undo
890 890
 		$nodes = $folder->getDirectoryListing();
891 891
 
892 892
 		/** @var IShare[] $shares */
893
-		$shares = array_reduce($nodes, function ($carry, $node) {
893
+		$shares = array_reduce($nodes, function($carry, $node) {
894 894
 			$carry = array_merge($carry, $this->getAllShares($node, true));
895 895
 			return $carry;
896 896
 		}, []);
@@ -1110,7 +1110,7 @@  discard block
 block discarded – undo
1110 1110
 				if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $node)) {
1111 1111
 					$resharingRight = true;
1112 1112
 				}
1113
-			} catch (InvalidPathException|NotFoundException $e) {
1113
+			} catch (InvalidPathException | NotFoundException $e) {
1114 1114
 			}
1115 1115
 		}
1116 1116
 
@@ -1389,7 +1389,7 @@  discard block
 block discarded – undo
1389 1389
 			$share = $this->shareManager->updateShare($share);
1390 1390
 		} catch (HintException $e) {
1391 1391
 			$code = $e->getCode() === 0 ? 403 : $e->getCode();
1392
-			throw new OCSException($e->getHint(), (int)$code);
1392
+			throw new OCSException($e->getHint(), (int) $code);
1393 1393
 		} catch (\Exception $e) {
1394 1394
 			$this->logger->error($e->getMessage(), ['exception' => $e]);
1395 1395
 			throw new OCSBadRequestException('Failed to update share.', $e);
@@ -1434,7 +1434,7 @@  discard block
 block discarded – undo
1434 1434
 			}
1435 1435
 		}
1436 1436
 
1437
-		$result = array_values(array_filter(array_map(function (IShare $share) {
1437
+		$result = array_values(array_filter(array_map(function(IShare $share) {
1438 1438
 			$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
1439 1439
 			$node = $userFolder->getFirstNodeById($share->getNodeId());
1440 1440
 			if (!$node) {
@@ -1447,13 +1447,13 @@  discard block
 block discarded – undo
1447 1447
 
1448 1448
 			try {
1449 1449
 				$formattedShare = $this->formatShare($share, $node);
1450
-				$formattedShare['path'] = '/' . $share->getNode()->getName();
1450
+				$formattedShare['path'] = '/'.$share->getNode()->getName();
1451 1451
 				$formattedShare['permissions'] = 0;
1452 1452
 				return $formattedShare;
1453 1453
 			} catch (NotFoundException $e) {
1454 1454
 				return null;
1455 1455
 			}
1456
-		}, $pendingShares), function ($entry) {
1456
+		}, $pendingShares), function($entry) {
1457 1457
 			return $entry !== null;
1458 1458
 		}));
1459 1459
 
@@ -1487,7 +1487,7 @@  discard block
 block discarded – undo
1487 1487
 			$this->shareManager->acceptShare($share, $this->userId);
1488 1488
 		} catch (HintException $e) {
1489 1489
 			$code = $e->getCode() === 0 ? 403 : $e->getCode();
1490
-			throw new OCSException($e->getHint(), (int)$code);
1490
+			throw new OCSException($e->getHint(), (int) $code);
1491 1491
 		} catch (\Exception $e) {
1492 1492
 			$this->logger->error($e->getMessage(), ['exception' => $e]);
1493 1493
 			throw new OCSBadRequestException('Failed to accept share.', $e);
@@ -1737,7 +1737,7 @@  discard block
 block discarded – undo
1737 1737
 		foreach ($providers as $prefix => $type) {
1738 1738
 			try {
1739 1739
 				if ($type === null || $this->shareManager->shareProviderExists($type)) {
1740
-					return $this->shareManager->getShareById($prefix . ':' . $id, $this->userId);
1740
+					return $this->shareManager->getShareById($prefix.':'.$id, $this->userId);
1741 1741
 				}
1742 1742
 			} catch (ShareNotFound $e) {
1743 1743
 				// Do nothing, continue to next provider
@@ -1902,7 +1902,7 @@  discard block
 block discarded – undo
1902 1902
 					if ($this->shareProviderResharingRights($viewer, $share, $node)) {
1903 1903
 						return true;
1904 1904
 					}
1905
-				} catch (InvalidPathException|NotFoundException $e) {
1905
+				} catch (InvalidPathException | NotFoundException $e) {
1906 1906
 				}
1907 1907
 			}
1908 1908
 		}
Please login to merge, or discard this patch.
apps/files_sharing/lib/Controller/ShareesAPIController.php 1 patch
Indentation   +346 added lines, -346 removed lines patch added patch discarded remove patch
@@ -39,350 +39,350 @@
 block discarded – undo
39 39
  */
40 40
 class ShareesAPIController extends OCSController {
41 41
 
42
-	protected int $offset = 0;
43
-	protected int $limit = 10;
44
-
45
-	/** @var Files_SharingShareesSearchResult */
46
-	protected array $result = [
47
-		'exact' => [
48
-			'users' => [],
49
-			'groups' => [],
50
-			'remotes' => [],
51
-			'remote_groups' => [],
52
-			'emails' => [],
53
-			'circles' => [],
54
-			'rooms' => [],
55
-		],
56
-		'users' => [],
57
-		'groups' => [],
58
-		'remotes' => [],
59
-		'remote_groups' => [],
60
-		'emails' => [],
61
-		'lookup' => [],
62
-		'circles' => [],
63
-		'rooms' => [],
64
-		'lookupEnabled' => false,
65
-	];
66
-
67
-	public function __construct(
68
-		string $appName,
69
-		IRequest $request,
70
-		protected ?string $userId,
71
-		protected IConfig $config,
72
-		protected IURLGenerator $urlGenerator,
73
-		protected IManager $shareManager,
74
-		protected ISearch $collaboratorSearch,
75
-	) {
76
-		parent::__construct($appName, $request);
77
-	}
78
-
79
-	/**
80
-	 * Search for sharees
81
-	 *
82
-	 * @param string $search Text to search for
83
-	 * @param string|null $itemType Limit to specific item types
84
-	 * @param int $page Page offset for searching
85
-	 * @param int $perPage Limit amount of search results per page
86
-	 * @param int|list<int>|null $shareType Limit to specific share types
87
-	 * @param bool $lookup If a global lookup should be performed too
88
-	 * @return DataResponse<Http::STATUS_OK, Files_SharingShareesSearchResult, array{Link?: string}>
89
-	 * @throws OCSBadRequestException Invalid search parameters
90
-	 *
91
-	 * 200: Sharees search result returned
92
-	 */
93
-	#[NoAdminRequired]
94
-	public function search(string $search = '', ?string $itemType = null, int $page = 1, int $perPage = 200, $shareType = null, bool $lookup = false): DataResponse {
95
-
96
-		// only search for string larger than a given threshold
97
-		$threshold = $this->config->getSystemValueInt('sharing.minSearchStringLength', 0);
98
-		if (strlen($search) < $threshold) {
99
-			return new DataResponse($this->result);
100
-		}
101
-
102
-		if ($this->shareManager->sharingDisabledForUser($this->userId)) {
103
-			return new DataResponse($this->result);
104
-		}
105
-
106
-		// never return more than the max. number of results configured in the config.php
107
-		$maxResults = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
108
-		if ($maxResults > 0) {
109
-			$perPage = min($perPage, $maxResults);
110
-		}
111
-		if ($perPage <= 0) {
112
-			throw new OCSBadRequestException('Invalid perPage argument');
113
-		}
114
-		if ($page <= 0) {
115
-			throw new OCSBadRequestException('Invalid page');
116
-		}
117
-
118
-		$shareTypes = [
119
-			IShare::TYPE_USER,
120
-		];
121
-
122
-		if ($itemType === null) {
123
-			throw new OCSBadRequestException('Missing itemType');
124
-		} elseif ($itemType === 'file' || $itemType === 'folder') {
125
-			if ($this->shareManager->allowGroupSharing()) {
126
-				$shareTypes[] = IShare::TYPE_GROUP;
127
-			}
128
-
129
-			if ($this->isRemoteSharingAllowed($itemType)) {
130
-				$shareTypes[] = IShare::TYPE_REMOTE;
131
-			}
132
-
133
-			if ($this->isRemoteGroupSharingAllowed($itemType)) {
134
-				$shareTypes[] = IShare::TYPE_REMOTE_GROUP;
135
-			}
136
-
137
-			if ($this->shareManager->shareProviderExists(IShare::TYPE_EMAIL)) {
138
-				$shareTypes[] = IShare::TYPE_EMAIL;
139
-			}
140
-
141
-			if ($this->shareManager->shareProviderExists(IShare::TYPE_ROOM)) {
142
-				$shareTypes[] = IShare::TYPE_ROOM;
143
-			}
144
-		} else {
145
-			if ($this->shareManager->allowGroupSharing()) {
146
-				$shareTypes[] = IShare::TYPE_GROUP;
147
-			}
148
-			$shareTypes[] = IShare::TYPE_EMAIL;
149
-		}
150
-
151
-		// FIXME: DI
152
-		if (Server::get(IAppManager::class)->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) {
153
-			$shareTypes[] = IShare::TYPE_CIRCLE;
154
-		}
155
-
156
-		if ($itemType === 'calendar') {
157
-			$shareTypes[] = IShare::TYPE_REMOTE;
158
-		}
159
-
160
-		if ($shareType !== null && is_array($shareType)) {
161
-			$shareTypes = array_intersect($shareTypes, $shareType);
162
-		} elseif (is_numeric($shareType)) {
163
-			$shareTypes = array_intersect($shareTypes, [(int)$shareType]);
164
-		}
165
-		sort($shareTypes);
166
-
167
-		$this->limit = $perPage;
168
-		$this->offset = $perPage * ($page - 1);
169
-
170
-		// In global scale mode we always search the lookup server
171
-		$this->result['lookupEnabled'] = Server::get(GlobalScaleIConfig::class)->isGlobalScaleEnabled();
172
-		// TODO: Reconsider using lookup server for non-global-scale federation
173
-
174
-		[$result, $hasMoreResults] = $this->collaboratorSearch->search($search, $shareTypes, $this->result['lookupEnabled'], $this->limit, $this->offset);
175
-
176
-		// extra treatment for 'exact' subarray, with a single merge expected keys might be lost
177
-		if (isset($result['exact'])) {
178
-			$result['exact'] = array_merge($this->result['exact'], $result['exact']);
179
-		}
180
-		$this->result = array_merge($this->result, $result);
181
-		$response = new DataResponse($this->result);
182
-
183
-		if ($hasMoreResults) {
184
-			$response->setHeaders(['Link' => $this->getPaginationLink($page, [
185
-				'search' => $search,
186
-				'itemType' => $itemType,
187
-				'shareType' => $shareTypes,
188
-				'perPage' => $perPage,
189
-			])]);
190
-		}
191
-
192
-		return $response;
193
-	}
194
-
195
-	/**
196
-	 * @param string $user
197
-	 * @param int $shareType
198
-	 *
199
-	 * @return Generator<array<string>>
200
-	 */
201
-	private function getAllShareesByType(string $user, int $shareType): Generator {
202
-		$offset = 0;
203
-		$pageSize = 50;
204
-
205
-		while (count($page = $this->shareManager->getSharesBy(
206
-			$user,
207
-			$shareType,
208
-			null,
209
-			false,
210
-			$pageSize,
211
-			$offset
212
-		))) {
213
-			foreach ($page as $share) {
214
-				yield [$share->getSharedWith(), $share->getSharedWithDisplayName() ?? $share->getSharedWith()];
215
-			}
216
-
217
-			$offset += $pageSize;
218
-		}
219
-	}
220
-
221
-	private function sortShareesByFrequency(array $sharees): array {
222
-		usort($sharees, function (array $s1, array $s2): int {
223
-			return $s2['count'] - $s1['count'];
224
-		});
225
-		return $sharees;
226
-	}
227
-
228
-	private $searchResultTypeMap = [
229
-		IShare::TYPE_USER => 'users',
230
-		IShare::TYPE_GROUP => 'groups',
231
-		IShare::TYPE_REMOTE => 'remotes',
232
-		IShare::TYPE_REMOTE_GROUP => 'remote_groups',
233
-		IShare::TYPE_EMAIL => 'emails',
234
-	];
235
-
236
-	private function getAllSharees(string $user, array $shareTypes): ISearchResult {
237
-		$result = [];
238
-		foreach ($shareTypes as $shareType) {
239
-			$sharees = $this->getAllShareesByType($user, $shareType);
240
-			$shareTypeResults = [];
241
-			foreach ($sharees as [$sharee, $displayname]) {
242
-				if (!isset($this->searchResultTypeMap[$shareType]) || trim($sharee) === '') {
243
-					continue;
244
-				}
245
-
246
-				if (!isset($shareTypeResults[$sharee])) {
247
-					$shareTypeResults[$sharee] = [
248
-						'count' => 1,
249
-						'label' => $displayname,
250
-						'value' => [
251
-							'shareType' => $shareType,
252
-							'shareWith' => $sharee,
253
-						],
254
-					];
255
-				} else {
256
-					$shareTypeResults[$sharee]['count']++;
257
-				}
258
-			}
259
-			$result = array_merge($result, array_values($shareTypeResults));
260
-		}
261
-
262
-		$top5 = array_slice(
263
-			$this->sortShareesByFrequency($result),
264
-			0,
265
-			5
266
-		);
267
-
268
-		$searchResult = new SearchResult();
269
-		foreach ($this->searchResultTypeMap as $int => $str) {
270
-			$searchResult->addResultSet(new SearchResultType($str), [], []);
271
-			foreach ($top5 as $x) {
272
-				if ($x['value']['shareType'] === $int) {
273
-					$searchResult->addResultSet(new SearchResultType($str), [], [$x]);
274
-				}
275
-			}
276
-		}
277
-		return $searchResult;
278
-	}
279
-
280
-	/**
281
-	 * Find recommended sharees
282
-	 *
283
-	 * @param string $itemType Limit to specific item types
284
-	 * @param int|list<int>|null $shareType Limit to specific share types
285
-	 * @return DataResponse<Http::STATUS_OK, Files_SharingShareesRecommendedResult, array{}>
286
-	 *
287
-	 * 200: Recommended sharees returned
288
-	 */
289
-	#[NoAdminRequired]
290
-	public function findRecommended(string $itemType, $shareType = null): DataResponse {
291
-		$shareTypes = [
292
-			IShare::TYPE_USER,
293
-		];
294
-
295
-		if ($itemType === 'file' || $itemType === 'folder') {
296
-			if ($this->shareManager->allowGroupSharing()) {
297
-				$shareTypes[] = IShare::TYPE_GROUP;
298
-			}
299
-
300
-			if ($this->isRemoteSharingAllowed($itemType)) {
301
-				$shareTypes[] = IShare::TYPE_REMOTE;
302
-			}
303
-
304
-			if ($this->isRemoteGroupSharingAllowed($itemType)) {
305
-				$shareTypes[] = IShare::TYPE_REMOTE_GROUP;
306
-			}
307
-
308
-			if ($this->shareManager->shareProviderExists(IShare::TYPE_EMAIL)) {
309
-				$shareTypes[] = IShare::TYPE_EMAIL;
310
-			}
311
-
312
-			if ($this->shareManager->shareProviderExists(IShare::TYPE_ROOM)) {
313
-				$shareTypes[] = IShare::TYPE_ROOM;
314
-			}
315
-		} else {
316
-			$shareTypes[] = IShare::TYPE_GROUP;
317
-			$shareTypes[] = IShare::TYPE_EMAIL;
318
-		}
319
-
320
-		// FIXME: DI
321
-		if (Server::get(IAppManager::class)->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) {
322
-			$shareTypes[] = IShare::TYPE_CIRCLE;
323
-		}
324
-
325
-		if (isset($_GET['shareType']) && is_array($_GET['shareType'])) {
326
-			$shareTypes = array_intersect($shareTypes, $_GET['shareType']);
327
-			sort($shareTypes);
328
-		} elseif (is_numeric($shareType)) {
329
-			$shareTypes = array_intersect($shareTypes, [(int)$shareType]);
330
-			sort($shareTypes);
331
-		}
332
-
333
-		return new DataResponse(
334
-			$this->getAllSharees($this->userId, $shareTypes)->asArray()
335
-		);
336
-	}
337
-
338
-	/**
339
-	 * Method to get out the static call for better testing
340
-	 *
341
-	 * @param string $itemType
342
-	 * @return bool
343
-	 */
344
-	protected function isRemoteSharingAllowed(string $itemType): bool {
345
-		try {
346
-			// FIXME: static foo makes unit testing unnecessarily difficult
347
-			$backend = Share::getBackend($itemType);
348
-			return $backend->isShareTypeAllowed(IShare::TYPE_REMOTE);
349
-		} catch (\Exception $e) {
350
-			return false;
351
-		}
352
-	}
353
-
354
-	protected function isRemoteGroupSharingAllowed(string $itemType): bool {
355
-		try {
356
-			// FIXME: static foo makes unit testing unnecessarily difficult
357
-			$backend = Share::getBackend($itemType);
358
-			return $backend->isShareTypeAllowed(IShare::TYPE_REMOTE_GROUP);
359
-		} catch (\Exception $e) {
360
-			return false;
361
-		}
362
-	}
363
-
364
-
365
-	/**
366
-	 * Generates a bunch of pagination links for the current page
367
-	 *
368
-	 * @param int $page Current page
369
-	 * @param array $params Parameters for the URL
370
-	 * @return string
371
-	 */
372
-	protected function getPaginationLink(int $page, array $params): string {
373
-		if ($this->isV2()) {
374
-			$url = $this->urlGenerator->getAbsoluteURL('/ocs/v2.php/apps/files_sharing/api/v1/sharees') . '?';
375
-		} else {
376
-			$url = $this->urlGenerator->getAbsoluteURL('/ocs/v1.php/apps/files_sharing/api/v1/sharees') . '?';
377
-		}
378
-		$params['page'] = $page + 1;
379
-		return '<' . $url . http_build_query($params) . '>; rel="next"';
380
-	}
381
-
382
-	/**
383
-	 * @return bool
384
-	 */
385
-	protected function isV2(): bool {
386
-		return $this->request->getScriptName() === '/ocs/v2.php';
387
-	}
42
+    protected int $offset = 0;
43
+    protected int $limit = 10;
44
+
45
+    /** @var Files_SharingShareesSearchResult */
46
+    protected array $result = [
47
+        'exact' => [
48
+            'users' => [],
49
+            'groups' => [],
50
+            'remotes' => [],
51
+            'remote_groups' => [],
52
+            'emails' => [],
53
+            'circles' => [],
54
+            'rooms' => [],
55
+        ],
56
+        'users' => [],
57
+        'groups' => [],
58
+        'remotes' => [],
59
+        'remote_groups' => [],
60
+        'emails' => [],
61
+        'lookup' => [],
62
+        'circles' => [],
63
+        'rooms' => [],
64
+        'lookupEnabled' => false,
65
+    ];
66
+
67
+    public function __construct(
68
+        string $appName,
69
+        IRequest $request,
70
+        protected ?string $userId,
71
+        protected IConfig $config,
72
+        protected IURLGenerator $urlGenerator,
73
+        protected IManager $shareManager,
74
+        protected ISearch $collaboratorSearch,
75
+    ) {
76
+        parent::__construct($appName, $request);
77
+    }
78
+
79
+    /**
80
+     * Search for sharees
81
+     *
82
+     * @param string $search Text to search for
83
+     * @param string|null $itemType Limit to specific item types
84
+     * @param int $page Page offset for searching
85
+     * @param int $perPage Limit amount of search results per page
86
+     * @param int|list<int>|null $shareType Limit to specific share types
87
+     * @param bool $lookup If a global lookup should be performed too
88
+     * @return DataResponse<Http::STATUS_OK, Files_SharingShareesSearchResult, array{Link?: string}>
89
+     * @throws OCSBadRequestException Invalid search parameters
90
+     *
91
+     * 200: Sharees search result returned
92
+     */
93
+    #[NoAdminRequired]
94
+    public function search(string $search = '', ?string $itemType = null, int $page = 1, int $perPage = 200, $shareType = null, bool $lookup = false): DataResponse {
95
+
96
+        // only search for string larger than a given threshold
97
+        $threshold = $this->config->getSystemValueInt('sharing.minSearchStringLength', 0);
98
+        if (strlen($search) < $threshold) {
99
+            return new DataResponse($this->result);
100
+        }
101
+
102
+        if ($this->shareManager->sharingDisabledForUser($this->userId)) {
103
+            return new DataResponse($this->result);
104
+        }
105
+
106
+        // never return more than the max. number of results configured in the config.php
107
+        $maxResults = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
108
+        if ($maxResults > 0) {
109
+            $perPage = min($perPage, $maxResults);
110
+        }
111
+        if ($perPage <= 0) {
112
+            throw new OCSBadRequestException('Invalid perPage argument');
113
+        }
114
+        if ($page <= 0) {
115
+            throw new OCSBadRequestException('Invalid page');
116
+        }
117
+
118
+        $shareTypes = [
119
+            IShare::TYPE_USER,
120
+        ];
121
+
122
+        if ($itemType === null) {
123
+            throw new OCSBadRequestException('Missing itemType');
124
+        } elseif ($itemType === 'file' || $itemType === 'folder') {
125
+            if ($this->shareManager->allowGroupSharing()) {
126
+                $shareTypes[] = IShare::TYPE_GROUP;
127
+            }
128
+
129
+            if ($this->isRemoteSharingAllowed($itemType)) {
130
+                $shareTypes[] = IShare::TYPE_REMOTE;
131
+            }
132
+
133
+            if ($this->isRemoteGroupSharingAllowed($itemType)) {
134
+                $shareTypes[] = IShare::TYPE_REMOTE_GROUP;
135
+            }
136
+
137
+            if ($this->shareManager->shareProviderExists(IShare::TYPE_EMAIL)) {
138
+                $shareTypes[] = IShare::TYPE_EMAIL;
139
+            }
140
+
141
+            if ($this->shareManager->shareProviderExists(IShare::TYPE_ROOM)) {
142
+                $shareTypes[] = IShare::TYPE_ROOM;
143
+            }
144
+        } else {
145
+            if ($this->shareManager->allowGroupSharing()) {
146
+                $shareTypes[] = IShare::TYPE_GROUP;
147
+            }
148
+            $shareTypes[] = IShare::TYPE_EMAIL;
149
+        }
150
+
151
+        // FIXME: DI
152
+        if (Server::get(IAppManager::class)->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) {
153
+            $shareTypes[] = IShare::TYPE_CIRCLE;
154
+        }
155
+
156
+        if ($itemType === 'calendar') {
157
+            $shareTypes[] = IShare::TYPE_REMOTE;
158
+        }
159
+
160
+        if ($shareType !== null && is_array($shareType)) {
161
+            $shareTypes = array_intersect($shareTypes, $shareType);
162
+        } elseif (is_numeric($shareType)) {
163
+            $shareTypes = array_intersect($shareTypes, [(int)$shareType]);
164
+        }
165
+        sort($shareTypes);
166
+
167
+        $this->limit = $perPage;
168
+        $this->offset = $perPage * ($page - 1);
169
+
170
+        // In global scale mode we always search the lookup server
171
+        $this->result['lookupEnabled'] = Server::get(GlobalScaleIConfig::class)->isGlobalScaleEnabled();
172
+        // TODO: Reconsider using lookup server for non-global-scale federation
173
+
174
+        [$result, $hasMoreResults] = $this->collaboratorSearch->search($search, $shareTypes, $this->result['lookupEnabled'], $this->limit, $this->offset);
175
+
176
+        // extra treatment for 'exact' subarray, with a single merge expected keys might be lost
177
+        if (isset($result['exact'])) {
178
+            $result['exact'] = array_merge($this->result['exact'], $result['exact']);
179
+        }
180
+        $this->result = array_merge($this->result, $result);
181
+        $response = new DataResponse($this->result);
182
+
183
+        if ($hasMoreResults) {
184
+            $response->setHeaders(['Link' => $this->getPaginationLink($page, [
185
+                'search' => $search,
186
+                'itemType' => $itemType,
187
+                'shareType' => $shareTypes,
188
+                'perPage' => $perPage,
189
+            ])]);
190
+        }
191
+
192
+        return $response;
193
+    }
194
+
195
+    /**
196
+     * @param string $user
197
+     * @param int $shareType
198
+     *
199
+     * @return Generator<array<string>>
200
+     */
201
+    private function getAllShareesByType(string $user, int $shareType): Generator {
202
+        $offset = 0;
203
+        $pageSize = 50;
204
+
205
+        while (count($page = $this->shareManager->getSharesBy(
206
+            $user,
207
+            $shareType,
208
+            null,
209
+            false,
210
+            $pageSize,
211
+            $offset
212
+        ))) {
213
+            foreach ($page as $share) {
214
+                yield [$share->getSharedWith(), $share->getSharedWithDisplayName() ?? $share->getSharedWith()];
215
+            }
216
+
217
+            $offset += $pageSize;
218
+        }
219
+    }
220
+
221
+    private function sortShareesByFrequency(array $sharees): array {
222
+        usort($sharees, function (array $s1, array $s2): int {
223
+            return $s2['count'] - $s1['count'];
224
+        });
225
+        return $sharees;
226
+    }
227
+
228
+    private $searchResultTypeMap = [
229
+        IShare::TYPE_USER => 'users',
230
+        IShare::TYPE_GROUP => 'groups',
231
+        IShare::TYPE_REMOTE => 'remotes',
232
+        IShare::TYPE_REMOTE_GROUP => 'remote_groups',
233
+        IShare::TYPE_EMAIL => 'emails',
234
+    ];
235
+
236
+    private function getAllSharees(string $user, array $shareTypes): ISearchResult {
237
+        $result = [];
238
+        foreach ($shareTypes as $shareType) {
239
+            $sharees = $this->getAllShareesByType($user, $shareType);
240
+            $shareTypeResults = [];
241
+            foreach ($sharees as [$sharee, $displayname]) {
242
+                if (!isset($this->searchResultTypeMap[$shareType]) || trim($sharee) === '') {
243
+                    continue;
244
+                }
245
+
246
+                if (!isset($shareTypeResults[$sharee])) {
247
+                    $shareTypeResults[$sharee] = [
248
+                        'count' => 1,
249
+                        'label' => $displayname,
250
+                        'value' => [
251
+                            'shareType' => $shareType,
252
+                            'shareWith' => $sharee,
253
+                        ],
254
+                    ];
255
+                } else {
256
+                    $shareTypeResults[$sharee]['count']++;
257
+                }
258
+            }
259
+            $result = array_merge($result, array_values($shareTypeResults));
260
+        }
261
+
262
+        $top5 = array_slice(
263
+            $this->sortShareesByFrequency($result),
264
+            0,
265
+            5
266
+        );
267
+
268
+        $searchResult = new SearchResult();
269
+        foreach ($this->searchResultTypeMap as $int => $str) {
270
+            $searchResult->addResultSet(new SearchResultType($str), [], []);
271
+            foreach ($top5 as $x) {
272
+                if ($x['value']['shareType'] === $int) {
273
+                    $searchResult->addResultSet(new SearchResultType($str), [], [$x]);
274
+                }
275
+            }
276
+        }
277
+        return $searchResult;
278
+    }
279
+
280
+    /**
281
+     * Find recommended sharees
282
+     *
283
+     * @param string $itemType Limit to specific item types
284
+     * @param int|list<int>|null $shareType Limit to specific share types
285
+     * @return DataResponse<Http::STATUS_OK, Files_SharingShareesRecommendedResult, array{}>
286
+     *
287
+     * 200: Recommended sharees returned
288
+     */
289
+    #[NoAdminRequired]
290
+    public function findRecommended(string $itemType, $shareType = null): DataResponse {
291
+        $shareTypes = [
292
+            IShare::TYPE_USER,
293
+        ];
294
+
295
+        if ($itemType === 'file' || $itemType === 'folder') {
296
+            if ($this->shareManager->allowGroupSharing()) {
297
+                $shareTypes[] = IShare::TYPE_GROUP;
298
+            }
299
+
300
+            if ($this->isRemoteSharingAllowed($itemType)) {
301
+                $shareTypes[] = IShare::TYPE_REMOTE;
302
+            }
303
+
304
+            if ($this->isRemoteGroupSharingAllowed($itemType)) {
305
+                $shareTypes[] = IShare::TYPE_REMOTE_GROUP;
306
+            }
307
+
308
+            if ($this->shareManager->shareProviderExists(IShare::TYPE_EMAIL)) {
309
+                $shareTypes[] = IShare::TYPE_EMAIL;
310
+            }
311
+
312
+            if ($this->shareManager->shareProviderExists(IShare::TYPE_ROOM)) {
313
+                $shareTypes[] = IShare::TYPE_ROOM;
314
+            }
315
+        } else {
316
+            $shareTypes[] = IShare::TYPE_GROUP;
317
+            $shareTypes[] = IShare::TYPE_EMAIL;
318
+        }
319
+
320
+        // FIXME: DI
321
+        if (Server::get(IAppManager::class)->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) {
322
+            $shareTypes[] = IShare::TYPE_CIRCLE;
323
+        }
324
+
325
+        if (isset($_GET['shareType']) && is_array($_GET['shareType'])) {
326
+            $shareTypes = array_intersect($shareTypes, $_GET['shareType']);
327
+            sort($shareTypes);
328
+        } elseif (is_numeric($shareType)) {
329
+            $shareTypes = array_intersect($shareTypes, [(int)$shareType]);
330
+            sort($shareTypes);
331
+        }
332
+
333
+        return new DataResponse(
334
+            $this->getAllSharees($this->userId, $shareTypes)->asArray()
335
+        );
336
+    }
337
+
338
+    /**
339
+     * Method to get out the static call for better testing
340
+     *
341
+     * @param string $itemType
342
+     * @return bool
343
+     */
344
+    protected function isRemoteSharingAllowed(string $itemType): bool {
345
+        try {
346
+            // FIXME: static foo makes unit testing unnecessarily difficult
347
+            $backend = Share::getBackend($itemType);
348
+            return $backend->isShareTypeAllowed(IShare::TYPE_REMOTE);
349
+        } catch (\Exception $e) {
350
+            return false;
351
+        }
352
+    }
353
+
354
+    protected function isRemoteGroupSharingAllowed(string $itemType): bool {
355
+        try {
356
+            // FIXME: static foo makes unit testing unnecessarily difficult
357
+            $backend = Share::getBackend($itemType);
358
+            return $backend->isShareTypeAllowed(IShare::TYPE_REMOTE_GROUP);
359
+        } catch (\Exception $e) {
360
+            return false;
361
+        }
362
+    }
363
+
364
+
365
+    /**
366
+     * Generates a bunch of pagination links for the current page
367
+     *
368
+     * @param int $page Current page
369
+     * @param array $params Parameters for the URL
370
+     * @return string
371
+     */
372
+    protected function getPaginationLink(int $page, array $params): string {
373
+        if ($this->isV2()) {
374
+            $url = $this->urlGenerator->getAbsoluteURL('/ocs/v2.php/apps/files_sharing/api/v1/sharees') . '?';
375
+        } else {
376
+            $url = $this->urlGenerator->getAbsoluteURL('/ocs/v1.php/apps/files_sharing/api/v1/sharees') . '?';
377
+        }
378
+        $params['page'] = $page + 1;
379
+        return '<' . $url . http_build_query($params) . '>; rel="next"';
380
+    }
381
+
382
+    /**
383
+     * @return bool
384
+     */
385
+    protected function isV2(): bool {
386
+        return $this->request->getScriptName() === '/ocs/v2.php';
387
+    }
388 388
 }
Please login to merge, or discard this patch.