Completed
Push — master ( ae2507...fe34ea )
by
unknown
52:09 queued 24:26
created
lib/private/Image.php 1 patch
Indentation   +1111 added lines, -1111 removed lines patch added patch discarded remove patch
@@ -22,719 +22,719 @@  discard block
 block discarded – undo
22 22
  * Class for basic image manipulation
23 23
  */
24 24
 class Image implements IImage {
25
-	// Default memory limit for images to load (256 MBytes).
26
-	protected const DEFAULT_MEMORY_LIMIT = 256;
27
-
28
-	// Default quality for jpeg images
29
-	protected const DEFAULT_JPEG_QUALITY = 80;
30
-
31
-	// Default quality for webp images
32
-	protected const DEFAULT_WEBP_QUALITY = 80;
33
-
34
-	// tmp resource.
35
-	protected GdImage|false $resource = false;
36
-	// Default to png if file type isn't evident.
37
-	protected int $imageType = IMAGETYPE_PNG;
38
-	// Default to png
39
-	protected ?string $mimeType = 'image/png';
40
-	protected ?string $filePath = null;
41
-	private ?finfo $fileInfo = null;
42
-	private LoggerInterface $logger;
43
-	private IAppConfig $appConfig;
44
-	private IConfig $config;
45
-	private ?array $exif = null;
46
-
47
-	/**
48
-	 * @throws \InvalidArgumentException in case the $imageRef parameter is not null
49
-	 */
50
-	public function __construct(
51
-		?LoggerInterface $logger = null,
52
-		?IAppConfig $appConfig = null,
53
-		?IConfig $config = null,
54
-	) {
55
-		$this->logger = $logger ?? Server::get(LoggerInterface::class);
56
-		$this->appConfig = $appConfig ?? Server::get(IAppConfig::class);
57
-		$this->config = $config ?? Server::get(IConfig::class);
58
-
59
-		if (class_exists(finfo::class)) {
60
-			$this->fileInfo = new finfo(FILEINFO_MIME_TYPE);
61
-		}
62
-	}
63
-
64
-	/**
65
-	 * Determine whether the object contains an image resource.
66
-	 *
67
-	 * @psalm-assert-if-true \GdImage $this->resource
68
-	 * @return bool
69
-	 */
70
-	public function valid(): bool {
71
-		if (is_object($this->resource) && get_class($this->resource) === \GdImage::class) {
72
-			return true;
73
-		}
74
-
75
-		return false;
76
-	}
77
-
78
-	/**
79
-	 * Returns the MIME type of the image or null if no image is loaded.
80
-	 *
81
-	 * @return string
82
-	 */
83
-	public function mimeType(): ?string {
84
-		return $this->valid() ? $this->mimeType : null;
85
-	}
86
-
87
-	/**
88
-	 * Returns the width of the image or -1 if no image is loaded.
89
-	 *
90
-	 * @return int
91
-	 */
92
-	public function width(): int {
93
-		if ($this->valid()) {
94
-			return imagesx($this->resource);
95
-		}
96
-		return -1;
97
-	}
98
-
99
-	/**
100
-	 * Returns the height of the image or -1 if no image is loaded.
101
-	 *
102
-	 * @return int
103
-	 */
104
-	public function height(): int {
105
-		if ($this->valid()) {
106
-			return imagesy($this->resource);
107
-		}
108
-		return -1;
109
-	}
110
-
111
-	/**
112
-	 * Returns the width when the image orientation is top-left.
113
-	 *
114
-	 * @return int
115
-	 */
116
-	public function widthTopLeft(): int {
117
-		$o = $this->getOrientation();
118
-		$this->logger->debug('Image->widthTopLeft() Orientation: ' . $o, ['app' => 'core']);
119
-		switch ($o) {
120
-			case -1:
121
-			case 1:
122
-			case 2: // Not tested
123
-			case 3:
124
-			case 4: // Not tested
125
-				return $this->width();
126
-			case 5: // Not tested
127
-			case 6:
128
-			case 7: // Not tested
129
-			case 8:
130
-				return $this->height();
131
-		}
132
-		return $this->width();
133
-	}
134
-
135
-	/**
136
-	 * Returns the height when the image orientation is top-left.
137
-	 *
138
-	 * @return int
139
-	 */
140
-	public function heightTopLeft(): int {
141
-		$o = $this->getOrientation();
142
-		$this->logger->debug('Image->heightTopLeft() Orientation: ' . $o, ['app' => 'core']);
143
-		switch ($o) {
144
-			case -1:
145
-			case 1:
146
-			case 2: // Not tested
147
-			case 3:
148
-			case 4: // Not tested
149
-				return $this->height();
150
-			case 5: // Not tested
151
-			case 6:
152
-			case 7: // Not tested
153
-			case 8:
154
-				return $this->width();
155
-		}
156
-		return $this->height();
157
-	}
158
-
159
-	/**
160
-	 * Outputs the image.
161
-	 *
162
-	 * @param string $mimeType
163
-	 * @return bool
164
-	 */
165
-	public function show(?string $mimeType = null): bool {
166
-		if ($mimeType === null) {
167
-			$mimeType = $this->mimeType();
168
-		}
169
-		if ($mimeType !== null) {
170
-			header('Content-Type: ' . $mimeType);
171
-		}
172
-		return $this->_output(null, $mimeType);
173
-	}
174
-
175
-	/**
176
-	 * Saves the image.
177
-	 *
178
-	 * @param string $filePath
179
-	 * @param string $mimeType
180
-	 * @return bool
181
-	 */
182
-
183
-	public function save(?string $filePath = null, ?string $mimeType = null): bool {
184
-		if ($mimeType === null) {
185
-			$mimeType = $this->mimeType();
186
-		}
187
-		if ($filePath === null) {
188
-			if ($this->filePath === null) {
189
-				$this->logger->error(__METHOD__ . '(): called with no path.', ['app' => 'core']);
190
-				return false;
191
-			} else {
192
-				$filePath = $this->filePath;
193
-			}
194
-		}
195
-		return $this->_output($filePath, $mimeType);
196
-	}
197
-
198
-	/**
199
-	 * Outputs/saves the image.
200
-	 *
201
-	 * @throws \Exception
202
-	 */
203
-	private function _output(?string $filePath = null, ?string $mimeType = null): bool {
204
-		if ($filePath !== null && $filePath !== '') {
205
-			if (!file_exists(dirname($filePath))) {
206
-				mkdir(dirname($filePath), 0777, true);
207
-			}
208
-			$isWritable = is_writable(dirname($filePath));
209
-			if (!$isWritable) {
210
-				$this->logger->error(__METHOD__ . '(): Directory \'' . dirname($filePath) . '\' is not writable.', ['app' => 'core']);
211
-				return false;
212
-			} elseif (file_exists($filePath) && !is_writable($filePath)) {
213
-				$this->logger->error(__METHOD__ . '(): File \'' . $filePath . '\' is not writable.', ['app' => 'core']);
214
-				return false;
215
-			}
216
-		}
217
-		if (!$this->valid()) {
218
-			return false;
219
-		}
220
-
221
-		$imageType = $this->imageType;
222
-		if ($mimeType !== null) {
223
-			switch ($mimeType) {
224
-				case 'image/gif':
225
-					$imageType = IMAGETYPE_GIF;
226
-					break;
227
-				case 'image/jpeg':
228
-					$imageType = IMAGETYPE_JPEG;
229
-					break;
230
-				case 'image/png':
231
-					$imageType = IMAGETYPE_PNG;
232
-					break;
233
-				case 'image/x-xbitmap':
234
-					$imageType = IMAGETYPE_XBM;
235
-					break;
236
-				case 'image/bmp':
237
-				case 'image/x-ms-bmp':
238
-					$imageType = IMAGETYPE_BMP;
239
-					break;
240
-				case 'image/webp':
241
-					$imageType = IMAGETYPE_WEBP;
242
-					break;
243
-				default:
244
-					throw new \Exception('Image::_output(): "' . $mimeType . '" is not supported when forcing a specific output format');
245
-			}
246
-		}
247
-
248
-		switch ($imageType) {
249
-			case IMAGETYPE_GIF:
250
-				$retVal = imagegif($this->resource, $filePath);
251
-				break;
252
-			case IMAGETYPE_JPEG:
253
-				imageinterlace($this->resource, true);
254
-				$retVal = imagejpeg($this->resource, $filePath, $this->getJpegQuality());
255
-				break;
256
-			case IMAGETYPE_PNG:
257
-				$retVal = imagepng($this->resource, $filePath);
258
-				break;
259
-			case IMAGETYPE_XBM:
260
-				if (function_exists('imagexbm')) {
261
-					$retVal = imagexbm($this->resource, $filePath);
262
-				} else {
263
-					throw new \Exception('Image::_output(): imagexbm() is not supported.');
264
-				}
265
-
266
-				break;
267
-			case IMAGETYPE_WBMP:
268
-				$retVal = imagewbmp($this->resource, $filePath);
269
-				break;
270
-			case IMAGETYPE_BMP:
271
-				$retVal = imagebmp($this->resource, $filePath);
272
-				break;
273
-			case IMAGETYPE_WEBP:
274
-				$retVal = imagewebp($this->resource, null, $this->getWebpQuality());
275
-				break;
276
-			default:
277
-				$retVal = imagepng($this->resource, $filePath);
278
-		}
279
-		return $retVal;
280
-	}
281
-
282
-	/**
283
-	 * Prints the image when called as $image().
284
-	 */
285
-	public function __invoke() {
286
-		return $this->show();
287
-	}
288
-
289
-	/**
290
-	 * @param \GdImage $resource
291
-	 */
292
-	public function setResource(\GdImage $resource): void {
293
-		$this->resource = $resource;
294
-	}
295
-
296
-	/**
297
-	 * @return false|\GdImage Returns the image resource if any
298
-	 */
299
-	public function resource() {
300
-		return $this->resource;
301
-	}
302
-
303
-	/**
304
-	 * @return string Returns the mimetype of the data. Returns null if the data is not valid.
305
-	 */
306
-	public function dataMimeType(): ?string {
307
-		if (!$this->valid()) {
308
-			return null;
309
-		}
310
-
311
-		switch ($this->mimeType) {
312
-			case 'image/png':
313
-			case 'image/jpeg':
314
-			case 'image/gif':
315
-			case 'image/webp':
316
-				return $this->mimeType;
317
-			default:
318
-				return 'image/png';
319
-		}
320
-	}
321
-
322
-	/**
323
-	 * @return null|string Returns the raw image data.
324
-	 */
325
-	public function data(): ?string {
326
-		if (!$this->valid()) {
327
-			return null;
328
-		}
329
-		ob_start();
330
-		switch ($this->mimeType) {
331
-			case 'image/png':
332
-				$res = imagepng($this->resource);
333
-				break;
334
-			case 'image/jpeg':
335
-				imageinterlace($this->resource, true);
336
-				$quality = $this->getJpegQuality();
337
-				$res = imagejpeg($this->resource, null, $quality);
338
-				break;
339
-			case 'image/gif':
340
-				$res = imagegif($this->resource);
341
-				break;
342
-			case 'image/webp':
343
-				$res = imagewebp($this->resource, null, $this->getWebpQuality());
344
-				break;
345
-			default:
346
-				$res = imagepng($this->resource);
347
-				$this->logger->info('Image->data. Could not guess mime-type, defaulting to png', ['app' => 'core']);
348
-				break;
349
-		}
350
-		if (!$res) {
351
-			$this->logger->error('Image->data. Error getting image data.', ['app' => 'core']);
352
-		}
353
-		return ob_get_clean();
354
-	}
355
-
356
-	/**
357
-	 * @return string - base64 encoded, which is suitable for embedding in a VCard.
358
-	 */
359
-	public function __toString(): string {
360
-		$data = $this->data();
361
-		if ($data === null) {
362
-			return '';
363
-		} else {
364
-			return base64_encode($data);
365
-		}
366
-	}
367
-
368
-	protected function getJpegQuality(): int {
369
-		$quality = $this->appConfig->getValueInt('preview', 'jpeg_quality', self::DEFAULT_JPEG_QUALITY);
370
-		return min(100, max(10, $quality));
371
-	}
372
-
373
-	protected function getWebpQuality(): int {
374
-		$quality = $this->appConfig->getValueInt('preview', 'webp_quality', self::DEFAULT_WEBP_QUALITY);
375
-		return min(100, max(10, $quality));
376
-	}
377
-
378
-	private function isValidExifData(array $exif): bool {
379
-		if (!isset($exif['Orientation'])) {
380
-			return false;
381
-		}
382
-
383
-		if (!is_numeric($exif['Orientation'])) {
384
-			return false;
385
-		}
386
-
387
-		return true;
388
-	}
389
-
390
-	/**
391
-	 * (I'm open for suggestions on better method name ;)
392
-	 * Get the orientation based on EXIF data.
393
-	 *
394
-	 * @return int The orientation or -1 if no EXIF data is available.
395
-	 */
396
-	public function getOrientation(): int {
397
-		if ($this->exif !== null) {
398
-			return $this->exif['Orientation'];
399
-		}
400
-
401
-		if ($this->imageType !== IMAGETYPE_JPEG) {
402
-			$this->logger->debug('Image->fixOrientation() Image is not a JPEG.', ['app' => 'core']);
403
-			return -1;
404
-		}
405
-		if (!is_callable('exif_read_data')) {
406
-			$this->logger->debug('Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
407
-			return -1;
408
-		}
409
-		if (!$this->valid()) {
410
-			$this->logger->debug('Image->fixOrientation() No image loaded.', ['app' => 'core']);
411
-			return -1;
412
-		}
413
-		if (is_null($this->filePath) || !is_readable($this->filePath)) {
414
-			$this->logger->debug('Image->fixOrientation() No readable file path set.', ['app' => 'core']);
415
-			return -1;
416
-		}
417
-		$exif = @exif_read_data($this->filePath, 'IFD0');
418
-		if ($exif === false || !$this->isValidExifData($exif)) {
419
-			return -1;
420
-		}
421
-		$this->exif = $exif;
422
-		return (int)$exif['Orientation'];
423
-	}
424
-
425
-	public function readExif(string $data): void {
426
-		if (!is_callable('exif_read_data')) {
427
-			$this->logger->debug('Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
428
-			return;
429
-		}
430
-		if (!$this->valid()) {
431
-			$this->logger->debug('Image->fixOrientation() No image loaded.', ['app' => 'core']);
432
-			return;
433
-		}
434
-
435
-		$exif = @exif_read_data('data://image/jpeg;base64,' . base64_encode($data));
436
-		if ($exif === false || !$this->isValidExifData($exif)) {
437
-			return;
438
-		}
439
-		$this->exif = $exif;
440
-	}
441
-
442
-	/**
443
-	 * (I'm open for suggestions on better method name ;)
444
-	 * Fixes orientation based on EXIF data.
445
-	 *
446
-	 * @return bool
447
-	 */
448
-	public function fixOrientation(): bool {
449
-		if (!$this->valid()) {
450
-			$this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
451
-			return false;
452
-		}
453
-		$o = $this->getOrientation();
454
-		$this->logger->debug('Image->fixOrientation() Orientation: ' . $o, ['app' => 'core']);
455
-		$rotate = 0;
456
-		$flip = false;
457
-		switch ($o) {
458
-			case -1:
459
-				return false; //Nothing to fix
460
-			case 1:
461
-				$rotate = 0;
462
-				break;
463
-			case 2:
464
-				$rotate = 0;
465
-				$flip = true;
466
-				break;
467
-			case 3:
468
-				$rotate = 180;
469
-				break;
470
-			case 4:
471
-				$rotate = 180;
472
-				$flip = true;
473
-				break;
474
-			case 5:
475
-				$rotate = 90;
476
-				$flip = true;
477
-				break;
478
-			case 6:
479
-				$rotate = 270;
480
-				break;
481
-			case 7:
482
-				$rotate = 270;
483
-				$flip = true;
484
-				break;
485
-			case 8:
486
-				$rotate = 90;
487
-				break;
488
-		}
489
-		if ($flip && function_exists('imageflip')) {
490
-			imageflip($this->resource, IMG_FLIP_HORIZONTAL);
491
-		}
492
-		if ($rotate) {
493
-			$res = imagerotate($this->resource, $rotate, 0);
494
-			if ($res) {
495
-				if (imagealphablending($res, true)) {
496
-					if (imagesavealpha($res, true)) {
497
-						imagedestroy($this->resource);
498
-						$this->resource = $res;
499
-						return true;
500
-					} else {
501
-						$this->logger->debug('Image->fixOrientation() Error during alpha-saving', ['app' => 'core']);
502
-						return false;
503
-					}
504
-				} else {
505
-					$this->logger->debug('Image->fixOrientation() Error during alpha-blending', ['app' => 'core']);
506
-					return false;
507
-				}
508
-			} else {
509
-				$this->logger->debug('Image->fixOrientation() Error during orientation fixing', ['app' => 'core']);
510
-				return false;
511
-			}
512
-		}
513
-		return false;
514
-	}
515
-
516
-	/**
517
-	 * Loads an image from an open file handle.
518
-	 * It is the responsibility of the caller to position the pointer at the correct place and to close the handle again.
519
-	 *
520
-	 * @param resource $handle
521
-	 * @return \GdImage|false An image resource or false on error
522
-	 */
523
-	public function loadFromFileHandle($handle) {
524
-		$contents = stream_get_contents($handle);
525
-		if ($this->loadFromData($contents)) {
526
-			return $this->resource;
527
-		}
528
-		return false;
529
-	}
530
-
531
-	/**
532
-	 * Check if allocating an image with the given size is allowed.
533
-	 *
534
-	 * @param int $width The image width.
535
-	 * @param int $height The image height.
536
-	 * @return bool true if allocating is allowed, false otherwise
537
-	 */
538
-	private function checkImageMemory($width, $height) {
539
-		$memory_limit = $this->config->getSystemValueInt('preview_max_memory', self::DEFAULT_MEMORY_LIMIT);
540
-		if ($memory_limit < 0) {
541
-			// Not limited.
542
-			return true;
543
-		}
544
-
545
-		// Assume 32 bits per pixel.
546
-		if ($width * $height * 4 > $memory_limit * 1024 * 1024) {
547
-			$this->logger->info('Image size of ' . $width . 'x' . $height . ' would exceed allowed memory limit of ' . $memory_limit . '. You may increase the preview_max_memory in your config.php if you need previews of this image.');
548
-			return false;
549
-		}
550
-
551
-		return true;
552
-	}
553
-
554
-	/**
555
-	 * Check if loading an image file from the given path is allowed.
556
-	 *
557
-	 * @param string $path The path to a local file.
558
-	 * @return bool true if allocating is allowed, false otherwise
559
-	 */
560
-	private function checkImageSize($path) {
561
-		$size = @getimagesize($path);
562
-		if (!$size) {
563
-			return false;
564
-		}
565
-
566
-		$width = $size[0];
567
-		$height = $size[1];
568
-		if (!$this->checkImageMemory($width, $height)) {
569
-			return false;
570
-		}
571
-
572
-		return true;
573
-	}
574
-
575
-	/**
576
-	 * Check if loading an image from the given data is allowed.
577
-	 *
578
-	 * @param string $data A string of image data as read from a file.
579
-	 * @return bool true if allocating is allowed, false otherwise
580
-	 */
581
-	private function checkImageDataSize($data) {
582
-		$size = @getimagesizefromstring($data);
583
-		if (!$size) {
584
-			return false;
585
-		}
586
-
587
-		$width = $size[0];
588
-		$height = $size[1];
589
-		if (!$this->checkImageMemory($width, $height)) {
590
-			return false;
591
-		}
592
-
593
-		return true;
594
-	}
595
-
596
-	/**
597
-	 * Loads an image from a local file.
598
-	 *
599
-	 * @param bool|string $imagePath The path to a local file.
600
-	 * @return bool|\GdImage An image resource or false on error
601
-	 */
602
-	public function loadFromFile($imagePath = false) {
603
-		// exif_imagetype throws "read error!" if file is less than 12 byte
604
-		if (is_bool($imagePath) || !@is_file($imagePath) || !file_exists($imagePath) || filesize($imagePath) < 12 || !is_readable($imagePath)) {
605
-			return false;
606
-		}
607
-		$iType = exif_imagetype($imagePath);
608
-		switch ($iType) {
609
-			case IMAGETYPE_GIF:
610
-				if (imagetypes() & IMG_GIF) {
611
-					if (!$this->checkImageSize($imagePath)) {
612
-						return false;
613
-					}
614
-					$this->resource = imagecreatefromgif($imagePath);
615
-					if ($this->resource) {
616
-						// Preserve transparency
617
-						imagealphablending($this->resource, true);
618
-						imagesavealpha($this->resource, true);
619
-					} else {
620
-						$this->logger->debug('Image->loadFromFile, GIF image not valid: ' . $imagePath, ['app' => 'core']);
621
-					}
622
-				} else {
623
-					$this->logger->debug('Image->loadFromFile, GIF images not supported: ' . $imagePath, ['app' => 'core']);
624
-				}
625
-				break;
626
-			case IMAGETYPE_JPEG:
627
-				if (imagetypes() & IMG_JPG) {
628
-					if (!$this->checkImageSize($imagePath)) {
629
-						return false;
630
-					}
631
-					if (@getimagesize($imagePath) !== false) {
632
-						$this->resource = @imagecreatefromjpeg($imagePath);
633
-					} else {
634
-						$this->logger->debug('Image->loadFromFile, JPG image not valid: ' . $imagePath, ['app' => 'core']);
635
-					}
636
-				} else {
637
-					$this->logger->debug('Image->loadFromFile, JPG images not supported: ' . $imagePath, ['app' => 'core']);
638
-				}
639
-				break;
640
-			case IMAGETYPE_PNG:
641
-				if (imagetypes() & IMG_PNG) {
642
-					if (!$this->checkImageSize($imagePath)) {
643
-						return false;
644
-					}
645
-					$this->resource = @imagecreatefrompng($imagePath);
646
-					if ($this->resource) {
647
-						// Preserve transparency
648
-						imagealphablending($this->resource, true);
649
-						imagesavealpha($this->resource, true);
650
-					} else {
651
-						$this->logger->debug('Image->loadFromFile, PNG image not valid: ' . $imagePath, ['app' => 'core']);
652
-					}
653
-				} else {
654
-					$this->logger->debug('Image->loadFromFile, PNG images not supported: ' . $imagePath, ['app' => 'core']);
655
-				}
656
-				break;
657
-			case IMAGETYPE_XBM:
658
-				if (imagetypes() & IMG_XPM) {
659
-					if (!$this->checkImageSize($imagePath)) {
660
-						return false;
661
-					}
662
-					$this->resource = @imagecreatefromxbm($imagePath);
663
-				} else {
664
-					$this->logger->debug('Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']);
665
-				}
666
-				break;
667
-			case IMAGETYPE_WBMP:
668
-				if (imagetypes() & IMG_WBMP) {
669
-					if (!$this->checkImageSize($imagePath)) {
670
-						return false;
671
-					}
672
-					$this->resource = @imagecreatefromwbmp($imagePath);
673
-				} else {
674
-					$this->logger->debug('Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']);
675
-				}
676
-				break;
677
-			case IMAGETYPE_BMP:
678
-				$this->resource = imagecreatefrombmp($imagePath);
679
-				break;
680
-			case IMAGETYPE_WEBP:
681
-				if (imagetypes() & IMG_WEBP) {
682
-					if (!$this->checkImageSize($imagePath)) {
683
-						return false;
684
-					}
685
-
686
-					// Check for animated header before generating preview since libgd does not handle them well
687
-					// Adapted from here: https://stackoverflow.com/a/68491679/4085517 (stripped to only to check for animations + added additional error checking)
688
-					// Header format details here: https://developers.google.com/speed/webp/docs/riff_container
689
-
690
-					// Load up the header data, if any
691
-					$fp = fopen($imagePath, 'rb');
692
-					if (!$fp) {
693
-						return false;
694
-					}
695
-					$data = fread($fp, 90);
696
-					if ($data === false) {
697
-						return false;
698
-					}
699
-					fclose($fp);
700
-					unset($fp);
701
-
702
-					$headerFormat = 'A4Riff/' // get n string
703
-						. 'I1Filesize/' // get integer (file size but not actual size)
704
-						. 'A4Webp/' // get n string
705
-						. 'A4Vp/' // get n string
706
-						. 'A74Chunk';
707
-
708
-					$header = unpack($headerFormat, $data);
709
-					unset($data, $headerFormat);
710
-					if ($header === false) {
711
-						return false;
712
-					}
713
-
714
-					// Check if we're really dealing with a valid WEBP header rather than just one suffixed ".webp"
715
-					if (!isset($header['Riff']) || strtoupper($header['Riff']) !== 'RIFF') {
716
-						return false;
717
-					}
718
-					if (!isset($header['Webp']) || strtoupper($header['Webp']) !== 'WEBP') {
719
-						return false;
720
-					}
721
-					if (!isset($header['Vp']) || strpos(strtoupper($header['Vp']), 'VP8') === false) {
722
-						return false;
723
-					}
724
-
725
-					// Check for animation indicators
726
-					if (strpos(strtoupper($header['Chunk']), 'ANIM') !== false || strpos(strtoupper($header['Chunk']), 'ANMF') !== false) {
727
-						// Animated so don't let it reach libgd
728
-						$this->logger->debug('Image->loadFromFile, animated WEBP images not supported: ' . $imagePath, ['app' => 'core']);
729
-					} else {
730
-						// We're safe so give it to libgd
731
-						$this->resource = @imagecreatefromwebp($imagePath);
732
-					}
733
-				} else {
734
-					$this->logger->debug('Image->loadFromFile, WEBP images not supported: ' . $imagePath, ['app' => 'core']);
735
-				}
736
-				break;
737
-				/*
25
+    // Default memory limit for images to load (256 MBytes).
26
+    protected const DEFAULT_MEMORY_LIMIT = 256;
27
+
28
+    // Default quality for jpeg images
29
+    protected const DEFAULT_JPEG_QUALITY = 80;
30
+
31
+    // Default quality for webp images
32
+    protected const DEFAULT_WEBP_QUALITY = 80;
33
+
34
+    // tmp resource.
35
+    protected GdImage|false $resource = false;
36
+    // Default to png if file type isn't evident.
37
+    protected int $imageType = IMAGETYPE_PNG;
38
+    // Default to png
39
+    protected ?string $mimeType = 'image/png';
40
+    protected ?string $filePath = null;
41
+    private ?finfo $fileInfo = null;
42
+    private LoggerInterface $logger;
43
+    private IAppConfig $appConfig;
44
+    private IConfig $config;
45
+    private ?array $exif = null;
46
+
47
+    /**
48
+     * @throws \InvalidArgumentException in case the $imageRef parameter is not null
49
+     */
50
+    public function __construct(
51
+        ?LoggerInterface $logger = null,
52
+        ?IAppConfig $appConfig = null,
53
+        ?IConfig $config = null,
54
+    ) {
55
+        $this->logger = $logger ?? Server::get(LoggerInterface::class);
56
+        $this->appConfig = $appConfig ?? Server::get(IAppConfig::class);
57
+        $this->config = $config ?? Server::get(IConfig::class);
58
+
59
+        if (class_exists(finfo::class)) {
60
+            $this->fileInfo = new finfo(FILEINFO_MIME_TYPE);
61
+        }
62
+    }
63
+
64
+    /**
65
+     * Determine whether the object contains an image resource.
66
+     *
67
+     * @psalm-assert-if-true \GdImage $this->resource
68
+     * @return bool
69
+     */
70
+    public function valid(): bool {
71
+        if (is_object($this->resource) && get_class($this->resource) === \GdImage::class) {
72
+            return true;
73
+        }
74
+
75
+        return false;
76
+    }
77
+
78
+    /**
79
+     * Returns the MIME type of the image or null if no image is loaded.
80
+     *
81
+     * @return string
82
+     */
83
+    public function mimeType(): ?string {
84
+        return $this->valid() ? $this->mimeType : null;
85
+    }
86
+
87
+    /**
88
+     * Returns the width of the image or -1 if no image is loaded.
89
+     *
90
+     * @return int
91
+     */
92
+    public function width(): int {
93
+        if ($this->valid()) {
94
+            return imagesx($this->resource);
95
+        }
96
+        return -1;
97
+    }
98
+
99
+    /**
100
+     * Returns the height of the image or -1 if no image is loaded.
101
+     *
102
+     * @return int
103
+     */
104
+    public function height(): int {
105
+        if ($this->valid()) {
106
+            return imagesy($this->resource);
107
+        }
108
+        return -1;
109
+    }
110
+
111
+    /**
112
+     * Returns the width when the image orientation is top-left.
113
+     *
114
+     * @return int
115
+     */
116
+    public function widthTopLeft(): int {
117
+        $o = $this->getOrientation();
118
+        $this->logger->debug('Image->widthTopLeft() Orientation: ' . $o, ['app' => 'core']);
119
+        switch ($o) {
120
+            case -1:
121
+            case 1:
122
+            case 2: // Not tested
123
+            case 3:
124
+            case 4: // Not tested
125
+                return $this->width();
126
+            case 5: // Not tested
127
+            case 6:
128
+            case 7: // Not tested
129
+            case 8:
130
+                return $this->height();
131
+        }
132
+        return $this->width();
133
+    }
134
+
135
+    /**
136
+     * Returns the height when the image orientation is top-left.
137
+     *
138
+     * @return int
139
+     */
140
+    public function heightTopLeft(): int {
141
+        $o = $this->getOrientation();
142
+        $this->logger->debug('Image->heightTopLeft() Orientation: ' . $o, ['app' => 'core']);
143
+        switch ($o) {
144
+            case -1:
145
+            case 1:
146
+            case 2: // Not tested
147
+            case 3:
148
+            case 4: // Not tested
149
+                return $this->height();
150
+            case 5: // Not tested
151
+            case 6:
152
+            case 7: // Not tested
153
+            case 8:
154
+                return $this->width();
155
+        }
156
+        return $this->height();
157
+    }
158
+
159
+    /**
160
+     * Outputs the image.
161
+     *
162
+     * @param string $mimeType
163
+     * @return bool
164
+     */
165
+    public function show(?string $mimeType = null): bool {
166
+        if ($mimeType === null) {
167
+            $mimeType = $this->mimeType();
168
+        }
169
+        if ($mimeType !== null) {
170
+            header('Content-Type: ' . $mimeType);
171
+        }
172
+        return $this->_output(null, $mimeType);
173
+    }
174
+
175
+    /**
176
+     * Saves the image.
177
+     *
178
+     * @param string $filePath
179
+     * @param string $mimeType
180
+     * @return bool
181
+     */
182
+
183
+    public function save(?string $filePath = null, ?string $mimeType = null): bool {
184
+        if ($mimeType === null) {
185
+            $mimeType = $this->mimeType();
186
+        }
187
+        if ($filePath === null) {
188
+            if ($this->filePath === null) {
189
+                $this->logger->error(__METHOD__ . '(): called with no path.', ['app' => 'core']);
190
+                return false;
191
+            } else {
192
+                $filePath = $this->filePath;
193
+            }
194
+        }
195
+        return $this->_output($filePath, $mimeType);
196
+    }
197
+
198
+    /**
199
+     * Outputs/saves the image.
200
+     *
201
+     * @throws \Exception
202
+     */
203
+    private function _output(?string $filePath = null, ?string $mimeType = null): bool {
204
+        if ($filePath !== null && $filePath !== '') {
205
+            if (!file_exists(dirname($filePath))) {
206
+                mkdir(dirname($filePath), 0777, true);
207
+            }
208
+            $isWritable = is_writable(dirname($filePath));
209
+            if (!$isWritable) {
210
+                $this->logger->error(__METHOD__ . '(): Directory \'' . dirname($filePath) . '\' is not writable.', ['app' => 'core']);
211
+                return false;
212
+            } elseif (file_exists($filePath) && !is_writable($filePath)) {
213
+                $this->logger->error(__METHOD__ . '(): File \'' . $filePath . '\' is not writable.', ['app' => 'core']);
214
+                return false;
215
+            }
216
+        }
217
+        if (!$this->valid()) {
218
+            return false;
219
+        }
220
+
221
+        $imageType = $this->imageType;
222
+        if ($mimeType !== null) {
223
+            switch ($mimeType) {
224
+                case 'image/gif':
225
+                    $imageType = IMAGETYPE_GIF;
226
+                    break;
227
+                case 'image/jpeg':
228
+                    $imageType = IMAGETYPE_JPEG;
229
+                    break;
230
+                case 'image/png':
231
+                    $imageType = IMAGETYPE_PNG;
232
+                    break;
233
+                case 'image/x-xbitmap':
234
+                    $imageType = IMAGETYPE_XBM;
235
+                    break;
236
+                case 'image/bmp':
237
+                case 'image/x-ms-bmp':
238
+                    $imageType = IMAGETYPE_BMP;
239
+                    break;
240
+                case 'image/webp':
241
+                    $imageType = IMAGETYPE_WEBP;
242
+                    break;
243
+                default:
244
+                    throw new \Exception('Image::_output(): "' . $mimeType . '" is not supported when forcing a specific output format');
245
+            }
246
+        }
247
+
248
+        switch ($imageType) {
249
+            case IMAGETYPE_GIF:
250
+                $retVal = imagegif($this->resource, $filePath);
251
+                break;
252
+            case IMAGETYPE_JPEG:
253
+                imageinterlace($this->resource, true);
254
+                $retVal = imagejpeg($this->resource, $filePath, $this->getJpegQuality());
255
+                break;
256
+            case IMAGETYPE_PNG:
257
+                $retVal = imagepng($this->resource, $filePath);
258
+                break;
259
+            case IMAGETYPE_XBM:
260
+                if (function_exists('imagexbm')) {
261
+                    $retVal = imagexbm($this->resource, $filePath);
262
+                } else {
263
+                    throw new \Exception('Image::_output(): imagexbm() is not supported.');
264
+                }
265
+
266
+                break;
267
+            case IMAGETYPE_WBMP:
268
+                $retVal = imagewbmp($this->resource, $filePath);
269
+                break;
270
+            case IMAGETYPE_BMP:
271
+                $retVal = imagebmp($this->resource, $filePath);
272
+                break;
273
+            case IMAGETYPE_WEBP:
274
+                $retVal = imagewebp($this->resource, null, $this->getWebpQuality());
275
+                break;
276
+            default:
277
+                $retVal = imagepng($this->resource, $filePath);
278
+        }
279
+        return $retVal;
280
+    }
281
+
282
+    /**
283
+     * Prints the image when called as $image().
284
+     */
285
+    public function __invoke() {
286
+        return $this->show();
287
+    }
288
+
289
+    /**
290
+     * @param \GdImage $resource
291
+     */
292
+    public function setResource(\GdImage $resource): void {
293
+        $this->resource = $resource;
294
+    }
295
+
296
+    /**
297
+     * @return false|\GdImage Returns the image resource if any
298
+     */
299
+    public function resource() {
300
+        return $this->resource;
301
+    }
302
+
303
+    /**
304
+     * @return string Returns the mimetype of the data. Returns null if the data is not valid.
305
+     */
306
+    public function dataMimeType(): ?string {
307
+        if (!$this->valid()) {
308
+            return null;
309
+        }
310
+
311
+        switch ($this->mimeType) {
312
+            case 'image/png':
313
+            case 'image/jpeg':
314
+            case 'image/gif':
315
+            case 'image/webp':
316
+                return $this->mimeType;
317
+            default:
318
+                return 'image/png';
319
+        }
320
+    }
321
+
322
+    /**
323
+     * @return null|string Returns the raw image data.
324
+     */
325
+    public function data(): ?string {
326
+        if (!$this->valid()) {
327
+            return null;
328
+        }
329
+        ob_start();
330
+        switch ($this->mimeType) {
331
+            case 'image/png':
332
+                $res = imagepng($this->resource);
333
+                break;
334
+            case 'image/jpeg':
335
+                imageinterlace($this->resource, true);
336
+                $quality = $this->getJpegQuality();
337
+                $res = imagejpeg($this->resource, null, $quality);
338
+                break;
339
+            case 'image/gif':
340
+                $res = imagegif($this->resource);
341
+                break;
342
+            case 'image/webp':
343
+                $res = imagewebp($this->resource, null, $this->getWebpQuality());
344
+                break;
345
+            default:
346
+                $res = imagepng($this->resource);
347
+                $this->logger->info('Image->data. Could not guess mime-type, defaulting to png', ['app' => 'core']);
348
+                break;
349
+        }
350
+        if (!$res) {
351
+            $this->logger->error('Image->data. Error getting image data.', ['app' => 'core']);
352
+        }
353
+        return ob_get_clean();
354
+    }
355
+
356
+    /**
357
+     * @return string - base64 encoded, which is suitable for embedding in a VCard.
358
+     */
359
+    public function __toString(): string {
360
+        $data = $this->data();
361
+        if ($data === null) {
362
+            return '';
363
+        } else {
364
+            return base64_encode($data);
365
+        }
366
+    }
367
+
368
+    protected function getJpegQuality(): int {
369
+        $quality = $this->appConfig->getValueInt('preview', 'jpeg_quality', self::DEFAULT_JPEG_QUALITY);
370
+        return min(100, max(10, $quality));
371
+    }
372
+
373
+    protected function getWebpQuality(): int {
374
+        $quality = $this->appConfig->getValueInt('preview', 'webp_quality', self::DEFAULT_WEBP_QUALITY);
375
+        return min(100, max(10, $quality));
376
+    }
377
+
378
+    private function isValidExifData(array $exif): bool {
379
+        if (!isset($exif['Orientation'])) {
380
+            return false;
381
+        }
382
+
383
+        if (!is_numeric($exif['Orientation'])) {
384
+            return false;
385
+        }
386
+
387
+        return true;
388
+    }
389
+
390
+    /**
391
+     * (I'm open for suggestions on better method name ;)
392
+     * Get the orientation based on EXIF data.
393
+     *
394
+     * @return int The orientation or -1 if no EXIF data is available.
395
+     */
396
+    public function getOrientation(): int {
397
+        if ($this->exif !== null) {
398
+            return $this->exif['Orientation'];
399
+        }
400
+
401
+        if ($this->imageType !== IMAGETYPE_JPEG) {
402
+            $this->logger->debug('Image->fixOrientation() Image is not a JPEG.', ['app' => 'core']);
403
+            return -1;
404
+        }
405
+        if (!is_callable('exif_read_data')) {
406
+            $this->logger->debug('Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
407
+            return -1;
408
+        }
409
+        if (!$this->valid()) {
410
+            $this->logger->debug('Image->fixOrientation() No image loaded.', ['app' => 'core']);
411
+            return -1;
412
+        }
413
+        if (is_null($this->filePath) || !is_readable($this->filePath)) {
414
+            $this->logger->debug('Image->fixOrientation() No readable file path set.', ['app' => 'core']);
415
+            return -1;
416
+        }
417
+        $exif = @exif_read_data($this->filePath, 'IFD0');
418
+        if ($exif === false || !$this->isValidExifData($exif)) {
419
+            return -1;
420
+        }
421
+        $this->exif = $exif;
422
+        return (int)$exif['Orientation'];
423
+    }
424
+
425
+    public function readExif(string $data): void {
426
+        if (!is_callable('exif_read_data')) {
427
+            $this->logger->debug('Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
428
+            return;
429
+        }
430
+        if (!$this->valid()) {
431
+            $this->logger->debug('Image->fixOrientation() No image loaded.', ['app' => 'core']);
432
+            return;
433
+        }
434
+
435
+        $exif = @exif_read_data('data://image/jpeg;base64,' . base64_encode($data));
436
+        if ($exif === false || !$this->isValidExifData($exif)) {
437
+            return;
438
+        }
439
+        $this->exif = $exif;
440
+    }
441
+
442
+    /**
443
+     * (I'm open for suggestions on better method name ;)
444
+     * Fixes orientation based on EXIF data.
445
+     *
446
+     * @return bool
447
+     */
448
+    public function fixOrientation(): bool {
449
+        if (!$this->valid()) {
450
+            $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
451
+            return false;
452
+        }
453
+        $o = $this->getOrientation();
454
+        $this->logger->debug('Image->fixOrientation() Orientation: ' . $o, ['app' => 'core']);
455
+        $rotate = 0;
456
+        $flip = false;
457
+        switch ($o) {
458
+            case -1:
459
+                return false; //Nothing to fix
460
+            case 1:
461
+                $rotate = 0;
462
+                break;
463
+            case 2:
464
+                $rotate = 0;
465
+                $flip = true;
466
+                break;
467
+            case 3:
468
+                $rotate = 180;
469
+                break;
470
+            case 4:
471
+                $rotate = 180;
472
+                $flip = true;
473
+                break;
474
+            case 5:
475
+                $rotate = 90;
476
+                $flip = true;
477
+                break;
478
+            case 6:
479
+                $rotate = 270;
480
+                break;
481
+            case 7:
482
+                $rotate = 270;
483
+                $flip = true;
484
+                break;
485
+            case 8:
486
+                $rotate = 90;
487
+                break;
488
+        }
489
+        if ($flip && function_exists('imageflip')) {
490
+            imageflip($this->resource, IMG_FLIP_HORIZONTAL);
491
+        }
492
+        if ($rotate) {
493
+            $res = imagerotate($this->resource, $rotate, 0);
494
+            if ($res) {
495
+                if (imagealphablending($res, true)) {
496
+                    if (imagesavealpha($res, true)) {
497
+                        imagedestroy($this->resource);
498
+                        $this->resource = $res;
499
+                        return true;
500
+                    } else {
501
+                        $this->logger->debug('Image->fixOrientation() Error during alpha-saving', ['app' => 'core']);
502
+                        return false;
503
+                    }
504
+                } else {
505
+                    $this->logger->debug('Image->fixOrientation() Error during alpha-blending', ['app' => 'core']);
506
+                    return false;
507
+                }
508
+            } else {
509
+                $this->logger->debug('Image->fixOrientation() Error during orientation fixing', ['app' => 'core']);
510
+                return false;
511
+            }
512
+        }
513
+        return false;
514
+    }
515
+
516
+    /**
517
+     * Loads an image from an open file handle.
518
+     * It is the responsibility of the caller to position the pointer at the correct place and to close the handle again.
519
+     *
520
+     * @param resource $handle
521
+     * @return \GdImage|false An image resource or false on error
522
+     */
523
+    public function loadFromFileHandle($handle) {
524
+        $contents = stream_get_contents($handle);
525
+        if ($this->loadFromData($contents)) {
526
+            return $this->resource;
527
+        }
528
+        return false;
529
+    }
530
+
531
+    /**
532
+     * Check if allocating an image with the given size is allowed.
533
+     *
534
+     * @param int $width The image width.
535
+     * @param int $height The image height.
536
+     * @return bool true if allocating is allowed, false otherwise
537
+     */
538
+    private function checkImageMemory($width, $height) {
539
+        $memory_limit = $this->config->getSystemValueInt('preview_max_memory', self::DEFAULT_MEMORY_LIMIT);
540
+        if ($memory_limit < 0) {
541
+            // Not limited.
542
+            return true;
543
+        }
544
+
545
+        // Assume 32 bits per pixel.
546
+        if ($width * $height * 4 > $memory_limit * 1024 * 1024) {
547
+            $this->logger->info('Image size of ' . $width . 'x' . $height . ' would exceed allowed memory limit of ' . $memory_limit . '. You may increase the preview_max_memory in your config.php if you need previews of this image.');
548
+            return false;
549
+        }
550
+
551
+        return true;
552
+    }
553
+
554
+    /**
555
+     * Check if loading an image file from the given path is allowed.
556
+     *
557
+     * @param string $path The path to a local file.
558
+     * @return bool true if allocating is allowed, false otherwise
559
+     */
560
+    private function checkImageSize($path) {
561
+        $size = @getimagesize($path);
562
+        if (!$size) {
563
+            return false;
564
+        }
565
+
566
+        $width = $size[0];
567
+        $height = $size[1];
568
+        if (!$this->checkImageMemory($width, $height)) {
569
+            return false;
570
+        }
571
+
572
+        return true;
573
+    }
574
+
575
+    /**
576
+     * Check if loading an image from the given data is allowed.
577
+     *
578
+     * @param string $data A string of image data as read from a file.
579
+     * @return bool true if allocating is allowed, false otherwise
580
+     */
581
+    private function checkImageDataSize($data) {
582
+        $size = @getimagesizefromstring($data);
583
+        if (!$size) {
584
+            return false;
585
+        }
586
+
587
+        $width = $size[0];
588
+        $height = $size[1];
589
+        if (!$this->checkImageMemory($width, $height)) {
590
+            return false;
591
+        }
592
+
593
+        return true;
594
+    }
595
+
596
+    /**
597
+     * Loads an image from a local file.
598
+     *
599
+     * @param bool|string $imagePath The path to a local file.
600
+     * @return bool|\GdImage An image resource or false on error
601
+     */
602
+    public function loadFromFile($imagePath = false) {
603
+        // exif_imagetype throws "read error!" if file is less than 12 byte
604
+        if (is_bool($imagePath) || !@is_file($imagePath) || !file_exists($imagePath) || filesize($imagePath) < 12 || !is_readable($imagePath)) {
605
+            return false;
606
+        }
607
+        $iType = exif_imagetype($imagePath);
608
+        switch ($iType) {
609
+            case IMAGETYPE_GIF:
610
+                if (imagetypes() & IMG_GIF) {
611
+                    if (!$this->checkImageSize($imagePath)) {
612
+                        return false;
613
+                    }
614
+                    $this->resource = imagecreatefromgif($imagePath);
615
+                    if ($this->resource) {
616
+                        // Preserve transparency
617
+                        imagealphablending($this->resource, true);
618
+                        imagesavealpha($this->resource, true);
619
+                    } else {
620
+                        $this->logger->debug('Image->loadFromFile, GIF image not valid: ' . $imagePath, ['app' => 'core']);
621
+                    }
622
+                } else {
623
+                    $this->logger->debug('Image->loadFromFile, GIF images not supported: ' . $imagePath, ['app' => 'core']);
624
+                }
625
+                break;
626
+            case IMAGETYPE_JPEG:
627
+                if (imagetypes() & IMG_JPG) {
628
+                    if (!$this->checkImageSize($imagePath)) {
629
+                        return false;
630
+                    }
631
+                    if (@getimagesize($imagePath) !== false) {
632
+                        $this->resource = @imagecreatefromjpeg($imagePath);
633
+                    } else {
634
+                        $this->logger->debug('Image->loadFromFile, JPG image not valid: ' . $imagePath, ['app' => 'core']);
635
+                    }
636
+                } else {
637
+                    $this->logger->debug('Image->loadFromFile, JPG images not supported: ' . $imagePath, ['app' => 'core']);
638
+                }
639
+                break;
640
+            case IMAGETYPE_PNG:
641
+                if (imagetypes() & IMG_PNG) {
642
+                    if (!$this->checkImageSize($imagePath)) {
643
+                        return false;
644
+                    }
645
+                    $this->resource = @imagecreatefrompng($imagePath);
646
+                    if ($this->resource) {
647
+                        // Preserve transparency
648
+                        imagealphablending($this->resource, true);
649
+                        imagesavealpha($this->resource, true);
650
+                    } else {
651
+                        $this->logger->debug('Image->loadFromFile, PNG image not valid: ' . $imagePath, ['app' => 'core']);
652
+                    }
653
+                } else {
654
+                    $this->logger->debug('Image->loadFromFile, PNG images not supported: ' . $imagePath, ['app' => 'core']);
655
+                }
656
+                break;
657
+            case IMAGETYPE_XBM:
658
+                if (imagetypes() & IMG_XPM) {
659
+                    if (!$this->checkImageSize($imagePath)) {
660
+                        return false;
661
+                    }
662
+                    $this->resource = @imagecreatefromxbm($imagePath);
663
+                } else {
664
+                    $this->logger->debug('Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']);
665
+                }
666
+                break;
667
+            case IMAGETYPE_WBMP:
668
+                if (imagetypes() & IMG_WBMP) {
669
+                    if (!$this->checkImageSize($imagePath)) {
670
+                        return false;
671
+                    }
672
+                    $this->resource = @imagecreatefromwbmp($imagePath);
673
+                } else {
674
+                    $this->logger->debug('Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']);
675
+                }
676
+                break;
677
+            case IMAGETYPE_BMP:
678
+                $this->resource = imagecreatefrombmp($imagePath);
679
+                break;
680
+            case IMAGETYPE_WEBP:
681
+                if (imagetypes() & IMG_WEBP) {
682
+                    if (!$this->checkImageSize($imagePath)) {
683
+                        return false;
684
+                    }
685
+
686
+                    // Check for animated header before generating preview since libgd does not handle them well
687
+                    // Adapted from here: https://stackoverflow.com/a/68491679/4085517 (stripped to only to check for animations + added additional error checking)
688
+                    // Header format details here: https://developers.google.com/speed/webp/docs/riff_container
689
+
690
+                    // Load up the header data, if any
691
+                    $fp = fopen($imagePath, 'rb');
692
+                    if (!$fp) {
693
+                        return false;
694
+                    }
695
+                    $data = fread($fp, 90);
696
+                    if ($data === false) {
697
+                        return false;
698
+                    }
699
+                    fclose($fp);
700
+                    unset($fp);
701
+
702
+                    $headerFormat = 'A4Riff/' // get n string
703
+                        . 'I1Filesize/' // get integer (file size but not actual size)
704
+                        . 'A4Webp/' // get n string
705
+                        . 'A4Vp/' // get n string
706
+                        . 'A74Chunk';
707
+
708
+                    $header = unpack($headerFormat, $data);
709
+                    unset($data, $headerFormat);
710
+                    if ($header === false) {
711
+                        return false;
712
+                    }
713
+
714
+                    // Check if we're really dealing with a valid WEBP header rather than just one suffixed ".webp"
715
+                    if (!isset($header['Riff']) || strtoupper($header['Riff']) !== 'RIFF') {
716
+                        return false;
717
+                    }
718
+                    if (!isset($header['Webp']) || strtoupper($header['Webp']) !== 'WEBP') {
719
+                        return false;
720
+                    }
721
+                    if (!isset($header['Vp']) || strpos(strtoupper($header['Vp']), 'VP8') === false) {
722
+                        return false;
723
+                    }
724
+
725
+                    // Check for animation indicators
726
+                    if (strpos(strtoupper($header['Chunk']), 'ANIM') !== false || strpos(strtoupper($header['Chunk']), 'ANMF') !== false) {
727
+                        // Animated so don't let it reach libgd
728
+                        $this->logger->debug('Image->loadFromFile, animated WEBP images not supported: ' . $imagePath, ['app' => 'core']);
729
+                    } else {
730
+                        // We're safe so give it to libgd
731
+                        $this->resource = @imagecreatefromwebp($imagePath);
732
+                    }
733
+                } else {
734
+                    $this->logger->debug('Image->loadFromFile, WEBP images not supported: ' . $imagePath, ['app' => 'core']);
735
+                }
736
+                break;
737
+                /*
738 738
 				case IMAGETYPE_TIFF_II: // (intel byte order)
739 739
 					break;
740 740
 				case IMAGETYPE_TIFF_MM: // (motorola byte order)
@@ -758,405 +758,405 @@  discard block
 block discarded – undo
758 758
 				case IMAGETYPE_PSD:
759 759
 					break;
760 760
 				*/
761
-			default:
762
-
763
-				// this is mostly file created from encrypted file
764
-				$data = file_get_contents($imagePath);
765
-				if (!$this->checkImageDataSize($data)) {
766
-					return false;
767
-				}
768
-				$this->resource = @imagecreatefromstring($data);
769
-				$iType = IMAGETYPE_PNG;
770
-				$this->logger->debug('Image->loadFromFile, Default', ['app' => 'core']);
771
-				break;
772
-		}
773
-		if ($this->valid()) {
774
-			$this->imageType = $iType;
775
-			$this->mimeType = image_type_to_mime_type($iType);
776
-			$this->filePath = $imagePath;
777
-		}
778
-		return $this->resource;
779
-	}
780
-
781
-	/**
782
-	 * @inheritDoc
783
-	 */
784
-	public function loadFromData(string $str): GdImage|false {
785
-		if (!$this->checkImageDataSize($str)) {
786
-			return false;
787
-		}
788
-		$this->resource = @imagecreatefromstring($str);
789
-		if ($this->fileInfo) {
790
-			$this->mimeType = $this->fileInfo->buffer($str);
791
-		}
792
-		if ($this->valid()) {
793
-			imagealphablending($this->resource, false);
794
-			imagesavealpha($this->resource, true);
795
-		}
796
-
797
-		if (!$this->resource) {
798
-			$this->logger->debug('Image->loadFromFile, could not load', ['app' => 'core']);
799
-			return false;
800
-		}
801
-		return $this->resource;
802
-	}
803
-
804
-	/**
805
-	 * Loads an image from a base64 encoded string.
806
-	 *
807
-	 * @param string $str A string base64 encoded string of image data.
808
-	 * @return bool|\GdImage An image resource or false on error
809
-	 */
810
-	public function loadFromBase64(string $str) {
811
-		$data = base64_decode($str);
812
-		if ($data) { // try to load from string data
813
-			if (!$this->checkImageDataSize($data)) {
814
-				return false;
815
-			}
816
-			$this->resource = @imagecreatefromstring($data);
817
-			if ($this->fileInfo) {
818
-				$this->mimeType = $this->fileInfo->buffer($data);
819
-			}
820
-			if (!$this->resource) {
821
-				$this->logger->debug('Image->loadFromBase64, could not load', ['app' => 'core']);
822
-				return false;
823
-			}
824
-			return $this->resource;
825
-		} else {
826
-			return false;
827
-		}
828
-	}
829
-
830
-	/**
831
-	 * Resizes the image preserving ratio.
832
-	 *
833
-	 * @param int $maxSize The maximum size of either the width or height.
834
-	 * @return bool
835
-	 */
836
-	public function resize(int $maxSize): bool {
837
-		if (!$this->valid()) {
838
-			$this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
839
-			return false;
840
-		}
841
-		$result = $this->resizeNew($maxSize);
842
-		$this->resource = $result;
843
-		return $this->valid();
844
-	}
845
-
846
-	private function resizeNew(int $maxSize): \GdImage|false {
847
-		if (!$this->valid()) {
848
-			$this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
849
-			return false;
850
-		}
851
-		$widthOrig = imagesx($this->resource);
852
-		$heightOrig = imagesy($this->resource);
853
-		$ratioOrig = $widthOrig / $heightOrig;
854
-
855
-		if ($ratioOrig > 1) {
856
-			$newHeight = round($maxSize / $ratioOrig);
857
-			$newWidth = $maxSize;
858
-		} else {
859
-			$newWidth = round($maxSize * $ratioOrig);
860
-			$newHeight = $maxSize;
861
-		}
862
-
863
-		return $this->preciseResizeNew((int)round($newWidth), (int)round($newHeight));
864
-	}
865
-
866
-	/**
867
-	 * @param int $width
868
-	 * @param int $height
869
-	 * @return bool
870
-	 */
871
-	public function preciseResize(int $width, int $height): bool {
872
-		if (!$this->valid()) {
873
-			$this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
874
-			return false;
875
-		}
876
-		$result = $this->preciseResizeNew($width, $height);
877
-		$this->resource = $result;
878
-		return $this->valid();
879
-	}
880
-
881
-	public function preciseResizeNew(int $width, int $height): \GdImage|false {
882
-		if (!($width > 0) || !($height > 0)) {
883
-			$this->logger->info(__METHOD__ . '(): Requested image size not bigger than 0', ['app' => 'core']);
884
-			return false;
885
-		}
886
-		if (!$this->valid()) {
887
-			$this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
888
-			return false;
889
-		}
890
-		$widthOrig = imagesx($this->resource);
891
-		$heightOrig = imagesy($this->resource);
892
-		$process = imagecreatetruecolor($width, $height);
893
-		if ($process === false) {
894
-			$this->logger->debug(__METHOD__ . '(): Error creating true color image', ['app' => 'core']);
895
-			return false;
896
-		}
897
-
898
-		// preserve transparency
899
-		if ($this->imageType === IMAGETYPE_GIF || $this->imageType === IMAGETYPE_PNG) {
900
-			$alpha = imagecolorallocatealpha($process, 0, 0, 0, 127);
901
-			if ($alpha === false) {
902
-				$alpha = null;
903
-			}
904
-			imagecolortransparent($process, $alpha);
905
-			imagealphablending($process, false);
906
-			imagesavealpha($process, true);
907
-		}
908
-
909
-		$res = imagecopyresampled($process, $this->resource, 0, 0, 0, 0, $width, $height, $widthOrig, $heightOrig);
910
-		if ($res === false) {
911
-			$this->logger->debug(__METHOD__ . '(): Error re-sampling process image', ['app' => 'core']);
912
-			imagedestroy($process);
913
-			return false;
914
-		}
915
-		return $process;
916
-	}
917
-
918
-	/**
919
-	 * Crops the image to the middle square. If the image is already square it just returns.
920
-	 *
921
-	 * @param int $size maximum size for the result (optional)
922
-	 * @return bool for success or failure
923
-	 */
924
-	public function centerCrop(int $size = 0): bool {
925
-		if (!$this->valid()) {
926
-			$this->logger->debug('Image->centerCrop, No image loaded', ['app' => 'core']);
927
-			return false;
928
-		}
929
-		$widthOrig = imagesx($this->resource);
930
-		$heightOrig = imagesy($this->resource);
931
-		if ($widthOrig === $heightOrig && $size == 0) {
932
-			return true;
933
-		}
934
-		$ratioOrig = $widthOrig / $heightOrig;
935
-		$width = $height = min($widthOrig, $heightOrig);
936
-
937
-		if ($ratioOrig > 1) {
938
-			$x = (int)(($widthOrig / 2) - ($width / 2));
939
-			$y = 0;
940
-		} else {
941
-			$y = (int)(($heightOrig / 2) - ($height / 2));
942
-			$x = 0;
943
-		}
944
-		if ($size > 0) {
945
-			$targetWidth = $size;
946
-			$targetHeight = $size;
947
-		} else {
948
-			$targetWidth = $width;
949
-			$targetHeight = $height;
950
-		}
951
-		$process = imagecreatetruecolor($targetWidth, $targetHeight);
952
-		if ($process === false) {
953
-			$this->logger->debug('Image->centerCrop, Error creating true color image', ['app' => 'core']);
954
-			return false;
955
-		}
956
-
957
-		// preserve transparency
958
-		if ($this->imageType === IMAGETYPE_GIF || $this->imageType === IMAGETYPE_PNG) {
959
-			$alpha = imagecolorallocatealpha($process, 0, 0, 0, 127);
960
-			if ($alpha === false) {
961
-				$alpha = null;
962
-			}
963
-			imagecolortransparent($process, $alpha);
964
-			imagealphablending($process, false);
965
-			imagesavealpha($process, true);
966
-		}
967
-
968
-		$result = imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $targetWidth, $targetHeight, $width, $height);
969
-		if ($result === false) {
970
-			$this->logger->debug('Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, ['app' => 'core']);
971
-			return false;
972
-		}
973
-		imagedestroy($this->resource);
974
-		$this->resource = $process;
975
-		return true;
976
-	}
977
-
978
-	/**
979
-	 * Crops the image from point $x$y with dimension $wx$h.
980
-	 *
981
-	 * @param int $x Horizontal position
982
-	 * @param int $y Vertical position
983
-	 * @param int $w Width
984
-	 * @param int $h Height
985
-	 * @return bool for success or failure
986
-	 */
987
-	public function crop(int $x, int $y, int $w, int $h): bool {
988
-		if (!$this->valid()) {
989
-			$this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
990
-			return false;
991
-		}
992
-		$result = $this->cropNew($x, $y, $w, $h);
993
-		imagedestroy($this->resource);
994
-		$this->resource = $result;
995
-		return $this->valid();
996
-	}
997
-
998
-	/**
999
-	 * Crops the image from point $x$y with dimension $wx$h.
1000
-	 *
1001
-	 * @param int $x Horizontal position
1002
-	 * @param int $y Vertical position
1003
-	 * @param int $w Width
1004
-	 * @param int $h Height
1005
-	 * @return \GdImage|false
1006
-	 */
1007
-	public function cropNew(int $x, int $y, int $w, int $h) {
1008
-		if (!$this->valid()) {
1009
-			$this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1010
-			return false;
1011
-		}
1012
-		$process = imagecreatetruecolor($w, $h);
1013
-		if ($process === false) {
1014
-			$this->logger->debug(__METHOD__ . '(): Error creating true color image', ['app' => 'core']);
1015
-			return false;
1016
-		}
1017
-
1018
-		// preserve transparency
1019
-		if ($this->imageType === IMAGETYPE_GIF || $this->imageType === IMAGETYPE_PNG) {
1020
-			$alpha = imagecolorallocatealpha($process, 0, 0, 0, 127);
1021
-			if ($alpha === false) {
1022
-				$alpha = null;
1023
-			}
1024
-			imagecolortransparent($process, $alpha);
1025
-			imagealphablending($process, false);
1026
-			imagesavealpha($process, true);
1027
-		}
1028
-
1029
-		$result = imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $w, $h, $w, $h);
1030
-		if ($result === false) {
1031
-			$this->logger->debug(__METHOD__ . '(): Error re-sampling process image ' . $w . 'x' . $h, ['app' => 'core']);
1032
-			return false;
1033
-		}
1034
-		return $process;
1035
-	}
1036
-
1037
-	/**
1038
-	 * Resizes the image to fit within a boundary while preserving ratio.
1039
-	 *
1040
-	 * Warning: Images smaller than $maxWidth x $maxHeight will end up being scaled up
1041
-	 *
1042
-	 * @param int $maxWidth
1043
-	 * @param int $maxHeight
1044
-	 * @return bool
1045
-	 */
1046
-	public function fitIn(int $maxWidth, int $maxHeight): bool {
1047
-		if (!$this->valid()) {
1048
-			$this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1049
-			return false;
1050
-		}
1051
-		$widthOrig = imagesx($this->resource);
1052
-		$heightOrig = imagesy($this->resource);
1053
-		$ratio = $widthOrig / $heightOrig;
1054
-
1055
-		$newWidth = min($maxWidth, $ratio * $maxHeight);
1056
-		$newHeight = min($maxHeight, $maxWidth / $ratio);
1057
-
1058
-		$this->preciseResize((int)round($newWidth), (int)round($newHeight));
1059
-		return true;
1060
-	}
1061
-
1062
-	/**
1063
-	 * Shrinks larger images to fit within specified boundaries while preserving ratio.
1064
-	 *
1065
-	 * @param int $maxWidth
1066
-	 * @param int $maxHeight
1067
-	 * @return bool
1068
-	 */
1069
-	public function scaleDownToFit(int $maxWidth, int $maxHeight): bool {
1070
-		if (!$this->valid()) {
1071
-			$this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1072
-			return false;
1073
-		}
1074
-		$widthOrig = imagesx($this->resource);
1075
-		$heightOrig = imagesy($this->resource);
1076
-
1077
-		if ($widthOrig > $maxWidth || $heightOrig > $maxHeight) {
1078
-			return $this->fitIn($maxWidth, $maxHeight);
1079
-		}
1080
-
1081
-		return false;
1082
-	}
1083
-
1084
-	public function copy(): IImage {
1085
-		$image = new self($this->logger, $this->appConfig, $this->config);
1086
-		if (!$this->valid()) {
1087
-			/* image is invalid, return an empty one */
1088
-			return $image;
1089
-		}
1090
-		$image->resource = imagecreatetruecolor($this->width(), $this->height());
1091
-		if (!$image->valid()) {
1092
-			/* image creation failed, cannot copy in it */
1093
-			return $image;
1094
-		}
1095
-		imagecopy(
1096
-			$image->resource,
1097
-			$this->resource,
1098
-			0,
1099
-			0,
1100
-			0,
1101
-			0,
1102
-			$this->width(),
1103
-			$this->height()
1104
-		);
1105
-
1106
-		return $image;
1107
-	}
1108
-
1109
-	public function cropCopy(int $x, int $y, int $w, int $h): IImage {
1110
-		$image = new self($this->logger, $this->appConfig, $this->config);
1111
-		$image->imageType = $this->imageType;
1112
-		$image->mimeType = $this->mimeType;
1113
-		$image->resource = $this->cropNew($x, $y, $w, $h);
1114
-
1115
-		return $image;
1116
-	}
1117
-
1118
-	public function preciseResizeCopy(int $width, int $height): IImage {
1119
-		$image = new self($this->logger, $this->appConfig, $this->config);
1120
-		$image->imageType = $this->imageType;
1121
-		$image->mimeType = $this->mimeType;
1122
-		$image->resource = $this->preciseResizeNew($width, $height);
1123
-
1124
-		return $image;
1125
-	}
1126
-
1127
-	public function resizeCopy(int $maxSize): IImage {
1128
-		$image = new self($this->logger, $this->appConfig, $this->config);
1129
-		$image->imageType = $this->imageType;
1130
-		$image->mimeType = $this->mimeType;
1131
-		$image->resource = $this->resizeNew($maxSize);
1132
-
1133
-		return $image;
1134
-	}
1135
-
1136
-	/**
1137
-	 * Destroys the current image and resets the object
1138
-	 */
1139
-	public function destroy(): void {
1140
-		$this->resource = false;
1141
-	}
1142
-
1143
-	public function __destruct() {
1144
-		$this->destroy();
1145
-	}
761
+            default:
762
+
763
+                // this is mostly file created from encrypted file
764
+                $data = file_get_contents($imagePath);
765
+                if (!$this->checkImageDataSize($data)) {
766
+                    return false;
767
+                }
768
+                $this->resource = @imagecreatefromstring($data);
769
+                $iType = IMAGETYPE_PNG;
770
+                $this->logger->debug('Image->loadFromFile, Default', ['app' => 'core']);
771
+                break;
772
+        }
773
+        if ($this->valid()) {
774
+            $this->imageType = $iType;
775
+            $this->mimeType = image_type_to_mime_type($iType);
776
+            $this->filePath = $imagePath;
777
+        }
778
+        return $this->resource;
779
+    }
780
+
781
+    /**
782
+     * @inheritDoc
783
+     */
784
+    public function loadFromData(string $str): GdImage|false {
785
+        if (!$this->checkImageDataSize($str)) {
786
+            return false;
787
+        }
788
+        $this->resource = @imagecreatefromstring($str);
789
+        if ($this->fileInfo) {
790
+            $this->mimeType = $this->fileInfo->buffer($str);
791
+        }
792
+        if ($this->valid()) {
793
+            imagealphablending($this->resource, false);
794
+            imagesavealpha($this->resource, true);
795
+        }
796
+
797
+        if (!$this->resource) {
798
+            $this->logger->debug('Image->loadFromFile, could not load', ['app' => 'core']);
799
+            return false;
800
+        }
801
+        return $this->resource;
802
+    }
803
+
804
+    /**
805
+     * Loads an image from a base64 encoded string.
806
+     *
807
+     * @param string $str A string base64 encoded string of image data.
808
+     * @return bool|\GdImage An image resource or false on error
809
+     */
810
+    public function loadFromBase64(string $str) {
811
+        $data = base64_decode($str);
812
+        if ($data) { // try to load from string data
813
+            if (!$this->checkImageDataSize($data)) {
814
+                return false;
815
+            }
816
+            $this->resource = @imagecreatefromstring($data);
817
+            if ($this->fileInfo) {
818
+                $this->mimeType = $this->fileInfo->buffer($data);
819
+            }
820
+            if (!$this->resource) {
821
+                $this->logger->debug('Image->loadFromBase64, could not load', ['app' => 'core']);
822
+                return false;
823
+            }
824
+            return $this->resource;
825
+        } else {
826
+            return false;
827
+        }
828
+    }
829
+
830
+    /**
831
+     * Resizes the image preserving ratio.
832
+     *
833
+     * @param int $maxSize The maximum size of either the width or height.
834
+     * @return bool
835
+     */
836
+    public function resize(int $maxSize): bool {
837
+        if (!$this->valid()) {
838
+            $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
839
+            return false;
840
+        }
841
+        $result = $this->resizeNew($maxSize);
842
+        $this->resource = $result;
843
+        return $this->valid();
844
+    }
845
+
846
+    private function resizeNew(int $maxSize): \GdImage|false {
847
+        if (!$this->valid()) {
848
+            $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
849
+            return false;
850
+        }
851
+        $widthOrig = imagesx($this->resource);
852
+        $heightOrig = imagesy($this->resource);
853
+        $ratioOrig = $widthOrig / $heightOrig;
854
+
855
+        if ($ratioOrig > 1) {
856
+            $newHeight = round($maxSize / $ratioOrig);
857
+            $newWidth = $maxSize;
858
+        } else {
859
+            $newWidth = round($maxSize * $ratioOrig);
860
+            $newHeight = $maxSize;
861
+        }
862
+
863
+        return $this->preciseResizeNew((int)round($newWidth), (int)round($newHeight));
864
+    }
865
+
866
+    /**
867
+     * @param int $width
868
+     * @param int $height
869
+     * @return bool
870
+     */
871
+    public function preciseResize(int $width, int $height): bool {
872
+        if (!$this->valid()) {
873
+            $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
874
+            return false;
875
+        }
876
+        $result = $this->preciseResizeNew($width, $height);
877
+        $this->resource = $result;
878
+        return $this->valid();
879
+    }
880
+
881
+    public function preciseResizeNew(int $width, int $height): \GdImage|false {
882
+        if (!($width > 0) || !($height > 0)) {
883
+            $this->logger->info(__METHOD__ . '(): Requested image size not bigger than 0', ['app' => 'core']);
884
+            return false;
885
+        }
886
+        if (!$this->valid()) {
887
+            $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
888
+            return false;
889
+        }
890
+        $widthOrig = imagesx($this->resource);
891
+        $heightOrig = imagesy($this->resource);
892
+        $process = imagecreatetruecolor($width, $height);
893
+        if ($process === false) {
894
+            $this->logger->debug(__METHOD__ . '(): Error creating true color image', ['app' => 'core']);
895
+            return false;
896
+        }
897
+
898
+        // preserve transparency
899
+        if ($this->imageType === IMAGETYPE_GIF || $this->imageType === IMAGETYPE_PNG) {
900
+            $alpha = imagecolorallocatealpha($process, 0, 0, 0, 127);
901
+            if ($alpha === false) {
902
+                $alpha = null;
903
+            }
904
+            imagecolortransparent($process, $alpha);
905
+            imagealphablending($process, false);
906
+            imagesavealpha($process, true);
907
+        }
908
+
909
+        $res = imagecopyresampled($process, $this->resource, 0, 0, 0, 0, $width, $height, $widthOrig, $heightOrig);
910
+        if ($res === false) {
911
+            $this->logger->debug(__METHOD__ . '(): Error re-sampling process image', ['app' => 'core']);
912
+            imagedestroy($process);
913
+            return false;
914
+        }
915
+        return $process;
916
+    }
917
+
918
+    /**
919
+     * Crops the image to the middle square. If the image is already square it just returns.
920
+     *
921
+     * @param int $size maximum size for the result (optional)
922
+     * @return bool for success or failure
923
+     */
924
+    public function centerCrop(int $size = 0): bool {
925
+        if (!$this->valid()) {
926
+            $this->logger->debug('Image->centerCrop, No image loaded', ['app' => 'core']);
927
+            return false;
928
+        }
929
+        $widthOrig = imagesx($this->resource);
930
+        $heightOrig = imagesy($this->resource);
931
+        if ($widthOrig === $heightOrig && $size == 0) {
932
+            return true;
933
+        }
934
+        $ratioOrig = $widthOrig / $heightOrig;
935
+        $width = $height = min($widthOrig, $heightOrig);
936
+
937
+        if ($ratioOrig > 1) {
938
+            $x = (int)(($widthOrig / 2) - ($width / 2));
939
+            $y = 0;
940
+        } else {
941
+            $y = (int)(($heightOrig / 2) - ($height / 2));
942
+            $x = 0;
943
+        }
944
+        if ($size > 0) {
945
+            $targetWidth = $size;
946
+            $targetHeight = $size;
947
+        } else {
948
+            $targetWidth = $width;
949
+            $targetHeight = $height;
950
+        }
951
+        $process = imagecreatetruecolor($targetWidth, $targetHeight);
952
+        if ($process === false) {
953
+            $this->logger->debug('Image->centerCrop, Error creating true color image', ['app' => 'core']);
954
+            return false;
955
+        }
956
+
957
+        // preserve transparency
958
+        if ($this->imageType === IMAGETYPE_GIF || $this->imageType === IMAGETYPE_PNG) {
959
+            $alpha = imagecolorallocatealpha($process, 0, 0, 0, 127);
960
+            if ($alpha === false) {
961
+                $alpha = null;
962
+            }
963
+            imagecolortransparent($process, $alpha);
964
+            imagealphablending($process, false);
965
+            imagesavealpha($process, true);
966
+        }
967
+
968
+        $result = imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $targetWidth, $targetHeight, $width, $height);
969
+        if ($result === false) {
970
+            $this->logger->debug('Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, ['app' => 'core']);
971
+            return false;
972
+        }
973
+        imagedestroy($this->resource);
974
+        $this->resource = $process;
975
+        return true;
976
+    }
977
+
978
+    /**
979
+     * Crops the image from point $x$y with dimension $wx$h.
980
+     *
981
+     * @param int $x Horizontal position
982
+     * @param int $y Vertical position
983
+     * @param int $w Width
984
+     * @param int $h Height
985
+     * @return bool for success or failure
986
+     */
987
+    public function crop(int $x, int $y, int $w, int $h): bool {
988
+        if (!$this->valid()) {
989
+            $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
990
+            return false;
991
+        }
992
+        $result = $this->cropNew($x, $y, $w, $h);
993
+        imagedestroy($this->resource);
994
+        $this->resource = $result;
995
+        return $this->valid();
996
+    }
997
+
998
+    /**
999
+     * Crops the image from point $x$y with dimension $wx$h.
1000
+     *
1001
+     * @param int $x Horizontal position
1002
+     * @param int $y Vertical position
1003
+     * @param int $w Width
1004
+     * @param int $h Height
1005
+     * @return \GdImage|false
1006
+     */
1007
+    public function cropNew(int $x, int $y, int $w, int $h) {
1008
+        if (!$this->valid()) {
1009
+            $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1010
+            return false;
1011
+        }
1012
+        $process = imagecreatetruecolor($w, $h);
1013
+        if ($process === false) {
1014
+            $this->logger->debug(__METHOD__ . '(): Error creating true color image', ['app' => 'core']);
1015
+            return false;
1016
+        }
1017
+
1018
+        // preserve transparency
1019
+        if ($this->imageType === IMAGETYPE_GIF || $this->imageType === IMAGETYPE_PNG) {
1020
+            $alpha = imagecolorallocatealpha($process, 0, 0, 0, 127);
1021
+            if ($alpha === false) {
1022
+                $alpha = null;
1023
+            }
1024
+            imagecolortransparent($process, $alpha);
1025
+            imagealphablending($process, false);
1026
+            imagesavealpha($process, true);
1027
+        }
1028
+
1029
+        $result = imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $w, $h, $w, $h);
1030
+        if ($result === false) {
1031
+            $this->logger->debug(__METHOD__ . '(): Error re-sampling process image ' . $w . 'x' . $h, ['app' => 'core']);
1032
+            return false;
1033
+        }
1034
+        return $process;
1035
+    }
1036
+
1037
+    /**
1038
+     * Resizes the image to fit within a boundary while preserving ratio.
1039
+     *
1040
+     * Warning: Images smaller than $maxWidth x $maxHeight will end up being scaled up
1041
+     *
1042
+     * @param int $maxWidth
1043
+     * @param int $maxHeight
1044
+     * @return bool
1045
+     */
1046
+    public function fitIn(int $maxWidth, int $maxHeight): bool {
1047
+        if (!$this->valid()) {
1048
+            $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1049
+            return false;
1050
+        }
1051
+        $widthOrig = imagesx($this->resource);
1052
+        $heightOrig = imagesy($this->resource);
1053
+        $ratio = $widthOrig / $heightOrig;
1054
+
1055
+        $newWidth = min($maxWidth, $ratio * $maxHeight);
1056
+        $newHeight = min($maxHeight, $maxWidth / $ratio);
1057
+
1058
+        $this->preciseResize((int)round($newWidth), (int)round($newHeight));
1059
+        return true;
1060
+    }
1061
+
1062
+    /**
1063
+     * Shrinks larger images to fit within specified boundaries while preserving ratio.
1064
+     *
1065
+     * @param int $maxWidth
1066
+     * @param int $maxHeight
1067
+     * @return bool
1068
+     */
1069
+    public function scaleDownToFit(int $maxWidth, int $maxHeight): bool {
1070
+        if (!$this->valid()) {
1071
+            $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1072
+            return false;
1073
+        }
1074
+        $widthOrig = imagesx($this->resource);
1075
+        $heightOrig = imagesy($this->resource);
1076
+
1077
+        if ($widthOrig > $maxWidth || $heightOrig > $maxHeight) {
1078
+            return $this->fitIn($maxWidth, $maxHeight);
1079
+        }
1080
+
1081
+        return false;
1082
+    }
1083
+
1084
+    public function copy(): IImage {
1085
+        $image = new self($this->logger, $this->appConfig, $this->config);
1086
+        if (!$this->valid()) {
1087
+            /* image is invalid, return an empty one */
1088
+            return $image;
1089
+        }
1090
+        $image->resource = imagecreatetruecolor($this->width(), $this->height());
1091
+        if (!$image->valid()) {
1092
+            /* image creation failed, cannot copy in it */
1093
+            return $image;
1094
+        }
1095
+        imagecopy(
1096
+            $image->resource,
1097
+            $this->resource,
1098
+            0,
1099
+            0,
1100
+            0,
1101
+            0,
1102
+            $this->width(),
1103
+            $this->height()
1104
+        );
1105
+
1106
+        return $image;
1107
+    }
1108
+
1109
+    public function cropCopy(int $x, int $y, int $w, int $h): IImage {
1110
+        $image = new self($this->logger, $this->appConfig, $this->config);
1111
+        $image->imageType = $this->imageType;
1112
+        $image->mimeType = $this->mimeType;
1113
+        $image->resource = $this->cropNew($x, $y, $w, $h);
1114
+
1115
+        return $image;
1116
+    }
1117
+
1118
+    public function preciseResizeCopy(int $width, int $height): IImage {
1119
+        $image = new self($this->logger, $this->appConfig, $this->config);
1120
+        $image->imageType = $this->imageType;
1121
+        $image->mimeType = $this->mimeType;
1122
+        $image->resource = $this->preciseResizeNew($width, $height);
1123
+
1124
+        return $image;
1125
+    }
1126
+
1127
+    public function resizeCopy(int $maxSize): IImage {
1128
+        $image = new self($this->logger, $this->appConfig, $this->config);
1129
+        $image->imageType = $this->imageType;
1130
+        $image->mimeType = $this->mimeType;
1131
+        $image->resource = $this->resizeNew($maxSize);
1132
+
1133
+        return $image;
1134
+    }
1135
+
1136
+    /**
1137
+     * Destroys the current image and resets the object
1138
+     */
1139
+    public function destroy(): void {
1140
+        $this->resource = false;
1141
+    }
1142
+
1143
+    public function __destruct() {
1144
+        $this->destroy();
1145
+    }
1146 1146
 }
1147 1147
 
1148 1148
 if (!function_exists('exif_imagetype')) {
1149
-	/**
1150
-	 * Workaround if exif_imagetype does not exist
1151
-	 *
1152
-	 * @link https://www.php.net/manual/en/function.exif-imagetype.php#80383
1153
-	 * @param string $fileName
1154
-	 * @return int|false
1155
-	 */
1156
-	function exif_imagetype(string $fileName) {
1157
-		if (($info = getimagesize($fileName)) !== false) {
1158
-			return $info[2];
1159
-		}
1160
-		return false;
1161
-	}
1149
+    /**
1150
+     * Workaround if exif_imagetype does not exist
1151
+     *
1152
+     * @link https://www.php.net/manual/en/function.exif-imagetype.php#80383
1153
+     * @param string $fileName
1154
+     * @return int|false
1155
+     */
1156
+    function exif_imagetype(string $fileName) {
1157
+        if (($info = getimagesize($fileName)) !== false) {
1158
+            return $info[2];
1159
+        }
1160
+        return false;
1161
+    }
1162 1162
 }
Please login to merge, or discard this patch.
tests/lib/Files/Mount/RootMountProviderTest.php 1 patch
Indentation   +92 added lines, -92 removed lines patch added patch discarded remove patch
@@ -20,105 +20,105 @@
 block discarded – undo
20 20
 
21 21
 #[\PHPUnit\Framework\Attributes\Group('DB')]
22 22
 class RootMountProviderTest extends TestCase {
23
-	private StorageFactory $loader;
23
+    private StorageFactory $loader;
24 24
 
25
-	protected function setUp(): void {
26
-		parent::setUp();
25
+    protected function setUp(): void {
26
+        parent::setUp();
27 27
 
28
-		$this->loader = new StorageFactory();
29
-	}
28
+        $this->loader = new StorageFactory();
29
+    }
30 30
 
31
-	private function getConfig(array $systemConfig): IConfig {
32
-		$config = $this->createMock(IConfig::class);
33
-		$config->method('getSystemValue')
34
-			->willReturnCallback(function (string $key, $default) use ($systemConfig) {
35
-				return $systemConfig[$key] ?? $default;
36
-			});
37
-		return $config;
38
-	}
31
+    private function getConfig(array $systemConfig): IConfig {
32
+        $config = $this->createMock(IConfig::class);
33
+        $config->method('getSystemValue')
34
+            ->willReturnCallback(function (string $key, $default) use ($systemConfig) {
35
+                return $systemConfig[$key] ?? $default;
36
+            });
37
+        return $config;
38
+    }
39 39
 
40
-	private function getProvider(array $systemConfig): RootMountProvider {
41
-		$config = $this->getConfig($systemConfig);
42
-		$objectStoreConfig = new PrimaryObjectStoreConfig($config, $this->createMock(IAppManager::class));
43
-		return new RootMountProvider($objectStoreConfig, $config);
44
-	}
40
+    private function getProvider(array $systemConfig): RootMountProvider {
41
+        $config = $this->getConfig($systemConfig);
42
+        $objectStoreConfig = new PrimaryObjectStoreConfig($config, $this->createMock(IAppManager::class));
43
+        return new RootMountProvider($objectStoreConfig, $config);
44
+    }
45 45
 
46
-	public function testLocal(): void {
47
-		$provider = $this->getProvider([
48
-			'datadirectory' => '/data',
49
-		]);
50
-		$mounts = $provider->getRootMounts($this->loader);
51
-		$this->assertCount(1, $mounts);
52
-		$mount = $mounts[0];
53
-		$this->assertEquals('/', $mount->getMountPoint());
54
-		/** @var LocalRootStorage $storage */
55
-		$storage = $mount->getStorage();
56
-		$this->assertInstanceOf(LocalRootStorage::class, $storage);
57
-		$this->assertEquals('/data/', $storage->getSourcePath(''));
58
-	}
46
+    public function testLocal(): void {
47
+        $provider = $this->getProvider([
48
+            'datadirectory' => '/data',
49
+        ]);
50
+        $mounts = $provider->getRootMounts($this->loader);
51
+        $this->assertCount(1, $mounts);
52
+        $mount = $mounts[0];
53
+        $this->assertEquals('/', $mount->getMountPoint());
54
+        /** @var LocalRootStorage $storage */
55
+        $storage = $mount->getStorage();
56
+        $this->assertInstanceOf(LocalRootStorage::class, $storage);
57
+        $this->assertEquals('/data/', $storage->getSourcePath(''));
58
+    }
59 59
 
60
-	public function testObjectStore(): void {
61
-		$provider = $this->getProvider([
62
-			'objectstore' => [
63
-				'class' => "OC\Files\ObjectStore\S3",
64
-				'arguments' => [
65
-					'bucket' => 'nextcloud',
66
-					'autocreate' => true,
67
-					'key' => 'minio',
68
-					'secret' => 'minio123',
69
-					'hostname' => 'localhost',
70
-					'port' => 9000,
71
-					'use_ssl' => false,
72
-					'use_path_style' => true,
73
-					'uploadPartSize' => 52428800,
74
-				],
75
-			],
76
-		]);
77
-		$mounts = $provider->getRootMounts($this->loader);
78
-		$this->assertCount(1, $mounts);
79
-		$mount = $mounts[0];
80
-		$this->assertEquals('/', $mount->getMountPoint());
81
-		/** @var ObjectStoreStorage $storage */
82
-		$storage = $mount->getStorage();
83
-		$this->assertInstanceOf(ObjectStoreStorage::class, $storage);
60
+    public function testObjectStore(): void {
61
+        $provider = $this->getProvider([
62
+            'objectstore' => [
63
+                'class' => "OC\Files\ObjectStore\S3",
64
+                'arguments' => [
65
+                    'bucket' => 'nextcloud',
66
+                    'autocreate' => true,
67
+                    'key' => 'minio',
68
+                    'secret' => 'minio123',
69
+                    'hostname' => 'localhost',
70
+                    'port' => 9000,
71
+                    'use_ssl' => false,
72
+                    'use_path_style' => true,
73
+                    'uploadPartSize' => 52428800,
74
+                ],
75
+            ],
76
+        ]);
77
+        $mounts = $provider->getRootMounts($this->loader);
78
+        $this->assertCount(1, $mounts);
79
+        $mount = $mounts[0];
80
+        $this->assertEquals('/', $mount->getMountPoint());
81
+        /** @var ObjectStoreStorage $storage */
82
+        $storage = $mount->getStorage();
83
+        $this->assertInstanceOf(ObjectStoreStorage::class, $storage);
84 84
 
85
-		$class = new \ReflectionClass($storage);
86
-		$prop = $class->getProperty('objectStore');
87
-		$prop->setAccessible(true);
88
-		/** @var S3 $objectStore */
89
-		$objectStore = $prop->getValue($storage);
90
-		$this->assertEquals('nextcloud', $objectStore->getBucket());
91
-	}
85
+        $class = new \ReflectionClass($storage);
86
+        $prop = $class->getProperty('objectStore');
87
+        $prop->setAccessible(true);
88
+        /** @var S3 $objectStore */
89
+        $objectStore = $prop->getValue($storage);
90
+        $this->assertEquals('nextcloud', $objectStore->getBucket());
91
+    }
92 92
 
93
-	public function testObjectStoreMultiBucket(): void {
94
-		$provider = $this->getProvider([
95
-			'objectstore_multibucket' => [
96
-				'class' => "OC\Files\ObjectStore\S3",
97
-				'arguments' => [
98
-					'bucket' => 'nextcloud',
99
-					'autocreate' => true,
100
-					'key' => 'minio',
101
-					'secret' => 'minio123',
102
-					'hostname' => 'localhost',
103
-					'port' => 9000,
104
-					'use_ssl' => false,
105
-					'use_path_style' => true,
106
-					'uploadPartSize' => 52428800,
107
-				],
108
-			],
109
-		]);
110
-		$mounts = $provider->getRootMounts($this->loader);
111
-		$this->assertCount(1, $mounts);
112
-		$mount = $mounts[0];
113
-		$this->assertEquals('/', $mount->getMountPoint());
114
-		/** @var ObjectStoreStorage $storage */
115
-		$storage = $mount->getStorage();
116
-		$this->assertInstanceOf(ObjectStoreStorage::class, $storage);
93
+    public function testObjectStoreMultiBucket(): void {
94
+        $provider = $this->getProvider([
95
+            'objectstore_multibucket' => [
96
+                'class' => "OC\Files\ObjectStore\S3",
97
+                'arguments' => [
98
+                    'bucket' => 'nextcloud',
99
+                    'autocreate' => true,
100
+                    'key' => 'minio',
101
+                    'secret' => 'minio123',
102
+                    'hostname' => 'localhost',
103
+                    'port' => 9000,
104
+                    'use_ssl' => false,
105
+                    'use_path_style' => true,
106
+                    'uploadPartSize' => 52428800,
107
+                ],
108
+            ],
109
+        ]);
110
+        $mounts = $provider->getRootMounts($this->loader);
111
+        $this->assertCount(1, $mounts);
112
+        $mount = $mounts[0];
113
+        $this->assertEquals('/', $mount->getMountPoint());
114
+        /** @var ObjectStoreStorage $storage */
115
+        $storage = $mount->getStorage();
116
+        $this->assertInstanceOf(ObjectStoreStorage::class, $storage);
117 117
 
118
-		$class = new \ReflectionClass($storage);
119
-		$prop = $class->getProperty('objectStore');
120
-		/** @var S3 $objectStore */
121
-		$objectStore = $prop->getValue($storage);
122
-		$this->assertEquals('nextcloud0', $objectStore->getBucket());
123
-	}
118
+        $class = new \ReflectionClass($storage);
119
+        $prop = $class->getProperty('objectStore');
120
+        /** @var S3 $objectStore */
121
+        $objectStore = $prop->getValue($storage);
122
+        $this->assertEquals('nextcloud0', $objectStore->getBucket());
123
+    }
124 124
 }
Please login to merge, or discard this patch.
tests/lib/Files/ViewTest.php 1 patch
Indentation   +2774 added lines, -2774 removed lines patch added patch discarded remove patch
@@ -51,40 +51,40 @@  discard block
 block discarded – undo
51 51
 use Test\Traits\UserTrait;
52 52
 
53 53
 class TemporaryNoTouch extends Temporary {
54
-	public function touch(string $path, ?int $mtime = null): bool {
55
-		return false;
56
-	}
54
+    public function touch(string $path, ?int $mtime = null): bool {
55
+        return false;
56
+    }
57 57
 }
58 58
 
59 59
 class TemporaryNoCross extends Temporary {
60
-	public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool {
61
-		return Common::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime);
62
-	}
60
+    public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool {
61
+        return Common::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime);
62
+    }
63 63
 
64
-	public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
65
-		return Common::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
66
-	}
64
+    public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
65
+        return Common::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
66
+    }
67 67
 }
68 68
 
69 69
 class TemporaryNoLocal extends Temporary {
70
-	public function instanceOfStorage(string $class): bool {
71
-		if ($class === '\OC\Files\Storage\Local') {
72
-			return false;
73
-		} else {
74
-			return parent::instanceOfStorage($class);
75
-		}
76
-	}
70
+    public function instanceOfStorage(string $class): bool {
71
+        if ($class === '\OC\Files\Storage\Local') {
72
+            return false;
73
+        } else {
74
+            return parent::instanceOfStorage($class);
75
+        }
76
+    }
77 77
 }
78 78
 
79 79
 class TestEventHandler {
80
-	public function umount() {
81
-	}
82
-	public function post_umount() {
83
-	}
84
-	public function preCallback() {
85
-	}
86
-	public function postCallback() {
87
-	}
80
+    public function umount() {
81
+    }
82
+    public function post_umount() {
83
+    }
84
+    public function preCallback() {
85
+    }
86
+    public function postCallback() {
87
+    }
88 88
 }
89 89
 
90 90
 /**
@@ -96,2756 +96,2756 @@  discard block
 block discarded – undo
96 96
 #[\PHPUnit\Framework\Attributes\Medium]
97 97
 #[\PHPUnit\Framework\Attributes\Group('DB')]
98 98
 class ViewTest extends \Test\TestCase {
99
-	use UserTrait;
100
-
101
-	/**
102
-	 * @var Storage[] $storages
103
-	 */
104
-	private $storages = [];
105
-
106
-	/**
107
-	 * @var string
108
-	 */
109
-	private $user;
110
-
111
-	/**
112
-	 * @var IUser
113
-	 */
114
-	private $userObject;
115
-
116
-	/**
117
-	 * @var IGroup
118
-	 */
119
-	private $groupObject;
120
-
121
-	/** @var Storage */
122
-	private $tempStorage;
123
-
124
-	protected function setUp(): void {
125
-		parent::setUp();
126
-		\OC_Hook::clear();
127
-
128
-		Server::get(IUserManager::class)->clearBackends();
129
-		Server::get(IUserManager::class)->registerBackend(new \Test\Util\User\Dummy());
130
-
131
-		//login
132
-		$userManager = Server::get(IUserManager::class);
133
-		$groupManager = Server::get(IGroupManager::class);
134
-		$this->user = 'test';
135
-		$this->userObject = $userManager->createUser('test', 'test');
136
-
137
-		$this->groupObject = $groupManager->createGroup('group1');
138
-		$this->groupObject->addUser($this->userObject);
139
-
140
-		self::loginAsUser($this->user);
141
-
142
-		/** @var IMountManager $manager */
143
-		$manager = Server::get(IMountManager::class);
144
-		$manager->removeMount('/test');
145
-
146
-		$this->tempStorage = null;
147
-	}
148
-
149
-	protected function tearDown(): void {
150
-		\OC_User::setUserId($this->user);
151
-		foreach ($this->storages as $storage) {
152
-			$cache = $storage->getCache();
153
-			$ids = $cache->getAll();
154
-			$cache->clear();
155
-		}
156
-
157
-		if ($this->tempStorage) {
158
-			system('rm -rf ' . escapeshellarg($this->tempStorage->getDataDir()));
159
-		}
160
-
161
-		self::logout();
162
-
163
-		/** @var SetupManager $setupManager */
164
-		$setupManager = Server::get(SetupManager::class);
165
-		$setupManager->setupRoot();
166
-
167
-		$this->userObject->delete();
168
-		$this->groupObject->delete();
169
-
170
-		$mountProviderCollection = Server::get(IMountProviderCollection::class);
171
-		self::invokePrivate($mountProviderCollection, 'providers', [[]]);
172
-
173
-		parent::tearDown();
174
-	}
175
-
176
-	public function testCacheAPI(): void {
177
-		$storage1 = $this->getTestStorage();
178
-		$storage2 = $this->getTestStorage();
179
-		$storage3 = $this->getTestStorage();
180
-		$root = self::getUniqueID('/');
181
-		Filesystem::mount($storage1, [], $root . '/');
182
-		Filesystem::mount($storage2, [], $root . '/substorage');
183
-		Filesystem::mount($storage3, [], $root . '/folder/anotherstorage');
184
-		$textSize = strlen("dummy file data\n");
185
-		$imageSize = filesize(\OC::$SERVERROOT . '/core/img/logo/logo.png');
186
-		$storageSize = $textSize * 2 + $imageSize;
187
-
188
-		$storageInfo = $storage3->getCache()->get('');
189
-		$this->assertEquals($storageSize, $storageInfo['size']);
190
-
191
-		$rootView = new View($root);
192
-
193
-		$cachedData = $rootView->getFileInfo('/foo.txt');
194
-		$this->assertEquals($textSize, $cachedData['size']);
195
-		$this->assertEquals('text/plain', $cachedData['mimetype']);
196
-		$this->assertNotEquals(-1, $cachedData['permissions']);
197
-
198
-		$cachedData = $rootView->getFileInfo('/');
199
-		$this->assertEquals($storageSize * 3, $cachedData['size']);
200
-		$this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
201
-
202
-		// get cached data excluding mount points
203
-		$cachedData = $rootView->getFileInfo('/', false);
204
-		$this->assertEquals($storageSize, $cachedData['size']);
205
-		$this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
206
-
207
-		$cachedData = $rootView->getFileInfo('/folder');
208
-		$this->assertEquals($storageSize + $textSize, $cachedData['size']);
209
-		$this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
210
-
211
-		$folderData = $rootView->getDirectoryContent('/');
212
-		/**
213
-		 * expected entries:
214
-		 * folder
215
-		 * foo.png
216
-		 * foo.txt
217
-		 * substorage
218
-		 */
219
-		$this->assertCount(4, $folderData);
220
-		$this->assertEquals('folder', $folderData[0]['name']);
221
-		$this->assertEquals('foo.png', $folderData[1]['name']);
222
-		$this->assertEquals('foo.txt', $folderData[2]['name']);
223
-		$this->assertEquals('substorage', $folderData[3]['name']);
224
-
225
-		$this->assertEquals($storageSize + $textSize, $folderData[0]['size']);
226
-		$this->assertEquals($imageSize, $folderData[1]['size']);
227
-		$this->assertEquals($textSize, $folderData[2]['size']);
228
-		$this->assertEquals($storageSize, $folderData[3]['size']);
229
-
230
-		$folderData = $rootView->getDirectoryContent('/substorage');
231
-		/**
232
-		 * expected entries:
233
-		 * folder
234
-		 * foo.png
235
-		 * foo.txt
236
-		 */
237
-		$this->assertCount(3, $folderData);
238
-		$this->assertEquals('folder', $folderData[0]['name']);
239
-		$this->assertEquals('foo.png', $folderData[1]['name']);
240
-		$this->assertEquals('foo.txt', $folderData[2]['name']);
241
-
242
-		$folderView = new View($root . '/folder');
243
-		$this->assertEquals($rootView->getFileInfo('/folder'), $folderView->getFileInfo('/'));
244
-
245
-		$cachedData = $rootView->getFileInfo('/foo.txt');
246
-		$this->assertFalse($cachedData['encrypted']);
247
-		$id = $rootView->putFileInfo('/foo.txt', ['encrypted' => true]);
248
-		$cachedData = $rootView->getFileInfo('/foo.txt');
249
-		$this->assertTrue($cachedData['encrypted']);
250
-		$this->assertEquals($cachedData['fileid'], $id);
251
-
252
-		$this->assertFalse($rootView->getFileInfo('/non/existing'));
253
-		$this->assertEquals([], $rootView->getDirectoryContent('/non/existing'));
254
-	}
255
-
256
-	public function testGetPath(): void {
257
-		$user = $this->createMock(IUser::class);
258
-		$user->method('getUID')
259
-			->willReturn('test');
260
-		$storage1 = $this->getTestStorage();
261
-		$storage2 = $this->getTestStorage();
262
-		$storage3 = $this->getTestStorage();
263
-
264
-		Filesystem::mount($storage1, [], '/test/files');
265
-		Filesystem::mount($storage2, [], '/test/files/substorage');
266
-		Filesystem::mount($storage3, [], '/test/files/folder/anotherstorage');
267
-
268
-		$userMountCache = Server::get(IUserMountCache::class);
269
-		$userMountCache->registerMounts($user, [
270
-			new MountPoint($storage1, '/test/files'),
271
-			new MountPoint($storage2, '/test/files/substorage'),
272
-			new MountPoint($storage3, '/test/files/folder/anotherstorage'),
273
-		]);
274
-
275
-		$rootView = new View('/test/files');
276
-
277
-
278
-		$cachedData = $rootView->getFileInfo('/foo.txt');
279
-		$id1 = $cachedData->getId();
280
-		$this->assertEquals('/foo.txt', $rootView->getPath($id1));
281
-
282
-		$cachedData = $rootView->getFileInfo('/substorage/foo.txt');
283
-		$id2 = $cachedData->getId();
284
-		$this->assertEquals('/substorage/foo.txt', $rootView->getPath($id2));
285
-
286
-		$folderView = new View('/test/files/substorage');
287
-		$this->assertEquals('/foo.txt', $folderView->getPath($id2));
288
-	}
289
-
290
-
291
-	public function testGetPathNotExisting(): void {
292
-		$this->expectException(NotFoundException::class);
293
-
294
-		$storage1 = $this->getTestStorage();
295
-		Filesystem::mount($storage1, [], '/');
296
-
297
-		$rootView = new View('');
298
-		$cachedData = $rootView->getFileInfo('/foo.txt');
299
-		/** @var int $id1 */
300
-		$id1 = $cachedData['fileid'];
301
-		$folderView = new View('/substorage');
302
-		$this->assertNull($folderView->getPath($id1));
303
-	}
304
-
305
-	public function testMountPointOverwrite(): void {
306
-		$storage1 = $this->getTestStorage(false);
307
-		$storage2 = $this->getTestStorage();
308
-		$storage1->mkdir('substorage');
309
-		Filesystem::mount($storage1, [], '/');
310
-		Filesystem::mount($storage2, [], '/substorage');
311
-
312
-		$rootView = new View('');
313
-		$folderContent = $rootView->getDirectoryContent('/');
314
-		$this->assertCount(4, $folderContent);
315
-	}
316
-
317
-	public static function sharingDisabledPermissionProvider(): array {
318
-		return [
319
-			['no', '', true],
320
-			['yes', 'group1', false],
321
-		];
322
-	}
323
-
324
-	#[\PHPUnit\Framework\Attributes\DataProvider('sharingDisabledPermissionProvider')]
325
-	public function testRemoveSharePermissionWhenSharingDisabledForUser($excludeGroups, $excludeGroupsList, $expectedShareable): void {
326
-		// Reset sharing disabled for users cache
327
-		self::invokePrivate(Server::get(ShareDisableChecker::class), 'sharingDisabledForUsersCache', [new CappedMemoryCache()]);
328
-
329
-		$config = Server::get(IConfig::class);
330
-		$oldExcludeGroupsFlag = $config->getAppValue('core', 'shareapi_exclude_groups', 'no');
331
-		$oldExcludeGroupsList = $config->getAppValue('core', 'shareapi_exclude_groups_list', '');
332
-		$config->setAppValue('core', 'shareapi_exclude_groups', $excludeGroups);
333
-		$config->setAppValue('core', 'shareapi_exclude_groups_list', $excludeGroupsList);
334
-
335
-		$storage1 = $this->getTestStorage();
336
-		$storage2 = $this->getTestStorage();
337
-		Filesystem::mount($storage1, [], '/');
338
-		Filesystem::mount($storage2, [], '/mount');
339
-
340
-		$view = new View('/');
341
-
342
-		$folderContent = $view->getDirectoryContent('');
343
-		$this->assertEquals($expectedShareable, $folderContent[0]->isShareable());
344
-
345
-		$folderContent = $view->getDirectoryContent('mount');
346
-		$this->assertEquals($expectedShareable, $folderContent[0]->isShareable());
347
-
348
-		$config->setAppValue('core', 'shareapi_exclude_groups', $oldExcludeGroupsFlag);
349
-		$config->setAppValue('core', 'shareapi_exclude_groups_list', $oldExcludeGroupsList);
350
-
351
-		// Reset sharing disabled for users cache
352
-		self::invokePrivate(Server::get(ShareDisableChecker::class), 'sharingDisabledForUsersCache', [new CappedMemoryCache()]);
353
-	}
354
-
355
-	public function testCacheIncompleteFolder(): void {
356
-		$storage1 = $this->getTestStorage(false);
357
-		Filesystem::mount($storage1, [], '/incomplete');
358
-		$rootView = new View('/incomplete');
359
-
360
-		$entries = $rootView->getDirectoryContent('/');
361
-		$this->assertCount(3, $entries);
362
-
363
-		// /folder will already be in the cache but not scanned
364
-		$entries = $rootView->getDirectoryContent('/folder');
365
-		$this->assertCount(1, $entries);
366
-	}
367
-
368
-	public function testAutoScan(): void {
369
-		$storage1 = $this->getTestStorage(false);
370
-		$storage2 = $this->getTestStorage(false);
371
-		Filesystem::mount($storage1, [], '/');
372
-		Filesystem::mount($storage2, [], '/substorage');
373
-		$textSize = strlen("dummy file data\n");
374
-
375
-		$rootView = new View('');
376
-
377
-		$cachedData = $rootView->getFileInfo('/');
378
-		$this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
379
-		$this->assertEquals(-1, $cachedData['size']);
380
-
381
-		$folderData = $rootView->getDirectoryContent('/substorage/folder');
382
-		$this->assertEquals('text/plain', $folderData[0]['mimetype']);
383
-		$this->assertEquals($textSize, $folderData[0]['size']);
384
-	}
385
-
386
-	public function testSearch(): void {
387
-		$storage1 = $this->getTestStorage();
388
-		$storage2 = $this->getTestStorage();
389
-		$storage3 = $this->getTestStorage();
390
-		Filesystem::mount($storage1, [], '/');
391
-		Filesystem::mount($storage2, [], '/substorage');
392
-		Filesystem::mount($storage3, [], '/folder/anotherstorage');
393
-
394
-		$rootView = new View('');
395
-
396
-		$results = $rootView->search('foo');
397
-		$this->assertCount(6, $results);
398
-		$paths = [];
399
-		foreach ($results as $result) {
400
-			$this->assertEquals($result['path'], Filesystem::normalizePath($result['path']));
401
-			$paths[] = $result['path'];
402
-		}
403
-		$this->assertContains('/foo.txt', $paths);
404
-		$this->assertContains('/foo.png', $paths);
405
-		$this->assertContains('/substorage/foo.txt', $paths);
406
-		$this->assertContains('/substorage/foo.png', $paths);
407
-		$this->assertContains('/folder/anotherstorage/foo.txt', $paths);
408
-		$this->assertContains('/folder/anotherstorage/foo.png', $paths);
409
-
410
-		$folderView = new View('/folder');
411
-		$results = $folderView->search('bar');
412
-		$this->assertCount(2, $results);
413
-		$paths = [];
414
-		foreach ($results as $result) {
415
-			$paths[] = $result['path'];
416
-		}
417
-		$this->assertContains('/anotherstorage/folder/bar.txt', $paths);
418
-		$this->assertContains('/bar.txt', $paths);
419
-
420
-		$results = $folderView->search('foo');
421
-		$this->assertCount(2, $results);
422
-		$paths = [];
423
-		foreach ($results as $result) {
424
-			$paths[] = $result['path'];
425
-		}
426
-		$this->assertContains('/anotherstorage/foo.txt', $paths);
427
-		$this->assertContains('/anotherstorage/foo.png', $paths);
428
-
429
-		$this->assertCount(6, $rootView->searchByMime('text'));
430
-		$this->assertCount(3, $folderView->searchByMime('text'));
431
-	}
432
-
433
-	public function testWatcher(): void {
434
-		$storage1 = $this->getTestStorage();
435
-		Filesystem::mount($storage1, [], '/');
436
-		$storage1->getWatcher()->setPolicy(Watcher::CHECK_ALWAYS);
437
-
438
-		$rootView = new View('');
439
-
440
-		$cachedData = $rootView->getFileInfo('foo.txt');
441
-		$this->assertEquals(16, $cachedData['size']);
442
-
443
-		$rootView->putFileInfo('foo.txt', ['storage_mtime' => 10]);
444
-		$storage1->file_put_contents('foo.txt', 'foo');
445
-		clearstatcache();
446
-
447
-		$cachedData = $rootView->getFileInfo('foo.txt');
448
-		$this->assertEquals(3, $cachedData['size']);
449
-	}
450
-
451
-	public function testCopyBetweenStorageNoCross(): void {
452
-		$storage1 = $this->getTestStorage(true, TemporaryNoCross::class);
453
-		$storage2 = $this->getTestStorage(true, TemporaryNoCross::class);
454
-		$this->copyBetweenStorages($storage1, $storage2);
455
-	}
456
-
457
-	public function testCopyBetweenStorageCross(): void {
458
-		$storage1 = $this->getTestStorage();
459
-		$storage2 = $this->getTestStorage();
460
-		$this->copyBetweenStorages($storage1, $storage2);
461
-	}
462
-
463
-	public function testCopyBetweenStorageCrossNonLocal(): void {
464
-		$storage1 = $this->getTestStorage(true, TemporaryNoLocal::class);
465
-		$storage2 = $this->getTestStorage(true, TemporaryNoLocal::class);
466
-		$this->copyBetweenStorages($storage1, $storage2);
467
-	}
468
-
469
-	public function copyBetweenStorages($storage1, $storage2) {
470
-		Filesystem::mount($storage1, [], '/');
471
-		Filesystem::mount($storage2, [], '/substorage');
472
-
473
-		$rootView = new View('');
474
-		$rootView->mkdir('substorage/emptyfolder');
475
-		$rootView->copy('substorage', 'anotherfolder');
476
-		$this->assertTrue($rootView->is_dir('/anotherfolder'));
477
-		$this->assertTrue($rootView->is_dir('/substorage'));
478
-		$this->assertTrue($rootView->is_dir('/anotherfolder/emptyfolder'));
479
-		$this->assertTrue($rootView->is_dir('/substorage/emptyfolder'));
480
-		$this->assertTrue($rootView->file_exists('/anotherfolder/foo.txt'));
481
-		$this->assertTrue($rootView->file_exists('/anotherfolder/foo.png'));
482
-		$this->assertTrue($rootView->file_exists('/anotherfolder/folder/bar.txt'));
483
-		$this->assertTrue($rootView->file_exists('/substorage/foo.txt'));
484
-		$this->assertTrue($rootView->file_exists('/substorage/foo.png'));
485
-		$this->assertTrue($rootView->file_exists('/substorage/folder/bar.txt'));
486
-	}
487
-
488
-	public function testMoveBetweenStorageNoCross(): void {
489
-		$storage1 = $this->getTestStorage(true, TemporaryNoCross::class);
490
-		$storage2 = $this->getTestStorage(true, TemporaryNoCross::class);
491
-		$this->moveBetweenStorages($storage1, $storage2);
492
-	}
493
-
494
-	public function testMoveBetweenStorageCross(): void {
495
-		$storage1 = $this->getTestStorage();
496
-		$storage2 = $this->getTestStorage();
497
-		$this->moveBetweenStorages($storage1, $storage2);
498
-	}
499
-
500
-	public function testMoveBetweenStorageCrossNonLocal(): void {
501
-		$storage1 = $this->getTestStorage(true, TemporaryNoLocal::class);
502
-		$storage2 = $this->getTestStorage(true, TemporaryNoLocal::class);
503
-		$this->moveBetweenStorages($storage1, $storage2);
504
-	}
505
-
506
-	public function moveBetweenStorages($storage1, $storage2) {
507
-		Filesystem::mount($storage1, [], '/' . $this->user . '/');
508
-		Filesystem::mount($storage2, [], '/' . $this->user . '/substorage');
509
-
510
-		$rootView = new View('/' . $this->user);
511
-		$rootView->rename('foo.txt', 'substorage/folder/foo.txt');
512
-		$this->assertFalse($rootView->file_exists('foo.txt'));
513
-		$this->assertTrue($rootView->file_exists('substorage/folder/foo.txt'));
514
-		$rootView->rename('substorage/folder', 'anotherfolder');
515
-		$this->assertFalse($rootView->is_dir('substorage/folder'));
516
-		$this->assertTrue($rootView->file_exists('anotherfolder/foo.txt'));
517
-		$this->assertTrue($rootView->file_exists('anotherfolder/bar.txt'));
518
-	}
519
-
520
-	public function testUnlink(): void {
521
-		$storage1 = $this->getTestStorage();
522
-		$storage2 = $this->getTestStorage();
523
-		Filesystem::mount($storage1, [], '/');
524
-		Filesystem::mount($storage2, [], '/substorage');
525
-
526
-		$rootView = new View('');
527
-		$rootView->file_put_contents('/foo.txt', 'asd');
528
-		$rootView->file_put_contents('/substorage/bar.txt', 'asd');
529
-
530
-		$this->assertTrue($rootView->file_exists('foo.txt'));
531
-		$this->assertTrue($rootView->file_exists('substorage/bar.txt'));
532
-
533
-		$this->assertTrue($rootView->unlink('foo.txt'));
534
-		$this->assertTrue($rootView->unlink('substorage/bar.txt'));
535
-
536
-		$this->assertFalse($rootView->file_exists('foo.txt'));
537
-		$this->assertFalse($rootView->file_exists('substorage/bar.txt'));
538
-	}
539
-
540
-	public static function rmdirOrUnlinkDataProvider(): array {
541
-		return [['rmdir'], ['unlink']];
542
-	}
543
-
544
-	#[\PHPUnit\Framework\Attributes\DataProvider('rmdirOrUnlinkDataProvider')]
545
-	public function testRmdir($method): void {
546
-		$storage1 = $this->getTestStorage();
547
-		Filesystem::mount($storage1, [], '/');
548
-
549
-		$rootView = new View('');
550
-		$rootView->mkdir('sub');
551
-		$rootView->mkdir('sub/deep');
552
-		$rootView->file_put_contents('/sub/deep/foo.txt', 'asd');
553
-
554
-		$this->assertTrue($rootView->file_exists('sub/deep/foo.txt'));
555
-
556
-		$this->assertTrue($rootView->$method('sub'));
557
-
558
-		$this->assertFalse($rootView->file_exists('sub'));
559
-	}
560
-
561
-	public function testUnlinkRootMustFail(): void {
562
-		$storage1 = $this->getTestStorage();
563
-		$storage2 = $this->getTestStorage();
564
-		Filesystem::mount($storage1, [], '/');
565
-		Filesystem::mount($storage2, [], '/substorage');
566
-
567
-		$rootView = new View('');
568
-		$rootView->file_put_contents('/foo.txt', 'asd');
569
-		$rootView->file_put_contents('/substorage/bar.txt', 'asd');
570
-
571
-		$this->assertFalse($rootView->unlink(''));
572
-		$this->assertFalse($rootView->unlink('/'));
573
-		$this->assertFalse($rootView->unlink('substorage'));
574
-		$this->assertFalse($rootView->unlink('/substorage'));
575
-	}
576
-
577
-	public function testTouch(): void {
578
-		$storage = $this->getTestStorage(true, TemporaryNoTouch::class);
579
-
580
-		Filesystem::mount($storage, [], '/');
581
-
582
-		$rootView = new View('');
583
-		$oldCachedData = $rootView->getFileInfo('foo.txt');
584
-
585
-		$rootView->touch('foo.txt', 500);
586
-
587
-		$cachedData = $rootView->getFileInfo('foo.txt');
588
-		$this->assertEquals(500, $cachedData['mtime']);
589
-		$this->assertEquals($oldCachedData['storage_mtime'], $cachedData['storage_mtime']);
590
-
591
-		$rootView->putFileInfo('foo.txt', ['storage_mtime' => 1000]); //make sure the watcher detects the change
592
-		$rootView->file_put_contents('foo.txt', 'asd');
593
-		$cachedData = $rootView->getFileInfo('foo.txt');
594
-		$this->assertGreaterThanOrEqual($oldCachedData['mtime'], $cachedData['mtime']);
595
-		$this->assertEquals($cachedData['storage_mtime'], $cachedData['mtime']);
596
-	}
597
-
598
-	public function testTouchFloat(): void {
599
-		$storage = $this->getTestStorage(true, TemporaryNoTouch::class);
600
-
601
-		Filesystem::mount($storage, [], '/');
602
-
603
-		$rootView = new View('');
604
-		$oldCachedData = $rootView->getFileInfo('foo.txt');
605
-
606
-		$rootView->touch('foo.txt', 500.5);
607
-
608
-		$cachedData = $rootView->getFileInfo('foo.txt');
609
-		$this->assertEquals(500, $cachedData['mtime']);
610
-	}
611
-
612
-	public function testViewHooks(): void {
613
-		$storage1 = $this->getTestStorage();
614
-		$storage2 = $this->getTestStorage();
615
-		$defaultRoot = Filesystem::getRoot();
616
-		Filesystem::mount($storage1, [], '/');
617
-		Filesystem::mount($storage2, [], $defaultRoot . '/substorage');
618
-		\OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook');
619
-
620
-		$rootView = new View('');
621
-		$subView = new View($defaultRoot . '/substorage');
622
-		$this->hookPath = null;
623
-
624
-		$rootView->file_put_contents('/foo.txt', 'asd');
625
-		$this->assertNull($this->hookPath);
626
-
627
-		$subView->file_put_contents('/foo.txt', 'asd');
628
-		$this->assertEquals('/substorage/foo.txt', $this->hookPath);
629
-	}
630
-
631
-	private $hookPath;
632
-
633
-	public function dummyHook($params) {
634
-		$this->hookPath = $params['path'];
635
-	}
636
-
637
-	public function testSearchNotOutsideView(): void {
638
-		$storage1 = $this->getTestStorage();
639
-		Filesystem::mount($storage1, [], '/');
640
-		$storage1->rename('folder', 'foo');
641
-		$scanner = $storage1->getScanner();
642
-		$scanner->scan('');
643
-
644
-		$view = new View('/foo');
645
-
646
-		$result = $view->search('.txt');
647
-		$this->assertCount(1, $result);
648
-	}
649
-
650
-	/**
651
-	 * @param bool $scan
652
-	 * @param string $class
653
-	 * @return Storage
654
-	 */
655
-	private function getTestStorage($scan = true, $class = Temporary::class) {
656
-		/**
657
-		 * @var Storage $storage
658
-		 */
659
-		$storage = new $class([]);
660
-		$textData = "dummy file data\n";
661
-		$imgData = file_get_contents(\OC::$SERVERROOT . '/core/img/logo/logo.png');
662
-		$storage->mkdir('folder');
663
-		$storage->file_put_contents('foo.txt', $textData);
664
-		$storage->file_put_contents('foo.png', $imgData);
665
-		$storage->file_put_contents('folder/bar.txt', $textData);
666
-
667
-		if ($scan) {
668
-			$scanner = $storage->getScanner();
669
-			$scanner->scan('');
670
-		}
671
-		$this->storages[] = $storage;
672
-		return $storage;
673
-	}
674
-
675
-	public function testViewHooksIfRootStartsTheSame(): void {
676
-		$storage1 = $this->getTestStorage();
677
-		$storage2 = $this->getTestStorage();
678
-		$defaultRoot = Filesystem::getRoot();
679
-		Filesystem::mount($storage1, [], '/');
680
-		Filesystem::mount($storage2, [], $defaultRoot . '_substorage');
681
-		\OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook');
682
-
683
-		$subView = new View($defaultRoot . '_substorage');
684
-		$this->hookPath = null;
685
-
686
-		$subView->file_put_contents('/foo.txt', 'asd');
687
-		$this->assertNull($this->hookPath);
688
-	}
689
-
690
-	private $hookWritePath;
691
-	private $hookCreatePath;
692
-	private $hookUpdatePath;
693
-
694
-	public function dummyHookWrite($params) {
695
-		$this->hookWritePath = $params['path'];
696
-	}
697
-
698
-	public function dummyHookUpdate($params) {
699
-		$this->hookUpdatePath = $params['path'];
700
-	}
701
-
702
-	public function dummyHookCreate($params) {
703
-		$this->hookCreatePath = $params['path'];
704
-	}
705
-
706
-	public function testEditNoCreateHook(): void {
707
-		$storage1 = $this->getTestStorage();
708
-		$storage2 = $this->getTestStorage();
709
-		$defaultRoot = Filesystem::getRoot();
710
-		Filesystem::mount($storage1, [], '/');
711
-		Filesystem::mount($storage2, [], $defaultRoot);
712
-		\OC_Hook::connect('OC_Filesystem', 'post_create', $this, 'dummyHookCreate');
713
-		\OC_Hook::connect('OC_Filesystem', 'post_update', $this, 'dummyHookUpdate');
714
-		\OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHookWrite');
715
-
716
-		$view = new View($defaultRoot);
717
-		$this->hookWritePath = $this->hookUpdatePath = $this->hookCreatePath = null;
718
-
719
-		$view->file_put_contents('/asd.txt', 'foo');
720
-		$this->assertEquals('/asd.txt', $this->hookCreatePath);
721
-		$this->assertNull($this->hookUpdatePath);
722
-		$this->assertEquals('/asd.txt', $this->hookWritePath);
723
-
724
-		$this->hookWritePath = $this->hookUpdatePath = $this->hookCreatePath = null;
725
-
726
-		$view->file_put_contents('/asd.txt', 'foo');
727
-		$this->assertNull($this->hookCreatePath);
728
-		$this->assertEquals('/asd.txt', $this->hookUpdatePath);
729
-		$this->assertEquals('/asd.txt', $this->hookWritePath);
730
-
731
-		\OC_Hook::clear('OC_Filesystem', 'post_create');
732
-		\OC_Hook::clear('OC_Filesystem', 'post_update');
733
-		\OC_Hook::clear('OC_Filesystem', 'post_write');
734
-	}
735
-
736
-	#[\PHPUnit\Framework\Attributes\DataProvider('resolvePathTestProvider')]
737
-	public function testResolvePath($expected, $pathToTest): void {
738
-		$storage1 = $this->getTestStorage();
739
-		Filesystem::mount($storage1, [], '/');
740
-
741
-		$view = new View('');
742
-
743
-		$result = $view->resolvePath($pathToTest);
744
-		$this->assertEquals($expected, $result[1]);
745
-
746
-		$exists = $view->file_exists($pathToTest);
747
-		$this->assertTrue($exists);
748
-
749
-		$exists = $view->file_exists($result[1]);
750
-		$this->assertTrue($exists);
751
-	}
752
-
753
-	public static function resolvePathTestProvider(): array {
754
-		return [
755
-			['foo.txt', 'foo.txt'],
756
-			['foo.txt', '/foo.txt'],
757
-			['folder', 'folder'],
758
-			['folder', '/folder'],
759
-			['folder', 'folder/'],
760
-			['folder', '/folder/'],
761
-			['folder/bar.txt', 'folder/bar.txt'],
762
-			['folder/bar.txt', '/folder/bar.txt'],
763
-			['', ''],
764
-			['', '/'],
765
-		];
766
-	}
767
-
768
-	public function testUTF8Names(): void {
769
-		$names = ['虚', '和知しゃ和で', 'regular ascii', 'sɨˈrɪlɪk', 'ѨѬ', 'أنا أحب القراءة كثيرا'];
770
-
771
-		$storage = new Temporary([]);
772
-		Filesystem::mount($storage, [], '/');
773
-
774
-		$rootView = new View('');
775
-		foreach ($names as $name) {
776
-			$rootView->file_put_contents('/' . $name, 'dummy content');
777
-		}
778
-
779
-		$list = $rootView->getDirectoryContent('/');
780
-
781
-		$this->assertCount(count($names), $list);
782
-		foreach ($list as $item) {
783
-			$this->assertContains($item['name'], $names);
784
-		}
785
-
786
-		$cache = $storage->getCache();
787
-		$scanner = $storage->getScanner();
788
-		$scanner->scan('');
789
-
790
-		$list = $cache->getFolderContents('');
791
-
792
-		$this->assertCount(count($names), $list);
793
-		foreach ($list as $item) {
794
-			$this->assertContains($item['name'], $names);
795
-		}
796
-	}
797
-
798
-	public function xtestLongPath() {
799
-		$storage = new Temporary([]);
800
-		Filesystem::mount($storage, [], '/');
801
-
802
-		$rootView = new View('');
803
-
804
-		$longPath = '';
805
-		$ds = DIRECTORY_SEPARATOR;
806
-		/*
99
+    use UserTrait;
100
+
101
+    /**
102
+     * @var Storage[] $storages
103
+     */
104
+    private $storages = [];
105
+
106
+    /**
107
+     * @var string
108
+     */
109
+    private $user;
110
+
111
+    /**
112
+     * @var IUser
113
+     */
114
+    private $userObject;
115
+
116
+    /**
117
+     * @var IGroup
118
+     */
119
+    private $groupObject;
120
+
121
+    /** @var Storage */
122
+    private $tempStorage;
123
+
124
+    protected function setUp(): void {
125
+        parent::setUp();
126
+        \OC_Hook::clear();
127
+
128
+        Server::get(IUserManager::class)->clearBackends();
129
+        Server::get(IUserManager::class)->registerBackend(new \Test\Util\User\Dummy());
130
+
131
+        //login
132
+        $userManager = Server::get(IUserManager::class);
133
+        $groupManager = Server::get(IGroupManager::class);
134
+        $this->user = 'test';
135
+        $this->userObject = $userManager->createUser('test', 'test');
136
+
137
+        $this->groupObject = $groupManager->createGroup('group1');
138
+        $this->groupObject->addUser($this->userObject);
139
+
140
+        self::loginAsUser($this->user);
141
+
142
+        /** @var IMountManager $manager */
143
+        $manager = Server::get(IMountManager::class);
144
+        $manager->removeMount('/test');
145
+
146
+        $this->tempStorage = null;
147
+    }
148
+
149
+    protected function tearDown(): void {
150
+        \OC_User::setUserId($this->user);
151
+        foreach ($this->storages as $storage) {
152
+            $cache = $storage->getCache();
153
+            $ids = $cache->getAll();
154
+            $cache->clear();
155
+        }
156
+
157
+        if ($this->tempStorage) {
158
+            system('rm -rf ' . escapeshellarg($this->tempStorage->getDataDir()));
159
+        }
160
+
161
+        self::logout();
162
+
163
+        /** @var SetupManager $setupManager */
164
+        $setupManager = Server::get(SetupManager::class);
165
+        $setupManager->setupRoot();
166
+
167
+        $this->userObject->delete();
168
+        $this->groupObject->delete();
169
+
170
+        $mountProviderCollection = Server::get(IMountProviderCollection::class);
171
+        self::invokePrivate($mountProviderCollection, 'providers', [[]]);
172
+
173
+        parent::tearDown();
174
+    }
175
+
176
+    public function testCacheAPI(): void {
177
+        $storage1 = $this->getTestStorage();
178
+        $storage2 = $this->getTestStorage();
179
+        $storage3 = $this->getTestStorage();
180
+        $root = self::getUniqueID('/');
181
+        Filesystem::mount($storage1, [], $root . '/');
182
+        Filesystem::mount($storage2, [], $root . '/substorage');
183
+        Filesystem::mount($storage3, [], $root . '/folder/anotherstorage');
184
+        $textSize = strlen("dummy file data\n");
185
+        $imageSize = filesize(\OC::$SERVERROOT . '/core/img/logo/logo.png');
186
+        $storageSize = $textSize * 2 + $imageSize;
187
+
188
+        $storageInfo = $storage3->getCache()->get('');
189
+        $this->assertEquals($storageSize, $storageInfo['size']);
190
+
191
+        $rootView = new View($root);
192
+
193
+        $cachedData = $rootView->getFileInfo('/foo.txt');
194
+        $this->assertEquals($textSize, $cachedData['size']);
195
+        $this->assertEquals('text/plain', $cachedData['mimetype']);
196
+        $this->assertNotEquals(-1, $cachedData['permissions']);
197
+
198
+        $cachedData = $rootView->getFileInfo('/');
199
+        $this->assertEquals($storageSize * 3, $cachedData['size']);
200
+        $this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
201
+
202
+        // get cached data excluding mount points
203
+        $cachedData = $rootView->getFileInfo('/', false);
204
+        $this->assertEquals($storageSize, $cachedData['size']);
205
+        $this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
206
+
207
+        $cachedData = $rootView->getFileInfo('/folder');
208
+        $this->assertEquals($storageSize + $textSize, $cachedData['size']);
209
+        $this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
210
+
211
+        $folderData = $rootView->getDirectoryContent('/');
212
+        /**
213
+         * expected entries:
214
+         * folder
215
+         * foo.png
216
+         * foo.txt
217
+         * substorage
218
+         */
219
+        $this->assertCount(4, $folderData);
220
+        $this->assertEquals('folder', $folderData[0]['name']);
221
+        $this->assertEquals('foo.png', $folderData[1]['name']);
222
+        $this->assertEquals('foo.txt', $folderData[2]['name']);
223
+        $this->assertEquals('substorage', $folderData[3]['name']);
224
+
225
+        $this->assertEquals($storageSize + $textSize, $folderData[0]['size']);
226
+        $this->assertEquals($imageSize, $folderData[1]['size']);
227
+        $this->assertEquals($textSize, $folderData[2]['size']);
228
+        $this->assertEquals($storageSize, $folderData[3]['size']);
229
+
230
+        $folderData = $rootView->getDirectoryContent('/substorage');
231
+        /**
232
+         * expected entries:
233
+         * folder
234
+         * foo.png
235
+         * foo.txt
236
+         */
237
+        $this->assertCount(3, $folderData);
238
+        $this->assertEquals('folder', $folderData[0]['name']);
239
+        $this->assertEquals('foo.png', $folderData[1]['name']);
240
+        $this->assertEquals('foo.txt', $folderData[2]['name']);
241
+
242
+        $folderView = new View($root . '/folder');
243
+        $this->assertEquals($rootView->getFileInfo('/folder'), $folderView->getFileInfo('/'));
244
+
245
+        $cachedData = $rootView->getFileInfo('/foo.txt');
246
+        $this->assertFalse($cachedData['encrypted']);
247
+        $id = $rootView->putFileInfo('/foo.txt', ['encrypted' => true]);
248
+        $cachedData = $rootView->getFileInfo('/foo.txt');
249
+        $this->assertTrue($cachedData['encrypted']);
250
+        $this->assertEquals($cachedData['fileid'], $id);
251
+
252
+        $this->assertFalse($rootView->getFileInfo('/non/existing'));
253
+        $this->assertEquals([], $rootView->getDirectoryContent('/non/existing'));
254
+    }
255
+
256
+    public function testGetPath(): void {
257
+        $user = $this->createMock(IUser::class);
258
+        $user->method('getUID')
259
+            ->willReturn('test');
260
+        $storage1 = $this->getTestStorage();
261
+        $storage2 = $this->getTestStorage();
262
+        $storage3 = $this->getTestStorage();
263
+
264
+        Filesystem::mount($storage1, [], '/test/files');
265
+        Filesystem::mount($storage2, [], '/test/files/substorage');
266
+        Filesystem::mount($storage3, [], '/test/files/folder/anotherstorage');
267
+
268
+        $userMountCache = Server::get(IUserMountCache::class);
269
+        $userMountCache->registerMounts($user, [
270
+            new MountPoint($storage1, '/test/files'),
271
+            new MountPoint($storage2, '/test/files/substorage'),
272
+            new MountPoint($storage3, '/test/files/folder/anotherstorage'),
273
+        ]);
274
+
275
+        $rootView = new View('/test/files');
276
+
277
+
278
+        $cachedData = $rootView->getFileInfo('/foo.txt');
279
+        $id1 = $cachedData->getId();
280
+        $this->assertEquals('/foo.txt', $rootView->getPath($id1));
281
+
282
+        $cachedData = $rootView->getFileInfo('/substorage/foo.txt');
283
+        $id2 = $cachedData->getId();
284
+        $this->assertEquals('/substorage/foo.txt', $rootView->getPath($id2));
285
+
286
+        $folderView = new View('/test/files/substorage');
287
+        $this->assertEquals('/foo.txt', $folderView->getPath($id2));
288
+    }
289
+
290
+
291
+    public function testGetPathNotExisting(): void {
292
+        $this->expectException(NotFoundException::class);
293
+
294
+        $storage1 = $this->getTestStorage();
295
+        Filesystem::mount($storage1, [], '/');
296
+
297
+        $rootView = new View('');
298
+        $cachedData = $rootView->getFileInfo('/foo.txt');
299
+        /** @var int $id1 */
300
+        $id1 = $cachedData['fileid'];
301
+        $folderView = new View('/substorage');
302
+        $this->assertNull($folderView->getPath($id1));
303
+    }
304
+
305
+    public function testMountPointOverwrite(): void {
306
+        $storage1 = $this->getTestStorage(false);
307
+        $storage2 = $this->getTestStorage();
308
+        $storage1->mkdir('substorage');
309
+        Filesystem::mount($storage1, [], '/');
310
+        Filesystem::mount($storage2, [], '/substorage');
311
+
312
+        $rootView = new View('');
313
+        $folderContent = $rootView->getDirectoryContent('/');
314
+        $this->assertCount(4, $folderContent);
315
+    }
316
+
317
+    public static function sharingDisabledPermissionProvider(): array {
318
+        return [
319
+            ['no', '', true],
320
+            ['yes', 'group1', false],
321
+        ];
322
+    }
323
+
324
+    #[\PHPUnit\Framework\Attributes\DataProvider('sharingDisabledPermissionProvider')]
325
+    public function testRemoveSharePermissionWhenSharingDisabledForUser($excludeGroups, $excludeGroupsList, $expectedShareable): void {
326
+        // Reset sharing disabled for users cache
327
+        self::invokePrivate(Server::get(ShareDisableChecker::class), 'sharingDisabledForUsersCache', [new CappedMemoryCache()]);
328
+
329
+        $config = Server::get(IConfig::class);
330
+        $oldExcludeGroupsFlag = $config->getAppValue('core', 'shareapi_exclude_groups', 'no');
331
+        $oldExcludeGroupsList = $config->getAppValue('core', 'shareapi_exclude_groups_list', '');
332
+        $config->setAppValue('core', 'shareapi_exclude_groups', $excludeGroups);
333
+        $config->setAppValue('core', 'shareapi_exclude_groups_list', $excludeGroupsList);
334
+
335
+        $storage1 = $this->getTestStorage();
336
+        $storage2 = $this->getTestStorage();
337
+        Filesystem::mount($storage1, [], '/');
338
+        Filesystem::mount($storage2, [], '/mount');
339
+
340
+        $view = new View('/');
341
+
342
+        $folderContent = $view->getDirectoryContent('');
343
+        $this->assertEquals($expectedShareable, $folderContent[0]->isShareable());
344
+
345
+        $folderContent = $view->getDirectoryContent('mount');
346
+        $this->assertEquals($expectedShareable, $folderContent[0]->isShareable());
347
+
348
+        $config->setAppValue('core', 'shareapi_exclude_groups', $oldExcludeGroupsFlag);
349
+        $config->setAppValue('core', 'shareapi_exclude_groups_list', $oldExcludeGroupsList);
350
+
351
+        // Reset sharing disabled for users cache
352
+        self::invokePrivate(Server::get(ShareDisableChecker::class), 'sharingDisabledForUsersCache', [new CappedMemoryCache()]);
353
+    }
354
+
355
+    public function testCacheIncompleteFolder(): void {
356
+        $storage1 = $this->getTestStorage(false);
357
+        Filesystem::mount($storage1, [], '/incomplete');
358
+        $rootView = new View('/incomplete');
359
+
360
+        $entries = $rootView->getDirectoryContent('/');
361
+        $this->assertCount(3, $entries);
362
+
363
+        // /folder will already be in the cache but not scanned
364
+        $entries = $rootView->getDirectoryContent('/folder');
365
+        $this->assertCount(1, $entries);
366
+    }
367
+
368
+    public function testAutoScan(): void {
369
+        $storage1 = $this->getTestStorage(false);
370
+        $storage2 = $this->getTestStorage(false);
371
+        Filesystem::mount($storage1, [], '/');
372
+        Filesystem::mount($storage2, [], '/substorage');
373
+        $textSize = strlen("dummy file data\n");
374
+
375
+        $rootView = new View('');
376
+
377
+        $cachedData = $rootView->getFileInfo('/');
378
+        $this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
379
+        $this->assertEquals(-1, $cachedData['size']);
380
+
381
+        $folderData = $rootView->getDirectoryContent('/substorage/folder');
382
+        $this->assertEquals('text/plain', $folderData[0]['mimetype']);
383
+        $this->assertEquals($textSize, $folderData[0]['size']);
384
+    }
385
+
386
+    public function testSearch(): void {
387
+        $storage1 = $this->getTestStorage();
388
+        $storage2 = $this->getTestStorage();
389
+        $storage3 = $this->getTestStorage();
390
+        Filesystem::mount($storage1, [], '/');
391
+        Filesystem::mount($storage2, [], '/substorage');
392
+        Filesystem::mount($storage3, [], '/folder/anotherstorage');
393
+
394
+        $rootView = new View('');
395
+
396
+        $results = $rootView->search('foo');
397
+        $this->assertCount(6, $results);
398
+        $paths = [];
399
+        foreach ($results as $result) {
400
+            $this->assertEquals($result['path'], Filesystem::normalizePath($result['path']));
401
+            $paths[] = $result['path'];
402
+        }
403
+        $this->assertContains('/foo.txt', $paths);
404
+        $this->assertContains('/foo.png', $paths);
405
+        $this->assertContains('/substorage/foo.txt', $paths);
406
+        $this->assertContains('/substorage/foo.png', $paths);
407
+        $this->assertContains('/folder/anotherstorage/foo.txt', $paths);
408
+        $this->assertContains('/folder/anotherstorage/foo.png', $paths);
409
+
410
+        $folderView = new View('/folder');
411
+        $results = $folderView->search('bar');
412
+        $this->assertCount(2, $results);
413
+        $paths = [];
414
+        foreach ($results as $result) {
415
+            $paths[] = $result['path'];
416
+        }
417
+        $this->assertContains('/anotherstorage/folder/bar.txt', $paths);
418
+        $this->assertContains('/bar.txt', $paths);
419
+
420
+        $results = $folderView->search('foo');
421
+        $this->assertCount(2, $results);
422
+        $paths = [];
423
+        foreach ($results as $result) {
424
+            $paths[] = $result['path'];
425
+        }
426
+        $this->assertContains('/anotherstorage/foo.txt', $paths);
427
+        $this->assertContains('/anotherstorage/foo.png', $paths);
428
+
429
+        $this->assertCount(6, $rootView->searchByMime('text'));
430
+        $this->assertCount(3, $folderView->searchByMime('text'));
431
+    }
432
+
433
+    public function testWatcher(): void {
434
+        $storage1 = $this->getTestStorage();
435
+        Filesystem::mount($storage1, [], '/');
436
+        $storage1->getWatcher()->setPolicy(Watcher::CHECK_ALWAYS);
437
+
438
+        $rootView = new View('');
439
+
440
+        $cachedData = $rootView->getFileInfo('foo.txt');
441
+        $this->assertEquals(16, $cachedData['size']);
442
+
443
+        $rootView->putFileInfo('foo.txt', ['storage_mtime' => 10]);
444
+        $storage1->file_put_contents('foo.txt', 'foo');
445
+        clearstatcache();
446
+
447
+        $cachedData = $rootView->getFileInfo('foo.txt');
448
+        $this->assertEquals(3, $cachedData['size']);
449
+    }
450
+
451
+    public function testCopyBetweenStorageNoCross(): void {
452
+        $storage1 = $this->getTestStorage(true, TemporaryNoCross::class);
453
+        $storage2 = $this->getTestStorage(true, TemporaryNoCross::class);
454
+        $this->copyBetweenStorages($storage1, $storage2);
455
+    }
456
+
457
+    public function testCopyBetweenStorageCross(): void {
458
+        $storage1 = $this->getTestStorage();
459
+        $storage2 = $this->getTestStorage();
460
+        $this->copyBetweenStorages($storage1, $storage2);
461
+    }
462
+
463
+    public function testCopyBetweenStorageCrossNonLocal(): void {
464
+        $storage1 = $this->getTestStorage(true, TemporaryNoLocal::class);
465
+        $storage2 = $this->getTestStorage(true, TemporaryNoLocal::class);
466
+        $this->copyBetweenStorages($storage1, $storage2);
467
+    }
468
+
469
+    public function copyBetweenStorages($storage1, $storage2) {
470
+        Filesystem::mount($storage1, [], '/');
471
+        Filesystem::mount($storage2, [], '/substorage');
472
+
473
+        $rootView = new View('');
474
+        $rootView->mkdir('substorage/emptyfolder');
475
+        $rootView->copy('substorage', 'anotherfolder');
476
+        $this->assertTrue($rootView->is_dir('/anotherfolder'));
477
+        $this->assertTrue($rootView->is_dir('/substorage'));
478
+        $this->assertTrue($rootView->is_dir('/anotherfolder/emptyfolder'));
479
+        $this->assertTrue($rootView->is_dir('/substorage/emptyfolder'));
480
+        $this->assertTrue($rootView->file_exists('/anotherfolder/foo.txt'));
481
+        $this->assertTrue($rootView->file_exists('/anotherfolder/foo.png'));
482
+        $this->assertTrue($rootView->file_exists('/anotherfolder/folder/bar.txt'));
483
+        $this->assertTrue($rootView->file_exists('/substorage/foo.txt'));
484
+        $this->assertTrue($rootView->file_exists('/substorage/foo.png'));
485
+        $this->assertTrue($rootView->file_exists('/substorage/folder/bar.txt'));
486
+    }
487
+
488
+    public function testMoveBetweenStorageNoCross(): void {
489
+        $storage1 = $this->getTestStorage(true, TemporaryNoCross::class);
490
+        $storage2 = $this->getTestStorage(true, TemporaryNoCross::class);
491
+        $this->moveBetweenStorages($storage1, $storage2);
492
+    }
493
+
494
+    public function testMoveBetweenStorageCross(): void {
495
+        $storage1 = $this->getTestStorage();
496
+        $storage2 = $this->getTestStorage();
497
+        $this->moveBetweenStorages($storage1, $storage2);
498
+    }
499
+
500
+    public function testMoveBetweenStorageCrossNonLocal(): void {
501
+        $storage1 = $this->getTestStorage(true, TemporaryNoLocal::class);
502
+        $storage2 = $this->getTestStorage(true, TemporaryNoLocal::class);
503
+        $this->moveBetweenStorages($storage1, $storage2);
504
+    }
505
+
506
+    public function moveBetweenStorages($storage1, $storage2) {
507
+        Filesystem::mount($storage1, [], '/' . $this->user . '/');
508
+        Filesystem::mount($storage2, [], '/' . $this->user . '/substorage');
509
+
510
+        $rootView = new View('/' . $this->user);
511
+        $rootView->rename('foo.txt', 'substorage/folder/foo.txt');
512
+        $this->assertFalse($rootView->file_exists('foo.txt'));
513
+        $this->assertTrue($rootView->file_exists('substorage/folder/foo.txt'));
514
+        $rootView->rename('substorage/folder', 'anotherfolder');
515
+        $this->assertFalse($rootView->is_dir('substorage/folder'));
516
+        $this->assertTrue($rootView->file_exists('anotherfolder/foo.txt'));
517
+        $this->assertTrue($rootView->file_exists('anotherfolder/bar.txt'));
518
+    }
519
+
520
+    public function testUnlink(): void {
521
+        $storage1 = $this->getTestStorage();
522
+        $storage2 = $this->getTestStorage();
523
+        Filesystem::mount($storage1, [], '/');
524
+        Filesystem::mount($storage2, [], '/substorage');
525
+
526
+        $rootView = new View('');
527
+        $rootView->file_put_contents('/foo.txt', 'asd');
528
+        $rootView->file_put_contents('/substorage/bar.txt', 'asd');
529
+
530
+        $this->assertTrue($rootView->file_exists('foo.txt'));
531
+        $this->assertTrue($rootView->file_exists('substorage/bar.txt'));
532
+
533
+        $this->assertTrue($rootView->unlink('foo.txt'));
534
+        $this->assertTrue($rootView->unlink('substorage/bar.txt'));
535
+
536
+        $this->assertFalse($rootView->file_exists('foo.txt'));
537
+        $this->assertFalse($rootView->file_exists('substorage/bar.txt'));
538
+    }
539
+
540
+    public static function rmdirOrUnlinkDataProvider(): array {
541
+        return [['rmdir'], ['unlink']];
542
+    }
543
+
544
+    #[\PHPUnit\Framework\Attributes\DataProvider('rmdirOrUnlinkDataProvider')]
545
+    public function testRmdir($method): void {
546
+        $storage1 = $this->getTestStorage();
547
+        Filesystem::mount($storage1, [], '/');
548
+
549
+        $rootView = new View('');
550
+        $rootView->mkdir('sub');
551
+        $rootView->mkdir('sub/deep');
552
+        $rootView->file_put_contents('/sub/deep/foo.txt', 'asd');
553
+
554
+        $this->assertTrue($rootView->file_exists('sub/deep/foo.txt'));
555
+
556
+        $this->assertTrue($rootView->$method('sub'));
557
+
558
+        $this->assertFalse($rootView->file_exists('sub'));
559
+    }
560
+
561
+    public function testUnlinkRootMustFail(): void {
562
+        $storage1 = $this->getTestStorage();
563
+        $storage2 = $this->getTestStorage();
564
+        Filesystem::mount($storage1, [], '/');
565
+        Filesystem::mount($storage2, [], '/substorage');
566
+
567
+        $rootView = new View('');
568
+        $rootView->file_put_contents('/foo.txt', 'asd');
569
+        $rootView->file_put_contents('/substorage/bar.txt', 'asd');
570
+
571
+        $this->assertFalse($rootView->unlink(''));
572
+        $this->assertFalse($rootView->unlink('/'));
573
+        $this->assertFalse($rootView->unlink('substorage'));
574
+        $this->assertFalse($rootView->unlink('/substorage'));
575
+    }
576
+
577
+    public function testTouch(): void {
578
+        $storage = $this->getTestStorage(true, TemporaryNoTouch::class);
579
+
580
+        Filesystem::mount($storage, [], '/');
581
+
582
+        $rootView = new View('');
583
+        $oldCachedData = $rootView->getFileInfo('foo.txt');
584
+
585
+        $rootView->touch('foo.txt', 500);
586
+
587
+        $cachedData = $rootView->getFileInfo('foo.txt');
588
+        $this->assertEquals(500, $cachedData['mtime']);
589
+        $this->assertEquals($oldCachedData['storage_mtime'], $cachedData['storage_mtime']);
590
+
591
+        $rootView->putFileInfo('foo.txt', ['storage_mtime' => 1000]); //make sure the watcher detects the change
592
+        $rootView->file_put_contents('foo.txt', 'asd');
593
+        $cachedData = $rootView->getFileInfo('foo.txt');
594
+        $this->assertGreaterThanOrEqual($oldCachedData['mtime'], $cachedData['mtime']);
595
+        $this->assertEquals($cachedData['storage_mtime'], $cachedData['mtime']);
596
+    }
597
+
598
+    public function testTouchFloat(): void {
599
+        $storage = $this->getTestStorage(true, TemporaryNoTouch::class);
600
+
601
+        Filesystem::mount($storage, [], '/');
602
+
603
+        $rootView = new View('');
604
+        $oldCachedData = $rootView->getFileInfo('foo.txt');
605
+
606
+        $rootView->touch('foo.txt', 500.5);
607
+
608
+        $cachedData = $rootView->getFileInfo('foo.txt');
609
+        $this->assertEquals(500, $cachedData['mtime']);
610
+    }
611
+
612
+    public function testViewHooks(): void {
613
+        $storage1 = $this->getTestStorage();
614
+        $storage2 = $this->getTestStorage();
615
+        $defaultRoot = Filesystem::getRoot();
616
+        Filesystem::mount($storage1, [], '/');
617
+        Filesystem::mount($storage2, [], $defaultRoot . '/substorage');
618
+        \OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook');
619
+
620
+        $rootView = new View('');
621
+        $subView = new View($defaultRoot . '/substorage');
622
+        $this->hookPath = null;
623
+
624
+        $rootView->file_put_contents('/foo.txt', 'asd');
625
+        $this->assertNull($this->hookPath);
626
+
627
+        $subView->file_put_contents('/foo.txt', 'asd');
628
+        $this->assertEquals('/substorage/foo.txt', $this->hookPath);
629
+    }
630
+
631
+    private $hookPath;
632
+
633
+    public function dummyHook($params) {
634
+        $this->hookPath = $params['path'];
635
+    }
636
+
637
+    public function testSearchNotOutsideView(): void {
638
+        $storage1 = $this->getTestStorage();
639
+        Filesystem::mount($storage1, [], '/');
640
+        $storage1->rename('folder', 'foo');
641
+        $scanner = $storage1->getScanner();
642
+        $scanner->scan('');
643
+
644
+        $view = new View('/foo');
645
+
646
+        $result = $view->search('.txt');
647
+        $this->assertCount(1, $result);
648
+    }
649
+
650
+    /**
651
+     * @param bool $scan
652
+     * @param string $class
653
+     * @return Storage
654
+     */
655
+    private function getTestStorage($scan = true, $class = Temporary::class) {
656
+        /**
657
+         * @var Storage $storage
658
+         */
659
+        $storage = new $class([]);
660
+        $textData = "dummy file data\n";
661
+        $imgData = file_get_contents(\OC::$SERVERROOT . '/core/img/logo/logo.png');
662
+        $storage->mkdir('folder');
663
+        $storage->file_put_contents('foo.txt', $textData);
664
+        $storage->file_put_contents('foo.png', $imgData);
665
+        $storage->file_put_contents('folder/bar.txt', $textData);
666
+
667
+        if ($scan) {
668
+            $scanner = $storage->getScanner();
669
+            $scanner->scan('');
670
+        }
671
+        $this->storages[] = $storage;
672
+        return $storage;
673
+    }
674
+
675
+    public function testViewHooksIfRootStartsTheSame(): void {
676
+        $storage1 = $this->getTestStorage();
677
+        $storage2 = $this->getTestStorage();
678
+        $defaultRoot = Filesystem::getRoot();
679
+        Filesystem::mount($storage1, [], '/');
680
+        Filesystem::mount($storage2, [], $defaultRoot . '_substorage');
681
+        \OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook');
682
+
683
+        $subView = new View($defaultRoot . '_substorage');
684
+        $this->hookPath = null;
685
+
686
+        $subView->file_put_contents('/foo.txt', 'asd');
687
+        $this->assertNull($this->hookPath);
688
+    }
689
+
690
+    private $hookWritePath;
691
+    private $hookCreatePath;
692
+    private $hookUpdatePath;
693
+
694
+    public function dummyHookWrite($params) {
695
+        $this->hookWritePath = $params['path'];
696
+    }
697
+
698
+    public function dummyHookUpdate($params) {
699
+        $this->hookUpdatePath = $params['path'];
700
+    }
701
+
702
+    public function dummyHookCreate($params) {
703
+        $this->hookCreatePath = $params['path'];
704
+    }
705
+
706
+    public function testEditNoCreateHook(): void {
707
+        $storage1 = $this->getTestStorage();
708
+        $storage2 = $this->getTestStorage();
709
+        $defaultRoot = Filesystem::getRoot();
710
+        Filesystem::mount($storage1, [], '/');
711
+        Filesystem::mount($storage2, [], $defaultRoot);
712
+        \OC_Hook::connect('OC_Filesystem', 'post_create', $this, 'dummyHookCreate');
713
+        \OC_Hook::connect('OC_Filesystem', 'post_update', $this, 'dummyHookUpdate');
714
+        \OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHookWrite');
715
+
716
+        $view = new View($defaultRoot);
717
+        $this->hookWritePath = $this->hookUpdatePath = $this->hookCreatePath = null;
718
+
719
+        $view->file_put_contents('/asd.txt', 'foo');
720
+        $this->assertEquals('/asd.txt', $this->hookCreatePath);
721
+        $this->assertNull($this->hookUpdatePath);
722
+        $this->assertEquals('/asd.txt', $this->hookWritePath);
723
+
724
+        $this->hookWritePath = $this->hookUpdatePath = $this->hookCreatePath = null;
725
+
726
+        $view->file_put_contents('/asd.txt', 'foo');
727
+        $this->assertNull($this->hookCreatePath);
728
+        $this->assertEquals('/asd.txt', $this->hookUpdatePath);
729
+        $this->assertEquals('/asd.txt', $this->hookWritePath);
730
+
731
+        \OC_Hook::clear('OC_Filesystem', 'post_create');
732
+        \OC_Hook::clear('OC_Filesystem', 'post_update');
733
+        \OC_Hook::clear('OC_Filesystem', 'post_write');
734
+    }
735
+
736
+    #[\PHPUnit\Framework\Attributes\DataProvider('resolvePathTestProvider')]
737
+    public function testResolvePath($expected, $pathToTest): void {
738
+        $storage1 = $this->getTestStorage();
739
+        Filesystem::mount($storage1, [], '/');
740
+
741
+        $view = new View('');
742
+
743
+        $result = $view->resolvePath($pathToTest);
744
+        $this->assertEquals($expected, $result[1]);
745
+
746
+        $exists = $view->file_exists($pathToTest);
747
+        $this->assertTrue($exists);
748
+
749
+        $exists = $view->file_exists($result[1]);
750
+        $this->assertTrue($exists);
751
+    }
752
+
753
+    public static function resolvePathTestProvider(): array {
754
+        return [
755
+            ['foo.txt', 'foo.txt'],
756
+            ['foo.txt', '/foo.txt'],
757
+            ['folder', 'folder'],
758
+            ['folder', '/folder'],
759
+            ['folder', 'folder/'],
760
+            ['folder', '/folder/'],
761
+            ['folder/bar.txt', 'folder/bar.txt'],
762
+            ['folder/bar.txt', '/folder/bar.txt'],
763
+            ['', ''],
764
+            ['', '/'],
765
+        ];
766
+    }
767
+
768
+    public function testUTF8Names(): void {
769
+        $names = ['虚', '和知しゃ和で', 'regular ascii', 'sɨˈrɪlɪk', 'ѨѬ', 'أنا أحب القراءة كثيرا'];
770
+
771
+        $storage = new Temporary([]);
772
+        Filesystem::mount($storage, [], '/');
773
+
774
+        $rootView = new View('');
775
+        foreach ($names as $name) {
776
+            $rootView->file_put_contents('/' . $name, 'dummy content');
777
+        }
778
+
779
+        $list = $rootView->getDirectoryContent('/');
780
+
781
+        $this->assertCount(count($names), $list);
782
+        foreach ($list as $item) {
783
+            $this->assertContains($item['name'], $names);
784
+        }
785
+
786
+        $cache = $storage->getCache();
787
+        $scanner = $storage->getScanner();
788
+        $scanner->scan('');
789
+
790
+        $list = $cache->getFolderContents('');
791
+
792
+        $this->assertCount(count($names), $list);
793
+        foreach ($list as $item) {
794
+            $this->assertContains($item['name'], $names);
795
+        }
796
+    }
797
+
798
+    public function xtestLongPath() {
799
+        $storage = new Temporary([]);
800
+        Filesystem::mount($storage, [], '/');
801
+
802
+        $rootView = new View('');
803
+
804
+        $longPath = '';
805
+        $ds = DIRECTORY_SEPARATOR;
806
+        /*
807 807
 		 * 4096 is the maximum path length in file_cache.path in *nix
808 808
 		 */
809
-		$folderName = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123456789';
810
-		$tmpdirLength = strlen(Server::get(ITempManager::class)->getTemporaryFolder());
811
-		$depth = ((4000 - $tmpdirLength) / 57);
812
-
813
-		foreach (range(0, $depth - 1) as $i) {
814
-			$longPath .= $ds . $folderName;
815
-			$result = $rootView->mkdir($longPath);
816
-			$this->assertTrue($result, "mkdir failed on $i - path length: " . strlen($longPath));
817
-
818
-			$result = $rootView->file_put_contents($longPath . "{$ds}test.txt", 'lorem');
819
-			$this->assertEquals(5, $result, "file_put_contents failed on $i");
820
-
821
-			$this->assertTrue($rootView->file_exists($longPath));
822
-			$this->assertTrue($rootView->file_exists($longPath . "{$ds}test.txt"));
823
-		}
824
-
825
-		$cache = $storage->getCache();
826
-		$scanner = $storage->getScanner();
827
-		$scanner->scan('');
828
-
829
-		$longPath = $folderName;
830
-		foreach (range(0, $depth - 1) as $i) {
831
-			$cachedFolder = $cache->get($longPath);
832
-			$this->assertTrue(is_array($cachedFolder), "No cache entry for folder at $i");
833
-			$this->assertEquals($folderName, $cachedFolder['name'], "Wrong cache entry for folder at $i");
834
-
835
-			$cachedFile = $cache->get($longPath . '/test.txt');
836
-			$this->assertTrue(is_array($cachedFile), "No cache entry for file at $i");
837
-			$this->assertEquals('test.txt', $cachedFile['name'], "Wrong cache entry for file at $i");
838
-
839
-			$longPath .= $ds . $folderName;
840
-		}
841
-	}
842
-
843
-	public function testTouchNotSupported(): void {
844
-		$storage = new TemporaryNoTouch([]);
845
-		$scanner = $storage->getScanner();
846
-		Filesystem::mount($storage, [], '/test/');
847
-		$past = time() - 100;
848
-		$storage->file_put_contents('test', 'foobar');
849
-		$scanner->scan('');
850
-		$view = new View('');
851
-		$info = $view->getFileInfo('/test/test');
852
-
853
-		$view->touch('/test/test', $past);
854
-		$scanner->scanFile('test', Scanner::REUSE_ETAG);
855
-
856
-		$info2 = $view->getFileInfo('/test/test');
857
-		$this->assertSame($info['etag'], $info2['etag']);
858
-	}
859
-
860
-	public function testWatcherEtagCrossStorage(): void {
861
-		$storage1 = new Temporary([]);
862
-		$storage2 = new Temporary([]);
863
-		$scanner1 = $storage1->getScanner();
864
-		$scanner2 = $storage2->getScanner();
865
-		$storage1->mkdir('sub');
866
-		Filesystem::mount($storage1, [], '/test/');
867
-		Filesystem::mount($storage2, [], '/test/sub/storage');
868
-
869
-		$past = time() - 100;
870
-		$storage2->file_put_contents('test.txt', 'foobar');
871
-		$scanner1->scan('');
872
-		$scanner2->scan('');
873
-		$view = new View('');
874
-
875
-		$storage2->getWatcher('')->setPolicy(Watcher::CHECK_ALWAYS);
876
-
877
-		$oldFileInfo = $view->getFileInfo('/test/sub/storage/test.txt');
878
-		$oldFolderInfo = $view->getFileInfo('/test');
879
-
880
-		$storage2->getCache()->update($oldFileInfo->getId(), [
881
-			'storage_mtime' => $past,
882
-		]);
883
-
884
-		$oldEtag = $oldFolderInfo->getEtag();
885
-
886
-		$view->getFileInfo('/test/sub/storage/test.txt');
887
-		$newFolderInfo = $view->getFileInfo('/test');
888
-
889
-		$this->assertNotEquals($newFolderInfo->getEtag(), $oldEtag);
890
-	}
891
-
892
-	#[\PHPUnit\Framework\Attributes\DataProvider('absolutePathProvider')]
893
-	public function testGetAbsolutePath($expectedPath, $relativePath): void {
894
-		$view = new View('/files');
895
-		$this->assertEquals($expectedPath, $view->getAbsolutePath($relativePath));
896
-	}
897
-
898
-	public function testPartFileInfo(): void {
899
-		$storage = new Temporary([]);
900
-		$scanner = $storage->getScanner();
901
-		Filesystem::mount($storage, [], '/test/');
902
-		$sizeWritten = $storage->file_put_contents('test.part', 'foobar');
903
-		$scanner->scan('');
904
-		$view = new View('/test');
905
-		$info = $view->getFileInfo('test.part');
906
-
907
-		$this->assertInstanceOf('\OCP\Files\FileInfo', $info);
908
-		$this->assertNull($info->getId());
909
-		$this->assertEquals(6, $sizeWritten);
910
-		$this->assertEquals(6, $info->getSize());
911
-		$this->assertEquals('foobar', $view->file_get_contents('test.part'));
912
-	}
913
-
914
-	public static function absolutePathProvider(): array {
915
-		return [
916
-			['/files', ''],
917
-			['/files/0', '0'],
918
-			['/files/false', 'false'],
919
-			['/files/true', 'true'],
920
-			['/files', '/'],
921
-			['/files/test', 'test'],
922
-			['/files/test', '/test'],
923
-		];
924
-	}
925
-
926
-	#[\PHPUnit\Framework\Attributes\DataProvider('chrootRelativePathProvider')]
927
-	public function testChrootGetRelativePath($root, $absolutePath, $expectedPath): void {
928
-		$view = new View('/files');
929
-		$view->chroot($root);
930
-		$this->assertEquals($expectedPath, $view->getRelativePath($absolutePath));
931
-	}
932
-
933
-	public static function chrootRelativePathProvider(): array {
934
-		return self::relativePathProvider('/');
935
-	}
936
-
937
-	#[\PHPUnit\Framework\Attributes\DataProvider('initRelativePathProvider')]
938
-	public function testInitGetRelativePath($root, $absolutePath, $expectedPath): void {
939
-		$view = new View($root);
940
-		$this->assertEquals($expectedPath, $view->getRelativePath($absolutePath));
941
-	}
942
-
943
-	public static function initRelativePathProvider(): array {
944
-		return self::relativePathProvider(null);
945
-	}
946
-
947
-	public static function relativePathProvider($missingRootExpectedPath): array {
948
-		return [
949
-			// No root - returns the path
950
-			['', '/files', '/files'],
951
-			['', '/files/', '/files/'],
952
-
953
-			// Root equals path - /
954
-			['/files/', '/files/', '/'],
955
-			['/files/', '/files', '/'],
956
-			['/files', '/files/', '/'],
957
-			['/files', '/files', '/'],
958
-
959
-			// False negatives: chroot fixes those by adding the leading slash.
960
-			// But setting them up with this root (instead of chroot($root))
961
-			// will fail them, although they should be the same.
962
-			// TODO init should be fixed, so it also adds the leading slash
963
-			['files/', '/files/', $missingRootExpectedPath],
964
-			['files', '/files/', $missingRootExpectedPath],
965
-			['files/', '/files', $missingRootExpectedPath],
966
-			['files', '/files', $missingRootExpectedPath],
967
-
968
-			// False negatives: Paths provided to the method should have a leading slash
969
-			// TODO input should be checked to have a leading slash
970
-			['/files/', 'files/', null],
971
-			['/files', 'files/', null],
972
-			['/files/', 'files', null],
973
-			['/files', 'files', null],
974
-
975
-			// with trailing slashes
976
-			['/files/', '/files/0', '0'],
977
-			['/files/', '/files/false', 'false'],
978
-			['/files/', '/files/true', 'true'],
979
-			['/files/', '/files/test', 'test'],
980
-			['/files/', '/files/test/foo', 'test/foo'],
981
-
982
-			// without trailing slashes
983
-			// TODO false expectation: Should match "with trailing slashes"
984
-			['/files', '/files/0', '/0'],
985
-			['/files', '/files/false', '/false'],
986
-			['/files', '/files/true', '/true'],
987
-			['/files', '/files/test', '/test'],
988
-			['/files', '/files/test/foo', '/test/foo'],
989
-
990
-			// leading slashes
991
-			['/files/', '/files_trashbin/', null],
992
-			['/files', '/files_trashbin/', null],
993
-			['/files/', '/files_trashbin', null],
994
-			['/files', '/files_trashbin', null],
995
-
996
-			// no leading slashes
997
-			['files/', 'files_trashbin/', null],
998
-			['files', 'files_trashbin/', null],
999
-			['files/', 'files_trashbin', null],
1000
-			['files', 'files_trashbin', null],
1001
-
1002
-			// mixed leading slashes
1003
-			['files/', '/files_trashbin/', null],
1004
-			['/files/', 'files_trashbin/', null],
1005
-			['files', '/files_trashbin/', null],
1006
-			['/files', 'files_trashbin/', null],
1007
-			['files/', '/files_trashbin', null],
1008
-			['/files/', 'files_trashbin', null],
1009
-			['files', '/files_trashbin', null],
1010
-			['/files', 'files_trashbin', null],
1011
-
1012
-			['files', 'files_trashbin/test', null],
1013
-			['/files', '/files_trashbin/test', null],
1014
-			['/files', 'files_trashbin/test', null],
1015
-		];
1016
-	}
1017
-
1018
-	public function testFileView(): void {
1019
-		$storage = new Temporary([]);
1020
-		$scanner = $storage->getScanner();
1021
-		$storage->file_put_contents('foo.txt', 'bar');
1022
-		Filesystem::mount($storage, [], '/test/');
1023
-		$scanner->scan('');
1024
-		$view = new View('/test/foo.txt');
1025
-
1026
-		$this->assertEquals('bar', $view->file_get_contents(''));
1027
-		$fh = tmpfile();
1028
-		fwrite($fh, 'foo');
1029
-		rewind($fh);
1030
-		$view->file_put_contents('', $fh);
1031
-		$this->assertEquals('foo', $view->file_get_contents(''));
1032
-	}
1033
-
1034
-	#[\PHPUnit\Framework\Attributes\DataProvider('tooLongPathDataProvider')]
1035
-	public function testTooLongPath($operation, $param0 = null): void {
1036
-		$this->expectException(InvalidPathException::class);
1037
-
1038
-
1039
-		$longPath = '';
1040
-		// 4000 is the maximum path length in file_cache.path
1041
-		$folderName = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123456789';
1042
-		$depth = (4000 / 57);
1043
-		foreach (range(0, $depth + 1) as $i) {
1044
-			$longPath .= '/' . $folderName;
1045
-		}
1046
-
1047
-		$storage = new Temporary([]);
1048
-		$this->tempStorage = $storage; // for later hard cleanup
1049
-		Filesystem::mount($storage, [], '/');
1050
-
1051
-		$rootView = new View('');
1052
-
1053
-		if ($param0 === '@0') {
1054
-			$param0 = $longPath;
1055
-		}
1056
-
1057
-		if ($operation === 'hash') {
1058
-			$param0 = $longPath;
1059
-			$longPath = 'md5';
1060
-		}
1061
-
1062
-		call_user_func([$rootView, $operation], $longPath, $param0);
1063
-	}
1064
-
1065
-	public static function tooLongPathDataProvider(): array {
1066
-		return [
1067
-			['getAbsolutePath'],
1068
-			['getRelativePath'],
1069
-			['getMountPoint'],
1070
-			['resolvePath'],
1071
-			['getLocalFile'],
1072
-			['mkdir'],
1073
-			['rmdir'],
1074
-			['opendir'],
1075
-			['is_dir'],
1076
-			['is_file'],
1077
-			['stat'],
1078
-			['filetype'],
1079
-			['filesize'],
1080
-			['readfile'],
1081
-			['isCreatable'],
1082
-			['isReadable'],
1083
-			['isUpdatable'],
1084
-			['isDeletable'],
1085
-			['isSharable'],
1086
-			['file_exists'],
1087
-			['filemtime'],
1088
-			['touch'],
1089
-			['file_get_contents'],
1090
-			['unlink'],
1091
-			['deleteAll'],
1092
-			['toTmpFile'],
1093
-			['getMimeType'],
1094
-			['free_space'],
1095
-			['getFileInfo'],
1096
-			['getDirectoryContent'],
1097
-			['getOwner'],
1098
-			['getETag'],
1099
-			['file_put_contents', 'ipsum'],
1100
-			['rename', '@0'],
1101
-			['copy', '@0'],
1102
-			['fopen', 'r'],
1103
-			['fromTmpFile', '@0'],
1104
-			['hash'],
1105
-			['hasUpdated', 0],
1106
-			['putFileInfo', []],
1107
-		];
1108
-	}
1109
-
1110
-	public function testRenameCrossStoragePreserveMtime(): void {
1111
-		$storage1 = new Temporary([]);
1112
-		$storage2 = new Temporary([]);
1113
-		$storage1->mkdir('sub');
1114
-		$storage1->mkdir('foo');
1115
-		$storage1->file_put_contents('foo.txt', 'asd');
1116
-		$storage1->file_put_contents('foo/bar.txt', 'asd');
1117
-		Filesystem::mount($storage1, [], '/test/');
1118
-		Filesystem::mount($storage2, [], '/test/sub/storage');
1119
-
1120
-		$view = new View('');
1121
-		$time = time() - 200;
1122
-		$view->touch('/test/foo.txt', $time);
1123
-		$view->touch('/test/foo', $time);
1124
-		$view->touch('/test/foo/bar.txt', $time);
1125
-
1126
-		$view->rename('/test/foo.txt', '/test/sub/storage/foo.txt');
1127
-
1128
-		$this->assertEquals($time, $view->filemtime('/test/sub/storage/foo.txt'));
1129
-
1130
-		$view->rename('/test/foo', '/test/sub/storage/foo');
1131
-
1132
-		$this->assertEquals($time, $view->filemtime('/test/sub/storage/foo/bar.txt'));
1133
-	}
1134
-
1135
-	public function testRenameFailDeleteTargetKeepSource(): void {
1136
-		$this->doTestCopyRenameFail('rename');
1137
-	}
1138
-
1139
-	public function testCopyFailDeleteTargetKeepSource(): void {
1140
-		$this->doTestCopyRenameFail('copy');
1141
-	}
1142
-
1143
-	private function doTestCopyRenameFail($operation) {
1144
-		$storage1 = new Temporary([]);
1145
-		/** @var \PHPUnit\Framework\MockObject\MockObject|Temporary $storage2 */
1146
-		$storage2 = $this->getMockBuilder(TemporaryNoCross::class)
1147
-			->setConstructorArgs([[]])
1148
-			->onlyMethods(['fopen', 'writeStream'])
1149
-			->getMock();
1150
-
1151
-		$storage2->method('writeStream')
1152
-			->willThrowException(new GenericFileException('Failed to copy stream'));
1153
-
1154
-		$storage2->method('fopen')
1155
-			->willReturn(false);
1156
-
1157
-		$storage1->mkdir('sub');
1158
-		$storage1->file_put_contents('foo.txt', '0123456789ABCDEFGH');
1159
-		$storage1->mkdir('dirtomove');
1160
-		$storage1->file_put_contents('dirtomove/indir1.txt', '0123456'); // fits
1161
-		$storage1->file_put_contents('dirtomove/indir2.txt', '0123456789ABCDEFGH'); // doesn't fit
1162
-		$storage2->file_put_contents('existing.txt', '0123');
1163
-		$storage1->getScanner()->scan('');
1164
-		$storage2->getScanner()->scan('');
1165
-		Filesystem::mount($storage1, [], '/test/');
1166
-		Filesystem::mount($storage2, [], '/test/sub/storage');
1167
-
1168
-		// move file
1169
-		$view = new View('');
1170
-		$this->assertTrue($storage1->file_exists('foo.txt'));
1171
-		$this->assertFalse($storage2->file_exists('foo.txt'));
1172
-		$this->assertFalse($view->$operation('/test/foo.txt', '/test/sub/storage/foo.txt'));
1173
-		$this->assertFalse($storage2->file_exists('foo.txt'));
1174
-		$this->assertFalse($storage2->getCache()->get('foo.txt'));
1175
-		$this->assertTrue($storage1->file_exists('foo.txt'));
1176
-
1177
-		// if target exists, it will be deleted too
1178
-		$this->assertFalse($view->$operation('/test/foo.txt', '/test/sub/storage/existing.txt'));
1179
-		$this->assertFalse($storage2->file_exists('existing.txt'));
1180
-		$this->assertFalse($storage2->getCache()->get('existing.txt'));
1181
-		$this->assertTrue($storage1->file_exists('foo.txt'));
1182
-
1183
-		// move folder
1184
-		$this->assertFalse($view->$operation('/test/dirtomove/', '/test/sub/storage/dirtomove/'));
1185
-		// since the move failed, the full source tree is kept
1186
-		$this->assertTrue($storage1->file_exists('dirtomove/indir1.txt'));
1187
-		$this->assertTrue($storage1->file_exists('dirtomove/indir2.txt'));
1188
-		// second file not moved/copied
1189
-		$this->assertFalse($storage2->file_exists('dirtomove/indir2.txt'));
1190
-		$this->assertFalse($storage2->getCache()->get('dirtomove/indir2.txt'));
1191
-	}
1192
-
1193
-	public function testDeleteFailKeepCache(): void {
1194
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1195
-		$storage = $this->getMockBuilder(Temporary::class)
1196
-			->setConstructorArgs([[]])
1197
-			->onlyMethods(['unlink'])
1198
-			->getMock();
1199
-		$storage->expects($this->once())
1200
-			->method('unlink')
1201
-			->willReturn(false);
1202
-		$scanner = $storage->getScanner();
1203
-		$cache = $storage->getCache();
1204
-		$storage->file_put_contents('foo.txt', 'asd');
1205
-		$scanner->scan('');
1206
-		Filesystem::mount($storage, [], '/test/');
1207
-
1208
-		$view = new View('/test');
1209
-
1210
-		$this->assertFalse($view->unlink('foo.txt'));
1211
-		$this->assertTrue($cache->inCache('foo.txt'));
1212
-	}
1213
-
1214
-	public static function directoryTraversalProvider(): array {
1215
-		return [
1216
-			['../test/'],
1217
-			['..\\test\\my/../folder'],
1218
-			['/test/my/../foo\\'],
1219
-		];
1220
-	}
1221
-
1222
-	/**
1223
-	 * @param string $root
1224
-	 */
1225
-	#[\PHPUnit\Framework\Attributes\DataProvider('directoryTraversalProvider')]
1226
-	public function testConstructDirectoryTraversalException($root): void {
1227
-		$this->expectException(\Exception::class);
1228
-
1229
-		new View($root);
1230
-	}
1231
-
1232
-	public function testRenameOverWrite(): void {
1233
-		$storage = new Temporary([]);
1234
-		$scanner = $storage->getScanner();
1235
-		$storage->mkdir('sub');
1236
-		$storage->mkdir('foo');
1237
-		$storage->file_put_contents('foo.txt', 'asd');
1238
-		$storage->file_put_contents('foo/bar.txt', 'asd');
1239
-		$scanner->scan('');
1240
-		Filesystem::mount($storage, [], '/test/');
1241
-		$view = new View('');
1242
-		$this->assertTrue($view->rename('/test/foo.txt', '/test/foo/bar.txt'));
1243
-	}
1244
-
1245
-	public function testSetMountOptionsInStorage(): void {
1246
-		$mount = new MountPoint(Temporary::class, '/asd/', [[]], Filesystem::getLoader(), ['foo' => 'bar']);
1247
-		Filesystem::getMountManager()->addMount($mount);
1248
-		/** @var Common $storage */
1249
-		$storage = $mount->getStorage();
1250
-		$this->assertEquals($storage->getMountOption('foo'), 'bar');
1251
-	}
1252
-
1253
-	public function testSetMountOptionsWatcherPolicy(): void {
1254
-		$mount = new MountPoint(Temporary::class, '/asd/', [[]], Filesystem::getLoader(), ['filesystem_check_changes' => Watcher::CHECK_NEVER]);
1255
-		Filesystem::getMountManager()->addMount($mount);
1256
-		/** @var Common $storage */
1257
-		$storage = $mount->getStorage();
1258
-		$watcher = $storage->getWatcher();
1259
-		$this->assertEquals(Watcher::CHECK_NEVER, $watcher->getPolicy());
1260
-	}
1261
-
1262
-	public function testGetAbsolutePathOnNull(): void {
1263
-		$view = new View();
1264
-		$this->assertNull($view->getAbsolutePath(null));
1265
-	}
1266
-
1267
-	public function testGetRelativePathOnNull(): void {
1268
-		$view = new View();
1269
-		$this->assertNull($view->getRelativePath(null));
1270
-	}
1271
-
1272
-
1273
-	public function testNullAsRoot(): void {
1274
-		$this->expectException(\TypeError::class);
1275
-
1276
-		new View(null);
1277
-	}
1278
-
1279
-	/**
1280
-	 * e.g. reading from a folder that's being renamed
1281
-	 *
1282
-	 *
1283
-	 *
1284
-	 * @param string $rootPath
1285
-	 * @param string $pathPrefix
1286
-	 */
1287
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1288
-	public function testReadFromWriteLockedPath($rootPath, $pathPrefix): void {
1289
-		$this->expectException(LockedException::class);
1290
-
1291
-		$rootPath = str_replace('{folder}', 'files', $rootPath);
1292
-		$pathPrefix = str_replace('{folder}', 'files', $pathPrefix);
1293
-
1294
-		$view = new View($rootPath);
1295
-		$storage = new Temporary([]);
1296
-		Filesystem::mount($storage, [], '/');
1297
-		$this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1298
-		$view->lockFile($pathPrefix . '/foo/bar/asd', ILockingProvider::LOCK_SHARED);
1299
-	}
1300
-
1301
-	/**
1302
-	 * Reading from a files_encryption folder that's being renamed
1303
-	 *
1304
-	 *
1305
-	 * @param string $rootPath
1306
-	 * @param string $pathPrefix
1307
-	 */
1308
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1309
-	public function testReadFromWriteUnlockablePath($rootPath, $pathPrefix): void {
1310
-		$rootPath = str_replace('{folder}', 'files_encryption', $rootPath);
1311
-		$pathPrefix = str_replace('{folder}', 'files_encryption', $pathPrefix);
1312
-
1313
-		$view = new View($rootPath);
1314
-		$storage = new Temporary([]);
1315
-		Filesystem::mount($storage, [], '/');
1316
-		$this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1317
-		$this->assertFalse($view->lockFile($pathPrefix . '/foo/bar/asd', ILockingProvider::LOCK_SHARED));
1318
-	}
1319
-
1320
-	/**
1321
-	 * e.g. writing a file that's being downloaded
1322
-	 *
1323
-	 *
1324
-	 *
1325
-	 * @param string $rootPath
1326
-	 * @param string $pathPrefix
1327
-	 */
1328
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1329
-	public function testWriteToReadLockedFile($rootPath, $pathPrefix): void {
1330
-		$this->expectException(LockedException::class);
1331
-
1332
-		$rootPath = str_replace('{folder}', 'files', $rootPath);
1333
-		$pathPrefix = str_replace('{folder}', 'files', $pathPrefix);
1334
-
1335
-		$view = new View($rootPath);
1336
-		$storage = new Temporary([]);
1337
-		Filesystem::mount($storage, [], '/');
1338
-		$this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED));
1339
-		$view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE);
1340
-	}
1341
-
1342
-	/**
1343
-	 * Writing a file that's being downloaded
1344
-	 *
1345
-	 *
1346
-	 * @param string $rootPath
1347
-	 * @param string $pathPrefix
1348
-	 */
1349
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1350
-	public function testWriteToReadUnlockableFile($rootPath, $pathPrefix): void {
1351
-		$rootPath = str_replace('{folder}', 'files_encryption', $rootPath);
1352
-		$pathPrefix = str_replace('{folder}', 'files_encryption', $pathPrefix);
1353
-
1354
-		$view = new View($rootPath);
1355
-		$storage = new Temporary([]);
1356
-		Filesystem::mount($storage, [], '/');
1357
-		$this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED));
1358
-		$this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1359
-	}
1360
-
1361
-	/**
1362
-	 * Test that locks are on mount point paths instead of mount root
1363
-	 */
1364
-	public function testLockLocalMountPointPathInsteadOfStorageRoot(): void {
1365
-		$lockingProvider = Server::get(ILockingProvider::class);
1366
-		$view = new View('/testuser/files/');
1367
-		$storage = new Temporary([]);
1368
-		Filesystem::mount($storage, [], '/');
1369
-		$mountedStorage = new Temporary([]);
1370
-		Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint');
1371
-
1372
-		$this->assertTrue(
1373
-			$view->lockFile('/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, true),
1374
-			'Can lock mount point'
1375
-		);
1376
-
1377
-		// no exception here because storage root was not locked
1378
-		$mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1379
-
1380
-		$thrown = false;
1381
-		try {
1382
-			$storage->acquireLock('/testuser/files/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1383
-		} catch (LockedException $e) {
1384
-			$thrown = true;
1385
-		}
1386
-		$this->assertTrue($thrown, 'Mount point path was locked on root storage');
1387
-
1388
-		$lockingProvider->releaseAll();
1389
-	}
1390
-
1391
-	/**
1392
-	 * Test that locks are on mount point paths and also mount root when requested
1393
-	 */
1394
-	public function testLockStorageRootButNotLocalMountPoint(): void {
1395
-		$lockingProvider = Server::get(ILockingProvider::class);
1396
-		$view = new View('/testuser/files/');
1397
-		$storage = new Temporary([]);
1398
-		Filesystem::mount($storage, [], '/');
1399
-		$mountedStorage = new Temporary([]);
1400
-		Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint');
1401
-
1402
-		$this->assertTrue(
1403
-			$view->lockFile('/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, false),
1404
-			'Can lock mount point'
1405
-		);
1406
-
1407
-		$thrown = false;
1408
-		try {
1409
-			$mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1410
-		} catch (LockedException $e) {
1411
-			$thrown = true;
1412
-		}
1413
-		$this->assertTrue($thrown, 'Mount point storage root was locked on original storage');
1414
-
1415
-		// local mount point was not locked
1416
-		$storage->acquireLock('/testuser/files/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1417
-
1418
-		$lockingProvider->releaseAll();
1419
-	}
1420
-
1421
-	/**
1422
-	 * Test that locks are on mount point paths and also mount root when requested
1423
-	 */
1424
-	public function testLockMountPointPathFailReleasesBoth(): void {
1425
-		$lockingProvider = Server::get(ILockingProvider::class);
1426
-		$view = new View('/testuser/files/');
1427
-		$storage = new Temporary([]);
1428
-		Filesystem::mount($storage, [], '/');
1429
-		$mountedStorage = new Temporary([]);
1430
-		Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint.txt');
1431
-
1432
-		// this would happen if someone is writing on the mount point
1433
-		$mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1434
-
1435
-		$thrown = false;
1436
-		try {
1437
-			// this actually acquires two locks, one on the mount point and one on the storage root,
1438
-			// but the one on the storage root will fail
1439
-			$view->lockFile('/mountpoint.txt', ILockingProvider::LOCK_SHARED);
1440
-		} catch (LockedException $e) {
1441
-			$thrown = true;
1442
-		}
1443
-		$this->assertTrue($thrown, 'Cannot acquire shared lock because storage root is already locked');
1444
-
1445
-		// from here we expect that the lock on the local mount point was released properly
1446
-		// so acquiring an exclusive lock will succeed
1447
-		$storage->acquireLock('/testuser/files/mountpoint.txt', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1448
-
1449
-		$lockingProvider->releaseAll();
1450
-	}
1451
-
1452
-	public static function dataLockPaths(): array {
1453
-		return [
1454
-			['/testuser/{folder}', ''],
1455
-			['/testuser', '/{folder}'],
1456
-			['', '/testuser/{folder}'],
1457
-		];
1458
-	}
1459
-
1460
-	public static function pathRelativeToFilesProvider(): array {
1461
-		return [
1462
-			['admin/files', ''],
1463
-			['admin/files/x', 'x'],
1464
-			['/admin/files', ''],
1465
-			['/admin/files/sub', 'sub'],
1466
-			['/admin/files/sub/', 'sub'],
1467
-			['/admin/files/sub/sub2', 'sub/sub2'],
1468
-			['//admin//files/sub//sub2', 'sub/sub2'],
1469
-		];
1470
-	}
1471
-
1472
-	#[\PHPUnit\Framework\Attributes\DataProvider('pathRelativeToFilesProvider')]
1473
-	public function testGetPathRelativeToFiles($path, $expectedPath): void {
1474
-		$view = new View();
1475
-		$this->assertEquals($expectedPath, $view->getPathRelativeToFiles($path));
1476
-	}
1477
-
1478
-	public static function pathRelativeToFilesProviderExceptionCases(): array {
1479
-		return [
1480
-			[''],
1481
-			['x'],
1482
-			['files'],
1483
-			['/files'],
1484
-			['/admin/files_versions/abc'],
1485
-		];
1486
-	}
1487
-
1488
-	/**
1489
-	 * @param string $path
1490
-	 */
1491
-	#[\PHPUnit\Framework\Attributes\DataProvider('pathRelativeToFilesProviderExceptionCases')]
1492
-	public function testGetPathRelativeToFilesWithInvalidArgument($path): void {
1493
-		$this->expectException(\InvalidArgumentException::class);
1494
-		$this->expectExceptionMessage('$absolutePath must be relative to "files"');
1495
-
1496
-		$view = new View();
1497
-		$view->getPathRelativeToFiles($path);
1498
-	}
1499
-
1500
-	public function testChangeLock(): void {
1501
-		$view = new View('/testuser/files/');
1502
-		$storage = new Temporary([]);
1503
-		Filesystem::mount($storage, [], '/');
1504
-
1505
-		$view->lockFile('/test/sub', ILockingProvider::LOCK_SHARED);
1506
-		$this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED));
1507
-		$this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE));
1508
-
1509
-		$view->changeLock('//test/sub', ILockingProvider::LOCK_EXCLUSIVE);
1510
-		$this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE));
1511
-
1512
-		$view->changeLock('test/sub', ILockingProvider::LOCK_SHARED);
1513
-		$this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED));
1514
-
1515
-		$view->unlockFile('/test/sub/', ILockingProvider::LOCK_SHARED);
1516
-
1517
-		$this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED));
1518
-		$this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE));
1519
-	}
1520
-
1521
-	public static function hookPathProvider(): array {
1522
-		return [
1523
-			['/foo/files', '/foo', true],
1524
-			['/foo/files/bar', '/foo', true],
1525
-			['/foo', '/foo', false],
1526
-			['/foo', '/files/foo', true],
1527
-			['/foo', 'filesfoo', false],
1528
-			['', '/foo/files', true],
1529
-			['', '/foo/files/bar.txt', true],
1530
-		];
1531
-	}
1532
-
1533
-	/**
1534
-	 * @param $root
1535
-	 * @param $path
1536
-	 * @param $shouldEmit
1537
-	 */
1538
-	#[\PHPUnit\Framework\Attributes\DataProvider('hookPathProvider')]
1539
-	public function testHookPaths($root, $path, $shouldEmit): void {
1540
-		$filesystemReflection = new \ReflectionClass(Filesystem::class);
1541
-		$defaultRootValue = $filesystemReflection->getProperty('defaultInstance');
1542
-		$oldRoot = $defaultRootValue->getValue();
1543
-		$defaultView = new View('/foo/files');
1544
-		$defaultRootValue->setValue(null, $defaultView);
1545
-		$view = new View($root);
1546
-		$result = self::invokePrivate($view, 'shouldEmitHooks', [$path]);
1547
-		$defaultRootValue->setValue(null, $oldRoot);
1548
-		$this->assertEquals($shouldEmit, $result);
1549
-	}
1550
-
1551
-	/**
1552
-	 * Create test movable mount points
1553
-	 *
1554
-	 * @param array $mountPoints array of mount point locations
1555
-	 * @return array array of MountPoint objects
1556
-	 */
1557
-	private function createTestMovableMountPoints($mountPoints) {
1558
-		$mounts = [];
1559
-		foreach ($mountPoints as $mountPoint) {
1560
-			$storage = $this->getMockBuilder(Storage::class)
1561
-				->onlyMethods([])
1562
-				->getMock();
1563
-			$storage->method('getId')->willReturn('non-null-id');
1564
-			$storage->method('getStorageCache')->willReturnCallback(function () use ($storage) {
1565
-				return new \OC\Files\Cache\Storage($storage, true, Server::get(IDBConnection::class));
1566
-			});
1567
-
1568
-			$mounts[] = $this->getMockBuilder(TestMoveableMountPoint::class)
1569
-				->onlyMethods(['moveMount'])
1570
-				->setConstructorArgs([$storage, $mountPoint])
1571
-				->getMock();
1572
-		}
1573
-
1574
-		/** @var IMountProvider|\PHPUnit\Framework\MockObject\MockObject $mountProvider */
1575
-		$mountProvider = $this->createMock(IMountProvider::class);
1576
-		$mountProvider->expects($this->any())
1577
-			->method('getMountsForUser')
1578
-			->willReturn($mounts);
1579
-
1580
-		$mountProviderCollection = Server::get(IMountProviderCollection::class);
1581
-		$mountProviderCollection->registerProvider($mountProvider);
1582
-
1583
-		return $mounts;
1584
-	}
1585
-
1586
-	/**
1587
-	 * Test mount point move
1588
-	 */
1589
-	public function testMountPointMove(): void {
1590
-		self::loginAsUser($this->user);
1591
-
1592
-		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1593
-			$this->user . '/files/mount1',
1594
-			$this->user . '/files/mount2',
1595
-		]);
1596
-		$mount1->expects($this->once())
1597
-			->method('moveMount')
1598
-			->willReturn(true);
1599
-
1600
-		$mount2->expects($this->once())
1601
-			->method('moveMount')
1602
-			->willReturn(true);
1603
-
1604
-		$view = new View('/' . $this->user . '/files/');
1605
-		$view->mkdir('sub');
1606
-
1607
-		$this->assertTrue($view->rename('mount1', 'renamed_mount'), 'Can rename mount point');
1608
-		$this->assertTrue($view->rename('mount2', 'sub/moved_mount'), 'Can move a mount point into a subdirectory');
1609
-	}
1610
-
1611
-	public function testMoveMountPointOverwrite(): void {
1612
-		self::loginAsUser($this->user);
1613
-
1614
-		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1615
-			$this->user . '/files/mount1',
1616
-			$this->user . '/files/mount2',
1617
-		]);
1618
-
1619
-		$mount1->expects($this->never())
1620
-			->method('moveMount');
1621
-
1622
-		$mount2->expects($this->never())
1623
-			->method('moveMount');
1624
-
1625
-		$view = new View('/' . $this->user . '/files/');
1626
-
1627
-		$this->expectException(ForbiddenException::class);
1628
-		$view->rename('mount1', 'mount2');
1629
-	}
1630
-
1631
-	public function testMoveMountPointIntoMount(): void {
1632
-		self::loginAsUser($this->user);
1633
-
1634
-		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1635
-			$this->user . '/files/mount1',
1636
-			$this->user . '/files/mount2',
1637
-		]);
1638
-
1639
-		$mount1->expects($this->never())
1640
-			->method('moveMount');
1641
-
1642
-		$mount2->expects($this->never())
1643
-			->method('moveMount');
1644
-
1645
-		$view = new View('/' . $this->user . '/files/');
1646
-
1647
-		$this->expectException(ForbiddenException::class);
1648
-		$view->rename('mount1', 'mount2/sub');
1649
-	}
1650
-
1651
-	/**
1652
-	 * Test that moving a mount point into a shared folder is forbidden
1653
-	 */
1654
-	public function testMoveMountPointIntoSharedFolder(): void {
1655
-		self::loginAsUser($this->user);
1656
-
1657
-		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1658
-			$this->user . '/files/mount1',
1659
-			$this->user . '/files/mount2',
1660
-		]);
1661
-
1662
-		$mount1->expects($this->never())
1663
-			->method('moveMount');
1664
-
1665
-		$mount2->expects($this->once())
1666
-			->method('moveMount')
1667
-			->willReturn(true);
1668
-
1669
-		$view = new View('/' . $this->user . '/files/');
1670
-		$view->mkdir('shareddir');
1671
-		$view->mkdir('shareddir/sub');
1672
-		$view->mkdir('shareddir/sub2');
1673
-		// Create a similar named but non-shared folder
1674
-		$view->mkdir('shareddir notshared');
1675
-
1676
-		$fileId = $view->getFileInfo('shareddir')->getId();
1677
-		$userObject = Server::get(IUserManager::class)->createUser('test2', 'IHateNonMockableStaticClasses');
1678
-
1679
-		$userFolder = \OC::$server->getUserFolder($this->user);
1680
-		$shareDir = $userFolder->get('shareddir');
1681
-		$shareManager = Server::get(IShareManager::class);
1682
-		$share = $shareManager->newShare();
1683
-		$share->setSharedWith('test2')
1684
-			->setSharedBy($this->user)
1685
-			->setShareType(IShare::TYPE_USER)
1686
-			->setPermissions(Constants::PERMISSION_READ)
1687
-			->setNode($shareDir);
1688
-		$shareManager->createShare($share);
1689
-
1690
-		try {
1691
-			$view->rename('mount1', 'shareddir');
1692
-			$this->fail('Cannot overwrite shared folder');
1693
-		} catch (ForbiddenException $e) {
1694
-
1695
-		}
1696
-		try {
1697
-			$view->rename('mount1', 'shareddir/sub');
1698
-			$this->fail('Cannot move mount point into shared folder');
1699
-		} catch (ForbiddenException $e) {
1700
-
1701
-		}
1702
-		try {
1703
-			$view->rename('mount1', 'shareddir/sub/sub2');
1704
-			$this->fail('Cannot move mount point into shared subfolder');
1705
-		} catch (ForbiddenException $e) {
1706
-
1707
-		}
1708
-		$this->assertTrue($view->rename('mount2', 'shareddir notshared/sub'), 'Can move mount point into a similarly named but non-shared folder');
1709
-
1710
-		$shareManager->deleteShare($share);
1711
-		$userObject->delete();
1712
-	}
1713
-
1714
-	public static function basicOperationProviderForLocks(): array {
1715
-		return [
1716
-			// --- write hook ----
1717
-			[
1718
-				'touch',
1719
-				['touch-create.txt'],
1720
-				'touch-create.txt',
1721
-				'create',
1722
-				ILockingProvider::LOCK_SHARED,
1723
-				ILockingProvider::LOCK_EXCLUSIVE,
1724
-				ILockingProvider::LOCK_SHARED,
1725
-			],
1726
-			[
1727
-				'fopen',
1728
-				['test-write.txt', 'w'],
1729
-				'test-write.txt',
1730
-				'write',
1731
-				ILockingProvider::LOCK_SHARED,
1732
-				ILockingProvider::LOCK_EXCLUSIVE,
1733
-				null,
1734
-				// exclusive lock stays until fclose
1735
-				ILockingProvider::LOCK_EXCLUSIVE,
1736
-			],
1737
-			[
1738
-				'mkdir',
1739
-				['newdir'],
1740
-				'newdir',
1741
-				'write',
1742
-				ILockingProvider::LOCK_SHARED,
1743
-				ILockingProvider::LOCK_EXCLUSIVE,
1744
-				ILockingProvider::LOCK_SHARED,
1745
-			],
1746
-			[
1747
-				'file_put_contents',
1748
-				['file_put_contents.txt', 'blah'],
1749
-				'file_put_contents.txt',
1750
-				'write',
1751
-				ILockingProvider::LOCK_SHARED,
1752
-				ILockingProvider::LOCK_EXCLUSIVE,
1753
-				ILockingProvider::LOCK_SHARED,
1754
-				null,
1755
-				0,
1756
-			],
1757
-
1758
-			// ---- delete hook ----
1759
-			[
1760
-				'rmdir',
1761
-				['dir'],
1762
-				'dir',
1763
-				'delete',
1764
-				ILockingProvider::LOCK_SHARED,
1765
-				ILockingProvider::LOCK_EXCLUSIVE,
1766
-				ILockingProvider::LOCK_SHARED,
1767
-			],
1768
-			[
1769
-				'unlink',
1770
-				['test.txt'],
1771
-				'test.txt',
1772
-				'delete',
1773
-				ILockingProvider::LOCK_SHARED,
1774
-				ILockingProvider::LOCK_EXCLUSIVE,
1775
-				ILockingProvider::LOCK_SHARED,
1776
-			],
1777
-
1778
-			// ---- read hook (no post hooks) ----
1779
-			[
1780
-				'file_get_contents',
1781
-				['test.txt'],
1782
-				'test.txt',
1783
-				'read',
1784
-				ILockingProvider::LOCK_SHARED,
1785
-				ILockingProvider::LOCK_SHARED,
1786
-				null,
1787
-				null,
1788
-				false,
1789
-			],
1790
-			[
1791
-				'fopen',
1792
-				['test.txt', 'r'],
1793
-				'test.txt',
1794
-				'read',
1795
-				ILockingProvider::LOCK_SHARED,
1796
-				ILockingProvider::LOCK_SHARED,
1797
-				null,
1798
-			],
1799
-			[
1800
-				'opendir',
1801
-				['dir'],
1802
-				'dir',
1803
-				'read',
1804
-				ILockingProvider::LOCK_SHARED,
1805
-				ILockingProvider::LOCK_SHARED,
1806
-				null,
1807
-			],
1808
-
1809
-			// ---- no lock, touch hook ---
1810
-			['touch', ['test.txt'], 'test.txt', 'touch', null, null, null],
1811
-
1812
-			// ---- no hooks, no locks ---
1813
-			['is_dir', ['dir'], 'dir', ''],
1814
-			['is_file', ['dir'], 'dir', ''],
1815
-			[
1816
-				'stat',
1817
-				['dir'],
1818
-				'dir',
1819
-				'',
1820
-				ILockingProvider::LOCK_SHARED,
1821
-				ILockingProvider::LOCK_SHARED,
1822
-				ILockingProvider::LOCK_SHARED,
1823
-				null,
1824
-				false,
1825
-			],
1826
-			[
1827
-				'filetype',
1828
-				['dir'],
1829
-				'dir',
1830
-				'',
1831
-				ILockingProvider::LOCK_SHARED,
1832
-				ILockingProvider::LOCK_SHARED,
1833
-				ILockingProvider::LOCK_SHARED,
1834
-				null,
1835
-				false,
1836
-			],
1837
-			[
1838
-				'filesize',
1839
-				['dir'],
1840
-				'dir',
1841
-				'',
1842
-				ILockingProvider::LOCK_SHARED,
1843
-				ILockingProvider::LOCK_SHARED,
1844
-				ILockingProvider::LOCK_SHARED,
1845
-				null,
1846
-				/* Return an int */
1847
-				100
1848
-			],
1849
-			['isCreatable', ['dir'], 'dir', ''],
1850
-			['isReadable', ['dir'], 'dir', ''],
1851
-			['isUpdatable', ['dir'], 'dir', ''],
1852
-			['isDeletable', ['dir'], 'dir', ''],
1853
-			['isSharable', ['dir'], 'dir', ''],
1854
-			['file_exists', ['dir'], 'dir', ''],
1855
-			[
1856
-				'filemtime',
1857
-				['dir'],
1858
-				'dir',
1859
-				'',
1860
-				ILockingProvider::LOCK_SHARED,
1861
-				ILockingProvider::LOCK_SHARED,
1862
-				ILockingProvider::LOCK_SHARED,
1863
-				null,
1864
-				false,
1865
-			],
1866
-		];
1867
-	}
1868
-
1869
-	/**
1870
-	 * Test whether locks are set before and after the operation
1871
-	 *
1872
-	 *
1873
-	 * @param string $operation operation name on the view
1874
-	 * @param array $operationArgs arguments for the operation
1875
-	 * @param string $lockedPath path of the locked item to check
1876
-	 * @param string $hookType hook type
1877
-	 * @param ?int $expectedLockBefore expected lock during pre hooks
1878
-	 * @param ?int $expectedLockDuring expected lock during operation
1879
-	 * @param ?int $expectedLockAfter expected lock during post hooks
1880
-	 * @param ?int $expectedStrayLock expected lock after returning, should
1881
-	 *                                be null (unlock) for most operations
1882
-	 */
1883
-	#[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
1884
-	public function testLockBasicOperation(
1885
-		string $operation,
1886
-		array $operationArgs,
1887
-		string $lockedPath,
1888
-		string $hookType,
1889
-		?int $expectedLockBefore = ILockingProvider::LOCK_SHARED,
1890
-		?int $expectedLockDuring = ILockingProvider::LOCK_SHARED,
1891
-		?int $expectedLockAfter = ILockingProvider::LOCK_SHARED,
1892
-		?int $expectedStrayLock = null,
1893
-		mixed $returnValue = true,
1894
-	): void {
1895
-		$view = new View('/' . $this->user . '/files/');
1896
-
1897
-		/** @var Temporary&MockObject $storage */
1898
-		$storage = $this->getMockBuilder(Temporary::class)
1899
-			->onlyMethods([$operation])
1900
-			->getMock();
1901
-
1902
-		/* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
1903
-		Server::get(ITrashManager::class)->pauseTrash();
1904
-		/* Same thing with encryption wrapper */
1905
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
1906
-
1907
-		Filesystem::mount($storage, [], $this->user . '/');
1908
-
1909
-		// work directly on disk because mkdir might be mocked
1910
-		$realPath = $storage->getSourcePath('');
1911
-		mkdir($realPath . '/files');
1912
-		mkdir($realPath . '/files/dir');
1913
-		file_put_contents($realPath . '/files/test.txt', 'blah');
1914
-		$storage->getScanner()->scan('files');
1915
-
1916
-		$storage->expects($this->once())
1917
-			->method($operation)
1918
-			->willReturnCallback(
1919
-				function () use ($view, $lockedPath, &$lockTypeDuring, $returnValue) {
1920
-					$lockTypeDuring = $this->getFileLockType($view, $lockedPath);
1921
-
1922
-					return $returnValue;
1923
-				}
1924
-			);
1925
-
1926
-		$this->assertNull($this->getFileLockType($view, $lockedPath), 'File not locked before operation');
1927
-
1928
-		$this->connectMockHooks($hookType, $view, $lockedPath, $lockTypePre, $lockTypePost);
1929
-
1930
-		// do operation
1931
-		call_user_func_array([$view, $operation], $operationArgs);
1932
-
1933
-		if ($hookType !== '') {
1934
-			$this->assertEquals($expectedLockBefore, $lockTypePre, 'File locked properly during pre-hook');
1935
-			$this->assertEquals($expectedLockAfter, $lockTypePost, 'File locked properly during post-hook');
1936
-			$this->assertEquals($expectedLockDuring, $lockTypeDuring, 'File locked properly during operation');
1937
-		} else {
1938
-			$this->assertNull($lockTypeDuring, 'File not locked during operation');
1939
-		}
1940
-
1941
-		$this->assertEquals($expectedStrayLock, $this->getFileLockType($view, $lockedPath));
1942
-
1943
-		/* Resume trash to avoid side effects */
1944
-		Server::get(ITrashManager::class)->resumeTrash();
1945
-	}
1946
-
1947
-	/**
1948
-	 * Test locks for file_put_content with stream.
1949
-	 * This code path uses $storage->fopen instead
1950
-	 */
1951
-	public function testLockFilePutContentWithStream(): void {
1952
-		$view = new View('/' . $this->user . '/files/');
1953
-
1954
-		$path = 'test_file_put_contents.txt';
1955
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1956
-		$storage = $this->getMockBuilder(Temporary::class)
1957
-			->onlyMethods(['fopen'])
1958
-			->getMock();
1959
-
1960
-		Filesystem::mount($storage, [], $this->user . '/');
1961
-		$storage->mkdir('files');
1962
-
1963
-		$storage->expects($this->once())
1964
-			->method('fopen')
1965
-			->willReturnCallback(
1966
-				function () use ($view, $path, &$lockTypeDuring) {
1967
-					$lockTypeDuring = $this->getFileLockType($view, $path);
1968
-
1969
-					return fopen('php://temp', 'r+');
1970
-				}
1971
-			);
1972
-
1973
-		$this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost);
1974
-
1975
-		$this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation');
1976
-
1977
-		// do operation
1978
-		$view->file_put_contents($path, fopen('php://temp', 'r+'));
1979
-
1980
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook');
1981
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePost, 'File locked properly during post-hook');
1982
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation');
1983
-
1984
-		$this->assertNull($this->getFileLockType($view, $path));
1985
-	}
1986
-
1987
-	/**
1988
-	 * Test locks for fopen with fclose at the end
1989
-	 */
1990
-	public function testLockFopen(): void {
1991
-		$view = new View('/' . $this->user . '/files/');
1992
-
1993
-		$path = 'test_file_put_contents.txt';
1994
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1995
-		$storage = $this->getMockBuilder(Temporary::class)
1996
-			->onlyMethods(['fopen'])
1997
-			->getMock();
1998
-
1999
-		Filesystem::mount($storage, [], $this->user . '/');
2000
-		$storage->mkdir('files');
2001
-
2002
-		$storage->expects($this->once())
2003
-			->method('fopen')
2004
-			->willReturnCallback(
2005
-				function () use ($view, $path, &$lockTypeDuring) {
2006
-					$lockTypeDuring = $this->getFileLockType($view, $path);
2007
-
2008
-					return fopen('php://temp', 'r+');
2009
-				}
2010
-			);
2011
-
2012
-		$this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost);
2013
-
2014
-		$this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation');
2015
-
2016
-		// do operation
2017
-		$res = $view->fopen($path, 'w');
2018
-
2019
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook');
2020
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation');
2021
-		$this->assertNull($lockTypePost, 'No post hook, no lock check possible');
2022
-
2023
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File still locked after fopen');
2024
-
2025
-		fclose($res);
2026
-
2027
-		$this->assertNull($this->getFileLockType($view, $path), 'File unlocked after fclose');
2028
-	}
2029
-
2030
-	/**
2031
-	 * Test locks for fopen with fclose at the end
2032
-	 *
2033
-	 *
2034
-	 * @param string $operation operation name on the view
2035
-	 * @param array $operationArgs arguments for the operation
2036
-	 * @param string $path path of the locked item to check
2037
-	 */
2038
-	#[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
2039
-	public function testLockBasicOperationUnlocksAfterException(
2040
-		$operation,
2041
-		$operationArgs,
2042
-		$path,
2043
-	): void {
2044
-		if ($operation === 'touch') {
2045
-			$this->markTestSkipped('touch handles storage exceptions internally');
2046
-		}
2047
-		$view = new View('/' . $this->user . '/files/');
2048
-
2049
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2050
-		$storage = $this->getMockBuilder(Temporary::class)
2051
-			->onlyMethods([$operation])
2052
-			->getMock();
2053
-
2054
-		/* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
2055
-		Server::get(ITrashManager::class)->pauseTrash();
2056
-		/* Same thing with encryption wrapper */
2057
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2058
-
2059
-		Filesystem::mount($storage, [], $this->user . '/');
2060
-
2061
-		// work directly on disk because mkdir might be mocked
2062
-		$realPath = $storage->getSourcePath('');
2063
-		mkdir($realPath . '/files');
2064
-		mkdir($realPath . '/files/dir');
2065
-		file_put_contents($realPath . '/files/test.txt', 'blah');
2066
-		$storage->getScanner()->scan('files');
2067
-
2068
-		$storage->expects($this->once())
2069
-			->method($operation)
2070
-			->willReturnCallback(
2071
-				function (): void {
2072
-					throw new \Exception('Simulated exception');
2073
-				}
2074
-			);
2075
-
2076
-		$thrown = false;
2077
-		try {
2078
-			call_user_func_array([$view, $operation], $operationArgs);
2079
-		} catch (\Exception $e) {
2080
-			$thrown = true;
2081
-			$this->assertEquals('Simulated exception', $e->getMessage());
2082
-		}
2083
-		$this->assertTrue($thrown, 'Exception was rethrown');
2084
-		$this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception');
2085
-
2086
-		/* Resume trash to avoid side effects */
2087
-		Server::get(ITrashManager::class)->resumeTrash();
2088
-	}
2089
-
2090
-	public function testLockBasicOperationUnlocksAfterLockException(): void {
2091
-		$view = new View('/' . $this->user . '/files/');
2092
-
2093
-		$storage = new Temporary([]);
2094
-
2095
-		Filesystem::mount($storage, [], $this->user . '/');
2096
-
2097
-		$storage->mkdir('files');
2098
-		$storage->mkdir('files/dir');
2099
-		$storage->file_put_contents('files/test.txt', 'blah');
2100
-		$storage->getScanner()->scan('files');
2101
-
2102
-		// get a shared lock
2103
-		$handle = $view->fopen('test.txt', 'r');
2104
-
2105
-		$thrown = false;
2106
-		try {
2107
-			// try (and fail) to get a write lock
2108
-			$view->unlink('test.txt');
2109
-		} catch (\Exception $e) {
2110
-			$thrown = true;
2111
-			$this->assertInstanceOf(LockedException::class, $e);
2112
-		}
2113
-		$this->assertTrue($thrown, 'Exception was rethrown');
2114
-
2115
-		// clean shared lock
2116
-		fclose($handle);
2117
-
2118
-		$this->assertNull($this->getFileLockType($view, 'test.txt'), 'File got unlocked');
2119
-	}
2120
-
2121
-	/**
2122
-	 * Test locks for fopen with fclose at the end
2123
-	 *
2124
-	 *
2125
-	 * @param string $operation operation name on the view
2126
-	 * @param array $operationArgs arguments for the operation
2127
-	 * @param string $path path of the locked item to check
2128
-	 * @param string $hookType hook type
2129
-	 */
2130
-	#[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
2131
-	public function testLockBasicOperationUnlocksAfterCancelledHook(
2132
-		$operation,
2133
-		$operationArgs,
2134
-		$path,
2135
-		$hookType,
2136
-	): void {
2137
-		$view = new View('/' . $this->user . '/files/');
2138
-
2139
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2140
-		$storage = $this->getMockBuilder(Temporary::class)
2141
-			->onlyMethods([$operation])
2142
-			->getMock();
2143
-
2144
-		Filesystem::mount($storage, [], $this->user . '/');
2145
-		$storage->mkdir('files');
2146
-
2147
-		Util::connectHook(
2148
-			Filesystem::CLASSNAME,
2149
-			$hookType,
2150
-			HookHelper::class,
2151
-			'cancellingCallback'
2152
-		);
2153
-
2154
-		call_user_func_array([$view, $operation], $operationArgs);
2155
-
2156
-		$this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception');
2157
-	}
2158
-
2159
-	public static function lockFileRenameOrCopyDataProvider(): array {
2160
-		return [
2161
-			['rename', ILockingProvider::LOCK_EXCLUSIVE],
2162
-			['copy', ILockingProvider::LOCK_SHARED],
2163
-		];
2164
-	}
2165
-
2166
-	/**
2167
-	 * Test locks for rename or copy operation
2168
-	 *
2169
-	 *
2170
-	 * @param string $operation operation to be done on the view
2171
-	 * @param int $expectedLockTypeSourceDuring expected lock type on source file during
2172
-	 *                                          the operation
2173
-	 */
2174
-	#[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyDataProvider')]
2175
-	public function testLockFileRename($operation, $expectedLockTypeSourceDuring): void {
2176
-		$view = new View('/' . $this->user . '/files/');
2177
-
2178
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2179
-		$storage = $this->getMockBuilder(Temporary::class)
2180
-			->onlyMethods([$operation, 'getMetaData', 'filemtime'])
2181
-			->getMock();
2182
-
2183
-		$storage->expects($this->any())
2184
-			->method('getMetaData')
2185
-			->willReturn([
2186
-				'mtime' => 1885434487,
2187
-				'etag' => '',
2188
-				'mimetype' => 'text/plain',
2189
-				'permissions' => Constants::PERMISSION_ALL,
2190
-				'size' => 3
2191
-			]);
2192
-		$storage->expects($this->any())
2193
-			->method('filemtime')
2194
-			->willReturn(123456789);
2195
-
2196
-		$sourcePath = 'original.txt';
2197
-		$targetPath = 'target.txt';
2198
-
2199
-		/* Disable encryption wrapper to avoid it intercepting mocked call */
2200
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2201
-
2202
-		Filesystem::mount($storage, [], $this->user . '/');
2203
-		$storage->mkdir('files');
2204
-		$view->file_put_contents($sourcePath, 'meh');
2205
-
2206
-		$storage->expects($this->once())
2207
-			->method($operation)
2208
-			->willReturnCallback(
2209
-				function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2210
-					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2211
-					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2212
-
2213
-					return true;
2214
-				}
2215
-			);
2216
-
2217
-		$this->connectMockHooks($operation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2218
-		$this->connectMockHooks($operation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2219
-
2220
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2221
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2222
-
2223
-		$view->$operation($sourcePath, $targetPath);
2224
-
2225
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook');
2226
-		$this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation');
2227
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook');
2228
-
2229
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook');
2230
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation');
2231
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook');
2232
-
2233
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2234
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2235
-	}
2236
-
2237
-	/**
2238
-	 * simulate a failed copy operation.
2239
-	 * We expect that we catch the exception, free the lock and re-throw it.
2240
-	 *
2241
-	 */
2242
-	public function testLockFileCopyException(): void {
2243
-		$this->expectException(\Exception::class);
2244
-
2245
-		$view = new View('/' . $this->user . '/files/');
2246
-
2247
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2248
-		$storage = $this->getMockBuilder(Temporary::class)
2249
-			->onlyMethods(['copy'])
2250
-			->getMock();
2251
-
2252
-		$sourcePath = 'original.txt';
2253
-		$targetPath = 'target.txt';
2254
-
2255
-		/* Disable encryption wrapper to avoid it intercepting mocked call */
2256
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2257
-
2258
-		Filesystem::mount($storage, [], $this->user . '/');
2259
-		$storage->mkdir('files');
2260
-		$view->file_put_contents($sourcePath, 'meh');
2261
-
2262
-		$storage->expects($this->once())
2263
-			->method('copy')
2264
-			->willReturnCallback(
2265
-				function (): void {
2266
-					throw new \Exception();
2267
-				}
2268
-			);
2269
-
2270
-		$this->connectMockHooks('copy', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2271
-		$this->connectMockHooks('copy', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2272
-
2273
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2274
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2275
-
2276
-		try {
2277
-			$view->copy($sourcePath, $targetPath);
2278
-		} catch (\Exception $e) {
2279
-			$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2280
-			$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2281
-			throw $e;
2282
-		}
2283
-	}
2284
-
2285
-	/**
2286
-	 * Test rename operation: unlock first path when second path was locked
2287
-	 */
2288
-	public function testLockFileRenameUnlockOnException(): void {
2289
-		self::loginAsUser('test');
2290
-
2291
-		$view = new View('/' . $this->user . '/files/');
2292
-
2293
-		$sourcePath = 'original.txt';
2294
-		$targetPath = 'target.txt';
2295
-		$view->file_put_contents($sourcePath, 'meh');
2296
-
2297
-		// simulate that the target path is already locked
2298
-		$view->lockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
2299
-
2300
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2301
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file is locked before operation');
2302
-
2303
-		$thrown = false;
2304
-		try {
2305
-			$view->rename($sourcePath, $targetPath);
2306
-		} catch (LockedException $e) {
2307
-			$thrown = true;
2308
-		}
2309
-
2310
-		$this->assertTrue($thrown, 'LockedException thrown');
2311
-
2312
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2313
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file still locked after operation');
2314
-
2315
-		$view->unlockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
2316
-	}
2317
-
2318
-	/**
2319
-	 * Test rename operation: unlock first path when second path was locked
2320
-	 */
2321
-	public function testGetOwner(): void {
2322
-		self::loginAsUser('test');
2323
-
2324
-		$view = new View('/test/files/');
2325
-
2326
-		$path = 'foo.txt';
2327
-		$view->file_put_contents($path, 'meh');
2328
-
2329
-		$this->assertEquals('test', $view->getFileInfo($path)->getOwner()->getUID());
2330
-
2331
-		$folderInfo = $view->getDirectoryContent('');
2332
-		$folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2333
-			return $info->getName() === 'foo.txt';
2334
-		}));
2335
-
2336
-		$this->assertEquals('test', $folderInfo[0]->getOwner()->getUID());
2337
-
2338
-		$subStorage = new Temporary();
2339
-		Filesystem::mount($subStorage, [], '/test/files/asd');
2340
-
2341
-		$folderInfo = $view->getDirectoryContent('');
2342
-		$folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2343
-			return $info->getName() === 'asd';
2344
-		}));
2345
-
2346
-		$this->assertEquals('test', $folderInfo[0]->getOwner()->getUID());
2347
-	}
2348
-
2349
-	public static function lockFileRenameOrCopyCrossStorageDataProvider(): array {
2350
-		return [
2351
-			['rename', 'moveFromStorage', ILockingProvider::LOCK_EXCLUSIVE],
2352
-			['copy', 'copyFromStorage', ILockingProvider::LOCK_SHARED],
2353
-		];
2354
-	}
2355
-
2356
-	/**
2357
-	 * Test locks for rename or copy operation cross-storage
2358
-	 *
2359
-	 *
2360
-	 * @param string $viewOperation operation to be done on the view
2361
-	 * @param string $storageOperation operation to be mocked on the storage
2362
-	 * @param int $expectedLockTypeSourceDuring expected lock type on source file during
2363
-	 *                                          the operation
2364
-	 */
2365
-	#[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyCrossStorageDataProvider')]
2366
-	public function testLockFileRenameCrossStorage($viewOperation, $storageOperation, $expectedLockTypeSourceDuring): void {
2367
-		$view = new View('/' . $this->user . '/files/');
2368
-
2369
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2370
-		$storage = $this->getMockBuilder(Temporary::class)
2371
-			->onlyMethods([$storageOperation])
2372
-			->getMock();
2373
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage2 */
2374
-		$storage2 = $this->getMockBuilder(Temporary::class)
2375
-			->onlyMethods([$storageOperation, 'getMetaData', 'filemtime'])
2376
-			->getMock();
2377
-
2378
-		$storage2->expects($this->any())
2379
-			->method('getMetaData')
2380
-			->willReturn([
2381
-				'mtime' => 1885434487,
2382
-				'etag' => '',
2383
-				'mimetype' => 'text/plain',
2384
-				'permissions' => Constants::PERMISSION_ALL,
2385
-				'size' => 3
2386
-			]);
2387
-		$storage2->expects($this->any())
2388
-			->method('filemtime')
2389
-			->willReturn(123456789);
2390
-
2391
-		$sourcePath = 'original.txt';
2392
-		$targetPath = 'substorage/target.txt';
2393
-
2394
-		/* Disable encryption wrapper to avoid it intercepting mocked call */
2395
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2396
-
2397
-		Filesystem::mount($storage, [], $this->user . '/');
2398
-		Filesystem::mount($storage2, [], $this->user . '/files/substorage');
2399
-		$storage->mkdir('files');
2400
-		$view->file_put_contents($sourcePath, 'meh');
2401
-		$storage2->getUpdater()->update('');
2402
-
2403
-		$storage->expects($this->never())
2404
-			->method($storageOperation);
2405
-		$storage2->expects($this->once())
2406
-			->method($storageOperation)
2407
-			->willReturnCallback(
2408
-				function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2409
-					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2410
-					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2411
-
2412
-					return true;
2413
-				}
2414
-			);
2415
-
2416
-		$this->connectMockHooks($viewOperation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2417
-		$this->connectMockHooks($viewOperation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2418
-
2419
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2420
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2421
-
2422
-		$view->$viewOperation($sourcePath, $targetPath);
2423
-
2424
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook');
2425
-		$this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation');
2426
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook');
2427
-
2428
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook');
2429
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation');
2430
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook');
2431
-
2432
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2433
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2434
-	}
2435
-
2436
-	/**
2437
-	 * Test locks when moving a mount point
2438
-	 */
2439
-	public function testLockMoveMountPoint(): void {
2440
-		self::loginAsUser('test');
2441
-
2442
-		[$mount] = $this->createTestMovableMountPoints([
2443
-			$this->user . '/files/substorage',
2444
-		]);
2445
-
2446
-		$view = new View('/' . $this->user . '/files/');
2447
-		$view->mkdir('subdir');
2448
-
2449
-		$sourcePath = 'substorage';
2450
-		$targetPath = 'subdir/substorage_moved';
2451
-
2452
-		$mount->expects($this->once())
2453
-			->method('moveMount')
2454
-			->willReturnCallback(
2455
-				function ($target) use ($mount, $view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring, &$lockTypeSharedRootDuring) {
2456
-					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath, true);
2457
-					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath, true);
2458
-
2459
-					$lockTypeSharedRootDuring = $this->getFileLockType($view, $sourcePath, false);
2460
-
2461
-					$mount->setMountPoint($target);
2462
-
2463
-					return true;
2464
-				}
2465
-			);
2466
-
2467
-		$this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost, true);
2468
-		$this->connectMockHooks('rename', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost, true);
2469
-		// in pre-hook, mount point is still on $sourcePath
2470
-		$this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSharedRootPre, $dummy, false);
2471
-		// in post-hook, mount point is now on $targetPath
2472
-		$this->connectMockHooks('rename', $view, $targetPath, $dummy, $lockTypeSharedRootPost, false);
2473
-
2474
-		$this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked before operation');
2475
-		$this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked before operation');
2476
-		$this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked before operation');
2477
-
2478
-		$view->rename($sourcePath, $targetPath);
2479
-
2480
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source path locked properly during pre-hook');
2481
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeSourceDuring, 'Source path locked properly during operation');
2482
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source path locked properly during post-hook');
2483
-
2484
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target path locked properly during pre-hook');
2485
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target path locked properly during operation');
2486
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target path locked properly during post-hook');
2487
-
2488
-		$this->assertNull($lockTypeSharedRootPre, 'Shared storage root not locked during pre-hook');
2489
-		$this->assertNull($lockTypeSharedRootDuring, 'Shared storage root not locked during move');
2490
-		$this->assertNull($lockTypeSharedRootPost, 'Shared storage root not locked during post-hook');
2491
-
2492
-		$this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked after operation');
2493
-		$this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked after operation');
2494
-		$this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked after operation');
2495
-	}
2496
-
2497
-	/**
2498
-	 * Connect hook callbacks for hook type
2499
-	 *
2500
-	 * @param string $hookType hook type or null for none
2501
-	 * @param View $view view to check the lock on
2502
-	 * @param string $path path for which to check the lock
2503
-	 * @param int $lockTypePre variable to receive lock type that was active in the pre-hook
2504
-	 * @param int $lockTypePost variable to receive lock type that was active in the post-hook
2505
-	 * @param bool $onMountPoint true to check the mount point instead of the
2506
-	 *                           mounted storage
2507
-	 */
2508
-	private function connectMockHooks($hookType, $view, $path, &$lockTypePre, &$lockTypePost, $onMountPoint = false) {
2509
-		if ($hookType === null) {
2510
-			return;
2511
-		}
2512
-
2513
-		$eventHandler = $this->getMockBuilder(TestEventHandler::class)
2514
-			->onlyMethods(['preCallback', 'postCallback'])
2515
-			->getMock();
2516
-
2517
-		$eventHandler->expects($this->any())
2518
-			->method('preCallback')
2519
-			->willReturnCallback(
2520
-				function () use ($view, $path, $onMountPoint, &$lockTypePre): void {
2521
-					$lockTypePre = $this->getFileLockType($view, $path, $onMountPoint);
2522
-				}
2523
-			);
2524
-		$eventHandler->expects($this->any())
2525
-			->method('postCallback')
2526
-			->willReturnCallback(
2527
-				function () use ($view, $path, $onMountPoint, &$lockTypePost): void {
2528
-					$lockTypePost = $this->getFileLockType($view, $path, $onMountPoint);
2529
-				}
2530
-			);
2531
-
2532
-		if ($hookType !== '') {
2533
-			Util::connectHook(
2534
-				Filesystem::CLASSNAME,
2535
-				$hookType,
2536
-				$eventHandler,
2537
-				'preCallback'
2538
-			);
2539
-			Util::connectHook(
2540
-				Filesystem::CLASSNAME,
2541
-				'post_' . $hookType,
2542
-				$eventHandler,
2543
-				'postCallback'
2544
-			);
2545
-		}
2546
-	}
2547
-
2548
-	/**
2549
-	 * Returns the file lock type
2550
-	 *
2551
-	 * @param View $view view
2552
-	 * @param string $path path
2553
-	 * @param bool $onMountPoint true to check the mount point instead of the
2554
-	 *                           mounted storage
2555
-	 *
2556
-	 * @return int lock type or null if file was not locked
2557
-	 */
2558
-	private function getFileLockType(View $view, $path, $onMountPoint = false) {
2559
-		if ($this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE, $onMountPoint)) {
2560
-			return ILockingProvider::LOCK_EXCLUSIVE;
2561
-		} elseif ($this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED, $onMountPoint)) {
2562
-			return ILockingProvider::LOCK_SHARED;
2563
-		}
2564
-		return null;
2565
-	}
2566
-
2567
-
2568
-	public function testRemoveMoveableMountPoint(): void {
2569
-		$mountPoint = '/' . $this->user . '/files/mount/';
2570
-
2571
-		// Mock the mount point
2572
-		/** @var TestMoveableMountPoint|\PHPUnit\Framework\MockObject\MockObject $mount */
2573
-		$mount = $this->createMock(TestMoveableMountPoint::class);
2574
-		$mount->expects($this->once())
2575
-			->method('getMountPoint')
2576
-			->willReturn($mountPoint);
2577
-		$mount->expects($this->once())
2578
-			->method('removeMount')
2579
-			->willReturn('foo');
2580
-		$mount->expects($this->any())
2581
-			->method('getInternalPath')
2582
-			->willReturn('');
2583
-
2584
-		// Register mount
2585
-		Filesystem::getMountManager()->addMount($mount);
2586
-
2587
-		// Listen for events
2588
-		$eventHandler = $this->getMockBuilder(TestEventHandler::class)
2589
-			->onlyMethods(['umount', 'post_umount'])
2590
-			->getMock();
2591
-		$eventHandler->expects($this->once())
2592
-			->method('umount')
2593
-			->with([Filesystem::signal_param_path => '/mount']);
2594
-		$eventHandler->expects($this->once())
2595
-			->method('post_umount')
2596
-			->with([Filesystem::signal_param_path => '/mount']);
2597
-		Util::connectHook(
2598
-			Filesystem::CLASSNAME,
2599
-			'umount',
2600
-			$eventHandler,
2601
-			'umount'
2602
-		);
2603
-		Util::connectHook(
2604
-			Filesystem::CLASSNAME,
2605
-			'post_umount',
2606
-			$eventHandler,
2607
-			'post_umount'
2608
-		);
2609
-
2610
-		//Delete the mountpoint
2611
-		$view = new View('/' . $this->user . '/files');
2612
-		$this->assertEquals('foo', $view->rmdir('mount'));
2613
-	}
2614
-
2615
-	public static function mimeFilterProvider(): array {
2616
-		return [
2617
-			[null, ['test1.txt', 'test2.txt', 'test3.md', 'test4.png']],
2618
-			['text/plain', ['test1.txt', 'test2.txt']],
2619
-			['text/markdown', ['test3.md']],
2620
-			['text', ['test1.txt', 'test2.txt', 'test3.md']],
2621
-		];
2622
-	}
2623
-
2624
-	/**
2625
-	 * @param string $filter
2626
-	 * @param string[] $expected
2627
-	 */
2628
-	#[\PHPUnit\Framework\Attributes\DataProvider('mimeFilterProvider')]
2629
-	public function testGetDirectoryContentMimeFilter($filter, $expected): void {
2630
-		$storage1 = new Temporary();
2631
-		$root = self::getUniqueID('/');
2632
-		Filesystem::mount($storage1, [], $root . '/');
2633
-		$view = new View($root);
2634
-
2635
-		$view->file_put_contents('test1.txt', 'asd');
2636
-		$view->file_put_contents('test2.txt', 'asd');
2637
-		$view->file_put_contents('test3.md', 'asd');
2638
-		$view->file_put_contents('test4.png', '');
2639
-
2640
-		$content = $view->getDirectoryContent('', $filter);
2641
-
2642
-		$files = array_map(function (FileInfo $info) {
2643
-			return $info->getName();
2644
-		}, $content);
2645
-		sort($files);
2646
-
2647
-		$this->assertEquals($expected, $files);
2648
-	}
2649
-
2650
-	public function testFilePutContentsClearsChecksum(): void {
2651
-		$storage = new Temporary([]);
2652
-		$scanner = $storage->getScanner();
2653
-		$storage->file_put_contents('foo.txt', 'bar');
2654
-		Filesystem::mount($storage, [], '/test/');
2655
-		$scanner->scan('');
2656
-
2657
-		$view = new View('/test/foo.txt');
2658
-		$view->putFileInfo('.', ['checksum' => '42']);
2659
-
2660
-		$this->assertEquals('bar', $view->file_get_contents(''));
2661
-		$fh = tmpfile();
2662
-		fwrite($fh, 'fooo');
2663
-		rewind($fh);
2664
-		clearstatcache();
2665
-		$view->file_put_contents('', $fh);
2666
-		$this->assertEquals('fooo', $view->file_get_contents(''));
2667
-		$data = $view->getFileInfo('.');
2668
-		$this->assertEquals('', $data->getChecksum());
2669
-	}
2670
-
2671
-	public function testDeleteGhostFile(): void {
2672
-		$storage = new Temporary([]);
2673
-		$scanner = $storage->getScanner();
2674
-		$cache = $storage->getCache();
2675
-		$storage->file_put_contents('foo.txt', 'bar');
2676
-		Filesystem::mount($storage, [], '/test/');
2677
-		$scanner->scan('');
2678
-
2679
-		$storage->unlink('foo.txt');
2680
-
2681
-		$this->assertTrue($cache->inCache('foo.txt'));
2682
-
2683
-		$view = new View('/test');
2684
-		$rootInfo = $view->getFileInfo('');
2685
-		$this->assertEquals(3, $rootInfo->getSize());
2686
-		$view->unlink('foo.txt');
2687
-		$newInfo = $view->getFileInfo('');
2688
-
2689
-		$this->assertFalse($cache->inCache('foo.txt'));
2690
-		$this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag());
2691
-		$this->assertEquals(0, $newInfo->getSize());
2692
-	}
2693
-
2694
-	public function testDeleteGhostFolder(): void {
2695
-		$storage = new Temporary([]);
2696
-		$scanner = $storage->getScanner();
2697
-		$cache = $storage->getCache();
2698
-		$storage->mkdir('foo');
2699
-		$storage->file_put_contents('foo/foo.txt', 'bar');
2700
-		Filesystem::mount($storage, [], '/test/');
2701
-		$scanner->scan('');
2702
-
2703
-		$storage->rmdir('foo');
2704
-
2705
-		$this->assertTrue($cache->inCache('foo'));
2706
-		$this->assertTrue($cache->inCache('foo/foo.txt'));
2707
-
2708
-		$view = new View('/test');
2709
-		$rootInfo = $view->getFileInfo('');
2710
-		$this->assertEquals(3, $rootInfo->getSize());
2711
-		$view->rmdir('foo');
2712
-		$newInfo = $view->getFileInfo('');
2713
-
2714
-		$this->assertFalse($cache->inCache('foo'));
2715
-		$this->assertFalse($cache->inCache('foo/foo.txt'));
2716
-		$this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag());
2717
-		$this->assertEquals(0, $newInfo->getSize());
2718
-	}
2719
-
2720
-	public function testCreateParentDirectories(): void {
2721
-		$view = $this->getMockBuilder(View::class)
2722
-			->disableOriginalConstructor()
2723
-			->onlyMethods([
2724
-				'is_file',
2725
-				'file_exists',
2726
-				'mkdir',
2727
-			])
2728
-			->getMock();
2729
-
2730
-		$view->expects($this->exactly(3))
2731
-			->method('is_file')
2732
-			->willReturnMap([
2733
-				['/new', false],
2734
-				['/new/folder', false],
2735
-				['/new/folder/structure', false],
2736
-			]);
2737
-		$view->expects($this->exactly(3))
2738
-			->method('file_exists')
2739
-			->willReturnMap([
2740
-				['/new', true],
2741
-				['/new/folder', false],
2742
-				['/new/folder/structure', false],
2743
-			]);
2744
-
2745
-		$calls = ['/new/folder', '/new/folder/structure'];
2746
-		$view->expects($this->exactly(2))
2747
-			->method('mkdir')
2748
-			->willReturnCallback(function ($dir) use (&$calls): void {
2749
-				$expected = array_shift($calls);
2750
-				$this->assertEquals($expected, $dir);
2751
-			});
2752
-
2753
-		$this->assertTrue(self::invokePrivate($view, 'createParentDirectories', ['/new/folder/structure']));
2754
-	}
2755
-
2756
-	public function testCreateParentDirectoriesWithExistingFile(): void {
2757
-		$view = $this->getMockBuilder(View::class)
2758
-			->disableOriginalConstructor()
2759
-			->onlyMethods([
2760
-				'is_file',
2761
-				'file_exists',
2762
-				'mkdir',
2763
-			])
2764
-			->getMock();
2765
-
2766
-		$view
2767
-			->expects($this->once())
2768
-			->method('is_file')
2769
-			->with('/file.txt')
2770
-			->willReturn(true);
2771
-		$this->assertFalse(self::invokePrivate($view, 'createParentDirectories', ['/file.txt/folder/structure']));
2772
-	}
2773
-
2774
-	public function testCacheExtension(): void {
2775
-		$storage = new Temporary([]);
2776
-		$scanner = $storage->getScanner();
2777
-		$storage->file_put_contents('foo.txt', 'bar');
2778
-		$scanner->scan('');
2779
-
2780
-		Filesystem::mount($storage, [], '/test/');
2781
-		$view = new View('/test');
2782
-
2783
-		$info = $view->getFileInfo('/foo.txt');
2784
-		$this->assertEquals(0, $info->getUploadTime());
2785
-		$this->assertEquals(0, $info->getCreationTime());
2786
-
2787
-		$view->putFileInfo('/foo.txt', ['upload_time' => 25]);
2788
-
2789
-		$info = $view->getFileInfo('/foo.txt');
2790
-		$this->assertEquals(25, $info->getUploadTime());
2791
-		$this->assertEquals(0, $info->getCreationTime());
2792
-	}
2793
-
2794
-	public function testFopenGone(): void {
2795
-		$storage = new Temporary([]);
2796
-		$scanner = $storage->getScanner();
2797
-		$storage->file_put_contents('foo.txt', 'bar');
2798
-		$scanner->scan('');
2799
-		$cache = $storage->getCache();
2800
-
2801
-		Filesystem::mount($storage, [], '/test/');
2802
-		$view = new View('/test');
2803
-
2804
-		$storage->unlink('foo.txt');
2805
-
2806
-		$this->assertTrue($cache->inCache('foo.txt'));
2807
-
2808
-		$this->assertFalse($view->fopen('foo.txt', 'r'));
2809
-
2810
-		$this->assertFalse($cache->inCache('foo.txt'));
2811
-	}
2812
-
2813
-	public function testMountpointParentsCreated(): void {
2814
-		$storage1 = $this->getTestStorage();
2815
-		Filesystem::mount($storage1, [], '/');
2816
-
2817
-		$storage2 = $this->getTestStorage();
2818
-		Filesystem::mount($storage2, [], '/A/B/C');
2819
-
2820
-		$rootView = new View('');
2821
-
2822
-		$folderData = $rootView->getDirectoryContent('/');
2823
-		$this->assertCount(4, $folderData);
2824
-		$this->assertEquals('folder', $folderData[0]['name']);
2825
-		$this->assertEquals('foo.png', $folderData[1]['name']);
2826
-		$this->assertEquals('foo.txt', $folderData[2]['name']);
2827
-		$this->assertEquals('A', $folderData[3]['name']);
2828
-
2829
-		$folderData = $rootView->getDirectoryContent('/A');
2830
-		$this->assertCount(1, $folderData);
2831
-		$this->assertEquals('B', $folderData[0]['name']);
2832
-
2833
-		$folderData = $rootView->getDirectoryContent('/A/B');
2834
-		$this->assertCount(1, $folderData);
2835
-		$this->assertEquals('C', $folderData[0]['name']);
2836
-
2837
-		$folderData = $rootView->getDirectoryContent('/A/B/C');
2838
-		$this->assertCount(3, $folderData);
2839
-		$this->assertEquals('folder', $folderData[0]['name']);
2840
-		$this->assertEquals('foo.png', $folderData[1]['name']);
2841
-		$this->assertEquals('foo.txt', $folderData[2]['name']);
2842
-	}
2843
-
2844
-	public function testCopyPreservesContent() {
2845
-		$viewUser1 = new View('/' . 'userId' . '/files');
2846
-		$viewUser1->mkdir('');
2847
-		$viewUser1->file_put_contents('foo.txt', 'foo');
2848
-		$viewUser1->copy('foo.txt', 'bar.txt');
2849
-		$this->assertEquals('foo', $viewUser1->file_get_contents('bar.txt'));
2850
-	}
809
+        $folderName = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123456789';
810
+        $tmpdirLength = strlen(Server::get(ITempManager::class)->getTemporaryFolder());
811
+        $depth = ((4000 - $tmpdirLength) / 57);
812
+
813
+        foreach (range(0, $depth - 1) as $i) {
814
+            $longPath .= $ds . $folderName;
815
+            $result = $rootView->mkdir($longPath);
816
+            $this->assertTrue($result, "mkdir failed on $i - path length: " . strlen($longPath));
817
+
818
+            $result = $rootView->file_put_contents($longPath . "{$ds}test.txt", 'lorem');
819
+            $this->assertEquals(5, $result, "file_put_contents failed on $i");
820
+
821
+            $this->assertTrue($rootView->file_exists($longPath));
822
+            $this->assertTrue($rootView->file_exists($longPath . "{$ds}test.txt"));
823
+        }
824
+
825
+        $cache = $storage->getCache();
826
+        $scanner = $storage->getScanner();
827
+        $scanner->scan('');
828
+
829
+        $longPath = $folderName;
830
+        foreach (range(0, $depth - 1) as $i) {
831
+            $cachedFolder = $cache->get($longPath);
832
+            $this->assertTrue(is_array($cachedFolder), "No cache entry for folder at $i");
833
+            $this->assertEquals($folderName, $cachedFolder['name'], "Wrong cache entry for folder at $i");
834
+
835
+            $cachedFile = $cache->get($longPath . '/test.txt');
836
+            $this->assertTrue(is_array($cachedFile), "No cache entry for file at $i");
837
+            $this->assertEquals('test.txt', $cachedFile['name'], "Wrong cache entry for file at $i");
838
+
839
+            $longPath .= $ds . $folderName;
840
+        }
841
+    }
842
+
843
+    public function testTouchNotSupported(): void {
844
+        $storage = new TemporaryNoTouch([]);
845
+        $scanner = $storage->getScanner();
846
+        Filesystem::mount($storage, [], '/test/');
847
+        $past = time() - 100;
848
+        $storage->file_put_contents('test', 'foobar');
849
+        $scanner->scan('');
850
+        $view = new View('');
851
+        $info = $view->getFileInfo('/test/test');
852
+
853
+        $view->touch('/test/test', $past);
854
+        $scanner->scanFile('test', Scanner::REUSE_ETAG);
855
+
856
+        $info2 = $view->getFileInfo('/test/test');
857
+        $this->assertSame($info['etag'], $info2['etag']);
858
+    }
859
+
860
+    public function testWatcherEtagCrossStorage(): void {
861
+        $storage1 = new Temporary([]);
862
+        $storage2 = new Temporary([]);
863
+        $scanner1 = $storage1->getScanner();
864
+        $scanner2 = $storage2->getScanner();
865
+        $storage1->mkdir('sub');
866
+        Filesystem::mount($storage1, [], '/test/');
867
+        Filesystem::mount($storage2, [], '/test/sub/storage');
868
+
869
+        $past = time() - 100;
870
+        $storage2->file_put_contents('test.txt', 'foobar');
871
+        $scanner1->scan('');
872
+        $scanner2->scan('');
873
+        $view = new View('');
874
+
875
+        $storage2->getWatcher('')->setPolicy(Watcher::CHECK_ALWAYS);
876
+
877
+        $oldFileInfo = $view->getFileInfo('/test/sub/storage/test.txt');
878
+        $oldFolderInfo = $view->getFileInfo('/test');
879
+
880
+        $storage2->getCache()->update($oldFileInfo->getId(), [
881
+            'storage_mtime' => $past,
882
+        ]);
883
+
884
+        $oldEtag = $oldFolderInfo->getEtag();
885
+
886
+        $view->getFileInfo('/test/sub/storage/test.txt');
887
+        $newFolderInfo = $view->getFileInfo('/test');
888
+
889
+        $this->assertNotEquals($newFolderInfo->getEtag(), $oldEtag);
890
+    }
891
+
892
+    #[\PHPUnit\Framework\Attributes\DataProvider('absolutePathProvider')]
893
+    public function testGetAbsolutePath($expectedPath, $relativePath): void {
894
+        $view = new View('/files');
895
+        $this->assertEquals($expectedPath, $view->getAbsolutePath($relativePath));
896
+    }
897
+
898
+    public function testPartFileInfo(): void {
899
+        $storage = new Temporary([]);
900
+        $scanner = $storage->getScanner();
901
+        Filesystem::mount($storage, [], '/test/');
902
+        $sizeWritten = $storage->file_put_contents('test.part', 'foobar');
903
+        $scanner->scan('');
904
+        $view = new View('/test');
905
+        $info = $view->getFileInfo('test.part');
906
+
907
+        $this->assertInstanceOf('\OCP\Files\FileInfo', $info);
908
+        $this->assertNull($info->getId());
909
+        $this->assertEquals(6, $sizeWritten);
910
+        $this->assertEquals(6, $info->getSize());
911
+        $this->assertEquals('foobar', $view->file_get_contents('test.part'));
912
+    }
913
+
914
+    public static function absolutePathProvider(): array {
915
+        return [
916
+            ['/files', ''],
917
+            ['/files/0', '0'],
918
+            ['/files/false', 'false'],
919
+            ['/files/true', 'true'],
920
+            ['/files', '/'],
921
+            ['/files/test', 'test'],
922
+            ['/files/test', '/test'],
923
+        ];
924
+    }
925
+
926
+    #[\PHPUnit\Framework\Attributes\DataProvider('chrootRelativePathProvider')]
927
+    public function testChrootGetRelativePath($root, $absolutePath, $expectedPath): void {
928
+        $view = new View('/files');
929
+        $view->chroot($root);
930
+        $this->assertEquals($expectedPath, $view->getRelativePath($absolutePath));
931
+    }
932
+
933
+    public static function chrootRelativePathProvider(): array {
934
+        return self::relativePathProvider('/');
935
+    }
936
+
937
+    #[\PHPUnit\Framework\Attributes\DataProvider('initRelativePathProvider')]
938
+    public function testInitGetRelativePath($root, $absolutePath, $expectedPath): void {
939
+        $view = new View($root);
940
+        $this->assertEquals($expectedPath, $view->getRelativePath($absolutePath));
941
+    }
942
+
943
+    public static function initRelativePathProvider(): array {
944
+        return self::relativePathProvider(null);
945
+    }
946
+
947
+    public static function relativePathProvider($missingRootExpectedPath): array {
948
+        return [
949
+            // No root - returns the path
950
+            ['', '/files', '/files'],
951
+            ['', '/files/', '/files/'],
952
+
953
+            // Root equals path - /
954
+            ['/files/', '/files/', '/'],
955
+            ['/files/', '/files', '/'],
956
+            ['/files', '/files/', '/'],
957
+            ['/files', '/files', '/'],
958
+
959
+            // False negatives: chroot fixes those by adding the leading slash.
960
+            // But setting them up with this root (instead of chroot($root))
961
+            // will fail them, although they should be the same.
962
+            // TODO init should be fixed, so it also adds the leading slash
963
+            ['files/', '/files/', $missingRootExpectedPath],
964
+            ['files', '/files/', $missingRootExpectedPath],
965
+            ['files/', '/files', $missingRootExpectedPath],
966
+            ['files', '/files', $missingRootExpectedPath],
967
+
968
+            // False negatives: Paths provided to the method should have a leading slash
969
+            // TODO input should be checked to have a leading slash
970
+            ['/files/', 'files/', null],
971
+            ['/files', 'files/', null],
972
+            ['/files/', 'files', null],
973
+            ['/files', 'files', null],
974
+
975
+            // with trailing slashes
976
+            ['/files/', '/files/0', '0'],
977
+            ['/files/', '/files/false', 'false'],
978
+            ['/files/', '/files/true', 'true'],
979
+            ['/files/', '/files/test', 'test'],
980
+            ['/files/', '/files/test/foo', 'test/foo'],
981
+
982
+            // without trailing slashes
983
+            // TODO false expectation: Should match "with trailing slashes"
984
+            ['/files', '/files/0', '/0'],
985
+            ['/files', '/files/false', '/false'],
986
+            ['/files', '/files/true', '/true'],
987
+            ['/files', '/files/test', '/test'],
988
+            ['/files', '/files/test/foo', '/test/foo'],
989
+
990
+            // leading slashes
991
+            ['/files/', '/files_trashbin/', null],
992
+            ['/files', '/files_trashbin/', null],
993
+            ['/files/', '/files_trashbin', null],
994
+            ['/files', '/files_trashbin', null],
995
+
996
+            // no leading slashes
997
+            ['files/', 'files_trashbin/', null],
998
+            ['files', 'files_trashbin/', null],
999
+            ['files/', 'files_trashbin', null],
1000
+            ['files', 'files_trashbin', null],
1001
+
1002
+            // mixed leading slashes
1003
+            ['files/', '/files_trashbin/', null],
1004
+            ['/files/', 'files_trashbin/', null],
1005
+            ['files', '/files_trashbin/', null],
1006
+            ['/files', 'files_trashbin/', null],
1007
+            ['files/', '/files_trashbin', null],
1008
+            ['/files/', 'files_trashbin', null],
1009
+            ['files', '/files_trashbin', null],
1010
+            ['/files', 'files_trashbin', null],
1011
+
1012
+            ['files', 'files_trashbin/test', null],
1013
+            ['/files', '/files_trashbin/test', null],
1014
+            ['/files', 'files_trashbin/test', null],
1015
+        ];
1016
+    }
1017
+
1018
+    public function testFileView(): void {
1019
+        $storage = new Temporary([]);
1020
+        $scanner = $storage->getScanner();
1021
+        $storage->file_put_contents('foo.txt', 'bar');
1022
+        Filesystem::mount($storage, [], '/test/');
1023
+        $scanner->scan('');
1024
+        $view = new View('/test/foo.txt');
1025
+
1026
+        $this->assertEquals('bar', $view->file_get_contents(''));
1027
+        $fh = tmpfile();
1028
+        fwrite($fh, 'foo');
1029
+        rewind($fh);
1030
+        $view->file_put_contents('', $fh);
1031
+        $this->assertEquals('foo', $view->file_get_contents(''));
1032
+    }
1033
+
1034
+    #[\PHPUnit\Framework\Attributes\DataProvider('tooLongPathDataProvider')]
1035
+    public function testTooLongPath($operation, $param0 = null): void {
1036
+        $this->expectException(InvalidPathException::class);
1037
+
1038
+
1039
+        $longPath = '';
1040
+        // 4000 is the maximum path length in file_cache.path
1041
+        $folderName = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123456789';
1042
+        $depth = (4000 / 57);
1043
+        foreach (range(0, $depth + 1) as $i) {
1044
+            $longPath .= '/' . $folderName;
1045
+        }
1046
+
1047
+        $storage = new Temporary([]);
1048
+        $this->tempStorage = $storage; // for later hard cleanup
1049
+        Filesystem::mount($storage, [], '/');
1050
+
1051
+        $rootView = new View('');
1052
+
1053
+        if ($param0 === '@0') {
1054
+            $param0 = $longPath;
1055
+        }
1056
+
1057
+        if ($operation === 'hash') {
1058
+            $param0 = $longPath;
1059
+            $longPath = 'md5';
1060
+        }
1061
+
1062
+        call_user_func([$rootView, $operation], $longPath, $param0);
1063
+    }
1064
+
1065
+    public static function tooLongPathDataProvider(): array {
1066
+        return [
1067
+            ['getAbsolutePath'],
1068
+            ['getRelativePath'],
1069
+            ['getMountPoint'],
1070
+            ['resolvePath'],
1071
+            ['getLocalFile'],
1072
+            ['mkdir'],
1073
+            ['rmdir'],
1074
+            ['opendir'],
1075
+            ['is_dir'],
1076
+            ['is_file'],
1077
+            ['stat'],
1078
+            ['filetype'],
1079
+            ['filesize'],
1080
+            ['readfile'],
1081
+            ['isCreatable'],
1082
+            ['isReadable'],
1083
+            ['isUpdatable'],
1084
+            ['isDeletable'],
1085
+            ['isSharable'],
1086
+            ['file_exists'],
1087
+            ['filemtime'],
1088
+            ['touch'],
1089
+            ['file_get_contents'],
1090
+            ['unlink'],
1091
+            ['deleteAll'],
1092
+            ['toTmpFile'],
1093
+            ['getMimeType'],
1094
+            ['free_space'],
1095
+            ['getFileInfo'],
1096
+            ['getDirectoryContent'],
1097
+            ['getOwner'],
1098
+            ['getETag'],
1099
+            ['file_put_contents', 'ipsum'],
1100
+            ['rename', '@0'],
1101
+            ['copy', '@0'],
1102
+            ['fopen', 'r'],
1103
+            ['fromTmpFile', '@0'],
1104
+            ['hash'],
1105
+            ['hasUpdated', 0],
1106
+            ['putFileInfo', []],
1107
+        ];
1108
+    }
1109
+
1110
+    public function testRenameCrossStoragePreserveMtime(): void {
1111
+        $storage1 = new Temporary([]);
1112
+        $storage2 = new Temporary([]);
1113
+        $storage1->mkdir('sub');
1114
+        $storage1->mkdir('foo');
1115
+        $storage1->file_put_contents('foo.txt', 'asd');
1116
+        $storage1->file_put_contents('foo/bar.txt', 'asd');
1117
+        Filesystem::mount($storage1, [], '/test/');
1118
+        Filesystem::mount($storage2, [], '/test/sub/storage');
1119
+
1120
+        $view = new View('');
1121
+        $time = time() - 200;
1122
+        $view->touch('/test/foo.txt', $time);
1123
+        $view->touch('/test/foo', $time);
1124
+        $view->touch('/test/foo/bar.txt', $time);
1125
+
1126
+        $view->rename('/test/foo.txt', '/test/sub/storage/foo.txt');
1127
+
1128
+        $this->assertEquals($time, $view->filemtime('/test/sub/storage/foo.txt'));
1129
+
1130
+        $view->rename('/test/foo', '/test/sub/storage/foo');
1131
+
1132
+        $this->assertEquals($time, $view->filemtime('/test/sub/storage/foo/bar.txt'));
1133
+    }
1134
+
1135
+    public function testRenameFailDeleteTargetKeepSource(): void {
1136
+        $this->doTestCopyRenameFail('rename');
1137
+    }
1138
+
1139
+    public function testCopyFailDeleteTargetKeepSource(): void {
1140
+        $this->doTestCopyRenameFail('copy');
1141
+    }
1142
+
1143
+    private function doTestCopyRenameFail($operation) {
1144
+        $storage1 = new Temporary([]);
1145
+        /** @var \PHPUnit\Framework\MockObject\MockObject|Temporary $storage2 */
1146
+        $storage2 = $this->getMockBuilder(TemporaryNoCross::class)
1147
+            ->setConstructorArgs([[]])
1148
+            ->onlyMethods(['fopen', 'writeStream'])
1149
+            ->getMock();
1150
+
1151
+        $storage2->method('writeStream')
1152
+            ->willThrowException(new GenericFileException('Failed to copy stream'));
1153
+
1154
+        $storage2->method('fopen')
1155
+            ->willReturn(false);
1156
+
1157
+        $storage1->mkdir('sub');
1158
+        $storage1->file_put_contents('foo.txt', '0123456789ABCDEFGH');
1159
+        $storage1->mkdir('dirtomove');
1160
+        $storage1->file_put_contents('dirtomove/indir1.txt', '0123456'); // fits
1161
+        $storage1->file_put_contents('dirtomove/indir2.txt', '0123456789ABCDEFGH'); // doesn't fit
1162
+        $storage2->file_put_contents('existing.txt', '0123');
1163
+        $storage1->getScanner()->scan('');
1164
+        $storage2->getScanner()->scan('');
1165
+        Filesystem::mount($storage1, [], '/test/');
1166
+        Filesystem::mount($storage2, [], '/test/sub/storage');
1167
+
1168
+        // move file
1169
+        $view = new View('');
1170
+        $this->assertTrue($storage1->file_exists('foo.txt'));
1171
+        $this->assertFalse($storage2->file_exists('foo.txt'));
1172
+        $this->assertFalse($view->$operation('/test/foo.txt', '/test/sub/storage/foo.txt'));
1173
+        $this->assertFalse($storage2->file_exists('foo.txt'));
1174
+        $this->assertFalse($storage2->getCache()->get('foo.txt'));
1175
+        $this->assertTrue($storage1->file_exists('foo.txt'));
1176
+
1177
+        // if target exists, it will be deleted too
1178
+        $this->assertFalse($view->$operation('/test/foo.txt', '/test/sub/storage/existing.txt'));
1179
+        $this->assertFalse($storage2->file_exists('existing.txt'));
1180
+        $this->assertFalse($storage2->getCache()->get('existing.txt'));
1181
+        $this->assertTrue($storage1->file_exists('foo.txt'));
1182
+
1183
+        // move folder
1184
+        $this->assertFalse($view->$operation('/test/dirtomove/', '/test/sub/storage/dirtomove/'));
1185
+        // since the move failed, the full source tree is kept
1186
+        $this->assertTrue($storage1->file_exists('dirtomove/indir1.txt'));
1187
+        $this->assertTrue($storage1->file_exists('dirtomove/indir2.txt'));
1188
+        // second file not moved/copied
1189
+        $this->assertFalse($storage2->file_exists('dirtomove/indir2.txt'));
1190
+        $this->assertFalse($storage2->getCache()->get('dirtomove/indir2.txt'));
1191
+    }
1192
+
1193
+    public function testDeleteFailKeepCache(): void {
1194
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1195
+        $storage = $this->getMockBuilder(Temporary::class)
1196
+            ->setConstructorArgs([[]])
1197
+            ->onlyMethods(['unlink'])
1198
+            ->getMock();
1199
+        $storage->expects($this->once())
1200
+            ->method('unlink')
1201
+            ->willReturn(false);
1202
+        $scanner = $storage->getScanner();
1203
+        $cache = $storage->getCache();
1204
+        $storage->file_put_contents('foo.txt', 'asd');
1205
+        $scanner->scan('');
1206
+        Filesystem::mount($storage, [], '/test/');
1207
+
1208
+        $view = new View('/test');
1209
+
1210
+        $this->assertFalse($view->unlink('foo.txt'));
1211
+        $this->assertTrue($cache->inCache('foo.txt'));
1212
+    }
1213
+
1214
+    public static function directoryTraversalProvider(): array {
1215
+        return [
1216
+            ['../test/'],
1217
+            ['..\\test\\my/../folder'],
1218
+            ['/test/my/../foo\\'],
1219
+        ];
1220
+    }
1221
+
1222
+    /**
1223
+     * @param string $root
1224
+     */
1225
+    #[\PHPUnit\Framework\Attributes\DataProvider('directoryTraversalProvider')]
1226
+    public function testConstructDirectoryTraversalException($root): void {
1227
+        $this->expectException(\Exception::class);
1228
+
1229
+        new View($root);
1230
+    }
1231
+
1232
+    public function testRenameOverWrite(): void {
1233
+        $storage = new Temporary([]);
1234
+        $scanner = $storage->getScanner();
1235
+        $storage->mkdir('sub');
1236
+        $storage->mkdir('foo');
1237
+        $storage->file_put_contents('foo.txt', 'asd');
1238
+        $storage->file_put_contents('foo/bar.txt', 'asd');
1239
+        $scanner->scan('');
1240
+        Filesystem::mount($storage, [], '/test/');
1241
+        $view = new View('');
1242
+        $this->assertTrue($view->rename('/test/foo.txt', '/test/foo/bar.txt'));
1243
+    }
1244
+
1245
+    public function testSetMountOptionsInStorage(): void {
1246
+        $mount = new MountPoint(Temporary::class, '/asd/', [[]], Filesystem::getLoader(), ['foo' => 'bar']);
1247
+        Filesystem::getMountManager()->addMount($mount);
1248
+        /** @var Common $storage */
1249
+        $storage = $mount->getStorage();
1250
+        $this->assertEquals($storage->getMountOption('foo'), 'bar');
1251
+    }
1252
+
1253
+    public function testSetMountOptionsWatcherPolicy(): void {
1254
+        $mount = new MountPoint(Temporary::class, '/asd/', [[]], Filesystem::getLoader(), ['filesystem_check_changes' => Watcher::CHECK_NEVER]);
1255
+        Filesystem::getMountManager()->addMount($mount);
1256
+        /** @var Common $storage */
1257
+        $storage = $mount->getStorage();
1258
+        $watcher = $storage->getWatcher();
1259
+        $this->assertEquals(Watcher::CHECK_NEVER, $watcher->getPolicy());
1260
+    }
1261
+
1262
+    public function testGetAbsolutePathOnNull(): void {
1263
+        $view = new View();
1264
+        $this->assertNull($view->getAbsolutePath(null));
1265
+    }
1266
+
1267
+    public function testGetRelativePathOnNull(): void {
1268
+        $view = new View();
1269
+        $this->assertNull($view->getRelativePath(null));
1270
+    }
1271
+
1272
+
1273
+    public function testNullAsRoot(): void {
1274
+        $this->expectException(\TypeError::class);
1275
+
1276
+        new View(null);
1277
+    }
1278
+
1279
+    /**
1280
+     * e.g. reading from a folder that's being renamed
1281
+     *
1282
+     *
1283
+     *
1284
+     * @param string $rootPath
1285
+     * @param string $pathPrefix
1286
+     */
1287
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1288
+    public function testReadFromWriteLockedPath($rootPath, $pathPrefix): void {
1289
+        $this->expectException(LockedException::class);
1290
+
1291
+        $rootPath = str_replace('{folder}', 'files', $rootPath);
1292
+        $pathPrefix = str_replace('{folder}', 'files', $pathPrefix);
1293
+
1294
+        $view = new View($rootPath);
1295
+        $storage = new Temporary([]);
1296
+        Filesystem::mount($storage, [], '/');
1297
+        $this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1298
+        $view->lockFile($pathPrefix . '/foo/bar/asd', ILockingProvider::LOCK_SHARED);
1299
+    }
1300
+
1301
+    /**
1302
+     * Reading from a files_encryption folder that's being renamed
1303
+     *
1304
+     *
1305
+     * @param string $rootPath
1306
+     * @param string $pathPrefix
1307
+     */
1308
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1309
+    public function testReadFromWriteUnlockablePath($rootPath, $pathPrefix): void {
1310
+        $rootPath = str_replace('{folder}', 'files_encryption', $rootPath);
1311
+        $pathPrefix = str_replace('{folder}', 'files_encryption', $pathPrefix);
1312
+
1313
+        $view = new View($rootPath);
1314
+        $storage = new Temporary([]);
1315
+        Filesystem::mount($storage, [], '/');
1316
+        $this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1317
+        $this->assertFalse($view->lockFile($pathPrefix . '/foo/bar/asd', ILockingProvider::LOCK_SHARED));
1318
+    }
1319
+
1320
+    /**
1321
+     * e.g. writing a file that's being downloaded
1322
+     *
1323
+     *
1324
+     *
1325
+     * @param string $rootPath
1326
+     * @param string $pathPrefix
1327
+     */
1328
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1329
+    public function testWriteToReadLockedFile($rootPath, $pathPrefix): void {
1330
+        $this->expectException(LockedException::class);
1331
+
1332
+        $rootPath = str_replace('{folder}', 'files', $rootPath);
1333
+        $pathPrefix = str_replace('{folder}', 'files', $pathPrefix);
1334
+
1335
+        $view = new View($rootPath);
1336
+        $storage = new Temporary([]);
1337
+        Filesystem::mount($storage, [], '/');
1338
+        $this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED));
1339
+        $view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE);
1340
+    }
1341
+
1342
+    /**
1343
+     * Writing a file that's being downloaded
1344
+     *
1345
+     *
1346
+     * @param string $rootPath
1347
+     * @param string $pathPrefix
1348
+     */
1349
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1350
+    public function testWriteToReadUnlockableFile($rootPath, $pathPrefix): void {
1351
+        $rootPath = str_replace('{folder}', 'files_encryption', $rootPath);
1352
+        $pathPrefix = str_replace('{folder}', 'files_encryption', $pathPrefix);
1353
+
1354
+        $view = new View($rootPath);
1355
+        $storage = new Temporary([]);
1356
+        Filesystem::mount($storage, [], '/');
1357
+        $this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED));
1358
+        $this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1359
+    }
1360
+
1361
+    /**
1362
+     * Test that locks are on mount point paths instead of mount root
1363
+     */
1364
+    public function testLockLocalMountPointPathInsteadOfStorageRoot(): void {
1365
+        $lockingProvider = Server::get(ILockingProvider::class);
1366
+        $view = new View('/testuser/files/');
1367
+        $storage = new Temporary([]);
1368
+        Filesystem::mount($storage, [], '/');
1369
+        $mountedStorage = new Temporary([]);
1370
+        Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint');
1371
+
1372
+        $this->assertTrue(
1373
+            $view->lockFile('/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, true),
1374
+            'Can lock mount point'
1375
+        );
1376
+
1377
+        // no exception here because storage root was not locked
1378
+        $mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1379
+
1380
+        $thrown = false;
1381
+        try {
1382
+            $storage->acquireLock('/testuser/files/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1383
+        } catch (LockedException $e) {
1384
+            $thrown = true;
1385
+        }
1386
+        $this->assertTrue($thrown, 'Mount point path was locked on root storage');
1387
+
1388
+        $lockingProvider->releaseAll();
1389
+    }
1390
+
1391
+    /**
1392
+     * Test that locks are on mount point paths and also mount root when requested
1393
+     */
1394
+    public function testLockStorageRootButNotLocalMountPoint(): void {
1395
+        $lockingProvider = Server::get(ILockingProvider::class);
1396
+        $view = new View('/testuser/files/');
1397
+        $storage = new Temporary([]);
1398
+        Filesystem::mount($storage, [], '/');
1399
+        $mountedStorage = new Temporary([]);
1400
+        Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint');
1401
+
1402
+        $this->assertTrue(
1403
+            $view->lockFile('/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, false),
1404
+            'Can lock mount point'
1405
+        );
1406
+
1407
+        $thrown = false;
1408
+        try {
1409
+            $mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1410
+        } catch (LockedException $e) {
1411
+            $thrown = true;
1412
+        }
1413
+        $this->assertTrue($thrown, 'Mount point storage root was locked on original storage');
1414
+
1415
+        // local mount point was not locked
1416
+        $storage->acquireLock('/testuser/files/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1417
+
1418
+        $lockingProvider->releaseAll();
1419
+    }
1420
+
1421
+    /**
1422
+     * Test that locks are on mount point paths and also mount root when requested
1423
+     */
1424
+    public function testLockMountPointPathFailReleasesBoth(): void {
1425
+        $lockingProvider = Server::get(ILockingProvider::class);
1426
+        $view = new View('/testuser/files/');
1427
+        $storage = new Temporary([]);
1428
+        Filesystem::mount($storage, [], '/');
1429
+        $mountedStorage = new Temporary([]);
1430
+        Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint.txt');
1431
+
1432
+        // this would happen if someone is writing on the mount point
1433
+        $mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1434
+
1435
+        $thrown = false;
1436
+        try {
1437
+            // this actually acquires two locks, one on the mount point and one on the storage root,
1438
+            // but the one on the storage root will fail
1439
+            $view->lockFile('/mountpoint.txt', ILockingProvider::LOCK_SHARED);
1440
+        } catch (LockedException $e) {
1441
+            $thrown = true;
1442
+        }
1443
+        $this->assertTrue($thrown, 'Cannot acquire shared lock because storage root is already locked');
1444
+
1445
+        // from here we expect that the lock on the local mount point was released properly
1446
+        // so acquiring an exclusive lock will succeed
1447
+        $storage->acquireLock('/testuser/files/mountpoint.txt', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1448
+
1449
+        $lockingProvider->releaseAll();
1450
+    }
1451
+
1452
+    public static function dataLockPaths(): array {
1453
+        return [
1454
+            ['/testuser/{folder}', ''],
1455
+            ['/testuser', '/{folder}'],
1456
+            ['', '/testuser/{folder}'],
1457
+        ];
1458
+    }
1459
+
1460
+    public static function pathRelativeToFilesProvider(): array {
1461
+        return [
1462
+            ['admin/files', ''],
1463
+            ['admin/files/x', 'x'],
1464
+            ['/admin/files', ''],
1465
+            ['/admin/files/sub', 'sub'],
1466
+            ['/admin/files/sub/', 'sub'],
1467
+            ['/admin/files/sub/sub2', 'sub/sub2'],
1468
+            ['//admin//files/sub//sub2', 'sub/sub2'],
1469
+        ];
1470
+    }
1471
+
1472
+    #[\PHPUnit\Framework\Attributes\DataProvider('pathRelativeToFilesProvider')]
1473
+    public function testGetPathRelativeToFiles($path, $expectedPath): void {
1474
+        $view = new View();
1475
+        $this->assertEquals($expectedPath, $view->getPathRelativeToFiles($path));
1476
+    }
1477
+
1478
+    public static function pathRelativeToFilesProviderExceptionCases(): array {
1479
+        return [
1480
+            [''],
1481
+            ['x'],
1482
+            ['files'],
1483
+            ['/files'],
1484
+            ['/admin/files_versions/abc'],
1485
+        ];
1486
+    }
1487
+
1488
+    /**
1489
+     * @param string $path
1490
+     */
1491
+    #[\PHPUnit\Framework\Attributes\DataProvider('pathRelativeToFilesProviderExceptionCases')]
1492
+    public function testGetPathRelativeToFilesWithInvalidArgument($path): void {
1493
+        $this->expectException(\InvalidArgumentException::class);
1494
+        $this->expectExceptionMessage('$absolutePath must be relative to "files"');
1495
+
1496
+        $view = new View();
1497
+        $view->getPathRelativeToFiles($path);
1498
+    }
1499
+
1500
+    public function testChangeLock(): void {
1501
+        $view = new View('/testuser/files/');
1502
+        $storage = new Temporary([]);
1503
+        Filesystem::mount($storage, [], '/');
1504
+
1505
+        $view->lockFile('/test/sub', ILockingProvider::LOCK_SHARED);
1506
+        $this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED));
1507
+        $this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE));
1508
+
1509
+        $view->changeLock('//test/sub', ILockingProvider::LOCK_EXCLUSIVE);
1510
+        $this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE));
1511
+
1512
+        $view->changeLock('test/sub', ILockingProvider::LOCK_SHARED);
1513
+        $this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED));
1514
+
1515
+        $view->unlockFile('/test/sub/', ILockingProvider::LOCK_SHARED);
1516
+
1517
+        $this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED));
1518
+        $this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE));
1519
+    }
1520
+
1521
+    public static function hookPathProvider(): array {
1522
+        return [
1523
+            ['/foo/files', '/foo', true],
1524
+            ['/foo/files/bar', '/foo', true],
1525
+            ['/foo', '/foo', false],
1526
+            ['/foo', '/files/foo', true],
1527
+            ['/foo', 'filesfoo', false],
1528
+            ['', '/foo/files', true],
1529
+            ['', '/foo/files/bar.txt', true],
1530
+        ];
1531
+    }
1532
+
1533
+    /**
1534
+     * @param $root
1535
+     * @param $path
1536
+     * @param $shouldEmit
1537
+     */
1538
+    #[\PHPUnit\Framework\Attributes\DataProvider('hookPathProvider')]
1539
+    public function testHookPaths($root, $path, $shouldEmit): void {
1540
+        $filesystemReflection = new \ReflectionClass(Filesystem::class);
1541
+        $defaultRootValue = $filesystemReflection->getProperty('defaultInstance');
1542
+        $oldRoot = $defaultRootValue->getValue();
1543
+        $defaultView = new View('/foo/files');
1544
+        $defaultRootValue->setValue(null, $defaultView);
1545
+        $view = new View($root);
1546
+        $result = self::invokePrivate($view, 'shouldEmitHooks', [$path]);
1547
+        $defaultRootValue->setValue(null, $oldRoot);
1548
+        $this->assertEquals($shouldEmit, $result);
1549
+    }
1550
+
1551
+    /**
1552
+     * Create test movable mount points
1553
+     *
1554
+     * @param array $mountPoints array of mount point locations
1555
+     * @return array array of MountPoint objects
1556
+     */
1557
+    private function createTestMovableMountPoints($mountPoints) {
1558
+        $mounts = [];
1559
+        foreach ($mountPoints as $mountPoint) {
1560
+            $storage = $this->getMockBuilder(Storage::class)
1561
+                ->onlyMethods([])
1562
+                ->getMock();
1563
+            $storage->method('getId')->willReturn('non-null-id');
1564
+            $storage->method('getStorageCache')->willReturnCallback(function () use ($storage) {
1565
+                return new \OC\Files\Cache\Storage($storage, true, Server::get(IDBConnection::class));
1566
+            });
1567
+
1568
+            $mounts[] = $this->getMockBuilder(TestMoveableMountPoint::class)
1569
+                ->onlyMethods(['moveMount'])
1570
+                ->setConstructorArgs([$storage, $mountPoint])
1571
+                ->getMock();
1572
+        }
1573
+
1574
+        /** @var IMountProvider|\PHPUnit\Framework\MockObject\MockObject $mountProvider */
1575
+        $mountProvider = $this->createMock(IMountProvider::class);
1576
+        $mountProvider->expects($this->any())
1577
+            ->method('getMountsForUser')
1578
+            ->willReturn($mounts);
1579
+
1580
+        $mountProviderCollection = Server::get(IMountProviderCollection::class);
1581
+        $mountProviderCollection->registerProvider($mountProvider);
1582
+
1583
+        return $mounts;
1584
+    }
1585
+
1586
+    /**
1587
+     * Test mount point move
1588
+     */
1589
+    public function testMountPointMove(): void {
1590
+        self::loginAsUser($this->user);
1591
+
1592
+        [$mount1, $mount2] = $this->createTestMovableMountPoints([
1593
+            $this->user . '/files/mount1',
1594
+            $this->user . '/files/mount2',
1595
+        ]);
1596
+        $mount1->expects($this->once())
1597
+            ->method('moveMount')
1598
+            ->willReturn(true);
1599
+
1600
+        $mount2->expects($this->once())
1601
+            ->method('moveMount')
1602
+            ->willReturn(true);
1603
+
1604
+        $view = new View('/' . $this->user . '/files/');
1605
+        $view->mkdir('sub');
1606
+
1607
+        $this->assertTrue($view->rename('mount1', 'renamed_mount'), 'Can rename mount point');
1608
+        $this->assertTrue($view->rename('mount2', 'sub/moved_mount'), 'Can move a mount point into a subdirectory');
1609
+    }
1610
+
1611
+    public function testMoveMountPointOverwrite(): void {
1612
+        self::loginAsUser($this->user);
1613
+
1614
+        [$mount1, $mount2] = $this->createTestMovableMountPoints([
1615
+            $this->user . '/files/mount1',
1616
+            $this->user . '/files/mount2',
1617
+        ]);
1618
+
1619
+        $mount1->expects($this->never())
1620
+            ->method('moveMount');
1621
+
1622
+        $mount2->expects($this->never())
1623
+            ->method('moveMount');
1624
+
1625
+        $view = new View('/' . $this->user . '/files/');
1626
+
1627
+        $this->expectException(ForbiddenException::class);
1628
+        $view->rename('mount1', 'mount2');
1629
+    }
1630
+
1631
+    public function testMoveMountPointIntoMount(): void {
1632
+        self::loginAsUser($this->user);
1633
+
1634
+        [$mount1, $mount2] = $this->createTestMovableMountPoints([
1635
+            $this->user . '/files/mount1',
1636
+            $this->user . '/files/mount2',
1637
+        ]);
1638
+
1639
+        $mount1->expects($this->never())
1640
+            ->method('moveMount');
1641
+
1642
+        $mount2->expects($this->never())
1643
+            ->method('moveMount');
1644
+
1645
+        $view = new View('/' . $this->user . '/files/');
1646
+
1647
+        $this->expectException(ForbiddenException::class);
1648
+        $view->rename('mount1', 'mount2/sub');
1649
+    }
1650
+
1651
+    /**
1652
+     * Test that moving a mount point into a shared folder is forbidden
1653
+     */
1654
+    public function testMoveMountPointIntoSharedFolder(): void {
1655
+        self::loginAsUser($this->user);
1656
+
1657
+        [$mount1, $mount2] = $this->createTestMovableMountPoints([
1658
+            $this->user . '/files/mount1',
1659
+            $this->user . '/files/mount2',
1660
+        ]);
1661
+
1662
+        $mount1->expects($this->never())
1663
+            ->method('moveMount');
1664
+
1665
+        $mount2->expects($this->once())
1666
+            ->method('moveMount')
1667
+            ->willReturn(true);
1668
+
1669
+        $view = new View('/' . $this->user . '/files/');
1670
+        $view->mkdir('shareddir');
1671
+        $view->mkdir('shareddir/sub');
1672
+        $view->mkdir('shareddir/sub2');
1673
+        // Create a similar named but non-shared folder
1674
+        $view->mkdir('shareddir notshared');
1675
+
1676
+        $fileId = $view->getFileInfo('shareddir')->getId();
1677
+        $userObject = Server::get(IUserManager::class)->createUser('test2', 'IHateNonMockableStaticClasses');
1678
+
1679
+        $userFolder = \OC::$server->getUserFolder($this->user);
1680
+        $shareDir = $userFolder->get('shareddir');
1681
+        $shareManager = Server::get(IShareManager::class);
1682
+        $share = $shareManager->newShare();
1683
+        $share->setSharedWith('test2')
1684
+            ->setSharedBy($this->user)
1685
+            ->setShareType(IShare::TYPE_USER)
1686
+            ->setPermissions(Constants::PERMISSION_READ)
1687
+            ->setNode($shareDir);
1688
+        $shareManager->createShare($share);
1689
+
1690
+        try {
1691
+            $view->rename('mount1', 'shareddir');
1692
+            $this->fail('Cannot overwrite shared folder');
1693
+        } catch (ForbiddenException $e) {
1694
+
1695
+        }
1696
+        try {
1697
+            $view->rename('mount1', 'shareddir/sub');
1698
+            $this->fail('Cannot move mount point into shared folder');
1699
+        } catch (ForbiddenException $e) {
1700
+
1701
+        }
1702
+        try {
1703
+            $view->rename('mount1', 'shareddir/sub/sub2');
1704
+            $this->fail('Cannot move mount point into shared subfolder');
1705
+        } catch (ForbiddenException $e) {
1706
+
1707
+        }
1708
+        $this->assertTrue($view->rename('mount2', 'shareddir notshared/sub'), 'Can move mount point into a similarly named but non-shared folder');
1709
+
1710
+        $shareManager->deleteShare($share);
1711
+        $userObject->delete();
1712
+    }
1713
+
1714
+    public static function basicOperationProviderForLocks(): array {
1715
+        return [
1716
+            // --- write hook ----
1717
+            [
1718
+                'touch',
1719
+                ['touch-create.txt'],
1720
+                'touch-create.txt',
1721
+                'create',
1722
+                ILockingProvider::LOCK_SHARED,
1723
+                ILockingProvider::LOCK_EXCLUSIVE,
1724
+                ILockingProvider::LOCK_SHARED,
1725
+            ],
1726
+            [
1727
+                'fopen',
1728
+                ['test-write.txt', 'w'],
1729
+                'test-write.txt',
1730
+                'write',
1731
+                ILockingProvider::LOCK_SHARED,
1732
+                ILockingProvider::LOCK_EXCLUSIVE,
1733
+                null,
1734
+                // exclusive lock stays until fclose
1735
+                ILockingProvider::LOCK_EXCLUSIVE,
1736
+            ],
1737
+            [
1738
+                'mkdir',
1739
+                ['newdir'],
1740
+                'newdir',
1741
+                'write',
1742
+                ILockingProvider::LOCK_SHARED,
1743
+                ILockingProvider::LOCK_EXCLUSIVE,
1744
+                ILockingProvider::LOCK_SHARED,
1745
+            ],
1746
+            [
1747
+                'file_put_contents',
1748
+                ['file_put_contents.txt', 'blah'],
1749
+                'file_put_contents.txt',
1750
+                'write',
1751
+                ILockingProvider::LOCK_SHARED,
1752
+                ILockingProvider::LOCK_EXCLUSIVE,
1753
+                ILockingProvider::LOCK_SHARED,
1754
+                null,
1755
+                0,
1756
+            ],
1757
+
1758
+            // ---- delete hook ----
1759
+            [
1760
+                'rmdir',
1761
+                ['dir'],
1762
+                'dir',
1763
+                'delete',
1764
+                ILockingProvider::LOCK_SHARED,
1765
+                ILockingProvider::LOCK_EXCLUSIVE,
1766
+                ILockingProvider::LOCK_SHARED,
1767
+            ],
1768
+            [
1769
+                'unlink',
1770
+                ['test.txt'],
1771
+                'test.txt',
1772
+                'delete',
1773
+                ILockingProvider::LOCK_SHARED,
1774
+                ILockingProvider::LOCK_EXCLUSIVE,
1775
+                ILockingProvider::LOCK_SHARED,
1776
+            ],
1777
+
1778
+            // ---- read hook (no post hooks) ----
1779
+            [
1780
+                'file_get_contents',
1781
+                ['test.txt'],
1782
+                'test.txt',
1783
+                'read',
1784
+                ILockingProvider::LOCK_SHARED,
1785
+                ILockingProvider::LOCK_SHARED,
1786
+                null,
1787
+                null,
1788
+                false,
1789
+            ],
1790
+            [
1791
+                'fopen',
1792
+                ['test.txt', 'r'],
1793
+                'test.txt',
1794
+                'read',
1795
+                ILockingProvider::LOCK_SHARED,
1796
+                ILockingProvider::LOCK_SHARED,
1797
+                null,
1798
+            ],
1799
+            [
1800
+                'opendir',
1801
+                ['dir'],
1802
+                'dir',
1803
+                'read',
1804
+                ILockingProvider::LOCK_SHARED,
1805
+                ILockingProvider::LOCK_SHARED,
1806
+                null,
1807
+            ],
1808
+
1809
+            // ---- no lock, touch hook ---
1810
+            ['touch', ['test.txt'], 'test.txt', 'touch', null, null, null],
1811
+
1812
+            // ---- no hooks, no locks ---
1813
+            ['is_dir', ['dir'], 'dir', ''],
1814
+            ['is_file', ['dir'], 'dir', ''],
1815
+            [
1816
+                'stat',
1817
+                ['dir'],
1818
+                'dir',
1819
+                '',
1820
+                ILockingProvider::LOCK_SHARED,
1821
+                ILockingProvider::LOCK_SHARED,
1822
+                ILockingProvider::LOCK_SHARED,
1823
+                null,
1824
+                false,
1825
+            ],
1826
+            [
1827
+                'filetype',
1828
+                ['dir'],
1829
+                'dir',
1830
+                '',
1831
+                ILockingProvider::LOCK_SHARED,
1832
+                ILockingProvider::LOCK_SHARED,
1833
+                ILockingProvider::LOCK_SHARED,
1834
+                null,
1835
+                false,
1836
+            ],
1837
+            [
1838
+                'filesize',
1839
+                ['dir'],
1840
+                'dir',
1841
+                '',
1842
+                ILockingProvider::LOCK_SHARED,
1843
+                ILockingProvider::LOCK_SHARED,
1844
+                ILockingProvider::LOCK_SHARED,
1845
+                null,
1846
+                /* Return an int */
1847
+                100
1848
+            ],
1849
+            ['isCreatable', ['dir'], 'dir', ''],
1850
+            ['isReadable', ['dir'], 'dir', ''],
1851
+            ['isUpdatable', ['dir'], 'dir', ''],
1852
+            ['isDeletable', ['dir'], 'dir', ''],
1853
+            ['isSharable', ['dir'], 'dir', ''],
1854
+            ['file_exists', ['dir'], 'dir', ''],
1855
+            [
1856
+                'filemtime',
1857
+                ['dir'],
1858
+                'dir',
1859
+                '',
1860
+                ILockingProvider::LOCK_SHARED,
1861
+                ILockingProvider::LOCK_SHARED,
1862
+                ILockingProvider::LOCK_SHARED,
1863
+                null,
1864
+                false,
1865
+            ],
1866
+        ];
1867
+    }
1868
+
1869
+    /**
1870
+     * Test whether locks are set before and after the operation
1871
+     *
1872
+     *
1873
+     * @param string $operation operation name on the view
1874
+     * @param array $operationArgs arguments for the operation
1875
+     * @param string $lockedPath path of the locked item to check
1876
+     * @param string $hookType hook type
1877
+     * @param ?int $expectedLockBefore expected lock during pre hooks
1878
+     * @param ?int $expectedLockDuring expected lock during operation
1879
+     * @param ?int $expectedLockAfter expected lock during post hooks
1880
+     * @param ?int $expectedStrayLock expected lock after returning, should
1881
+     *                                be null (unlock) for most operations
1882
+     */
1883
+    #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
1884
+    public function testLockBasicOperation(
1885
+        string $operation,
1886
+        array $operationArgs,
1887
+        string $lockedPath,
1888
+        string $hookType,
1889
+        ?int $expectedLockBefore = ILockingProvider::LOCK_SHARED,
1890
+        ?int $expectedLockDuring = ILockingProvider::LOCK_SHARED,
1891
+        ?int $expectedLockAfter = ILockingProvider::LOCK_SHARED,
1892
+        ?int $expectedStrayLock = null,
1893
+        mixed $returnValue = true,
1894
+    ): void {
1895
+        $view = new View('/' . $this->user . '/files/');
1896
+
1897
+        /** @var Temporary&MockObject $storage */
1898
+        $storage = $this->getMockBuilder(Temporary::class)
1899
+            ->onlyMethods([$operation])
1900
+            ->getMock();
1901
+
1902
+        /* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
1903
+        Server::get(ITrashManager::class)->pauseTrash();
1904
+        /* Same thing with encryption wrapper */
1905
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
1906
+
1907
+        Filesystem::mount($storage, [], $this->user . '/');
1908
+
1909
+        // work directly on disk because mkdir might be mocked
1910
+        $realPath = $storage->getSourcePath('');
1911
+        mkdir($realPath . '/files');
1912
+        mkdir($realPath . '/files/dir');
1913
+        file_put_contents($realPath . '/files/test.txt', 'blah');
1914
+        $storage->getScanner()->scan('files');
1915
+
1916
+        $storage->expects($this->once())
1917
+            ->method($operation)
1918
+            ->willReturnCallback(
1919
+                function () use ($view, $lockedPath, &$lockTypeDuring, $returnValue) {
1920
+                    $lockTypeDuring = $this->getFileLockType($view, $lockedPath);
1921
+
1922
+                    return $returnValue;
1923
+                }
1924
+            );
1925
+
1926
+        $this->assertNull($this->getFileLockType($view, $lockedPath), 'File not locked before operation');
1927
+
1928
+        $this->connectMockHooks($hookType, $view, $lockedPath, $lockTypePre, $lockTypePost);
1929
+
1930
+        // do operation
1931
+        call_user_func_array([$view, $operation], $operationArgs);
1932
+
1933
+        if ($hookType !== '') {
1934
+            $this->assertEquals($expectedLockBefore, $lockTypePre, 'File locked properly during pre-hook');
1935
+            $this->assertEquals($expectedLockAfter, $lockTypePost, 'File locked properly during post-hook');
1936
+            $this->assertEquals($expectedLockDuring, $lockTypeDuring, 'File locked properly during operation');
1937
+        } else {
1938
+            $this->assertNull($lockTypeDuring, 'File not locked during operation');
1939
+        }
1940
+
1941
+        $this->assertEquals($expectedStrayLock, $this->getFileLockType($view, $lockedPath));
1942
+
1943
+        /* Resume trash to avoid side effects */
1944
+        Server::get(ITrashManager::class)->resumeTrash();
1945
+    }
1946
+
1947
+    /**
1948
+     * Test locks for file_put_content with stream.
1949
+     * This code path uses $storage->fopen instead
1950
+     */
1951
+    public function testLockFilePutContentWithStream(): void {
1952
+        $view = new View('/' . $this->user . '/files/');
1953
+
1954
+        $path = 'test_file_put_contents.txt';
1955
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1956
+        $storage = $this->getMockBuilder(Temporary::class)
1957
+            ->onlyMethods(['fopen'])
1958
+            ->getMock();
1959
+
1960
+        Filesystem::mount($storage, [], $this->user . '/');
1961
+        $storage->mkdir('files');
1962
+
1963
+        $storage->expects($this->once())
1964
+            ->method('fopen')
1965
+            ->willReturnCallback(
1966
+                function () use ($view, $path, &$lockTypeDuring) {
1967
+                    $lockTypeDuring = $this->getFileLockType($view, $path);
1968
+
1969
+                    return fopen('php://temp', 'r+');
1970
+                }
1971
+            );
1972
+
1973
+        $this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost);
1974
+
1975
+        $this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation');
1976
+
1977
+        // do operation
1978
+        $view->file_put_contents($path, fopen('php://temp', 'r+'));
1979
+
1980
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook');
1981
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePost, 'File locked properly during post-hook');
1982
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation');
1983
+
1984
+        $this->assertNull($this->getFileLockType($view, $path));
1985
+    }
1986
+
1987
+    /**
1988
+     * Test locks for fopen with fclose at the end
1989
+     */
1990
+    public function testLockFopen(): void {
1991
+        $view = new View('/' . $this->user . '/files/');
1992
+
1993
+        $path = 'test_file_put_contents.txt';
1994
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1995
+        $storage = $this->getMockBuilder(Temporary::class)
1996
+            ->onlyMethods(['fopen'])
1997
+            ->getMock();
1998
+
1999
+        Filesystem::mount($storage, [], $this->user . '/');
2000
+        $storage->mkdir('files');
2001
+
2002
+        $storage->expects($this->once())
2003
+            ->method('fopen')
2004
+            ->willReturnCallback(
2005
+                function () use ($view, $path, &$lockTypeDuring) {
2006
+                    $lockTypeDuring = $this->getFileLockType($view, $path);
2007
+
2008
+                    return fopen('php://temp', 'r+');
2009
+                }
2010
+            );
2011
+
2012
+        $this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost);
2013
+
2014
+        $this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation');
2015
+
2016
+        // do operation
2017
+        $res = $view->fopen($path, 'w');
2018
+
2019
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook');
2020
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation');
2021
+        $this->assertNull($lockTypePost, 'No post hook, no lock check possible');
2022
+
2023
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File still locked after fopen');
2024
+
2025
+        fclose($res);
2026
+
2027
+        $this->assertNull($this->getFileLockType($view, $path), 'File unlocked after fclose');
2028
+    }
2029
+
2030
+    /**
2031
+     * Test locks for fopen with fclose at the end
2032
+     *
2033
+     *
2034
+     * @param string $operation operation name on the view
2035
+     * @param array $operationArgs arguments for the operation
2036
+     * @param string $path path of the locked item to check
2037
+     */
2038
+    #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
2039
+    public function testLockBasicOperationUnlocksAfterException(
2040
+        $operation,
2041
+        $operationArgs,
2042
+        $path,
2043
+    ): void {
2044
+        if ($operation === 'touch') {
2045
+            $this->markTestSkipped('touch handles storage exceptions internally');
2046
+        }
2047
+        $view = new View('/' . $this->user . '/files/');
2048
+
2049
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2050
+        $storage = $this->getMockBuilder(Temporary::class)
2051
+            ->onlyMethods([$operation])
2052
+            ->getMock();
2053
+
2054
+        /* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
2055
+        Server::get(ITrashManager::class)->pauseTrash();
2056
+        /* Same thing with encryption wrapper */
2057
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2058
+
2059
+        Filesystem::mount($storage, [], $this->user . '/');
2060
+
2061
+        // work directly on disk because mkdir might be mocked
2062
+        $realPath = $storage->getSourcePath('');
2063
+        mkdir($realPath . '/files');
2064
+        mkdir($realPath . '/files/dir');
2065
+        file_put_contents($realPath . '/files/test.txt', 'blah');
2066
+        $storage->getScanner()->scan('files');
2067
+
2068
+        $storage->expects($this->once())
2069
+            ->method($operation)
2070
+            ->willReturnCallback(
2071
+                function (): void {
2072
+                    throw new \Exception('Simulated exception');
2073
+                }
2074
+            );
2075
+
2076
+        $thrown = false;
2077
+        try {
2078
+            call_user_func_array([$view, $operation], $operationArgs);
2079
+        } catch (\Exception $e) {
2080
+            $thrown = true;
2081
+            $this->assertEquals('Simulated exception', $e->getMessage());
2082
+        }
2083
+        $this->assertTrue($thrown, 'Exception was rethrown');
2084
+        $this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception');
2085
+
2086
+        /* Resume trash to avoid side effects */
2087
+        Server::get(ITrashManager::class)->resumeTrash();
2088
+    }
2089
+
2090
+    public function testLockBasicOperationUnlocksAfterLockException(): void {
2091
+        $view = new View('/' . $this->user . '/files/');
2092
+
2093
+        $storage = new Temporary([]);
2094
+
2095
+        Filesystem::mount($storage, [], $this->user . '/');
2096
+
2097
+        $storage->mkdir('files');
2098
+        $storage->mkdir('files/dir');
2099
+        $storage->file_put_contents('files/test.txt', 'blah');
2100
+        $storage->getScanner()->scan('files');
2101
+
2102
+        // get a shared lock
2103
+        $handle = $view->fopen('test.txt', 'r');
2104
+
2105
+        $thrown = false;
2106
+        try {
2107
+            // try (and fail) to get a write lock
2108
+            $view->unlink('test.txt');
2109
+        } catch (\Exception $e) {
2110
+            $thrown = true;
2111
+            $this->assertInstanceOf(LockedException::class, $e);
2112
+        }
2113
+        $this->assertTrue($thrown, 'Exception was rethrown');
2114
+
2115
+        // clean shared lock
2116
+        fclose($handle);
2117
+
2118
+        $this->assertNull($this->getFileLockType($view, 'test.txt'), 'File got unlocked');
2119
+    }
2120
+
2121
+    /**
2122
+     * Test locks for fopen with fclose at the end
2123
+     *
2124
+     *
2125
+     * @param string $operation operation name on the view
2126
+     * @param array $operationArgs arguments for the operation
2127
+     * @param string $path path of the locked item to check
2128
+     * @param string $hookType hook type
2129
+     */
2130
+    #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
2131
+    public function testLockBasicOperationUnlocksAfterCancelledHook(
2132
+        $operation,
2133
+        $operationArgs,
2134
+        $path,
2135
+        $hookType,
2136
+    ): void {
2137
+        $view = new View('/' . $this->user . '/files/');
2138
+
2139
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2140
+        $storage = $this->getMockBuilder(Temporary::class)
2141
+            ->onlyMethods([$operation])
2142
+            ->getMock();
2143
+
2144
+        Filesystem::mount($storage, [], $this->user . '/');
2145
+        $storage->mkdir('files');
2146
+
2147
+        Util::connectHook(
2148
+            Filesystem::CLASSNAME,
2149
+            $hookType,
2150
+            HookHelper::class,
2151
+            'cancellingCallback'
2152
+        );
2153
+
2154
+        call_user_func_array([$view, $operation], $operationArgs);
2155
+
2156
+        $this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception');
2157
+    }
2158
+
2159
+    public static function lockFileRenameOrCopyDataProvider(): array {
2160
+        return [
2161
+            ['rename', ILockingProvider::LOCK_EXCLUSIVE],
2162
+            ['copy', ILockingProvider::LOCK_SHARED],
2163
+        ];
2164
+    }
2165
+
2166
+    /**
2167
+     * Test locks for rename or copy operation
2168
+     *
2169
+     *
2170
+     * @param string $operation operation to be done on the view
2171
+     * @param int $expectedLockTypeSourceDuring expected lock type on source file during
2172
+     *                                          the operation
2173
+     */
2174
+    #[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyDataProvider')]
2175
+    public function testLockFileRename($operation, $expectedLockTypeSourceDuring): void {
2176
+        $view = new View('/' . $this->user . '/files/');
2177
+
2178
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2179
+        $storage = $this->getMockBuilder(Temporary::class)
2180
+            ->onlyMethods([$operation, 'getMetaData', 'filemtime'])
2181
+            ->getMock();
2182
+
2183
+        $storage->expects($this->any())
2184
+            ->method('getMetaData')
2185
+            ->willReturn([
2186
+                'mtime' => 1885434487,
2187
+                'etag' => '',
2188
+                'mimetype' => 'text/plain',
2189
+                'permissions' => Constants::PERMISSION_ALL,
2190
+                'size' => 3
2191
+            ]);
2192
+        $storage->expects($this->any())
2193
+            ->method('filemtime')
2194
+            ->willReturn(123456789);
2195
+
2196
+        $sourcePath = 'original.txt';
2197
+        $targetPath = 'target.txt';
2198
+
2199
+        /* Disable encryption wrapper to avoid it intercepting mocked call */
2200
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2201
+
2202
+        Filesystem::mount($storage, [], $this->user . '/');
2203
+        $storage->mkdir('files');
2204
+        $view->file_put_contents($sourcePath, 'meh');
2205
+
2206
+        $storage->expects($this->once())
2207
+            ->method($operation)
2208
+            ->willReturnCallback(
2209
+                function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2210
+                    $lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2211
+                    $lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2212
+
2213
+                    return true;
2214
+                }
2215
+            );
2216
+
2217
+        $this->connectMockHooks($operation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2218
+        $this->connectMockHooks($operation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2219
+
2220
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2221
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2222
+
2223
+        $view->$operation($sourcePath, $targetPath);
2224
+
2225
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook');
2226
+        $this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation');
2227
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook');
2228
+
2229
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook');
2230
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation');
2231
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook');
2232
+
2233
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2234
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2235
+    }
2236
+
2237
+    /**
2238
+     * simulate a failed copy operation.
2239
+     * We expect that we catch the exception, free the lock and re-throw it.
2240
+     *
2241
+     */
2242
+    public function testLockFileCopyException(): void {
2243
+        $this->expectException(\Exception::class);
2244
+
2245
+        $view = new View('/' . $this->user . '/files/');
2246
+
2247
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2248
+        $storage = $this->getMockBuilder(Temporary::class)
2249
+            ->onlyMethods(['copy'])
2250
+            ->getMock();
2251
+
2252
+        $sourcePath = 'original.txt';
2253
+        $targetPath = 'target.txt';
2254
+
2255
+        /* Disable encryption wrapper to avoid it intercepting mocked call */
2256
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2257
+
2258
+        Filesystem::mount($storage, [], $this->user . '/');
2259
+        $storage->mkdir('files');
2260
+        $view->file_put_contents($sourcePath, 'meh');
2261
+
2262
+        $storage->expects($this->once())
2263
+            ->method('copy')
2264
+            ->willReturnCallback(
2265
+                function (): void {
2266
+                    throw new \Exception();
2267
+                }
2268
+            );
2269
+
2270
+        $this->connectMockHooks('copy', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2271
+        $this->connectMockHooks('copy', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2272
+
2273
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2274
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2275
+
2276
+        try {
2277
+            $view->copy($sourcePath, $targetPath);
2278
+        } catch (\Exception $e) {
2279
+            $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2280
+            $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2281
+            throw $e;
2282
+        }
2283
+    }
2284
+
2285
+    /**
2286
+     * Test rename operation: unlock first path when second path was locked
2287
+     */
2288
+    public function testLockFileRenameUnlockOnException(): void {
2289
+        self::loginAsUser('test');
2290
+
2291
+        $view = new View('/' . $this->user . '/files/');
2292
+
2293
+        $sourcePath = 'original.txt';
2294
+        $targetPath = 'target.txt';
2295
+        $view->file_put_contents($sourcePath, 'meh');
2296
+
2297
+        // simulate that the target path is already locked
2298
+        $view->lockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
2299
+
2300
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2301
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file is locked before operation');
2302
+
2303
+        $thrown = false;
2304
+        try {
2305
+            $view->rename($sourcePath, $targetPath);
2306
+        } catch (LockedException $e) {
2307
+            $thrown = true;
2308
+        }
2309
+
2310
+        $this->assertTrue($thrown, 'LockedException thrown');
2311
+
2312
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2313
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file still locked after operation');
2314
+
2315
+        $view->unlockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
2316
+    }
2317
+
2318
+    /**
2319
+     * Test rename operation: unlock first path when second path was locked
2320
+     */
2321
+    public function testGetOwner(): void {
2322
+        self::loginAsUser('test');
2323
+
2324
+        $view = new View('/test/files/');
2325
+
2326
+        $path = 'foo.txt';
2327
+        $view->file_put_contents($path, 'meh');
2328
+
2329
+        $this->assertEquals('test', $view->getFileInfo($path)->getOwner()->getUID());
2330
+
2331
+        $folderInfo = $view->getDirectoryContent('');
2332
+        $folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2333
+            return $info->getName() === 'foo.txt';
2334
+        }));
2335
+
2336
+        $this->assertEquals('test', $folderInfo[0]->getOwner()->getUID());
2337
+
2338
+        $subStorage = new Temporary();
2339
+        Filesystem::mount($subStorage, [], '/test/files/asd');
2340
+
2341
+        $folderInfo = $view->getDirectoryContent('');
2342
+        $folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2343
+            return $info->getName() === 'asd';
2344
+        }));
2345
+
2346
+        $this->assertEquals('test', $folderInfo[0]->getOwner()->getUID());
2347
+    }
2348
+
2349
+    public static function lockFileRenameOrCopyCrossStorageDataProvider(): array {
2350
+        return [
2351
+            ['rename', 'moveFromStorage', ILockingProvider::LOCK_EXCLUSIVE],
2352
+            ['copy', 'copyFromStorage', ILockingProvider::LOCK_SHARED],
2353
+        ];
2354
+    }
2355
+
2356
+    /**
2357
+     * Test locks for rename or copy operation cross-storage
2358
+     *
2359
+     *
2360
+     * @param string $viewOperation operation to be done on the view
2361
+     * @param string $storageOperation operation to be mocked on the storage
2362
+     * @param int $expectedLockTypeSourceDuring expected lock type on source file during
2363
+     *                                          the operation
2364
+     */
2365
+    #[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyCrossStorageDataProvider')]
2366
+    public function testLockFileRenameCrossStorage($viewOperation, $storageOperation, $expectedLockTypeSourceDuring): void {
2367
+        $view = new View('/' . $this->user . '/files/');
2368
+
2369
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2370
+        $storage = $this->getMockBuilder(Temporary::class)
2371
+            ->onlyMethods([$storageOperation])
2372
+            ->getMock();
2373
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage2 */
2374
+        $storage2 = $this->getMockBuilder(Temporary::class)
2375
+            ->onlyMethods([$storageOperation, 'getMetaData', 'filemtime'])
2376
+            ->getMock();
2377
+
2378
+        $storage2->expects($this->any())
2379
+            ->method('getMetaData')
2380
+            ->willReturn([
2381
+                'mtime' => 1885434487,
2382
+                'etag' => '',
2383
+                'mimetype' => 'text/plain',
2384
+                'permissions' => Constants::PERMISSION_ALL,
2385
+                'size' => 3
2386
+            ]);
2387
+        $storage2->expects($this->any())
2388
+            ->method('filemtime')
2389
+            ->willReturn(123456789);
2390
+
2391
+        $sourcePath = 'original.txt';
2392
+        $targetPath = 'substorage/target.txt';
2393
+
2394
+        /* Disable encryption wrapper to avoid it intercepting mocked call */
2395
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2396
+
2397
+        Filesystem::mount($storage, [], $this->user . '/');
2398
+        Filesystem::mount($storage2, [], $this->user . '/files/substorage');
2399
+        $storage->mkdir('files');
2400
+        $view->file_put_contents($sourcePath, 'meh');
2401
+        $storage2->getUpdater()->update('');
2402
+
2403
+        $storage->expects($this->never())
2404
+            ->method($storageOperation);
2405
+        $storage2->expects($this->once())
2406
+            ->method($storageOperation)
2407
+            ->willReturnCallback(
2408
+                function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2409
+                    $lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2410
+                    $lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2411
+
2412
+                    return true;
2413
+                }
2414
+            );
2415
+
2416
+        $this->connectMockHooks($viewOperation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2417
+        $this->connectMockHooks($viewOperation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2418
+
2419
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2420
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2421
+
2422
+        $view->$viewOperation($sourcePath, $targetPath);
2423
+
2424
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook');
2425
+        $this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation');
2426
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook');
2427
+
2428
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook');
2429
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation');
2430
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook');
2431
+
2432
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2433
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2434
+    }
2435
+
2436
+    /**
2437
+     * Test locks when moving a mount point
2438
+     */
2439
+    public function testLockMoveMountPoint(): void {
2440
+        self::loginAsUser('test');
2441
+
2442
+        [$mount] = $this->createTestMovableMountPoints([
2443
+            $this->user . '/files/substorage',
2444
+        ]);
2445
+
2446
+        $view = new View('/' . $this->user . '/files/');
2447
+        $view->mkdir('subdir');
2448
+
2449
+        $sourcePath = 'substorage';
2450
+        $targetPath = 'subdir/substorage_moved';
2451
+
2452
+        $mount->expects($this->once())
2453
+            ->method('moveMount')
2454
+            ->willReturnCallback(
2455
+                function ($target) use ($mount, $view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring, &$lockTypeSharedRootDuring) {
2456
+                    $lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath, true);
2457
+                    $lockTypeTargetDuring = $this->getFileLockType($view, $targetPath, true);
2458
+
2459
+                    $lockTypeSharedRootDuring = $this->getFileLockType($view, $sourcePath, false);
2460
+
2461
+                    $mount->setMountPoint($target);
2462
+
2463
+                    return true;
2464
+                }
2465
+            );
2466
+
2467
+        $this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost, true);
2468
+        $this->connectMockHooks('rename', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost, true);
2469
+        // in pre-hook, mount point is still on $sourcePath
2470
+        $this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSharedRootPre, $dummy, false);
2471
+        // in post-hook, mount point is now on $targetPath
2472
+        $this->connectMockHooks('rename', $view, $targetPath, $dummy, $lockTypeSharedRootPost, false);
2473
+
2474
+        $this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked before operation');
2475
+        $this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked before operation');
2476
+        $this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked before operation');
2477
+
2478
+        $view->rename($sourcePath, $targetPath);
2479
+
2480
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source path locked properly during pre-hook');
2481
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeSourceDuring, 'Source path locked properly during operation');
2482
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source path locked properly during post-hook');
2483
+
2484
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target path locked properly during pre-hook');
2485
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target path locked properly during operation');
2486
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target path locked properly during post-hook');
2487
+
2488
+        $this->assertNull($lockTypeSharedRootPre, 'Shared storage root not locked during pre-hook');
2489
+        $this->assertNull($lockTypeSharedRootDuring, 'Shared storage root not locked during move');
2490
+        $this->assertNull($lockTypeSharedRootPost, 'Shared storage root not locked during post-hook');
2491
+
2492
+        $this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked after operation');
2493
+        $this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked after operation');
2494
+        $this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked after operation');
2495
+    }
2496
+
2497
+    /**
2498
+     * Connect hook callbacks for hook type
2499
+     *
2500
+     * @param string $hookType hook type or null for none
2501
+     * @param View $view view to check the lock on
2502
+     * @param string $path path for which to check the lock
2503
+     * @param int $lockTypePre variable to receive lock type that was active in the pre-hook
2504
+     * @param int $lockTypePost variable to receive lock type that was active in the post-hook
2505
+     * @param bool $onMountPoint true to check the mount point instead of the
2506
+     *                           mounted storage
2507
+     */
2508
+    private function connectMockHooks($hookType, $view, $path, &$lockTypePre, &$lockTypePost, $onMountPoint = false) {
2509
+        if ($hookType === null) {
2510
+            return;
2511
+        }
2512
+
2513
+        $eventHandler = $this->getMockBuilder(TestEventHandler::class)
2514
+            ->onlyMethods(['preCallback', 'postCallback'])
2515
+            ->getMock();
2516
+
2517
+        $eventHandler->expects($this->any())
2518
+            ->method('preCallback')
2519
+            ->willReturnCallback(
2520
+                function () use ($view, $path, $onMountPoint, &$lockTypePre): void {
2521
+                    $lockTypePre = $this->getFileLockType($view, $path, $onMountPoint);
2522
+                }
2523
+            );
2524
+        $eventHandler->expects($this->any())
2525
+            ->method('postCallback')
2526
+            ->willReturnCallback(
2527
+                function () use ($view, $path, $onMountPoint, &$lockTypePost): void {
2528
+                    $lockTypePost = $this->getFileLockType($view, $path, $onMountPoint);
2529
+                }
2530
+            );
2531
+
2532
+        if ($hookType !== '') {
2533
+            Util::connectHook(
2534
+                Filesystem::CLASSNAME,
2535
+                $hookType,
2536
+                $eventHandler,
2537
+                'preCallback'
2538
+            );
2539
+            Util::connectHook(
2540
+                Filesystem::CLASSNAME,
2541
+                'post_' . $hookType,
2542
+                $eventHandler,
2543
+                'postCallback'
2544
+            );
2545
+        }
2546
+    }
2547
+
2548
+    /**
2549
+     * Returns the file lock type
2550
+     *
2551
+     * @param View $view view
2552
+     * @param string $path path
2553
+     * @param bool $onMountPoint true to check the mount point instead of the
2554
+     *                           mounted storage
2555
+     *
2556
+     * @return int lock type or null if file was not locked
2557
+     */
2558
+    private function getFileLockType(View $view, $path, $onMountPoint = false) {
2559
+        if ($this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE, $onMountPoint)) {
2560
+            return ILockingProvider::LOCK_EXCLUSIVE;
2561
+        } elseif ($this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED, $onMountPoint)) {
2562
+            return ILockingProvider::LOCK_SHARED;
2563
+        }
2564
+        return null;
2565
+    }
2566
+
2567
+
2568
+    public function testRemoveMoveableMountPoint(): void {
2569
+        $mountPoint = '/' . $this->user . '/files/mount/';
2570
+
2571
+        // Mock the mount point
2572
+        /** @var TestMoveableMountPoint|\PHPUnit\Framework\MockObject\MockObject $mount */
2573
+        $mount = $this->createMock(TestMoveableMountPoint::class);
2574
+        $mount->expects($this->once())
2575
+            ->method('getMountPoint')
2576
+            ->willReturn($mountPoint);
2577
+        $mount->expects($this->once())
2578
+            ->method('removeMount')
2579
+            ->willReturn('foo');
2580
+        $mount->expects($this->any())
2581
+            ->method('getInternalPath')
2582
+            ->willReturn('');
2583
+
2584
+        // Register mount
2585
+        Filesystem::getMountManager()->addMount($mount);
2586
+
2587
+        // Listen for events
2588
+        $eventHandler = $this->getMockBuilder(TestEventHandler::class)
2589
+            ->onlyMethods(['umount', 'post_umount'])
2590
+            ->getMock();
2591
+        $eventHandler->expects($this->once())
2592
+            ->method('umount')
2593
+            ->with([Filesystem::signal_param_path => '/mount']);
2594
+        $eventHandler->expects($this->once())
2595
+            ->method('post_umount')
2596
+            ->with([Filesystem::signal_param_path => '/mount']);
2597
+        Util::connectHook(
2598
+            Filesystem::CLASSNAME,
2599
+            'umount',
2600
+            $eventHandler,
2601
+            'umount'
2602
+        );
2603
+        Util::connectHook(
2604
+            Filesystem::CLASSNAME,
2605
+            'post_umount',
2606
+            $eventHandler,
2607
+            'post_umount'
2608
+        );
2609
+
2610
+        //Delete the mountpoint
2611
+        $view = new View('/' . $this->user . '/files');
2612
+        $this->assertEquals('foo', $view->rmdir('mount'));
2613
+    }
2614
+
2615
+    public static function mimeFilterProvider(): array {
2616
+        return [
2617
+            [null, ['test1.txt', 'test2.txt', 'test3.md', 'test4.png']],
2618
+            ['text/plain', ['test1.txt', 'test2.txt']],
2619
+            ['text/markdown', ['test3.md']],
2620
+            ['text', ['test1.txt', 'test2.txt', 'test3.md']],
2621
+        ];
2622
+    }
2623
+
2624
+    /**
2625
+     * @param string $filter
2626
+     * @param string[] $expected
2627
+     */
2628
+    #[\PHPUnit\Framework\Attributes\DataProvider('mimeFilterProvider')]
2629
+    public function testGetDirectoryContentMimeFilter($filter, $expected): void {
2630
+        $storage1 = new Temporary();
2631
+        $root = self::getUniqueID('/');
2632
+        Filesystem::mount($storage1, [], $root . '/');
2633
+        $view = new View($root);
2634
+
2635
+        $view->file_put_contents('test1.txt', 'asd');
2636
+        $view->file_put_contents('test2.txt', 'asd');
2637
+        $view->file_put_contents('test3.md', 'asd');
2638
+        $view->file_put_contents('test4.png', '');
2639
+
2640
+        $content = $view->getDirectoryContent('', $filter);
2641
+
2642
+        $files = array_map(function (FileInfo $info) {
2643
+            return $info->getName();
2644
+        }, $content);
2645
+        sort($files);
2646
+
2647
+        $this->assertEquals($expected, $files);
2648
+    }
2649
+
2650
+    public function testFilePutContentsClearsChecksum(): void {
2651
+        $storage = new Temporary([]);
2652
+        $scanner = $storage->getScanner();
2653
+        $storage->file_put_contents('foo.txt', 'bar');
2654
+        Filesystem::mount($storage, [], '/test/');
2655
+        $scanner->scan('');
2656
+
2657
+        $view = new View('/test/foo.txt');
2658
+        $view->putFileInfo('.', ['checksum' => '42']);
2659
+
2660
+        $this->assertEquals('bar', $view->file_get_contents(''));
2661
+        $fh = tmpfile();
2662
+        fwrite($fh, 'fooo');
2663
+        rewind($fh);
2664
+        clearstatcache();
2665
+        $view->file_put_contents('', $fh);
2666
+        $this->assertEquals('fooo', $view->file_get_contents(''));
2667
+        $data = $view->getFileInfo('.');
2668
+        $this->assertEquals('', $data->getChecksum());
2669
+    }
2670
+
2671
+    public function testDeleteGhostFile(): void {
2672
+        $storage = new Temporary([]);
2673
+        $scanner = $storage->getScanner();
2674
+        $cache = $storage->getCache();
2675
+        $storage->file_put_contents('foo.txt', 'bar');
2676
+        Filesystem::mount($storage, [], '/test/');
2677
+        $scanner->scan('');
2678
+
2679
+        $storage->unlink('foo.txt');
2680
+
2681
+        $this->assertTrue($cache->inCache('foo.txt'));
2682
+
2683
+        $view = new View('/test');
2684
+        $rootInfo = $view->getFileInfo('');
2685
+        $this->assertEquals(3, $rootInfo->getSize());
2686
+        $view->unlink('foo.txt');
2687
+        $newInfo = $view->getFileInfo('');
2688
+
2689
+        $this->assertFalse($cache->inCache('foo.txt'));
2690
+        $this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag());
2691
+        $this->assertEquals(0, $newInfo->getSize());
2692
+    }
2693
+
2694
+    public function testDeleteGhostFolder(): void {
2695
+        $storage = new Temporary([]);
2696
+        $scanner = $storage->getScanner();
2697
+        $cache = $storage->getCache();
2698
+        $storage->mkdir('foo');
2699
+        $storage->file_put_contents('foo/foo.txt', 'bar');
2700
+        Filesystem::mount($storage, [], '/test/');
2701
+        $scanner->scan('');
2702
+
2703
+        $storage->rmdir('foo');
2704
+
2705
+        $this->assertTrue($cache->inCache('foo'));
2706
+        $this->assertTrue($cache->inCache('foo/foo.txt'));
2707
+
2708
+        $view = new View('/test');
2709
+        $rootInfo = $view->getFileInfo('');
2710
+        $this->assertEquals(3, $rootInfo->getSize());
2711
+        $view->rmdir('foo');
2712
+        $newInfo = $view->getFileInfo('');
2713
+
2714
+        $this->assertFalse($cache->inCache('foo'));
2715
+        $this->assertFalse($cache->inCache('foo/foo.txt'));
2716
+        $this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag());
2717
+        $this->assertEquals(0, $newInfo->getSize());
2718
+    }
2719
+
2720
+    public function testCreateParentDirectories(): void {
2721
+        $view = $this->getMockBuilder(View::class)
2722
+            ->disableOriginalConstructor()
2723
+            ->onlyMethods([
2724
+                'is_file',
2725
+                'file_exists',
2726
+                'mkdir',
2727
+            ])
2728
+            ->getMock();
2729
+
2730
+        $view->expects($this->exactly(3))
2731
+            ->method('is_file')
2732
+            ->willReturnMap([
2733
+                ['/new', false],
2734
+                ['/new/folder', false],
2735
+                ['/new/folder/structure', false],
2736
+            ]);
2737
+        $view->expects($this->exactly(3))
2738
+            ->method('file_exists')
2739
+            ->willReturnMap([
2740
+                ['/new', true],
2741
+                ['/new/folder', false],
2742
+                ['/new/folder/structure', false],
2743
+            ]);
2744
+
2745
+        $calls = ['/new/folder', '/new/folder/structure'];
2746
+        $view->expects($this->exactly(2))
2747
+            ->method('mkdir')
2748
+            ->willReturnCallback(function ($dir) use (&$calls): void {
2749
+                $expected = array_shift($calls);
2750
+                $this->assertEquals($expected, $dir);
2751
+            });
2752
+
2753
+        $this->assertTrue(self::invokePrivate($view, 'createParentDirectories', ['/new/folder/structure']));
2754
+    }
2755
+
2756
+    public function testCreateParentDirectoriesWithExistingFile(): void {
2757
+        $view = $this->getMockBuilder(View::class)
2758
+            ->disableOriginalConstructor()
2759
+            ->onlyMethods([
2760
+                'is_file',
2761
+                'file_exists',
2762
+                'mkdir',
2763
+            ])
2764
+            ->getMock();
2765
+
2766
+        $view
2767
+            ->expects($this->once())
2768
+            ->method('is_file')
2769
+            ->with('/file.txt')
2770
+            ->willReturn(true);
2771
+        $this->assertFalse(self::invokePrivate($view, 'createParentDirectories', ['/file.txt/folder/structure']));
2772
+    }
2773
+
2774
+    public function testCacheExtension(): void {
2775
+        $storage = new Temporary([]);
2776
+        $scanner = $storage->getScanner();
2777
+        $storage->file_put_contents('foo.txt', 'bar');
2778
+        $scanner->scan('');
2779
+
2780
+        Filesystem::mount($storage, [], '/test/');
2781
+        $view = new View('/test');
2782
+
2783
+        $info = $view->getFileInfo('/foo.txt');
2784
+        $this->assertEquals(0, $info->getUploadTime());
2785
+        $this->assertEquals(0, $info->getCreationTime());
2786
+
2787
+        $view->putFileInfo('/foo.txt', ['upload_time' => 25]);
2788
+
2789
+        $info = $view->getFileInfo('/foo.txt');
2790
+        $this->assertEquals(25, $info->getUploadTime());
2791
+        $this->assertEquals(0, $info->getCreationTime());
2792
+    }
2793
+
2794
+    public function testFopenGone(): void {
2795
+        $storage = new Temporary([]);
2796
+        $scanner = $storage->getScanner();
2797
+        $storage->file_put_contents('foo.txt', 'bar');
2798
+        $scanner->scan('');
2799
+        $cache = $storage->getCache();
2800
+
2801
+        Filesystem::mount($storage, [], '/test/');
2802
+        $view = new View('/test');
2803
+
2804
+        $storage->unlink('foo.txt');
2805
+
2806
+        $this->assertTrue($cache->inCache('foo.txt'));
2807
+
2808
+        $this->assertFalse($view->fopen('foo.txt', 'r'));
2809
+
2810
+        $this->assertFalse($cache->inCache('foo.txt'));
2811
+    }
2812
+
2813
+    public function testMountpointParentsCreated(): void {
2814
+        $storage1 = $this->getTestStorage();
2815
+        Filesystem::mount($storage1, [], '/');
2816
+
2817
+        $storage2 = $this->getTestStorage();
2818
+        Filesystem::mount($storage2, [], '/A/B/C');
2819
+
2820
+        $rootView = new View('');
2821
+
2822
+        $folderData = $rootView->getDirectoryContent('/');
2823
+        $this->assertCount(4, $folderData);
2824
+        $this->assertEquals('folder', $folderData[0]['name']);
2825
+        $this->assertEquals('foo.png', $folderData[1]['name']);
2826
+        $this->assertEquals('foo.txt', $folderData[2]['name']);
2827
+        $this->assertEquals('A', $folderData[3]['name']);
2828
+
2829
+        $folderData = $rootView->getDirectoryContent('/A');
2830
+        $this->assertCount(1, $folderData);
2831
+        $this->assertEquals('B', $folderData[0]['name']);
2832
+
2833
+        $folderData = $rootView->getDirectoryContent('/A/B');
2834
+        $this->assertCount(1, $folderData);
2835
+        $this->assertEquals('C', $folderData[0]['name']);
2836
+
2837
+        $folderData = $rootView->getDirectoryContent('/A/B/C');
2838
+        $this->assertCount(3, $folderData);
2839
+        $this->assertEquals('folder', $folderData[0]['name']);
2840
+        $this->assertEquals('foo.png', $folderData[1]['name']);
2841
+        $this->assertEquals('foo.txt', $folderData[2]['name']);
2842
+    }
2843
+
2844
+    public function testCopyPreservesContent() {
2845
+        $viewUser1 = new View('/' . 'userId' . '/files');
2846
+        $viewUser1->mkdir('');
2847
+        $viewUser1->file_put_contents('foo.txt', 'foo');
2848
+        $viewUser1->copy('foo.txt', 'bar.txt');
2849
+        $this->assertEquals('foo', $viewUser1->file_get_contents('bar.txt'));
2850
+    }
2851 2851
 }
Please login to merge, or discard this patch.