Completed
Push — master ( e91840...aec12f )
by Joas
28:23
created
lib/private/Image.php 1 patch
Indentation   +1114 added lines, -1114 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,408 +758,408 @@  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
-		if ($this->valid()) {
1141
-			imagedestroy($this->resource);
1142
-		}
1143
-		$this->resource = false;
1144
-	}
1145
-
1146
-	public function __destruct() {
1147
-		$this->destroy();
1148
-	}
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
+        if ($this->valid()) {
1141
+            imagedestroy($this->resource);
1142
+        }
1143
+        $this->resource = false;
1144
+    }
1145
+
1146
+    public function __destruct() {
1147
+        $this->destroy();
1148
+    }
1149 1149
 }
1150 1150
 
1151 1151
 if (!function_exists('exif_imagetype')) {
1152
-	/**
1153
-	 * Workaround if exif_imagetype does not exist
1154
-	 *
1155
-	 * @link https://www.php.net/manual/en/function.exif-imagetype.php#80383
1156
-	 * @param string $fileName
1157
-	 * @return int|false
1158
-	 */
1159
-	function exif_imagetype(string $fileName) {
1160
-		if (($info = getimagesize($fileName)) !== false) {
1161
-			return $info[2];
1162
-		}
1163
-		return false;
1164
-	}
1152
+    /**
1153
+     * Workaround if exif_imagetype does not exist
1154
+     *
1155
+     * @link https://www.php.net/manual/en/function.exif-imagetype.php#80383
1156
+     * @param string $fileName
1157
+     * @return int|false
1158
+     */
1159
+    function exif_imagetype(string $fileName) {
1160
+        if (($info = getimagesize($fileName)) !== false) {
1161
+            return $info[2];
1162
+        }
1163
+        return false;
1164
+    }
1165 1165
 }
Please login to merge, or discard this patch.
lib/private/DB/ConnectionFactory.php 1 patch
Indentation   +246 added lines, -246 removed lines patch added patch discarded remove patch
@@ -21,278 +21,278 @@
 block discarded – undo
21 21
  * Takes care of creating and configuring Doctrine connections.
22 22
  */
23 23
 class ConnectionFactory {
24
-	/** @var string default database name */
25
-	public const DEFAULT_DBNAME = 'owncloud';
24
+    /** @var string default database name */
25
+    public const DEFAULT_DBNAME = 'owncloud';
26 26
 
27
-	/** @var string default database table prefix */
28
-	public const DEFAULT_DBTABLEPREFIX = 'oc_';
27
+    /** @var string default database table prefix */
28
+    public const DEFAULT_DBTABLEPREFIX = 'oc_';
29 29
 
30
-	/**
31
-	 * @var array
32
-	 *
33
-	 * Array mapping DBMS type to default connection parameters passed to
34
-	 * \Doctrine\DBAL\DriverManager::getConnection().
35
-	 */
36
-	protected $defaultConnectionParams = [
37
-		'mysql' => [
38
-			'adapter' => AdapterMySQL::class,
39
-			'charset' => 'UTF8',
40
-			'driver' => 'pdo_mysql',
41
-			'wrapperClass' => Connection::class,
42
-		],
43
-		'oci' => [
44
-			'adapter' => AdapterOCI8::class,
45
-			'charset' => 'AL32UTF8',
46
-			'driver' => 'oci8',
47
-			'wrapperClass' => OracleConnection::class,
48
-		],
49
-		'pgsql' => [
50
-			'adapter' => AdapterPgSql::class,
51
-			'driver' => 'pdo_pgsql',
52
-			'wrapperClass' => Connection::class,
53
-		],
54
-		'sqlite3' => [
55
-			'adapter' => AdapterSqlite::class,
56
-			'driver' => 'pdo_sqlite',
57
-			'wrapperClass' => Connection::class,
58
-		],
59
-	];
30
+    /**
31
+     * @var array
32
+     *
33
+     * Array mapping DBMS type to default connection parameters passed to
34
+     * \Doctrine\DBAL\DriverManager::getConnection().
35
+     */
36
+    protected $defaultConnectionParams = [
37
+        'mysql' => [
38
+            'adapter' => AdapterMySQL::class,
39
+            'charset' => 'UTF8',
40
+            'driver' => 'pdo_mysql',
41
+            'wrapperClass' => Connection::class,
42
+        ],
43
+        'oci' => [
44
+            'adapter' => AdapterOCI8::class,
45
+            'charset' => 'AL32UTF8',
46
+            'driver' => 'oci8',
47
+            'wrapperClass' => OracleConnection::class,
48
+        ],
49
+        'pgsql' => [
50
+            'adapter' => AdapterPgSql::class,
51
+            'driver' => 'pdo_pgsql',
52
+            'wrapperClass' => Connection::class,
53
+        ],
54
+        'sqlite3' => [
55
+            'adapter' => AdapterSqlite::class,
56
+            'driver' => 'pdo_sqlite',
57
+            'wrapperClass' => Connection::class,
58
+        ],
59
+    ];
60 60
 
61
-	private ShardConnectionManager $shardConnectionManager;
62
-	private ICacheFactory $cacheFactory;
61
+    private ShardConnectionManager $shardConnectionManager;
62
+    private ICacheFactory $cacheFactory;
63 63
 
64
-	public function __construct(
65
-		private SystemConfig $config,
66
-		?ICacheFactory $cacheFactory = null,
67
-	) {
68
-		if ($this->config->getValue('mysql.utf8mb4', false)) {
69
-			$this->defaultConnectionParams['mysql']['charset'] = 'utf8mb4';
70
-		}
71
-		$collationOverride = $this->config->getValue('mysql.collation', null);
72
-		if ($collationOverride) {
73
-			$this->defaultConnectionParams['mysql']['collation'] = $collationOverride;
74
-		}
75
-		$this->shardConnectionManager = new ShardConnectionManager($this->config, $this);
76
-		$this->cacheFactory = $cacheFactory ?? Server::get(ICacheFactory::class);
77
-	}
64
+    public function __construct(
65
+        private SystemConfig $config,
66
+        ?ICacheFactory $cacheFactory = null,
67
+    ) {
68
+        if ($this->config->getValue('mysql.utf8mb4', false)) {
69
+            $this->defaultConnectionParams['mysql']['charset'] = 'utf8mb4';
70
+        }
71
+        $collationOverride = $this->config->getValue('mysql.collation', null);
72
+        if ($collationOverride) {
73
+            $this->defaultConnectionParams['mysql']['collation'] = $collationOverride;
74
+        }
75
+        $this->shardConnectionManager = new ShardConnectionManager($this->config, $this);
76
+        $this->cacheFactory = $cacheFactory ?? Server::get(ICacheFactory::class);
77
+    }
78 78
 
79
-	/**
80
-	 * @brief Get default connection parameters for a given DBMS.
81
-	 * @param string $type DBMS type
82
-	 * @throws \InvalidArgumentException If $type is invalid
83
-	 * @return array Default connection parameters.
84
-	 */
85
-	public function getDefaultConnectionParams($type) {
86
-		$normalizedType = $this->normalizeType($type);
87
-		if (!isset($this->defaultConnectionParams[$normalizedType])) {
88
-			throw new \InvalidArgumentException("Unsupported type: $type");
89
-		}
90
-		$result = $this->defaultConnectionParams[$normalizedType];
91
-		/**
92
-		 * {@see \PDO::MYSQL_ATTR_FOUND_ROWS} may not be defined, e.g. when the MySQL
93
-		 * driver is missing. In this case, we won't be able to connect anyway.
94
-		 * In PHP 8.5 it's deprecated and {@see \Pdo\Mysql::ATTR_FOUND_ROWS} should be used,
95
-		 * but that is only available since PHP 8.4
96
-		 */
97
-		if ($normalizedType === 'mysql') {
98
-			if (PHP_VERSION_ID >= 80500 && class_exists(\Pdo\Mysql::class)) {
99
-				/** @psalm-suppress UndefinedClass */
100
-				$result['driverOptions'] = [
101
-					\Pdo\Mysql::ATTR_FOUND_ROWS => true,
102
-				];
103
-			} elseif (PHP_VERSION_ID < 80500 && defined('\PDO::MYSQL_ATTR_FOUND_ROWS')) {
104
-				$result['driverOptions'] = [
105
-					\PDO::MYSQL_ATTR_FOUND_ROWS => true,
106
-				];
107
-			}
108
-		}
109
-		return $result;
110
-	}
79
+    /**
80
+     * @brief Get default connection parameters for a given DBMS.
81
+     * @param string $type DBMS type
82
+     * @throws \InvalidArgumentException If $type is invalid
83
+     * @return array Default connection parameters.
84
+     */
85
+    public function getDefaultConnectionParams($type) {
86
+        $normalizedType = $this->normalizeType($type);
87
+        if (!isset($this->defaultConnectionParams[$normalizedType])) {
88
+            throw new \InvalidArgumentException("Unsupported type: $type");
89
+        }
90
+        $result = $this->defaultConnectionParams[$normalizedType];
91
+        /**
92
+         * {@see \PDO::MYSQL_ATTR_FOUND_ROWS} may not be defined, e.g. when the MySQL
93
+         * driver is missing. In this case, we won't be able to connect anyway.
94
+         * In PHP 8.5 it's deprecated and {@see \Pdo\Mysql::ATTR_FOUND_ROWS} should be used,
95
+         * but that is only available since PHP 8.4
96
+         */
97
+        if ($normalizedType === 'mysql') {
98
+            if (PHP_VERSION_ID >= 80500 && class_exists(\Pdo\Mysql::class)) {
99
+                /** @psalm-suppress UndefinedClass */
100
+                $result['driverOptions'] = [
101
+                    \Pdo\Mysql::ATTR_FOUND_ROWS => true,
102
+                ];
103
+            } elseif (PHP_VERSION_ID < 80500 && defined('\PDO::MYSQL_ATTR_FOUND_ROWS')) {
104
+                $result['driverOptions'] = [
105
+                    \PDO::MYSQL_ATTR_FOUND_ROWS => true,
106
+                ];
107
+            }
108
+        }
109
+        return $result;
110
+    }
111 111
 
112
-	/**
113
-	 * @brief Get default connection parameters for a given DBMS.
114
-	 * @param string $type DBMS type
115
-	 * @param array $additionalConnectionParams Additional connection parameters
116
-	 * @return \OC\DB\Connection
117
-	 */
118
-	public function getConnection(string $type, array $additionalConnectionParams): Connection {
119
-		$normalizedType = $this->normalizeType($type);
120
-		$eventManager = new EventManager();
121
-		$eventManager->addEventSubscriber(new SetTransactionIsolationLevel());
122
-		$connectionParams = $this->createConnectionParams('', $additionalConnectionParams, $type);
123
-		switch ($normalizedType) {
124
-			case 'pgsql':
125
-				// pg_connect used by Doctrine DBAL does not support URI notation (enclosed in brackets)
126
-				$matches = [];
127
-				if (preg_match('/^\[([^\]]+)\]$/', $connectionParams['host'], $matches)) {
128
-					// Host variable carries a port or socket.
129
-					$connectionParams['host'] = $matches[1];
130
-				}
131
-				break;
112
+    /**
113
+     * @brief Get default connection parameters for a given DBMS.
114
+     * @param string $type DBMS type
115
+     * @param array $additionalConnectionParams Additional connection parameters
116
+     * @return \OC\DB\Connection
117
+     */
118
+    public function getConnection(string $type, array $additionalConnectionParams): Connection {
119
+        $normalizedType = $this->normalizeType($type);
120
+        $eventManager = new EventManager();
121
+        $eventManager->addEventSubscriber(new SetTransactionIsolationLevel());
122
+        $connectionParams = $this->createConnectionParams('', $additionalConnectionParams, $type);
123
+        switch ($normalizedType) {
124
+            case 'pgsql':
125
+                // pg_connect used by Doctrine DBAL does not support URI notation (enclosed in brackets)
126
+                $matches = [];
127
+                if (preg_match('/^\[([^\]]+)\]$/', $connectionParams['host'], $matches)) {
128
+                    // Host variable carries a port or socket.
129
+                    $connectionParams['host'] = $matches[1];
130
+                }
131
+                break;
132 132
 
133
-			case 'oci':
134
-				$eventManager->addEventSubscriber(new OracleSessionInit);
135
-				$connectionParams = $this->forceConnectionStringOracle($connectionParams);
136
-				$connectionParams['primary'] = $this->forceConnectionStringOracle($connectionParams['primary']);
137
-				$connectionParams['replica'] = array_map([$this, 'forceConnectionStringOracle'], $connectionParams['replica']);
138
-				break;
133
+            case 'oci':
134
+                $eventManager->addEventSubscriber(new OracleSessionInit);
135
+                $connectionParams = $this->forceConnectionStringOracle($connectionParams);
136
+                $connectionParams['primary'] = $this->forceConnectionStringOracle($connectionParams['primary']);
137
+                $connectionParams['replica'] = array_map([$this, 'forceConnectionStringOracle'], $connectionParams['replica']);
138
+                break;
139 139
 
140
-			case 'sqlite3':
141
-				$journalMode = $connectionParams['sqlite.journal_mode'];
142
-				$connectionParams['platform'] = new OCSqlitePlatform();
143
-				$eventManager->addEventSubscriber(new SQLiteSessionInit(true, $journalMode));
144
-				break;
145
-		}
146
-		/** @var Connection $connection */
147
-		$connection = DriverManager::getConnection(
148
-			$connectionParams,
149
-			new Configuration(),
150
-			$eventManager
151
-		);
152
-		return $connection;
153
-	}
140
+            case 'sqlite3':
141
+                $journalMode = $connectionParams['sqlite.journal_mode'];
142
+                $connectionParams['platform'] = new OCSqlitePlatform();
143
+                $eventManager->addEventSubscriber(new SQLiteSessionInit(true, $journalMode));
144
+                break;
145
+        }
146
+        /** @var Connection $connection */
147
+        $connection = DriverManager::getConnection(
148
+            $connectionParams,
149
+            new Configuration(),
150
+            $eventManager
151
+        );
152
+        return $connection;
153
+    }
154 154
 
155
-	/**
156
-	 * @brief Normalize DBMS type
157
-	 * @param string $type DBMS type
158
-	 * @return string Normalized DBMS type
159
-	 */
160
-	public function normalizeType($type) {
161
-		return $type === 'sqlite' ? 'sqlite3' : $type;
162
-	}
155
+    /**
156
+     * @brief Normalize DBMS type
157
+     * @param string $type DBMS type
158
+     * @return string Normalized DBMS type
159
+     */
160
+    public function normalizeType($type) {
161
+        return $type === 'sqlite' ? 'sqlite3' : $type;
162
+    }
163 163
 
164
-	/**
165
-	 * Checks whether the specified DBMS type is valid.
166
-	 *
167
-	 * @param string $type
168
-	 * @return bool
169
-	 */
170
-	public function isValidType($type) {
171
-		$normalizedType = $this->normalizeType($type);
172
-		return isset($this->defaultConnectionParams[$normalizedType]);
173
-	}
164
+    /**
165
+     * Checks whether the specified DBMS type is valid.
166
+     *
167
+     * @param string $type
168
+     * @return bool
169
+     */
170
+    public function isValidType($type) {
171
+        $normalizedType = $this->normalizeType($type);
172
+        return isset($this->defaultConnectionParams[$normalizedType]);
173
+    }
174 174
 
175
-	/**
176
-	 * Create the connection parameters for the config
177
-	 */
178
-	public function createConnectionParams(string $configPrefix = '', array $additionalConnectionParams = [], ?string $type = null) {
179
-		// use provided type or if null use type from config
180
-		$type = $type ?? $this->config->getValue('dbtype', 'sqlite');
175
+    /**
176
+     * Create the connection parameters for the config
177
+     */
178
+    public function createConnectionParams(string $configPrefix = '', array $additionalConnectionParams = [], ?string $type = null) {
179
+        // use provided type or if null use type from config
180
+        $type = $type ?? $this->config->getValue('dbtype', 'sqlite');
181 181
 
182
-		$connectionParams = array_merge($this->getDefaultConnectionParams($type), [
183
-			'user' => $this->config->getValue($configPrefix . 'dbuser', $this->config->getValue('dbuser', '')),
184
-			'password' => $this->config->getValue($configPrefix . 'dbpassword', $this->config->getValue('dbpassword', '')),
185
-		]);
186
-		$name = $this->config->getValue($configPrefix . 'dbname', $this->config->getValue('dbname', self::DEFAULT_DBNAME));
182
+        $connectionParams = array_merge($this->getDefaultConnectionParams($type), [
183
+            'user' => $this->config->getValue($configPrefix . 'dbuser', $this->config->getValue('dbuser', '')),
184
+            'password' => $this->config->getValue($configPrefix . 'dbpassword', $this->config->getValue('dbpassword', '')),
185
+        ]);
186
+        $name = $this->config->getValue($configPrefix . 'dbname', $this->config->getValue('dbname', self::DEFAULT_DBNAME));
187 187
 
188
-		if ($this->normalizeType($type) === 'sqlite3') {
189
-			$dataDir = $this->config->getValue('datadirectory', \OC::$SERVERROOT . '/data');
190
-			$connectionParams['path'] = $dataDir . '/' . $name . '.db';
191
-		} else {
192
-			$host = $this->config->getValue($configPrefix . 'dbhost', $this->config->getValue('dbhost', ''));
193
-			$connectionParams = array_merge($connectionParams, $this->splitHostFromPortAndSocket($host));
194
-			$connectionParams['dbname'] = $name;
195
-		}
188
+        if ($this->normalizeType($type) === 'sqlite3') {
189
+            $dataDir = $this->config->getValue('datadirectory', \OC::$SERVERROOT . '/data');
190
+            $connectionParams['path'] = $dataDir . '/' . $name . '.db';
191
+        } else {
192
+            $host = $this->config->getValue($configPrefix . 'dbhost', $this->config->getValue('dbhost', ''));
193
+            $connectionParams = array_merge($connectionParams, $this->splitHostFromPortAndSocket($host));
194
+            $connectionParams['dbname'] = $name;
195
+        }
196 196
 
197
-		$connectionParams['tablePrefix'] = $this->config->getValue('dbtableprefix', self::DEFAULT_DBTABLEPREFIX);
198
-		$connectionParams['sqlite.journal_mode'] = $this->config->getValue('sqlite.journal_mode', 'WAL');
197
+        $connectionParams['tablePrefix'] = $this->config->getValue('dbtableprefix', self::DEFAULT_DBTABLEPREFIX);
198
+        $connectionParams['sqlite.journal_mode'] = $this->config->getValue('sqlite.journal_mode', 'WAL');
199 199
 
200
-		//additional driver options, eg. for mysql ssl
201
-		$driverOptions = $this->config->getValue($configPrefix . 'dbdriveroptions', $this->config->getValue('dbdriveroptions', null));
202
-		if ($driverOptions) {
203
-			$connectionParams['driverOptions'] = $driverOptions;
204
-		}
200
+        //additional driver options, eg. for mysql ssl
201
+        $driverOptions = $this->config->getValue($configPrefix . 'dbdriveroptions', $this->config->getValue('dbdriveroptions', null));
202
+        if ($driverOptions) {
203
+            $connectionParams['driverOptions'] = $driverOptions;
204
+        }
205 205
 
206
-		// set default table creation options
207
-		$connectionParams['defaultTableOptions'] = [
208
-			'collate' => 'utf8_bin',
209
-			'tablePrefix' => $connectionParams['tablePrefix']
210
-		];
206
+        // set default table creation options
207
+        $connectionParams['defaultTableOptions'] = [
208
+            'collate' => 'utf8_bin',
209
+            'tablePrefix' => $connectionParams['tablePrefix']
210
+        ];
211 211
 
212
-		if ($type === 'pgsql') {
213
-			$pgsqlSsl = $this->config->getValue('pgsql_ssl', false);
214
-			if (is_array($pgsqlSsl)) {
215
-				$connectionParams['sslmode'] = $pgsqlSsl['mode'] ?? '';
216
-				$connectionParams['sslrootcert'] = $pgsqlSsl['rootcert'] ?? '';
217
-				$connectionParams['sslcert'] = $pgsqlSsl['cert'] ?? '';
218
-				$connectionParams['sslkey'] = $pgsqlSsl['key'] ?? '';
219
-				$connectionParams['sslcrl'] = $pgsqlSsl['crl'] ?? '';
220
-			}
221
-		}
212
+        if ($type === 'pgsql') {
213
+            $pgsqlSsl = $this->config->getValue('pgsql_ssl', false);
214
+            if (is_array($pgsqlSsl)) {
215
+                $connectionParams['sslmode'] = $pgsqlSsl['mode'] ?? '';
216
+                $connectionParams['sslrootcert'] = $pgsqlSsl['rootcert'] ?? '';
217
+                $connectionParams['sslcert'] = $pgsqlSsl['cert'] ?? '';
218
+                $connectionParams['sslkey'] = $pgsqlSsl['key'] ?? '';
219
+                $connectionParams['sslcrl'] = $pgsqlSsl['crl'] ?? '';
220
+            }
221
+        }
222 222
 
223
-		if ($type === 'mysql' && $this->config->getValue('mysql.utf8mb4', false)) {
224
-			$connectionParams['defaultTableOptions'] = [
225
-				'collate' => 'utf8mb4_bin',
226
-				'charset' => 'utf8mb4',
227
-				'tablePrefix' => $connectionParams['tablePrefix']
228
-			];
229
-		}
223
+        if ($type === 'mysql' && $this->config->getValue('mysql.utf8mb4', false)) {
224
+            $connectionParams['defaultTableOptions'] = [
225
+                'collate' => 'utf8mb4_bin',
226
+                'charset' => 'utf8mb4',
227
+                'tablePrefix' => $connectionParams['tablePrefix']
228
+            ];
229
+        }
230 230
 
231
-		if ($this->config->getValue('dbpersistent', false)) {
232
-			$connectionParams['persistent'] = true;
233
-		}
231
+        if ($this->config->getValue('dbpersistent', false)) {
232
+            $connectionParams['persistent'] = true;
233
+        }
234 234
 
235
-		$connectionParams['sharding'] = $this->config->getValue('dbsharding', []);
236
-		if (!empty($connectionParams['sharding'])) {
237
-			$connectionParams['shard_connection_manager'] = $this->shardConnectionManager;
238
-			$connectionParams['auto_increment_handler'] = new AutoIncrementHandler(
239
-				$this->cacheFactory,
240
-				$this->shardConnectionManager,
241
-			);
242
-		} else {
243
-			// just in case only the presence could lead to funny behaviour
244
-			unset($connectionParams['sharding']);
245
-		}
235
+        $connectionParams['sharding'] = $this->config->getValue('dbsharding', []);
236
+        if (!empty($connectionParams['sharding'])) {
237
+            $connectionParams['shard_connection_manager'] = $this->shardConnectionManager;
238
+            $connectionParams['auto_increment_handler'] = new AutoIncrementHandler(
239
+                $this->cacheFactory,
240
+                $this->shardConnectionManager,
241
+            );
242
+        } else {
243
+            // just in case only the presence could lead to funny behaviour
244
+            unset($connectionParams['sharding']);
245
+        }
246 246
 
247
-		$connectionParams = array_merge($connectionParams, $additionalConnectionParams);
247
+        $connectionParams = array_merge($connectionParams, $additionalConnectionParams);
248 248
 
249
-		$replica = $this->config->getValue($configPrefix . 'dbreplica', $this->config->getValue('dbreplica', [])) ?: [$connectionParams];
250
-		return array_merge($connectionParams, [
251
-			'primary' => $connectionParams,
252
-			'replica' => $replica,
253
-		]);
254
-	}
249
+        $replica = $this->config->getValue($configPrefix . 'dbreplica', $this->config->getValue('dbreplica', [])) ?: [$connectionParams];
250
+        return array_merge($connectionParams, [
251
+            'primary' => $connectionParams,
252
+            'replica' => $replica,
253
+        ]);
254
+    }
255 255
 
256
-	/**
257
-	 * @param string $host
258
-	 * @return array
259
-	 */
260
-	protected function splitHostFromPortAndSocket($host): array {
261
-		$params = [
262
-			'host' => $host,
263
-		];
256
+    /**
257
+     * @param string $host
258
+     * @return array
259
+     */
260
+    protected function splitHostFromPortAndSocket($host): array {
261
+        $params = [
262
+            'host' => $host,
263
+        ];
264 264
 
265
-		$matches = [];
266
-		if (preg_match('/^(.*):([^\]:]+)$/', $host, $matches)) {
267
-			// Host variable carries a port or socket.
268
-			$params['host'] = $matches[1];
269
-			if (is_numeric($matches[2])) {
270
-				$params['port'] = (int)$matches[2];
271
-			} else {
272
-				$params['unix_socket'] = $matches[2];
273
-			}
274
-		}
265
+        $matches = [];
266
+        if (preg_match('/^(.*):([^\]:]+)$/', $host, $matches)) {
267
+            // Host variable carries a port or socket.
268
+            $params['host'] = $matches[1];
269
+            if (is_numeric($matches[2])) {
270
+                $params['port'] = (int)$matches[2];
271
+            } else {
272
+                $params['unix_socket'] = $matches[2];
273
+            }
274
+        }
275 275
 
276
-		return $params;
277
-	}
276
+        return $params;
277
+    }
278 278
 
279
-	protected function forceConnectionStringOracle(array $connectionParams): array {
280
-		// the driverOptions are unused in dbal and need to be mapped to the parameters
281
-		if (isset($connectionParams['driverOptions'])) {
282
-			$connectionParams = array_merge($connectionParams, $connectionParams['driverOptions']);
283
-		}
284
-		$host = $connectionParams['host'];
285
-		$port = $connectionParams['port'] ?? null;
286
-		$dbName = $connectionParams['dbname'];
279
+    protected function forceConnectionStringOracle(array $connectionParams): array {
280
+        // the driverOptions are unused in dbal and need to be mapped to the parameters
281
+        if (isset($connectionParams['driverOptions'])) {
282
+            $connectionParams = array_merge($connectionParams, $connectionParams['driverOptions']);
283
+        }
284
+        $host = $connectionParams['host'];
285
+        $port = $connectionParams['port'] ?? null;
286
+        $dbName = $connectionParams['dbname'];
287 287
 
288
-		// we set the connect string as dbname and unset the host to coerce doctrine into using it as connect string
289
-		if ($host === '') {
290
-			$connectionParams['dbname'] = $dbName; // use dbname as easy connect name
291
-		} else {
292
-			$connectionParams['dbname'] = '//' . $host . (!empty($port) ? ":{$port}" : '') . '/' . $dbName;
293
-		}
294
-		unset($connectionParams['host']);
288
+        // we set the connect string as dbname and unset the host to coerce doctrine into using it as connect string
289
+        if ($host === '') {
290
+            $connectionParams['dbname'] = $dbName; // use dbname as easy connect name
291
+        } else {
292
+            $connectionParams['dbname'] = '//' . $host . (!empty($port) ? ":{$port}" : '') . '/' . $dbName;
293
+        }
294
+        unset($connectionParams['host']);
295 295
 
296
-		return $connectionParams;
297
-	}
296
+        return $connectionParams;
297
+    }
298 298
 }
Please login to merge, or discard this patch.
lib/private/DB/SQLiteSessionInit.php 1 patch
Indentation   +38 added lines, -38 removed lines patch added patch discarded remove patch
@@ -12,46 +12,46 @@
 block discarded – undo
12 12
 use Doctrine\DBAL\Events;
13 13
 
14 14
 class SQLiteSessionInit implements EventSubscriber {
15
-	/**
16
-	 * @var bool
17
-	 */
18
-	private $caseSensitiveLike;
15
+    /**
16
+     * @var bool
17
+     */
18
+    private $caseSensitiveLike;
19 19
 
20
-	/**
21
-	 * @var string
22
-	 */
23
-	private $journalMode;
20
+    /**
21
+     * @var string
22
+     */
23
+    private $journalMode;
24 24
 
25
-	/**
26
-	 * Configure case sensitive like for each connection
27
-	 *
28
-	 * @param bool $caseSensitiveLike
29
-	 * @param string $journalMode
30
-	 */
31
-	public function __construct($caseSensitiveLike, $journalMode) {
32
-		$this->caseSensitiveLike = $caseSensitiveLike;
33
-		$this->journalMode = $journalMode;
34
-	}
25
+    /**
26
+     * Configure case sensitive like for each connection
27
+     *
28
+     * @param bool $caseSensitiveLike
29
+     * @param string $journalMode
30
+     */
31
+    public function __construct($caseSensitiveLike, $journalMode) {
32
+        $this->caseSensitiveLike = $caseSensitiveLike;
33
+        $this->journalMode = $journalMode;
34
+    }
35 35
 
36
-	/**
37
-	 * @param ConnectionEventArgs $args
38
-	 * @return void
39
-	 */
40
-	public function postConnect(ConnectionEventArgs $args) {
41
-		$sensitive = $this->caseSensitiveLike ? 'true' : 'false';
42
-		$args->getConnection()->executeUpdate('PRAGMA case_sensitive_like = ' . $sensitive);
43
-		$args->getConnection()->executeUpdate('PRAGMA journal_mode = ' . $this->journalMode);
44
-		/** @var \Doctrine\DBAL\Driver\PDO\Connection $connection */
45
-		$connection = $args->getConnection()->getWrappedConnection();
46
-		$pdo = $connection->getWrappedConnection();
47
-		if (PHP_VERSION_ID >= 80500 && method_exists($pdo, 'createFunction')) {
48
-			$pdo->createFunction('md5', 'md5', 1);
49
-		} else {
50
-			$pdo->sqliteCreateFunction('md5', 'md5', 1);
51
-		}
52
-	}
36
+    /**
37
+     * @param ConnectionEventArgs $args
38
+     * @return void
39
+     */
40
+    public function postConnect(ConnectionEventArgs $args) {
41
+        $sensitive = $this->caseSensitiveLike ? 'true' : 'false';
42
+        $args->getConnection()->executeUpdate('PRAGMA case_sensitive_like = ' . $sensitive);
43
+        $args->getConnection()->executeUpdate('PRAGMA journal_mode = ' . $this->journalMode);
44
+        /** @var \Doctrine\DBAL\Driver\PDO\Connection $connection */
45
+        $connection = $args->getConnection()->getWrappedConnection();
46
+        $pdo = $connection->getWrappedConnection();
47
+        if (PHP_VERSION_ID >= 80500 && method_exists($pdo, 'createFunction')) {
48
+            $pdo->createFunction('md5', 'md5', 1);
49
+        } else {
50
+            $pdo->sqliteCreateFunction('md5', 'md5', 1);
51
+        }
52
+    }
53 53
 
54
-	public function getSubscribedEvents() {
55
-		return [Events::postConnect];
56
-	}
54
+    public function getSubscribedEvents() {
55
+        return [Events::postConnect];
56
+    }
57 57
 }
Please login to merge, or discard this patch.
lib/private/Log.php 2 patches
Indentation   +412 added lines, -412 removed lines patch added patch discarded remove patch
@@ -35,416 +35,416 @@
 block discarded – undo
35 35
  * MonoLog is an example implementing this interface.
36 36
  */
37 37
 class Log implements ILogger, IDataLogger {
38
-	private ?bool $logConditionSatisfied = null;
39
-	private ?IEventDispatcher $eventDispatcher = null;
40
-	private int $nestingLevel = 0;
41
-
42
-	public function __construct(
43
-		private IWriter $logger,
44
-		private SystemConfig $config,
45
-		private Normalizer $normalizer = new Normalizer(),
46
-		private ?IRegistry $crashReporters = null,
47
-	) {
48
-	}
49
-
50
-	public function setEventDispatcher(IEventDispatcher $eventDispatcher): void {
51
-		$this->eventDispatcher = $eventDispatcher;
52
-	}
53
-
54
-	/**
55
-	 * System is unusable.
56
-	 *
57
-	 * @param string $message
58
-	 * @param array $context
59
-	 */
60
-	public function emergency(string $message, array $context = []): void {
61
-		$this->log(ILogger::FATAL, $message, $context);
62
-	}
63
-
64
-	/**
65
-	 * Action must be taken immediately.
66
-	 *
67
-	 * Example: Entire website down, database unavailable, etc. This should
68
-	 * trigger the SMS alerts and wake you up.
69
-	 *
70
-	 * @param string $message
71
-	 * @param array $context
72
-	 */
73
-	public function alert(string $message, array $context = []): void {
74
-		$this->log(ILogger::ERROR, $message, $context);
75
-	}
76
-
77
-	/**
78
-	 * Critical conditions.
79
-	 *
80
-	 * Example: Application component unavailable, unexpected exception.
81
-	 *
82
-	 * @param string $message
83
-	 * @param array $context
84
-	 */
85
-	public function critical(string $message, array $context = []): void {
86
-		$this->log(ILogger::ERROR, $message, $context);
87
-	}
88
-
89
-	/**
90
-	 * Runtime errors that do not require immediate action but should typically
91
-	 * be logged and monitored.
92
-	 *
93
-	 * @param string $message
94
-	 * @param array $context
95
-	 */
96
-	public function error(string $message, array $context = []): void {
97
-		$this->log(ILogger::ERROR, $message, $context);
98
-	}
99
-
100
-	/**
101
-	 * Exceptional occurrences that are not errors.
102
-	 *
103
-	 * Example: Use of deprecated APIs, poor use of an API, undesirable things
104
-	 * that are not necessarily wrong.
105
-	 *
106
-	 * @param string $message
107
-	 * @param array $context
108
-	 */
109
-	public function warning(string $message, array $context = []): void {
110
-		$this->log(ILogger::WARN, $message, $context);
111
-	}
112
-
113
-	/**
114
-	 * Normal but significant events.
115
-	 *
116
-	 * @param string $message
117
-	 * @param array $context
118
-	 */
119
-	public function notice(string $message, array $context = []): void {
120
-		$this->log(ILogger::INFO, $message, $context);
121
-	}
122
-
123
-	/**
124
-	 * Interesting events.
125
-	 *
126
-	 * Example: User logs in, SQL logs.
127
-	 *
128
-	 * @param string $message
129
-	 * @param array $context
130
-	 */
131
-	public function info(string $message, array $context = []): void {
132
-		$this->log(ILogger::INFO, $message, $context);
133
-	}
134
-
135
-	/**
136
-	 * Detailed debug information.
137
-	 *
138
-	 * @param string $message
139
-	 * @param array $context
140
-	 */
141
-	public function debug(string $message, array $context = []): void {
142
-		$this->log(ILogger::DEBUG, $message, $context);
143
-	}
144
-
145
-
146
-	/**
147
-	 * Logs with an arbitrary level.
148
-	 *
149
-	 * @param int $level
150
-	 * @param string $message
151
-	 * @param array $context
152
-	 */
153
-	public function log(int $level, string $message, array $context = []): void {
154
-		$minLevel = $this->getLogLevel($context, $message);
155
-		if ($level < $minLevel
156
-			&& (($this->crashReporters?->hasReporters() ?? false) === false)
157
-			&& (($this->eventDispatcher?->hasListeners(BeforeMessageLoggedEvent::class) ?? false) === false)) {
158
-			return; // no crash reporter, no listeners, we can stop for lower log level
159
-		}
160
-
161
-		$context = array_map($this->normalizer->format(...), $context);
162
-
163
-		$app = $context['app'] ?? 'no app in context';
164
-		$entry = $this->interpolateMessage($context, $message);
165
-
166
-		$this->eventDispatcher?->dispatchTyped(new BeforeMessageLoggedEvent($app, $level, $entry));
167
-
168
-		$hasBacktrace = isset($entry['exception']);
169
-		$logBacktrace = $this->config->getValue('log.backtrace', false);
170
-		if (!$hasBacktrace && $logBacktrace) {
171
-			$entry['backtrace'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
172
-		}
173
-
174
-		try {
175
-			if ($level >= $minLevel) {
176
-				$this->writeLog($app, $entry, $level);
177
-
178
-				if ($this->crashReporters !== null) {
179
-					$messageContext = array_merge(
180
-						$context,
181
-						[
182
-							'level' => $level
183
-						]
184
-					);
185
-					$this->crashReporters->delegateMessage($entry['message'], $messageContext);
186
-				}
187
-			} else {
188
-				$this->crashReporters?->delegateBreadcrumb($entry['message'], 'log', $context);
189
-			}
190
-		} catch (Throwable $e) {
191
-			// make sure we dont hard crash if logging fails
192
-		}
193
-	}
194
-
195
-	public function getLogLevel(array $context, string $message): int {
196
-		if ($this->nestingLevel > 1) {
197
-			return ILogger::WARN;
198
-		}
199
-
200
-		$this->nestingLevel++;
201
-		/**
202
-		 * @psalm-var array{
203
-		 *   shared_secret?: string,
204
-		 *   users?: string[],
205
-		 *   apps?: string[],
206
-		 *   matches?: array<array-key, array{
207
-		 *     shared_secret?: string,
208
-		 *     users?: string[],
209
-		 *     apps?: string[],
210
-		 *     message?: string,
211
-		 *     loglevel: 0|1|2|3|4,
212
-		 *   }>
213
-		 * } $logCondition
214
-		 */
215
-		$logCondition = $this->config->getValue('log.condition', []);
216
-
217
-		$userId = false;
218
-
219
-		/**
220
-		 * check for a special log condition - this enables an increased log on
221
-		 * a per request/user base
222
-		 */
223
-		if ($this->logConditionSatisfied === null) {
224
-			// default to false to just process this once per request
225
-			$this->logConditionSatisfied = false;
226
-			if (!empty($logCondition)) {
227
-				// check for secret token in the request
228
-				if (isset($logCondition['shared_secret']) && $this->checkLogSecret($logCondition['shared_secret'])) {
229
-					$this->logConditionSatisfied = true;
230
-				}
231
-
232
-				// check for user
233
-				if (isset($logCondition['users'])) {
234
-					$user = \OCP\Server::get(IUserSession::class)->getUser();
235
-
236
-					if ($user === null) {
237
-						// User is not known for this request yet
238
-						$this->logConditionSatisfied = null;
239
-					} elseif (in_array($user->getUID(), $logCondition['users'], true)) {
240
-						// if the user matches set the log condition to satisfied
241
-						$this->logConditionSatisfied = true;
242
-					} else {
243
-						$userId = $user->getUID();
244
-					}
245
-				}
246
-			}
247
-		}
248
-
249
-		// if log condition is satisfied change the required log level to DEBUG
250
-		if ($this->logConditionSatisfied) {
251
-			$this->nestingLevel--;
252
-			return ILogger::DEBUG;
253
-		}
254
-
255
-		if ($userId === false && isset($logCondition['matches'])) {
256
-			$user = \OCP\Server::get(IUserSession::class)->getUser();
257
-			$userId = $user === null ? false : $user->getUID();
258
-		}
259
-
260
-		if (isset($context['app'])) {
261
-			/**
262
-			 * check log condition based on the context of each log message
263
-			 * once this is met -> change the required log level to debug
264
-			 */
265
-			if (in_array($context['app'], $logCondition['apps'] ?? [], true)) {
266
-				$this->nestingLevel--;
267
-				return ILogger::DEBUG;
268
-			}
269
-		}
270
-
271
-		if (!isset($logCondition['matches'])) {
272
-			$configLogLevel = $this->config->getValue('loglevel', ILogger::WARN);
273
-			if (is_numeric($configLogLevel)) {
274
-				$this->nestingLevel--;
275
-				return min((int)$configLogLevel, ILogger::FATAL);
276
-			}
277
-
278
-			// Invalid configuration, warn the user and fall back to default level of WARN
279
-			error_log('Nextcloud configuration: "loglevel" is not a valid integer');
280
-			$this->nestingLevel--;
281
-			return ILogger::WARN;
282
-		}
283
-
284
-		foreach ($logCondition['matches'] as $option) {
285
-			if (
286
-				(!isset($option['shared_secret']) || $this->checkLogSecret($option['shared_secret']))
287
-				&& (!isset($option['users']) || in_array($userId, $option['users'], true))
288
-				&& (!isset($option['apps']) || (isset($context['app']) && in_array($context['app'], $option['apps'], true)))
289
-				&& (!isset($option['message']) || str_contains($message, $option['message']))
290
-			) {
291
-				if (!isset($option['apps']) && !isset($option['loglevel']) && !isset($option['message'])) {
292
-					/* Only user and/or secret are listed as conditions, we can cache the result for the rest of the request */
293
-					$this->logConditionSatisfied = true;
294
-					$this->nestingLevel--;
295
-					return ILogger::DEBUG;
296
-				}
297
-				$this->nestingLevel--;
298
-				return $option['loglevel'] ?? ILogger::DEBUG;
299
-			}
300
-		}
301
-
302
-		$this->nestingLevel--;
303
-		return ILogger::WARN;
304
-	}
305
-
306
-	protected function checkLogSecret(string $conditionSecret): bool {
307
-		$request = \OCP\Server::get(IRequest::class);
308
-
309
-		if ($request->getMethod() === 'PUT'
310
-			&& !str_contains($request->getHeader('Content-Type'), 'application/x-www-form-urlencoded')
311
-			&& !str_contains($request->getHeader('Content-Type'), 'application/json')) {
312
-			return hash_equals($conditionSecret, '');
313
-		}
314
-
315
-		// if token is found in the request change set the log condition to satisfied
316
-		return hash_equals($conditionSecret, $request->getParam('log_secret', ''));
317
-	}
318
-
319
-	/**
320
-	 * Logs an exception very detailed
321
-	 *
322
-	 * @param Exception|Throwable $exception
323
-	 * @param array $context
324
-	 * @return void
325
-	 * @since 8.2.0
326
-	 */
327
-	public function logException(Throwable $exception, array $context = []): void {
328
-		$app = $context['app'] ?? 'no app in context';
329
-		$level = $context['level'] ?? ILogger::ERROR;
330
-
331
-		$minLevel = $this->getLogLevel($context, $context['message'] ?? $exception->getMessage());
332
-		if ($level < $minLevel
333
-			&& (($this->crashReporters?->hasReporters() ?? false) === false)
334
-			&& (($this->eventDispatcher?->hasListeners(BeforeMessageLoggedEvent::class) ?? false) === false)) {
335
-			return; // no crash reporter, no listeners, we can stop for lower log level
336
-		}
337
-
338
-		// if an error is raised before the autoloader is properly setup, we can't serialize exceptions
339
-		try {
340
-			$serializer = $this->getSerializer();
341
-		} catch (Throwable $e) {
342
-			$this->error('Failed to load ExceptionSerializer serializer while trying to log ' . $exception->getMessage());
343
-			return;
344
-		}
345
-
346
-		$context = array_map($this->normalizer->format(...), $context);
347
-		$data = $context;
348
-		unset($data['app'], $data['level']);
349
-
350
-		$data = array_merge($serializer->serializeException($exception), $data);
351
-		$data = $this->interpolateMessage($data, isset($context['message']) && $context['message'] !== '' ? $context['message'] : ('Exception thrown: ' . get_class($exception)), 'CustomMessage');
352
-
353
-		$this->eventDispatcher?->dispatchTyped(new BeforeMessageLoggedEvent($app, $level, $data));
354
-
355
-		try {
356
-			if ($level >= $minLevel) {
357
-				if (!$this->logger instanceof IFileBased) {
358
-					$data = json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES);
359
-				}
360
-				$this->writeLog($app, $data, $level);
361
-			}
362
-
363
-			$context['level'] = $level;
364
-			if (!is_null($this->crashReporters)) {
365
-				$this->crashReporters->delegateReport($exception, $context);
366
-			}
367
-		} catch (Throwable $e) {
368
-			// make sure we dont hard crash if logging fails
369
-		}
370
-	}
371
-
372
-	public function logData(string $message, array $data, array $context = []): void {
373
-		$app = $context['app'] ?? 'no app in context';
374
-		$level = $context['level'] ?? ILogger::ERROR;
375
-
376
-		$minLevel = $this->getLogLevel($context, $message);
377
-		$data = array_map($this->normalizer->format(...), $data);
378
-
379
-		try {
380
-			if ($level >= $minLevel) {
381
-				$data['message'] = $message;
382
-				if (!$this->logger instanceof IFileBased) {
383
-					$data = json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES);
384
-				}
385
-				$this->writeLog($app, $data, $level);
386
-			}
387
-		} catch (Throwable $e) {
388
-			// make sure we dont hard crash if logging fails
389
-			error_log('Error when trying to log exception: ' . $e->getMessage() . ' ' . $e->getTraceAsString());
390
-		}
391
-	}
392
-
393
-	/**
394
-	 * @param string $app
395
-	 * @param string|array $entry
396
-	 * @param int $level
397
-	 */
398
-	protected function writeLog(string $app, $entry, int $level): void {
399
-		$this->logger->write($app, $entry, $level);
400
-	}
401
-
402
-	public function getLogPath():string {
403
-		if ($this->logger instanceof IFileBased) {
404
-			return $this->logger->getLogFilePath();
405
-		}
406
-		throw new \RuntimeException('Log implementation has no path');
407
-	}
408
-
409
-	/**
410
-	 * Interpolate $message as defined in PSR-3
411
-	 *
412
-	 * Returns an array containing the context without the interpolated
413
-	 * parameters placeholders and the message as the 'message' - or
414
-	 * user-defined - key.
415
-	 */
416
-	private function interpolateMessage(array $context, string $message, string $messageKey = 'message'): array {
417
-		$replace = [];
418
-		$usedContextKeys = [];
419
-		foreach ($context as $key => $val) {
420
-			$fullKey = '{' . $key . '}';
421
-			$replace[$fullKey] = $val;
422
-			if (str_contains($message, $fullKey)) {
423
-				$usedContextKeys[$key] = true;
424
-			}
425
-		}
426
-		return array_merge(array_diff_key($context, $usedContextKeys), [$messageKey => strtr($message, $replace)]);
427
-	}
428
-
429
-	/**
430
-	 * @throws Throwable
431
-	 */
432
-	protected function getSerializer(): ExceptionSerializer {
433
-		$serializer = new ExceptionSerializer($this->config);
434
-		try {
435
-			/** @var Coordinator $coordinator */
436
-			$coordinator = \OCP\Server::get(Coordinator::class);
437
-			foreach ($coordinator->getRegistrationContext()->getSensitiveMethods() as $registration) {
438
-				$serializer->enlistSensitiveMethods($registration->getName(), $registration->getValue());
439
-			}
440
-			// For not every app might be initialized at this time, we cannot assume that the return value
441
-			// of getSensitiveMethods() is complete. Running delegates in Coordinator::registerApps() is
442
-			// not possible due to dependencies on the one hand. On the other it would work only with
443
-			// adding public methods to the PsrLoggerAdapter and this class.
444
-			// Thus, serializer cannot be a property.
445
-		} catch (Throwable $t) {
446
-			// ignore app-defined sensitive methods in this case - they weren't loaded anyway
447
-		}
448
-		return $serializer;
449
-	}
38
+    private ?bool $logConditionSatisfied = null;
39
+    private ?IEventDispatcher $eventDispatcher = null;
40
+    private int $nestingLevel = 0;
41
+
42
+    public function __construct(
43
+        private IWriter $logger,
44
+        private SystemConfig $config,
45
+        private Normalizer $normalizer = new Normalizer(),
46
+        private ?IRegistry $crashReporters = null,
47
+    ) {
48
+    }
49
+
50
+    public function setEventDispatcher(IEventDispatcher $eventDispatcher): void {
51
+        $this->eventDispatcher = $eventDispatcher;
52
+    }
53
+
54
+    /**
55
+     * System is unusable.
56
+     *
57
+     * @param string $message
58
+     * @param array $context
59
+     */
60
+    public function emergency(string $message, array $context = []): void {
61
+        $this->log(ILogger::FATAL, $message, $context);
62
+    }
63
+
64
+    /**
65
+     * Action must be taken immediately.
66
+     *
67
+     * Example: Entire website down, database unavailable, etc. This should
68
+     * trigger the SMS alerts and wake you up.
69
+     *
70
+     * @param string $message
71
+     * @param array $context
72
+     */
73
+    public function alert(string $message, array $context = []): void {
74
+        $this->log(ILogger::ERROR, $message, $context);
75
+    }
76
+
77
+    /**
78
+     * Critical conditions.
79
+     *
80
+     * Example: Application component unavailable, unexpected exception.
81
+     *
82
+     * @param string $message
83
+     * @param array $context
84
+     */
85
+    public function critical(string $message, array $context = []): void {
86
+        $this->log(ILogger::ERROR, $message, $context);
87
+    }
88
+
89
+    /**
90
+     * Runtime errors that do not require immediate action but should typically
91
+     * be logged and monitored.
92
+     *
93
+     * @param string $message
94
+     * @param array $context
95
+     */
96
+    public function error(string $message, array $context = []): void {
97
+        $this->log(ILogger::ERROR, $message, $context);
98
+    }
99
+
100
+    /**
101
+     * Exceptional occurrences that are not errors.
102
+     *
103
+     * Example: Use of deprecated APIs, poor use of an API, undesirable things
104
+     * that are not necessarily wrong.
105
+     *
106
+     * @param string $message
107
+     * @param array $context
108
+     */
109
+    public function warning(string $message, array $context = []): void {
110
+        $this->log(ILogger::WARN, $message, $context);
111
+    }
112
+
113
+    /**
114
+     * Normal but significant events.
115
+     *
116
+     * @param string $message
117
+     * @param array $context
118
+     */
119
+    public function notice(string $message, array $context = []): void {
120
+        $this->log(ILogger::INFO, $message, $context);
121
+    }
122
+
123
+    /**
124
+     * Interesting events.
125
+     *
126
+     * Example: User logs in, SQL logs.
127
+     *
128
+     * @param string $message
129
+     * @param array $context
130
+     */
131
+    public function info(string $message, array $context = []): void {
132
+        $this->log(ILogger::INFO, $message, $context);
133
+    }
134
+
135
+    /**
136
+     * Detailed debug information.
137
+     *
138
+     * @param string $message
139
+     * @param array $context
140
+     */
141
+    public function debug(string $message, array $context = []): void {
142
+        $this->log(ILogger::DEBUG, $message, $context);
143
+    }
144
+
145
+
146
+    /**
147
+     * Logs with an arbitrary level.
148
+     *
149
+     * @param int $level
150
+     * @param string $message
151
+     * @param array $context
152
+     */
153
+    public function log(int $level, string $message, array $context = []): void {
154
+        $minLevel = $this->getLogLevel($context, $message);
155
+        if ($level < $minLevel
156
+            && (($this->crashReporters?->hasReporters() ?? false) === false)
157
+            && (($this->eventDispatcher?->hasListeners(BeforeMessageLoggedEvent::class) ?? false) === false)) {
158
+            return; // no crash reporter, no listeners, we can stop for lower log level
159
+        }
160
+
161
+        $context = array_map($this->normalizer->format(...), $context);
162
+
163
+        $app = $context['app'] ?? 'no app in context';
164
+        $entry = $this->interpolateMessage($context, $message);
165
+
166
+        $this->eventDispatcher?->dispatchTyped(new BeforeMessageLoggedEvent($app, $level, $entry));
167
+
168
+        $hasBacktrace = isset($entry['exception']);
169
+        $logBacktrace = $this->config->getValue('log.backtrace', false);
170
+        if (!$hasBacktrace && $logBacktrace) {
171
+            $entry['backtrace'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
172
+        }
173
+
174
+        try {
175
+            if ($level >= $minLevel) {
176
+                $this->writeLog($app, $entry, $level);
177
+
178
+                if ($this->crashReporters !== null) {
179
+                    $messageContext = array_merge(
180
+                        $context,
181
+                        [
182
+                            'level' => $level
183
+                        ]
184
+                    );
185
+                    $this->crashReporters->delegateMessage($entry['message'], $messageContext);
186
+                }
187
+            } else {
188
+                $this->crashReporters?->delegateBreadcrumb($entry['message'], 'log', $context);
189
+            }
190
+        } catch (Throwable $e) {
191
+            // make sure we dont hard crash if logging fails
192
+        }
193
+    }
194
+
195
+    public function getLogLevel(array $context, string $message): int {
196
+        if ($this->nestingLevel > 1) {
197
+            return ILogger::WARN;
198
+        }
199
+
200
+        $this->nestingLevel++;
201
+        /**
202
+         * @psalm-var array{
203
+         *   shared_secret?: string,
204
+         *   users?: string[],
205
+         *   apps?: string[],
206
+         *   matches?: array<array-key, array{
207
+         *     shared_secret?: string,
208
+         *     users?: string[],
209
+         *     apps?: string[],
210
+         *     message?: string,
211
+         *     loglevel: 0|1|2|3|4,
212
+         *   }>
213
+         * } $logCondition
214
+         */
215
+        $logCondition = $this->config->getValue('log.condition', []);
216
+
217
+        $userId = false;
218
+
219
+        /**
220
+         * check for a special log condition - this enables an increased log on
221
+         * a per request/user base
222
+         */
223
+        if ($this->logConditionSatisfied === null) {
224
+            // default to false to just process this once per request
225
+            $this->logConditionSatisfied = false;
226
+            if (!empty($logCondition)) {
227
+                // check for secret token in the request
228
+                if (isset($logCondition['shared_secret']) && $this->checkLogSecret($logCondition['shared_secret'])) {
229
+                    $this->logConditionSatisfied = true;
230
+                }
231
+
232
+                // check for user
233
+                if (isset($logCondition['users'])) {
234
+                    $user = \OCP\Server::get(IUserSession::class)->getUser();
235
+
236
+                    if ($user === null) {
237
+                        // User is not known for this request yet
238
+                        $this->logConditionSatisfied = null;
239
+                    } elseif (in_array($user->getUID(), $logCondition['users'], true)) {
240
+                        // if the user matches set the log condition to satisfied
241
+                        $this->logConditionSatisfied = true;
242
+                    } else {
243
+                        $userId = $user->getUID();
244
+                    }
245
+                }
246
+            }
247
+        }
248
+
249
+        // if log condition is satisfied change the required log level to DEBUG
250
+        if ($this->logConditionSatisfied) {
251
+            $this->nestingLevel--;
252
+            return ILogger::DEBUG;
253
+        }
254
+
255
+        if ($userId === false && isset($logCondition['matches'])) {
256
+            $user = \OCP\Server::get(IUserSession::class)->getUser();
257
+            $userId = $user === null ? false : $user->getUID();
258
+        }
259
+
260
+        if (isset($context['app'])) {
261
+            /**
262
+             * check log condition based on the context of each log message
263
+             * once this is met -> change the required log level to debug
264
+             */
265
+            if (in_array($context['app'], $logCondition['apps'] ?? [], true)) {
266
+                $this->nestingLevel--;
267
+                return ILogger::DEBUG;
268
+            }
269
+        }
270
+
271
+        if (!isset($logCondition['matches'])) {
272
+            $configLogLevel = $this->config->getValue('loglevel', ILogger::WARN);
273
+            if (is_numeric($configLogLevel)) {
274
+                $this->nestingLevel--;
275
+                return min((int)$configLogLevel, ILogger::FATAL);
276
+            }
277
+
278
+            // Invalid configuration, warn the user and fall back to default level of WARN
279
+            error_log('Nextcloud configuration: "loglevel" is not a valid integer');
280
+            $this->nestingLevel--;
281
+            return ILogger::WARN;
282
+        }
283
+
284
+        foreach ($logCondition['matches'] as $option) {
285
+            if (
286
+                (!isset($option['shared_secret']) || $this->checkLogSecret($option['shared_secret']))
287
+                && (!isset($option['users']) || in_array($userId, $option['users'], true))
288
+                && (!isset($option['apps']) || (isset($context['app']) && in_array($context['app'], $option['apps'], true)))
289
+                && (!isset($option['message']) || str_contains($message, $option['message']))
290
+            ) {
291
+                if (!isset($option['apps']) && !isset($option['loglevel']) && !isset($option['message'])) {
292
+                    /* Only user and/or secret are listed as conditions, we can cache the result for the rest of the request */
293
+                    $this->logConditionSatisfied = true;
294
+                    $this->nestingLevel--;
295
+                    return ILogger::DEBUG;
296
+                }
297
+                $this->nestingLevel--;
298
+                return $option['loglevel'] ?? ILogger::DEBUG;
299
+            }
300
+        }
301
+
302
+        $this->nestingLevel--;
303
+        return ILogger::WARN;
304
+    }
305
+
306
+    protected function checkLogSecret(string $conditionSecret): bool {
307
+        $request = \OCP\Server::get(IRequest::class);
308
+
309
+        if ($request->getMethod() === 'PUT'
310
+            && !str_contains($request->getHeader('Content-Type'), 'application/x-www-form-urlencoded')
311
+            && !str_contains($request->getHeader('Content-Type'), 'application/json')) {
312
+            return hash_equals($conditionSecret, '');
313
+        }
314
+
315
+        // if token is found in the request change set the log condition to satisfied
316
+        return hash_equals($conditionSecret, $request->getParam('log_secret', ''));
317
+    }
318
+
319
+    /**
320
+     * Logs an exception very detailed
321
+     *
322
+     * @param Exception|Throwable $exception
323
+     * @param array $context
324
+     * @return void
325
+     * @since 8.2.0
326
+     */
327
+    public function logException(Throwable $exception, array $context = []): void {
328
+        $app = $context['app'] ?? 'no app in context';
329
+        $level = $context['level'] ?? ILogger::ERROR;
330
+
331
+        $minLevel = $this->getLogLevel($context, $context['message'] ?? $exception->getMessage());
332
+        if ($level < $minLevel
333
+            && (($this->crashReporters?->hasReporters() ?? false) === false)
334
+            && (($this->eventDispatcher?->hasListeners(BeforeMessageLoggedEvent::class) ?? false) === false)) {
335
+            return; // no crash reporter, no listeners, we can stop for lower log level
336
+        }
337
+
338
+        // if an error is raised before the autoloader is properly setup, we can't serialize exceptions
339
+        try {
340
+            $serializer = $this->getSerializer();
341
+        } catch (Throwable $e) {
342
+            $this->error('Failed to load ExceptionSerializer serializer while trying to log ' . $exception->getMessage());
343
+            return;
344
+        }
345
+
346
+        $context = array_map($this->normalizer->format(...), $context);
347
+        $data = $context;
348
+        unset($data['app'], $data['level']);
349
+
350
+        $data = array_merge($serializer->serializeException($exception), $data);
351
+        $data = $this->interpolateMessage($data, isset($context['message']) && $context['message'] !== '' ? $context['message'] : ('Exception thrown: ' . get_class($exception)), 'CustomMessage');
352
+
353
+        $this->eventDispatcher?->dispatchTyped(new BeforeMessageLoggedEvent($app, $level, $data));
354
+
355
+        try {
356
+            if ($level >= $minLevel) {
357
+                if (!$this->logger instanceof IFileBased) {
358
+                    $data = json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES);
359
+                }
360
+                $this->writeLog($app, $data, $level);
361
+            }
362
+
363
+            $context['level'] = $level;
364
+            if (!is_null($this->crashReporters)) {
365
+                $this->crashReporters->delegateReport($exception, $context);
366
+            }
367
+        } catch (Throwable $e) {
368
+            // make sure we dont hard crash if logging fails
369
+        }
370
+    }
371
+
372
+    public function logData(string $message, array $data, array $context = []): void {
373
+        $app = $context['app'] ?? 'no app in context';
374
+        $level = $context['level'] ?? ILogger::ERROR;
375
+
376
+        $minLevel = $this->getLogLevel($context, $message);
377
+        $data = array_map($this->normalizer->format(...), $data);
378
+
379
+        try {
380
+            if ($level >= $minLevel) {
381
+                $data['message'] = $message;
382
+                if (!$this->logger instanceof IFileBased) {
383
+                    $data = json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES);
384
+                }
385
+                $this->writeLog($app, $data, $level);
386
+            }
387
+        } catch (Throwable $e) {
388
+            // make sure we dont hard crash if logging fails
389
+            error_log('Error when trying to log exception: ' . $e->getMessage() . ' ' . $e->getTraceAsString());
390
+        }
391
+    }
392
+
393
+    /**
394
+     * @param string $app
395
+     * @param string|array $entry
396
+     * @param int $level
397
+     */
398
+    protected function writeLog(string $app, $entry, int $level): void {
399
+        $this->logger->write($app, $entry, $level);
400
+    }
401
+
402
+    public function getLogPath():string {
403
+        if ($this->logger instanceof IFileBased) {
404
+            return $this->logger->getLogFilePath();
405
+        }
406
+        throw new \RuntimeException('Log implementation has no path');
407
+    }
408
+
409
+    /**
410
+     * Interpolate $message as defined in PSR-3
411
+     *
412
+     * Returns an array containing the context without the interpolated
413
+     * parameters placeholders and the message as the 'message' - or
414
+     * user-defined - key.
415
+     */
416
+    private function interpolateMessage(array $context, string $message, string $messageKey = 'message'): array {
417
+        $replace = [];
418
+        $usedContextKeys = [];
419
+        foreach ($context as $key => $val) {
420
+            $fullKey = '{' . $key . '}';
421
+            $replace[$fullKey] = $val;
422
+            if (str_contains($message, $fullKey)) {
423
+                $usedContextKeys[$key] = true;
424
+            }
425
+        }
426
+        return array_merge(array_diff_key($context, $usedContextKeys), [$messageKey => strtr($message, $replace)]);
427
+    }
428
+
429
+    /**
430
+     * @throws Throwable
431
+     */
432
+    protected function getSerializer(): ExceptionSerializer {
433
+        $serializer = new ExceptionSerializer($this->config);
434
+        try {
435
+            /** @var Coordinator $coordinator */
436
+            $coordinator = \OCP\Server::get(Coordinator::class);
437
+            foreach ($coordinator->getRegistrationContext()->getSensitiveMethods() as $registration) {
438
+                $serializer->enlistSensitiveMethods($registration->getName(), $registration->getValue());
439
+            }
440
+            // For not every app might be initialized at this time, we cannot assume that the return value
441
+            // of getSensitiveMethods() is complete. Running delegates in Coordinator::registerApps() is
442
+            // not possible due to dependencies on the one hand. On the other it would work only with
443
+            // adding public methods to the PsrLoggerAdapter and this class.
444
+            // Thus, serializer cannot be a property.
445
+        } catch (Throwable $t) {
446
+            // ignore app-defined sensitive methods in this case - they weren't loaded anyway
447
+        }
448
+        return $serializer;
449
+    }
450 450
 }
Please login to merge, or discard this patch.
Spacing   +5 added lines, -5 removed lines patch added patch discarded remove patch
@@ -272,7 +272,7 @@  discard block
 block discarded – undo
272 272
 			$configLogLevel = $this->config->getValue('loglevel', ILogger::WARN);
273 273
 			if (is_numeric($configLogLevel)) {
274 274
 				$this->nestingLevel--;
275
-				return min((int)$configLogLevel, ILogger::FATAL);
275
+				return min((int) $configLogLevel, ILogger::FATAL);
276 276
 			}
277 277
 
278 278
 			// Invalid configuration, warn the user and fall back to default level of WARN
@@ -339,7 +339,7 @@  discard block
 block discarded – undo
339 339
 		try {
340 340
 			$serializer = $this->getSerializer();
341 341
 		} catch (Throwable $e) {
342
-			$this->error('Failed to load ExceptionSerializer serializer while trying to log ' . $exception->getMessage());
342
+			$this->error('Failed to load ExceptionSerializer serializer while trying to log '.$exception->getMessage());
343 343
 			return;
344 344
 		}
345 345
 
@@ -348,7 +348,7 @@  discard block
 block discarded – undo
348 348
 		unset($data['app'], $data['level']);
349 349
 
350 350
 		$data = array_merge($serializer->serializeException($exception), $data);
351
-		$data = $this->interpolateMessage($data, isset($context['message']) && $context['message'] !== '' ? $context['message'] : ('Exception thrown: ' . get_class($exception)), 'CustomMessage');
351
+		$data = $this->interpolateMessage($data, isset($context['message']) && $context['message'] !== '' ? $context['message'] : ('Exception thrown: '.get_class($exception)), 'CustomMessage');
352 352
 
353 353
 		$this->eventDispatcher?->dispatchTyped(new BeforeMessageLoggedEvent($app, $level, $data));
354 354
 
@@ -386,7 +386,7 @@  discard block
 block discarded – undo
386 386
 			}
387 387
 		} catch (Throwable $e) {
388 388
 			// make sure we dont hard crash if logging fails
389
-			error_log('Error when trying to log exception: ' . $e->getMessage() . ' ' . $e->getTraceAsString());
389
+			error_log('Error when trying to log exception: '.$e->getMessage().' '.$e->getTraceAsString());
390 390
 		}
391 391
 	}
392 392
 
@@ -417,7 +417,7 @@  discard block
 block discarded – undo
417 417
 		$replace = [];
418 418
 		$usedContextKeys = [];
419 419
 		foreach ($context as $key => $val) {
420
-			$fullKey = '{' . $key . '}';
420
+			$fullKey = '{'.$key.'}';
421 421
 			$replace[$fullKey] = $val;
422 422
 			if (str_contains($message, $fullKey)) {
423 423
 				$usedContextKeys[$key] = true;
Please login to merge, or discard this patch.
lib/versioncheck.php 2 patches
Indentation   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -7,16 +7,16 @@
 block discarded – undo
7 7
  */
8 8
 // Show warning if a PHP version below 8.2 is used,
9 9
 if (PHP_VERSION_ID < 80200) {
10
-	http_response_code(500);
11
-	echo 'This version of Nextcloud requires at least PHP 8.2<br/>';
12
-	echo 'You are currently running ' . PHP_VERSION . '. Please update your PHP version.';
13
-	exit(1);
10
+    http_response_code(500);
11
+    echo 'This version of Nextcloud requires at least PHP 8.2<br/>';
12
+    echo 'You are currently running ' . PHP_VERSION . '. Please update your PHP version.';
13
+    exit(1);
14 14
 }
15 15
 
16 16
 // Show warning if >= PHP 8.6 is used as Nextcloud is not compatible with >= PHP 8.6 for now
17 17
 if (PHP_VERSION_ID >= 80600) {
18
-	http_response_code(500);
19
-	echo 'This version of Nextcloud is not compatible with PHP>=8.6.<br/>';
20
-	echo 'You are currently running ' . PHP_VERSION . '.';
21
-	exit(1);
18
+    http_response_code(500);
19
+    echo 'This version of Nextcloud is not compatible with PHP>=8.6.<br/>';
20
+    echo 'You are currently running ' . PHP_VERSION . '.';
21
+    exit(1);
22 22
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -9,7 +9,7 @@  discard block
 block discarded – undo
9 9
 if (PHP_VERSION_ID < 80200) {
10 10
 	http_response_code(500);
11 11
 	echo 'This version of Nextcloud requires at least PHP 8.2<br/>';
12
-	echo 'You are currently running ' . PHP_VERSION . '. Please update your PHP version.';
12
+	echo 'You are currently running '.PHP_VERSION.'. Please update your PHP version.';
13 13
 	exit(1);
14 14
 }
15 15
 
@@ -17,6 +17,6 @@  discard block
 block discarded – undo
17 17
 if (PHP_VERSION_ID >= 80600) {
18 18
 	http_response_code(500);
19 19
 	echo 'This version of Nextcloud is not compatible with PHP>=8.6.<br/>';
20
-	echo 'You are currently running ' . PHP_VERSION . '.';
20
+	echo 'You are currently running '.PHP_VERSION.'.';
21 21
 	exit(1);
22 22
 }
Please login to merge, or discard this patch.
apps/files_sharing/tests/SharedMountTest.php 1 patch
Indentation   +394 added lines, -394 removed lines patch added patch discarded remove patch
@@ -26,406 +26,406 @@
 block discarded – undo
26 26
 #[\PHPUnit\Framework\Attributes\Group('SLOWDB')]
27 27
 class SharedMountTest extends TestCase {
28 28
 
29
-	/** @var IGroupManager */
30
-	private $groupManager;
29
+    /** @var IGroupManager */
30
+    private $groupManager;
31 31
 
32
-	/** @var IUserManager */
33
-	private $userManager;
32
+    /** @var IUserManager */
33
+    private $userManager;
34 34
 
35
-	private $folder2;
36
-
37
-	protected function setUp(): void {
38
-		parent::setUp();
35
+    private $folder2;
36
+
37
+    protected function setUp(): void {
38
+        parent::setUp();
39 39
 
40
-		$this->folder = '/folder_share_storage_test';
41
-		$this->folder2 = '/folder_share_storage_test2';
40
+        $this->folder = '/folder_share_storage_test';
41
+        $this->folder2 = '/folder_share_storage_test2';
42 42
 
43
-		$this->filename = '/share-api-storage.txt';
44
-
45
-
46
-		$this->view->mkdir($this->folder);
47
-		$this->view->mkdir($this->folder2);
48
-
49
-		// save file with content
50
-		$this->view->file_put_contents($this->filename, 'root file');
51
-		$this->view->file_put_contents($this->folder . $this->filename, 'file in subfolder');
52
-		$this->view->file_put_contents($this->folder2 . $this->filename, 'file in subfolder2');
53
-
54
-		$this->groupManager = Server::get(IGroupManager::class);
55
-		$this->userManager = Server::get(IUserManager::class);
56
-	}
57
-
58
-	protected function tearDown(): void {
59
-		if ($this->view) {
60
-			if ($this->view->file_exists($this->folder)) {
61
-				$this->view->unlink($this->folder);
62
-			}
63
-			if ($this->view->file_exists($this->filename)) {
64
-				$this->view->unlink($this->filename);
65
-			}
66
-		}
67
-
68
-		parent::tearDown();
69
-	}
70
-
71
-	/**
72
-	 * test if the mount point moves up if the parent folder no longer exists
73
-	 */
74
-	public function testShareMountLoseParentFolder(): void {
75
-
76
-		// share to user
77
-		$share = $this->share(
78
-			IShare::TYPE_USER,
79
-			$this->folder,
80
-			self::TEST_FILES_SHARING_API_USER1,
81
-			self::TEST_FILES_SHARING_API_USER2,
82
-			Constants::PERMISSION_ALL);
83
-		$this->shareManager->acceptShare($share, self::TEST_FILES_SHARING_API_USER2);
84
-
85
-		$share->setTarget('/foo/bar' . $this->folder);
86
-		$this->shareManager->moveShare($share, self::TEST_FILES_SHARING_API_USER2);
87
-
88
-		$share = $this->shareManager->getShareById($share->getFullId());
89
-		$this->assertSame('/foo/bar' . $this->folder, $share->getTarget());
90
-
91
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
92
-		// share should have moved up
93
-
94
-		$share = $this->shareManager->getShareById($share->getFullId());
95
-		$this->assertSame($this->folder, $share->getTarget());
96
-
97
-		//cleanup
98
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
99
-		$this->shareManager->deleteShare($share);
100
-		$this->view->unlink($this->folder);
101
-	}
102
-
103
-	public function testDeleteParentOfMountPoint(): void {
104
-		// share to user
105
-		$share = $this->share(
106
-			IShare::TYPE_USER,
107
-			$this->folder,
108
-			self::TEST_FILES_SHARING_API_USER1,
109
-			self::TEST_FILES_SHARING_API_USER2,
110
-			Constants::PERMISSION_ALL
111
-		);
112
-
113
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
114
-		$user2View = new View('/' . self::TEST_FILES_SHARING_API_USER2 . '/files');
115
-		$this->assertTrue($user2View->file_exists($this->folder));
116
-
117
-		// create a local folder
118
-		$result = $user2View->mkdir('localfolder');
119
-		$this->assertTrue($result);
120
-
121
-		// move mount point to local folder
122
-		$result = $user2View->rename($this->folder, '/localfolder/' . $this->folder);
123
-		$this->assertTrue($result);
124
-
125
-		// mount point in the root folder should no longer exist
126
-		$this->assertFalse($user2View->is_dir($this->folder));
127
-
128
-		// delete the local folder
129
-		$result = $user2View->unlink('/localfolder');
130
-		$this->assertTrue($result);
131
-
132
-		//enforce reload of the mount points
133
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
134
-
135
-		//mount point should be back at the root
136
-		$this->assertTrue($user2View->is_dir($this->folder));
137
-
138
-		//cleanup
139
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
140
-		$this->view->unlink($this->folder);
141
-	}
142
-
143
-	public function testMoveSharedFile(): void {
144
-		$share = $this->share(
145
-			IShare::TYPE_USER,
146
-			$this->filename,
147
-			self::TEST_FILES_SHARING_API_USER1,
148
-			self::TEST_FILES_SHARING_API_USER2,
149
-			Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_SHARE
150
-		);
151
-
152
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
153
-
154
-		Filesystem::rename($this->filename, $this->filename . '_renamed');
155
-
156
-		$this->assertTrue(Filesystem::file_exists($this->filename . '_renamed'));
157
-		$this->assertFalse(Filesystem::file_exists($this->filename));
158
-
159
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
160
-		$this->assertTrue(Filesystem::file_exists($this->filename));
161
-		$this->assertFalse(Filesystem::file_exists($this->filename . '_renamed'));
162
-
163
-		// rename back to original name
164
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
165
-		Filesystem::rename($this->filename . '_renamed', $this->filename);
166
-		$this->assertFalse(Filesystem::file_exists($this->filename . '_renamed'));
167
-		$this->assertTrue(Filesystem::file_exists($this->filename));
168
-
169
-		//cleanup
170
-		$this->shareManager->deleteShare($share);
171
-	}
172
-
173
-	/**
174
-	 * share file with a group if a user renames the file the filename should not change
175
-	 * for the other users
176
-	 */
177
-	public function testMoveGroupShare(): void {
178
-		$testGroup = $this->groupManager->createGroup('testGroup');
179
-		$user1 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER1);
180
-		$user2 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER2);
181
-		$user3 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER3);
182
-		$testGroup->addUser($user1);
183
-		$testGroup->addUser($user2);
184
-		$testGroup->addUser($user3);
185
-
186
-		$fileinfo = $this->view->getFileInfo($this->filename);
187
-		$share = $this->share(
188
-			IShare::TYPE_GROUP,
189
-			$this->filename,
190
-			self::TEST_FILES_SHARING_API_USER1,
191
-			'testGroup',
192
-			Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_SHARE
193
-		);
194
-		$this->shareManager->acceptShare($share, $user1->getUID());
195
-		$this->shareManager->acceptShare($share, $user2->getUID());
196
-		$this->shareManager->acceptShare($share, $user3->getUID());
197
-
198
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
199
-
200
-		$this->assertTrue(Filesystem::file_exists($this->filename));
201
-
202
-		Filesystem::rename($this->filename, 'newFileName');
203
-
204
-		$this->assertTrue(Filesystem::file_exists('newFileName'));
205
-		$this->assertFalse(Filesystem::file_exists($this->filename));
206
-
207
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER3);
208
-		$this->assertTrue(Filesystem::file_exists($this->filename));
209
-		$this->assertFalse(Filesystem::file_exists('newFileName'));
210
-
211
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER3);
212
-		$this->assertTrue(Filesystem::file_exists($this->filename));
213
-		$this->assertFalse(Filesystem::file_exists('newFileName'));
214
-
215
-		//cleanup
216
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
217
-		$this->shareManager->deleteShare($share);
218
-		$testGroup->removeUser($user1);
219
-		$testGroup->removeUser($user2);
220
-		$testGroup->removeUser($user3);
221
-	}
222
-
223
-	/**
224
-	 * @param string $path
225
-	 * @param string $expectedResult
226
-	 * @param bool $exception if a exception is expected
227
-	 */
228
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataProviderTestStripUserFilesPath')]
229
-	public function testStripUserFilesPath($path, $expectedResult, $exception): void {
230
-		$testClass = new DummyTestClassSharedMount(null, null);
231
-		try {
232
-			$result = $testClass->stripUserFilesPathDummy($path);
233
-			$this->assertSame($expectedResult, $result);
234
-		} catch (\Exception $e) {
235
-			if ($exception) {
236
-				$this->assertSame(10, $e->getCode());
237
-			} else {
238
-				$this->assertTrue(false, 'Exception caught, but expected: ' . $expectedResult);
239
-			}
240
-		}
241
-	}
242
-
243
-	public static function dataProviderTestStripUserFilesPath() {
244
-		return [
245
-			['/user/files/foo.txt', '/foo.txt', false],
246
-			['/user/files/folder/foo.txt', '/folder/foo.txt', false],
247
-			['/data/user/files/foo.txt', null, true],
248
-			['/data/user/files/', null, true],
249
-			['/files/foo.txt', null, true],
250
-			['/foo.txt', null, true],
251
-		];
252
-	}
253
-
254
-	/**
255
-	 * If the permissions on a group share are upgraded be sure to still respect
256
-	 * removed shares by a member of that group
257
-	 */
258
-	public function testPermissionUpgradeOnUserDeletedGroupShare(): void {
259
-		$testGroup = $this->groupManager->createGroup('testGroup');
260
-		$user1 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER1);
261
-		$user2 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER2);
262
-		$user3 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER3);
263
-		$testGroup->addUser($user1);
264
-		$testGroup->addUser($user2);
265
-		$testGroup->addUser($user3);
266
-
267
-		$connection = Server::get(IDBConnection::class);
268
-
269
-		// Share item with group
270
-		$fileinfo = $this->view->getFileInfo($this->folder);
271
-		$share = $this->share(
272
-			IShare::TYPE_GROUP,
273
-			$this->folder,
274
-			self::TEST_FILES_SHARING_API_USER1,
275
-			'testGroup',
276
-			Constants::PERMISSION_READ
277
-		);
278
-		$this->shareManager->acceptShare($share, $user1->getUID());
279
-		$this->shareManager->acceptShare($share, $user2->getUID());
280
-		$this->shareManager->acceptShare($share, $user3->getUID());
281
-
282
-		// Login as user 2 and verify the item exists
283
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
284
-		$this->assertTrue(Filesystem::file_exists($this->folder));
285
-		$result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER2);
286
-		$this->assertNotEmpty($result);
287
-		$this->assertEquals(Constants::PERMISSION_READ, $result->getPermissions());
288
-
289
-		// Delete the share
290
-		$this->assertTrue(Filesystem::rmdir($this->folder));
291
-		$this->assertFalse(Filesystem::file_exists($this->folder));
292
-
293
-		// Verify we do not get a share
294
-		$result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER2);
295
-		$this->assertEquals(0, $result->getPermissions());
296
-
297
-		// Login as user 1 again and change permissions
298
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
299
-		$share->setPermissions(Constants::PERMISSION_ALL);
300
-		$share = $this->shareManager->updateShare($share);
301
-
302
-		// Login as user 2 and verify
303
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
304
-		$this->assertFalse(Filesystem::file_exists($this->folder));
305
-		$result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER2);
306
-		$this->assertEquals(0, $result->getPermissions());
307
-
308
-		$this->shareManager->deleteShare($share);
309
-
310
-		//cleanup
311
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
312
-		$testGroup->removeUser($user1);
313
-		$testGroup->removeUser($user2);
314
-		$testGroup->removeUser($user3);
315
-	}
316
-
317
-	/**
318
-	 * test if the mount point gets renamed if a folder exists at the target
319
-	 */
320
-	public function testShareMountOverFolder(): void {
321
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
322
-		$this->view2->mkdir('bar');
323
-
324
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
325
-
326
-		// share to user
327
-		$share = $this->share(
328
-			IShare::TYPE_USER,
329
-			$this->folder,
330
-			self::TEST_FILES_SHARING_API_USER1,
331
-			self::TEST_FILES_SHARING_API_USER2,
332
-			Constants::PERMISSION_ALL);
333
-		$this->shareManager->acceptShare($share, self::TEST_FILES_SHARING_API_USER2);
334
-
335
-		$share->setTarget('/bar');
336
-		$this->shareManager->moveShare($share, self::TEST_FILES_SHARING_API_USER2);
337
-
338
-		$share = $this->shareManager->getShareById($share->getFullId());
339
-
340
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
341
-		// share should have been moved
342
-
343
-		$share = $this->shareManager->getShareById($share->getFullId());
344
-		$this->assertSame('/bar (2)', $share->getTarget());
345
-
346
-		//cleanup
347
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
348
-		$this->shareManager->deleteShare($share);
349
-		$this->view->unlink($this->folder);
350
-	}
351
-
352
-	/**
353
-	 * test if the mount point gets renamed if another share exists at the target
354
-	 */
355
-	public function testShareMountOverShare(): void {
356
-		// create a shared cache
357
-		$caches = [];
358
-		$cacheFactory = $this->createMock(ICacheFactory::class);
359
-		$cacheFactory->method('createLocal')
360
-			->willReturnCallback(function (string $prefix) use (&$caches) {
361
-				if (!isset($caches[$prefix])) {
362
-					$caches[$prefix] = new ArrayCache($prefix);
363
-				}
364
-				return $caches[$prefix];
365
-			});
366
-		$cacheFactory->method('createDistributed')
367
-			->willReturnCallback(function (string $prefix) use (&$caches) {
368
-				if (!isset($caches[$prefix])) {
369
-					$caches[$prefix] = new ArrayCache($prefix);
370
-				}
371
-				return $caches[$prefix];
372
-			});
373
-
374
-		// hack to overwrite the cache factory, we can't use the proper "overwriteService" since the mount provider is created before this test is called
375
-		$mountProvider = Server::get(MountProvider::class);
376
-		$reflectionClass = new \ReflectionClass($mountProvider);
377
-		$reflectionCacheFactory = $reflectionClass->getProperty('cacheFactory');
378
-		$reflectionCacheFactory->setValue($mountProvider, $cacheFactory);
379
-
380
-		// share to user
381
-		$share = $this->share(
382
-			IShare::TYPE_USER,
383
-			$this->folder,
384
-			self::TEST_FILES_SHARING_API_USER1,
385
-			self::TEST_FILES_SHARING_API_USER2,
386
-			Constants::PERMISSION_ALL);
387
-		$this->shareManager->acceptShare($share, self::TEST_FILES_SHARING_API_USER2);
388
-
389
-		$share->setTarget('/foobar');
390
-		$this->shareManager->moveShare($share, self::TEST_FILES_SHARING_API_USER2);
391
-
392
-
393
-		// share to user
394
-		$share2 = $this->share(
395
-			IShare::TYPE_USER,
396
-			$this->folder2,
397
-			self::TEST_FILES_SHARING_API_USER1,
398
-			self::TEST_FILES_SHARING_API_USER2,
399
-			Constants::PERMISSION_ALL);
400
-		$this->shareManager->acceptShare($share2, self::TEST_FILES_SHARING_API_USER2);
401
-
402
-		$share2->setTarget('/foobar');
403
-		$this->shareManager->moveShare($share2, self::TEST_FILES_SHARING_API_USER2);
404
-
405
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
406
-		// one of the shares should have been moved
407
-
408
-		$share = $this->shareManager->getShareById($share->getFullId());
409
-		$share2 = $this->shareManager->getShareById($share2->getFullId());
410
-
411
-		// we don't know or care which share got the "(2)" just that one of them did
412
-		$this->assertNotEquals($share->getTarget(), $share2->getTarget());
413
-		$this->assertSame('/foobar', min($share->getTarget(), $share2->getTarget()));
414
-		$this->assertSame('/foobar (2)', max($share->getTarget(), $share2->getTarget()));
415
-
416
-		//cleanup
417
-		self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
418
-		$this->shareManager->deleteShare($share);
419
-		$this->view->unlink($this->folder);
420
-	}
43
+        $this->filename = '/share-api-storage.txt';
44
+
45
+
46
+        $this->view->mkdir($this->folder);
47
+        $this->view->mkdir($this->folder2);
48
+
49
+        // save file with content
50
+        $this->view->file_put_contents($this->filename, 'root file');
51
+        $this->view->file_put_contents($this->folder . $this->filename, 'file in subfolder');
52
+        $this->view->file_put_contents($this->folder2 . $this->filename, 'file in subfolder2');
53
+
54
+        $this->groupManager = Server::get(IGroupManager::class);
55
+        $this->userManager = Server::get(IUserManager::class);
56
+    }
57
+
58
+    protected function tearDown(): void {
59
+        if ($this->view) {
60
+            if ($this->view->file_exists($this->folder)) {
61
+                $this->view->unlink($this->folder);
62
+            }
63
+            if ($this->view->file_exists($this->filename)) {
64
+                $this->view->unlink($this->filename);
65
+            }
66
+        }
67
+
68
+        parent::tearDown();
69
+    }
70
+
71
+    /**
72
+     * test if the mount point moves up if the parent folder no longer exists
73
+     */
74
+    public function testShareMountLoseParentFolder(): void {
75
+
76
+        // share to user
77
+        $share = $this->share(
78
+            IShare::TYPE_USER,
79
+            $this->folder,
80
+            self::TEST_FILES_SHARING_API_USER1,
81
+            self::TEST_FILES_SHARING_API_USER2,
82
+            Constants::PERMISSION_ALL);
83
+        $this->shareManager->acceptShare($share, self::TEST_FILES_SHARING_API_USER2);
84
+
85
+        $share->setTarget('/foo/bar' . $this->folder);
86
+        $this->shareManager->moveShare($share, self::TEST_FILES_SHARING_API_USER2);
87
+
88
+        $share = $this->shareManager->getShareById($share->getFullId());
89
+        $this->assertSame('/foo/bar' . $this->folder, $share->getTarget());
90
+
91
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
92
+        // share should have moved up
93
+
94
+        $share = $this->shareManager->getShareById($share->getFullId());
95
+        $this->assertSame($this->folder, $share->getTarget());
96
+
97
+        //cleanup
98
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
99
+        $this->shareManager->deleteShare($share);
100
+        $this->view->unlink($this->folder);
101
+    }
102
+
103
+    public function testDeleteParentOfMountPoint(): void {
104
+        // share to user
105
+        $share = $this->share(
106
+            IShare::TYPE_USER,
107
+            $this->folder,
108
+            self::TEST_FILES_SHARING_API_USER1,
109
+            self::TEST_FILES_SHARING_API_USER2,
110
+            Constants::PERMISSION_ALL
111
+        );
112
+
113
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
114
+        $user2View = new View('/' . self::TEST_FILES_SHARING_API_USER2 . '/files');
115
+        $this->assertTrue($user2View->file_exists($this->folder));
116
+
117
+        // create a local folder
118
+        $result = $user2View->mkdir('localfolder');
119
+        $this->assertTrue($result);
120
+
121
+        // move mount point to local folder
122
+        $result = $user2View->rename($this->folder, '/localfolder/' . $this->folder);
123
+        $this->assertTrue($result);
124
+
125
+        // mount point in the root folder should no longer exist
126
+        $this->assertFalse($user2View->is_dir($this->folder));
127
+
128
+        // delete the local folder
129
+        $result = $user2View->unlink('/localfolder');
130
+        $this->assertTrue($result);
131
+
132
+        //enforce reload of the mount points
133
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
134
+
135
+        //mount point should be back at the root
136
+        $this->assertTrue($user2View->is_dir($this->folder));
137
+
138
+        //cleanup
139
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
140
+        $this->view->unlink($this->folder);
141
+    }
142
+
143
+    public function testMoveSharedFile(): void {
144
+        $share = $this->share(
145
+            IShare::TYPE_USER,
146
+            $this->filename,
147
+            self::TEST_FILES_SHARING_API_USER1,
148
+            self::TEST_FILES_SHARING_API_USER2,
149
+            Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_SHARE
150
+        );
151
+
152
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
153
+
154
+        Filesystem::rename($this->filename, $this->filename . '_renamed');
155
+
156
+        $this->assertTrue(Filesystem::file_exists($this->filename . '_renamed'));
157
+        $this->assertFalse(Filesystem::file_exists($this->filename));
158
+
159
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
160
+        $this->assertTrue(Filesystem::file_exists($this->filename));
161
+        $this->assertFalse(Filesystem::file_exists($this->filename . '_renamed'));
162
+
163
+        // rename back to original name
164
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
165
+        Filesystem::rename($this->filename . '_renamed', $this->filename);
166
+        $this->assertFalse(Filesystem::file_exists($this->filename . '_renamed'));
167
+        $this->assertTrue(Filesystem::file_exists($this->filename));
168
+
169
+        //cleanup
170
+        $this->shareManager->deleteShare($share);
171
+    }
172
+
173
+    /**
174
+     * share file with a group if a user renames the file the filename should not change
175
+     * for the other users
176
+     */
177
+    public function testMoveGroupShare(): void {
178
+        $testGroup = $this->groupManager->createGroup('testGroup');
179
+        $user1 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER1);
180
+        $user2 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER2);
181
+        $user3 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER3);
182
+        $testGroup->addUser($user1);
183
+        $testGroup->addUser($user2);
184
+        $testGroup->addUser($user3);
185
+
186
+        $fileinfo = $this->view->getFileInfo($this->filename);
187
+        $share = $this->share(
188
+            IShare::TYPE_GROUP,
189
+            $this->filename,
190
+            self::TEST_FILES_SHARING_API_USER1,
191
+            'testGroup',
192
+            Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_SHARE
193
+        );
194
+        $this->shareManager->acceptShare($share, $user1->getUID());
195
+        $this->shareManager->acceptShare($share, $user2->getUID());
196
+        $this->shareManager->acceptShare($share, $user3->getUID());
197
+
198
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
199
+
200
+        $this->assertTrue(Filesystem::file_exists($this->filename));
201
+
202
+        Filesystem::rename($this->filename, 'newFileName');
203
+
204
+        $this->assertTrue(Filesystem::file_exists('newFileName'));
205
+        $this->assertFalse(Filesystem::file_exists($this->filename));
206
+
207
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER3);
208
+        $this->assertTrue(Filesystem::file_exists($this->filename));
209
+        $this->assertFalse(Filesystem::file_exists('newFileName'));
210
+
211
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER3);
212
+        $this->assertTrue(Filesystem::file_exists($this->filename));
213
+        $this->assertFalse(Filesystem::file_exists('newFileName'));
214
+
215
+        //cleanup
216
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
217
+        $this->shareManager->deleteShare($share);
218
+        $testGroup->removeUser($user1);
219
+        $testGroup->removeUser($user2);
220
+        $testGroup->removeUser($user3);
221
+    }
222
+
223
+    /**
224
+     * @param string $path
225
+     * @param string $expectedResult
226
+     * @param bool $exception if a exception is expected
227
+     */
228
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataProviderTestStripUserFilesPath')]
229
+    public function testStripUserFilesPath($path, $expectedResult, $exception): void {
230
+        $testClass = new DummyTestClassSharedMount(null, null);
231
+        try {
232
+            $result = $testClass->stripUserFilesPathDummy($path);
233
+            $this->assertSame($expectedResult, $result);
234
+        } catch (\Exception $e) {
235
+            if ($exception) {
236
+                $this->assertSame(10, $e->getCode());
237
+            } else {
238
+                $this->assertTrue(false, 'Exception caught, but expected: ' . $expectedResult);
239
+            }
240
+        }
241
+    }
242
+
243
+    public static function dataProviderTestStripUserFilesPath() {
244
+        return [
245
+            ['/user/files/foo.txt', '/foo.txt', false],
246
+            ['/user/files/folder/foo.txt', '/folder/foo.txt', false],
247
+            ['/data/user/files/foo.txt', null, true],
248
+            ['/data/user/files/', null, true],
249
+            ['/files/foo.txt', null, true],
250
+            ['/foo.txt', null, true],
251
+        ];
252
+    }
253
+
254
+    /**
255
+     * If the permissions on a group share are upgraded be sure to still respect
256
+     * removed shares by a member of that group
257
+     */
258
+    public function testPermissionUpgradeOnUserDeletedGroupShare(): void {
259
+        $testGroup = $this->groupManager->createGroup('testGroup');
260
+        $user1 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER1);
261
+        $user2 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER2);
262
+        $user3 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER3);
263
+        $testGroup->addUser($user1);
264
+        $testGroup->addUser($user2);
265
+        $testGroup->addUser($user3);
266
+
267
+        $connection = Server::get(IDBConnection::class);
268
+
269
+        // Share item with group
270
+        $fileinfo = $this->view->getFileInfo($this->folder);
271
+        $share = $this->share(
272
+            IShare::TYPE_GROUP,
273
+            $this->folder,
274
+            self::TEST_FILES_SHARING_API_USER1,
275
+            'testGroup',
276
+            Constants::PERMISSION_READ
277
+        );
278
+        $this->shareManager->acceptShare($share, $user1->getUID());
279
+        $this->shareManager->acceptShare($share, $user2->getUID());
280
+        $this->shareManager->acceptShare($share, $user3->getUID());
281
+
282
+        // Login as user 2 and verify the item exists
283
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
284
+        $this->assertTrue(Filesystem::file_exists($this->folder));
285
+        $result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER2);
286
+        $this->assertNotEmpty($result);
287
+        $this->assertEquals(Constants::PERMISSION_READ, $result->getPermissions());
288
+
289
+        // Delete the share
290
+        $this->assertTrue(Filesystem::rmdir($this->folder));
291
+        $this->assertFalse(Filesystem::file_exists($this->folder));
292
+
293
+        // Verify we do not get a share
294
+        $result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER2);
295
+        $this->assertEquals(0, $result->getPermissions());
296
+
297
+        // Login as user 1 again and change permissions
298
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
299
+        $share->setPermissions(Constants::PERMISSION_ALL);
300
+        $share = $this->shareManager->updateShare($share);
301
+
302
+        // Login as user 2 and verify
303
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
304
+        $this->assertFalse(Filesystem::file_exists($this->folder));
305
+        $result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER2);
306
+        $this->assertEquals(0, $result->getPermissions());
307
+
308
+        $this->shareManager->deleteShare($share);
309
+
310
+        //cleanup
311
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
312
+        $testGroup->removeUser($user1);
313
+        $testGroup->removeUser($user2);
314
+        $testGroup->removeUser($user3);
315
+    }
316
+
317
+    /**
318
+     * test if the mount point gets renamed if a folder exists at the target
319
+     */
320
+    public function testShareMountOverFolder(): void {
321
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
322
+        $this->view2->mkdir('bar');
323
+
324
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
325
+
326
+        // share to user
327
+        $share = $this->share(
328
+            IShare::TYPE_USER,
329
+            $this->folder,
330
+            self::TEST_FILES_SHARING_API_USER1,
331
+            self::TEST_FILES_SHARING_API_USER2,
332
+            Constants::PERMISSION_ALL);
333
+        $this->shareManager->acceptShare($share, self::TEST_FILES_SHARING_API_USER2);
334
+
335
+        $share->setTarget('/bar');
336
+        $this->shareManager->moveShare($share, self::TEST_FILES_SHARING_API_USER2);
337
+
338
+        $share = $this->shareManager->getShareById($share->getFullId());
339
+
340
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
341
+        // share should have been moved
342
+
343
+        $share = $this->shareManager->getShareById($share->getFullId());
344
+        $this->assertSame('/bar (2)', $share->getTarget());
345
+
346
+        //cleanup
347
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
348
+        $this->shareManager->deleteShare($share);
349
+        $this->view->unlink($this->folder);
350
+    }
351
+
352
+    /**
353
+     * test if the mount point gets renamed if another share exists at the target
354
+     */
355
+    public function testShareMountOverShare(): void {
356
+        // create a shared cache
357
+        $caches = [];
358
+        $cacheFactory = $this->createMock(ICacheFactory::class);
359
+        $cacheFactory->method('createLocal')
360
+            ->willReturnCallback(function (string $prefix) use (&$caches) {
361
+                if (!isset($caches[$prefix])) {
362
+                    $caches[$prefix] = new ArrayCache($prefix);
363
+                }
364
+                return $caches[$prefix];
365
+            });
366
+        $cacheFactory->method('createDistributed')
367
+            ->willReturnCallback(function (string $prefix) use (&$caches) {
368
+                if (!isset($caches[$prefix])) {
369
+                    $caches[$prefix] = new ArrayCache($prefix);
370
+                }
371
+                return $caches[$prefix];
372
+            });
373
+
374
+        // hack to overwrite the cache factory, we can't use the proper "overwriteService" since the mount provider is created before this test is called
375
+        $mountProvider = Server::get(MountProvider::class);
376
+        $reflectionClass = new \ReflectionClass($mountProvider);
377
+        $reflectionCacheFactory = $reflectionClass->getProperty('cacheFactory');
378
+        $reflectionCacheFactory->setValue($mountProvider, $cacheFactory);
379
+
380
+        // share to user
381
+        $share = $this->share(
382
+            IShare::TYPE_USER,
383
+            $this->folder,
384
+            self::TEST_FILES_SHARING_API_USER1,
385
+            self::TEST_FILES_SHARING_API_USER2,
386
+            Constants::PERMISSION_ALL);
387
+        $this->shareManager->acceptShare($share, self::TEST_FILES_SHARING_API_USER2);
388
+
389
+        $share->setTarget('/foobar');
390
+        $this->shareManager->moveShare($share, self::TEST_FILES_SHARING_API_USER2);
391
+
392
+
393
+        // share to user
394
+        $share2 = $this->share(
395
+            IShare::TYPE_USER,
396
+            $this->folder2,
397
+            self::TEST_FILES_SHARING_API_USER1,
398
+            self::TEST_FILES_SHARING_API_USER2,
399
+            Constants::PERMISSION_ALL);
400
+        $this->shareManager->acceptShare($share2, self::TEST_FILES_SHARING_API_USER2);
401
+
402
+        $share2->setTarget('/foobar');
403
+        $this->shareManager->moveShare($share2, self::TEST_FILES_SHARING_API_USER2);
404
+
405
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
406
+        // one of the shares should have been moved
407
+
408
+        $share = $this->shareManager->getShareById($share->getFullId());
409
+        $share2 = $this->shareManager->getShareById($share2->getFullId());
410
+
411
+        // we don't know or care which share got the "(2)" just that one of them did
412
+        $this->assertNotEquals($share->getTarget(), $share2->getTarget());
413
+        $this->assertSame('/foobar', min($share->getTarget(), $share2->getTarget()));
414
+        $this->assertSame('/foobar (2)', max($share->getTarget(), $share2->getTarget()));
415
+
416
+        //cleanup
417
+        self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
418
+        $this->shareManager->deleteShare($share);
419
+        $this->view->unlink($this->folder);
420
+    }
421 421
 }
422 422
 
423 423
 class DummyTestClassSharedMount extends SharedMount {
424
-	public function __construct($storage, $mountpoint, $arguments = null, $loader = null) {
425
-		// noop
426
-	}
424
+    public function __construct($storage, $mountpoint, $arguments = null, $loader = null) {
425
+        // noop
426
+    }
427 427
 
428
-	public function stripUserFilesPathDummy($path) {
429
-		return $this->stripUserFilesPath($path);
430
-	}
428
+    public function stripUserFilesPathDummy($path) {
429
+        return $this->stripUserFilesPath($path);
430
+    }
431 431
 }
Please login to merge, or discard this patch.
tests/lib/Files/ViewTest.php 2 patches
Indentation   +2775 added lines, -2775 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,2757 +96,2757 @@  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
-		$defaultRootValue->setAccessible(true);
1543
-		$oldRoot = $defaultRootValue->getValue();
1544
-		$defaultView = new View('/foo/files');
1545
-		$defaultRootValue->setValue(null, $defaultView);
1546
-		$view = new View($root);
1547
-		$result = self::invokePrivate($view, 'shouldEmitHooks', [$path]);
1548
-		$defaultRootValue->setValue(null, $oldRoot);
1549
-		$this->assertEquals($shouldEmit, $result);
1550
-	}
1551
-
1552
-	/**
1553
-	 * Create test movable mount points
1554
-	 *
1555
-	 * @param array $mountPoints array of mount point locations
1556
-	 * @return array array of MountPoint objects
1557
-	 */
1558
-	private function createTestMovableMountPoints($mountPoints) {
1559
-		$mounts = [];
1560
-		foreach ($mountPoints as $mountPoint) {
1561
-			$storage = $this->getMockBuilder(Storage::class)
1562
-				->onlyMethods([])
1563
-				->getMock();
1564
-			$storage->method('getId')->willReturn('non-null-id');
1565
-			$storage->method('getStorageCache')->willReturnCallback(function () use ($storage) {
1566
-				return new \OC\Files\Cache\Storage($storage, true, Server::get(IDBConnection::class));
1567
-			});
1568
-
1569
-			$mounts[] = $this->getMockBuilder(TestMoveableMountPoint::class)
1570
-				->onlyMethods(['moveMount'])
1571
-				->setConstructorArgs([$storage, $mountPoint])
1572
-				->getMock();
1573
-		}
1574
-
1575
-		/** @var IMountProvider|\PHPUnit\Framework\MockObject\MockObject $mountProvider */
1576
-		$mountProvider = $this->createMock(IMountProvider::class);
1577
-		$mountProvider->expects($this->any())
1578
-			->method('getMountsForUser')
1579
-			->willReturn($mounts);
1580
-
1581
-		$mountProviderCollection = Server::get(IMountProviderCollection::class);
1582
-		$mountProviderCollection->registerProvider($mountProvider);
1583
-
1584
-		return $mounts;
1585
-	}
1586
-
1587
-	/**
1588
-	 * Test mount point move
1589
-	 */
1590
-	public function testMountPointMove(): void {
1591
-		self::loginAsUser($this->user);
1592
-
1593
-		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1594
-			$this->user . '/files/mount1',
1595
-			$this->user . '/files/mount2',
1596
-		]);
1597
-		$mount1->expects($this->once())
1598
-			->method('moveMount')
1599
-			->willReturn(true);
1600
-
1601
-		$mount2->expects($this->once())
1602
-			->method('moveMount')
1603
-			->willReturn(true);
1604
-
1605
-		$view = new View('/' . $this->user . '/files/');
1606
-		$view->mkdir('sub');
1607
-
1608
-		$this->assertTrue($view->rename('mount1', 'renamed_mount'), 'Can rename mount point');
1609
-		$this->assertTrue($view->rename('mount2', 'sub/moved_mount'), 'Can move a mount point into a subdirectory');
1610
-	}
1611
-
1612
-	public function testMoveMountPointOverwrite(): void {
1613
-		self::loginAsUser($this->user);
1614
-
1615
-		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1616
-			$this->user . '/files/mount1',
1617
-			$this->user . '/files/mount2',
1618
-		]);
1619
-
1620
-		$mount1->expects($this->never())
1621
-			->method('moveMount');
1622
-
1623
-		$mount2->expects($this->never())
1624
-			->method('moveMount');
1625
-
1626
-		$view = new View('/' . $this->user . '/files/');
1627
-
1628
-		$this->expectException(ForbiddenException::class);
1629
-		$view->rename('mount1', 'mount2');
1630
-	}
1631
-
1632
-	public function testMoveMountPointIntoMount(): void {
1633
-		self::loginAsUser($this->user);
1634
-
1635
-		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1636
-			$this->user . '/files/mount1',
1637
-			$this->user . '/files/mount2',
1638
-		]);
1639
-
1640
-		$mount1->expects($this->never())
1641
-			->method('moveMount');
1642
-
1643
-		$mount2->expects($this->never())
1644
-			->method('moveMount');
1645
-
1646
-		$view = new View('/' . $this->user . '/files/');
1647
-
1648
-		$this->expectException(ForbiddenException::class);
1649
-		$view->rename('mount1', 'mount2/sub');
1650
-	}
1651
-
1652
-	/**
1653
-	 * Test that moving a mount point into a shared folder is forbidden
1654
-	 */
1655
-	public function testMoveMountPointIntoSharedFolder(): void {
1656
-		self::loginAsUser($this->user);
1657
-
1658
-		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1659
-			$this->user . '/files/mount1',
1660
-			$this->user . '/files/mount2',
1661
-		]);
1662
-
1663
-		$mount1->expects($this->never())
1664
-			->method('moveMount');
1665
-
1666
-		$mount2->expects($this->once())
1667
-			->method('moveMount')
1668
-			->willReturn(true);
1669
-
1670
-		$view = new View('/' . $this->user . '/files/');
1671
-		$view->mkdir('shareddir');
1672
-		$view->mkdir('shareddir/sub');
1673
-		$view->mkdir('shareddir/sub2');
1674
-		// Create a similar named but non-shared folder
1675
-		$view->mkdir('shareddir notshared');
1676
-
1677
-		$fileId = $view->getFileInfo('shareddir')->getId();
1678
-		$userObject = Server::get(IUserManager::class)->createUser('test2', 'IHateNonMockableStaticClasses');
1679
-
1680
-		$userFolder = \OC::$server->getUserFolder($this->user);
1681
-		$shareDir = $userFolder->get('shareddir');
1682
-		$shareManager = Server::get(IShareManager::class);
1683
-		$share = $shareManager->newShare();
1684
-		$share->setSharedWith('test2')
1685
-			->setSharedBy($this->user)
1686
-			->setShareType(IShare::TYPE_USER)
1687
-			->setPermissions(Constants::PERMISSION_READ)
1688
-			->setNode($shareDir);
1689
-		$shareManager->createShare($share);
1690
-
1691
-		try {
1692
-			$view->rename('mount1', 'shareddir');
1693
-			$this->fail('Cannot overwrite shared folder');
1694
-		} catch (ForbiddenException $e) {
1695
-
1696
-		}
1697
-		try {
1698
-			$view->rename('mount1', 'shareddir/sub');
1699
-			$this->fail('Cannot move mount point into shared folder');
1700
-		} catch (ForbiddenException $e) {
1701
-
1702
-		}
1703
-		try {
1704
-			$view->rename('mount1', 'shareddir/sub/sub2');
1705
-			$this->fail('Cannot move mount point into shared subfolder');
1706
-		} catch (ForbiddenException $e) {
1707
-
1708
-		}
1709
-		$this->assertTrue($view->rename('mount2', 'shareddir notshared/sub'), 'Can move mount point into a similarly named but non-shared folder');
1710
-
1711
-		$shareManager->deleteShare($share);
1712
-		$userObject->delete();
1713
-	}
1714
-
1715
-	public static function basicOperationProviderForLocks(): array {
1716
-		return [
1717
-			// --- write hook ----
1718
-			[
1719
-				'touch',
1720
-				['touch-create.txt'],
1721
-				'touch-create.txt',
1722
-				'create',
1723
-				ILockingProvider::LOCK_SHARED,
1724
-				ILockingProvider::LOCK_EXCLUSIVE,
1725
-				ILockingProvider::LOCK_SHARED,
1726
-			],
1727
-			[
1728
-				'fopen',
1729
-				['test-write.txt', 'w'],
1730
-				'test-write.txt',
1731
-				'write',
1732
-				ILockingProvider::LOCK_SHARED,
1733
-				ILockingProvider::LOCK_EXCLUSIVE,
1734
-				null,
1735
-				// exclusive lock stays until fclose
1736
-				ILockingProvider::LOCK_EXCLUSIVE,
1737
-			],
1738
-			[
1739
-				'mkdir',
1740
-				['newdir'],
1741
-				'newdir',
1742
-				'write',
1743
-				ILockingProvider::LOCK_SHARED,
1744
-				ILockingProvider::LOCK_EXCLUSIVE,
1745
-				ILockingProvider::LOCK_SHARED,
1746
-			],
1747
-			[
1748
-				'file_put_contents',
1749
-				['file_put_contents.txt', 'blah'],
1750
-				'file_put_contents.txt',
1751
-				'write',
1752
-				ILockingProvider::LOCK_SHARED,
1753
-				ILockingProvider::LOCK_EXCLUSIVE,
1754
-				ILockingProvider::LOCK_SHARED,
1755
-				null,
1756
-				0,
1757
-			],
1758
-
1759
-			// ---- delete hook ----
1760
-			[
1761
-				'rmdir',
1762
-				['dir'],
1763
-				'dir',
1764
-				'delete',
1765
-				ILockingProvider::LOCK_SHARED,
1766
-				ILockingProvider::LOCK_EXCLUSIVE,
1767
-				ILockingProvider::LOCK_SHARED,
1768
-			],
1769
-			[
1770
-				'unlink',
1771
-				['test.txt'],
1772
-				'test.txt',
1773
-				'delete',
1774
-				ILockingProvider::LOCK_SHARED,
1775
-				ILockingProvider::LOCK_EXCLUSIVE,
1776
-				ILockingProvider::LOCK_SHARED,
1777
-			],
1778
-
1779
-			// ---- read hook (no post hooks) ----
1780
-			[
1781
-				'file_get_contents',
1782
-				['test.txt'],
1783
-				'test.txt',
1784
-				'read',
1785
-				ILockingProvider::LOCK_SHARED,
1786
-				ILockingProvider::LOCK_SHARED,
1787
-				null,
1788
-				null,
1789
-				false,
1790
-			],
1791
-			[
1792
-				'fopen',
1793
-				['test.txt', 'r'],
1794
-				'test.txt',
1795
-				'read',
1796
-				ILockingProvider::LOCK_SHARED,
1797
-				ILockingProvider::LOCK_SHARED,
1798
-				null,
1799
-			],
1800
-			[
1801
-				'opendir',
1802
-				['dir'],
1803
-				'dir',
1804
-				'read',
1805
-				ILockingProvider::LOCK_SHARED,
1806
-				ILockingProvider::LOCK_SHARED,
1807
-				null,
1808
-			],
1809
-
1810
-			// ---- no lock, touch hook ---
1811
-			['touch', ['test.txt'], 'test.txt', 'touch', null, null, null],
1812
-
1813
-			// ---- no hooks, no locks ---
1814
-			['is_dir', ['dir'], 'dir', ''],
1815
-			['is_file', ['dir'], 'dir', ''],
1816
-			[
1817
-				'stat',
1818
-				['dir'],
1819
-				'dir',
1820
-				'',
1821
-				ILockingProvider::LOCK_SHARED,
1822
-				ILockingProvider::LOCK_SHARED,
1823
-				ILockingProvider::LOCK_SHARED,
1824
-				null,
1825
-				false,
1826
-			],
1827
-			[
1828
-				'filetype',
1829
-				['dir'],
1830
-				'dir',
1831
-				'',
1832
-				ILockingProvider::LOCK_SHARED,
1833
-				ILockingProvider::LOCK_SHARED,
1834
-				ILockingProvider::LOCK_SHARED,
1835
-				null,
1836
-				false,
1837
-			],
1838
-			[
1839
-				'filesize',
1840
-				['dir'],
1841
-				'dir',
1842
-				'',
1843
-				ILockingProvider::LOCK_SHARED,
1844
-				ILockingProvider::LOCK_SHARED,
1845
-				ILockingProvider::LOCK_SHARED,
1846
-				null,
1847
-				/* Return an int */
1848
-				100
1849
-			],
1850
-			['isCreatable', ['dir'], 'dir', ''],
1851
-			['isReadable', ['dir'], 'dir', ''],
1852
-			['isUpdatable', ['dir'], 'dir', ''],
1853
-			['isDeletable', ['dir'], 'dir', ''],
1854
-			['isSharable', ['dir'], 'dir', ''],
1855
-			['file_exists', ['dir'], 'dir', ''],
1856
-			[
1857
-				'filemtime',
1858
-				['dir'],
1859
-				'dir',
1860
-				'',
1861
-				ILockingProvider::LOCK_SHARED,
1862
-				ILockingProvider::LOCK_SHARED,
1863
-				ILockingProvider::LOCK_SHARED,
1864
-				null,
1865
-				false,
1866
-			],
1867
-		];
1868
-	}
1869
-
1870
-	/**
1871
-	 * Test whether locks are set before and after the operation
1872
-	 *
1873
-	 *
1874
-	 * @param string $operation operation name on the view
1875
-	 * @param array $operationArgs arguments for the operation
1876
-	 * @param string $lockedPath path of the locked item to check
1877
-	 * @param string $hookType hook type
1878
-	 * @param ?int $expectedLockBefore expected lock during pre hooks
1879
-	 * @param ?int $expectedLockDuring expected lock during operation
1880
-	 * @param ?int $expectedLockAfter expected lock during post hooks
1881
-	 * @param ?int $expectedStrayLock expected lock after returning, should
1882
-	 *                                be null (unlock) for most operations
1883
-	 */
1884
-	#[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
1885
-	public function testLockBasicOperation(
1886
-		string $operation,
1887
-		array $operationArgs,
1888
-		string $lockedPath,
1889
-		string $hookType,
1890
-		?int $expectedLockBefore = ILockingProvider::LOCK_SHARED,
1891
-		?int $expectedLockDuring = ILockingProvider::LOCK_SHARED,
1892
-		?int $expectedLockAfter = ILockingProvider::LOCK_SHARED,
1893
-		?int $expectedStrayLock = null,
1894
-		mixed $returnValue = true,
1895
-	): void {
1896
-		$view = new View('/' . $this->user . '/files/');
1897
-
1898
-		/** @var Temporary&MockObject $storage */
1899
-		$storage = $this->getMockBuilder(Temporary::class)
1900
-			->onlyMethods([$operation])
1901
-			->getMock();
1902
-
1903
-		/* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
1904
-		Server::get(ITrashManager::class)->pauseTrash();
1905
-		/* Same thing with encryption wrapper */
1906
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
1907
-
1908
-		Filesystem::mount($storage, [], $this->user . '/');
1909
-
1910
-		// work directly on disk because mkdir might be mocked
1911
-		$realPath = $storage->getSourcePath('');
1912
-		mkdir($realPath . '/files');
1913
-		mkdir($realPath . '/files/dir');
1914
-		file_put_contents($realPath . '/files/test.txt', 'blah');
1915
-		$storage->getScanner()->scan('files');
1916
-
1917
-		$storage->expects($this->once())
1918
-			->method($operation)
1919
-			->willReturnCallback(
1920
-				function () use ($view, $lockedPath, &$lockTypeDuring, $returnValue) {
1921
-					$lockTypeDuring = $this->getFileLockType($view, $lockedPath);
1922
-
1923
-					return $returnValue;
1924
-				}
1925
-			);
1926
-
1927
-		$this->assertNull($this->getFileLockType($view, $lockedPath), 'File not locked before operation');
1928
-
1929
-		$this->connectMockHooks($hookType, $view, $lockedPath, $lockTypePre, $lockTypePost);
1930
-
1931
-		// do operation
1932
-		call_user_func_array([$view, $operation], $operationArgs);
1933
-
1934
-		if ($hookType !== '') {
1935
-			$this->assertEquals($expectedLockBefore, $lockTypePre, 'File locked properly during pre-hook');
1936
-			$this->assertEquals($expectedLockAfter, $lockTypePost, 'File locked properly during post-hook');
1937
-			$this->assertEquals($expectedLockDuring, $lockTypeDuring, 'File locked properly during operation');
1938
-		} else {
1939
-			$this->assertNull($lockTypeDuring, 'File not locked during operation');
1940
-		}
1941
-
1942
-		$this->assertEquals($expectedStrayLock, $this->getFileLockType($view, $lockedPath));
1943
-
1944
-		/* Resume trash to avoid side effects */
1945
-		Server::get(ITrashManager::class)->resumeTrash();
1946
-	}
1947
-
1948
-	/**
1949
-	 * Test locks for file_put_content with stream.
1950
-	 * This code path uses $storage->fopen instead
1951
-	 */
1952
-	public function testLockFilePutContentWithStream(): void {
1953
-		$view = new View('/' . $this->user . '/files/');
1954
-
1955
-		$path = 'test_file_put_contents.txt';
1956
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1957
-		$storage = $this->getMockBuilder(Temporary::class)
1958
-			->onlyMethods(['fopen'])
1959
-			->getMock();
1960
-
1961
-		Filesystem::mount($storage, [], $this->user . '/');
1962
-		$storage->mkdir('files');
1963
-
1964
-		$storage->expects($this->once())
1965
-			->method('fopen')
1966
-			->willReturnCallback(
1967
-				function () use ($view, $path, &$lockTypeDuring) {
1968
-					$lockTypeDuring = $this->getFileLockType($view, $path);
1969
-
1970
-					return fopen('php://temp', 'r+');
1971
-				}
1972
-			);
1973
-
1974
-		$this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost);
1975
-
1976
-		$this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation');
1977
-
1978
-		// do operation
1979
-		$view->file_put_contents($path, fopen('php://temp', 'r+'));
1980
-
1981
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook');
1982
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePost, 'File locked properly during post-hook');
1983
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation');
1984
-
1985
-		$this->assertNull($this->getFileLockType($view, $path));
1986
-	}
1987
-
1988
-	/**
1989
-	 * Test locks for fopen with fclose at the end
1990
-	 */
1991
-	public function testLockFopen(): void {
1992
-		$view = new View('/' . $this->user . '/files/');
1993
-
1994
-		$path = 'test_file_put_contents.txt';
1995
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1996
-		$storage = $this->getMockBuilder(Temporary::class)
1997
-			->onlyMethods(['fopen'])
1998
-			->getMock();
1999
-
2000
-		Filesystem::mount($storage, [], $this->user . '/');
2001
-		$storage->mkdir('files');
2002
-
2003
-		$storage->expects($this->once())
2004
-			->method('fopen')
2005
-			->willReturnCallback(
2006
-				function () use ($view, $path, &$lockTypeDuring) {
2007
-					$lockTypeDuring = $this->getFileLockType($view, $path);
2008
-
2009
-					return fopen('php://temp', 'r+');
2010
-				}
2011
-			);
2012
-
2013
-		$this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost);
2014
-
2015
-		$this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation');
2016
-
2017
-		// do operation
2018
-		$res = $view->fopen($path, 'w');
2019
-
2020
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook');
2021
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation');
2022
-		$this->assertNull($lockTypePost, 'No post hook, no lock check possible');
2023
-
2024
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File still locked after fopen');
2025
-
2026
-		fclose($res);
2027
-
2028
-		$this->assertNull($this->getFileLockType($view, $path), 'File unlocked after fclose');
2029
-	}
2030
-
2031
-	/**
2032
-	 * Test locks for fopen with fclose at the end
2033
-	 *
2034
-	 *
2035
-	 * @param string $operation operation name on the view
2036
-	 * @param array $operationArgs arguments for the operation
2037
-	 * @param string $path path of the locked item to check
2038
-	 */
2039
-	#[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
2040
-	public function testLockBasicOperationUnlocksAfterException(
2041
-		$operation,
2042
-		$operationArgs,
2043
-		$path,
2044
-	): void {
2045
-		if ($operation === 'touch') {
2046
-			$this->markTestSkipped('touch handles storage exceptions internally');
2047
-		}
2048
-		$view = new View('/' . $this->user . '/files/');
2049
-
2050
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2051
-		$storage = $this->getMockBuilder(Temporary::class)
2052
-			->onlyMethods([$operation])
2053
-			->getMock();
2054
-
2055
-		/* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
2056
-		Server::get(ITrashManager::class)->pauseTrash();
2057
-		/* Same thing with encryption wrapper */
2058
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2059
-
2060
-		Filesystem::mount($storage, [], $this->user . '/');
2061
-
2062
-		// work directly on disk because mkdir might be mocked
2063
-		$realPath = $storage->getSourcePath('');
2064
-		mkdir($realPath . '/files');
2065
-		mkdir($realPath . '/files/dir');
2066
-		file_put_contents($realPath . '/files/test.txt', 'blah');
2067
-		$storage->getScanner()->scan('files');
2068
-
2069
-		$storage->expects($this->once())
2070
-			->method($operation)
2071
-			->willReturnCallback(
2072
-				function (): void {
2073
-					throw new \Exception('Simulated exception');
2074
-				}
2075
-			);
2076
-
2077
-		$thrown = false;
2078
-		try {
2079
-			call_user_func_array([$view, $operation], $operationArgs);
2080
-		} catch (\Exception $e) {
2081
-			$thrown = true;
2082
-			$this->assertEquals('Simulated exception', $e->getMessage());
2083
-		}
2084
-		$this->assertTrue($thrown, 'Exception was rethrown');
2085
-		$this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception');
2086
-
2087
-		/* Resume trash to avoid side effects */
2088
-		Server::get(ITrashManager::class)->resumeTrash();
2089
-	}
2090
-
2091
-	public function testLockBasicOperationUnlocksAfterLockException(): void {
2092
-		$view = new View('/' . $this->user . '/files/');
2093
-
2094
-		$storage = new Temporary([]);
2095
-
2096
-		Filesystem::mount($storage, [], $this->user . '/');
2097
-
2098
-		$storage->mkdir('files');
2099
-		$storage->mkdir('files/dir');
2100
-		$storage->file_put_contents('files/test.txt', 'blah');
2101
-		$storage->getScanner()->scan('files');
2102
-
2103
-		// get a shared lock
2104
-		$handle = $view->fopen('test.txt', 'r');
2105
-
2106
-		$thrown = false;
2107
-		try {
2108
-			// try (and fail) to get a write lock
2109
-			$view->unlink('test.txt');
2110
-		} catch (\Exception $e) {
2111
-			$thrown = true;
2112
-			$this->assertInstanceOf(LockedException::class, $e);
2113
-		}
2114
-		$this->assertTrue($thrown, 'Exception was rethrown');
2115
-
2116
-		// clean shared lock
2117
-		fclose($handle);
2118
-
2119
-		$this->assertNull($this->getFileLockType($view, 'test.txt'), 'File got unlocked');
2120
-	}
2121
-
2122
-	/**
2123
-	 * Test locks for fopen with fclose at the end
2124
-	 *
2125
-	 *
2126
-	 * @param string $operation operation name on the view
2127
-	 * @param array $operationArgs arguments for the operation
2128
-	 * @param string $path path of the locked item to check
2129
-	 * @param string $hookType hook type
2130
-	 */
2131
-	#[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
2132
-	public function testLockBasicOperationUnlocksAfterCancelledHook(
2133
-		$operation,
2134
-		$operationArgs,
2135
-		$path,
2136
-		$hookType,
2137
-	): void {
2138
-		$view = new View('/' . $this->user . '/files/');
2139
-
2140
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2141
-		$storage = $this->getMockBuilder(Temporary::class)
2142
-			->onlyMethods([$operation])
2143
-			->getMock();
2144
-
2145
-		Filesystem::mount($storage, [], $this->user . '/');
2146
-		$storage->mkdir('files');
2147
-
2148
-		Util::connectHook(
2149
-			Filesystem::CLASSNAME,
2150
-			$hookType,
2151
-			HookHelper::class,
2152
-			'cancellingCallback'
2153
-		);
2154
-
2155
-		call_user_func_array([$view, $operation], $operationArgs);
2156
-
2157
-		$this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception');
2158
-	}
2159
-
2160
-	public static function lockFileRenameOrCopyDataProvider(): array {
2161
-		return [
2162
-			['rename', ILockingProvider::LOCK_EXCLUSIVE],
2163
-			['copy', ILockingProvider::LOCK_SHARED],
2164
-		];
2165
-	}
2166
-
2167
-	/**
2168
-	 * Test locks for rename or copy operation
2169
-	 *
2170
-	 *
2171
-	 * @param string $operation operation to be done on the view
2172
-	 * @param int $expectedLockTypeSourceDuring expected lock type on source file during
2173
-	 *                                          the operation
2174
-	 */
2175
-	#[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyDataProvider')]
2176
-	public function testLockFileRename($operation, $expectedLockTypeSourceDuring): void {
2177
-		$view = new View('/' . $this->user . '/files/');
2178
-
2179
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2180
-		$storage = $this->getMockBuilder(Temporary::class)
2181
-			->onlyMethods([$operation, 'getMetaData', 'filemtime'])
2182
-			->getMock();
2183
-
2184
-		$storage->expects($this->any())
2185
-			->method('getMetaData')
2186
-			->willReturn([
2187
-				'mtime' => 1885434487,
2188
-				'etag' => '',
2189
-				'mimetype' => 'text/plain',
2190
-				'permissions' => Constants::PERMISSION_ALL,
2191
-				'size' => 3
2192
-			]);
2193
-		$storage->expects($this->any())
2194
-			->method('filemtime')
2195
-			->willReturn(123456789);
2196
-
2197
-		$sourcePath = 'original.txt';
2198
-		$targetPath = 'target.txt';
2199
-
2200
-		/* Disable encryption wrapper to avoid it intercepting mocked call */
2201
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2202
-
2203
-		Filesystem::mount($storage, [], $this->user . '/');
2204
-		$storage->mkdir('files');
2205
-		$view->file_put_contents($sourcePath, 'meh');
2206
-
2207
-		$storage->expects($this->once())
2208
-			->method($operation)
2209
-			->willReturnCallback(
2210
-				function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2211
-					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2212
-					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2213
-
2214
-					return true;
2215
-				}
2216
-			);
2217
-
2218
-		$this->connectMockHooks($operation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2219
-		$this->connectMockHooks($operation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2220
-
2221
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2222
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2223
-
2224
-		$view->$operation($sourcePath, $targetPath);
2225
-
2226
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook');
2227
-		$this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation');
2228
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook');
2229
-
2230
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook');
2231
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation');
2232
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook');
2233
-
2234
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2235
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2236
-	}
2237
-
2238
-	/**
2239
-	 * simulate a failed copy operation.
2240
-	 * We expect that we catch the exception, free the lock and re-throw it.
2241
-	 *
2242
-	 */
2243
-	public function testLockFileCopyException(): void {
2244
-		$this->expectException(\Exception::class);
2245
-
2246
-		$view = new View('/' . $this->user . '/files/');
2247
-
2248
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2249
-		$storage = $this->getMockBuilder(Temporary::class)
2250
-			->onlyMethods(['copy'])
2251
-			->getMock();
2252
-
2253
-		$sourcePath = 'original.txt';
2254
-		$targetPath = 'target.txt';
2255
-
2256
-		/* Disable encryption wrapper to avoid it intercepting mocked call */
2257
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2258
-
2259
-		Filesystem::mount($storage, [], $this->user . '/');
2260
-		$storage->mkdir('files');
2261
-		$view->file_put_contents($sourcePath, 'meh');
2262
-
2263
-		$storage->expects($this->once())
2264
-			->method('copy')
2265
-			->willReturnCallback(
2266
-				function (): void {
2267
-					throw new \Exception();
2268
-				}
2269
-			);
2270
-
2271
-		$this->connectMockHooks('copy', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2272
-		$this->connectMockHooks('copy', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2273
-
2274
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2275
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2276
-
2277
-		try {
2278
-			$view->copy($sourcePath, $targetPath);
2279
-		} catch (\Exception $e) {
2280
-			$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2281
-			$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2282
-			throw $e;
2283
-		}
2284
-	}
2285
-
2286
-	/**
2287
-	 * Test rename operation: unlock first path when second path was locked
2288
-	 */
2289
-	public function testLockFileRenameUnlockOnException(): void {
2290
-		self::loginAsUser('test');
2291
-
2292
-		$view = new View('/' . $this->user . '/files/');
2293
-
2294
-		$sourcePath = 'original.txt';
2295
-		$targetPath = 'target.txt';
2296
-		$view->file_put_contents($sourcePath, 'meh');
2297
-
2298
-		// simulate that the target path is already locked
2299
-		$view->lockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
2300
-
2301
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2302
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file is locked before operation');
2303
-
2304
-		$thrown = false;
2305
-		try {
2306
-			$view->rename($sourcePath, $targetPath);
2307
-		} catch (LockedException $e) {
2308
-			$thrown = true;
2309
-		}
2310
-
2311
-		$this->assertTrue($thrown, 'LockedException thrown');
2312
-
2313
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2314
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file still locked after operation');
2315
-
2316
-		$view->unlockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
2317
-	}
2318
-
2319
-	/**
2320
-	 * Test rename operation: unlock first path when second path was locked
2321
-	 */
2322
-	public function testGetOwner(): void {
2323
-		self::loginAsUser('test');
2324
-
2325
-		$view = new View('/test/files/');
2326
-
2327
-		$path = 'foo.txt';
2328
-		$view->file_put_contents($path, 'meh');
2329
-
2330
-		$this->assertEquals('test', $view->getFileInfo($path)->getOwner()->getUID());
2331
-
2332
-		$folderInfo = $view->getDirectoryContent('');
2333
-		$folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2334
-			return $info->getName() === 'foo.txt';
2335
-		}));
2336
-
2337
-		$this->assertEquals('test', $folderInfo[0]->getOwner()->getUID());
2338
-
2339
-		$subStorage = new Temporary();
2340
-		Filesystem::mount($subStorage, [], '/test/files/asd');
2341
-
2342
-		$folderInfo = $view->getDirectoryContent('');
2343
-		$folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2344
-			return $info->getName() === 'asd';
2345
-		}));
2346
-
2347
-		$this->assertEquals('test', $folderInfo[0]->getOwner()->getUID());
2348
-	}
2349
-
2350
-	public static function lockFileRenameOrCopyCrossStorageDataProvider(): array {
2351
-		return [
2352
-			['rename', 'moveFromStorage', ILockingProvider::LOCK_EXCLUSIVE],
2353
-			['copy', 'copyFromStorage', ILockingProvider::LOCK_SHARED],
2354
-		];
2355
-	}
2356
-
2357
-	/**
2358
-	 * Test locks for rename or copy operation cross-storage
2359
-	 *
2360
-	 *
2361
-	 * @param string $viewOperation operation to be done on the view
2362
-	 * @param string $storageOperation operation to be mocked on the storage
2363
-	 * @param int $expectedLockTypeSourceDuring expected lock type on source file during
2364
-	 *                                          the operation
2365
-	 */
2366
-	#[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyCrossStorageDataProvider')]
2367
-	public function testLockFileRenameCrossStorage($viewOperation, $storageOperation, $expectedLockTypeSourceDuring): void {
2368
-		$view = new View('/' . $this->user . '/files/');
2369
-
2370
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2371
-		$storage = $this->getMockBuilder(Temporary::class)
2372
-			->onlyMethods([$storageOperation])
2373
-			->getMock();
2374
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage2 */
2375
-		$storage2 = $this->getMockBuilder(Temporary::class)
2376
-			->onlyMethods([$storageOperation, 'getMetaData', 'filemtime'])
2377
-			->getMock();
2378
-
2379
-		$storage2->expects($this->any())
2380
-			->method('getMetaData')
2381
-			->willReturn([
2382
-				'mtime' => 1885434487,
2383
-				'etag' => '',
2384
-				'mimetype' => 'text/plain',
2385
-				'permissions' => Constants::PERMISSION_ALL,
2386
-				'size' => 3
2387
-			]);
2388
-		$storage2->expects($this->any())
2389
-			->method('filemtime')
2390
-			->willReturn(123456789);
2391
-
2392
-		$sourcePath = 'original.txt';
2393
-		$targetPath = 'substorage/target.txt';
2394
-
2395
-		/* Disable encryption wrapper to avoid it intercepting mocked call */
2396
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2397
-
2398
-		Filesystem::mount($storage, [], $this->user . '/');
2399
-		Filesystem::mount($storage2, [], $this->user . '/files/substorage');
2400
-		$storage->mkdir('files');
2401
-		$view->file_put_contents($sourcePath, 'meh');
2402
-		$storage2->getUpdater()->update('');
2403
-
2404
-		$storage->expects($this->never())
2405
-			->method($storageOperation);
2406
-		$storage2->expects($this->once())
2407
-			->method($storageOperation)
2408
-			->willReturnCallback(
2409
-				function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2410
-					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2411
-					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2412
-
2413
-					return true;
2414
-				}
2415
-			);
2416
-
2417
-		$this->connectMockHooks($viewOperation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2418
-		$this->connectMockHooks($viewOperation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2419
-
2420
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2421
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2422
-
2423
-		$view->$viewOperation($sourcePath, $targetPath);
2424
-
2425
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook');
2426
-		$this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation');
2427
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook');
2428
-
2429
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook');
2430
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation');
2431
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook');
2432
-
2433
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2434
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2435
-	}
2436
-
2437
-	/**
2438
-	 * Test locks when moving a mount point
2439
-	 */
2440
-	public function testLockMoveMountPoint(): void {
2441
-		self::loginAsUser('test');
2442
-
2443
-		[$mount] = $this->createTestMovableMountPoints([
2444
-			$this->user . '/files/substorage',
2445
-		]);
2446
-
2447
-		$view = new View('/' . $this->user . '/files/');
2448
-		$view->mkdir('subdir');
2449
-
2450
-		$sourcePath = 'substorage';
2451
-		$targetPath = 'subdir/substorage_moved';
2452
-
2453
-		$mount->expects($this->once())
2454
-			->method('moveMount')
2455
-			->willReturnCallback(
2456
-				function ($target) use ($mount, $view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring, &$lockTypeSharedRootDuring) {
2457
-					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath, true);
2458
-					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath, true);
2459
-
2460
-					$lockTypeSharedRootDuring = $this->getFileLockType($view, $sourcePath, false);
2461
-
2462
-					$mount->setMountPoint($target);
2463
-
2464
-					return true;
2465
-				}
2466
-			);
2467
-
2468
-		$this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost, true);
2469
-		$this->connectMockHooks('rename', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost, true);
2470
-		// in pre-hook, mount point is still on $sourcePath
2471
-		$this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSharedRootPre, $dummy, false);
2472
-		// in post-hook, mount point is now on $targetPath
2473
-		$this->connectMockHooks('rename', $view, $targetPath, $dummy, $lockTypeSharedRootPost, false);
2474
-
2475
-		$this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked before operation');
2476
-		$this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked before operation');
2477
-		$this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked before operation');
2478
-
2479
-		$view->rename($sourcePath, $targetPath);
2480
-
2481
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source path locked properly during pre-hook');
2482
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeSourceDuring, 'Source path locked properly during operation');
2483
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source path locked properly during post-hook');
2484
-
2485
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target path locked properly during pre-hook');
2486
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target path locked properly during operation');
2487
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target path locked properly during post-hook');
2488
-
2489
-		$this->assertNull($lockTypeSharedRootPre, 'Shared storage root not locked during pre-hook');
2490
-		$this->assertNull($lockTypeSharedRootDuring, 'Shared storage root not locked during move');
2491
-		$this->assertNull($lockTypeSharedRootPost, 'Shared storage root not locked during post-hook');
2492
-
2493
-		$this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked after operation');
2494
-		$this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked after operation');
2495
-		$this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked after operation');
2496
-	}
2497
-
2498
-	/**
2499
-	 * Connect hook callbacks for hook type
2500
-	 *
2501
-	 * @param string $hookType hook type or null for none
2502
-	 * @param View $view view to check the lock on
2503
-	 * @param string $path path for which to check the lock
2504
-	 * @param int $lockTypePre variable to receive lock type that was active in the pre-hook
2505
-	 * @param int $lockTypePost variable to receive lock type that was active in the post-hook
2506
-	 * @param bool $onMountPoint true to check the mount point instead of the
2507
-	 *                           mounted storage
2508
-	 */
2509
-	private function connectMockHooks($hookType, $view, $path, &$lockTypePre, &$lockTypePost, $onMountPoint = false) {
2510
-		if ($hookType === null) {
2511
-			return;
2512
-		}
2513
-
2514
-		$eventHandler = $this->getMockBuilder(TestEventHandler::class)
2515
-			->onlyMethods(['preCallback', 'postCallback'])
2516
-			->getMock();
2517
-
2518
-		$eventHandler->expects($this->any())
2519
-			->method('preCallback')
2520
-			->willReturnCallback(
2521
-				function () use ($view, $path, $onMountPoint, &$lockTypePre): void {
2522
-					$lockTypePre = $this->getFileLockType($view, $path, $onMountPoint);
2523
-				}
2524
-			);
2525
-		$eventHandler->expects($this->any())
2526
-			->method('postCallback')
2527
-			->willReturnCallback(
2528
-				function () use ($view, $path, $onMountPoint, &$lockTypePost): void {
2529
-					$lockTypePost = $this->getFileLockType($view, $path, $onMountPoint);
2530
-				}
2531
-			);
2532
-
2533
-		if ($hookType !== '') {
2534
-			Util::connectHook(
2535
-				Filesystem::CLASSNAME,
2536
-				$hookType,
2537
-				$eventHandler,
2538
-				'preCallback'
2539
-			);
2540
-			Util::connectHook(
2541
-				Filesystem::CLASSNAME,
2542
-				'post_' . $hookType,
2543
-				$eventHandler,
2544
-				'postCallback'
2545
-			);
2546
-		}
2547
-	}
2548
-
2549
-	/**
2550
-	 * Returns the file lock type
2551
-	 *
2552
-	 * @param View $view view
2553
-	 * @param string $path path
2554
-	 * @param bool $onMountPoint true to check the mount point instead of the
2555
-	 *                           mounted storage
2556
-	 *
2557
-	 * @return int lock type or null if file was not locked
2558
-	 */
2559
-	private function getFileLockType(View $view, $path, $onMountPoint = false) {
2560
-		if ($this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE, $onMountPoint)) {
2561
-			return ILockingProvider::LOCK_EXCLUSIVE;
2562
-		} elseif ($this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED, $onMountPoint)) {
2563
-			return ILockingProvider::LOCK_SHARED;
2564
-		}
2565
-		return null;
2566
-	}
2567
-
2568
-
2569
-	public function testRemoveMoveableMountPoint(): void {
2570
-		$mountPoint = '/' . $this->user . '/files/mount/';
2571
-
2572
-		// Mock the mount point
2573
-		/** @var TestMoveableMountPoint|\PHPUnit\Framework\MockObject\MockObject $mount */
2574
-		$mount = $this->createMock(TestMoveableMountPoint::class);
2575
-		$mount->expects($this->once())
2576
-			->method('getMountPoint')
2577
-			->willReturn($mountPoint);
2578
-		$mount->expects($this->once())
2579
-			->method('removeMount')
2580
-			->willReturn('foo');
2581
-		$mount->expects($this->any())
2582
-			->method('getInternalPath')
2583
-			->willReturn('');
2584
-
2585
-		// Register mount
2586
-		Filesystem::getMountManager()->addMount($mount);
2587
-
2588
-		// Listen for events
2589
-		$eventHandler = $this->getMockBuilder(TestEventHandler::class)
2590
-			->onlyMethods(['umount', 'post_umount'])
2591
-			->getMock();
2592
-		$eventHandler->expects($this->once())
2593
-			->method('umount')
2594
-			->with([Filesystem::signal_param_path => '/mount']);
2595
-		$eventHandler->expects($this->once())
2596
-			->method('post_umount')
2597
-			->with([Filesystem::signal_param_path => '/mount']);
2598
-		Util::connectHook(
2599
-			Filesystem::CLASSNAME,
2600
-			'umount',
2601
-			$eventHandler,
2602
-			'umount'
2603
-		);
2604
-		Util::connectHook(
2605
-			Filesystem::CLASSNAME,
2606
-			'post_umount',
2607
-			$eventHandler,
2608
-			'post_umount'
2609
-		);
2610
-
2611
-		//Delete the mountpoint
2612
-		$view = new View('/' . $this->user . '/files');
2613
-		$this->assertEquals('foo', $view->rmdir('mount'));
2614
-	}
2615
-
2616
-	public static function mimeFilterProvider(): array {
2617
-		return [
2618
-			[null, ['test1.txt', 'test2.txt', 'test3.md', 'test4.png']],
2619
-			['text/plain', ['test1.txt', 'test2.txt']],
2620
-			['text/markdown', ['test3.md']],
2621
-			['text', ['test1.txt', 'test2.txt', 'test3.md']],
2622
-		];
2623
-	}
2624
-
2625
-	/**
2626
-	 * @param string $filter
2627
-	 * @param string[] $expected
2628
-	 */
2629
-	#[\PHPUnit\Framework\Attributes\DataProvider('mimeFilterProvider')]
2630
-	public function testGetDirectoryContentMimeFilter($filter, $expected): void {
2631
-		$storage1 = new Temporary();
2632
-		$root = self::getUniqueID('/');
2633
-		Filesystem::mount($storage1, [], $root . '/');
2634
-		$view = new View($root);
2635
-
2636
-		$view->file_put_contents('test1.txt', 'asd');
2637
-		$view->file_put_contents('test2.txt', 'asd');
2638
-		$view->file_put_contents('test3.md', 'asd');
2639
-		$view->file_put_contents('test4.png', '');
2640
-
2641
-		$content = $view->getDirectoryContent('', $filter);
2642
-
2643
-		$files = array_map(function (FileInfo $info) {
2644
-			return $info->getName();
2645
-		}, $content);
2646
-		sort($files);
2647
-
2648
-		$this->assertEquals($expected, $files);
2649
-	}
2650
-
2651
-	public function testFilePutContentsClearsChecksum(): void {
2652
-		$storage = new Temporary([]);
2653
-		$scanner = $storage->getScanner();
2654
-		$storage->file_put_contents('foo.txt', 'bar');
2655
-		Filesystem::mount($storage, [], '/test/');
2656
-		$scanner->scan('');
2657
-
2658
-		$view = new View('/test/foo.txt');
2659
-		$view->putFileInfo('.', ['checksum' => '42']);
2660
-
2661
-		$this->assertEquals('bar', $view->file_get_contents(''));
2662
-		$fh = tmpfile();
2663
-		fwrite($fh, 'fooo');
2664
-		rewind($fh);
2665
-		clearstatcache();
2666
-		$view->file_put_contents('', $fh);
2667
-		$this->assertEquals('fooo', $view->file_get_contents(''));
2668
-		$data = $view->getFileInfo('.');
2669
-		$this->assertEquals('', $data->getChecksum());
2670
-	}
2671
-
2672
-	public function testDeleteGhostFile(): void {
2673
-		$storage = new Temporary([]);
2674
-		$scanner = $storage->getScanner();
2675
-		$cache = $storage->getCache();
2676
-		$storage->file_put_contents('foo.txt', 'bar');
2677
-		Filesystem::mount($storage, [], '/test/');
2678
-		$scanner->scan('');
2679
-
2680
-		$storage->unlink('foo.txt');
2681
-
2682
-		$this->assertTrue($cache->inCache('foo.txt'));
2683
-
2684
-		$view = new View('/test');
2685
-		$rootInfo = $view->getFileInfo('');
2686
-		$this->assertEquals(3, $rootInfo->getSize());
2687
-		$view->unlink('foo.txt');
2688
-		$newInfo = $view->getFileInfo('');
2689
-
2690
-		$this->assertFalse($cache->inCache('foo.txt'));
2691
-		$this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag());
2692
-		$this->assertEquals(0, $newInfo->getSize());
2693
-	}
2694
-
2695
-	public function testDeleteGhostFolder(): void {
2696
-		$storage = new Temporary([]);
2697
-		$scanner = $storage->getScanner();
2698
-		$cache = $storage->getCache();
2699
-		$storage->mkdir('foo');
2700
-		$storage->file_put_contents('foo/foo.txt', 'bar');
2701
-		Filesystem::mount($storage, [], '/test/');
2702
-		$scanner->scan('');
2703
-
2704
-		$storage->rmdir('foo');
2705
-
2706
-		$this->assertTrue($cache->inCache('foo'));
2707
-		$this->assertTrue($cache->inCache('foo/foo.txt'));
2708
-
2709
-		$view = new View('/test');
2710
-		$rootInfo = $view->getFileInfo('');
2711
-		$this->assertEquals(3, $rootInfo->getSize());
2712
-		$view->rmdir('foo');
2713
-		$newInfo = $view->getFileInfo('');
2714
-
2715
-		$this->assertFalse($cache->inCache('foo'));
2716
-		$this->assertFalse($cache->inCache('foo/foo.txt'));
2717
-		$this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag());
2718
-		$this->assertEquals(0, $newInfo->getSize());
2719
-	}
2720
-
2721
-	public function testCreateParentDirectories(): void {
2722
-		$view = $this->getMockBuilder(View::class)
2723
-			->disableOriginalConstructor()
2724
-			->onlyMethods([
2725
-				'is_file',
2726
-				'file_exists',
2727
-				'mkdir',
2728
-			])
2729
-			->getMock();
2730
-
2731
-		$view->expects($this->exactly(3))
2732
-			->method('is_file')
2733
-			->willReturnMap([
2734
-				['/new', false],
2735
-				['/new/folder', false],
2736
-				['/new/folder/structure', false],
2737
-			]);
2738
-		$view->expects($this->exactly(3))
2739
-			->method('file_exists')
2740
-			->willReturnMap([
2741
-				['/new', true],
2742
-				['/new/folder', false],
2743
-				['/new/folder/structure', false],
2744
-			]);
2745
-
2746
-		$calls = ['/new/folder', '/new/folder/structure'];
2747
-		$view->expects($this->exactly(2))
2748
-			->method('mkdir')
2749
-			->willReturnCallback(function ($dir) use (&$calls): void {
2750
-				$expected = array_shift($calls);
2751
-				$this->assertEquals($expected, $dir);
2752
-			});
2753
-
2754
-		$this->assertTrue(self::invokePrivate($view, 'createParentDirectories', ['/new/folder/structure']));
2755
-	}
2756
-
2757
-	public function testCreateParentDirectoriesWithExistingFile(): void {
2758
-		$view = $this->getMockBuilder(View::class)
2759
-			->disableOriginalConstructor()
2760
-			->onlyMethods([
2761
-				'is_file',
2762
-				'file_exists',
2763
-				'mkdir',
2764
-			])
2765
-			->getMock();
2766
-
2767
-		$view
2768
-			->expects($this->once())
2769
-			->method('is_file')
2770
-			->with('/file.txt')
2771
-			->willReturn(true);
2772
-		$this->assertFalse(self::invokePrivate($view, 'createParentDirectories', ['/file.txt/folder/structure']));
2773
-	}
2774
-
2775
-	public function testCacheExtension(): void {
2776
-		$storage = new Temporary([]);
2777
-		$scanner = $storage->getScanner();
2778
-		$storage->file_put_contents('foo.txt', 'bar');
2779
-		$scanner->scan('');
2780
-
2781
-		Filesystem::mount($storage, [], '/test/');
2782
-		$view = new View('/test');
2783
-
2784
-		$info = $view->getFileInfo('/foo.txt');
2785
-		$this->assertEquals(0, $info->getUploadTime());
2786
-		$this->assertEquals(0, $info->getCreationTime());
2787
-
2788
-		$view->putFileInfo('/foo.txt', ['upload_time' => 25]);
2789
-
2790
-		$info = $view->getFileInfo('/foo.txt');
2791
-		$this->assertEquals(25, $info->getUploadTime());
2792
-		$this->assertEquals(0, $info->getCreationTime());
2793
-	}
2794
-
2795
-	public function testFopenGone(): void {
2796
-		$storage = new Temporary([]);
2797
-		$scanner = $storage->getScanner();
2798
-		$storage->file_put_contents('foo.txt', 'bar');
2799
-		$scanner->scan('');
2800
-		$cache = $storage->getCache();
2801
-
2802
-		Filesystem::mount($storage, [], '/test/');
2803
-		$view = new View('/test');
2804
-
2805
-		$storage->unlink('foo.txt');
2806
-
2807
-		$this->assertTrue($cache->inCache('foo.txt'));
2808
-
2809
-		$this->assertFalse($view->fopen('foo.txt', 'r'));
2810
-
2811
-		$this->assertFalse($cache->inCache('foo.txt'));
2812
-	}
2813
-
2814
-	public function testMountpointParentsCreated(): void {
2815
-		$storage1 = $this->getTestStorage();
2816
-		Filesystem::mount($storage1, [], '/');
2817
-
2818
-		$storage2 = $this->getTestStorage();
2819
-		Filesystem::mount($storage2, [], '/A/B/C');
2820
-
2821
-		$rootView = new View('');
2822
-
2823
-		$folderData = $rootView->getDirectoryContent('/');
2824
-		$this->assertCount(4, $folderData);
2825
-		$this->assertEquals('folder', $folderData[0]['name']);
2826
-		$this->assertEquals('foo.png', $folderData[1]['name']);
2827
-		$this->assertEquals('foo.txt', $folderData[2]['name']);
2828
-		$this->assertEquals('A', $folderData[3]['name']);
2829
-
2830
-		$folderData = $rootView->getDirectoryContent('/A');
2831
-		$this->assertCount(1, $folderData);
2832
-		$this->assertEquals('B', $folderData[0]['name']);
2833
-
2834
-		$folderData = $rootView->getDirectoryContent('/A/B');
2835
-		$this->assertCount(1, $folderData);
2836
-		$this->assertEquals('C', $folderData[0]['name']);
2837
-
2838
-		$folderData = $rootView->getDirectoryContent('/A/B/C');
2839
-		$this->assertCount(3, $folderData);
2840
-		$this->assertEquals('folder', $folderData[0]['name']);
2841
-		$this->assertEquals('foo.png', $folderData[1]['name']);
2842
-		$this->assertEquals('foo.txt', $folderData[2]['name']);
2843
-	}
2844
-
2845
-	public function testCopyPreservesContent() {
2846
-		$viewUser1 = new View('/' . 'userId' . '/files');
2847
-		$viewUser1->mkdir('');
2848
-		$viewUser1->file_put_contents('foo.txt', 'foo');
2849
-		$viewUser1->copy('foo.txt', 'bar.txt');
2850
-		$this->assertEquals('foo', $viewUser1->file_get_contents('bar.txt'));
2851
-	}
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
+        $defaultRootValue->setAccessible(true);
1543
+        $oldRoot = $defaultRootValue->getValue();
1544
+        $defaultView = new View('/foo/files');
1545
+        $defaultRootValue->setValue(null, $defaultView);
1546
+        $view = new View($root);
1547
+        $result = self::invokePrivate($view, 'shouldEmitHooks', [$path]);
1548
+        $defaultRootValue->setValue(null, $oldRoot);
1549
+        $this->assertEquals($shouldEmit, $result);
1550
+    }
1551
+
1552
+    /**
1553
+     * Create test movable mount points
1554
+     *
1555
+     * @param array $mountPoints array of mount point locations
1556
+     * @return array array of MountPoint objects
1557
+     */
1558
+    private function createTestMovableMountPoints($mountPoints) {
1559
+        $mounts = [];
1560
+        foreach ($mountPoints as $mountPoint) {
1561
+            $storage = $this->getMockBuilder(Storage::class)
1562
+                ->onlyMethods([])
1563
+                ->getMock();
1564
+            $storage->method('getId')->willReturn('non-null-id');
1565
+            $storage->method('getStorageCache')->willReturnCallback(function () use ($storage) {
1566
+                return new \OC\Files\Cache\Storage($storage, true, Server::get(IDBConnection::class));
1567
+            });
1568
+
1569
+            $mounts[] = $this->getMockBuilder(TestMoveableMountPoint::class)
1570
+                ->onlyMethods(['moveMount'])
1571
+                ->setConstructorArgs([$storage, $mountPoint])
1572
+                ->getMock();
1573
+        }
1574
+
1575
+        /** @var IMountProvider|\PHPUnit\Framework\MockObject\MockObject $mountProvider */
1576
+        $mountProvider = $this->createMock(IMountProvider::class);
1577
+        $mountProvider->expects($this->any())
1578
+            ->method('getMountsForUser')
1579
+            ->willReturn($mounts);
1580
+
1581
+        $mountProviderCollection = Server::get(IMountProviderCollection::class);
1582
+        $mountProviderCollection->registerProvider($mountProvider);
1583
+
1584
+        return $mounts;
1585
+    }
1586
+
1587
+    /**
1588
+     * Test mount point move
1589
+     */
1590
+    public function testMountPointMove(): void {
1591
+        self::loginAsUser($this->user);
1592
+
1593
+        [$mount1, $mount2] = $this->createTestMovableMountPoints([
1594
+            $this->user . '/files/mount1',
1595
+            $this->user . '/files/mount2',
1596
+        ]);
1597
+        $mount1->expects($this->once())
1598
+            ->method('moveMount')
1599
+            ->willReturn(true);
1600
+
1601
+        $mount2->expects($this->once())
1602
+            ->method('moveMount')
1603
+            ->willReturn(true);
1604
+
1605
+        $view = new View('/' . $this->user . '/files/');
1606
+        $view->mkdir('sub');
1607
+
1608
+        $this->assertTrue($view->rename('mount1', 'renamed_mount'), 'Can rename mount point');
1609
+        $this->assertTrue($view->rename('mount2', 'sub/moved_mount'), 'Can move a mount point into a subdirectory');
1610
+    }
1611
+
1612
+    public function testMoveMountPointOverwrite(): void {
1613
+        self::loginAsUser($this->user);
1614
+
1615
+        [$mount1, $mount2] = $this->createTestMovableMountPoints([
1616
+            $this->user . '/files/mount1',
1617
+            $this->user . '/files/mount2',
1618
+        ]);
1619
+
1620
+        $mount1->expects($this->never())
1621
+            ->method('moveMount');
1622
+
1623
+        $mount2->expects($this->never())
1624
+            ->method('moveMount');
1625
+
1626
+        $view = new View('/' . $this->user . '/files/');
1627
+
1628
+        $this->expectException(ForbiddenException::class);
1629
+        $view->rename('mount1', 'mount2');
1630
+    }
1631
+
1632
+    public function testMoveMountPointIntoMount(): void {
1633
+        self::loginAsUser($this->user);
1634
+
1635
+        [$mount1, $mount2] = $this->createTestMovableMountPoints([
1636
+            $this->user . '/files/mount1',
1637
+            $this->user . '/files/mount2',
1638
+        ]);
1639
+
1640
+        $mount1->expects($this->never())
1641
+            ->method('moveMount');
1642
+
1643
+        $mount2->expects($this->never())
1644
+            ->method('moveMount');
1645
+
1646
+        $view = new View('/' . $this->user . '/files/');
1647
+
1648
+        $this->expectException(ForbiddenException::class);
1649
+        $view->rename('mount1', 'mount2/sub');
1650
+    }
1651
+
1652
+    /**
1653
+     * Test that moving a mount point into a shared folder is forbidden
1654
+     */
1655
+    public function testMoveMountPointIntoSharedFolder(): void {
1656
+        self::loginAsUser($this->user);
1657
+
1658
+        [$mount1, $mount2] = $this->createTestMovableMountPoints([
1659
+            $this->user . '/files/mount1',
1660
+            $this->user . '/files/mount2',
1661
+        ]);
1662
+
1663
+        $mount1->expects($this->never())
1664
+            ->method('moveMount');
1665
+
1666
+        $mount2->expects($this->once())
1667
+            ->method('moveMount')
1668
+            ->willReturn(true);
1669
+
1670
+        $view = new View('/' . $this->user . '/files/');
1671
+        $view->mkdir('shareddir');
1672
+        $view->mkdir('shareddir/sub');
1673
+        $view->mkdir('shareddir/sub2');
1674
+        // Create a similar named but non-shared folder
1675
+        $view->mkdir('shareddir notshared');
1676
+
1677
+        $fileId = $view->getFileInfo('shareddir')->getId();
1678
+        $userObject = Server::get(IUserManager::class)->createUser('test2', 'IHateNonMockableStaticClasses');
1679
+
1680
+        $userFolder = \OC::$server->getUserFolder($this->user);
1681
+        $shareDir = $userFolder->get('shareddir');
1682
+        $shareManager = Server::get(IShareManager::class);
1683
+        $share = $shareManager->newShare();
1684
+        $share->setSharedWith('test2')
1685
+            ->setSharedBy($this->user)
1686
+            ->setShareType(IShare::TYPE_USER)
1687
+            ->setPermissions(Constants::PERMISSION_READ)
1688
+            ->setNode($shareDir);
1689
+        $shareManager->createShare($share);
1690
+
1691
+        try {
1692
+            $view->rename('mount1', 'shareddir');
1693
+            $this->fail('Cannot overwrite shared folder');
1694
+        } catch (ForbiddenException $e) {
1695
+
1696
+        }
1697
+        try {
1698
+            $view->rename('mount1', 'shareddir/sub');
1699
+            $this->fail('Cannot move mount point into shared folder');
1700
+        } catch (ForbiddenException $e) {
1701
+
1702
+        }
1703
+        try {
1704
+            $view->rename('mount1', 'shareddir/sub/sub2');
1705
+            $this->fail('Cannot move mount point into shared subfolder');
1706
+        } catch (ForbiddenException $e) {
1707
+
1708
+        }
1709
+        $this->assertTrue($view->rename('mount2', 'shareddir notshared/sub'), 'Can move mount point into a similarly named but non-shared folder');
1710
+
1711
+        $shareManager->deleteShare($share);
1712
+        $userObject->delete();
1713
+    }
1714
+
1715
+    public static function basicOperationProviderForLocks(): array {
1716
+        return [
1717
+            // --- write hook ----
1718
+            [
1719
+                'touch',
1720
+                ['touch-create.txt'],
1721
+                'touch-create.txt',
1722
+                'create',
1723
+                ILockingProvider::LOCK_SHARED,
1724
+                ILockingProvider::LOCK_EXCLUSIVE,
1725
+                ILockingProvider::LOCK_SHARED,
1726
+            ],
1727
+            [
1728
+                'fopen',
1729
+                ['test-write.txt', 'w'],
1730
+                'test-write.txt',
1731
+                'write',
1732
+                ILockingProvider::LOCK_SHARED,
1733
+                ILockingProvider::LOCK_EXCLUSIVE,
1734
+                null,
1735
+                // exclusive lock stays until fclose
1736
+                ILockingProvider::LOCK_EXCLUSIVE,
1737
+            ],
1738
+            [
1739
+                'mkdir',
1740
+                ['newdir'],
1741
+                'newdir',
1742
+                'write',
1743
+                ILockingProvider::LOCK_SHARED,
1744
+                ILockingProvider::LOCK_EXCLUSIVE,
1745
+                ILockingProvider::LOCK_SHARED,
1746
+            ],
1747
+            [
1748
+                'file_put_contents',
1749
+                ['file_put_contents.txt', 'blah'],
1750
+                'file_put_contents.txt',
1751
+                'write',
1752
+                ILockingProvider::LOCK_SHARED,
1753
+                ILockingProvider::LOCK_EXCLUSIVE,
1754
+                ILockingProvider::LOCK_SHARED,
1755
+                null,
1756
+                0,
1757
+            ],
1758
+
1759
+            // ---- delete hook ----
1760
+            [
1761
+                'rmdir',
1762
+                ['dir'],
1763
+                'dir',
1764
+                'delete',
1765
+                ILockingProvider::LOCK_SHARED,
1766
+                ILockingProvider::LOCK_EXCLUSIVE,
1767
+                ILockingProvider::LOCK_SHARED,
1768
+            ],
1769
+            [
1770
+                'unlink',
1771
+                ['test.txt'],
1772
+                'test.txt',
1773
+                'delete',
1774
+                ILockingProvider::LOCK_SHARED,
1775
+                ILockingProvider::LOCK_EXCLUSIVE,
1776
+                ILockingProvider::LOCK_SHARED,
1777
+            ],
1778
+
1779
+            // ---- read hook (no post hooks) ----
1780
+            [
1781
+                'file_get_contents',
1782
+                ['test.txt'],
1783
+                'test.txt',
1784
+                'read',
1785
+                ILockingProvider::LOCK_SHARED,
1786
+                ILockingProvider::LOCK_SHARED,
1787
+                null,
1788
+                null,
1789
+                false,
1790
+            ],
1791
+            [
1792
+                'fopen',
1793
+                ['test.txt', 'r'],
1794
+                'test.txt',
1795
+                'read',
1796
+                ILockingProvider::LOCK_SHARED,
1797
+                ILockingProvider::LOCK_SHARED,
1798
+                null,
1799
+            ],
1800
+            [
1801
+                'opendir',
1802
+                ['dir'],
1803
+                'dir',
1804
+                'read',
1805
+                ILockingProvider::LOCK_SHARED,
1806
+                ILockingProvider::LOCK_SHARED,
1807
+                null,
1808
+            ],
1809
+
1810
+            // ---- no lock, touch hook ---
1811
+            ['touch', ['test.txt'], 'test.txt', 'touch', null, null, null],
1812
+
1813
+            // ---- no hooks, no locks ---
1814
+            ['is_dir', ['dir'], 'dir', ''],
1815
+            ['is_file', ['dir'], 'dir', ''],
1816
+            [
1817
+                'stat',
1818
+                ['dir'],
1819
+                'dir',
1820
+                '',
1821
+                ILockingProvider::LOCK_SHARED,
1822
+                ILockingProvider::LOCK_SHARED,
1823
+                ILockingProvider::LOCK_SHARED,
1824
+                null,
1825
+                false,
1826
+            ],
1827
+            [
1828
+                'filetype',
1829
+                ['dir'],
1830
+                'dir',
1831
+                '',
1832
+                ILockingProvider::LOCK_SHARED,
1833
+                ILockingProvider::LOCK_SHARED,
1834
+                ILockingProvider::LOCK_SHARED,
1835
+                null,
1836
+                false,
1837
+            ],
1838
+            [
1839
+                'filesize',
1840
+                ['dir'],
1841
+                'dir',
1842
+                '',
1843
+                ILockingProvider::LOCK_SHARED,
1844
+                ILockingProvider::LOCK_SHARED,
1845
+                ILockingProvider::LOCK_SHARED,
1846
+                null,
1847
+                /* Return an int */
1848
+                100
1849
+            ],
1850
+            ['isCreatable', ['dir'], 'dir', ''],
1851
+            ['isReadable', ['dir'], 'dir', ''],
1852
+            ['isUpdatable', ['dir'], 'dir', ''],
1853
+            ['isDeletable', ['dir'], 'dir', ''],
1854
+            ['isSharable', ['dir'], 'dir', ''],
1855
+            ['file_exists', ['dir'], 'dir', ''],
1856
+            [
1857
+                'filemtime',
1858
+                ['dir'],
1859
+                'dir',
1860
+                '',
1861
+                ILockingProvider::LOCK_SHARED,
1862
+                ILockingProvider::LOCK_SHARED,
1863
+                ILockingProvider::LOCK_SHARED,
1864
+                null,
1865
+                false,
1866
+            ],
1867
+        ];
1868
+    }
1869
+
1870
+    /**
1871
+     * Test whether locks are set before and after the operation
1872
+     *
1873
+     *
1874
+     * @param string $operation operation name on the view
1875
+     * @param array $operationArgs arguments for the operation
1876
+     * @param string $lockedPath path of the locked item to check
1877
+     * @param string $hookType hook type
1878
+     * @param ?int $expectedLockBefore expected lock during pre hooks
1879
+     * @param ?int $expectedLockDuring expected lock during operation
1880
+     * @param ?int $expectedLockAfter expected lock during post hooks
1881
+     * @param ?int $expectedStrayLock expected lock after returning, should
1882
+     *                                be null (unlock) for most operations
1883
+     */
1884
+    #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
1885
+    public function testLockBasicOperation(
1886
+        string $operation,
1887
+        array $operationArgs,
1888
+        string $lockedPath,
1889
+        string $hookType,
1890
+        ?int $expectedLockBefore = ILockingProvider::LOCK_SHARED,
1891
+        ?int $expectedLockDuring = ILockingProvider::LOCK_SHARED,
1892
+        ?int $expectedLockAfter = ILockingProvider::LOCK_SHARED,
1893
+        ?int $expectedStrayLock = null,
1894
+        mixed $returnValue = true,
1895
+    ): void {
1896
+        $view = new View('/' . $this->user . '/files/');
1897
+
1898
+        /** @var Temporary&MockObject $storage */
1899
+        $storage = $this->getMockBuilder(Temporary::class)
1900
+            ->onlyMethods([$operation])
1901
+            ->getMock();
1902
+
1903
+        /* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
1904
+        Server::get(ITrashManager::class)->pauseTrash();
1905
+        /* Same thing with encryption wrapper */
1906
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
1907
+
1908
+        Filesystem::mount($storage, [], $this->user . '/');
1909
+
1910
+        // work directly on disk because mkdir might be mocked
1911
+        $realPath = $storage->getSourcePath('');
1912
+        mkdir($realPath . '/files');
1913
+        mkdir($realPath . '/files/dir');
1914
+        file_put_contents($realPath . '/files/test.txt', 'blah');
1915
+        $storage->getScanner()->scan('files');
1916
+
1917
+        $storage->expects($this->once())
1918
+            ->method($operation)
1919
+            ->willReturnCallback(
1920
+                function () use ($view, $lockedPath, &$lockTypeDuring, $returnValue) {
1921
+                    $lockTypeDuring = $this->getFileLockType($view, $lockedPath);
1922
+
1923
+                    return $returnValue;
1924
+                }
1925
+            );
1926
+
1927
+        $this->assertNull($this->getFileLockType($view, $lockedPath), 'File not locked before operation');
1928
+
1929
+        $this->connectMockHooks($hookType, $view, $lockedPath, $lockTypePre, $lockTypePost);
1930
+
1931
+        // do operation
1932
+        call_user_func_array([$view, $operation], $operationArgs);
1933
+
1934
+        if ($hookType !== '') {
1935
+            $this->assertEquals($expectedLockBefore, $lockTypePre, 'File locked properly during pre-hook');
1936
+            $this->assertEquals($expectedLockAfter, $lockTypePost, 'File locked properly during post-hook');
1937
+            $this->assertEquals($expectedLockDuring, $lockTypeDuring, 'File locked properly during operation');
1938
+        } else {
1939
+            $this->assertNull($lockTypeDuring, 'File not locked during operation');
1940
+        }
1941
+
1942
+        $this->assertEquals($expectedStrayLock, $this->getFileLockType($view, $lockedPath));
1943
+
1944
+        /* Resume trash to avoid side effects */
1945
+        Server::get(ITrashManager::class)->resumeTrash();
1946
+    }
1947
+
1948
+    /**
1949
+     * Test locks for file_put_content with stream.
1950
+     * This code path uses $storage->fopen instead
1951
+     */
1952
+    public function testLockFilePutContentWithStream(): void {
1953
+        $view = new View('/' . $this->user . '/files/');
1954
+
1955
+        $path = 'test_file_put_contents.txt';
1956
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1957
+        $storage = $this->getMockBuilder(Temporary::class)
1958
+            ->onlyMethods(['fopen'])
1959
+            ->getMock();
1960
+
1961
+        Filesystem::mount($storage, [], $this->user . '/');
1962
+        $storage->mkdir('files');
1963
+
1964
+        $storage->expects($this->once())
1965
+            ->method('fopen')
1966
+            ->willReturnCallback(
1967
+                function () use ($view, $path, &$lockTypeDuring) {
1968
+                    $lockTypeDuring = $this->getFileLockType($view, $path);
1969
+
1970
+                    return fopen('php://temp', 'r+');
1971
+                }
1972
+            );
1973
+
1974
+        $this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost);
1975
+
1976
+        $this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation');
1977
+
1978
+        // do operation
1979
+        $view->file_put_contents($path, fopen('php://temp', 'r+'));
1980
+
1981
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook');
1982
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePost, 'File locked properly during post-hook');
1983
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation');
1984
+
1985
+        $this->assertNull($this->getFileLockType($view, $path));
1986
+    }
1987
+
1988
+    /**
1989
+     * Test locks for fopen with fclose at the end
1990
+     */
1991
+    public function testLockFopen(): void {
1992
+        $view = new View('/' . $this->user . '/files/');
1993
+
1994
+        $path = 'test_file_put_contents.txt';
1995
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1996
+        $storage = $this->getMockBuilder(Temporary::class)
1997
+            ->onlyMethods(['fopen'])
1998
+            ->getMock();
1999
+
2000
+        Filesystem::mount($storage, [], $this->user . '/');
2001
+        $storage->mkdir('files');
2002
+
2003
+        $storage->expects($this->once())
2004
+            ->method('fopen')
2005
+            ->willReturnCallback(
2006
+                function () use ($view, $path, &$lockTypeDuring) {
2007
+                    $lockTypeDuring = $this->getFileLockType($view, $path);
2008
+
2009
+                    return fopen('php://temp', 'r+');
2010
+                }
2011
+            );
2012
+
2013
+        $this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost);
2014
+
2015
+        $this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation');
2016
+
2017
+        // do operation
2018
+        $res = $view->fopen($path, 'w');
2019
+
2020
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook');
2021
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation');
2022
+        $this->assertNull($lockTypePost, 'No post hook, no lock check possible');
2023
+
2024
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File still locked after fopen');
2025
+
2026
+        fclose($res);
2027
+
2028
+        $this->assertNull($this->getFileLockType($view, $path), 'File unlocked after fclose');
2029
+    }
2030
+
2031
+    /**
2032
+     * Test locks for fopen with fclose at the end
2033
+     *
2034
+     *
2035
+     * @param string $operation operation name on the view
2036
+     * @param array $operationArgs arguments for the operation
2037
+     * @param string $path path of the locked item to check
2038
+     */
2039
+    #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
2040
+    public function testLockBasicOperationUnlocksAfterException(
2041
+        $operation,
2042
+        $operationArgs,
2043
+        $path,
2044
+    ): void {
2045
+        if ($operation === 'touch') {
2046
+            $this->markTestSkipped('touch handles storage exceptions internally');
2047
+        }
2048
+        $view = new View('/' . $this->user . '/files/');
2049
+
2050
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2051
+        $storage = $this->getMockBuilder(Temporary::class)
2052
+            ->onlyMethods([$operation])
2053
+            ->getMock();
2054
+
2055
+        /* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
2056
+        Server::get(ITrashManager::class)->pauseTrash();
2057
+        /* Same thing with encryption wrapper */
2058
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2059
+
2060
+        Filesystem::mount($storage, [], $this->user . '/');
2061
+
2062
+        // work directly on disk because mkdir might be mocked
2063
+        $realPath = $storage->getSourcePath('');
2064
+        mkdir($realPath . '/files');
2065
+        mkdir($realPath . '/files/dir');
2066
+        file_put_contents($realPath . '/files/test.txt', 'blah');
2067
+        $storage->getScanner()->scan('files');
2068
+
2069
+        $storage->expects($this->once())
2070
+            ->method($operation)
2071
+            ->willReturnCallback(
2072
+                function (): void {
2073
+                    throw new \Exception('Simulated exception');
2074
+                }
2075
+            );
2076
+
2077
+        $thrown = false;
2078
+        try {
2079
+            call_user_func_array([$view, $operation], $operationArgs);
2080
+        } catch (\Exception $e) {
2081
+            $thrown = true;
2082
+            $this->assertEquals('Simulated exception', $e->getMessage());
2083
+        }
2084
+        $this->assertTrue($thrown, 'Exception was rethrown');
2085
+        $this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception');
2086
+
2087
+        /* Resume trash to avoid side effects */
2088
+        Server::get(ITrashManager::class)->resumeTrash();
2089
+    }
2090
+
2091
+    public function testLockBasicOperationUnlocksAfterLockException(): void {
2092
+        $view = new View('/' . $this->user . '/files/');
2093
+
2094
+        $storage = new Temporary([]);
2095
+
2096
+        Filesystem::mount($storage, [], $this->user . '/');
2097
+
2098
+        $storage->mkdir('files');
2099
+        $storage->mkdir('files/dir');
2100
+        $storage->file_put_contents('files/test.txt', 'blah');
2101
+        $storage->getScanner()->scan('files');
2102
+
2103
+        // get a shared lock
2104
+        $handle = $view->fopen('test.txt', 'r');
2105
+
2106
+        $thrown = false;
2107
+        try {
2108
+            // try (and fail) to get a write lock
2109
+            $view->unlink('test.txt');
2110
+        } catch (\Exception $e) {
2111
+            $thrown = true;
2112
+            $this->assertInstanceOf(LockedException::class, $e);
2113
+        }
2114
+        $this->assertTrue($thrown, 'Exception was rethrown');
2115
+
2116
+        // clean shared lock
2117
+        fclose($handle);
2118
+
2119
+        $this->assertNull($this->getFileLockType($view, 'test.txt'), 'File got unlocked');
2120
+    }
2121
+
2122
+    /**
2123
+     * Test locks for fopen with fclose at the end
2124
+     *
2125
+     *
2126
+     * @param string $operation operation name on the view
2127
+     * @param array $operationArgs arguments for the operation
2128
+     * @param string $path path of the locked item to check
2129
+     * @param string $hookType hook type
2130
+     */
2131
+    #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
2132
+    public function testLockBasicOperationUnlocksAfterCancelledHook(
2133
+        $operation,
2134
+        $operationArgs,
2135
+        $path,
2136
+        $hookType,
2137
+    ): void {
2138
+        $view = new View('/' . $this->user . '/files/');
2139
+
2140
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2141
+        $storage = $this->getMockBuilder(Temporary::class)
2142
+            ->onlyMethods([$operation])
2143
+            ->getMock();
2144
+
2145
+        Filesystem::mount($storage, [], $this->user . '/');
2146
+        $storage->mkdir('files');
2147
+
2148
+        Util::connectHook(
2149
+            Filesystem::CLASSNAME,
2150
+            $hookType,
2151
+            HookHelper::class,
2152
+            'cancellingCallback'
2153
+        );
2154
+
2155
+        call_user_func_array([$view, $operation], $operationArgs);
2156
+
2157
+        $this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception');
2158
+    }
2159
+
2160
+    public static function lockFileRenameOrCopyDataProvider(): array {
2161
+        return [
2162
+            ['rename', ILockingProvider::LOCK_EXCLUSIVE],
2163
+            ['copy', ILockingProvider::LOCK_SHARED],
2164
+        ];
2165
+    }
2166
+
2167
+    /**
2168
+     * Test locks for rename or copy operation
2169
+     *
2170
+     *
2171
+     * @param string $operation operation to be done on the view
2172
+     * @param int $expectedLockTypeSourceDuring expected lock type on source file during
2173
+     *                                          the operation
2174
+     */
2175
+    #[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyDataProvider')]
2176
+    public function testLockFileRename($operation, $expectedLockTypeSourceDuring): void {
2177
+        $view = new View('/' . $this->user . '/files/');
2178
+
2179
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2180
+        $storage = $this->getMockBuilder(Temporary::class)
2181
+            ->onlyMethods([$operation, 'getMetaData', 'filemtime'])
2182
+            ->getMock();
2183
+
2184
+        $storage->expects($this->any())
2185
+            ->method('getMetaData')
2186
+            ->willReturn([
2187
+                'mtime' => 1885434487,
2188
+                'etag' => '',
2189
+                'mimetype' => 'text/plain',
2190
+                'permissions' => Constants::PERMISSION_ALL,
2191
+                'size' => 3
2192
+            ]);
2193
+        $storage->expects($this->any())
2194
+            ->method('filemtime')
2195
+            ->willReturn(123456789);
2196
+
2197
+        $sourcePath = 'original.txt';
2198
+        $targetPath = 'target.txt';
2199
+
2200
+        /* Disable encryption wrapper to avoid it intercepting mocked call */
2201
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2202
+
2203
+        Filesystem::mount($storage, [], $this->user . '/');
2204
+        $storage->mkdir('files');
2205
+        $view->file_put_contents($sourcePath, 'meh');
2206
+
2207
+        $storage->expects($this->once())
2208
+            ->method($operation)
2209
+            ->willReturnCallback(
2210
+                function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2211
+                    $lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2212
+                    $lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2213
+
2214
+                    return true;
2215
+                }
2216
+            );
2217
+
2218
+        $this->connectMockHooks($operation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2219
+        $this->connectMockHooks($operation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2220
+
2221
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2222
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2223
+
2224
+        $view->$operation($sourcePath, $targetPath);
2225
+
2226
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook');
2227
+        $this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation');
2228
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook');
2229
+
2230
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook');
2231
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation');
2232
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook');
2233
+
2234
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2235
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2236
+    }
2237
+
2238
+    /**
2239
+     * simulate a failed copy operation.
2240
+     * We expect that we catch the exception, free the lock and re-throw it.
2241
+     *
2242
+     */
2243
+    public function testLockFileCopyException(): void {
2244
+        $this->expectException(\Exception::class);
2245
+
2246
+        $view = new View('/' . $this->user . '/files/');
2247
+
2248
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2249
+        $storage = $this->getMockBuilder(Temporary::class)
2250
+            ->onlyMethods(['copy'])
2251
+            ->getMock();
2252
+
2253
+        $sourcePath = 'original.txt';
2254
+        $targetPath = 'target.txt';
2255
+
2256
+        /* Disable encryption wrapper to avoid it intercepting mocked call */
2257
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2258
+
2259
+        Filesystem::mount($storage, [], $this->user . '/');
2260
+        $storage->mkdir('files');
2261
+        $view->file_put_contents($sourcePath, 'meh');
2262
+
2263
+        $storage->expects($this->once())
2264
+            ->method('copy')
2265
+            ->willReturnCallback(
2266
+                function (): void {
2267
+                    throw new \Exception();
2268
+                }
2269
+            );
2270
+
2271
+        $this->connectMockHooks('copy', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2272
+        $this->connectMockHooks('copy', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2273
+
2274
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2275
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2276
+
2277
+        try {
2278
+            $view->copy($sourcePath, $targetPath);
2279
+        } catch (\Exception $e) {
2280
+            $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2281
+            $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2282
+            throw $e;
2283
+        }
2284
+    }
2285
+
2286
+    /**
2287
+     * Test rename operation: unlock first path when second path was locked
2288
+     */
2289
+    public function testLockFileRenameUnlockOnException(): void {
2290
+        self::loginAsUser('test');
2291
+
2292
+        $view = new View('/' . $this->user . '/files/');
2293
+
2294
+        $sourcePath = 'original.txt';
2295
+        $targetPath = 'target.txt';
2296
+        $view->file_put_contents($sourcePath, 'meh');
2297
+
2298
+        // simulate that the target path is already locked
2299
+        $view->lockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
2300
+
2301
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2302
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file is locked before operation');
2303
+
2304
+        $thrown = false;
2305
+        try {
2306
+            $view->rename($sourcePath, $targetPath);
2307
+        } catch (LockedException $e) {
2308
+            $thrown = true;
2309
+        }
2310
+
2311
+        $this->assertTrue($thrown, 'LockedException thrown');
2312
+
2313
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2314
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file still locked after operation');
2315
+
2316
+        $view->unlockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
2317
+    }
2318
+
2319
+    /**
2320
+     * Test rename operation: unlock first path when second path was locked
2321
+     */
2322
+    public function testGetOwner(): void {
2323
+        self::loginAsUser('test');
2324
+
2325
+        $view = new View('/test/files/');
2326
+
2327
+        $path = 'foo.txt';
2328
+        $view->file_put_contents($path, 'meh');
2329
+
2330
+        $this->assertEquals('test', $view->getFileInfo($path)->getOwner()->getUID());
2331
+
2332
+        $folderInfo = $view->getDirectoryContent('');
2333
+        $folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2334
+            return $info->getName() === 'foo.txt';
2335
+        }));
2336
+
2337
+        $this->assertEquals('test', $folderInfo[0]->getOwner()->getUID());
2338
+
2339
+        $subStorage = new Temporary();
2340
+        Filesystem::mount($subStorage, [], '/test/files/asd');
2341
+
2342
+        $folderInfo = $view->getDirectoryContent('');
2343
+        $folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2344
+            return $info->getName() === 'asd';
2345
+        }));
2346
+
2347
+        $this->assertEquals('test', $folderInfo[0]->getOwner()->getUID());
2348
+    }
2349
+
2350
+    public static function lockFileRenameOrCopyCrossStorageDataProvider(): array {
2351
+        return [
2352
+            ['rename', 'moveFromStorage', ILockingProvider::LOCK_EXCLUSIVE],
2353
+            ['copy', 'copyFromStorage', ILockingProvider::LOCK_SHARED],
2354
+        ];
2355
+    }
2356
+
2357
+    /**
2358
+     * Test locks for rename or copy operation cross-storage
2359
+     *
2360
+     *
2361
+     * @param string $viewOperation operation to be done on the view
2362
+     * @param string $storageOperation operation to be mocked on the storage
2363
+     * @param int $expectedLockTypeSourceDuring expected lock type on source file during
2364
+     *                                          the operation
2365
+     */
2366
+    #[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyCrossStorageDataProvider')]
2367
+    public function testLockFileRenameCrossStorage($viewOperation, $storageOperation, $expectedLockTypeSourceDuring): void {
2368
+        $view = new View('/' . $this->user . '/files/');
2369
+
2370
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2371
+        $storage = $this->getMockBuilder(Temporary::class)
2372
+            ->onlyMethods([$storageOperation])
2373
+            ->getMock();
2374
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage2 */
2375
+        $storage2 = $this->getMockBuilder(Temporary::class)
2376
+            ->onlyMethods([$storageOperation, 'getMetaData', 'filemtime'])
2377
+            ->getMock();
2378
+
2379
+        $storage2->expects($this->any())
2380
+            ->method('getMetaData')
2381
+            ->willReturn([
2382
+                'mtime' => 1885434487,
2383
+                'etag' => '',
2384
+                'mimetype' => 'text/plain',
2385
+                'permissions' => Constants::PERMISSION_ALL,
2386
+                'size' => 3
2387
+            ]);
2388
+        $storage2->expects($this->any())
2389
+            ->method('filemtime')
2390
+            ->willReturn(123456789);
2391
+
2392
+        $sourcePath = 'original.txt';
2393
+        $targetPath = 'substorage/target.txt';
2394
+
2395
+        /* Disable encryption wrapper to avoid it intercepting mocked call */
2396
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2397
+
2398
+        Filesystem::mount($storage, [], $this->user . '/');
2399
+        Filesystem::mount($storage2, [], $this->user . '/files/substorage');
2400
+        $storage->mkdir('files');
2401
+        $view->file_put_contents($sourcePath, 'meh');
2402
+        $storage2->getUpdater()->update('');
2403
+
2404
+        $storage->expects($this->never())
2405
+            ->method($storageOperation);
2406
+        $storage2->expects($this->once())
2407
+            ->method($storageOperation)
2408
+            ->willReturnCallback(
2409
+                function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2410
+                    $lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2411
+                    $lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2412
+
2413
+                    return true;
2414
+                }
2415
+            );
2416
+
2417
+        $this->connectMockHooks($viewOperation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2418
+        $this->connectMockHooks($viewOperation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2419
+
2420
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2421
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2422
+
2423
+        $view->$viewOperation($sourcePath, $targetPath);
2424
+
2425
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook');
2426
+        $this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation');
2427
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook');
2428
+
2429
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook');
2430
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation');
2431
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook');
2432
+
2433
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2434
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2435
+    }
2436
+
2437
+    /**
2438
+     * Test locks when moving a mount point
2439
+     */
2440
+    public function testLockMoveMountPoint(): void {
2441
+        self::loginAsUser('test');
2442
+
2443
+        [$mount] = $this->createTestMovableMountPoints([
2444
+            $this->user . '/files/substorage',
2445
+        ]);
2446
+
2447
+        $view = new View('/' . $this->user . '/files/');
2448
+        $view->mkdir('subdir');
2449
+
2450
+        $sourcePath = 'substorage';
2451
+        $targetPath = 'subdir/substorage_moved';
2452
+
2453
+        $mount->expects($this->once())
2454
+            ->method('moveMount')
2455
+            ->willReturnCallback(
2456
+                function ($target) use ($mount, $view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring, &$lockTypeSharedRootDuring) {
2457
+                    $lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath, true);
2458
+                    $lockTypeTargetDuring = $this->getFileLockType($view, $targetPath, true);
2459
+
2460
+                    $lockTypeSharedRootDuring = $this->getFileLockType($view, $sourcePath, false);
2461
+
2462
+                    $mount->setMountPoint($target);
2463
+
2464
+                    return true;
2465
+                }
2466
+            );
2467
+
2468
+        $this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost, true);
2469
+        $this->connectMockHooks('rename', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost, true);
2470
+        // in pre-hook, mount point is still on $sourcePath
2471
+        $this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSharedRootPre, $dummy, false);
2472
+        // in post-hook, mount point is now on $targetPath
2473
+        $this->connectMockHooks('rename', $view, $targetPath, $dummy, $lockTypeSharedRootPost, false);
2474
+
2475
+        $this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked before operation');
2476
+        $this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked before operation');
2477
+        $this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked before operation');
2478
+
2479
+        $view->rename($sourcePath, $targetPath);
2480
+
2481
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source path locked properly during pre-hook');
2482
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeSourceDuring, 'Source path locked properly during operation');
2483
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source path locked properly during post-hook');
2484
+
2485
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target path locked properly during pre-hook');
2486
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target path locked properly during operation');
2487
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target path locked properly during post-hook');
2488
+
2489
+        $this->assertNull($lockTypeSharedRootPre, 'Shared storage root not locked during pre-hook');
2490
+        $this->assertNull($lockTypeSharedRootDuring, 'Shared storage root not locked during move');
2491
+        $this->assertNull($lockTypeSharedRootPost, 'Shared storage root not locked during post-hook');
2492
+
2493
+        $this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked after operation');
2494
+        $this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked after operation');
2495
+        $this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked after operation');
2496
+    }
2497
+
2498
+    /**
2499
+     * Connect hook callbacks for hook type
2500
+     *
2501
+     * @param string $hookType hook type or null for none
2502
+     * @param View $view view to check the lock on
2503
+     * @param string $path path for which to check the lock
2504
+     * @param int $lockTypePre variable to receive lock type that was active in the pre-hook
2505
+     * @param int $lockTypePost variable to receive lock type that was active in the post-hook
2506
+     * @param bool $onMountPoint true to check the mount point instead of the
2507
+     *                           mounted storage
2508
+     */
2509
+    private function connectMockHooks($hookType, $view, $path, &$lockTypePre, &$lockTypePost, $onMountPoint = false) {
2510
+        if ($hookType === null) {
2511
+            return;
2512
+        }
2513
+
2514
+        $eventHandler = $this->getMockBuilder(TestEventHandler::class)
2515
+            ->onlyMethods(['preCallback', 'postCallback'])
2516
+            ->getMock();
2517
+
2518
+        $eventHandler->expects($this->any())
2519
+            ->method('preCallback')
2520
+            ->willReturnCallback(
2521
+                function () use ($view, $path, $onMountPoint, &$lockTypePre): void {
2522
+                    $lockTypePre = $this->getFileLockType($view, $path, $onMountPoint);
2523
+                }
2524
+            );
2525
+        $eventHandler->expects($this->any())
2526
+            ->method('postCallback')
2527
+            ->willReturnCallback(
2528
+                function () use ($view, $path, $onMountPoint, &$lockTypePost): void {
2529
+                    $lockTypePost = $this->getFileLockType($view, $path, $onMountPoint);
2530
+                }
2531
+            );
2532
+
2533
+        if ($hookType !== '') {
2534
+            Util::connectHook(
2535
+                Filesystem::CLASSNAME,
2536
+                $hookType,
2537
+                $eventHandler,
2538
+                'preCallback'
2539
+            );
2540
+            Util::connectHook(
2541
+                Filesystem::CLASSNAME,
2542
+                'post_' . $hookType,
2543
+                $eventHandler,
2544
+                'postCallback'
2545
+            );
2546
+        }
2547
+    }
2548
+
2549
+    /**
2550
+     * Returns the file lock type
2551
+     *
2552
+     * @param View $view view
2553
+     * @param string $path path
2554
+     * @param bool $onMountPoint true to check the mount point instead of the
2555
+     *                           mounted storage
2556
+     *
2557
+     * @return int lock type or null if file was not locked
2558
+     */
2559
+    private function getFileLockType(View $view, $path, $onMountPoint = false) {
2560
+        if ($this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE, $onMountPoint)) {
2561
+            return ILockingProvider::LOCK_EXCLUSIVE;
2562
+        } elseif ($this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED, $onMountPoint)) {
2563
+            return ILockingProvider::LOCK_SHARED;
2564
+        }
2565
+        return null;
2566
+    }
2567
+
2568
+
2569
+    public function testRemoveMoveableMountPoint(): void {
2570
+        $mountPoint = '/' . $this->user . '/files/mount/';
2571
+
2572
+        // Mock the mount point
2573
+        /** @var TestMoveableMountPoint|\PHPUnit\Framework\MockObject\MockObject $mount */
2574
+        $mount = $this->createMock(TestMoveableMountPoint::class);
2575
+        $mount->expects($this->once())
2576
+            ->method('getMountPoint')
2577
+            ->willReturn($mountPoint);
2578
+        $mount->expects($this->once())
2579
+            ->method('removeMount')
2580
+            ->willReturn('foo');
2581
+        $mount->expects($this->any())
2582
+            ->method('getInternalPath')
2583
+            ->willReturn('');
2584
+
2585
+        // Register mount
2586
+        Filesystem::getMountManager()->addMount($mount);
2587
+
2588
+        // Listen for events
2589
+        $eventHandler = $this->getMockBuilder(TestEventHandler::class)
2590
+            ->onlyMethods(['umount', 'post_umount'])
2591
+            ->getMock();
2592
+        $eventHandler->expects($this->once())
2593
+            ->method('umount')
2594
+            ->with([Filesystem::signal_param_path => '/mount']);
2595
+        $eventHandler->expects($this->once())
2596
+            ->method('post_umount')
2597
+            ->with([Filesystem::signal_param_path => '/mount']);
2598
+        Util::connectHook(
2599
+            Filesystem::CLASSNAME,
2600
+            'umount',
2601
+            $eventHandler,
2602
+            'umount'
2603
+        );
2604
+        Util::connectHook(
2605
+            Filesystem::CLASSNAME,
2606
+            'post_umount',
2607
+            $eventHandler,
2608
+            'post_umount'
2609
+        );
2610
+
2611
+        //Delete the mountpoint
2612
+        $view = new View('/' . $this->user . '/files');
2613
+        $this->assertEquals('foo', $view->rmdir('mount'));
2614
+    }
2615
+
2616
+    public static function mimeFilterProvider(): array {
2617
+        return [
2618
+            [null, ['test1.txt', 'test2.txt', 'test3.md', 'test4.png']],
2619
+            ['text/plain', ['test1.txt', 'test2.txt']],
2620
+            ['text/markdown', ['test3.md']],
2621
+            ['text', ['test1.txt', 'test2.txt', 'test3.md']],
2622
+        ];
2623
+    }
2624
+
2625
+    /**
2626
+     * @param string $filter
2627
+     * @param string[] $expected
2628
+     */
2629
+    #[\PHPUnit\Framework\Attributes\DataProvider('mimeFilterProvider')]
2630
+    public function testGetDirectoryContentMimeFilter($filter, $expected): void {
2631
+        $storage1 = new Temporary();
2632
+        $root = self::getUniqueID('/');
2633
+        Filesystem::mount($storage1, [], $root . '/');
2634
+        $view = new View($root);
2635
+
2636
+        $view->file_put_contents('test1.txt', 'asd');
2637
+        $view->file_put_contents('test2.txt', 'asd');
2638
+        $view->file_put_contents('test3.md', 'asd');
2639
+        $view->file_put_contents('test4.png', '');
2640
+
2641
+        $content = $view->getDirectoryContent('', $filter);
2642
+
2643
+        $files = array_map(function (FileInfo $info) {
2644
+            return $info->getName();
2645
+        }, $content);
2646
+        sort($files);
2647
+
2648
+        $this->assertEquals($expected, $files);
2649
+    }
2650
+
2651
+    public function testFilePutContentsClearsChecksum(): void {
2652
+        $storage = new Temporary([]);
2653
+        $scanner = $storage->getScanner();
2654
+        $storage->file_put_contents('foo.txt', 'bar');
2655
+        Filesystem::mount($storage, [], '/test/');
2656
+        $scanner->scan('');
2657
+
2658
+        $view = new View('/test/foo.txt');
2659
+        $view->putFileInfo('.', ['checksum' => '42']);
2660
+
2661
+        $this->assertEquals('bar', $view->file_get_contents(''));
2662
+        $fh = tmpfile();
2663
+        fwrite($fh, 'fooo');
2664
+        rewind($fh);
2665
+        clearstatcache();
2666
+        $view->file_put_contents('', $fh);
2667
+        $this->assertEquals('fooo', $view->file_get_contents(''));
2668
+        $data = $view->getFileInfo('.');
2669
+        $this->assertEquals('', $data->getChecksum());
2670
+    }
2671
+
2672
+    public function testDeleteGhostFile(): void {
2673
+        $storage = new Temporary([]);
2674
+        $scanner = $storage->getScanner();
2675
+        $cache = $storage->getCache();
2676
+        $storage->file_put_contents('foo.txt', 'bar');
2677
+        Filesystem::mount($storage, [], '/test/');
2678
+        $scanner->scan('');
2679
+
2680
+        $storage->unlink('foo.txt');
2681
+
2682
+        $this->assertTrue($cache->inCache('foo.txt'));
2683
+
2684
+        $view = new View('/test');
2685
+        $rootInfo = $view->getFileInfo('');
2686
+        $this->assertEquals(3, $rootInfo->getSize());
2687
+        $view->unlink('foo.txt');
2688
+        $newInfo = $view->getFileInfo('');
2689
+
2690
+        $this->assertFalse($cache->inCache('foo.txt'));
2691
+        $this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag());
2692
+        $this->assertEquals(0, $newInfo->getSize());
2693
+    }
2694
+
2695
+    public function testDeleteGhostFolder(): void {
2696
+        $storage = new Temporary([]);
2697
+        $scanner = $storage->getScanner();
2698
+        $cache = $storage->getCache();
2699
+        $storage->mkdir('foo');
2700
+        $storage->file_put_contents('foo/foo.txt', 'bar');
2701
+        Filesystem::mount($storage, [], '/test/');
2702
+        $scanner->scan('');
2703
+
2704
+        $storage->rmdir('foo');
2705
+
2706
+        $this->assertTrue($cache->inCache('foo'));
2707
+        $this->assertTrue($cache->inCache('foo/foo.txt'));
2708
+
2709
+        $view = new View('/test');
2710
+        $rootInfo = $view->getFileInfo('');
2711
+        $this->assertEquals(3, $rootInfo->getSize());
2712
+        $view->rmdir('foo');
2713
+        $newInfo = $view->getFileInfo('');
2714
+
2715
+        $this->assertFalse($cache->inCache('foo'));
2716
+        $this->assertFalse($cache->inCache('foo/foo.txt'));
2717
+        $this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag());
2718
+        $this->assertEquals(0, $newInfo->getSize());
2719
+    }
2720
+
2721
+    public function testCreateParentDirectories(): void {
2722
+        $view = $this->getMockBuilder(View::class)
2723
+            ->disableOriginalConstructor()
2724
+            ->onlyMethods([
2725
+                'is_file',
2726
+                'file_exists',
2727
+                'mkdir',
2728
+            ])
2729
+            ->getMock();
2730
+
2731
+        $view->expects($this->exactly(3))
2732
+            ->method('is_file')
2733
+            ->willReturnMap([
2734
+                ['/new', false],
2735
+                ['/new/folder', false],
2736
+                ['/new/folder/structure', false],
2737
+            ]);
2738
+        $view->expects($this->exactly(3))
2739
+            ->method('file_exists')
2740
+            ->willReturnMap([
2741
+                ['/new', true],
2742
+                ['/new/folder', false],
2743
+                ['/new/folder/structure', false],
2744
+            ]);
2745
+
2746
+        $calls = ['/new/folder', '/new/folder/structure'];
2747
+        $view->expects($this->exactly(2))
2748
+            ->method('mkdir')
2749
+            ->willReturnCallback(function ($dir) use (&$calls): void {
2750
+                $expected = array_shift($calls);
2751
+                $this->assertEquals($expected, $dir);
2752
+            });
2753
+
2754
+        $this->assertTrue(self::invokePrivate($view, 'createParentDirectories', ['/new/folder/structure']));
2755
+    }
2756
+
2757
+    public function testCreateParentDirectoriesWithExistingFile(): void {
2758
+        $view = $this->getMockBuilder(View::class)
2759
+            ->disableOriginalConstructor()
2760
+            ->onlyMethods([
2761
+                'is_file',
2762
+                'file_exists',
2763
+                'mkdir',
2764
+            ])
2765
+            ->getMock();
2766
+
2767
+        $view
2768
+            ->expects($this->once())
2769
+            ->method('is_file')
2770
+            ->with('/file.txt')
2771
+            ->willReturn(true);
2772
+        $this->assertFalse(self::invokePrivate($view, 'createParentDirectories', ['/file.txt/folder/structure']));
2773
+    }
2774
+
2775
+    public function testCacheExtension(): void {
2776
+        $storage = new Temporary([]);
2777
+        $scanner = $storage->getScanner();
2778
+        $storage->file_put_contents('foo.txt', 'bar');
2779
+        $scanner->scan('');
2780
+
2781
+        Filesystem::mount($storage, [], '/test/');
2782
+        $view = new View('/test');
2783
+
2784
+        $info = $view->getFileInfo('/foo.txt');
2785
+        $this->assertEquals(0, $info->getUploadTime());
2786
+        $this->assertEquals(0, $info->getCreationTime());
2787
+
2788
+        $view->putFileInfo('/foo.txt', ['upload_time' => 25]);
2789
+
2790
+        $info = $view->getFileInfo('/foo.txt');
2791
+        $this->assertEquals(25, $info->getUploadTime());
2792
+        $this->assertEquals(0, $info->getCreationTime());
2793
+    }
2794
+
2795
+    public function testFopenGone(): void {
2796
+        $storage = new Temporary([]);
2797
+        $scanner = $storage->getScanner();
2798
+        $storage->file_put_contents('foo.txt', 'bar');
2799
+        $scanner->scan('');
2800
+        $cache = $storage->getCache();
2801
+
2802
+        Filesystem::mount($storage, [], '/test/');
2803
+        $view = new View('/test');
2804
+
2805
+        $storage->unlink('foo.txt');
2806
+
2807
+        $this->assertTrue($cache->inCache('foo.txt'));
2808
+
2809
+        $this->assertFalse($view->fopen('foo.txt', 'r'));
2810
+
2811
+        $this->assertFalse($cache->inCache('foo.txt'));
2812
+    }
2813
+
2814
+    public function testMountpointParentsCreated(): void {
2815
+        $storage1 = $this->getTestStorage();
2816
+        Filesystem::mount($storage1, [], '/');
2817
+
2818
+        $storage2 = $this->getTestStorage();
2819
+        Filesystem::mount($storage2, [], '/A/B/C');
2820
+
2821
+        $rootView = new View('');
2822
+
2823
+        $folderData = $rootView->getDirectoryContent('/');
2824
+        $this->assertCount(4, $folderData);
2825
+        $this->assertEquals('folder', $folderData[0]['name']);
2826
+        $this->assertEquals('foo.png', $folderData[1]['name']);
2827
+        $this->assertEquals('foo.txt', $folderData[2]['name']);
2828
+        $this->assertEquals('A', $folderData[3]['name']);
2829
+
2830
+        $folderData = $rootView->getDirectoryContent('/A');
2831
+        $this->assertCount(1, $folderData);
2832
+        $this->assertEquals('B', $folderData[0]['name']);
2833
+
2834
+        $folderData = $rootView->getDirectoryContent('/A/B');
2835
+        $this->assertCount(1, $folderData);
2836
+        $this->assertEquals('C', $folderData[0]['name']);
2837
+
2838
+        $folderData = $rootView->getDirectoryContent('/A/B/C');
2839
+        $this->assertCount(3, $folderData);
2840
+        $this->assertEquals('folder', $folderData[0]['name']);
2841
+        $this->assertEquals('foo.png', $folderData[1]['name']);
2842
+        $this->assertEquals('foo.txt', $folderData[2]['name']);
2843
+    }
2844
+
2845
+    public function testCopyPreservesContent() {
2846
+        $viewUser1 = new View('/' . 'userId' . '/files');
2847
+        $viewUser1->mkdir('');
2848
+        $viewUser1->file_put_contents('foo.txt', 'foo');
2849
+        $viewUser1->copy('foo.txt', 'bar.txt');
2850
+        $this->assertEquals('foo', $viewUser1->file_get_contents('bar.txt'));
2851
+    }
2852 2852
 }
Please login to merge, or discard this patch.
Spacing   +90 added lines, -90 removed lines patch added patch discarded remove patch
@@ -155,7 +155,7 @@  discard block
 block discarded – undo
155 155
 		}
156 156
 
157 157
 		if ($this->tempStorage) {
158
-			system('rm -rf ' . escapeshellarg($this->tempStorage->getDataDir()));
158
+			system('rm -rf '.escapeshellarg($this->tempStorage->getDataDir()));
159 159
 		}
160 160
 
161 161
 		self::logout();
@@ -178,11 +178,11 @@  discard block
 block discarded – undo
178 178
 		$storage2 = $this->getTestStorage();
179 179
 		$storage3 = $this->getTestStorage();
180 180
 		$root = self::getUniqueID('/');
181
-		Filesystem::mount($storage1, [], $root . '/');
182
-		Filesystem::mount($storage2, [], $root . '/substorage');
183
-		Filesystem::mount($storage3, [], $root . '/folder/anotherstorage');
181
+		Filesystem::mount($storage1, [], $root.'/');
182
+		Filesystem::mount($storage2, [], $root.'/substorage');
183
+		Filesystem::mount($storage3, [], $root.'/folder/anotherstorage');
184 184
 		$textSize = strlen("dummy file data\n");
185
-		$imageSize = filesize(\OC::$SERVERROOT . '/core/img/logo/logo.png');
185
+		$imageSize = filesize(\OC::$SERVERROOT.'/core/img/logo/logo.png');
186 186
 		$storageSize = $textSize * 2 + $imageSize;
187 187
 
188 188
 		$storageInfo = $storage3->getCache()->get('');
@@ -239,7 +239,7 @@  discard block
 block discarded – undo
239 239
 		$this->assertEquals('foo.png', $folderData[1]['name']);
240 240
 		$this->assertEquals('foo.txt', $folderData[2]['name']);
241 241
 
242
-		$folderView = new View($root . '/folder');
242
+		$folderView = new View($root.'/folder');
243 243
 		$this->assertEquals($rootView->getFileInfo('/folder'), $folderView->getFileInfo('/'));
244 244
 
245 245
 		$cachedData = $rootView->getFileInfo('/foo.txt');
@@ -504,10 +504,10 @@  discard block
 block discarded – undo
504 504
 	}
505 505
 
506 506
 	public function moveBetweenStorages($storage1, $storage2) {
507
-		Filesystem::mount($storage1, [], '/' . $this->user . '/');
508
-		Filesystem::mount($storage2, [], '/' . $this->user . '/substorage');
507
+		Filesystem::mount($storage1, [], '/'.$this->user.'/');
508
+		Filesystem::mount($storage2, [], '/'.$this->user.'/substorage');
509 509
 
510
-		$rootView = new View('/' . $this->user);
510
+		$rootView = new View('/'.$this->user);
511 511
 		$rootView->rename('foo.txt', 'substorage/folder/foo.txt');
512 512
 		$this->assertFalse($rootView->file_exists('foo.txt'));
513 513
 		$this->assertTrue($rootView->file_exists('substorage/folder/foo.txt'));
@@ -614,11 +614,11 @@  discard block
 block discarded – undo
614 614
 		$storage2 = $this->getTestStorage();
615 615
 		$defaultRoot = Filesystem::getRoot();
616 616
 		Filesystem::mount($storage1, [], '/');
617
-		Filesystem::mount($storage2, [], $defaultRoot . '/substorage');
617
+		Filesystem::mount($storage2, [], $defaultRoot.'/substorage');
618 618
 		\OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook');
619 619
 
620 620
 		$rootView = new View('');
621
-		$subView = new View($defaultRoot . '/substorage');
621
+		$subView = new View($defaultRoot.'/substorage');
622 622
 		$this->hookPath = null;
623 623
 
624 624
 		$rootView->file_put_contents('/foo.txt', 'asd');
@@ -658,7 +658,7 @@  discard block
 block discarded – undo
658 658
 		 */
659 659
 		$storage = new $class([]);
660 660
 		$textData = "dummy file data\n";
661
-		$imgData = file_get_contents(\OC::$SERVERROOT . '/core/img/logo/logo.png');
661
+		$imgData = file_get_contents(\OC::$SERVERROOT.'/core/img/logo/logo.png');
662 662
 		$storage->mkdir('folder');
663 663
 		$storage->file_put_contents('foo.txt', $textData);
664 664
 		$storage->file_put_contents('foo.png', $imgData);
@@ -677,10 +677,10 @@  discard block
 block discarded – undo
677 677
 		$storage2 = $this->getTestStorage();
678 678
 		$defaultRoot = Filesystem::getRoot();
679 679
 		Filesystem::mount($storage1, [], '/');
680
-		Filesystem::mount($storage2, [], $defaultRoot . '_substorage');
680
+		Filesystem::mount($storage2, [], $defaultRoot.'_substorage');
681 681
 		\OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook');
682 682
 
683
-		$subView = new View($defaultRoot . '_substorage');
683
+		$subView = new View($defaultRoot.'_substorage');
684 684
 		$this->hookPath = null;
685 685
 
686 686
 		$subView->file_put_contents('/foo.txt', 'asd');
@@ -773,7 +773,7 @@  discard block
 block discarded – undo
773 773
 
774 774
 		$rootView = new View('');
775 775
 		foreach ($names as $name) {
776
-			$rootView->file_put_contents('/' . $name, 'dummy content');
776
+			$rootView->file_put_contents('/'.$name, 'dummy content');
777 777
 		}
778 778
 
779 779
 		$list = $rootView->getDirectoryContent('/');
@@ -811,15 +811,15 @@  discard block
 block discarded – undo
811 811
 		$depth = ((4000 - $tmpdirLength) / 57);
812 812
 
813 813
 		foreach (range(0, $depth - 1) as $i) {
814
-			$longPath .= $ds . $folderName;
814
+			$longPath .= $ds.$folderName;
815 815
 			$result = $rootView->mkdir($longPath);
816
-			$this->assertTrue($result, "mkdir failed on $i - path length: " . strlen($longPath));
816
+			$this->assertTrue($result, "mkdir failed on $i - path length: ".strlen($longPath));
817 817
 
818
-			$result = $rootView->file_put_contents($longPath . "{$ds}test.txt", 'lorem');
818
+			$result = $rootView->file_put_contents($longPath."{$ds}test.txt", 'lorem');
819 819
 			$this->assertEquals(5, $result, "file_put_contents failed on $i");
820 820
 
821 821
 			$this->assertTrue($rootView->file_exists($longPath));
822
-			$this->assertTrue($rootView->file_exists($longPath . "{$ds}test.txt"));
822
+			$this->assertTrue($rootView->file_exists($longPath."{$ds}test.txt"));
823 823
 		}
824 824
 
825 825
 		$cache = $storage->getCache();
@@ -832,11 +832,11 @@  discard block
 block discarded – undo
832 832
 			$this->assertTrue(is_array($cachedFolder), "No cache entry for folder at $i");
833 833
 			$this->assertEquals($folderName, $cachedFolder['name'], "Wrong cache entry for folder at $i");
834 834
 
835
-			$cachedFile = $cache->get($longPath . '/test.txt');
835
+			$cachedFile = $cache->get($longPath.'/test.txt');
836 836
 			$this->assertTrue(is_array($cachedFile), "No cache entry for file at $i");
837 837
 			$this->assertEquals('test.txt', $cachedFile['name'], "Wrong cache entry for file at $i");
838 838
 
839
-			$longPath .= $ds . $folderName;
839
+			$longPath .= $ds.$folderName;
840 840
 		}
841 841
 	}
842 842
 
@@ -1041,7 +1041,7 @@  discard block
 block discarded – undo
1041 1041
 		$folderName = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123456789';
1042 1042
 		$depth = (4000 / 57);
1043 1043
 		foreach (range(0, $depth + 1) as $i) {
1044
-			$longPath .= '/' . $folderName;
1044
+			$longPath .= '/'.$folderName;
1045 1045
 		}
1046 1046
 
1047 1047
 		$storage = new Temporary([]);
@@ -1294,8 +1294,8 @@  discard block
 block discarded – undo
1294 1294
 		$view = new View($rootPath);
1295 1295
 		$storage = new Temporary([]);
1296 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);
1297
+		$this->assertTrue($view->lockFile($pathPrefix.'/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1298
+		$view->lockFile($pathPrefix.'/foo/bar/asd', ILockingProvider::LOCK_SHARED);
1299 1299
 	}
1300 1300
 
1301 1301
 	/**
@@ -1313,8 +1313,8 @@  discard block
 block discarded – undo
1313 1313
 		$view = new View($rootPath);
1314 1314
 		$storage = new Temporary([]);
1315 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));
1316
+		$this->assertFalse($view->lockFile($pathPrefix.'/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1317
+		$this->assertFalse($view->lockFile($pathPrefix.'/foo/bar/asd', ILockingProvider::LOCK_SHARED));
1318 1318
 	}
1319 1319
 
1320 1320
 	/**
@@ -1335,8 +1335,8 @@  discard block
 block discarded – undo
1335 1335
 		$view = new View($rootPath);
1336 1336
 		$storage = new Temporary([]);
1337 1337
 		Filesystem::mount($storage, [], '/');
1338
-		$this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED));
1339
-		$view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE);
1338
+		$this->assertTrue($view->lockFile($pathPrefix.'/foo/bar', ILockingProvider::LOCK_SHARED));
1339
+		$view->lockFile($pathPrefix.'/foo/bar', ILockingProvider::LOCK_EXCLUSIVE);
1340 1340
 	}
1341 1341
 
1342 1342
 	/**
@@ -1354,8 +1354,8 @@  discard block
 block discarded – undo
1354 1354
 		$view = new View($rootPath);
1355 1355
 		$storage = new Temporary([]);
1356 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));
1357
+		$this->assertFalse($view->lockFile($pathPrefix.'/foo/bar', ILockingProvider::LOCK_SHARED));
1358
+		$this->assertFalse($view->lockFile($pathPrefix.'/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1359 1359
 	}
1360 1360
 
1361 1361
 	/**
@@ -1562,7 +1562,7 @@  discard block
 block discarded – undo
1562 1562
 				->onlyMethods([])
1563 1563
 				->getMock();
1564 1564
 			$storage->method('getId')->willReturn('non-null-id');
1565
-			$storage->method('getStorageCache')->willReturnCallback(function () use ($storage) {
1565
+			$storage->method('getStorageCache')->willReturnCallback(function() use ($storage) {
1566 1566
 				return new \OC\Files\Cache\Storage($storage, true, Server::get(IDBConnection::class));
1567 1567
 			});
1568 1568
 
@@ -1591,8 +1591,8 @@  discard block
 block discarded – undo
1591 1591
 		self::loginAsUser($this->user);
1592 1592
 
1593 1593
 		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1594
-			$this->user . '/files/mount1',
1595
-			$this->user . '/files/mount2',
1594
+			$this->user.'/files/mount1',
1595
+			$this->user.'/files/mount2',
1596 1596
 		]);
1597 1597
 		$mount1->expects($this->once())
1598 1598
 			->method('moveMount')
@@ -1602,7 +1602,7 @@  discard block
 block discarded – undo
1602 1602
 			->method('moveMount')
1603 1603
 			->willReturn(true);
1604 1604
 
1605
-		$view = new View('/' . $this->user . '/files/');
1605
+		$view = new View('/'.$this->user.'/files/');
1606 1606
 		$view->mkdir('sub');
1607 1607
 
1608 1608
 		$this->assertTrue($view->rename('mount1', 'renamed_mount'), 'Can rename mount point');
@@ -1613,8 +1613,8 @@  discard block
 block discarded – undo
1613 1613
 		self::loginAsUser($this->user);
1614 1614
 
1615 1615
 		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1616
-			$this->user . '/files/mount1',
1617
-			$this->user . '/files/mount2',
1616
+			$this->user.'/files/mount1',
1617
+			$this->user.'/files/mount2',
1618 1618
 		]);
1619 1619
 
1620 1620
 		$mount1->expects($this->never())
@@ -1623,7 +1623,7 @@  discard block
 block discarded – undo
1623 1623
 		$mount2->expects($this->never())
1624 1624
 			->method('moveMount');
1625 1625
 
1626
-		$view = new View('/' . $this->user . '/files/');
1626
+		$view = new View('/'.$this->user.'/files/');
1627 1627
 
1628 1628
 		$this->expectException(ForbiddenException::class);
1629 1629
 		$view->rename('mount1', 'mount2');
@@ -1633,8 +1633,8 @@  discard block
 block discarded – undo
1633 1633
 		self::loginAsUser($this->user);
1634 1634
 
1635 1635
 		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1636
-			$this->user . '/files/mount1',
1637
-			$this->user . '/files/mount2',
1636
+			$this->user.'/files/mount1',
1637
+			$this->user.'/files/mount2',
1638 1638
 		]);
1639 1639
 
1640 1640
 		$mount1->expects($this->never())
@@ -1643,7 +1643,7 @@  discard block
 block discarded – undo
1643 1643
 		$mount2->expects($this->never())
1644 1644
 			->method('moveMount');
1645 1645
 
1646
-		$view = new View('/' . $this->user . '/files/');
1646
+		$view = new View('/'.$this->user.'/files/');
1647 1647
 
1648 1648
 		$this->expectException(ForbiddenException::class);
1649 1649
 		$view->rename('mount1', 'mount2/sub');
@@ -1656,8 +1656,8 @@  discard block
 block discarded – undo
1656 1656
 		self::loginAsUser($this->user);
1657 1657
 
1658 1658
 		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1659
-			$this->user . '/files/mount1',
1660
-			$this->user . '/files/mount2',
1659
+			$this->user.'/files/mount1',
1660
+			$this->user.'/files/mount2',
1661 1661
 		]);
1662 1662
 
1663 1663
 		$mount1->expects($this->never())
@@ -1667,7 +1667,7 @@  discard block
 block discarded – undo
1667 1667
 			->method('moveMount')
1668 1668
 			->willReturn(true);
1669 1669
 
1670
-		$view = new View('/' . $this->user . '/files/');
1670
+		$view = new View('/'.$this->user.'/files/');
1671 1671
 		$view->mkdir('shareddir');
1672 1672
 		$view->mkdir('shareddir/sub');
1673 1673
 		$view->mkdir('shareddir/sub2');
@@ -1893,7 +1893,7 @@  discard block
 block discarded – undo
1893 1893
 		?int $expectedStrayLock = null,
1894 1894
 		mixed $returnValue = true,
1895 1895
 	): void {
1896
-		$view = new View('/' . $this->user . '/files/');
1896
+		$view = new View('/'.$this->user.'/files/');
1897 1897
 
1898 1898
 		/** @var Temporary&MockObject $storage */
1899 1899
 		$storage = $this->getMockBuilder(Temporary::class)
@@ -1905,19 +1905,19 @@  discard block
 block discarded – undo
1905 1905
 		/* Same thing with encryption wrapper */
1906 1906
 		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
1907 1907
 
1908
-		Filesystem::mount($storage, [], $this->user . '/');
1908
+		Filesystem::mount($storage, [], $this->user.'/');
1909 1909
 
1910 1910
 		// work directly on disk because mkdir might be mocked
1911 1911
 		$realPath = $storage->getSourcePath('');
1912
-		mkdir($realPath . '/files');
1913
-		mkdir($realPath . '/files/dir');
1914
-		file_put_contents($realPath . '/files/test.txt', 'blah');
1912
+		mkdir($realPath.'/files');
1913
+		mkdir($realPath.'/files/dir');
1914
+		file_put_contents($realPath.'/files/test.txt', 'blah');
1915 1915
 		$storage->getScanner()->scan('files');
1916 1916
 
1917 1917
 		$storage->expects($this->once())
1918 1918
 			->method($operation)
1919 1919
 			->willReturnCallback(
1920
-				function () use ($view, $lockedPath, &$lockTypeDuring, $returnValue) {
1920
+				function() use ($view, $lockedPath, &$lockTypeDuring, $returnValue) {
1921 1921
 					$lockTypeDuring = $this->getFileLockType($view, $lockedPath);
1922 1922
 
1923 1923
 					return $returnValue;
@@ -1950,7 +1950,7 @@  discard block
 block discarded – undo
1950 1950
 	 * This code path uses $storage->fopen instead
1951 1951
 	 */
1952 1952
 	public function testLockFilePutContentWithStream(): void {
1953
-		$view = new View('/' . $this->user . '/files/');
1953
+		$view = new View('/'.$this->user.'/files/');
1954 1954
 
1955 1955
 		$path = 'test_file_put_contents.txt';
1956 1956
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
@@ -1958,13 +1958,13 @@  discard block
 block discarded – undo
1958 1958
 			->onlyMethods(['fopen'])
1959 1959
 			->getMock();
1960 1960
 
1961
-		Filesystem::mount($storage, [], $this->user . '/');
1961
+		Filesystem::mount($storage, [], $this->user.'/');
1962 1962
 		$storage->mkdir('files');
1963 1963
 
1964 1964
 		$storage->expects($this->once())
1965 1965
 			->method('fopen')
1966 1966
 			->willReturnCallback(
1967
-				function () use ($view, $path, &$lockTypeDuring) {
1967
+				function() use ($view, $path, &$lockTypeDuring) {
1968 1968
 					$lockTypeDuring = $this->getFileLockType($view, $path);
1969 1969
 
1970 1970
 					return fopen('php://temp', 'r+');
@@ -1989,7 +1989,7 @@  discard block
 block discarded – undo
1989 1989
 	 * Test locks for fopen with fclose at the end
1990 1990
 	 */
1991 1991
 	public function testLockFopen(): void {
1992
-		$view = new View('/' . $this->user . '/files/');
1992
+		$view = new View('/'.$this->user.'/files/');
1993 1993
 
1994 1994
 		$path = 'test_file_put_contents.txt';
1995 1995
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
@@ -1997,13 +1997,13 @@  discard block
 block discarded – undo
1997 1997
 			->onlyMethods(['fopen'])
1998 1998
 			->getMock();
1999 1999
 
2000
-		Filesystem::mount($storage, [], $this->user . '/');
2000
+		Filesystem::mount($storage, [], $this->user.'/');
2001 2001
 		$storage->mkdir('files');
2002 2002
 
2003 2003
 		$storage->expects($this->once())
2004 2004
 			->method('fopen')
2005 2005
 			->willReturnCallback(
2006
-				function () use ($view, $path, &$lockTypeDuring) {
2006
+				function() use ($view, $path, &$lockTypeDuring) {
2007 2007
 					$lockTypeDuring = $this->getFileLockType($view, $path);
2008 2008
 
2009 2009
 					return fopen('php://temp', 'r+');
@@ -2045,7 +2045,7 @@  discard block
 block discarded – undo
2045 2045
 		if ($operation === 'touch') {
2046 2046
 			$this->markTestSkipped('touch handles storage exceptions internally');
2047 2047
 		}
2048
-		$view = new View('/' . $this->user . '/files/');
2048
+		$view = new View('/'.$this->user.'/files/');
2049 2049
 
2050 2050
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2051 2051
 		$storage = $this->getMockBuilder(Temporary::class)
@@ -2057,19 +2057,19 @@  discard block
 block discarded – undo
2057 2057
 		/* Same thing with encryption wrapper */
2058 2058
 		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2059 2059
 
2060
-		Filesystem::mount($storage, [], $this->user . '/');
2060
+		Filesystem::mount($storage, [], $this->user.'/');
2061 2061
 
2062 2062
 		// work directly on disk because mkdir might be mocked
2063 2063
 		$realPath = $storage->getSourcePath('');
2064
-		mkdir($realPath . '/files');
2065
-		mkdir($realPath . '/files/dir');
2066
-		file_put_contents($realPath . '/files/test.txt', 'blah');
2064
+		mkdir($realPath.'/files');
2065
+		mkdir($realPath.'/files/dir');
2066
+		file_put_contents($realPath.'/files/test.txt', 'blah');
2067 2067
 		$storage->getScanner()->scan('files');
2068 2068
 
2069 2069
 		$storage->expects($this->once())
2070 2070
 			->method($operation)
2071 2071
 			->willReturnCallback(
2072
-				function (): void {
2072
+				function(): void {
2073 2073
 					throw new \Exception('Simulated exception');
2074 2074
 				}
2075 2075
 			);
@@ -2089,11 +2089,11 @@  discard block
 block discarded – undo
2089 2089
 	}
2090 2090
 
2091 2091
 	public function testLockBasicOperationUnlocksAfterLockException(): void {
2092
-		$view = new View('/' . $this->user . '/files/');
2092
+		$view = new View('/'.$this->user.'/files/');
2093 2093
 
2094 2094
 		$storage = new Temporary([]);
2095 2095
 
2096
-		Filesystem::mount($storage, [], $this->user . '/');
2096
+		Filesystem::mount($storage, [], $this->user.'/');
2097 2097
 
2098 2098
 		$storage->mkdir('files');
2099 2099
 		$storage->mkdir('files/dir');
@@ -2135,14 +2135,14 @@  discard block
 block discarded – undo
2135 2135
 		$path,
2136 2136
 		$hookType,
2137 2137
 	): void {
2138
-		$view = new View('/' . $this->user . '/files/');
2138
+		$view = new View('/'.$this->user.'/files/');
2139 2139
 
2140 2140
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2141 2141
 		$storage = $this->getMockBuilder(Temporary::class)
2142 2142
 			->onlyMethods([$operation])
2143 2143
 			->getMock();
2144 2144
 
2145
-		Filesystem::mount($storage, [], $this->user . '/');
2145
+		Filesystem::mount($storage, [], $this->user.'/');
2146 2146
 		$storage->mkdir('files');
2147 2147
 
2148 2148
 		Util::connectHook(
@@ -2174,7 +2174,7 @@  discard block
 block discarded – undo
2174 2174
 	 */
2175 2175
 	#[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyDataProvider')]
2176 2176
 	public function testLockFileRename($operation, $expectedLockTypeSourceDuring): void {
2177
-		$view = new View('/' . $this->user . '/files/');
2177
+		$view = new View('/'.$this->user.'/files/');
2178 2178
 
2179 2179
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2180 2180
 		$storage = $this->getMockBuilder(Temporary::class)
@@ -2200,14 +2200,14 @@  discard block
 block discarded – undo
2200 2200
 		/* Disable encryption wrapper to avoid it intercepting mocked call */
2201 2201
 		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2202 2202
 
2203
-		Filesystem::mount($storage, [], $this->user . '/');
2203
+		Filesystem::mount($storage, [], $this->user.'/');
2204 2204
 		$storage->mkdir('files');
2205 2205
 		$view->file_put_contents($sourcePath, 'meh');
2206 2206
 
2207 2207
 		$storage->expects($this->once())
2208 2208
 			->method($operation)
2209 2209
 			->willReturnCallback(
2210
-				function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2210
+				function() use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2211 2211
 					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2212 2212
 					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2213 2213
 
@@ -2243,7 +2243,7 @@  discard block
 block discarded – undo
2243 2243
 	public function testLockFileCopyException(): void {
2244 2244
 		$this->expectException(\Exception::class);
2245 2245
 
2246
-		$view = new View('/' . $this->user . '/files/');
2246
+		$view = new View('/'.$this->user.'/files/');
2247 2247
 
2248 2248
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2249 2249
 		$storage = $this->getMockBuilder(Temporary::class)
@@ -2256,14 +2256,14 @@  discard block
 block discarded – undo
2256 2256
 		/* Disable encryption wrapper to avoid it intercepting mocked call */
2257 2257
 		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2258 2258
 
2259
-		Filesystem::mount($storage, [], $this->user . '/');
2259
+		Filesystem::mount($storage, [], $this->user.'/');
2260 2260
 		$storage->mkdir('files');
2261 2261
 		$view->file_put_contents($sourcePath, 'meh');
2262 2262
 
2263 2263
 		$storage->expects($this->once())
2264 2264
 			->method('copy')
2265 2265
 			->willReturnCallback(
2266
-				function (): void {
2266
+				function(): void {
2267 2267
 					throw new \Exception();
2268 2268
 				}
2269 2269
 			);
@@ -2289,7 +2289,7 @@  discard block
 block discarded – undo
2289 2289
 	public function testLockFileRenameUnlockOnException(): void {
2290 2290
 		self::loginAsUser('test');
2291 2291
 
2292
-		$view = new View('/' . $this->user . '/files/');
2292
+		$view = new View('/'.$this->user.'/files/');
2293 2293
 
2294 2294
 		$sourcePath = 'original.txt';
2295 2295
 		$targetPath = 'target.txt';
@@ -2330,7 +2330,7 @@  discard block
 block discarded – undo
2330 2330
 		$this->assertEquals('test', $view->getFileInfo($path)->getOwner()->getUID());
2331 2331
 
2332 2332
 		$folderInfo = $view->getDirectoryContent('');
2333
-		$folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2333
+		$folderInfo = array_values(array_filter($folderInfo, function(FileInfo $info) {
2334 2334
 			return $info->getName() === 'foo.txt';
2335 2335
 		}));
2336 2336
 
@@ -2340,7 +2340,7 @@  discard block
 block discarded – undo
2340 2340
 		Filesystem::mount($subStorage, [], '/test/files/asd');
2341 2341
 
2342 2342
 		$folderInfo = $view->getDirectoryContent('');
2343
-		$folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2343
+		$folderInfo = array_values(array_filter($folderInfo, function(FileInfo $info) {
2344 2344
 			return $info->getName() === 'asd';
2345 2345
 		}));
2346 2346
 
@@ -2365,7 +2365,7 @@  discard block
 block discarded – undo
2365 2365
 	 */
2366 2366
 	#[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyCrossStorageDataProvider')]
2367 2367
 	public function testLockFileRenameCrossStorage($viewOperation, $storageOperation, $expectedLockTypeSourceDuring): void {
2368
-		$view = new View('/' . $this->user . '/files/');
2368
+		$view = new View('/'.$this->user.'/files/');
2369 2369
 
2370 2370
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2371 2371
 		$storage = $this->getMockBuilder(Temporary::class)
@@ -2395,8 +2395,8 @@  discard block
 block discarded – undo
2395 2395
 		/* Disable encryption wrapper to avoid it intercepting mocked call */
2396 2396
 		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2397 2397
 
2398
-		Filesystem::mount($storage, [], $this->user . '/');
2399
-		Filesystem::mount($storage2, [], $this->user . '/files/substorage');
2398
+		Filesystem::mount($storage, [], $this->user.'/');
2399
+		Filesystem::mount($storage2, [], $this->user.'/files/substorage');
2400 2400
 		$storage->mkdir('files');
2401 2401
 		$view->file_put_contents($sourcePath, 'meh');
2402 2402
 		$storage2->getUpdater()->update('');
@@ -2406,7 +2406,7 @@  discard block
 block discarded – undo
2406 2406
 		$storage2->expects($this->once())
2407 2407
 			->method($storageOperation)
2408 2408
 			->willReturnCallback(
2409
-				function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2409
+				function() use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2410 2410
 					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2411 2411
 					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2412 2412
 
@@ -2441,10 +2441,10 @@  discard block
 block discarded – undo
2441 2441
 		self::loginAsUser('test');
2442 2442
 
2443 2443
 		[$mount] = $this->createTestMovableMountPoints([
2444
-			$this->user . '/files/substorage',
2444
+			$this->user.'/files/substorage',
2445 2445
 		]);
2446 2446
 
2447
-		$view = new View('/' . $this->user . '/files/');
2447
+		$view = new View('/'.$this->user.'/files/');
2448 2448
 		$view->mkdir('subdir');
2449 2449
 
2450 2450
 		$sourcePath = 'substorage';
@@ -2453,7 +2453,7 @@  discard block
 block discarded – undo
2453 2453
 		$mount->expects($this->once())
2454 2454
 			->method('moveMount')
2455 2455
 			->willReturnCallback(
2456
-				function ($target) use ($mount, $view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring, &$lockTypeSharedRootDuring) {
2456
+				function($target) use ($mount, $view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring, &$lockTypeSharedRootDuring) {
2457 2457
 					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath, true);
2458 2458
 					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath, true);
2459 2459
 
@@ -2518,14 +2518,14 @@  discard block
 block discarded – undo
2518 2518
 		$eventHandler->expects($this->any())
2519 2519
 			->method('preCallback')
2520 2520
 			->willReturnCallback(
2521
-				function () use ($view, $path, $onMountPoint, &$lockTypePre): void {
2521
+				function() use ($view, $path, $onMountPoint, &$lockTypePre): void {
2522 2522
 					$lockTypePre = $this->getFileLockType($view, $path, $onMountPoint);
2523 2523
 				}
2524 2524
 			);
2525 2525
 		$eventHandler->expects($this->any())
2526 2526
 			->method('postCallback')
2527 2527
 			->willReturnCallback(
2528
-				function () use ($view, $path, $onMountPoint, &$lockTypePost): void {
2528
+				function() use ($view, $path, $onMountPoint, &$lockTypePost): void {
2529 2529
 					$lockTypePost = $this->getFileLockType($view, $path, $onMountPoint);
2530 2530
 				}
2531 2531
 			);
@@ -2539,7 +2539,7 @@  discard block
 block discarded – undo
2539 2539
 			);
2540 2540
 			Util::connectHook(
2541 2541
 				Filesystem::CLASSNAME,
2542
-				'post_' . $hookType,
2542
+				'post_'.$hookType,
2543 2543
 				$eventHandler,
2544 2544
 				'postCallback'
2545 2545
 			);
@@ -2567,7 +2567,7 @@  discard block
 block discarded – undo
2567 2567
 
2568 2568
 
2569 2569
 	public function testRemoveMoveableMountPoint(): void {
2570
-		$mountPoint = '/' . $this->user . '/files/mount/';
2570
+		$mountPoint = '/'.$this->user.'/files/mount/';
2571 2571
 
2572 2572
 		// Mock the mount point
2573 2573
 		/** @var TestMoveableMountPoint|\PHPUnit\Framework\MockObject\MockObject $mount */
@@ -2609,7 +2609,7 @@  discard block
 block discarded – undo
2609 2609
 		);
2610 2610
 
2611 2611
 		//Delete the mountpoint
2612
-		$view = new View('/' . $this->user . '/files');
2612
+		$view = new View('/'.$this->user.'/files');
2613 2613
 		$this->assertEquals('foo', $view->rmdir('mount'));
2614 2614
 	}
2615 2615
 
@@ -2630,7 +2630,7 @@  discard block
 block discarded – undo
2630 2630
 	public function testGetDirectoryContentMimeFilter($filter, $expected): void {
2631 2631
 		$storage1 = new Temporary();
2632 2632
 		$root = self::getUniqueID('/');
2633
-		Filesystem::mount($storage1, [], $root . '/');
2633
+		Filesystem::mount($storage1, [], $root.'/');
2634 2634
 		$view = new View($root);
2635 2635
 
2636 2636
 		$view->file_put_contents('test1.txt', 'asd');
@@ -2640,7 +2640,7 @@  discard block
 block discarded – undo
2640 2640
 
2641 2641
 		$content = $view->getDirectoryContent('', $filter);
2642 2642
 
2643
-		$files = array_map(function (FileInfo $info) {
2643
+		$files = array_map(function(FileInfo $info) {
2644 2644
 			return $info->getName();
2645 2645
 		}, $content);
2646 2646
 		sort($files);
@@ -2746,7 +2746,7 @@  discard block
 block discarded – undo
2746 2746
 		$calls = ['/new/folder', '/new/folder/structure'];
2747 2747
 		$view->expects($this->exactly(2))
2748 2748
 			->method('mkdir')
2749
-			->willReturnCallback(function ($dir) use (&$calls): void {
2749
+			->willReturnCallback(function($dir) use (&$calls): void {
2750 2750
 				$expected = array_shift($calls);
2751 2751
 				$this->assertEquals($expected, $dir);
2752 2752
 			});
@@ -2843,7 +2843,7 @@  discard block
 block discarded – undo
2843 2843
 	}
2844 2844
 
2845 2845
 	public function testCopyPreservesContent() {
2846
-		$viewUser1 = new View('/' . 'userId' . '/files');
2846
+		$viewUser1 = new View('/'.'userId'.'/files');
2847 2847
 		$viewUser1->mkdir('');
2848 2848
 		$viewUser1->file_put_contents('foo.txt', 'foo');
2849 2849
 		$viewUser1->copy('foo.txt', 'bar.txt');
Please login to merge, or discard this patch.
tests/lib/HelperStorageTest.php 1 patch
Indentation   +226 added lines, -226 removed lines patch added patch discarded remove patch
@@ -22,230 +22,230 @@
 block discarded – undo
22 22
  */
23 23
 #[\PHPUnit\Framework\Attributes\Group('DB')]
24 24
 class HelperStorageTest extends \Test\TestCase {
25
-	use UserTrait;
26
-
27
-	/** @var string */
28
-	private $user;
29
-	/** @var Storage */
30
-	private $storageMock;
31
-	/** @var Storage */
32
-	private $storage;
33
-	private bool $savedQuotaIncludeExternalStorage;
34
-
35
-	protected function setUp(): void {
36
-		parent::setUp();
37
-
38
-		$this->user = $this->getUniqueID('user_');
39
-		$this->createUser($this->user, $this->user);
40
-		$this->savedQuotaIncludeExternalStorage = $this->getIncludeExternalStorage();
41
-
42
-		Filesystem::tearDown();
43
-		\OC_User::setUserId($this->user);
44
-		Filesystem::init($this->user, '/' . $this->user . '/files');
45
-
46
-		/** @var IMountManager $manager */
47
-		$manager = Server::get(IMountManager::class);
48
-		$manager->removeMount('/' . $this->user);
49
-
50
-		$this->storageMock = null;
51
-	}
52
-
53
-	protected function tearDown(): void {
54
-		$this->setIncludeExternalStorage($this->savedQuotaIncludeExternalStorage);
55
-		$this->user = null;
56
-
57
-		if ($this->storageMock) {
58
-			$this->storageMock->getCache()->clear();
59
-			$this->storageMock = null;
60
-		}
61
-		Filesystem::tearDown();
62
-
63
-		\OC_User::setUserId('');
64
-		Server::get(IConfig::class)->deleteAllUserValues($this->user);
65
-
66
-		parent::tearDown();
67
-	}
68
-
69
-	/**
70
-	 * Returns a storage mock that returns the given value as
71
-	 * free space
72
-	 *
73
-	 * @param int $freeSpace free space value
74
-	 * @return Storage
75
-	 */
76
-	private function getStorageMock($freeSpace = 12) {
77
-		$this->storageMock = $this->getMockBuilder(Temporary::class)
78
-			->onlyMethods(['free_space'])
79
-			->setConstructorArgs([[]])
80
-			->getMock();
81
-
82
-		$this->storageMock->expects($this->once())
83
-			->method('free_space')
84
-			->willReturn($freeSpace);
85
-		return $this->storageMock;
86
-	}
87
-
88
-	/**
89
-	 * Test getting the storage info
90
-	 */
91
-	public function testGetStorageInfo(): void {
92
-		$homeStorage = $this->getStorageMock(12);
93
-		Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
94
-		$homeStorage->file_put_contents('test.txt', '01234');
95
-
96
-		$storageInfo = \OC_Helper::getStorageInfo('');
97
-		$this->assertEquals(12, $storageInfo['free']);
98
-		$this->assertEquals(5, $storageInfo['used']);
99
-		$this->assertEquals(17, $storageInfo['total']);
100
-	}
101
-
102
-	private function getIncludeExternalStorage(): bool {
103
-		$class = new \ReflectionClass(\OC_Helper::class);
104
-		$prop = $class->getProperty('quotaIncludeExternalStorage');
105
-		return $prop->getValue(null) ?? false;
106
-	}
107
-
108
-	private function setIncludeExternalStorage(bool $include) {
109
-		$class = new \ReflectionClass(\OC_Helper::class);
110
-		$prop = $class->getProperty('quotaIncludeExternalStorage');
111
-		$prop->setValue(null, $include);
112
-	}
113
-
114
-	/**
115
-	 * Test getting the storage info, ignoring extra mount points
116
-	 */
117
-	public function testGetStorageInfoExcludingExtStorage(): void {
118
-		$homeStorage = $this->getStorageMock(12);
119
-		Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
120
-		$homeStorage->file_put_contents('test.txt', '01234');
121
-
122
-		$extStorage = new Temporary([]);
123
-		$extStorage->file_put_contents('extfile.txt', 'abcdefghijklmnopq');
124
-		$extStorage->getScanner()->scan(''); // update root size
125
-
126
-		$this->setIncludeExternalStorage(false);
127
-
128
-		Filesystem::mount($extStorage, [], '/' . $this->user . '/files/ext');
129
-
130
-		$storageInfo = \OC_Helper::getStorageInfo('');
131
-		$this->assertEquals(12, $storageInfo['free']);
132
-		$this->assertEquals(5, $storageInfo['used']);
133
-		$this->assertEquals(17, $storageInfo['total']);
134
-	}
135
-
136
-	/**
137
-	 * Test getting the storage info, including extra mount points
138
-	 */
139
-	public function testGetStorageInfoIncludingExtStorage(): void {
140
-		$homeStorage = new Temporary([]);
141
-		Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
142
-		$homeStorage->file_put_contents('test.txt', '01234');
143
-
144
-		$extStorage = new Temporary([]);
145
-		$extStorage->file_put_contents('extfile.txt', 'abcdefghijklmnopq');
146
-		$extStorage->getScanner()->scan(''); // update root size
147
-
148
-		Filesystem::mount($extStorage, [], '/' . $this->user . '/files/ext');
149
-
150
-		$this->setIncludeExternalStorage(true);
151
-
152
-		$config = Server::get(IConfig::class);
153
-		$config->setUserValue($this->user, 'files', 'quota', '25');
154
-
155
-		$storageInfo = \OC_Helper::getStorageInfo('');
156
-		$this->assertEquals(3, $storageInfo['free']);
157
-		$this->assertEquals(22, $storageInfo['used']);
158
-		$this->assertEquals(25, $storageInfo['total']);
159
-
160
-		$config->setUserValue($this->user, 'files', 'quota', 'default');
161
-	}
162
-
163
-	/**
164
-	 * Test getting the storage info excluding extra mount points
165
-	 * when user has no quota set, even when quota ext storage option
166
-	 * was set
167
-	 */
168
-	public function testGetStorageInfoIncludingExtStorageWithNoUserQuota(): void {
169
-		$homeStorage = $this->getStorageMock(12);
170
-		Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
171
-		$homeStorage->file_put_contents('test.txt', '01234');
172
-
173
-		$extStorage = new Temporary([]);
174
-		$extStorage->file_put_contents('extfile.txt', 'abcdefghijklmnopq');
175
-		$extStorage->getScanner()->scan(''); // update root size
176
-
177
-		Filesystem::mount($extStorage, [], '/' . $this->user . '/files/ext');
178
-
179
-		$config = Server::get(IConfig::class);
180
-		$this->setIncludeExternalStorage(true);
181
-
182
-		$storageInfo = \OC_Helper::getStorageInfo('');
183
-		$this->assertEquals(12, $storageInfo['free'], '12 bytes free in home storage');
184
-		$this->assertEquals(22, $storageInfo['used'], '5 bytes of home storage and 17 bytes of the temporary storage are used');
185
-		$this->assertEquals(34, $storageInfo['total'], '5 bytes used and 12 bytes free in home storage as well as 17 bytes used in temporary storage');
186
-	}
187
-
188
-
189
-	/**
190
-	 * Test getting the storage info with quota enabled
191
-	 */
192
-	public function testGetStorageInfoWithQuota(): void {
193
-		$homeStorage = $this->getStorageMock(12);
194
-		$homeStorage->file_put_contents('test.txt', '01234');
195
-		$homeStorage = new Quota(
196
-			[
197
-				'storage' => $homeStorage,
198
-				'quota' => 7
199
-			]
200
-		);
201
-		Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
202
-
203
-		$storageInfo = \OC_Helper::getStorageInfo('');
204
-		$this->assertEquals(2, $storageInfo['free']);
205
-		$this->assertEquals(5, $storageInfo['used']);
206
-		$this->assertEquals(7, $storageInfo['total']);
207
-	}
208
-
209
-	/**
210
-	 * Test getting the storage info when data exceeds quota
211
-	 */
212
-	public function testGetStorageInfoWhenSizeExceedsQuota(): void {
213
-		$homeStorage = $this->getStorageMock(12);
214
-		$homeStorage->file_put_contents('test.txt', '0123456789');
215
-		$homeStorage = new Quota(
216
-			[
217
-				'storage' => $homeStorage,
218
-				'quota' => 7
219
-			]
220
-		);
221
-		Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
222
-
223
-		$storageInfo = \OC_Helper::getStorageInfo('');
224
-		$this->assertEquals(0, $storageInfo['free']);
225
-		$this->assertEquals(10, $storageInfo['used']);
226
-		// total = quota
227
-		$this->assertEquals(7, $storageInfo['total']);
228
-	}
229
-
230
-	/**
231
-	 * Test getting the storage info when the remaining
232
-	 * free storage space is less than the quota
233
-	 */
234
-	public function testGetStorageInfoWhenFreeSpaceLessThanQuota(): void {
235
-		$homeStorage = $this->getStorageMock(12);
236
-		$homeStorage->file_put_contents('test.txt', '01234');
237
-		$homeStorage = new Quota(
238
-			[
239
-				'storage' => $homeStorage,
240
-				'quota' => 18
241
-			]
242
-		);
243
-		Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
244
-
245
-		$storageInfo = \OC_Helper::getStorageInfo('');
246
-		$this->assertEquals(12, $storageInfo['free']);
247
-		$this->assertEquals(5, $storageInfo['used']);
248
-		// total = free + used (because quota > total)
249
-		$this->assertEquals(17, $storageInfo['total']);
250
-	}
25
+    use UserTrait;
26
+
27
+    /** @var string */
28
+    private $user;
29
+    /** @var Storage */
30
+    private $storageMock;
31
+    /** @var Storage */
32
+    private $storage;
33
+    private bool $savedQuotaIncludeExternalStorage;
34
+
35
+    protected function setUp(): void {
36
+        parent::setUp();
37
+
38
+        $this->user = $this->getUniqueID('user_');
39
+        $this->createUser($this->user, $this->user);
40
+        $this->savedQuotaIncludeExternalStorage = $this->getIncludeExternalStorage();
41
+
42
+        Filesystem::tearDown();
43
+        \OC_User::setUserId($this->user);
44
+        Filesystem::init($this->user, '/' . $this->user . '/files');
45
+
46
+        /** @var IMountManager $manager */
47
+        $manager = Server::get(IMountManager::class);
48
+        $manager->removeMount('/' . $this->user);
49
+
50
+        $this->storageMock = null;
51
+    }
52
+
53
+    protected function tearDown(): void {
54
+        $this->setIncludeExternalStorage($this->savedQuotaIncludeExternalStorage);
55
+        $this->user = null;
56
+
57
+        if ($this->storageMock) {
58
+            $this->storageMock->getCache()->clear();
59
+            $this->storageMock = null;
60
+        }
61
+        Filesystem::tearDown();
62
+
63
+        \OC_User::setUserId('');
64
+        Server::get(IConfig::class)->deleteAllUserValues($this->user);
65
+
66
+        parent::tearDown();
67
+    }
68
+
69
+    /**
70
+     * Returns a storage mock that returns the given value as
71
+     * free space
72
+     *
73
+     * @param int $freeSpace free space value
74
+     * @return Storage
75
+     */
76
+    private function getStorageMock($freeSpace = 12) {
77
+        $this->storageMock = $this->getMockBuilder(Temporary::class)
78
+            ->onlyMethods(['free_space'])
79
+            ->setConstructorArgs([[]])
80
+            ->getMock();
81
+
82
+        $this->storageMock->expects($this->once())
83
+            ->method('free_space')
84
+            ->willReturn($freeSpace);
85
+        return $this->storageMock;
86
+    }
87
+
88
+    /**
89
+     * Test getting the storage info
90
+     */
91
+    public function testGetStorageInfo(): void {
92
+        $homeStorage = $this->getStorageMock(12);
93
+        Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
94
+        $homeStorage->file_put_contents('test.txt', '01234');
95
+
96
+        $storageInfo = \OC_Helper::getStorageInfo('');
97
+        $this->assertEquals(12, $storageInfo['free']);
98
+        $this->assertEquals(5, $storageInfo['used']);
99
+        $this->assertEquals(17, $storageInfo['total']);
100
+    }
101
+
102
+    private function getIncludeExternalStorage(): bool {
103
+        $class = new \ReflectionClass(\OC_Helper::class);
104
+        $prop = $class->getProperty('quotaIncludeExternalStorage');
105
+        return $prop->getValue(null) ?? false;
106
+    }
107
+
108
+    private function setIncludeExternalStorage(bool $include) {
109
+        $class = new \ReflectionClass(\OC_Helper::class);
110
+        $prop = $class->getProperty('quotaIncludeExternalStorage');
111
+        $prop->setValue(null, $include);
112
+    }
113
+
114
+    /**
115
+     * Test getting the storage info, ignoring extra mount points
116
+     */
117
+    public function testGetStorageInfoExcludingExtStorage(): void {
118
+        $homeStorage = $this->getStorageMock(12);
119
+        Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
120
+        $homeStorage->file_put_contents('test.txt', '01234');
121
+
122
+        $extStorage = new Temporary([]);
123
+        $extStorage->file_put_contents('extfile.txt', 'abcdefghijklmnopq');
124
+        $extStorage->getScanner()->scan(''); // update root size
125
+
126
+        $this->setIncludeExternalStorage(false);
127
+
128
+        Filesystem::mount($extStorage, [], '/' . $this->user . '/files/ext');
129
+
130
+        $storageInfo = \OC_Helper::getStorageInfo('');
131
+        $this->assertEquals(12, $storageInfo['free']);
132
+        $this->assertEquals(5, $storageInfo['used']);
133
+        $this->assertEquals(17, $storageInfo['total']);
134
+    }
135
+
136
+    /**
137
+     * Test getting the storage info, including extra mount points
138
+     */
139
+    public function testGetStorageInfoIncludingExtStorage(): void {
140
+        $homeStorage = new Temporary([]);
141
+        Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
142
+        $homeStorage->file_put_contents('test.txt', '01234');
143
+
144
+        $extStorage = new Temporary([]);
145
+        $extStorage->file_put_contents('extfile.txt', 'abcdefghijklmnopq');
146
+        $extStorage->getScanner()->scan(''); // update root size
147
+
148
+        Filesystem::mount($extStorage, [], '/' . $this->user . '/files/ext');
149
+
150
+        $this->setIncludeExternalStorage(true);
151
+
152
+        $config = Server::get(IConfig::class);
153
+        $config->setUserValue($this->user, 'files', 'quota', '25');
154
+
155
+        $storageInfo = \OC_Helper::getStorageInfo('');
156
+        $this->assertEquals(3, $storageInfo['free']);
157
+        $this->assertEquals(22, $storageInfo['used']);
158
+        $this->assertEquals(25, $storageInfo['total']);
159
+
160
+        $config->setUserValue($this->user, 'files', 'quota', 'default');
161
+    }
162
+
163
+    /**
164
+     * Test getting the storage info excluding extra mount points
165
+     * when user has no quota set, even when quota ext storage option
166
+     * was set
167
+     */
168
+    public function testGetStorageInfoIncludingExtStorageWithNoUserQuota(): void {
169
+        $homeStorage = $this->getStorageMock(12);
170
+        Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
171
+        $homeStorage->file_put_contents('test.txt', '01234');
172
+
173
+        $extStorage = new Temporary([]);
174
+        $extStorage->file_put_contents('extfile.txt', 'abcdefghijklmnopq');
175
+        $extStorage->getScanner()->scan(''); // update root size
176
+
177
+        Filesystem::mount($extStorage, [], '/' . $this->user . '/files/ext');
178
+
179
+        $config = Server::get(IConfig::class);
180
+        $this->setIncludeExternalStorage(true);
181
+
182
+        $storageInfo = \OC_Helper::getStorageInfo('');
183
+        $this->assertEquals(12, $storageInfo['free'], '12 bytes free in home storage');
184
+        $this->assertEquals(22, $storageInfo['used'], '5 bytes of home storage and 17 bytes of the temporary storage are used');
185
+        $this->assertEquals(34, $storageInfo['total'], '5 bytes used and 12 bytes free in home storage as well as 17 bytes used in temporary storage');
186
+    }
187
+
188
+
189
+    /**
190
+     * Test getting the storage info with quota enabled
191
+     */
192
+    public function testGetStorageInfoWithQuota(): void {
193
+        $homeStorage = $this->getStorageMock(12);
194
+        $homeStorage->file_put_contents('test.txt', '01234');
195
+        $homeStorage = new Quota(
196
+            [
197
+                'storage' => $homeStorage,
198
+                'quota' => 7
199
+            ]
200
+        );
201
+        Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
202
+
203
+        $storageInfo = \OC_Helper::getStorageInfo('');
204
+        $this->assertEquals(2, $storageInfo['free']);
205
+        $this->assertEquals(5, $storageInfo['used']);
206
+        $this->assertEquals(7, $storageInfo['total']);
207
+    }
208
+
209
+    /**
210
+     * Test getting the storage info when data exceeds quota
211
+     */
212
+    public function testGetStorageInfoWhenSizeExceedsQuota(): void {
213
+        $homeStorage = $this->getStorageMock(12);
214
+        $homeStorage->file_put_contents('test.txt', '0123456789');
215
+        $homeStorage = new Quota(
216
+            [
217
+                'storage' => $homeStorage,
218
+                'quota' => 7
219
+            ]
220
+        );
221
+        Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
222
+
223
+        $storageInfo = \OC_Helper::getStorageInfo('');
224
+        $this->assertEquals(0, $storageInfo['free']);
225
+        $this->assertEquals(10, $storageInfo['used']);
226
+        // total = quota
227
+        $this->assertEquals(7, $storageInfo['total']);
228
+    }
229
+
230
+    /**
231
+     * Test getting the storage info when the remaining
232
+     * free storage space is less than the quota
233
+     */
234
+    public function testGetStorageInfoWhenFreeSpaceLessThanQuota(): void {
235
+        $homeStorage = $this->getStorageMock(12);
236
+        $homeStorage->file_put_contents('test.txt', '01234');
237
+        $homeStorage = new Quota(
238
+            [
239
+                'storage' => $homeStorage,
240
+                'quota' => 18
241
+            ]
242
+        );
243
+        Filesystem::mount($homeStorage, [], '/' . $this->user . '/files');
244
+
245
+        $storageInfo = \OC_Helper::getStorageInfo('');
246
+        $this->assertEquals(12, $storageInfo['free']);
247
+        $this->assertEquals(5, $storageInfo['used']);
248
+        // total = free + used (because quota > total)
249
+        $this->assertEquals(17, $storageInfo['total']);
250
+    }
251 251
 }
Please login to merge, or discard this patch.
tests/lib/TestCase.php 1 patch
Indentation   +594 added lines, -594 removed lines patch added patch discarded remove patch
@@ -41,602 +41,602 @@
 block discarded – undo
41 41
 use PHPUnit\Framework\Attributes\Group;
42 42
 
43 43
 if (version_compare(\PHPUnit\Runner\Version::id(), 10, '>=')) {
44
-	trait OnNotSuccessfulTestTrait {
45
-		protected function onNotSuccessfulTest(\Throwable $t): never {
46
-			$this->restoreAllServices();
47
-
48
-			// restore database connection
49
-			if (!$this->IsDatabaseAccessAllowed()) {
50
-				\OC::$server->registerService(IDBConnection::class, function () {
51
-					return self::$realDatabase;
52
-				});
53
-			}
54
-
55
-			parent::onNotSuccessfulTest($t);
56
-		}
57
-	}
44
+    trait OnNotSuccessfulTestTrait {
45
+        protected function onNotSuccessfulTest(\Throwable $t): never {
46
+            $this->restoreAllServices();
47
+
48
+            // restore database connection
49
+            if (!$this->IsDatabaseAccessAllowed()) {
50
+                \OC::$server->registerService(IDBConnection::class, function () {
51
+                    return self::$realDatabase;
52
+                });
53
+            }
54
+
55
+            parent::onNotSuccessfulTest($t);
56
+        }
57
+    }
58 58
 } else {
59
-	trait OnNotSuccessfulTestTrait {
60
-		protected function onNotSuccessfulTest(\Throwable $t): void {
61
-			$this->restoreAllServices();
62
-
63
-			// restore database connection
64
-			if (!$this->IsDatabaseAccessAllowed()) {
65
-				\OC::$server->registerService(IDBConnection::class, function () {
66
-					return self::$realDatabase;
67
-				});
68
-			}
69
-
70
-			parent::onNotSuccessfulTest($t);
71
-		}
72
-	}
59
+    trait OnNotSuccessfulTestTrait {
60
+        protected function onNotSuccessfulTest(\Throwable $t): void {
61
+            $this->restoreAllServices();
62
+
63
+            // restore database connection
64
+            if (!$this->IsDatabaseAccessAllowed()) {
65
+                \OC::$server->registerService(IDBConnection::class, function () {
66
+                    return self::$realDatabase;
67
+                });
68
+            }
69
+
70
+            parent::onNotSuccessfulTest($t);
71
+        }
72
+    }
73 73
 }
74 74
 
75 75
 abstract class TestCase extends \PHPUnit\Framework\TestCase {
76
-	/** @var \OC\Command\QueueBus */
77
-	private $commandBus;
78
-
79
-	/** @var IDBConnection */
80
-	protected static $realDatabase = null;
81
-
82
-	/** @var bool */
83
-	private static $wasDatabaseAllowed = false;
84
-
85
-	/** @var array */
86
-	protected $services = [];
87
-
88
-	use OnNotSuccessfulTestTrait;
89
-
90
-	/**
91
-	 * @param string $name
92
-	 * @param mixed $newService
93
-	 * @return bool
94
-	 */
95
-	public function overwriteService(string $name, $newService): bool {
96
-		if (isset($this->services[$name])) {
97
-			return false;
98
-		}
99
-
100
-		try {
101
-			$this->services[$name] = Server::get($name);
102
-		} catch (QueryException $e) {
103
-			$this->services[$name] = false;
104
-		}
105
-		$container = \OC::$server->getAppContainerForService($name);
106
-		$container = $container ?? \OC::$server;
107
-
108
-		$container->registerService($name, function () use ($newService) {
109
-			return $newService;
110
-		});
111
-
112
-		return true;
113
-	}
114
-
115
-	/**
116
-	 * @param string $name
117
-	 * @return bool
118
-	 */
119
-	public function restoreService(string $name): bool {
120
-		if (isset($this->services[$name])) {
121
-			$oldService = $this->services[$name];
122
-
123
-			$container = \OC::$server->getAppContainerForService($name);
124
-			$container = $container ?? \OC::$server;
125
-
126
-			if ($oldService !== false) {
127
-				$container->registerService($name, function () use ($oldService) {
128
-					return $oldService;
129
-				});
130
-			} else {
131
-				unset($container[$oldService]);
132
-			}
133
-
134
-
135
-			unset($this->services[$name]);
136
-			return true;
137
-		}
138
-
139
-		return false;
140
-	}
141
-
142
-	public function restoreAllServices() {
143
-		if (!empty($this->services)) {
144
-			if (!empty($this->services)) {
145
-				foreach ($this->services as $name => $service) {
146
-					$this->restoreService($name);
147
-				}
148
-			}
149
-		}
150
-	}
151
-
152
-	protected function getTestTraits() {
153
-		$traits = [];
154
-		$class = $this;
155
-		do {
156
-			$traits = array_merge(class_uses($class), $traits);
157
-		} while ($class = get_parent_class($class));
158
-		foreach ($traits as $trait => $same) {
159
-			$traits = array_merge(class_uses($trait), $traits);
160
-		}
161
-		$traits = array_unique($traits);
162
-		return array_filter($traits, function ($trait) {
163
-			return substr($trait, 0, 5) === 'Test\\';
164
-		});
165
-	}
166
-
167
-	protected function setUp(): void {
168
-		// overwrite the command bus with one we can run ourselves
169
-		$this->commandBus = new QueueBus();
170
-		$this->overwriteService('AsyncCommandBus', $this->commandBus);
171
-		$this->overwriteService(IBus::class, $this->commandBus);
172
-
173
-		// detect database access
174
-		self::$wasDatabaseAllowed = true;
175
-		if (!$this->IsDatabaseAccessAllowed()) {
176
-			self::$wasDatabaseAllowed = false;
177
-			if (is_null(self::$realDatabase)) {
178
-				self::$realDatabase = Server::get(IDBConnection::class);
179
-			}
180
-			\OC::$server->registerService(IDBConnection::class, function (): void {
181
-				$this->fail('Your test case is not allowed to access the database.');
182
-			});
183
-		}
184
-
185
-		$traits = $this->getTestTraits();
186
-		foreach ($traits as $trait) {
187
-			$methodName = 'setUp' . basename(str_replace('\\', '/', $trait));
188
-			if (method_exists($this, $methodName)) {
189
-				call_user_func([$this, $methodName]);
190
-			}
191
-		}
192
-	}
193
-
194
-	protected function tearDown(): void {
195
-		$this->restoreAllServices();
196
-
197
-		// restore database connection
198
-		if (!$this->IsDatabaseAccessAllowed()) {
199
-			\OC::$server->registerService(IDBConnection::class, function () {
200
-				return self::$realDatabase;
201
-			});
202
-		}
203
-
204
-		// further cleanup
205
-		$hookExceptions = \OC_Hook::$thrownExceptions;
206
-		\OC_Hook::$thrownExceptions = [];
207
-		Server::get(ILockingProvider::class)->releaseAll();
208
-		if (!empty($hookExceptions)) {
209
-			throw $hookExceptions[0];
210
-		}
211
-
212
-		// fail hard if xml errors have not been cleaned up
213
-		$errors = libxml_get_errors();
214
-		libxml_clear_errors();
215
-		if (!empty($errors)) {
216
-			self::assertEquals([], $errors, 'There have been xml parsing errors');
217
-		}
218
-
219
-		if ($this->IsDatabaseAccessAllowed()) {
220
-			Storage::getGlobalCache()->clearCache();
221
-		}
222
-
223
-		// tearDown the traits
224
-		$traits = $this->getTestTraits();
225
-		foreach ($traits as $trait) {
226
-			$methodName = 'tearDown' . basename(str_replace('\\', '/', $trait));
227
-			if (method_exists($this, $methodName)) {
228
-				call_user_func([$this, $methodName]);
229
-			}
230
-		}
231
-	}
232
-
233
-	/**
234
-	 * Allows us to test private methods/properties
235
-	 *
236
-	 * @param $object
237
-	 * @param $methodName
238
-	 * @param array $parameters
239
-	 * @return mixed
240
-	 */
241
-	protected static function invokePrivate($object, $methodName, array $parameters = []) {
242
-		if (is_string($object)) {
243
-			$className = $object;
244
-		} else {
245
-			$className = get_class($object);
246
-		}
247
-		$reflection = new \ReflectionClass($className);
248
-
249
-		if ($reflection->hasMethod($methodName)) {
250
-			$method = $reflection->getMethod($methodName);
251
-			return $method->invokeArgs($object, $parameters);
252
-		} elseif ($reflection->hasProperty($methodName)) {
253
-			$property = $reflection->getProperty($methodName);
254
-
255
-			if (!empty($parameters)) {
256
-				if ($property->isStatic()) {
257
-					$property->setValue(null, array_pop($parameters));
258
-				} else {
259
-					$property->setValue($object, array_pop($parameters));
260
-				}
261
-			}
262
-
263
-			if (is_object($object)) {
264
-				return $property->getValue($object);
265
-			}
266
-
267
-			return $property->getValue();
268
-		} elseif ($reflection->hasConstant($methodName)) {
269
-			return $reflection->getConstant($methodName);
270
-		}
271
-
272
-		return false;
273
-	}
274
-
275
-	/**
276
-	 * Returns a unique identifier as uniqid() is not reliable sometimes
277
-	 *
278
-	 * @param string $prefix
279
-	 * @param int $length
280
-	 * @return string
281
-	 */
282
-	protected static function getUniqueID($prefix = '', $length = 13) {
283
-		return $prefix . Server::get(ISecureRandom::class)->generate(
284
-			$length,
285
-			// Do not use dots and slashes as we use the value for file names
286
-			ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER
287
-		);
288
-	}
289
-
290
-	/**
291
-	 * Filter methods
292
-	 *
293
-	 * Returns all methods of the given class,
294
-	 * that are public or abstract and not in the ignoreMethods list,
295
-	 * to be able to fill onlyMethods() with an inverted list.
296
-	 *
297
-	 * @param string $className
298
-	 * @param string[] $filterMethods
299
-	 * @return string[]
300
-	 */
301
-	public function filterClassMethods(string $className, array $filterMethods): array {
302
-		$class = new \ReflectionClass($className);
303
-
304
-		$methods = [];
305
-		foreach ($class->getMethods() as $method) {
306
-			if (($method->isPublic() || $method->isAbstract()) && !in_array($method->getName(), $filterMethods, true)) {
307
-				$methods[] = $method->getName();
308
-			}
309
-		}
310
-
311
-		return $methods;
312
-	}
313
-
314
-	public static function tearDownAfterClass(): void {
315
-		if (!self::$wasDatabaseAllowed && self::$realDatabase !== null) {
316
-			// in case an error is thrown in a test, PHPUnit jumps straight to tearDownAfterClass,
317
-			// so we need the database again
318
-			\OC::$server->registerService(IDBConnection::class, function () {
319
-				return self::$realDatabase;
320
-			});
321
-		}
322
-		$dataDir = Server::get(IConfig::class)->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data-autotest');
323
-		if (self::$wasDatabaseAllowed && Server::get(IDBConnection::class)) {
324
-			$db = Server::get(IDBConnection::class);
325
-			if ($db->inTransaction()) {
326
-				$db->rollBack();
327
-				throw new \Exception('There was a transaction still in progress and needed to be rolled back. Please fix this in your test.');
328
-			}
329
-			$queryBuilder = $db->getQueryBuilder();
330
-
331
-			self::tearDownAfterClassCleanShares($queryBuilder);
332
-			self::tearDownAfterClassCleanStorages($queryBuilder);
333
-			self::tearDownAfterClassCleanFileCache($queryBuilder);
334
-		}
335
-		self::tearDownAfterClassCleanStrayDataFiles($dataDir);
336
-		self::tearDownAfterClassCleanStrayHooks();
337
-		self::tearDownAfterClassCleanStrayLocks();
338
-
339
-		// Ensure we start with fresh instances of some classes to reduce side-effects between tests
340
-		unset(\OC::$server[Factory::class]);
341
-		unset(\OC::$server[AppFetcher::class]);
342
-		unset(\OC::$server[Installer::class]);
343
-		unset(\OC::$server[Updater::class]);
344
-
345
-		/** @var SetupManager $setupManager */
346
-		$setupManager = Server::get(SetupManager::class);
347
-		$setupManager->tearDown();
348
-
349
-		/** @var MountProviderCollection $mountProviderCollection */
350
-		$mountProviderCollection = Server::get(MountProviderCollection::class);
351
-		$mountProviderCollection->clearProviders();
352
-
353
-		/** @var IConfig $config */
354
-		$config = Server::get(IConfig::class);
355
-		$mountProviderCollection->registerProvider(new CacheMountProvider($config));
356
-		$mountProviderCollection->registerHomeProvider(new LocalHomeMountProvider());
357
-		$objectStoreConfig = Server::get(PrimaryObjectStoreConfig::class);
358
-		$mountProviderCollection->registerRootProvider(new RootMountProvider($objectStoreConfig, $config));
359
-
360
-		$setupManager->setupRoot();
361
-
362
-		parent::tearDownAfterClass();
363
-	}
364
-
365
-	/**
366
-	 * Remove all entries from the share table
367
-	 *
368
-	 * @param IQueryBuilder $queryBuilder
369
-	 */
370
-	protected static function tearDownAfterClassCleanShares(IQueryBuilder $queryBuilder) {
371
-		$queryBuilder->delete('share')
372
-			->executeStatement();
373
-	}
374
-
375
-	/**
376
-	 * Remove all entries from the storages table
377
-	 *
378
-	 * @param IQueryBuilder $queryBuilder
379
-	 */
380
-	protected static function tearDownAfterClassCleanStorages(IQueryBuilder $queryBuilder) {
381
-		$queryBuilder->delete('storages')
382
-			->executeStatement();
383
-	}
384
-
385
-	/**
386
-	 * Remove all entries from the filecache table
387
-	 *
388
-	 * @param IQueryBuilder $queryBuilder
389
-	 */
390
-	protected static function tearDownAfterClassCleanFileCache(IQueryBuilder $queryBuilder) {
391
-		$queryBuilder->delete('filecache')
392
-			->runAcrossAllShards()
393
-			->executeStatement();
394
-	}
395
-
396
-	/**
397
-	 * Remove all unused files from the data dir
398
-	 *
399
-	 * @param string $dataDir
400
-	 */
401
-	protected static function tearDownAfterClassCleanStrayDataFiles($dataDir) {
402
-		$knownEntries = [
403
-			'nextcloud.log' => true,
404
-			'audit.log' => true,
405
-			'owncloud.db' => true,
406
-			'.ocdata' => true,
407
-			'..' => true,
408
-			'.' => true,
409
-		];
410
-
411
-		if ($dh = opendir($dataDir)) {
412
-			while (($file = readdir($dh)) !== false) {
413
-				if (!isset($knownEntries[$file])) {
414
-					self::tearDownAfterClassCleanStrayDataUnlinkDir($dataDir . '/' . $file);
415
-				}
416
-			}
417
-			closedir($dh);
418
-		}
419
-	}
420
-
421
-	/**
422
-	 * Recursive delete files and folders from a given directory
423
-	 *
424
-	 * @param string $dir
425
-	 */
426
-	protected static function tearDownAfterClassCleanStrayDataUnlinkDir($dir) {
427
-		if ($dh = @opendir($dir)) {
428
-			while (($file = readdir($dh)) !== false) {
429
-				if (Filesystem::isIgnoredDir($file)) {
430
-					continue;
431
-				}
432
-				$path = $dir . '/' . $file;
433
-				if (is_dir($path)) {
434
-					self::tearDownAfterClassCleanStrayDataUnlinkDir($path);
435
-				} else {
436
-					@unlink($path);
437
-				}
438
-			}
439
-			closedir($dh);
440
-		}
441
-		@rmdir($dir);
442
-	}
443
-
444
-	/**
445
-	 * Clean up the list of hooks
446
-	 */
447
-	protected static function tearDownAfterClassCleanStrayHooks() {
448
-		\OC_Hook::clear();
449
-	}
450
-
451
-	/**
452
-	 * Clean up the list of locks
453
-	 */
454
-	protected static function tearDownAfterClassCleanStrayLocks() {
455
-		Server::get(ILockingProvider::class)->releaseAll();
456
-	}
457
-
458
-	/**
459
-	 * Login and setup FS as a given user,
460
-	 * sets the given user as the current user.
461
-	 *
462
-	 * @param string $user user id or empty for a generic FS
463
-	 */
464
-	protected static function loginAsUser($user = '') {
465
-		self::logout();
466
-		Filesystem::tearDown();
467
-		\OC_User::setUserId($user);
468
-		$userObject = Server::get(IUserManager::class)->get($user);
469
-		if (!is_null($userObject)) {
470
-			$userObject->updateLastLoginTimestamp();
471
-		}
472
-		\OC_Util::setupFS($user);
473
-		if (Server::get(IUserManager::class)->userExists($user)) {
474
-			\OC::$server->getUserFolder($user);
475
-		}
476
-	}
477
-
478
-	/**
479
-	 * Logout the current user and tear down the filesystem.
480
-	 */
481
-	protected static function logout() {
482
-		\OC_Util::tearDownFS();
483
-		\OC_User::setUserId('');
484
-		// needed for fully logout
485
-		Server::get(IUserSession::class)->setUser(null);
486
-	}
487
-
488
-	/**
489
-	 * Run all commands pushed to the bus
490
-	 */
491
-	protected function runCommands() {
492
-		// get the user for which the fs is setup
493
-		$view = Filesystem::getView();
494
-		if ($view) {
495
-			[, $user] = explode('/', $view->getRoot());
496
-		} else {
497
-			$user = null;
498
-		}
499
-
500
-		\OC_Util::tearDownFS(); // command can't reply on the fs being setup
501
-		$this->commandBus->run();
502
-		\OC_Util::tearDownFS();
503
-
504
-		if ($user) {
505
-			\OC_Util::setupFS($user);
506
-		}
507
-	}
508
-
509
-	/**
510
-	 * Check if the given path is locked with a given type
511
-	 *
512
-	 * @param View $view view
513
-	 * @param string $path path to check
514
-	 * @param int $type lock type
515
-	 * @param bool $onMountPoint true to check the mount point instead of the
516
-	 *                           mounted storage
517
-	 *
518
-	 * @return boolean true if the file is locked with the
519
-	 *                 given type, false otherwise
520
-	 */
521
-	protected function isFileLocked($view, $path, $type, $onMountPoint = false) {
522
-		// Note: this seems convoluted but is necessary because
523
-		// the format of the lock key depends on the storage implementation
524
-		// (in our case mostly md5)
525
-
526
-		if ($type === ILockingProvider::LOCK_SHARED) {
527
-			// to check if the file has a shared lock, try acquiring an exclusive lock
528
-			$checkType = ILockingProvider::LOCK_EXCLUSIVE;
529
-		} else {
530
-			// a shared lock cannot be set if exclusive lock is in place
531
-			$checkType = ILockingProvider::LOCK_SHARED;
532
-		}
533
-		try {
534
-			$view->lockFile($path, $checkType, $onMountPoint);
535
-			// no exception, which means the lock of $type is not set
536
-			// clean up
537
-			$view->unlockFile($path, $checkType, $onMountPoint);
538
-			return false;
539
-		} catch (LockedException $e) {
540
-			// we could not acquire the counter-lock, which means
541
-			// the lock of $type was in place
542
-			return true;
543
-		}
544
-	}
545
-
546
-	protected function getGroupAnnotations(): array {
547
-		if (method_exists($this, 'getAnnotations')) {
548
-			$annotations = $this->getAnnotations();
549
-			return $annotations['class']['group'] ?? [];
550
-		}
551
-
552
-		$r = new \ReflectionClass($this);
553
-		$doc = $r->getDocComment();
554
-
555
-		if (class_exists(Group::class)) {
556
-			$attributes = array_map(function (\ReflectionAttribute $attribute) {
557
-				/** @var Group $group */
558
-				$group = $attribute->newInstance();
559
-				return $group->name();
560
-			}, $r->getAttributes(Group::class));
561
-			if (count($attributes) > 0) {
562
-				return $attributes;
563
-			}
564
-		}
565
-		preg_match_all('#@group\s+(.*?)\n#s', $doc, $annotations);
566
-		return $annotations[1] ?? [];
567
-	}
568
-
569
-	protected function IsDatabaseAccessAllowed(): bool {
570
-		$annotations = $this->getGroupAnnotations();
571
-		if (isset($annotations)) {
572
-			if (in_array('DB', $annotations) || in_array('SLOWDB', $annotations)) {
573
-				return true;
574
-			}
575
-		}
576
-
577
-		return false;
578
-	}
579
-
580
-	/**
581
-	 * @param string $expectedHtml
582
-	 * @param string $template
583
-	 * @param array $vars
584
-	 */
585
-	protected function assertTemplate($expectedHtml, $template, $vars = []) {
586
-		$requestToken = 12345;
587
-		/** @var Defaults|\PHPUnit\Framework\MockObject\MockObject $l10n */
588
-		$theme = $this->getMockBuilder('\OCP\Defaults')
589
-			->disableOriginalConstructor()->getMock();
590
-		$theme->expects($this->any())
591
-			->method('getName')
592
-			->willReturn('Nextcloud');
593
-		/** @var IL10N|\PHPUnit\Framework\MockObject\MockObject $l10n */
594
-		$l10n = $this->getMockBuilder(IL10N::class)
595
-			->disableOriginalConstructor()->getMock();
596
-		$l10n
597
-			->expects($this->any())
598
-			->method('t')
599
-			->willReturnCallback(function ($text, $parameters = []) {
600
-				return vsprintf($text, $parameters);
601
-			});
602
-
603
-		$t = new Base($template, $requestToken, $l10n, $theme);
604
-		$buf = $t->fetchPage($vars);
605
-		$this->assertHtmlStringEqualsHtmlString($expectedHtml, $buf);
606
-	}
607
-
608
-	/**
609
-	 * @param string $expectedHtml
610
-	 * @param string $actualHtml
611
-	 * @param string $message
612
-	 */
613
-	protected function assertHtmlStringEqualsHtmlString($expectedHtml, $actualHtml, $message = '') {
614
-		$expected = new DOMDocument();
615
-		$expected->preserveWhiteSpace = false;
616
-		$expected->formatOutput = true;
617
-		$expected->loadHTML($expectedHtml);
618
-
619
-		$actual = new DOMDocument();
620
-		$actual->preserveWhiteSpace = false;
621
-		$actual->formatOutput = true;
622
-		$actual->loadHTML($actualHtml);
623
-		$this->removeWhitespaces($actual);
624
-
625
-		$expectedHtml1 = $expected->saveHTML();
626
-		$actualHtml1 = $actual->saveHTML();
627
-		self::assertEquals($expectedHtml1, $actualHtml1, $message);
628
-	}
629
-
630
-
631
-	private function removeWhitespaces(DOMNode $domNode) {
632
-		foreach ($domNode->childNodes as $node) {
633
-			if ($node->hasChildNodes()) {
634
-				$this->removeWhitespaces($node);
635
-			} else {
636
-				if ($node instanceof \DOMText && $node->isWhitespaceInElementContent()) {
637
-					$domNode->removeChild($node);
638
-				}
639
-			}
640
-		}
641
-	}
76
+    /** @var \OC\Command\QueueBus */
77
+    private $commandBus;
78
+
79
+    /** @var IDBConnection */
80
+    protected static $realDatabase = null;
81
+
82
+    /** @var bool */
83
+    private static $wasDatabaseAllowed = false;
84
+
85
+    /** @var array */
86
+    protected $services = [];
87
+
88
+    use OnNotSuccessfulTestTrait;
89
+
90
+    /**
91
+     * @param string $name
92
+     * @param mixed $newService
93
+     * @return bool
94
+     */
95
+    public function overwriteService(string $name, $newService): bool {
96
+        if (isset($this->services[$name])) {
97
+            return false;
98
+        }
99
+
100
+        try {
101
+            $this->services[$name] = Server::get($name);
102
+        } catch (QueryException $e) {
103
+            $this->services[$name] = false;
104
+        }
105
+        $container = \OC::$server->getAppContainerForService($name);
106
+        $container = $container ?? \OC::$server;
107
+
108
+        $container->registerService($name, function () use ($newService) {
109
+            return $newService;
110
+        });
111
+
112
+        return true;
113
+    }
114
+
115
+    /**
116
+     * @param string $name
117
+     * @return bool
118
+     */
119
+    public function restoreService(string $name): bool {
120
+        if (isset($this->services[$name])) {
121
+            $oldService = $this->services[$name];
122
+
123
+            $container = \OC::$server->getAppContainerForService($name);
124
+            $container = $container ?? \OC::$server;
125
+
126
+            if ($oldService !== false) {
127
+                $container->registerService($name, function () use ($oldService) {
128
+                    return $oldService;
129
+                });
130
+            } else {
131
+                unset($container[$oldService]);
132
+            }
133
+
134
+
135
+            unset($this->services[$name]);
136
+            return true;
137
+        }
138
+
139
+        return false;
140
+    }
141
+
142
+    public function restoreAllServices() {
143
+        if (!empty($this->services)) {
144
+            if (!empty($this->services)) {
145
+                foreach ($this->services as $name => $service) {
146
+                    $this->restoreService($name);
147
+                }
148
+            }
149
+        }
150
+    }
151
+
152
+    protected function getTestTraits() {
153
+        $traits = [];
154
+        $class = $this;
155
+        do {
156
+            $traits = array_merge(class_uses($class), $traits);
157
+        } while ($class = get_parent_class($class));
158
+        foreach ($traits as $trait => $same) {
159
+            $traits = array_merge(class_uses($trait), $traits);
160
+        }
161
+        $traits = array_unique($traits);
162
+        return array_filter($traits, function ($trait) {
163
+            return substr($trait, 0, 5) === 'Test\\';
164
+        });
165
+    }
166
+
167
+    protected function setUp(): void {
168
+        // overwrite the command bus with one we can run ourselves
169
+        $this->commandBus = new QueueBus();
170
+        $this->overwriteService('AsyncCommandBus', $this->commandBus);
171
+        $this->overwriteService(IBus::class, $this->commandBus);
172
+
173
+        // detect database access
174
+        self::$wasDatabaseAllowed = true;
175
+        if (!$this->IsDatabaseAccessAllowed()) {
176
+            self::$wasDatabaseAllowed = false;
177
+            if (is_null(self::$realDatabase)) {
178
+                self::$realDatabase = Server::get(IDBConnection::class);
179
+            }
180
+            \OC::$server->registerService(IDBConnection::class, function (): void {
181
+                $this->fail('Your test case is not allowed to access the database.');
182
+            });
183
+        }
184
+
185
+        $traits = $this->getTestTraits();
186
+        foreach ($traits as $trait) {
187
+            $methodName = 'setUp' . basename(str_replace('\\', '/', $trait));
188
+            if (method_exists($this, $methodName)) {
189
+                call_user_func([$this, $methodName]);
190
+            }
191
+        }
192
+    }
193
+
194
+    protected function tearDown(): void {
195
+        $this->restoreAllServices();
196
+
197
+        // restore database connection
198
+        if (!$this->IsDatabaseAccessAllowed()) {
199
+            \OC::$server->registerService(IDBConnection::class, function () {
200
+                return self::$realDatabase;
201
+            });
202
+        }
203
+
204
+        // further cleanup
205
+        $hookExceptions = \OC_Hook::$thrownExceptions;
206
+        \OC_Hook::$thrownExceptions = [];
207
+        Server::get(ILockingProvider::class)->releaseAll();
208
+        if (!empty($hookExceptions)) {
209
+            throw $hookExceptions[0];
210
+        }
211
+
212
+        // fail hard if xml errors have not been cleaned up
213
+        $errors = libxml_get_errors();
214
+        libxml_clear_errors();
215
+        if (!empty($errors)) {
216
+            self::assertEquals([], $errors, 'There have been xml parsing errors');
217
+        }
218
+
219
+        if ($this->IsDatabaseAccessAllowed()) {
220
+            Storage::getGlobalCache()->clearCache();
221
+        }
222
+
223
+        // tearDown the traits
224
+        $traits = $this->getTestTraits();
225
+        foreach ($traits as $trait) {
226
+            $methodName = 'tearDown' . basename(str_replace('\\', '/', $trait));
227
+            if (method_exists($this, $methodName)) {
228
+                call_user_func([$this, $methodName]);
229
+            }
230
+        }
231
+    }
232
+
233
+    /**
234
+     * Allows us to test private methods/properties
235
+     *
236
+     * @param $object
237
+     * @param $methodName
238
+     * @param array $parameters
239
+     * @return mixed
240
+     */
241
+    protected static function invokePrivate($object, $methodName, array $parameters = []) {
242
+        if (is_string($object)) {
243
+            $className = $object;
244
+        } else {
245
+            $className = get_class($object);
246
+        }
247
+        $reflection = new \ReflectionClass($className);
248
+
249
+        if ($reflection->hasMethod($methodName)) {
250
+            $method = $reflection->getMethod($methodName);
251
+            return $method->invokeArgs($object, $parameters);
252
+        } elseif ($reflection->hasProperty($methodName)) {
253
+            $property = $reflection->getProperty($methodName);
254
+
255
+            if (!empty($parameters)) {
256
+                if ($property->isStatic()) {
257
+                    $property->setValue(null, array_pop($parameters));
258
+                } else {
259
+                    $property->setValue($object, array_pop($parameters));
260
+                }
261
+            }
262
+
263
+            if (is_object($object)) {
264
+                return $property->getValue($object);
265
+            }
266
+
267
+            return $property->getValue();
268
+        } elseif ($reflection->hasConstant($methodName)) {
269
+            return $reflection->getConstant($methodName);
270
+        }
271
+
272
+        return false;
273
+    }
274
+
275
+    /**
276
+     * Returns a unique identifier as uniqid() is not reliable sometimes
277
+     *
278
+     * @param string $prefix
279
+     * @param int $length
280
+     * @return string
281
+     */
282
+    protected static function getUniqueID($prefix = '', $length = 13) {
283
+        return $prefix . Server::get(ISecureRandom::class)->generate(
284
+            $length,
285
+            // Do not use dots and slashes as we use the value for file names
286
+            ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER
287
+        );
288
+    }
289
+
290
+    /**
291
+     * Filter methods
292
+     *
293
+     * Returns all methods of the given class,
294
+     * that are public or abstract and not in the ignoreMethods list,
295
+     * to be able to fill onlyMethods() with an inverted list.
296
+     *
297
+     * @param string $className
298
+     * @param string[] $filterMethods
299
+     * @return string[]
300
+     */
301
+    public function filterClassMethods(string $className, array $filterMethods): array {
302
+        $class = new \ReflectionClass($className);
303
+
304
+        $methods = [];
305
+        foreach ($class->getMethods() as $method) {
306
+            if (($method->isPublic() || $method->isAbstract()) && !in_array($method->getName(), $filterMethods, true)) {
307
+                $methods[] = $method->getName();
308
+            }
309
+        }
310
+
311
+        return $methods;
312
+    }
313
+
314
+    public static function tearDownAfterClass(): void {
315
+        if (!self::$wasDatabaseAllowed && self::$realDatabase !== null) {
316
+            // in case an error is thrown in a test, PHPUnit jumps straight to tearDownAfterClass,
317
+            // so we need the database again
318
+            \OC::$server->registerService(IDBConnection::class, function () {
319
+                return self::$realDatabase;
320
+            });
321
+        }
322
+        $dataDir = Server::get(IConfig::class)->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data-autotest');
323
+        if (self::$wasDatabaseAllowed && Server::get(IDBConnection::class)) {
324
+            $db = Server::get(IDBConnection::class);
325
+            if ($db->inTransaction()) {
326
+                $db->rollBack();
327
+                throw new \Exception('There was a transaction still in progress and needed to be rolled back. Please fix this in your test.');
328
+            }
329
+            $queryBuilder = $db->getQueryBuilder();
330
+
331
+            self::tearDownAfterClassCleanShares($queryBuilder);
332
+            self::tearDownAfterClassCleanStorages($queryBuilder);
333
+            self::tearDownAfterClassCleanFileCache($queryBuilder);
334
+        }
335
+        self::tearDownAfterClassCleanStrayDataFiles($dataDir);
336
+        self::tearDownAfterClassCleanStrayHooks();
337
+        self::tearDownAfterClassCleanStrayLocks();
338
+
339
+        // Ensure we start with fresh instances of some classes to reduce side-effects between tests
340
+        unset(\OC::$server[Factory::class]);
341
+        unset(\OC::$server[AppFetcher::class]);
342
+        unset(\OC::$server[Installer::class]);
343
+        unset(\OC::$server[Updater::class]);
344
+
345
+        /** @var SetupManager $setupManager */
346
+        $setupManager = Server::get(SetupManager::class);
347
+        $setupManager->tearDown();
348
+
349
+        /** @var MountProviderCollection $mountProviderCollection */
350
+        $mountProviderCollection = Server::get(MountProviderCollection::class);
351
+        $mountProviderCollection->clearProviders();
352
+
353
+        /** @var IConfig $config */
354
+        $config = Server::get(IConfig::class);
355
+        $mountProviderCollection->registerProvider(new CacheMountProvider($config));
356
+        $mountProviderCollection->registerHomeProvider(new LocalHomeMountProvider());
357
+        $objectStoreConfig = Server::get(PrimaryObjectStoreConfig::class);
358
+        $mountProviderCollection->registerRootProvider(new RootMountProvider($objectStoreConfig, $config));
359
+
360
+        $setupManager->setupRoot();
361
+
362
+        parent::tearDownAfterClass();
363
+    }
364
+
365
+    /**
366
+     * Remove all entries from the share table
367
+     *
368
+     * @param IQueryBuilder $queryBuilder
369
+     */
370
+    protected static function tearDownAfterClassCleanShares(IQueryBuilder $queryBuilder) {
371
+        $queryBuilder->delete('share')
372
+            ->executeStatement();
373
+    }
374
+
375
+    /**
376
+     * Remove all entries from the storages table
377
+     *
378
+     * @param IQueryBuilder $queryBuilder
379
+     */
380
+    protected static function tearDownAfterClassCleanStorages(IQueryBuilder $queryBuilder) {
381
+        $queryBuilder->delete('storages')
382
+            ->executeStatement();
383
+    }
384
+
385
+    /**
386
+     * Remove all entries from the filecache table
387
+     *
388
+     * @param IQueryBuilder $queryBuilder
389
+     */
390
+    protected static function tearDownAfterClassCleanFileCache(IQueryBuilder $queryBuilder) {
391
+        $queryBuilder->delete('filecache')
392
+            ->runAcrossAllShards()
393
+            ->executeStatement();
394
+    }
395
+
396
+    /**
397
+     * Remove all unused files from the data dir
398
+     *
399
+     * @param string $dataDir
400
+     */
401
+    protected static function tearDownAfterClassCleanStrayDataFiles($dataDir) {
402
+        $knownEntries = [
403
+            'nextcloud.log' => true,
404
+            'audit.log' => true,
405
+            'owncloud.db' => true,
406
+            '.ocdata' => true,
407
+            '..' => true,
408
+            '.' => true,
409
+        ];
410
+
411
+        if ($dh = opendir($dataDir)) {
412
+            while (($file = readdir($dh)) !== false) {
413
+                if (!isset($knownEntries[$file])) {
414
+                    self::tearDownAfterClassCleanStrayDataUnlinkDir($dataDir . '/' . $file);
415
+                }
416
+            }
417
+            closedir($dh);
418
+        }
419
+    }
420
+
421
+    /**
422
+     * Recursive delete files and folders from a given directory
423
+     *
424
+     * @param string $dir
425
+     */
426
+    protected static function tearDownAfterClassCleanStrayDataUnlinkDir($dir) {
427
+        if ($dh = @opendir($dir)) {
428
+            while (($file = readdir($dh)) !== false) {
429
+                if (Filesystem::isIgnoredDir($file)) {
430
+                    continue;
431
+                }
432
+                $path = $dir . '/' . $file;
433
+                if (is_dir($path)) {
434
+                    self::tearDownAfterClassCleanStrayDataUnlinkDir($path);
435
+                } else {
436
+                    @unlink($path);
437
+                }
438
+            }
439
+            closedir($dh);
440
+        }
441
+        @rmdir($dir);
442
+    }
443
+
444
+    /**
445
+     * Clean up the list of hooks
446
+     */
447
+    protected static function tearDownAfterClassCleanStrayHooks() {
448
+        \OC_Hook::clear();
449
+    }
450
+
451
+    /**
452
+     * Clean up the list of locks
453
+     */
454
+    protected static function tearDownAfterClassCleanStrayLocks() {
455
+        Server::get(ILockingProvider::class)->releaseAll();
456
+    }
457
+
458
+    /**
459
+     * Login and setup FS as a given user,
460
+     * sets the given user as the current user.
461
+     *
462
+     * @param string $user user id or empty for a generic FS
463
+     */
464
+    protected static function loginAsUser($user = '') {
465
+        self::logout();
466
+        Filesystem::tearDown();
467
+        \OC_User::setUserId($user);
468
+        $userObject = Server::get(IUserManager::class)->get($user);
469
+        if (!is_null($userObject)) {
470
+            $userObject->updateLastLoginTimestamp();
471
+        }
472
+        \OC_Util::setupFS($user);
473
+        if (Server::get(IUserManager::class)->userExists($user)) {
474
+            \OC::$server->getUserFolder($user);
475
+        }
476
+    }
477
+
478
+    /**
479
+     * Logout the current user and tear down the filesystem.
480
+     */
481
+    protected static function logout() {
482
+        \OC_Util::tearDownFS();
483
+        \OC_User::setUserId('');
484
+        // needed for fully logout
485
+        Server::get(IUserSession::class)->setUser(null);
486
+    }
487
+
488
+    /**
489
+     * Run all commands pushed to the bus
490
+     */
491
+    protected function runCommands() {
492
+        // get the user for which the fs is setup
493
+        $view = Filesystem::getView();
494
+        if ($view) {
495
+            [, $user] = explode('/', $view->getRoot());
496
+        } else {
497
+            $user = null;
498
+        }
499
+
500
+        \OC_Util::tearDownFS(); // command can't reply on the fs being setup
501
+        $this->commandBus->run();
502
+        \OC_Util::tearDownFS();
503
+
504
+        if ($user) {
505
+            \OC_Util::setupFS($user);
506
+        }
507
+    }
508
+
509
+    /**
510
+     * Check if the given path is locked with a given type
511
+     *
512
+     * @param View $view view
513
+     * @param string $path path to check
514
+     * @param int $type lock type
515
+     * @param bool $onMountPoint true to check the mount point instead of the
516
+     *                           mounted storage
517
+     *
518
+     * @return boolean true if the file is locked with the
519
+     *                 given type, false otherwise
520
+     */
521
+    protected function isFileLocked($view, $path, $type, $onMountPoint = false) {
522
+        // Note: this seems convoluted but is necessary because
523
+        // the format of the lock key depends on the storage implementation
524
+        // (in our case mostly md5)
525
+
526
+        if ($type === ILockingProvider::LOCK_SHARED) {
527
+            // to check if the file has a shared lock, try acquiring an exclusive lock
528
+            $checkType = ILockingProvider::LOCK_EXCLUSIVE;
529
+        } else {
530
+            // a shared lock cannot be set if exclusive lock is in place
531
+            $checkType = ILockingProvider::LOCK_SHARED;
532
+        }
533
+        try {
534
+            $view->lockFile($path, $checkType, $onMountPoint);
535
+            // no exception, which means the lock of $type is not set
536
+            // clean up
537
+            $view->unlockFile($path, $checkType, $onMountPoint);
538
+            return false;
539
+        } catch (LockedException $e) {
540
+            // we could not acquire the counter-lock, which means
541
+            // the lock of $type was in place
542
+            return true;
543
+        }
544
+    }
545
+
546
+    protected function getGroupAnnotations(): array {
547
+        if (method_exists($this, 'getAnnotations')) {
548
+            $annotations = $this->getAnnotations();
549
+            return $annotations['class']['group'] ?? [];
550
+        }
551
+
552
+        $r = new \ReflectionClass($this);
553
+        $doc = $r->getDocComment();
554
+
555
+        if (class_exists(Group::class)) {
556
+            $attributes = array_map(function (\ReflectionAttribute $attribute) {
557
+                /** @var Group $group */
558
+                $group = $attribute->newInstance();
559
+                return $group->name();
560
+            }, $r->getAttributes(Group::class));
561
+            if (count($attributes) > 0) {
562
+                return $attributes;
563
+            }
564
+        }
565
+        preg_match_all('#@group\s+(.*?)\n#s', $doc, $annotations);
566
+        return $annotations[1] ?? [];
567
+    }
568
+
569
+    protected function IsDatabaseAccessAllowed(): bool {
570
+        $annotations = $this->getGroupAnnotations();
571
+        if (isset($annotations)) {
572
+            if (in_array('DB', $annotations) || in_array('SLOWDB', $annotations)) {
573
+                return true;
574
+            }
575
+        }
576
+
577
+        return false;
578
+    }
579
+
580
+    /**
581
+     * @param string $expectedHtml
582
+     * @param string $template
583
+     * @param array $vars
584
+     */
585
+    protected function assertTemplate($expectedHtml, $template, $vars = []) {
586
+        $requestToken = 12345;
587
+        /** @var Defaults|\PHPUnit\Framework\MockObject\MockObject $l10n */
588
+        $theme = $this->getMockBuilder('\OCP\Defaults')
589
+            ->disableOriginalConstructor()->getMock();
590
+        $theme->expects($this->any())
591
+            ->method('getName')
592
+            ->willReturn('Nextcloud');
593
+        /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject $l10n */
594
+        $l10n = $this->getMockBuilder(IL10N::class)
595
+            ->disableOriginalConstructor()->getMock();
596
+        $l10n
597
+            ->expects($this->any())
598
+            ->method('t')
599
+            ->willReturnCallback(function ($text, $parameters = []) {
600
+                return vsprintf($text, $parameters);
601
+            });
602
+
603
+        $t = new Base($template, $requestToken, $l10n, $theme);
604
+        $buf = $t->fetchPage($vars);
605
+        $this->assertHtmlStringEqualsHtmlString($expectedHtml, $buf);
606
+    }
607
+
608
+    /**
609
+     * @param string $expectedHtml
610
+     * @param string $actualHtml
611
+     * @param string $message
612
+     */
613
+    protected function assertHtmlStringEqualsHtmlString($expectedHtml, $actualHtml, $message = '') {
614
+        $expected = new DOMDocument();
615
+        $expected->preserveWhiteSpace = false;
616
+        $expected->formatOutput = true;
617
+        $expected->loadHTML($expectedHtml);
618
+
619
+        $actual = new DOMDocument();
620
+        $actual->preserveWhiteSpace = false;
621
+        $actual->formatOutput = true;
622
+        $actual->loadHTML($actualHtml);
623
+        $this->removeWhitespaces($actual);
624
+
625
+        $expectedHtml1 = $expected->saveHTML();
626
+        $actualHtml1 = $actual->saveHTML();
627
+        self::assertEquals($expectedHtml1, $actualHtml1, $message);
628
+    }
629
+
630
+
631
+    private function removeWhitespaces(DOMNode $domNode) {
632
+        foreach ($domNode->childNodes as $node) {
633
+            if ($node->hasChildNodes()) {
634
+                $this->removeWhitespaces($node);
635
+            } else {
636
+                if ($node instanceof \DOMText && $node->isWhitespaceInElementContent()) {
637
+                    $domNode->removeChild($node);
638
+                }
639
+            }
640
+        }
641
+    }
642 642
 }
Please login to merge, or discard this patch.