Completed
Push — master ( 445cce...52a994 )
by John
20:56
created
lib/private/PreviewManager.php 1 patch
Indentation   +414 added lines, -414 removed lines patch added patch discarded remove patch
@@ -34,418 +34,418 @@
 block discarded – undo
34 34
  * @psalm-import-type ProviderClosure from IPreview
35 35
  */
36 36
 class PreviewManager implements IPreview {
37
-	protected IConfig $config;
38
-	protected IRootFolder $rootFolder;
39
-	protected IEventDispatcher $eventDispatcher;
40
-	private ?Generator $generator = null;
41
-	private GeneratorHelper $helper;
42
-	protected bool $providerListDirty = false;
43
-	protected bool $registeredCoreProviders = false;
44
-	/**
45
-	 * @var array<string, list<ProviderClosure>> $providers
46
-	 */
47
-	protected array $providers = [];
48
-
49
-	/** @var array mime type => support status */
50
-	protected array $mimeTypeSupportMap = [];
51
-	/** @var ?list<class-string<IProviderV2>> $defaultProviders */
52
-	protected ?array $defaultProviders = null;
53
-	protected ?string $userId;
54
-	private Coordinator $bootstrapCoordinator;
55
-
56
-	/**
57
-	 * Hash map (without value) of loaded bootstrap providers
58
-	 * @psalm-var array<string, null>
59
-	 */
60
-	private array $loadedBootstrapProviders = [];
61
-	private ContainerInterface $container;
62
-	private IBinaryFinder $binaryFinder;
63
-	private IMagickSupport $imagickSupport;
64
-	private bool $enablePreviews;
65
-
66
-	public function __construct(
67
-		IConfig $config,
68
-		IRootFolder $rootFolder,
69
-		IEventDispatcher $eventDispatcher,
70
-		GeneratorHelper $helper,
71
-		?string $userId,
72
-		Coordinator $bootstrapCoordinator,
73
-		ContainerInterface $container,
74
-		IBinaryFinder $binaryFinder,
75
-		IMagickSupport $imagickSupport,
76
-	) {
77
-		$this->config = $config;
78
-		$this->rootFolder = $rootFolder;
79
-		$this->eventDispatcher = $eventDispatcher;
80
-		$this->helper = $helper;
81
-		$this->userId = $userId;
82
-		$this->bootstrapCoordinator = $bootstrapCoordinator;
83
-		$this->container = $container;
84
-		$this->binaryFinder = $binaryFinder;
85
-		$this->imagickSupport = $imagickSupport;
86
-		$this->enablePreviews = $config->getSystemValueBool('enable_previews', true);
87
-	}
88
-
89
-	/**
90
-	 * In order to improve lazy loading a closure can be registered which will be
91
-	 * called in case preview providers are actually requested
92
-	 *
93
-	 * @param string $mimeTypeRegex Regex with the mime types that are supported by this provider
94
-	 * @param ProviderClosure $callable
95
-	 */
96
-	public function registerProvider(string $mimeTypeRegex, Closure $callable): void {
97
-		if (!$this->enablePreviews) {
98
-			return;
99
-		}
100
-
101
-		if (!isset($this->providers[$mimeTypeRegex])) {
102
-			$this->providers[$mimeTypeRegex] = [];
103
-		}
104
-		$this->providers[$mimeTypeRegex][] = $callable;
105
-		$this->providerListDirty = true;
106
-	}
107
-
108
-	/**
109
-	 * Get all providers
110
-	 */
111
-	public function getProviders(): array {
112
-		if (!$this->enablePreviews) {
113
-			return [];
114
-		}
115
-
116
-		$this->registerCoreProviders();
117
-		$this->registerBootstrapProviders();
118
-		if ($this->providerListDirty) {
119
-			$keys = array_map('strlen', array_keys($this->providers));
120
-			array_multisort($keys, SORT_DESC, $this->providers);
121
-			$this->providerListDirty = false;
122
-		}
123
-
124
-		return $this->providers;
125
-	}
126
-
127
-	/**
128
-	 * Does the manager have any providers
129
-	 */
130
-	public function hasProviders(): bool {
131
-		$this->registerCoreProviders();
132
-		return !empty($this->providers);
133
-	}
134
-
135
-	private function getGenerator(): Generator {
136
-		if ($this->generator === null) {
137
-			$this->generator = new Generator(
138
-				$this->config,
139
-				$this,
140
-				new GeneratorHelper(),
141
-				$this->eventDispatcher,
142
-				$this->container->get(LoggerInterface::class),
143
-				$this->container->get(PreviewMapper::class),
144
-				$this->container->get(StorageFactory::class),
145
-				$this->container->get(IGenerator::class),
146
-			);
147
-		}
148
-		return $this->generator;
149
-	}
150
-
151
-	public function getPreview(
152
-		File $file,
153
-		int $width = -1,
154
-		int $height = -1,
155
-		bool $crop = false,
156
-		string $mode = IPreview::MODE_FILL,
157
-		?string $mimeType = null,
158
-		bool $cacheResult = true,
159
-	): ISimpleFile {
160
-		$this->throwIfPreviewsDisabled($file, $mimeType);
161
-		$previewConcurrency = $this->getGenerator()->getNumConcurrentPreviews('preview_concurrency_all');
162
-		$sem = Generator::guardWithSemaphore(Generator::SEMAPHORE_ID_ALL, $previewConcurrency);
163
-		try {
164
-			$preview = $this->getGenerator()->getPreview($file, $width, $height, $crop, $mode, $mimeType, $cacheResult);
165
-		} finally {
166
-			Generator::unguardWithSemaphore($sem);
167
-		}
168
-
169
-		return $preview;
170
-	}
171
-
172
-	/**
173
-	 * Generates previews of a file
174
-	 *
175
-	 * @param array $specifications
176
-	 * @return ISimpleFile the last preview that was generated
177
-	 * @throws NotFoundException
178
-	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
179
-	 * @since 19.0.0
180
-	 */
181
-	public function generatePreviews(File $file, array $specifications, ?string $mimeType = null): ISimpleFile {
182
-		$this->throwIfPreviewsDisabled($file, $mimeType);
183
-		return $this->getGenerator()->generatePreviews($file, $specifications, $mimeType);
184
-	}
185
-
186
-	public function isMimeSupported(string $mimeType = '*'): bool {
187
-		if (!$this->enablePreviews) {
188
-			return false;
189
-		}
190
-
191
-		if (isset($this->mimeTypeSupportMap[$mimeType])) {
192
-			return $this->mimeTypeSupportMap[$mimeType];
193
-		}
194
-
195
-		$this->registerCoreProviders();
196
-		$this->registerBootstrapProviders();
197
-		$providerMimeTypes = array_keys($this->providers);
198
-		foreach ($providerMimeTypes as $supportedMimeType) {
199
-			if (preg_match($supportedMimeType, $mimeType)) {
200
-				$this->mimeTypeSupportMap[$mimeType] = true;
201
-				return true;
202
-			}
203
-		}
204
-		$this->mimeTypeSupportMap[$mimeType] = false;
205
-		return false;
206
-	}
207
-
208
-	public function isAvailable(\OCP\Files\FileInfo $file, ?string $mimeType = null): bool {
209
-		if (!$this->enablePreviews) {
210
-			return false;
211
-		}
212
-
213
-		$fileMimeType = $mimeType ?? $file->getMimeType();
214
-
215
-		$this->registerCoreProviders();
216
-		if (!$this->isMimeSupported($fileMimeType)) {
217
-			return false;
218
-		}
219
-
220
-		$mount = $file->getMountPoint();
221
-		if ($mount && !$mount->getOption('previews', true)) {
222
-			return false;
223
-		}
224
-
225
-		foreach ($this->providers as $supportedMimeType => $providers) {
226
-			if (preg_match($supportedMimeType, $fileMimeType)) {
227
-				foreach ($providers as $providerClosure) {
228
-					$provider = $this->helper->getProvider($providerClosure);
229
-					if (!$provider) {
230
-						continue;
231
-					}
232
-					if ($provider->isAvailable($file)) {
233
-						return true;
234
-					}
235
-				}
236
-			}
237
-		}
238
-		return false;
239
-	}
240
-
241
-	/**
242
-	 * List of enabled default providers
243
-	 *
244
-	 * The following providers are enabled by default:
245
-	 *  - OC\Preview\BMP
246
-	 *  - OC\Preview\GIF
247
-	 *  - OC\Preview\JPEG
248
-	 *  - OC\Preview\MarkDown
249
-	 *  - OC\Preview\PNG
250
-	 *  - OC\Preview\TXT
251
-	 *  - OC\Preview\XBitmap
252
-	 *
253
-	 * The following providers are disabled by default due to performance or privacy concerns:
254
-	 *  - OC\Preview\Font
255
-	 *  - OC\Preview\HEIC
256
-	 *  - OC\Preview\Illustrator
257
-	 *  - OC\Preview\MP3
258
-	 *  - OC\Preview\MSOffice2003
259
-	 *  - OC\Preview\MSOffice2007
260
-	 *  - OC\Preview\MSOfficeDoc
261
-	 *  - OC\Preview\Movie
262
-	 *  - OC\Preview\OpenDocument
263
-	 *  - OC\Preview\PDF
264
-	 *  - OC\Preview\Photoshop
265
-	 *  - OC\Preview\Postscript
266
-	 *  - OC\Preview\SVG
267
-	 *  - OC\Preview\StarOffice
268
-	 *  - OC\Preview\TIFF
269
-	 *
270
-	 * @return list<class-string<IProviderV2>>
271
-	 */
272
-	protected function getEnabledDefaultProvider(): array {
273
-		if ($this->defaultProviders !== null) {
274
-			return $this->defaultProviders;
275
-		}
276
-
277
-		$imageProviders = [
278
-			Preview\PNG::class,
279
-			Preview\JPEG::class,
280
-			Preview\GIF::class,
281
-			Preview\BMP::class,
282
-			Preview\XBitmap::class,
283
-			Preview\Krita::class,
284
-			Preview\WebP::class,
285
-		];
286
-
287
-		$this->defaultProviders = $this->config->getSystemValue('enabledPreviewProviders', array_merge([
288
-			Preview\MarkDown::class,
289
-			Preview\TXT::class,
290
-			Preview\OpenDocument::class,
291
-		], $imageProviders));
292
-
293
-		if (in_array(Preview\Image::class, $this->defaultProviders)) {
294
-			$this->defaultProviders = array_merge($this->defaultProviders, $imageProviders);
295
-		}
296
-		$this->defaultProviders = array_values(array_unique($this->defaultProviders));
297
-		/** @var list<class-string<IProviderV2>> $providers */
298
-		$providers = $this->defaultProviders;
299
-		return $providers;
300
-	}
301
-
302
-	/**
303
-	 * Register the default providers (if enabled)
304
-	 */
305
-	protected function registerCoreProvider(string $class, string $mimeType, array $options = []): void {
306
-		if (in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
307
-			$this->registerProvider($mimeType, function () use ($class, $options) {
308
-				return new $class($options);
309
-			});
310
-		}
311
-	}
312
-
313
-	/**
314
-	 * Register the default providers (if enabled)
315
-	 */
316
-	protected function registerCoreProviders(): void {
317
-		if ($this->registeredCoreProviders) {
318
-			return;
319
-		}
320
-		$this->registeredCoreProviders = true;
321
-
322
-		$this->registerCoreProvider(Preview\TXT::class, '/text\/plain/');
323
-		$this->registerCoreProvider(Preview\MarkDown::class, '/text\/(x-)?markdown/');
324
-		$this->registerCoreProvider(Preview\PNG::class, '/image\/png/');
325
-		$this->registerCoreProvider(Preview\JPEG::class, '/image\/jpeg/');
326
-		$this->registerCoreProvider(Preview\GIF::class, '/image\/gif/');
327
-		$this->registerCoreProvider(Preview\BMP::class, '/image\/bmp/');
328
-		$this->registerCoreProvider(Preview\XBitmap::class, '/image\/x-xbitmap/');
329
-		$this->registerCoreProvider(Preview\WebP::class, '/image\/webp/');
330
-		$this->registerCoreProvider(Preview\Krita::class, '/application\/x-krita/');
331
-		$this->registerCoreProvider(Preview\MP3::class, '/audio\/mpeg$/');
332
-		$this->registerCoreProvider(Preview\OpenDocument::class, '/application\/vnd.oasis.opendocument.*/');
333
-		$this->registerCoreProvider(Preview\Imaginary::class, Preview\Imaginary::supportedMimeTypes());
334
-		$this->registerCoreProvider(Preview\ImaginaryPDF::class, Preview\ImaginaryPDF::supportedMimeTypes());
335
-
336
-		// SVG and Bitmap require imagick
337
-		if ($this->imagickSupport->hasExtension()) {
338
-			$imagickProviders = [
339
-				'SVG' => ['mimetype' => '/image\/svg\+xml/', 'class' => Preview\SVG::class],
340
-				'TIFF' => ['mimetype' => '/image\/tiff/', 'class' => Preview\TIFF::class],
341
-				'PDF' => ['mimetype' => '/application\/pdf/', 'class' => Preview\PDF::class],
342
-				'AI' => ['mimetype' => '/application\/illustrator/', 'class' => Preview\Illustrator::class],
343
-				'PSD' => ['mimetype' => '/application\/x-photoshop/', 'class' => Preview\Photoshop::class],
344
-				'EPS' => ['mimetype' => '/application\/postscript/', 'class' => Preview\Postscript::class],
345
-				'TTF' => ['mimetype' => '/application\/(?:font-sfnt|x-font$)/', 'class' => Preview\Font::class],
346
-				'HEIC' => ['mimetype' => '/image\/(x-)?hei(f|c)/', 'class' => Preview\HEIC::class],
347
-				'TGA' => ['mimetype' => '/image\/(x-)?t(ar)?ga/', 'class' => Preview\TGA::class],
348
-				'SGI' => ['mimetype' => '/image\/(x-)?sgi/', 'class' => Preview\SGI::class],
349
-			];
350
-
351
-			foreach ($imagickProviders as $queryFormat => $provider) {
352
-				$class = $provider['class'];
353
-				if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
354
-					continue;
355
-				}
356
-
357
-				if ($this->imagickSupport->supportsFormat($queryFormat)) {
358
-					$this->registerCoreProvider($class, $provider['mimetype']);
359
-				}
360
-			}
361
-		}
362
-
363
-		$this->registerCoreProvidersOffice();
364
-
365
-		// Video requires ffmpeg
366
-		if (in_array(Preview\Movie::class, $this->getEnabledDefaultProvider())) {
367
-			$movieBinary = $this->config->getSystemValue('preview_ffmpeg_path', null);
368
-			if (!is_string($movieBinary)) {
369
-				$movieBinary = $this->binaryFinder->findBinaryPath('ffmpeg');
370
-			}
371
-
372
-
373
-			if (is_string($movieBinary)) {
374
-				$this->registerCoreProvider(Preview\Movie::class, '/video\/.*/', ['movieBinary' => $movieBinary]);
375
-			}
376
-		}
377
-	}
378
-
379
-	private function registerCoreProvidersOffice(): void {
380
-		$officeProviders = [
381
-			['mimetype' => '/application\/msword/', 'class' => Preview\MSOfficeDoc::class],
382
-			['mimetype' => '/application\/vnd.ms-.*/', 'class' => Preview\MSOffice2003::class],
383
-			['mimetype' => '/application\/vnd.openxmlformats-officedocument.*/', 'class' => Preview\MSOffice2007::class],
384
-			['mimetype' => '/application\/vnd.oasis.opendocument.*/', 'class' => Preview\OpenDocument::class],
385
-			['mimetype' => '/application\/vnd.sun.xml.*/', 'class' => Preview\StarOffice::class],
386
-			['mimetype' => '/image\/emf/', 'class' => Preview\EMF::class],
387
-		];
388
-
389
-		$findBinary = true;
390
-		$officeBinary = false;
391
-
392
-		foreach ($officeProviders as $provider) {
393
-			$class = $provider['class'];
394
-			if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
395
-				continue;
396
-			}
397
-
398
-			if ($findBinary) {
399
-				// Office requires openoffice or libreoffice
400
-				$officeBinary = $this->config->getSystemValue('preview_libreoffice_path', false);
401
-				if ($officeBinary === false) {
402
-					$officeBinary = $this->binaryFinder->findBinaryPath('libreoffice');
403
-				}
404
-				if ($officeBinary === false) {
405
-					$officeBinary = $this->binaryFinder->findBinaryPath('openoffice');
406
-				}
407
-				$findBinary = false;
408
-			}
409
-
410
-			if ($officeBinary) {
411
-				$this->registerCoreProvider($class, $provider['mimetype'], ['officeBinary' => $officeBinary]);
412
-			}
413
-		}
414
-	}
415
-
416
-	private function registerBootstrapProviders(): void {
417
-		$context = $this->bootstrapCoordinator->getRegistrationContext();
418
-
419
-		if ($context === null) {
420
-			// Just ignore for now
421
-			return;
422
-		}
423
-
424
-		$providers = $context->getPreviewProviders();
425
-		foreach ($providers as $provider) {
426
-			$key = $provider->getMimeTypeRegex() . '-' . $provider->getService();
427
-			if (array_key_exists($key, $this->loadedBootstrapProviders)) {
428
-				// Do not load the provider more than once
429
-				continue;
430
-			}
431
-			$this->loadedBootstrapProviders[$key] = null;
432
-
433
-			$this->registerProvider($provider->getMimeTypeRegex(), function () use ($provider): IProviderV2|false {
434
-				try {
435
-					return $this->container->get($provider->getService());
436
-				} catch (NotFoundExceptionInterface) {
437
-					return false;
438
-				}
439
-			});
440
-		}
441
-	}
442
-
443
-	/**
444
-	 * @throws NotFoundException if preview generation is disabled
445
-	 */
446
-	private function throwIfPreviewsDisabled(File $file, ?string $mimeType = null): void {
447
-		if (!$this->isAvailable($file, $mimeType)) {
448
-			throw new NotFoundException('Previews disabled');
449
-		}
450
-	}
37
+    protected IConfig $config;
38
+    protected IRootFolder $rootFolder;
39
+    protected IEventDispatcher $eventDispatcher;
40
+    private ?Generator $generator = null;
41
+    private GeneratorHelper $helper;
42
+    protected bool $providerListDirty = false;
43
+    protected bool $registeredCoreProviders = false;
44
+    /**
45
+     * @var array<string, list<ProviderClosure>> $providers
46
+     */
47
+    protected array $providers = [];
48
+
49
+    /** @var array mime type => support status */
50
+    protected array $mimeTypeSupportMap = [];
51
+    /** @var ?list<class-string<IProviderV2>> $defaultProviders */
52
+    protected ?array $defaultProviders = null;
53
+    protected ?string $userId;
54
+    private Coordinator $bootstrapCoordinator;
55
+
56
+    /**
57
+     * Hash map (without value) of loaded bootstrap providers
58
+     * @psalm-var array<string, null>
59
+     */
60
+    private array $loadedBootstrapProviders = [];
61
+    private ContainerInterface $container;
62
+    private IBinaryFinder $binaryFinder;
63
+    private IMagickSupport $imagickSupport;
64
+    private bool $enablePreviews;
65
+
66
+    public function __construct(
67
+        IConfig $config,
68
+        IRootFolder $rootFolder,
69
+        IEventDispatcher $eventDispatcher,
70
+        GeneratorHelper $helper,
71
+        ?string $userId,
72
+        Coordinator $bootstrapCoordinator,
73
+        ContainerInterface $container,
74
+        IBinaryFinder $binaryFinder,
75
+        IMagickSupport $imagickSupport,
76
+    ) {
77
+        $this->config = $config;
78
+        $this->rootFolder = $rootFolder;
79
+        $this->eventDispatcher = $eventDispatcher;
80
+        $this->helper = $helper;
81
+        $this->userId = $userId;
82
+        $this->bootstrapCoordinator = $bootstrapCoordinator;
83
+        $this->container = $container;
84
+        $this->binaryFinder = $binaryFinder;
85
+        $this->imagickSupport = $imagickSupport;
86
+        $this->enablePreviews = $config->getSystemValueBool('enable_previews', true);
87
+    }
88
+
89
+    /**
90
+     * In order to improve lazy loading a closure can be registered which will be
91
+     * called in case preview providers are actually requested
92
+     *
93
+     * @param string $mimeTypeRegex Regex with the mime types that are supported by this provider
94
+     * @param ProviderClosure $callable
95
+     */
96
+    public function registerProvider(string $mimeTypeRegex, Closure $callable): void {
97
+        if (!$this->enablePreviews) {
98
+            return;
99
+        }
100
+
101
+        if (!isset($this->providers[$mimeTypeRegex])) {
102
+            $this->providers[$mimeTypeRegex] = [];
103
+        }
104
+        $this->providers[$mimeTypeRegex][] = $callable;
105
+        $this->providerListDirty = true;
106
+    }
107
+
108
+    /**
109
+     * Get all providers
110
+     */
111
+    public function getProviders(): array {
112
+        if (!$this->enablePreviews) {
113
+            return [];
114
+        }
115
+
116
+        $this->registerCoreProviders();
117
+        $this->registerBootstrapProviders();
118
+        if ($this->providerListDirty) {
119
+            $keys = array_map('strlen', array_keys($this->providers));
120
+            array_multisort($keys, SORT_DESC, $this->providers);
121
+            $this->providerListDirty = false;
122
+        }
123
+
124
+        return $this->providers;
125
+    }
126
+
127
+    /**
128
+     * Does the manager have any providers
129
+     */
130
+    public function hasProviders(): bool {
131
+        $this->registerCoreProviders();
132
+        return !empty($this->providers);
133
+    }
134
+
135
+    private function getGenerator(): Generator {
136
+        if ($this->generator === null) {
137
+            $this->generator = new Generator(
138
+                $this->config,
139
+                $this,
140
+                new GeneratorHelper(),
141
+                $this->eventDispatcher,
142
+                $this->container->get(LoggerInterface::class),
143
+                $this->container->get(PreviewMapper::class),
144
+                $this->container->get(StorageFactory::class),
145
+                $this->container->get(IGenerator::class),
146
+            );
147
+        }
148
+        return $this->generator;
149
+    }
150
+
151
+    public function getPreview(
152
+        File $file,
153
+        int $width = -1,
154
+        int $height = -1,
155
+        bool $crop = false,
156
+        string $mode = IPreview::MODE_FILL,
157
+        ?string $mimeType = null,
158
+        bool $cacheResult = true,
159
+    ): ISimpleFile {
160
+        $this->throwIfPreviewsDisabled($file, $mimeType);
161
+        $previewConcurrency = $this->getGenerator()->getNumConcurrentPreviews('preview_concurrency_all');
162
+        $sem = Generator::guardWithSemaphore(Generator::SEMAPHORE_ID_ALL, $previewConcurrency);
163
+        try {
164
+            $preview = $this->getGenerator()->getPreview($file, $width, $height, $crop, $mode, $mimeType, $cacheResult);
165
+        } finally {
166
+            Generator::unguardWithSemaphore($sem);
167
+        }
168
+
169
+        return $preview;
170
+    }
171
+
172
+    /**
173
+     * Generates previews of a file
174
+     *
175
+     * @param array $specifications
176
+     * @return ISimpleFile the last preview that was generated
177
+     * @throws NotFoundException
178
+     * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
179
+     * @since 19.0.0
180
+     */
181
+    public function generatePreviews(File $file, array $specifications, ?string $mimeType = null): ISimpleFile {
182
+        $this->throwIfPreviewsDisabled($file, $mimeType);
183
+        return $this->getGenerator()->generatePreviews($file, $specifications, $mimeType);
184
+    }
185
+
186
+    public function isMimeSupported(string $mimeType = '*'): bool {
187
+        if (!$this->enablePreviews) {
188
+            return false;
189
+        }
190
+
191
+        if (isset($this->mimeTypeSupportMap[$mimeType])) {
192
+            return $this->mimeTypeSupportMap[$mimeType];
193
+        }
194
+
195
+        $this->registerCoreProviders();
196
+        $this->registerBootstrapProviders();
197
+        $providerMimeTypes = array_keys($this->providers);
198
+        foreach ($providerMimeTypes as $supportedMimeType) {
199
+            if (preg_match($supportedMimeType, $mimeType)) {
200
+                $this->mimeTypeSupportMap[$mimeType] = true;
201
+                return true;
202
+            }
203
+        }
204
+        $this->mimeTypeSupportMap[$mimeType] = false;
205
+        return false;
206
+    }
207
+
208
+    public function isAvailable(\OCP\Files\FileInfo $file, ?string $mimeType = null): bool {
209
+        if (!$this->enablePreviews) {
210
+            return false;
211
+        }
212
+
213
+        $fileMimeType = $mimeType ?? $file->getMimeType();
214
+
215
+        $this->registerCoreProviders();
216
+        if (!$this->isMimeSupported($fileMimeType)) {
217
+            return false;
218
+        }
219
+
220
+        $mount = $file->getMountPoint();
221
+        if ($mount && !$mount->getOption('previews', true)) {
222
+            return false;
223
+        }
224
+
225
+        foreach ($this->providers as $supportedMimeType => $providers) {
226
+            if (preg_match($supportedMimeType, $fileMimeType)) {
227
+                foreach ($providers as $providerClosure) {
228
+                    $provider = $this->helper->getProvider($providerClosure);
229
+                    if (!$provider) {
230
+                        continue;
231
+                    }
232
+                    if ($provider->isAvailable($file)) {
233
+                        return true;
234
+                    }
235
+                }
236
+            }
237
+        }
238
+        return false;
239
+    }
240
+
241
+    /**
242
+     * List of enabled default providers
243
+     *
244
+     * The following providers are enabled by default:
245
+     *  - OC\Preview\BMP
246
+     *  - OC\Preview\GIF
247
+     *  - OC\Preview\JPEG
248
+     *  - OC\Preview\MarkDown
249
+     *  - OC\Preview\PNG
250
+     *  - OC\Preview\TXT
251
+     *  - OC\Preview\XBitmap
252
+     *
253
+     * The following providers are disabled by default due to performance or privacy concerns:
254
+     *  - OC\Preview\Font
255
+     *  - OC\Preview\HEIC
256
+     *  - OC\Preview\Illustrator
257
+     *  - OC\Preview\MP3
258
+     *  - OC\Preview\MSOffice2003
259
+     *  - OC\Preview\MSOffice2007
260
+     *  - OC\Preview\MSOfficeDoc
261
+     *  - OC\Preview\Movie
262
+     *  - OC\Preview\OpenDocument
263
+     *  - OC\Preview\PDF
264
+     *  - OC\Preview\Photoshop
265
+     *  - OC\Preview\Postscript
266
+     *  - OC\Preview\SVG
267
+     *  - OC\Preview\StarOffice
268
+     *  - OC\Preview\TIFF
269
+     *
270
+     * @return list<class-string<IProviderV2>>
271
+     */
272
+    protected function getEnabledDefaultProvider(): array {
273
+        if ($this->defaultProviders !== null) {
274
+            return $this->defaultProviders;
275
+        }
276
+
277
+        $imageProviders = [
278
+            Preview\PNG::class,
279
+            Preview\JPEG::class,
280
+            Preview\GIF::class,
281
+            Preview\BMP::class,
282
+            Preview\XBitmap::class,
283
+            Preview\Krita::class,
284
+            Preview\WebP::class,
285
+        ];
286
+
287
+        $this->defaultProviders = $this->config->getSystemValue('enabledPreviewProviders', array_merge([
288
+            Preview\MarkDown::class,
289
+            Preview\TXT::class,
290
+            Preview\OpenDocument::class,
291
+        ], $imageProviders));
292
+
293
+        if (in_array(Preview\Image::class, $this->defaultProviders)) {
294
+            $this->defaultProviders = array_merge($this->defaultProviders, $imageProviders);
295
+        }
296
+        $this->defaultProviders = array_values(array_unique($this->defaultProviders));
297
+        /** @var list<class-string<IProviderV2>> $providers */
298
+        $providers = $this->defaultProviders;
299
+        return $providers;
300
+    }
301
+
302
+    /**
303
+     * Register the default providers (if enabled)
304
+     */
305
+    protected function registerCoreProvider(string $class, string $mimeType, array $options = []): void {
306
+        if (in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
307
+            $this->registerProvider($mimeType, function () use ($class, $options) {
308
+                return new $class($options);
309
+            });
310
+        }
311
+    }
312
+
313
+    /**
314
+     * Register the default providers (if enabled)
315
+     */
316
+    protected function registerCoreProviders(): void {
317
+        if ($this->registeredCoreProviders) {
318
+            return;
319
+        }
320
+        $this->registeredCoreProviders = true;
321
+
322
+        $this->registerCoreProvider(Preview\TXT::class, '/text\/plain/');
323
+        $this->registerCoreProvider(Preview\MarkDown::class, '/text\/(x-)?markdown/');
324
+        $this->registerCoreProvider(Preview\PNG::class, '/image\/png/');
325
+        $this->registerCoreProvider(Preview\JPEG::class, '/image\/jpeg/');
326
+        $this->registerCoreProvider(Preview\GIF::class, '/image\/gif/');
327
+        $this->registerCoreProvider(Preview\BMP::class, '/image\/bmp/');
328
+        $this->registerCoreProvider(Preview\XBitmap::class, '/image\/x-xbitmap/');
329
+        $this->registerCoreProvider(Preview\WebP::class, '/image\/webp/');
330
+        $this->registerCoreProvider(Preview\Krita::class, '/application\/x-krita/');
331
+        $this->registerCoreProvider(Preview\MP3::class, '/audio\/mpeg$/');
332
+        $this->registerCoreProvider(Preview\OpenDocument::class, '/application\/vnd.oasis.opendocument.*/');
333
+        $this->registerCoreProvider(Preview\Imaginary::class, Preview\Imaginary::supportedMimeTypes());
334
+        $this->registerCoreProvider(Preview\ImaginaryPDF::class, Preview\ImaginaryPDF::supportedMimeTypes());
335
+
336
+        // SVG and Bitmap require imagick
337
+        if ($this->imagickSupport->hasExtension()) {
338
+            $imagickProviders = [
339
+                'SVG' => ['mimetype' => '/image\/svg\+xml/', 'class' => Preview\SVG::class],
340
+                'TIFF' => ['mimetype' => '/image\/tiff/', 'class' => Preview\TIFF::class],
341
+                'PDF' => ['mimetype' => '/application\/pdf/', 'class' => Preview\PDF::class],
342
+                'AI' => ['mimetype' => '/application\/illustrator/', 'class' => Preview\Illustrator::class],
343
+                'PSD' => ['mimetype' => '/application\/x-photoshop/', 'class' => Preview\Photoshop::class],
344
+                'EPS' => ['mimetype' => '/application\/postscript/', 'class' => Preview\Postscript::class],
345
+                'TTF' => ['mimetype' => '/application\/(?:font-sfnt|x-font$)/', 'class' => Preview\Font::class],
346
+                'HEIC' => ['mimetype' => '/image\/(x-)?hei(f|c)/', 'class' => Preview\HEIC::class],
347
+                'TGA' => ['mimetype' => '/image\/(x-)?t(ar)?ga/', 'class' => Preview\TGA::class],
348
+                'SGI' => ['mimetype' => '/image\/(x-)?sgi/', 'class' => Preview\SGI::class],
349
+            ];
350
+
351
+            foreach ($imagickProviders as $queryFormat => $provider) {
352
+                $class = $provider['class'];
353
+                if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
354
+                    continue;
355
+                }
356
+
357
+                if ($this->imagickSupport->supportsFormat($queryFormat)) {
358
+                    $this->registerCoreProvider($class, $provider['mimetype']);
359
+                }
360
+            }
361
+        }
362
+
363
+        $this->registerCoreProvidersOffice();
364
+
365
+        // Video requires ffmpeg
366
+        if (in_array(Preview\Movie::class, $this->getEnabledDefaultProvider())) {
367
+            $movieBinary = $this->config->getSystemValue('preview_ffmpeg_path', null);
368
+            if (!is_string($movieBinary)) {
369
+                $movieBinary = $this->binaryFinder->findBinaryPath('ffmpeg');
370
+            }
371
+
372
+
373
+            if (is_string($movieBinary)) {
374
+                $this->registerCoreProvider(Preview\Movie::class, '/video\/.*/', ['movieBinary' => $movieBinary]);
375
+            }
376
+        }
377
+    }
378
+
379
+    private function registerCoreProvidersOffice(): void {
380
+        $officeProviders = [
381
+            ['mimetype' => '/application\/msword/', 'class' => Preview\MSOfficeDoc::class],
382
+            ['mimetype' => '/application\/vnd.ms-.*/', 'class' => Preview\MSOffice2003::class],
383
+            ['mimetype' => '/application\/vnd.openxmlformats-officedocument.*/', 'class' => Preview\MSOffice2007::class],
384
+            ['mimetype' => '/application\/vnd.oasis.opendocument.*/', 'class' => Preview\OpenDocument::class],
385
+            ['mimetype' => '/application\/vnd.sun.xml.*/', 'class' => Preview\StarOffice::class],
386
+            ['mimetype' => '/image\/emf/', 'class' => Preview\EMF::class],
387
+        ];
388
+
389
+        $findBinary = true;
390
+        $officeBinary = false;
391
+
392
+        foreach ($officeProviders as $provider) {
393
+            $class = $provider['class'];
394
+            if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
395
+                continue;
396
+            }
397
+
398
+            if ($findBinary) {
399
+                // Office requires openoffice or libreoffice
400
+                $officeBinary = $this->config->getSystemValue('preview_libreoffice_path', false);
401
+                if ($officeBinary === false) {
402
+                    $officeBinary = $this->binaryFinder->findBinaryPath('libreoffice');
403
+                }
404
+                if ($officeBinary === false) {
405
+                    $officeBinary = $this->binaryFinder->findBinaryPath('openoffice');
406
+                }
407
+                $findBinary = false;
408
+            }
409
+
410
+            if ($officeBinary) {
411
+                $this->registerCoreProvider($class, $provider['mimetype'], ['officeBinary' => $officeBinary]);
412
+            }
413
+        }
414
+    }
415
+
416
+    private function registerBootstrapProviders(): void {
417
+        $context = $this->bootstrapCoordinator->getRegistrationContext();
418
+
419
+        if ($context === null) {
420
+            // Just ignore for now
421
+            return;
422
+        }
423
+
424
+        $providers = $context->getPreviewProviders();
425
+        foreach ($providers as $provider) {
426
+            $key = $provider->getMimeTypeRegex() . '-' . $provider->getService();
427
+            if (array_key_exists($key, $this->loadedBootstrapProviders)) {
428
+                // Do not load the provider more than once
429
+                continue;
430
+            }
431
+            $this->loadedBootstrapProviders[$key] = null;
432
+
433
+            $this->registerProvider($provider->getMimeTypeRegex(), function () use ($provider): IProviderV2|false {
434
+                try {
435
+                    return $this->container->get($provider->getService());
436
+                } catch (NotFoundExceptionInterface) {
437
+                    return false;
438
+                }
439
+            });
440
+        }
441
+    }
442
+
443
+    /**
444
+     * @throws NotFoundException if preview generation is disabled
445
+     */
446
+    private function throwIfPreviewsDisabled(File $file, ?string $mimeType = null): void {
447
+        if (!$this->isAvailable($file, $mimeType)) {
448
+            throw new NotFoundException('Previews disabled');
449
+        }
450
+    }
451 451
 }
Please login to merge, or discard this patch.
lib/private/DB/SchemaWrapper.php 1 patch
Indentation   +133 added lines, -133 removed lines patch added patch discarded remove patch
@@ -15,137 +15,137 @@
 block discarded – undo
15 15
 use Psr\Log\LoggerInterface;
16 16
 
17 17
 class SchemaWrapper implements ISchemaWrapper {
18
-	/** @var Connection */
19
-	protected $connection;
20
-
21
-	/** @var Schema */
22
-	protected $schema;
23
-
24
-	/** @var array */
25
-	protected $tablesToDelete = [];
26
-
27
-	public function __construct(Connection $connection, ?Schema $schema = null) {
28
-		$this->connection = $connection;
29
-		if ($schema) {
30
-			$this->schema = $schema;
31
-		} else {
32
-			$this->schema = $this->connection->createSchema();
33
-		}
34
-	}
35
-
36
-	public function getWrappedSchema() {
37
-		return $this->schema;
38
-	}
39
-
40
-	public function performDropTableCalls() {
41
-		foreach ($this->tablesToDelete as $tableName => $true) {
42
-			$this->connection->dropTable($tableName);
43
-			foreach ($this->connection->getShardConnections() as $shardConnection) {
44
-				$shardConnection->dropTable($tableName);
45
-			}
46
-			unset($this->tablesToDelete[$tableName]);
47
-		}
48
-	}
49
-
50
-	/**
51
-	 * Gets all table names
52
-	 *
53
-	 * @return array
54
-	 */
55
-	public function getTableNamesWithoutPrefix() {
56
-		$tableNames = $this->schema->getTableNames();
57
-		return array_map(function ($tableName) {
58
-			if (str_starts_with($tableName, $this->connection->getPrefix())) {
59
-				return substr($tableName, strlen($this->connection->getPrefix()));
60
-			}
61
-
62
-			return $tableName;
63
-		}, $tableNames);
64
-	}
65
-
66
-	// Overwritten methods
67
-
68
-	/**
69
-	 * @return array
70
-	 */
71
-	public function getTableNames() {
72
-		return $this->schema->getTableNames();
73
-	}
74
-
75
-	/**
76
-	 * @param string $tableName
77
-	 *
78
-	 * @return \Doctrine\DBAL\Schema\Table
79
-	 * @throws \Doctrine\DBAL\Schema\SchemaException
80
-	 */
81
-	public function getTable($tableName) {
82
-		return $this->schema->getTable($this->connection->getPrefix() . $tableName);
83
-	}
84
-
85
-	/**
86
-	 * Does this schema have a table with the given name?
87
-	 *
88
-	 * @param string $tableName
89
-	 *
90
-	 * @return boolean
91
-	 */
92
-	public function hasTable($tableName) {
93
-		return $this->schema->hasTable($this->connection->getPrefix() . $tableName);
94
-	}
95
-
96
-	/**
97
-	 * Creates a new table.
98
-	 *
99
-	 * @param string $tableName
100
-	 * @return \Doctrine\DBAL\Schema\Table
101
-	 */
102
-	public function createTable($tableName) {
103
-		unset($this->tablesToDelete[$tableName]);
104
-		return $this->schema->createTable($this->connection->getPrefix() . $tableName);
105
-	}
106
-
107
-	/**
108
-	 * Drops a table from the schema.
109
-	 *
110
-	 * @param string $tableName
111
-	 * @return \Doctrine\DBAL\Schema\Schema
112
-	 */
113
-	public function dropTable($tableName) {
114
-		$this->tablesToDelete[$tableName] = true;
115
-		return $this->schema->dropTable($this->connection->getPrefix() . $tableName);
116
-	}
117
-
118
-	/**
119
-	 * Gets all tables of this schema.
120
-	 *
121
-	 * @return \Doctrine\DBAL\Schema\Table[]
122
-	 */
123
-	public function getTables() {
124
-		return $this->schema->getTables();
125
-	}
126
-
127
-	/**
128
-	 * Gets the DatabasePlatform for the database.
129
-	 *
130
-	 * @return AbstractPlatform
131
-	 *
132
-	 * @throws Exception
133
-	 */
134
-	public function getDatabasePlatform() {
135
-		return $this->connection->getDatabasePlatform();
136
-	}
137
-
138
-	public function dropAutoincrementColumn(string $table, string $column): void {
139
-		$tableObj = $this->schema->getTable($this->connection->getPrefix() . $table);
140
-		$tableObj->modifyColumn('id', ['autoincrement' => false]);
141
-		$platform = $this->getDatabasePlatform();
142
-		if ($platform instanceof OraclePlatform) {
143
-			try {
144
-				$this->connection->executeStatement('DROP TRIGGER "' . $this->connection->getPrefix() . $table . '_AI_PK"');
145
-				$this->connection->executeStatement('DROP SEQUENCE "' . $this->connection->getPrefix() . $table . '_SEQ"');
146
-			} catch (Exception $e) {
147
-				Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
148
-			}
149
-		}
150
-	}
18
+    /** @var Connection */
19
+    protected $connection;
20
+
21
+    /** @var Schema */
22
+    protected $schema;
23
+
24
+    /** @var array */
25
+    protected $tablesToDelete = [];
26
+
27
+    public function __construct(Connection $connection, ?Schema $schema = null) {
28
+        $this->connection = $connection;
29
+        if ($schema) {
30
+            $this->schema = $schema;
31
+        } else {
32
+            $this->schema = $this->connection->createSchema();
33
+        }
34
+    }
35
+
36
+    public function getWrappedSchema() {
37
+        return $this->schema;
38
+    }
39
+
40
+    public function performDropTableCalls() {
41
+        foreach ($this->tablesToDelete as $tableName => $true) {
42
+            $this->connection->dropTable($tableName);
43
+            foreach ($this->connection->getShardConnections() as $shardConnection) {
44
+                $shardConnection->dropTable($tableName);
45
+            }
46
+            unset($this->tablesToDelete[$tableName]);
47
+        }
48
+    }
49
+
50
+    /**
51
+     * Gets all table names
52
+     *
53
+     * @return array
54
+     */
55
+    public function getTableNamesWithoutPrefix() {
56
+        $tableNames = $this->schema->getTableNames();
57
+        return array_map(function ($tableName) {
58
+            if (str_starts_with($tableName, $this->connection->getPrefix())) {
59
+                return substr($tableName, strlen($this->connection->getPrefix()));
60
+            }
61
+
62
+            return $tableName;
63
+        }, $tableNames);
64
+    }
65
+
66
+    // Overwritten methods
67
+
68
+    /**
69
+     * @return array
70
+     */
71
+    public function getTableNames() {
72
+        return $this->schema->getTableNames();
73
+    }
74
+
75
+    /**
76
+     * @param string $tableName
77
+     *
78
+     * @return \Doctrine\DBAL\Schema\Table
79
+     * @throws \Doctrine\DBAL\Schema\SchemaException
80
+     */
81
+    public function getTable($tableName) {
82
+        return $this->schema->getTable($this->connection->getPrefix() . $tableName);
83
+    }
84
+
85
+    /**
86
+     * Does this schema have a table with the given name?
87
+     *
88
+     * @param string $tableName
89
+     *
90
+     * @return boolean
91
+     */
92
+    public function hasTable($tableName) {
93
+        return $this->schema->hasTable($this->connection->getPrefix() . $tableName);
94
+    }
95
+
96
+    /**
97
+     * Creates a new table.
98
+     *
99
+     * @param string $tableName
100
+     * @return \Doctrine\DBAL\Schema\Table
101
+     */
102
+    public function createTable($tableName) {
103
+        unset($this->tablesToDelete[$tableName]);
104
+        return $this->schema->createTable($this->connection->getPrefix() . $tableName);
105
+    }
106
+
107
+    /**
108
+     * Drops a table from the schema.
109
+     *
110
+     * @param string $tableName
111
+     * @return \Doctrine\DBAL\Schema\Schema
112
+     */
113
+    public function dropTable($tableName) {
114
+        $this->tablesToDelete[$tableName] = true;
115
+        return $this->schema->dropTable($this->connection->getPrefix() . $tableName);
116
+    }
117
+
118
+    /**
119
+     * Gets all tables of this schema.
120
+     *
121
+     * @return \Doctrine\DBAL\Schema\Table[]
122
+     */
123
+    public function getTables() {
124
+        return $this->schema->getTables();
125
+    }
126
+
127
+    /**
128
+     * Gets the DatabasePlatform for the database.
129
+     *
130
+     * @return AbstractPlatform
131
+     *
132
+     * @throws Exception
133
+     */
134
+    public function getDatabasePlatform() {
135
+        return $this->connection->getDatabasePlatform();
136
+    }
137
+
138
+    public function dropAutoincrementColumn(string $table, string $column): void {
139
+        $tableObj = $this->schema->getTable($this->connection->getPrefix() . $table);
140
+        $tableObj->modifyColumn('id', ['autoincrement' => false]);
141
+        $platform = $this->getDatabasePlatform();
142
+        if ($platform instanceof OraclePlatform) {
143
+            try {
144
+                $this->connection->executeStatement('DROP TRIGGER "' . $this->connection->getPrefix() . $table . '_AI_PK"');
145
+                $this->connection->executeStatement('DROP SEQUENCE "' . $this->connection->getPrefix() . $table . '_SEQ"');
146
+            } catch (Exception $e) {
147
+                Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
148
+            }
149
+        }
150
+    }
151 151
 }
Please login to merge, or discard this patch.
lib/private/Snowflake/Decoder.php 1 patch
Indentation   +98 added lines, -98 removed lines patch added patch discarded remove patch
@@ -21,102 +21,102 @@
 block discarded – undo
21 21
  * @since 33.0.0
22 22
  */
23 23
 final class Decoder implements IDecoder {
24
-	#[Override]
25
-	public function decode(string $snowflakeId): array {
26
-		if (!ctype_digit($snowflakeId)) {
27
-			throw new \Exception('Invalid Snowflake ID: ' . $snowflakeId);
28
-		}
29
-
30
-		/** @var array{seconds: positive-int, milliseconds: int<0,999>, serverId: int<0, 1023>, sequenceId: int<0,4095>, isCli: bool} $data */
31
-		$data = PHP_INT_SIZE === 8
32
-			? $this->decode64bits((int)$snowflakeId)
33
-			: $this->decode32bits($snowflakeId);
34
-
35
-		$data['createdAt'] = new \DateTimeImmutable(
36
-			sprintf(
37
-				'@%d.%03d',
38
-				$data['seconds'] + IGenerator::TS_OFFSET + intdiv($data['milliseconds'], 1000),
39
-				$data['milliseconds'] % 1000,
40
-			)
41
-		);
42
-
43
-		return $data;
44
-	}
45
-
46
-	private function decode64bits(int $snowflakeId): array {
47
-		$firstHalf = $snowflakeId >> 32;
48
-		$secondHalf = $snowflakeId & 0xFFFFFFFF;
49
-
50
-		$seconds = $firstHalf & 0x7FFFFFFF;
51
-		$milliseconds = $secondHalf >> 22;
52
-
53
-		return [
54
-			'seconds' => $seconds,
55
-			'milliseconds' => $milliseconds,
56
-			'serverId' => ($secondHalf >> 13) & 0x1FF,
57
-			'sequenceId' => $secondHalf & 0xFFF,
58
-			'isCli' => (bool)(($secondHalf >> 12) & 0x1),
59
-		];
60
-	}
61
-
62
-	private function decode32bits(string $snowflakeId): array {
63
-		$id = $this->convertBase16($snowflakeId);
64
-
65
-		$firstQuarter = (int)hexdec(substr($id, 0, 4));
66
-		$secondQuarter = (int)hexdec(substr($id, 4, 4));
67
-		$thirdQuarter = (int)hexdec(substr($id, 8, 4));
68
-		$fourthQuarter = (int)hexdec(substr($id, 12, 4));
69
-
70
-		$seconds = (($firstQuarter & 0x7FFF) << 16) | ($secondQuarter & 0xFFFF);
71
-		$milliseconds = ($thirdQuarter >> 6) & 0x3FF;
72
-
73
-		return [
74
-			'seconds' => $seconds,
75
-			'milliseconds' => $milliseconds,
76
-			'serverId' => (($thirdQuarter & 0x3F) << 3) | (($fourthQuarter >> 13) & 0x7),
77
-			'sequenceId' => $fourthQuarter & 0xFFF,
78
-			'isCli' => (bool)(($fourthQuarter >> 12) & 0x1),
79
-		];
80
-	}
81
-
82
-	/**
83
-	 * Convert base 10 number to base 16, padded to 16 characters
84
-	 *
85
-	 * Required on 32 bits systems as base_convert will lose precision with large numbers
86
-	 */
87
-	private function convertBase16(string $decimal): string {
88
-		$hex = '';
89
-		$digits = '0123456789ABCDEF';
90
-
91
-		while (strlen($decimal) > 0 && $decimal !== '0') {
92
-			$remainder = 0;
93
-			$newDecimal = '';
94
-
95
-			// Perform division by 16 manually for arbitrary precision
96
-			for ($i = 0; $i < strlen($decimal); $i++) {
97
-				$digit = (int)$decimal[$i];
98
-				$current = $remainder * 10 + $digit;
99
-
100
-				if ($current >= 16) {
101
-					$quotient = (int)($current / 16);
102
-					$remainder = $current % 16;
103
-					$newDecimal .= chr(ord('0') + $quotient);
104
-				} else {
105
-					$remainder = $current;
106
-					// Only add quotient digit if we already have some digits in result
107
-					if (strlen($newDecimal) > 0) {
108
-						$newDecimal .= '0';
109
-					}
110
-				}
111
-			}
112
-
113
-			// Add the remainder (0-15) as hex digit
114
-			$hex = $digits[$remainder] . $hex;
115
-
116
-			// Update decimal for next iteration
117
-			$decimal = ltrim($newDecimal, '0');
118
-		}
119
-
120
-		return str_pad($hex, 16, '0', STR_PAD_LEFT);
121
-	}
24
+    #[Override]
25
+    public function decode(string $snowflakeId): array {
26
+        if (!ctype_digit($snowflakeId)) {
27
+            throw new \Exception('Invalid Snowflake ID: ' . $snowflakeId);
28
+        }
29
+
30
+        /** @var array{seconds: positive-int, milliseconds: int<0,999>, serverId: int<0, 1023>, sequenceId: int<0,4095>, isCli: bool} $data */
31
+        $data = PHP_INT_SIZE === 8
32
+            ? $this->decode64bits((int)$snowflakeId)
33
+            : $this->decode32bits($snowflakeId);
34
+
35
+        $data['createdAt'] = new \DateTimeImmutable(
36
+            sprintf(
37
+                '@%d.%03d',
38
+                $data['seconds'] + IGenerator::TS_OFFSET + intdiv($data['milliseconds'], 1000),
39
+                $data['milliseconds'] % 1000,
40
+            )
41
+        );
42
+
43
+        return $data;
44
+    }
45
+
46
+    private function decode64bits(int $snowflakeId): array {
47
+        $firstHalf = $snowflakeId >> 32;
48
+        $secondHalf = $snowflakeId & 0xFFFFFFFF;
49
+
50
+        $seconds = $firstHalf & 0x7FFFFFFF;
51
+        $milliseconds = $secondHalf >> 22;
52
+
53
+        return [
54
+            'seconds' => $seconds,
55
+            'milliseconds' => $milliseconds,
56
+            'serverId' => ($secondHalf >> 13) & 0x1FF,
57
+            'sequenceId' => $secondHalf & 0xFFF,
58
+            'isCli' => (bool)(($secondHalf >> 12) & 0x1),
59
+        ];
60
+    }
61
+
62
+    private function decode32bits(string $snowflakeId): array {
63
+        $id = $this->convertBase16($snowflakeId);
64
+
65
+        $firstQuarter = (int)hexdec(substr($id, 0, 4));
66
+        $secondQuarter = (int)hexdec(substr($id, 4, 4));
67
+        $thirdQuarter = (int)hexdec(substr($id, 8, 4));
68
+        $fourthQuarter = (int)hexdec(substr($id, 12, 4));
69
+
70
+        $seconds = (($firstQuarter & 0x7FFF) << 16) | ($secondQuarter & 0xFFFF);
71
+        $milliseconds = ($thirdQuarter >> 6) & 0x3FF;
72
+
73
+        return [
74
+            'seconds' => $seconds,
75
+            'milliseconds' => $milliseconds,
76
+            'serverId' => (($thirdQuarter & 0x3F) << 3) | (($fourthQuarter >> 13) & 0x7),
77
+            'sequenceId' => $fourthQuarter & 0xFFF,
78
+            'isCli' => (bool)(($fourthQuarter >> 12) & 0x1),
79
+        ];
80
+    }
81
+
82
+    /**
83
+     * Convert base 10 number to base 16, padded to 16 characters
84
+     *
85
+     * Required on 32 bits systems as base_convert will lose precision with large numbers
86
+     */
87
+    private function convertBase16(string $decimal): string {
88
+        $hex = '';
89
+        $digits = '0123456789ABCDEF';
90
+
91
+        while (strlen($decimal) > 0 && $decimal !== '0') {
92
+            $remainder = 0;
93
+            $newDecimal = '';
94
+
95
+            // Perform division by 16 manually for arbitrary precision
96
+            for ($i = 0; $i < strlen($decimal); $i++) {
97
+                $digit = (int)$decimal[$i];
98
+                $current = $remainder * 10 + $digit;
99
+
100
+                if ($current >= 16) {
101
+                    $quotient = (int)($current / 16);
102
+                    $remainder = $current % 16;
103
+                    $newDecimal .= chr(ord('0') + $quotient);
104
+                } else {
105
+                    $remainder = $current;
106
+                    // Only add quotient digit if we already have some digits in result
107
+                    if (strlen($newDecimal) > 0) {
108
+                        $newDecimal .= '0';
109
+                    }
110
+                }
111
+            }
112
+
113
+            // Add the remainder (0-15) as hex digit
114
+            $hex = $digits[$remainder] . $hex;
115
+
116
+            // Update decimal for next iteration
117
+            $decimal = ltrim($newDecimal, '0');
118
+        }
119
+
120
+        return str_pad($hex, 16, '0', STR_PAD_LEFT);
121
+    }
122 122
 }
Please login to merge, or discard this patch.
lib/private/Snowflake/Generator.php 1 patch
Indentation   +114 added lines, -114 removed lines patch added patch discarded remove patch
@@ -21,118 +21,118 @@
 block discarded – undo
21 21
  * @since 33.0.0
22 22
  */
23 23
 final class Generator implements IGenerator {
24
-	public function __construct(
25
-		private readonly ITimeFactory $timeFactory,
26
-	) {
27
-	}
28
-
29
-	#[Override]
30
-	public function nextId(): string {
31
-		// Time related
32
-		[$seconds, $milliseconds] = $this->getCurrentTime();
33
-
34
-		$serverId = $this->getServerId() & 0x1FF; // Keep 9 bits
35
-		$isCli = (int)$this->isCli(); // 1 bit
36
-		$sequenceId = $this->getSequenceId($seconds, $milliseconds, $serverId); //  12 bits
37
-		if ($sequenceId > 0xFFF || $sequenceId === false) {
38
-			// Throttle a bit, wait for next millisecond
39
-			usleep(1000);
40
-			return $this->nextId();
41
-		}
42
-
43
-		if (PHP_INT_SIZE === 8) {
44
-			$firstHalf = $seconds & 0x7FFFFFFF;
45
-			$secondHalf = (($milliseconds & 0x3FF) << 22) | ($serverId << 13) | ($isCli << 12) | $sequenceId;
46
-			return (string)($firstHalf << 32 | $secondHalf);
47
-		}
48
-
49
-		// Fallback for 32 bits systems
50
-		$firstQuarter = ($seconds >> 16) & 0x7FFF;
51
-		$secondQuarter = $seconds & 0xFFFF;
52
-		$thirdQuarter = ($milliseconds & 0x3FF) << 6 | ($serverId >> 3) & 0x3F;
53
-		$fourthQuarter = ($serverId & 0x7) << 13 | ($isCli & 0x1) << 12 | $sequenceId & 0xFFF;
54
-
55
-		$bin = pack('n*', $firstQuarter, $secondQuarter, $thirdQuarter, $fourthQuarter);
56
-
57
-		$bytes = unpack('C*', $bin);
58
-		if ($bytes === false) {
59
-			throw new \Exception('Fail to unpack');
60
-		}
61
-
62
-		return $this->convertToDecimal(array_values($bytes));
63
-	}
64
-
65
-	/**
66
-	 * Mostly copied from Symfony:
67
-	 * https://github.com/symfony/symfony/blob/v7.3.4/src/Symfony/Component/Uid/BinaryUtil.php#L49
68
-	 */
69
-	private function convertToDecimal(array $bytes): string {
70
-		$base = 10;
71
-		$digits = '';
72
-
73
-		while ($count = \count($bytes)) {
74
-			$quotient = [];
75
-			$remainder = 0;
76
-
77
-			for ($i = 0; $i !== $count; ++$i) {
78
-				$carry = $bytes[$i] + ($remainder << (PHP_INT_SIZE === 8 ? 16 : 8));
79
-				$digit = intdiv($carry, $base);
80
-				$remainder = $carry % $base;
81
-
82
-				if ($digit || $quotient) {
83
-					$quotient[] = $digit;
84
-				}
85
-			}
86
-
87
-			$digits = $remainder . $digits;
88
-			$bytes = $quotient;
89
-		}
90
-
91
-		return $digits;
92
-	}
93
-
94
-	private function getCurrentTime(): array {
95
-		$time = $this->timeFactory->now();
96
-		return [
97
-			$time->getTimestamp() - self::TS_OFFSET,
98
-			(int)$time->format('v'),
99
-		];
100
-	}
101
-
102
-	private function getServerId(): int {
103
-		return crc32(gethostname() ?: random_bytes(8));
104
-	}
105
-
106
-	private function isCli(): bool {
107
-		return PHP_SAPI === 'cli';
108
-	}
109
-
110
-	/**
111
-	 * Generates sequence ID from APCu (general case) or random if APCu disabled or CLI
112
-	 *
113
-	 * @return int|false Sequence ID or false if APCu not ready
114
-	 * @throws \Exception if there is an error with APCu
115
-	 */
116
-	private function getSequenceId(int $seconds, int $milliseconds, int $serverId): int|false {
117
-		$key = 'seq:' . $seconds . ':' . $milliseconds;
118
-
119
-		// Use APCu as fastest local cache, but not shared between processes in CLI
120
-		if (!$this->isCli() && function_exists('apcu_enabled') && apcu_enabled()) {
121
-			if ((int)apcu_cache_info(true)['creation_time'] === $seconds) {
122
-				// APCu cache was just started
123
-				// It means a sequence was maybe deleted
124
-				return false;
125
-			}
126
-
127
-			$sequenceId = apcu_inc($key, success: $success, ttl: 1);
128
-			if ($success === true) {
129
-				return $sequenceId;
130
-			}
131
-
132
-			throw new \Exception('Failed to generate SnowflakeId with APCu');
133
-		}
134
-
135
-		// Otherwise, just return a random number
136
-		return random_int(0, 0xFFF - 1);
137
-	}
24
+    public function __construct(
25
+        private readonly ITimeFactory $timeFactory,
26
+    ) {
27
+    }
28
+
29
+    #[Override]
30
+    public function nextId(): string {
31
+        // Time related
32
+        [$seconds, $milliseconds] = $this->getCurrentTime();
33
+
34
+        $serverId = $this->getServerId() & 0x1FF; // Keep 9 bits
35
+        $isCli = (int)$this->isCli(); // 1 bit
36
+        $sequenceId = $this->getSequenceId($seconds, $milliseconds, $serverId); //  12 bits
37
+        if ($sequenceId > 0xFFF || $sequenceId === false) {
38
+            // Throttle a bit, wait for next millisecond
39
+            usleep(1000);
40
+            return $this->nextId();
41
+        }
42
+
43
+        if (PHP_INT_SIZE === 8) {
44
+            $firstHalf = $seconds & 0x7FFFFFFF;
45
+            $secondHalf = (($milliseconds & 0x3FF) << 22) | ($serverId << 13) | ($isCli << 12) | $sequenceId;
46
+            return (string)($firstHalf << 32 | $secondHalf);
47
+        }
48
+
49
+        // Fallback for 32 bits systems
50
+        $firstQuarter = ($seconds >> 16) & 0x7FFF;
51
+        $secondQuarter = $seconds & 0xFFFF;
52
+        $thirdQuarter = ($milliseconds & 0x3FF) << 6 | ($serverId >> 3) & 0x3F;
53
+        $fourthQuarter = ($serverId & 0x7) << 13 | ($isCli & 0x1) << 12 | $sequenceId & 0xFFF;
54
+
55
+        $bin = pack('n*', $firstQuarter, $secondQuarter, $thirdQuarter, $fourthQuarter);
56
+
57
+        $bytes = unpack('C*', $bin);
58
+        if ($bytes === false) {
59
+            throw new \Exception('Fail to unpack');
60
+        }
61
+
62
+        return $this->convertToDecimal(array_values($bytes));
63
+    }
64
+
65
+    /**
66
+     * Mostly copied from Symfony:
67
+     * https://github.com/symfony/symfony/blob/v7.3.4/src/Symfony/Component/Uid/BinaryUtil.php#L49
68
+     */
69
+    private function convertToDecimal(array $bytes): string {
70
+        $base = 10;
71
+        $digits = '';
72
+
73
+        while ($count = \count($bytes)) {
74
+            $quotient = [];
75
+            $remainder = 0;
76
+
77
+            for ($i = 0; $i !== $count; ++$i) {
78
+                $carry = $bytes[$i] + ($remainder << (PHP_INT_SIZE === 8 ? 16 : 8));
79
+                $digit = intdiv($carry, $base);
80
+                $remainder = $carry % $base;
81
+
82
+                if ($digit || $quotient) {
83
+                    $quotient[] = $digit;
84
+                }
85
+            }
86
+
87
+            $digits = $remainder . $digits;
88
+            $bytes = $quotient;
89
+        }
90
+
91
+        return $digits;
92
+    }
93
+
94
+    private function getCurrentTime(): array {
95
+        $time = $this->timeFactory->now();
96
+        return [
97
+            $time->getTimestamp() - self::TS_OFFSET,
98
+            (int)$time->format('v'),
99
+        ];
100
+    }
101
+
102
+    private function getServerId(): int {
103
+        return crc32(gethostname() ?: random_bytes(8));
104
+    }
105
+
106
+    private function isCli(): bool {
107
+        return PHP_SAPI === 'cli';
108
+    }
109
+
110
+    /**
111
+     * Generates sequence ID from APCu (general case) or random if APCu disabled or CLI
112
+     *
113
+     * @return int|false Sequence ID or false if APCu not ready
114
+     * @throws \Exception if there is an error with APCu
115
+     */
116
+    private function getSequenceId(int $seconds, int $milliseconds, int $serverId): int|false {
117
+        $key = 'seq:' . $seconds . ':' . $milliseconds;
118
+
119
+        // Use APCu as fastest local cache, but not shared between processes in CLI
120
+        if (!$this->isCli() && function_exists('apcu_enabled') && apcu_enabled()) {
121
+            if ((int)apcu_cache_info(true)['creation_time'] === $seconds) {
122
+                // APCu cache was just started
123
+                // It means a sequence was maybe deleted
124
+                return false;
125
+            }
126
+
127
+            $sequenceId = apcu_inc($key, success: $success, ttl: 1);
128
+            if ($success === true) {
129
+                return $sequenceId;
130
+            }
131
+
132
+            throw new \Exception('Failed to generate SnowflakeId with APCu');
133
+        }
134
+
135
+        // Otherwise, just return a random number
136
+        return random_int(0, 0xFFF - 1);
137
+    }
138 138
 }
Please login to merge, or discard this patch.
apps/settings/lib/SetupChecks/PhpModules.php 1 patch
Indentation   +80 added lines, -80 removed lines patch added patch discarded remove patch
@@ -14,90 +14,90 @@
 block discarded – undo
14 14
 use OCP\SetupCheck\SetupResult;
15 15
 
16 16
 class PhpModules implements ISetupCheck {
17
-	protected const REQUIRED_MODULES = [
18
-		'ctype',
19
-		'curl',
20
-		'dom',
21
-		'fileinfo',
22
-		'gd',
23
-		'mbstring',
24
-		'openssl',
25
-		'posix',
26
-		'session',
27
-		'xml',
28
-		'xmlreader',
29
-		'xmlwriter',
30
-		'zip',
31
-		'zlib',
32
-	];
33
-	protected const RECOMMENDED_MODULES = [
34
-		'apcu',
35
-		'exif',
36
-		'gmp',
37
-		'intl',
38
-		'sodium',
39
-		'sysvsem',
40
-	];
17
+    protected const REQUIRED_MODULES = [
18
+        'ctype',
19
+        'curl',
20
+        'dom',
21
+        'fileinfo',
22
+        'gd',
23
+        'mbstring',
24
+        'openssl',
25
+        'posix',
26
+        'session',
27
+        'xml',
28
+        'xmlreader',
29
+        'xmlwriter',
30
+        'zip',
31
+        'zlib',
32
+    ];
33
+    protected const RECOMMENDED_MODULES = [
34
+        'apcu',
35
+        'exif',
36
+        'gmp',
37
+        'intl',
38
+        'sodium',
39
+        'sysvsem',
40
+    ];
41 41
 
42
-	public function __construct(
43
-		private IL10N $l10n,
44
-		private IURLGenerator $urlGenerator,
45
-	) {
46
-	}
42
+    public function __construct(
43
+        private IL10N $l10n,
44
+        private IURLGenerator $urlGenerator,
45
+    ) {
46
+    }
47 47
 
48
-	public function getName(): string {
49
-		return $this->l10n->t('PHP modules');
50
-	}
48
+    public function getName(): string {
49
+        return $this->l10n->t('PHP modules');
50
+    }
51 51
 
52
-	public function getCategory(): string {
53
-		return 'php';
54
-	}
52
+    public function getCategory(): string {
53
+        return 'php';
54
+    }
55 55
 
56
-	protected function getRecommendedModuleDescription(string $module): string {
57
-		return match($module) {
58
-			'intl' => $this->l10n->t('increases language translation performance and fixes sorting of non-ASCII characters'),
59
-			'sodium' => $this->l10n->t('for Argon2 for password hashing'),
60
-			'gmp' => $this->l10n->t('required for SFTP storage and recommended for WebAuthn performance'),
61
-			'exif' => $this->l10n->t('for picture rotation in server and metadata extraction in the Photos app'),
62
-			default => '',
63
-		};
64
-	}
56
+    protected function getRecommendedModuleDescription(string $module): string {
57
+        return match($module) {
58
+            'intl' => $this->l10n->t('increases language translation performance and fixes sorting of non-ASCII characters'),
59
+            'sodium' => $this->l10n->t('for Argon2 for password hashing'),
60
+            'gmp' => $this->l10n->t('required for SFTP storage and recommended for WebAuthn performance'),
61
+            'exif' => $this->l10n->t('for picture rotation in server and metadata extraction in the Photos app'),
62
+            default => '',
63
+        };
64
+    }
65 65
 
66
-	public function run(): SetupResult {
67
-		$missingRecommendedModules = $this->getMissingModules(self::RECOMMENDED_MODULES);
68
-		$missingRequiredModules = $this->getMissingModules(self::REQUIRED_MODULES);
69
-		if (!empty($missingRequiredModules)) {
70
-			return SetupResult::error(
71
-				$this->l10n->t('This instance is missing some required PHP modules. It is required to install them: %s.', implode(', ', $missingRequiredModules)),
72
-				$this->urlGenerator->linkToDocs('admin-php-modules')
73
-			);
74
-		} elseif (!empty($missingRecommendedModules)) {
75
-			$moduleList = implode(
76
-				"\n",
77
-				array_map(
78
-					fn (string $module) => '- ' . $module . ' ' . $this->getRecommendedModuleDescription($module),
79
-					$missingRecommendedModules
80
-				)
81
-			);
82
-			return SetupResult::info(
83
-				$this->l10n->t("This instance is missing some recommended PHP modules. For improved performance and better compatibility it is highly recommended to install them:\n%s", $moduleList),
84
-				$this->urlGenerator->linkToDocs('admin-php-modules')
85
-			);
86
-		} else {
87
-			return SetupResult::success();
88
-		}
89
-	}
66
+    public function run(): SetupResult {
67
+        $missingRecommendedModules = $this->getMissingModules(self::RECOMMENDED_MODULES);
68
+        $missingRequiredModules = $this->getMissingModules(self::REQUIRED_MODULES);
69
+        if (!empty($missingRequiredModules)) {
70
+            return SetupResult::error(
71
+                $this->l10n->t('This instance is missing some required PHP modules. It is required to install them: %s.', implode(', ', $missingRequiredModules)),
72
+                $this->urlGenerator->linkToDocs('admin-php-modules')
73
+            );
74
+        } elseif (!empty($missingRecommendedModules)) {
75
+            $moduleList = implode(
76
+                "\n",
77
+                array_map(
78
+                    fn (string $module) => '- ' . $module . ' ' . $this->getRecommendedModuleDescription($module),
79
+                    $missingRecommendedModules
80
+                )
81
+            );
82
+            return SetupResult::info(
83
+                $this->l10n->t("This instance is missing some recommended PHP modules. For improved performance and better compatibility it is highly recommended to install them:\n%s", $moduleList),
84
+                $this->urlGenerator->linkToDocs('admin-php-modules')
85
+            );
86
+        } else {
87
+            return SetupResult::success();
88
+        }
89
+    }
90 90
 
91
-	/**
92
-	 * Checks for potential PHP modules that would improve the instance
93
-	 *
94
-	 * @param string[] $modules modules to test
95
-	 * @return string[] A list of PHP modules which are missing
96
-	 */
97
-	protected function getMissingModules(array $modules): array {
98
-		return array_values(array_filter(
99
-			$modules,
100
-			fn (string $module) => !extension_loaded($module),
101
-		));
102
-	}
91
+    /**
92
+     * Checks for potential PHP modules that would improve the instance
93
+     *
94
+     * @param string[] $modules modules to test
95
+     * @return string[] A list of PHP modules which are missing
96
+     */
97
+    protected function getMissingModules(array $modules): array {
98
+        return array_values(array_filter(
99
+            $modules,
100
+            fn (string $module) => !extension_loaded($module),
101
+        ));
102
+    }
103 103
 }
Please login to merge, or discard this patch.
tests/lib/Preview/GeneratorTest.php 1 patch
Indentation   +428 added lines, -428 removed lines patch added patch discarded remove patch
@@ -34,432 +34,432 @@
 block discarded – undo
34 34
 }
35 35
 
36 36
 class GeneratorTest extends TestCase {
37
-	private IConfig&MockObject $config;
38
-	private IPreview&MockObject $previewManager;
39
-	private GeneratorHelper&MockObject $helper;
40
-	private IEventDispatcher&MockObject $eventDispatcher;
41
-	private Generator $generator;
42
-	private LoggerInterface&MockObject $logger;
43
-	private StorageFactory&MockObject $storageFactory;
44
-	private PreviewMapper&MockObject $previewMapper;
45
-	private IGenerator&MockObject $snowflakeGenerator;
46
-
47
-	protected function setUp(): void {
48
-		parent::setUp();
49
-
50
-		$this->config = $this->createMock(IConfig::class);
51
-		$this->previewManager = $this->createMock(IPreview::class);
52
-		$this->helper = $this->createMock(GeneratorHelper::class);
53
-		$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
54
-		$this->logger = $this->createMock(LoggerInterface::class);
55
-		$this->previewMapper = $this->createMock(PreviewMapper::class);
56
-		$this->storageFactory = $this->createMock(StorageFactory::class);
57
-		$this->snowflakeGenerator = $this->createMock(IGenerator::class);
58
-
59
-		$this->generator = new Generator(
60
-			$this->config,
61
-			$this->previewManager,
62
-			$this->helper,
63
-			$this->eventDispatcher,
64
-			$this->logger,
65
-			$this->previewMapper,
66
-			$this->storageFactory,
67
-			$this->snowflakeGenerator,
68
-		);
69
-	}
70
-
71
-	private function getFile(int $fileId, string $mimeType, bool $hasVersion = false): File {
72
-		$mountPoint = $this->createMock(IMountPoint::class);
73
-		$mountPoint->method('getNumericStorageId')->willReturn(42);
74
-		if ($hasVersion) {
75
-			$file = $this->createMock(VersionedPreviewFile::class);
76
-			$file->method('getPreviewVersion')->willReturn('abc');
77
-		} else {
78
-			$file = $this->createMock(File::class);
79
-		}
80
-		$file->method('isReadable')
81
-			->willReturn(true);
82
-		$file->method('getMimeType')
83
-			->willReturn($mimeType);
84
-		$file->method('getId')
85
-			->willReturn($fileId);
86
-		$file->method('getMountPoint')
87
-			->willReturn($mountPoint);
88
-		return $file;
89
-	}
90
-
91
-	#[TestWith([true])]
92
-	#[TestWith([false])]
93
-	public function testGetCachedPreview(bool $hasPreview): void {
94
-		$file = $this->getFile(42, 'myMimeType', $hasPreview);
95
-
96
-		$this->previewManager->method('isMimeSupported')
97
-			->with($this->equalTo('myMimeType'))
98
-			->willReturn(true);
99
-
100
-		$maxPreview = new Preview();
101
-		$maxPreview->setWidth(1000);
102
-		$maxPreview->setHeight(1000);
103
-		$maxPreview->setMax(true);
104
-		$maxPreview->setSize(1000);
105
-		$maxPreview->setCropped(false);
106
-		$maxPreview->setStorageId(1);
107
-		$maxPreview->setVersion($hasPreview ? 'abc' : null);
108
-		$maxPreview->setMimeType('image/png');
109
-
110
-		$previewFile = new Preview();
111
-		$previewFile->setWidth(256);
112
-		$previewFile->setHeight(256);
113
-		$previewFile->setMax(false);
114
-		$previewFile->setSize(1000);
115
-		$previewFile->setVersion($hasPreview ? 'abc' : null);
116
-		$previewFile->setCropped(false);
117
-		$previewFile->setStorageId(1);
118
-		$previewFile->setMimeType('image/png');
119
-
120
-		$this->previewMapper->method('getAvailablePreviews')
121
-			->with($this->equalTo([42]))
122
-			->willReturn([42 => [
123
-				$maxPreview,
124
-				$previewFile,
125
-			]]);
126
-
127
-		$this->eventDispatcher->expects($this->once())
128
-			->method('dispatchTyped')
129
-			->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null));
130
-
131
-		$result = $this->generator->getPreview($file, 100, 100);
132
-		$this->assertSame($hasPreview ? 'abc-256-256.png' : '256-256.png', $result->getName());
133
-		$this->assertSame(1000, $result->getSize());
134
-	}
135
-
136
-	#[TestWith([true])]
137
-	#[TestWith([false])]
138
-	public function testGetNewPreview(bool $hasVersion): void {
139
-		$file = $this->getFile(42, 'myMimeType', $hasVersion);
140
-
141
-		$this->previewManager->method('isMimeSupported')
142
-			->with($this->equalTo('myMimeType'))
143
-			->willReturn(true);
144
-
145
-		$this->previewMapper->method('getAvailablePreviews')
146
-			->with($this->equalTo([42]))
147
-			->willReturn([42 => []]);
148
-
149
-		$this->config->method('getSystemValueString')
150
-			->willReturnCallback(function ($key, $default) {
151
-				return $default;
152
-			});
153
-
154
-		$this->config->method('getSystemValueInt')
155
-			->willReturnCallback(function ($key, $default) {
156
-				return $default;
157
-			});
158
-
159
-		$invalidProvider = $this->createMock(IProviderV2::class);
160
-		$invalidProvider->method('isAvailable')
161
-			->willReturn(true);
162
-		$unavailableProvider = $this->createMock(IProviderV2::class);
163
-		$unavailableProvider->method('isAvailable')
164
-			->willReturn(false);
165
-		$validProvider = $this->createMock(IProviderV2::class);
166
-		$validProvider->method('isAvailable')
167
-			->with($file)
168
-			->willReturn(true);
169
-
170
-		$this->previewManager->method('getProviders')
171
-			->willReturn([
172
-				'/image\/png/' => ['wrongProvider'],
173
-				'/myMimeType/' => ['brokenProvider', 'invalidProvider', 'unavailableProvider', 'validProvider'],
174
-			]);
175
-
176
-		$this->helper->method('getProvider')
177
-			->willReturnCallback(function ($provider) use ($invalidProvider, $validProvider, $unavailableProvider) {
178
-				if ($provider === 'wrongProvider') {
179
-					$this->fail('Wrongprovider should not be constructed!');
180
-				} elseif ($provider === 'brokenProvider') {
181
-					return false;
182
-				} elseif ($provider === 'invalidProvider') {
183
-					return $invalidProvider;
184
-				} elseif ($provider === 'validProvider') {
185
-					return $validProvider;
186
-				} elseif ($provider === 'unavailableProvider') {
187
-					return $unavailableProvider;
188
-				}
189
-				$this->fail('Unexpected provider requested');
190
-			});
191
-
192
-		$image = $this->createMock(IImage::class);
193
-		$image->method('width')->willReturn(2048);
194
-		$image->method('height')->willReturn(2048);
195
-		$image->method('valid')->willReturn(true);
196
-		$image->method('dataMimeType')->willReturn('image/png');
197
-
198
-		$this->helper->method('getThumbnail')
199
-			->willReturnCallback(function ($provider, $file, $x, $y) use ($invalidProvider, $validProvider, $image): false|IImage {
200
-				if ($provider === $validProvider) {
201
-					return $image;
202
-				} else {
203
-					return false;
204
-				}
205
-			});
206
-
207
-		$image->method('data')
208
-			->willReturn('my data');
209
-
210
-		$this->previewMapper->method('insert')
211
-			->willReturnCallback(fn (Preview $preview): Preview => $preview);
212
-
213
-		$this->previewMapper->method('update')
214
-			->willReturnCallback(fn (Preview $preview): Preview => $preview);
215
-
216
-		$this->storageFactory->method('writePreview')
217
-			->willReturnCallback(function (Preview $preview, mixed $data) use ($hasVersion): int {
218
-				$data = stream_get_contents($data);
219
-				if ($hasVersion) {
220
-					switch ($preview->getName()) {
221
-						case 'abc-2048-2048-max.png':
222
-							$this->assertSame('my data', $data);
223
-							return 1000;
224
-						case 'abc-256-256.png':
225
-							$this->assertSame('my resized data', $data);
226
-							return 1000;
227
-					}
228
-				} else {
229
-					switch ($preview->getName()) {
230
-						case '2048-2048-max.png':
231
-							$this->assertSame('my data', $data);
232
-							return 1000;
233
-						case '256-256.png':
234
-							$this->assertSame('my resized data', $data);
235
-							return 1000;
236
-					}
237
-				}
238
-				$this->fail('file name is wrong:' . $preview->getName());
239
-			});
240
-
241
-		$image = $this->getMockImage(2048, 2048, 'my resized data');
242
-		$this->helper->method('getImage')
243
-			->willReturn($image);
244
-
245
-		$this->eventDispatcher->expects($this->once())
246
-			->method('dispatchTyped')
247
-			->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null));
248
-
249
-		$result = $this->generator->getPreview($file, 100, 100);
250
-		$this->assertSame($hasVersion ? 'abc-256-256.png' : '256-256.png', $result->getName());
251
-		$this->assertSame(1000, $result->getSize());
252
-	}
253
-
254
-	public function testInvalidMimeType(): void {
255
-		$this->expectException(NotFoundException::class);
256
-
257
-		$file = $this->getFile(42, 'invalidType');
258
-
259
-		$this->previewManager->method('isMimeSupported')
260
-			->with('invalidType')
261
-			->willReturn(false);
262
-
263
-		$maxPreview = new Preview();
264
-		$maxPreview->setWidth(2048);
265
-		$maxPreview->setHeight(2048);
266
-		$maxPreview->setMax(true);
267
-		$maxPreview->setSize(1000);
268
-		$maxPreview->setVersion(null);
269
-		$maxPreview->setMimetype('image/png');
270
-
271
-		$this->previewMapper->method('getAvailablePreviews')
272
-			->with($this->equalTo([42]))
273
-			->willReturn([42 => [
274
-				$maxPreview,
275
-			]]);
276
-
277
-		$this->eventDispatcher->expects($this->once())
278
-			->method('dispatchTyped')
279
-			->with(new BeforePreviewFetchedEvent($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType'));
280
-
281
-		$this->generator->getPreview($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType');
282
-	}
283
-
284
-	public function testReturnCachedPreviewsWithoutCheckingSupportedMimetype(): void {
285
-		$file = $this->getFile(42, 'myMimeType');
286
-
287
-		$maxPreview = new Preview();
288
-		$maxPreview->setWidth(2048);
289
-		$maxPreview->setHeight(2048);
290
-		$maxPreview->setMax(true);
291
-		$maxPreview->setSize(1000);
292
-		$maxPreview->setVersion(null);
293
-		$maxPreview->setMimeType('image/png');
294
-
295
-		$previewFile = new Preview();
296
-		$previewFile->setWidth(1024);
297
-		$previewFile->setHeight(512);
298
-		$previewFile->setMax(false);
299
-		$previewFile->setSize(1000);
300
-		$previewFile->setCropped(true);
301
-		$previewFile->setVersion(null);
302
-		$previewFile->setMimeType('image/png');
303
-
304
-		$this->previewMapper->method('getAvailablePreviews')
305
-			->with($this->equalTo([42]))
306
-			->willReturn([42 => [
307
-				$maxPreview,
308
-				$previewFile,
309
-			]]);
310
-
311
-		$this->previewManager->expects($this->never())
312
-			->method('isMimeSupported');
313
-
314
-		$this->eventDispatcher->expects($this->once())
315
-			->method('dispatchTyped')
316
-			->with(new BeforePreviewFetchedEvent($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType'));
317
-
318
-		$result = $this->generator->getPreview($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType');
319
-		$this->assertSame('1024-512-crop.png', $result->getName());
320
-	}
321
-
322
-	public function testNoProvider(): void {
323
-		$file = $this->getFile(42, 'myMimeType');
324
-
325
-		$this->previewMapper->method('getAvailablePreviews')
326
-			->with($this->equalTo([42]))
327
-			->willReturn([42 => []]);
328
-
329
-		$this->previewManager->method('getProviders')
330
-			->willReturn([]);
331
-
332
-		$this->eventDispatcher->expects($this->once())
333
-			->method('dispatchTyped')
334
-			->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null));
335
-
336
-		$this->expectException(NotFoundException::class);
337
-		$this->generator->getPreview($file, 100, 100);
338
-	}
339
-
340
-	private function getMockImage(int $width, int $height, string $data = '') {
341
-		$image = $this->createMock(IImage::class);
342
-		$image->method('height')->willReturn($width);
343
-		$image->method('width')->willReturn($height);
344
-		$image->method('valid')->willReturn(true);
345
-		$image->method('dataMimeType')->willReturn('image/png');
346
-		$image->method('data')->willReturn($data);
347
-
348
-		$image->method('resizeCopy')->willReturnCallback(function ($size) use ($data) {
349
-			return $this->getMockImage($size, $size, $data);
350
-		});
351
-		$image->method('preciseResizeCopy')->willReturnCallback(function ($width, $height) use ($data) {
352
-			return $this->getMockImage($width, $height, $data);
353
-		});
354
-		$image->method('cropCopy')->willReturnCallback(function ($x, $y, $width, $height) use ($data) {
355
-			return $this->getMockImage($width, $height, $data);
356
-		});
357
-
358
-		return $image;
359
-	}
360
-
361
-	public static function dataSize(): array {
362
-		return [
363
-			[1024, 2048, 512, 512, false, IPreview::MODE_FILL, 256, 512],
364
-			[1024, 2048, 512, 512, false, IPreview::MODE_COVER, 512, 1024],
365
-			[1024, 2048, 512, 512, true, IPreview::MODE_FILL, 1024, 1024],
366
-			[1024, 2048, 512, 512, true, IPreview::MODE_COVER, 1024, 1024],
367
-
368
-			[1024, 2048, -1, 512, false, IPreview::MODE_COVER, 256, 512],
369
-			[1024, 2048, 512, -1, false, IPreview::MODE_FILL, 512, 1024],
370
-
371
-			[1024, 2048, 250, 1100, true, IPreview::MODE_COVER, 256, 1126],
372
-			[1024, 1100, 250, 1100, true, IPreview::MODE_COVER, 250, 1100],
373
-
374
-			[1024, 2048, 4096, 2048, false, IPreview::MODE_FILL, 1024, 2048],
375
-			[1024, 2048, 4096, 2048, false, IPreview::MODE_COVER, 1024, 2048],
376
-
377
-
378
-			[2048, 1024, 512, 512, false, IPreview::MODE_FILL, 512, 256],
379
-			[2048, 1024, 512, 512, false, IPreview::MODE_COVER, 1024, 512],
380
-			[2048, 1024, 512, 512, true, IPreview::MODE_FILL, 1024, 1024],
381
-			[2048, 1024, 512, 512, true, IPreview::MODE_COVER, 1024, 1024],
382
-
383
-			[2048, 1024, -1, 512, false, IPreview::MODE_FILL, 1024, 512],
384
-			[2048, 1024, 512, -1, false, IPreview::MODE_COVER, 512, 256],
385
-
386
-			[2048, 1024, 4096, 1024, true, IPreview::MODE_FILL, 2048, 512],
387
-			[2048, 1024, 4096, 1024, true, IPreview::MODE_COVER, 2048, 512],
388
-
389
-			//Test minimum size
390
-			[2048, 1024, 32, 32, false, IPreview::MODE_FILL, 64, 32],
391
-			[2048, 1024, 32, 32, false, IPreview::MODE_COVER, 64, 32],
392
-			[2048, 1024, 32, 32, true, IPreview::MODE_FILL, 64, 64],
393
-			[2048, 1024, 32, 32, true, IPreview::MODE_COVER, 64, 64],
394
-		];
395
-	}
396
-
397
-	#[DataProvider('dataSize')]
398
-	public function testCorrectSize(int $maxX, int $maxY, int $reqX, int $reqY, bool $crop, string $mode, int $expectedX, int $expectedY): void {
399
-		$file = $this->getFile(42, 'myMimeType');
400
-
401
-		$this->previewManager->method('isMimeSupported')
402
-			->with($this->equalTo('myMimeType'))
403
-			->willReturn(true);
404
-
405
-		$maxPreview = new Preview();
406
-		$maxPreview->setWidth($maxX);
407
-		$maxPreview->setHeight($maxY);
408
-		$maxPreview->setMax(true);
409
-		$maxPreview->setSize(1000);
410
-		$maxPreview->setVersion(null);
411
-		$maxPreview->setMimeType('image/png');
412
-
413
-		$this->assertSame($maxPreview->getName(), $maxX . '-' . $maxY . '-max.png');
414
-		$this->assertSame($maxPreview->getMimeType(), 'image/png');
415
-
416
-		$this->previewMapper->method('getAvailablePreviews')
417
-			->with($this->equalTo([42]))
418
-			->willReturn([42 => [
419
-				$maxPreview,
420
-			]]);
421
-
422
-		$filename = $expectedX . '-' . $expectedY;
423
-		if ($crop) {
424
-			$filename .= '-crop';
425
-		}
426
-		$filename .= '.png';
427
-
428
-		$image = $this->getMockImage($maxX, $maxY);
429
-		$this->helper->method('getImage')
430
-			->willReturn($image);
431
-
432
-		$this->previewMapper->method('insert')
433
-			->willReturnCallback(function (Preview $preview) use ($filename): Preview {
434
-				$this->assertSame($preview->getName(), $filename);
435
-				return $preview;
436
-			});
437
-
438
-		$this->previewMapper->method('update')
439
-			->willReturnCallback(fn (Preview $preview): Preview => $preview);
440
-
441
-		$this->storageFactory->method('writePreview')
442
-			->willReturn(1000);
443
-
444
-		$this->eventDispatcher->expects($this->once())
445
-			->method('dispatchTyped')
446
-			->with(new BeforePreviewFetchedEvent($file, $reqX, $reqY, $crop, $mode, null));
447
-
448
-		$result = $this->generator->getPreview($file, $reqX, $reqY, $crop, $mode);
449
-		if ($expectedX === $maxX && $expectedY === $maxY) {
450
-			$this->assertSame($maxPreview->getName(), $result->getName());
451
-		} else {
452
-			$this->assertSame($filename, $result->getName());
453
-		}
454
-	}
455
-
456
-	public function testUnreadbleFile(): void {
457
-		$file = $this->createMock(File::class);
458
-		$file->method('isReadable')
459
-			->willReturn(false);
460
-
461
-		$this->expectException(NotFoundException::class);
462
-
463
-		$this->generator->getPreview($file, 100, 100, false);
464
-	}
37
+    private IConfig&MockObject $config;
38
+    private IPreview&MockObject $previewManager;
39
+    private GeneratorHelper&MockObject $helper;
40
+    private IEventDispatcher&MockObject $eventDispatcher;
41
+    private Generator $generator;
42
+    private LoggerInterface&MockObject $logger;
43
+    private StorageFactory&MockObject $storageFactory;
44
+    private PreviewMapper&MockObject $previewMapper;
45
+    private IGenerator&MockObject $snowflakeGenerator;
46
+
47
+    protected function setUp(): void {
48
+        parent::setUp();
49
+
50
+        $this->config = $this->createMock(IConfig::class);
51
+        $this->previewManager = $this->createMock(IPreview::class);
52
+        $this->helper = $this->createMock(GeneratorHelper::class);
53
+        $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
54
+        $this->logger = $this->createMock(LoggerInterface::class);
55
+        $this->previewMapper = $this->createMock(PreviewMapper::class);
56
+        $this->storageFactory = $this->createMock(StorageFactory::class);
57
+        $this->snowflakeGenerator = $this->createMock(IGenerator::class);
58
+
59
+        $this->generator = new Generator(
60
+            $this->config,
61
+            $this->previewManager,
62
+            $this->helper,
63
+            $this->eventDispatcher,
64
+            $this->logger,
65
+            $this->previewMapper,
66
+            $this->storageFactory,
67
+            $this->snowflakeGenerator,
68
+        );
69
+    }
70
+
71
+    private function getFile(int $fileId, string $mimeType, bool $hasVersion = false): File {
72
+        $mountPoint = $this->createMock(IMountPoint::class);
73
+        $mountPoint->method('getNumericStorageId')->willReturn(42);
74
+        if ($hasVersion) {
75
+            $file = $this->createMock(VersionedPreviewFile::class);
76
+            $file->method('getPreviewVersion')->willReturn('abc');
77
+        } else {
78
+            $file = $this->createMock(File::class);
79
+        }
80
+        $file->method('isReadable')
81
+            ->willReturn(true);
82
+        $file->method('getMimeType')
83
+            ->willReturn($mimeType);
84
+        $file->method('getId')
85
+            ->willReturn($fileId);
86
+        $file->method('getMountPoint')
87
+            ->willReturn($mountPoint);
88
+        return $file;
89
+    }
90
+
91
+    #[TestWith([true])]
92
+    #[TestWith([false])]
93
+    public function testGetCachedPreview(bool $hasPreview): void {
94
+        $file = $this->getFile(42, 'myMimeType', $hasPreview);
95
+
96
+        $this->previewManager->method('isMimeSupported')
97
+            ->with($this->equalTo('myMimeType'))
98
+            ->willReturn(true);
99
+
100
+        $maxPreview = new Preview();
101
+        $maxPreview->setWidth(1000);
102
+        $maxPreview->setHeight(1000);
103
+        $maxPreview->setMax(true);
104
+        $maxPreview->setSize(1000);
105
+        $maxPreview->setCropped(false);
106
+        $maxPreview->setStorageId(1);
107
+        $maxPreview->setVersion($hasPreview ? 'abc' : null);
108
+        $maxPreview->setMimeType('image/png');
109
+
110
+        $previewFile = new Preview();
111
+        $previewFile->setWidth(256);
112
+        $previewFile->setHeight(256);
113
+        $previewFile->setMax(false);
114
+        $previewFile->setSize(1000);
115
+        $previewFile->setVersion($hasPreview ? 'abc' : null);
116
+        $previewFile->setCropped(false);
117
+        $previewFile->setStorageId(1);
118
+        $previewFile->setMimeType('image/png');
119
+
120
+        $this->previewMapper->method('getAvailablePreviews')
121
+            ->with($this->equalTo([42]))
122
+            ->willReturn([42 => [
123
+                $maxPreview,
124
+                $previewFile,
125
+            ]]);
126
+
127
+        $this->eventDispatcher->expects($this->once())
128
+            ->method('dispatchTyped')
129
+            ->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null));
130
+
131
+        $result = $this->generator->getPreview($file, 100, 100);
132
+        $this->assertSame($hasPreview ? 'abc-256-256.png' : '256-256.png', $result->getName());
133
+        $this->assertSame(1000, $result->getSize());
134
+    }
135
+
136
+    #[TestWith([true])]
137
+    #[TestWith([false])]
138
+    public function testGetNewPreview(bool $hasVersion): void {
139
+        $file = $this->getFile(42, 'myMimeType', $hasVersion);
140
+
141
+        $this->previewManager->method('isMimeSupported')
142
+            ->with($this->equalTo('myMimeType'))
143
+            ->willReturn(true);
144
+
145
+        $this->previewMapper->method('getAvailablePreviews')
146
+            ->with($this->equalTo([42]))
147
+            ->willReturn([42 => []]);
148
+
149
+        $this->config->method('getSystemValueString')
150
+            ->willReturnCallback(function ($key, $default) {
151
+                return $default;
152
+            });
153
+
154
+        $this->config->method('getSystemValueInt')
155
+            ->willReturnCallback(function ($key, $default) {
156
+                return $default;
157
+            });
158
+
159
+        $invalidProvider = $this->createMock(IProviderV2::class);
160
+        $invalidProvider->method('isAvailable')
161
+            ->willReturn(true);
162
+        $unavailableProvider = $this->createMock(IProviderV2::class);
163
+        $unavailableProvider->method('isAvailable')
164
+            ->willReturn(false);
165
+        $validProvider = $this->createMock(IProviderV2::class);
166
+        $validProvider->method('isAvailable')
167
+            ->with($file)
168
+            ->willReturn(true);
169
+
170
+        $this->previewManager->method('getProviders')
171
+            ->willReturn([
172
+                '/image\/png/' => ['wrongProvider'],
173
+                '/myMimeType/' => ['brokenProvider', 'invalidProvider', 'unavailableProvider', 'validProvider'],
174
+            ]);
175
+
176
+        $this->helper->method('getProvider')
177
+            ->willReturnCallback(function ($provider) use ($invalidProvider, $validProvider, $unavailableProvider) {
178
+                if ($provider === 'wrongProvider') {
179
+                    $this->fail('Wrongprovider should not be constructed!');
180
+                } elseif ($provider === 'brokenProvider') {
181
+                    return false;
182
+                } elseif ($provider === 'invalidProvider') {
183
+                    return $invalidProvider;
184
+                } elseif ($provider === 'validProvider') {
185
+                    return $validProvider;
186
+                } elseif ($provider === 'unavailableProvider') {
187
+                    return $unavailableProvider;
188
+                }
189
+                $this->fail('Unexpected provider requested');
190
+            });
191
+
192
+        $image = $this->createMock(IImage::class);
193
+        $image->method('width')->willReturn(2048);
194
+        $image->method('height')->willReturn(2048);
195
+        $image->method('valid')->willReturn(true);
196
+        $image->method('dataMimeType')->willReturn('image/png');
197
+
198
+        $this->helper->method('getThumbnail')
199
+            ->willReturnCallback(function ($provider, $file, $x, $y) use ($invalidProvider, $validProvider, $image): false|IImage {
200
+                if ($provider === $validProvider) {
201
+                    return $image;
202
+                } else {
203
+                    return false;
204
+                }
205
+            });
206
+
207
+        $image->method('data')
208
+            ->willReturn('my data');
209
+
210
+        $this->previewMapper->method('insert')
211
+            ->willReturnCallback(fn (Preview $preview): Preview => $preview);
212
+
213
+        $this->previewMapper->method('update')
214
+            ->willReturnCallback(fn (Preview $preview): Preview => $preview);
215
+
216
+        $this->storageFactory->method('writePreview')
217
+            ->willReturnCallback(function (Preview $preview, mixed $data) use ($hasVersion): int {
218
+                $data = stream_get_contents($data);
219
+                if ($hasVersion) {
220
+                    switch ($preview->getName()) {
221
+                        case 'abc-2048-2048-max.png':
222
+                            $this->assertSame('my data', $data);
223
+                            return 1000;
224
+                        case 'abc-256-256.png':
225
+                            $this->assertSame('my resized data', $data);
226
+                            return 1000;
227
+                    }
228
+                } else {
229
+                    switch ($preview->getName()) {
230
+                        case '2048-2048-max.png':
231
+                            $this->assertSame('my data', $data);
232
+                            return 1000;
233
+                        case '256-256.png':
234
+                            $this->assertSame('my resized data', $data);
235
+                            return 1000;
236
+                    }
237
+                }
238
+                $this->fail('file name is wrong:' . $preview->getName());
239
+            });
240
+
241
+        $image = $this->getMockImage(2048, 2048, 'my resized data');
242
+        $this->helper->method('getImage')
243
+            ->willReturn($image);
244
+
245
+        $this->eventDispatcher->expects($this->once())
246
+            ->method('dispatchTyped')
247
+            ->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null));
248
+
249
+        $result = $this->generator->getPreview($file, 100, 100);
250
+        $this->assertSame($hasVersion ? 'abc-256-256.png' : '256-256.png', $result->getName());
251
+        $this->assertSame(1000, $result->getSize());
252
+    }
253
+
254
+    public function testInvalidMimeType(): void {
255
+        $this->expectException(NotFoundException::class);
256
+
257
+        $file = $this->getFile(42, 'invalidType');
258
+
259
+        $this->previewManager->method('isMimeSupported')
260
+            ->with('invalidType')
261
+            ->willReturn(false);
262
+
263
+        $maxPreview = new Preview();
264
+        $maxPreview->setWidth(2048);
265
+        $maxPreview->setHeight(2048);
266
+        $maxPreview->setMax(true);
267
+        $maxPreview->setSize(1000);
268
+        $maxPreview->setVersion(null);
269
+        $maxPreview->setMimetype('image/png');
270
+
271
+        $this->previewMapper->method('getAvailablePreviews')
272
+            ->with($this->equalTo([42]))
273
+            ->willReturn([42 => [
274
+                $maxPreview,
275
+            ]]);
276
+
277
+        $this->eventDispatcher->expects($this->once())
278
+            ->method('dispatchTyped')
279
+            ->with(new BeforePreviewFetchedEvent($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType'));
280
+
281
+        $this->generator->getPreview($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType');
282
+    }
283
+
284
+    public function testReturnCachedPreviewsWithoutCheckingSupportedMimetype(): void {
285
+        $file = $this->getFile(42, 'myMimeType');
286
+
287
+        $maxPreview = new Preview();
288
+        $maxPreview->setWidth(2048);
289
+        $maxPreview->setHeight(2048);
290
+        $maxPreview->setMax(true);
291
+        $maxPreview->setSize(1000);
292
+        $maxPreview->setVersion(null);
293
+        $maxPreview->setMimeType('image/png');
294
+
295
+        $previewFile = new Preview();
296
+        $previewFile->setWidth(1024);
297
+        $previewFile->setHeight(512);
298
+        $previewFile->setMax(false);
299
+        $previewFile->setSize(1000);
300
+        $previewFile->setCropped(true);
301
+        $previewFile->setVersion(null);
302
+        $previewFile->setMimeType('image/png');
303
+
304
+        $this->previewMapper->method('getAvailablePreviews')
305
+            ->with($this->equalTo([42]))
306
+            ->willReturn([42 => [
307
+                $maxPreview,
308
+                $previewFile,
309
+            ]]);
310
+
311
+        $this->previewManager->expects($this->never())
312
+            ->method('isMimeSupported');
313
+
314
+        $this->eventDispatcher->expects($this->once())
315
+            ->method('dispatchTyped')
316
+            ->with(new BeforePreviewFetchedEvent($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType'));
317
+
318
+        $result = $this->generator->getPreview($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType');
319
+        $this->assertSame('1024-512-crop.png', $result->getName());
320
+    }
321
+
322
+    public function testNoProvider(): void {
323
+        $file = $this->getFile(42, 'myMimeType');
324
+
325
+        $this->previewMapper->method('getAvailablePreviews')
326
+            ->with($this->equalTo([42]))
327
+            ->willReturn([42 => []]);
328
+
329
+        $this->previewManager->method('getProviders')
330
+            ->willReturn([]);
331
+
332
+        $this->eventDispatcher->expects($this->once())
333
+            ->method('dispatchTyped')
334
+            ->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null));
335
+
336
+        $this->expectException(NotFoundException::class);
337
+        $this->generator->getPreview($file, 100, 100);
338
+    }
339
+
340
+    private function getMockImage(int $width, int $height, string $data = '') {
341
+        $image = $this->createMock(IImage::class);
342
+        $image->method('height')->willReturn($width);
343
+        $image->method('width')->willReturn($height);
344
+        $image->method('valid')->willReturn(true);
345
+        $image->method('dataMimeType')->willReturn('image/png');
346
+        $image->method('data')->willReturn($data);
347
+
348
+        $image->method('resizeCopy')->willReturnCallback(function ($size) use ($data) {
349
+            return $this->getMockImage($size, $size, $data);
350
+        });
351
+        $image->method('preciseResizeCopy')->willReturnCallback(function ($width, $height) use ($data) {
352
+            return $this->getMockImage($width, $height, $data);
353
+        });
354
+        $image->method('cropCopy')->willReturnCallback(function ($x, $y, $width, $height) use ($data) {
355
+            return $this->getMockImage($width, $height, $data);
356
+        });
357
+
358
+        return $image;
359
+    }
360
+
361
+    public static function dataSize(): array {
362
+        return [
363
+            [1024, 2048, 512, 512, false, IPreview::MODE_FILL, 256, 512],
364
+            [1024, 2048, 512, 512, false, IPreview::MODE_COVER, 512, 1024],
365
+            [1024, 2048, 512, 512, true, IPreview::MODE_FILL, 1024, 1024],
366
+            [1024, 2048, 512, 512, true, IPreview::MODE_COVER, 1024, 1024],
367
+
368
+            [1024, 2048, -1, 512, false, IPreview::MODE_COVER, 256, 512],
369
+            [1024, 2048, 512, -1, false, IPreview::MODE_FILL, 512, 1024],
370
+
371
+            [1024, 2048, 250, 1100, true, IPreview::MODE_COVER, 256, 1126],
372
+            [1024, 1100, 250, 1100, true, IPreview::MODE_COVER, 250, 1100],
373
+
374
+            [1024, 2048, 4096, 2048, false, IPreview::MODE_FILL, 1024, 2048],
375
+            [1024, 2048, 4096, 2048, false, IPreview::MODE_COVER, 1024, 2048],
376
+
377
+
378
+            [2048, 1024, 512, 512, false, IPreview::MODE_FILL, 512, 256],
379
+            [2048, 1024, 512, 512, false, IPreview::MODE_COVER, 1024, 512],
380
+            [2048, 1024, 512, 512, true, IPreview::MODE_FILL, 1024, 1024],
381
+            [2048, 1024, 512, 512, true, IPreview::MODE_COVER, 1024, 1024],
382
+
383
+            [2048, 1024, -1, 512, false, IPreview::MODE_FILL, 1024, 512],
384
+            [2048, 1024, 512, -1, false, IPreview::MODE_COVER, 512, 256],
385
+
386
+            [2048, 1024, 4096, 1024, true, IPreview::MODE_FILL, 2048, 512],
387
+            [2048, 1024, 4096, 1024, true, IPreview::MODE_COVER, 2048, 512],
388
+
389
+            //Test minimum size
390
+            [2048, 1024, 32, 32, false, IPreview::MODE_FILL, 64, 32],
391
+            [2048, 1024, 32, 32, false, IPreview::MODE_COVER, 64, 32],
392
+            [2048, 1024, 32, 32, true, IPreview::MODE_FILL, 64, 64],
393
+            [2048, 1024, 32, 32, true, IPreview::MODE_COVER, 64, 64],
394
+        ];
395
+    }
396
+
397
+    #[DataProvider('dataSize')]
398
+    public function testCorrectSize(int $maxX, int $maxY, int $reqX, int $reqY, bool $crop, string $mode, int $expectedX, int $expectedY): void {
399
+        $file = $this->getFile(42, 'myMimeType');
400
+
401
+        $this->previewManager->method('isMimeSupported')
402
+            ->with($this->equalTo('myMimeType'))
403
+            ->willReturn(true);
404
+
405
+        $maxPreview = new Preview();
406
+        $maxPreview->setWidth($maxX);
407
+        $maxPreview->setHeight($maxY);
408
+        $maxPreview->setMax(true);
409
+        $maxPreview->setSize(1000);
410
+        $maxPreview->setVersion(null);
411
+        $maxPreview->setMimeType('image/png');
412
+
413
+        $this->assertSame($maxPreview->getName(), $maxX . '-' . $maxY . '-max.png');
414
+        $this->assertSame($maxPreview->getMimeType(), 'image/png');
415
+
416
+        $this->previewMapper->method('getAvailablePreviews')
417
+            ->with($this->equalTo([42]))
418
+            ->willReturn([42 => [
419
+                $maxPreview,
420
+            ]]);
421
+
422
+        $filename = $expectedX . '-' . $expectedY;
423
+        if ($crop) {
424
+            $filename .= '-crop';
425
+        }
426
+        $filename .= '.png';
427
+
428
+        $image = $this->getMockImage($maxX, $maxY);
429
+        $this->helper->method('getImage')
430
+            ->willReturn($image);
431
+
432
+        $this->previewMapper->method('insert')
433
+            ->willReturnCallback(function (Preview $preview) use ($filename): Preview {
434
+                $this->assertSame($preview->getName(), $filename);
435
+                return $preview;
436
+            });
437
+
438
+        $this->previewMapper->method('update')
439
+            ->willReturnCallback(fn (Preview $preview): Preview => $preview);
440
+
441
+        $this->storageFactory->method('writePreview')
442
+            ->willReturn(1000);
443
+
444
+        $this->eventDispatcher->expects($this->once())
445
+            ->method('dispatchTyped')
446
+            ->with(new BeforePreviewFetchedEvent($file, $reqX, $reqY, $crop, $mode, null));
447
+
448
+        $result = $this->generator->getPreview($file, $reqX, $reqY, $crop, $mode);
449
+        if ($expectedX === $maxX && $expectedY === $maxY) {
450
+            $this->assertSame($maxPreview->getName(), $result->getName());
451
+        } else {
452
+            $this->assertSame($filename, $result->getName());
453
+        }
454
+    }
455
+
456
+    public function testUnreadbleFile(): void {
457
+        $file = $this->createMock(File::class);
458
+        $file->method('isReadable')
459
+            ->willReturn(false);
460
+
461
+        $this->expectException(NotFoundException::class);
462
+
463
+        $this->generator->getPreview($file, 100, 100, false);
464
+    }
465 465
 }
Please login to merge, or discard this patch.
tests/lib/Preview/PreviewServiceTest.php 1 patch
Indentation   +38 added lines, -38 removed lines patch added patch discarded remove patch
@@ -20,42 +20,42 @@
 block discarded – undo
20 20
 #[CoversClass(PreviewService::class)]
21 21
 #[\PHPUnit\Framework\Attributes\Group('DB')]
22 22
 class PreviewServiceTest extends TestCase {
23
-	private PreviewService $previewService;
24
-	private PreviewMapper $previewMapper;
25
-	private IGenerator $snowflakeGenerator;
26
-
27
-	protected function setUp(): void {
28
-		$this->previewService = Server::get(PreviewService::class);
29
-		$this->previewMapper = Server::get(PreviewMapper::class);
30
-		$this->snowflakeGenerator = Server::get(IGenerator::class);
31
-		$this->previewService->deleteAll();
32
-	}
33
-
34
-	public function tearDown(): void {
35
-		$this->previewService->deleteAll();
36
-	}
37
-
38
-	public function testGetAvailableFileIds(): void {
39
-		foreach (range(1, 20) as $i) {
40
-			$preview = new Preview();
41
-			$preview->setId($this->snowflakeGenerator->nextId());
42
-			$preview->setFileId($i % 10);
43
-			$preview->setStorageId(1);
44
-			$preview->setWidth($i);
45
-			$preview->setHeight($i);
46
-			$preview->setMax(true);
47
-			$preview->setSourceMimeType('image/jpeg');
48
-			$preview->setCropped(true);
49
-			$preview->setEncrypted(false);
50
-			$preview->setMimetype('image/jpeg');
51
-			$preview->setEtag('abc');
52
-			$preview->setMtime((new \DateTime())->getTimestamp());
53
-			$preview->setSize(0);
54
-			$this->previewMapper->insert($preview);
55
-		}
56
-
57
-		$files = iterator_to_array($this->previewService->getAvailableFileIds());
58
-		$this->assertCount(1, $files);
59
-		$this->assertCount(10, $files[0]['fileIds']);
60
-	}
23
+    private PreviewService $previewService;
24
+    private PreviewMapper $previewMapper;
25
+    private IGenerator $snowflakeGenerator;
26
+
27
+    protected function setUp(): void {
28
+        $this->previewService = Server::get(PreviewService::class);
29
+        $this->previewMapper = Server::get(PreviewMapper::class);
30
+        $this->snowflakeGenerator = Server::get(IGenerator::class);
31
+        $this->previewService->deleteAll();
32
+    }
33
+
34
+    public function tearDown(): void {
35
+        $this->previewService->deleteAll();
36
+    }
37
+
38
+    public function testGetAvailableFileIds(): void {
39
+        foreach (range(1, 20) as $i) {
40
+            $preview = new Preview();
41
+            $preview->setId($this->snowflakeGenerator->nextId());
42
+            $preview->setFileId($i % 10);
43
+            $preview->setStorageId(1);
44
+            $preview->setWidth($i);
45
+            $preview->setHeight($i);
46
+            $preview->setMax(true);
47
+            $preview->setSourceMimeType('image/jpeg');
48
+            $preview->setCropped(true);
49
+            $preview->setEncrypted(false);
50
+            $preview->setMimetype('image/jpeg');
51
+            $preview->setEtag('abc');
52
+            $preview->setMtime((new \DateTime())->getTimestamp());
53
+            $preview->setSize(0);
54
+            $this->previewMapper->insert($preview);
55
+        }
56
+
57
+        $files = iterator_to_array($this->previewService->getAvailableFileIds());
58
+        $this->assertCount(1, $files);
59
+        $this->assertCount(10, $files[0]['fileIds']);
60
+    }
61 61
 }
Please login to merge, or discard this patch.
tests/lib/Preview/PreviewMapperTest.php 1 patch
Indentation   +62 added lines, -62 removed lines patch added patch discarded remove patch
@@ -19,76 +19,76 @@
 block discarded – undo
19 19
 
20 20
 #[\PHPUnit\Framework\Attributes\Group('DB')]
21 21
 class PreviewMapperTest extends TestCase {
22
-	private PreviewMapper $previewMapper;
23
-	private IDBConnection $connection;
24
-	private IGenerator $snowflake;
22
+    private PreviewMapper $previewMapper;
23
+    private IDBConnection $connection;
24
+    private IGenerator $snowflake;
25 25
 
26
-	public function setUp(): void {
27
-		$this->previewMapper = Server::get(PreviewMapper::class);
28
-		$this->connection = Server::get(IDBConnection::class);
29
-		$this->snowflake = Server::get(IGenerator::class);
26
+    public function setUp(): void {
27
+        $this->previewMapper = Server::get(PreviewMapper::class);
28
+        $this->connection = Server::get(IDBConnection::class);
29
+        $this->snowflake = Server::get(IGenerator::class);
30 30
 
31
-		$qb = $this->connection->getQueryBuilder();
32
-		$qb->delete('preview_locations')->executeStatement();
31
+        $qb = $this->connection->getQueryBuilder();
32
+        $qb->delete('preview_locations')->executeStatement();
33 33
 
34
-		$qb = $this->connection->getQueryBuilder();
35
-		$qb->delete('preview_versions')->executeStatement();
34
+        $qb = $this->connection->getQueryBuilder();
35
+        $qb->delete('preview_versions')->executeStatement();
36 36
 
37
-		$qb = $this->connection->getQueryBuilder();
38
-		$qb->delete('previews')->executeStatement();
39
-	}
37
+        $qb = $this->connection->getQueryBuilder();
38
+        $qb->delete('previews')->executeStatement();
39
+    }
40 40
 
41
-	public function testGetAvailablePreviews(): void {
42
-		// Empty
43
-		$this->assertEquals([], $this->previewMapper->getAvailablePreviews([]));
41
+    public function testGetAvailablePreviews(): void {
42
+        // Empty
43
+        $this->assertEquals([], $this->previewMapper->getAvailablePreviews([]));
44 44
 
45
-		// No preview available
46
-		$this->assertEquals([42 => []], $this->previewMapper->getAvailablePreviews([42]));
45
+        // No preview available
46
+        $this->assertEquals([42 => []], $this->previewMapper->getAvailablePreviews([42]));
47 47
 
48
-		$this->createPreviewForFileId(42);
49
-		$previews = $this->previewMapper->getAvailablePreviews([42]);
50
-		$this->assertNotEmpty($previews[42]);
51
-		$this->assertNull($previews[42][0]->getLocationId());
52
-		$this->assertNull($previews[42][0]->getBucketName());
53
-		$this->assertNull($previews[42][0]->getObjectStoreName());
48
+        $this->createPreviewForFileId(42);
49
+        $previews = $this->previewMapper->getAvailablePreviews([42]);
50
+        $this->assertNotEmpty($previews[42]);
51
+        $this->assertNull($previews[42][0]->getLocationId());
52
+        $this->assertNull($previews[42][0]->getBucketName());
53
+        $this->assertNull($previews[42][0]->getObjectStoreName());
54 54
 
55
-		$this->createPreviewForFileId(43, 2);
56
-		$previews = $this->previewMapper->getAvailablePreviews([43]);
57
-		$this->assertNotEmpty($previews[43]);
58
-		$this->assertEquals('preview-2', $previews[43][0]->getBucketName());
59
-		$this->assertEquals('default', $previews[43][0]->getObjectStoreName());
60
-	}
55
+        $this->createPreviewForFileId(43, 2);
56
+        $previews = $this->previewMapper->getAvailablePreviews([43]);
57
+        $this->assertNotEmpty($previews[43]);
58
+        $this->assertEquals('preview-2', $previews[43][0]->getBucketName());
59
+        $this->assertEquals('default', $previews[43][0]->getObjectStoreName());
60
+    }
61 61
 
62
-	private function createPreviewForFileId(int $fileId, ?int $bucket = null): void {
63
-		$locationId = null;
64
-		if ($bucket) {
65
-			$qb = $this->connection->getQueryBuilder();
66
-			$locationId = $this->snowflake->nextId();
67
-			$qb->insert('preview_locations')
68
-				->values([
69
-					'id' => $locationId,
70
-					'bucket_name' => $qb->createNamedParameter('preview-' . $bucket),
71
-					'object_store_name' => $qb->createNamedParameter('default'),
72
-				]);
73
-			$qb->executeStatement();
74
-		}
75
-		$preview = new Preview();
76
-		$preview->setId($this->snowflake->nextId());
77
-		$preview->setFileId($fileId);
78
-		$preview->setStorageId(1);
79
-		$preview->setCropped(true);
80
-		$preview->setMax(true);
81
-		$preview->setWidth(100);
82
-		$preview->setSourceMimeType('image/jpeg');
83
-		$preview->setHeight(100);
84
-		$preview->setSize(100);
85
-		$preview->setMtime(time());
86
-		$preview->setMimetype('image/jpeg');
87
-		$preview->setEtag('abcdefg');
62
+    private function createPreviewForFileId(int $fileId, ?int $bucket = null): void {
63
+        $locationId = null;
64
+        if ($bucket) {
65
+            $qb = $this->connection->getQueryBuilder();
66
+            $locationId = $this->snowflake->nextId();
67
+            $qb->insert('preview_locations')
68
+                ->values([
69
+                    'id' => $locationId,
70
+                    'bucket_name' => $qb->createNamedParameter('preview-' . $bucket),
71
+                    'object_store_name' => $qb->createNamedParameter('default'),
72
+                ]);
73
+            $qb->executeStatement();
74
+        }
75
+        $preview = new Preview();
76
+        $preview->setId($this->snowflake->nextId());
77
+        $preview->setFileId($fileId);
78
+        $preview->setStorageId(1);
79
+        $preview->setCropped(true);
80
+        $preview->setMax(true);
81
+        $preview->setWidth(100);
82
+        $preview->setSourceMimeType('image/jpeg');
83
+        $preview->setHeight(100);
84
+        $preview->setSize(100);
85
+        $preview->setMtime(time());
86
+        $preview->setMimetype('image/jpeg');
87
+        $preview->setEtag('abcdefg');
88 88
 
89
-		if ($locationId !== null) {
90
-			$preview->setLocationId($locationId);
91
-		}
92
-		$this->previewMapper->insert($preview);
93
-	}
89
+        if ($locationId !== null) {
90
+            $preview->setLocationId($locationId);
91
+        }
92
+        $this->previewMapper->insert($preview);
93
+    }
94 94
 }
Please login to merge, or discard this patch.
tests/lib/Preview/MovePreviewJobTest.php 1 patch
Indentation   +191 added lines, -191 removed lines patch added patch discarded remove patch
@@ -32,195 +32,195 @@
 block discarded – undo
32 32
 
33 33
 #[\PHPUnit\Framework\Attributes\Group('DB')]
34 34
 class MovePreviewJobTest extends TestCase {
35
-	private IAppData $previewAppData;
36
-	private PreviewMapper $previewMapper;
37
-	private IAppConfig&MockObject $appConfig;
38
-	private IConfig $config;
39
-	private StorageFactory $storageFactory;
40
-	private PreviewService $previewService;
41
-	private IDBConnection $db;
42
-	private IMimeTypeLoader&MockObject $mimeTypeLoader;
43
-	private IMimeTypeDetector&MockObject $mimeTypeDetector;
44
-	private LoggerInterface&MockObject $logger;
45
-
46
-	public function setUp(): void {
47
-		parent::setUp();
48
-		$this->previewAppData = Server::get(IAppDataFactory::class)->get('preview');
49
-		$this->previewMapper = Server::get(PreviewMapper::class);
50
-		$this->config = Server::get(IConfig::class);
51
-		$this->appConfig = $this->createMock(IAppConfig::class);
52
-		$this->appConfig->expects($this->any())
53
-			->method('getValueBool')
54
-			->willReturn(false);
55
-		$this->appConfig->expects($this->any())
56
-			->method('setValueBool')
57
-			->willReturn(true);
58
-		$this->storageFactory = Server::get(StorageFactory::class);
59
-		$this->previewService = Server::get(PreviewService::class);
60
-		$this->db = Server::get(IDBConnection::class);
61
-
62
-		$qb = $this->db->getQueryBuilder();
63
-		$qb->delete('filecache')
64
-			->where($qb->expr()->eq('fileid', $qb->createNamedParameter(5)))
65
-			->executeStatement();
66
-
67
-		$qb = $this->db->getQueryBuilder();
68
-		$qb->insert('filecache')
69
-			->values([
70
-				'fileid' => $qb->createNamedParameter(5),
71
-				'storage' => $qb->createNamedParameter(1),
72
-				'path' => $qb->createNamedParameter('test/abc'),
73
-				'path_hash' => $qb->createNamedParameter(md5('test')),
74
-				'parent' => $qb->createNamedParameter(0),
75
-				'name' => $qb->createNamedParameter('abc'),
76
-				'mimetype' => $qb->createNamedParameter(42),
77
-				'size' => $qb->createNamedParameter(1000),
78
-				'mtime' => $qb->createNamedParameter(1000),
79
-				'storage_mtime' => $qb->createNamedParameter(1000),
80
-				'encrypted' => $qb->createNamedParameter(0),
81
-				'unencrypted_size' => $qb->createNamedParameter(0),
82
-				'etag' => $qb->createNamedParameter('abcdefg'),
83
-				'permissions' => $qb->createNamedParameter(0),
84
-				'checksum' => $qb->createNamedParameter('abcdefg'),
85
-			])->executeStatement();
86
-
87
-		$this->mimeTypeDetector = $this->createMock(IMimeTypeDetector::class);
88
-		$this->mimeTypeDetector->method('detectPath')->willReturn('image/png');
89
-		$this->mimeTypeLoader = $this->createMock(IMimeTypeLoader::class);
90
-		$this->mimeTypeLoader->method('getId')->with('image/png')->willReturn(42);
91
-		$this->mimeTypeLoader->method('getMimetypeById')->with(42)->willReturn('image/png');
92
-		$this->logger = $this->createMock(LoggerInterface::class);
93
-	}
94
-
95
-	public function tearDown(): void {
96
-		foreach ($this->previewAppData->getDirectoryListing() as $folder) {
97
-			$folder->delete();
98
-		}
99
-		$this->previewService->deleteAll();
100
-
101
-		$qb = $this->db->getQueryBuilder();
102
-		$qb->delete('filecache')
103
-			->where($qb->expr()->eq('fileid', $qb->createNamedParameter(5)))
104
-			->executeStatement();
105
-	}
106
-
107
-	#[TestDox('Test the migration from the legacy flat hierarchy to the new database format')]
108
-	public function testMigrationLegacyPath(): void {
109
-		$folder = $this->previewAppData->newFolder('5');
110
-		$folder->newFile('64-64-crop.jpg', 'abcdefg');
111
-		$folder->newFile('128-128-crop.png', 'abcdefg');
112
-		$this->assertEquals(1, count($this->previewAppData->getDirectoryListing()));
113
-		$this->assertEquals(2, count($folder->getDirectoryListing()));
114
-		$this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5))));
115
-
116
-		$job = new MovePreviewJob(
117
-			Server::get(ITimeFactory::class),
118
-			$this->appConfig,
119
-			$this->config,
120
-			$this->previewMapper,
121
-			$this->storageFactory,
122
-			Server::get(IDBConnection::class),
123
-			Server::get(IRootFolder::class),
124
-			$this->mimeTypeDetector,
125
-			$this->mimeTypeLoader,
126
-			$this->logger,
127
-			Server::get(IGenerator::class),
128
-			Server::get(IAppDataFactory::class),
129
-		);
130
-		$this->invokePrivate($job, 'run', [[]]);
131
-		$this->assertEquals(0, count($this->previewAppData->getDirectoryListing()));
132
-		$this->assertEquals(2, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5))));
133
-	}
134
-
135
-	private static function getInternalFolder(string $name): string {
136
-		return implode('/', str_split(substr(md5($name), 0, 7))) . '/' . $name;
137
-	}
138
-
139
-	#[TestDox("Test the migration from the 'new' nested hierarchy to the database format")]
140
-	public function testMigrationPath(): void {
141
-		$folder = $this->previewAppData->newFolder(self::getInternalFolder((string)5));
142
-		$folder->newFile('64-64-crop.jpg', 'abcdefg');
143
-		$folder->newFile('128-128-crop.png', 'abcdefg');
144
-
145
-		$folder = $this->previewAppData->getFolder(self::getInternalFolder((string)5));
146
-		$this->assertEquals(2, count($folder->getDirectoryListing()));
147
-		$this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5))));
148
-
149
-		$job = new MovePreviewJob(
150
-			Server::get(ITimeFactory::class),
151
-			$this->appConfig,
152
-			$this->config,
153
-			$this->previewMapper,
154
-			$this->storageFactory,
155
-			Server::get(IDBConnection::class),
156
-			Server::get(IRootFolder::class),
157
-			$this->mimeTypeDetector,
158
-			$this->mimeTypeLoader,
159
-			$this->logger,
160
-			Server::get(IGenerator::class),
161
-			Server::get(IAppDataFactory::class)
162
-		);
163
-		$this->invokePrivate($job, 'run', [[]]);
164
-		$this->assertEquals(0, count($this->previewAppData->getDirectoryListing()));
165
-		$this->assertEquals(2, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5))));
166
-	}
167
-
168
-	#[TestDox("Test the migration from the 'new' nested hierarchy to the database format")]
169
-	public function testMigrationPathWithVersion(): void {
170
-		$folder = $this->previewAppData->newFolder(self::getInternalFolder((string)5));
171
-		// No version
172
-		$folder->newFile('128-128-crop.png', 'abcdefg');
173
-		$folder->newFile('256-256-max.png', 'abcdefg');
174
-		$folder->newFile('128-128.png', 'abcdefg');
175
-
176
-		// Version 1000
177
-		$folder->newFile('1000-128-128-crop.png', 'abcdefg');
178
-		$folder->newFile('1000-256-256-max.png', 'abcdefg');
179
-		$folder->newFile('1000-128-128.png', 'abcdefg');
180
-
181
-		// Version 1001
182
-		$folder->newFile('1001-128-128-crop.png', 'abcdefg');
183
-		$folder->newFile('1001-256-256-max.png', 'abcdefg');
184
-		$folder->newFile('1001-128-128.png', 'abcdefg');
185
-
186
-		$folder = $this->previewAppData->getFolder(self::getInternalFolder((string)5));
187
-		$this->assertEquals(9, count($folder->getDirectoryListing()));
188
-		$this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5))));
189
-
190
-		$job = new MovePreviewJob(
191
-			Server::get(ITimeFactory::class),
192
-			$this->appConfig,
193
-			$this->config,
194
-			$this->previewMapper,
195
-			$this->storageFactory,
196
-			Server::get(IDBConnection::class),
197
-			Server::get(IRootFolder::class),
198
-			$this->mimeTypeDetector,
199
-			$this->mimeTypeLoader,
200
-			$this->logger,
201
-			Server::get(IGenerator::class),
202
-			Server::get(IAppDataFactory::class)
203
-		);
204
-		$this->invokePrivate($job, 'run', [[]]);
205
-		$previews = iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5));
206
-		$this->assertEquals(9, count($previews));
207
-		$this->assertEquals(0, count($this->previewAppData->getDirectoryListing()));
208
-
209
-		$nameVersionMapping = [];
210
-		foreach ($previews as $preview) {
211
-			$nameVersionMapping[$preview->getName($this->mimeTypeLoader)] = $preview->getVersion();
212
-		}
213
-
214
-		$this->assertEquals([
215
-			'1000-128-128-crop.png' => 1000,
216
-			'1000-128-128.png' => 1000,
217
-			'1000-256-256-max.png' => 1000,
218
-			'1001-128-128-crop.png' => 1001,
219
-			'1001-128-128.png' => 1001,
220
-			'1001-256-256-max.png' => 1001,
221
-			'128-128-crop.png' => null,
222
-			'128-128.png' => null,
223
-			'256-256-max.png' => null,
224
-		], $nameVersionMapping);
225
-	}
35
+    private IAppData $previewAppData;
36
+    private PreviewMapper $previewMapper;
37
+    private IAppConfig&MockObject $appConfig;
38
+    private IConfig $config;
39
+    private StorageFactory $storageFactory;
40
+    private PreviewService $previewService;
41
+    private IDBConnection $db;
42
+    private IMimeTypeLoader&MockObject $mimeTypeLoader;
43
+    private IMimeTypeDetector&MockObject $mimeTypeDetector;
44
+    private LoggerInterface&MockObject $logger;
45
+
46
+    public function setUp(): void {
47
+        parent::setUp();
48
+        $this->previewAppData = Server::get(IAppDataFactory::class)->get('preview');
49
+        $this->previewMapper = Server::get(PreviewMapper::class);
50
+        $this->config = Server::get(IConfig::class);
51
+        $this->appConfig = $this->createMock(IAppConfig::class);
52
+        $this->appConfig->expects($this->any())
53
+            ->method('getValueBool')
54
+            ->willReturn(false);
55
+        $this->appConfig->expects($this->any())
56
+            ->method('setValueBool')
57
+            ->willReturn(true);
58
+        $this->storageFactory = Server::get(StorageFactory::class);
59
+        $this->previewService = Server::get(PreviewService::class);
60
+        $this->db = Server::get(IDBConnection::class);
61
+
62
+        $qb = $this->db->getQueryBuilder();
63
+        $qb->delete('filecache')
64
+            ->where($qb->expr()->eq('fileid', $qb->createNamedParameter(5)))
65
+            ->executeStatement();
66
+
67
+        $qb = $this->db->getQueryBuilder();
68
+        $qb->insert('filecache')
69
+            ->values([
70
+                'fileid' => $qb->createNamedParameter(5),
71
+                'storage' => $qb->createNamedParameter(1),
72
+                'path' => $qb->createNamedParameter('test/abc'),
73
+                'path_hash' => $qb->createNamedParameter(md5('test')),
74
+                'parent' => $qb->createNamedParameter(0),
75
+                'name' => $qb->createNamedParameter('abc'),
76
+                'mimetype' => $qb->createNamedParameter(42),
77
+                'size' => $qb->createNamedParameter(1000),
78
+                'mtime' => $qb->createNamedParameter(1000),
79
+                'storage_mtime' => $qb->createNamedParameter(1000),
80
+                'encrypted' => $qb->createNamedParameter(0),
81
+                'unencrypted_size' => $qb->createNamedParameter(0),
82
+                'etag' => $qb->createNamedParameter('abcdefg'),
83
+                'permissions' => $qb->createNamedParameter(0),
84
+                'checksum' => $qb->createNamedParameter('abcdefg'),
85
+            ])->executeStatement();
86
+
87
+        $this->mimeTypeDetector = $this->createMock(IMimeTypeDetector::class);
88
+        $this->mimeTypeDetector->method('detectPath')->willReturn('image/png');
89
+        $this->mimeTypeLoader = $this->createMock(IMimeTypeLoader::class);
90
+        $this->mimeTypeLoader->method('getId')->with('image/png')->willReturn(42);
91
+        $this->mimeTypeLoader->method('getMimetypeById')->with(42)->willReturn('image/png');
92
+        $this->logger = $this->createMock(LoggerInterface::class);
93
+    }
94
+
95
+    public function tearDown(): void {
96
+        foreach ($this->previewAppData->getDirectoryListing() as $folder) {
97
+            $folder->delete();
98
+        }
99
+        $this->previewService->deleteAll();
100
+
101
+        $qb = $this->db->getQueryBuilder();
102
+        $qb->delete('filecache')
103
+            ->where($qb->expr()->eq('fileid', $qb->createNamedParameter(5)))
104
+            ->executeStatement();
105
+    }
106
+
107
+    #[TestDox('Test the migration from the legacy flat hierarchy to the new database format')]
108
+    public function testMigrationLegacyPath(): void {
109
+        $folder = $this->previewAppData->newFolder('5');
110
+        $folder->newFile('64-64-crop.jpg', 'abcdefg');
111
+        $folder->newFile('128-128-crop.png', 'abcdefg');
112
+        $this->assertEquals(1, count($this->previewAppData->getDirectoryListing()));
113
+        $this->assertEquals(2, count($folder->getDirectoryListing()));
114
+        $this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5))));
115
+
116
+        $job = new MovePreviewJob(
117
+            Server::get(ITimeFactory::class),
118
+            $this->appConfig,
119
+            $this->config,
120
+            $this->previewMapper,
121
+            $this->storageFactory,
122
+            Server::get(IDBConnection::class),
123
+            Server::get(IRootFolder::class),
124
+            $this->mimeTypeDetector,
125
+            $this->mimeTypeLoader,
126
+            $this->logger,
127
+            Server::get(IGenerator::class),
128
+            Server::get(IAppDataFactory::class),
129
+        );
130
+        $this->invokePrivate($job, 'run', [[]]);
131
+        $this->assertEquals(0, count($this->previewAppData->getDirectoryListing()));
132
+        $this->assertEquals(2, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5))));
133
+    }
134
+
135
+    private static function getInternalFolder(string $name): string {
136
+        return implode('/', str_split(substr(md5($name), 0, 7))) . '/' . $name;
137
+    }
138
+
139
+    #[TestDox("Test the migration from the 'new' nested hierarchy to the database format")]
140
+    public function testMigrationPath(): void {
141
+        $folder = $this->previewAppData->newFolder(self::getInternalFolder((string)5));
142
+        $folder->newFile('64-64-crop.jpg', 'abcdefg');
143
+        $folder->newFile('128-128-crop.png', 'abcdefg');
144
+
145
+        $folder = $this->previewAppData->getFolder(self::getInternalFolder((string)5));
146
+        $this->assertEquals(2, count($folder->getDirectoryListing()));
147
+        $this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5))));
148
+
149
+        $job = new MovePreviewJob(
150
+            Server::get(ITimeFactory::class),
151
+            $this->appConfig,
152
+            $this->config,
153
+            $this->previewMapper,
154
+            $this->storageFactory,
155
+            Server::get(IDBConnection::class),
156
+            Server::get(IRootFolder::class),
157
+            $this->mimeTypeDetector,
158
+            $this->mimeTypeLoader,
159
+            $this->logger,
160
+            Server::get(IGenerator::class),
161
+            Server::get(IAppDataFactory::class)
162
+        );
163
+        $this->invokePrivate($job, 'run', [[]]);
164
+        $this->assertEquals(0, count($this->previewAppData->getDirectoryListing()));
165
+        $this->assertEquals(2, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5))));
166
+    }
167
+
168
+    #[TestDox("Test the migration from the 'new' nested hierarchy to the database format")]
169
+    public function testMigrationPathWithVersion(): void {
170
+        $folder = $this->previewAppData->newFolder(self::getInternalFolder((string)5));
171
+        // No version
172
+        $folder->newFile('128-128-crop.png', 'abcdefg');
173
+        $folder->newFile('256-256-max.png', 'abcdefg');
174
+        $folder->newFile('128-128.png', 'abcdefg');
175
+
176
+        // Version 1000
177
+        $folder->newFile('1000-128-128-crop.png', 'abcdefg');
178
+        $folder->newFile('1000-256-256-max.png', 'abcdefg');
179
+        $folder->newFile('1000-128-128.png', 'abcdefg');
180
+
181
+        // Version 1001
182
+        $folder->newFile('1001-128-128-crop.png', 'abcdefg');
183
+        $folder->newFile('1001-256-256-max.png', 'abcdefg');
184
+        $folder->newFile('1001-128-128.png', 'abcdefg');
185
+
186
+        $folder = $this->previewAppData->getFolder(self::getInternalFolder((string)5));
187
+        $this->assertEquals(9, count($folder->getDirectoryListing()));
188
+        $this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5))));
189
+
190
+        $job = new MovePreviewJob(
191
+            Server::get(ITimeFactory::class),
192
+            $this->appConfig,
193
+            $this->config,
194
+            $this->previewMapper,
195
+            $this->storageFactory,
196
+            Server::get(IDBConnection::class),
197
+            Server::get(IRootFolder::class),
198
+            $this->mimeTypeDetector,
199
+            $this->mimeTypeLoader,
200
+            $this->logger,
201
+            Server::get(IGenerator::class),
202
+            Server::get(IAppDataFactory::class)
203
+        );
204
+        $this->invokePrivate($job, 'run', [[]]);
205
+        $previews = iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5));
206
+        $this->assertEquals(9, count($previews));
207
+        $this->assertEquals(0, count($this->previewAppData->getDirectoryListing()));
208
+
209
+        $nameVersionMapping = [];
210
+        foreach ($previews as $preview) {
211
+            $nameVersionMapping[$preview->getName($this->mimeTypeLoader)] = $preview->getVersion();
212
+        }
213
+
214
+        $this->assertEquals([
215
+            '1000-128-128-crop.png' => 1000,
216
+            '1000-128-128.png' => 1000,
217
+            '1000-256-256-max.png' => 1000,
218
+            '1001-128-128-crop.png' => 1001,
219
+            '1001-128-128.png' => 1001,
220
+            '1001-256-256-max.png' => 1001,
221
+            '128-128-crop.png' => null,
222
+            '128-128.png' => null,
223
+            '256-256-max.png' => null,
224
+        ], $nameVersionMapping);
225
+    }
226 226
 }
Please login to merge, or discard this patch.