Preview   F
last analyzed

Complexity

Total Complexity 168

Size/Duplication

Total Lines 1283
Duplicated Lines 1.71 %

Coupling/Cohesion

Components 1
Dependencies 19

Importance

Changes 0
Metric Value
dl 22
loc 1283
rs 0.8
c 0
b 0
f 0
wmc 168
lcom 1
cbo 19

54 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 41 5
A getFile() 0 3 1
A getMaxX() 0 3 1
A getMaxY() 0 3 1
A getScalingUp() 0 3 1
A getThumbnailsFolder() 0 3 1
A getMaxScaleFactor() 0 3 1
A getConfigMaxX() 0 3 1
A getConfigMaxY() 0 3 1
A getFileInfo() 0 3 1
A getChildren() 0 10 2
A setFile() 0 7 1
A setMimetype() 0 3 1
A setMaxX() 0 10 2
A setMaxY() 0 10 2
A setScalingup() 0 8 2
A setMode() 0 5 1
A setKeepAspect() 0 5 1
A isFileValid() 0 16 3
A deletePreview() 0 11 4
A deleteAllPreviews() 0 24 4
B isCached() 0 61 9
A getMaxPreviewSize() 0 14 3
A thumbnailSizeExists() 0 9 3
A simulatePreviewDimensions() 0 13 2
A applyAspectRatio() 6 22 5
A applyCover() 6 16 3
A fixSize() 0 8 2
A isCachedBigger() 0 18 3
A getPossibleThumbnails() 0 24 5
A getDimensionsFromFilename() 0 8 1
A unscalable() 0 19 5
B getPreview() 0 27 8
A showPreview() 0 18 5
B getCachedPreview() 0 26 6
C resizeAndStore() 0 68 10
B scale() 0 38 6
A crop() 0 8 1
A cropAndFill() 10 35 3
A storePreview() 0 11 3
B buildCachePath() 0 25 9
A getPreviewPath() 0 10 3
B generatePreview() 0 51 9
A getMimeIcon() 0 12 2
A storeMaxPreview() 0 25 4
A limitMaxDim() 0 12 3
A post_write() 0 3 1
A prepare_delete_files() 0 3 1
A prepare_delete() 0 19 5
A addPathToDeleteFileMapper() 0 3 1
A getAllChildren() 0 17 3
A post_delete_files() 0 3 1
A post_delete_versions() 0 3 1
A post_delete() 0 22 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Preview often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Preview, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Björn Schießle <[email protected]>
4
 * @author Frank Karlitschek <[email protected]>
5
 * @author Georg Ehrke <[email protected]>
6
 * @author Joas Schilling <[email protected]>
7
 * @author Jörn Friedrich Dreyer <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Olivier Paroz <[email protected]>
11
 * @author Robin Appelman <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 * @author Thomas Müller <[email protected]>
14
 * @author Tobias Kaminsky <[email protected]>
15
 *
16
 * @copyright Copyright (c) 2018, ownCloud GmbH
17
 * @license AGPL-3.0
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
namespace OC;
33
34
use OC\Files\Filesystem;
35
use OC\Files\View;
36
use OCP\Files\File;
37
use OCP\Files\FileInfo;
38
use OCP\Files\Folder;
39
use OCP\Files\Node;
40
use OCP\Files\NotFoundException;
41
use OCP\IImage;
42
use OCP\Preview\IProvider2;
43
use OCP\Response;
44
use OCP\Util;
45
46
class Preview {
47
	//the thumbnail folder
48
	const THUMBNAILS_FOLDER = 'thumbnails';
49
50
	const MODE_FILL = 'fill';
51
	const MODE_COVER = 'cover';
52
53
	//config
54
	private $maxScaleFactor;
55
	/** @var int maximum width allowed for a preview */
56
	private $configMaxWidth;
57
	/** @var int maximum height allowed for a preview */
58
	private $configMaxHeight;
59
60
	//fileview object
61
	private $fileView = null;
62
	private $userView = null;
63
64
	//vars
65
	/** @var File */
66
	private $file;
67
	private $maxX;
68
	private $maxY;
69
	private $scalingUp;
70
	private $mimeType;
71
	private $keepAspect = false;
72
	private $mode = self::MODE_FILL;
73
74
	//used to calculate the size of the preview to generate
75
	/** @var int $maxPreviewWidth max width a preview can have */
76
	private $maxPreviewWidth;
77
	/** @var int $maxPreviewHeight max height a preview can have */
78
	private $maxPreviewHeight;
79
	/** @var int $previewWidth calculated width of the preview we're looking for */
80
	private $previewWidth;
81
	/** @var int $previewHeight calculated height of the preview we're looking for */
82
	private $previewHeight;
83
84
	// filemapper used for deleting previews
85
	// index is path, value is fileinfo
86
	public static $deleteFileMapper = [];
87
	public static $deleteChildrenMapper = [];
88
89
	/**
90
	 * preview images object
91
	 *
92
	 * @var IImage
93
	 */
94
	private $preview;
95
	/** @var string */
96
	private $versionId;
97
98
	/**
99
	 * check if thumbnail or bigger version of thumbnail of file is cached
100
	 *
101
	 * @param string $user userid - if no user is given, OC_User::getUser will be used
102
	 * @param string $root path of root
103
	 * @param Node $file The path to the file where you want a thumbnail from
104
	 * @param int $maxX The maximum X size of the thumbnail. It can be smaller depending on the
105
	 *     shape of the image
106
	 * @param int $maxY The maximum Y size of the thumbnail. It can be smaller depending on the
107
	 *     shape of the image
108
	 * @param bool $scalingUp Disable/Enable up-scaling of previews
109
	 * @param string $versionId
110
	 * @throws \Exception
111
	 */
112
	public function __construct(
113
		$user = '',
114
		$root = '/',
115
		Node $file = null, $maxX = 1,
116
		$maxY = 1,
117
		$scalingUp = true,
118
		$versionId = null
119
	) {
120
		//init fileviews
121
		if ($user === '') {
122
			$user = \OC_User::getUser();
123
		}
124
		$this->fileView = new View('/' . $user . '/' . $root);
125
		$this->userView = new View('/' . $user);
126
127
		//set config
128
		$sysConfig = \OC::$server->getConfig();
129
		$this->configMaxWidth = $sysConfig->getSystemValue('preview_max_x', 2048);
130
		$this->configMaxHeight = $sysConfig->getSystemValue('preview_max_y', 2048);
131
		$this->maxScaleFactor = $sysConfig->getSystemValue('preview_max_scale_factor', 2);
132
133
		//save parameters
134
		if ($file !== null) {
135
			$this->setFile($file, $versionId);
136
		}
137
		$this->setMaxX((int)$maxX);
138
		$this->setMaxY((int)$maxY);
139
		$this->setScalingup($scalingUp);
140
141
		$this->preview = null;
142
143
		//check if there are preview backends
144
		if (!\OC::$server->getPreviewManager()
145
				->hasProviders()
146
			&& \OC::$server->getConfig()
147
				->getSystemValue('enable_previews', true)
148
		) {
149
			Util::writeLog('core', 'No preview providers exist', Util::ERROR);
150
			throw new \Exception('No preview providers');
151
		}
152
	}
153
154
	/**
155
	 * returns the path of the file you want a thumbnail from
156
	 *
157
	 * @return File
158
	 */
159
	public function getFile() {
160
		return $this->file;
161
	}
162
163
	/**
164
	 * returns the max width of the preview
165
	 *
166
	 * @return integer
167
	 */
168
	public function getMaxX() {
169
		return $this->maxX;
170
	}
171
172
	/**
173
	 * returns the max height of the preview
174
	 *
175
	 * @return integer
176
	 */
177
	public function getMaxY() {
178
		return $this->maxY;
179
	}
180
181
	/**
182
	 * returns whether or not scalingup is enabled
183
	 *
184
	 * @return bool
185
	 */
186
	public function getScalingUp() {
187
		return $this->scalingUp;
188
	}
189
190
	/**
191
	 * returns the name of the thumbnailfolder
192
	 *
193
	 * @return string
194
	 */
195
	public function getThumbnailsFolder() {
196
		return self::THUMBNAILS_FOLDER;
197
	}
198
199
	/**
200
	 * returns the max scale factor
201
	 *
202
	 * @return string
203
	 */
204
	public function getMaxScaleFactor() {
205
		return $this->maxScaleFactor;
206
	}
207
208
	/**
209
	 * returns the max width set in ownCloud's config
210
	 *
211
	 * @return integer
212
	 */
213
	public function getConfigMaxX() {
214
		return $this->configMaxWidth;
215
	}
216
217
	/**
218
	 * returns the max height set in ownCloud's config
219
	 *
220
	 * @return integer
221
	 */
222
	public function getConfigMaxY() {
223
		return $this->configMaxHeight;
224
	}
225
226
	/**
227
	 * Returns the FileInfo object associated with the file to preview
228
	 *
229
	 * @return false|Files\FileInfo|\OCP\Files\FileInfo
230
	 */
231
	protected function getFileInfo() {
232
		return $this->file;
233
	}
234
235
	/**
236
	 * @return array|null
237
	 */
238
	private function getChildren() {
239
		$absPath = $this->file->getPath();
240
		$absPath = Files\Filesystem::normalizePath($absPath);
241
242
		if (\array_key_exists($absPath, self::$deleteChildrenMapper)) {
243
			return self::$deleteChildrenMapper[$absPath];
244
		}
245
246
		return null;
247
	}
248
249
	/**
250
	 * Sets the path of the file you want a preview of
251
	 *
252
	 * @param Node $file
253
	 * @param string $versionId
254
	 *
255
	 * @return Preview
256
	 */
257
	public function setFile(Node $file, $versionId = null) {
258
		$this->file = $file;
0 ignored issues
show
Documentation Bug introduced by
$file is of type object<OCP\Files\Node>, but the property $file was declared to be of type object<OCP\Files\File>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
259
		$this->versionId = $versionId;
260
		$this->mimeType = $this->file->getMimetype();
261
262
		return $this;
263
	}
264
265
	/**
266
	 * Forces the use of a specific media type
267
	 *
268
	 * @param string $mimeType
269
	 */
270
	public function setMimetype($mimeType) {
271
		$this->mimeType = $mimeType;
272
	}
273
274
	/**
275
	 * Sets the max width of the preview. It's capped by the maximum allowed size set in the
276
	 * configuration
277
	 *
278
	 * @param int $maxX
279
	 *
280
	 * @throws \Exception
281
	 * @return \OC\Preview
282
	 */
283
	public function setMaxX($maxX = 1) {
284
		if ($maxX <= 0) {
285
			throw new \Exception('Cannot set width of 0 or smaller!');
286
		}
287
		$configMaxX = $this->getConfigMaxX();
288
		$maxX = $this->limitMaxDim($maxX, $configMaxX, 'maxX');
289
		$this->maxX = $maxX;
290
291
		return $this;
292
	}
293
294
	/**
295
	 * Sets the max height of the preview. It's capped by the maximum allowed size set in the
296
	 * configuration
297
	 *
298
	 * @param int $maxY
299
	 *
300
	 * @throws \Exception
301
	 * @return \OC\Preview
302
	 */
303
	public function setMaxY($maxY = 1) {
304
		if ($maxY <= 0) {
305
			throw new \Exception('Cannot set height of 0 or smaller!');
306
		}
307
		$configMaxY = $this->getConfigMaxY();
308
		$maxY = $this->limitMaxDim($maxY, $configMaxY, 'maxY');
309
		$this->maxY = $maxY;
310
311
		return $this;
312
	}
313
314
	/**
315
	 * Sets whether we're allowed to scale up when generating a preview. It's capped by the maximum
316
	 * allowed scale factor set in the configuration
317
	 *
318
	 * @param bool $scalingUp
319
	 *
320
	 * @return \OC\Preview
321
	 */
322
	public function setScalingup($scalingUp) {
323
		if ($this->getMaxScaleFactor() === 1) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $this->getMaxScaleFactor() (string) and 1 (integer) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
324
			$scalingUp = false;
325
		}
326
		$this->scalingUp = $scalingUp;
327
328
		return $this;
329
	}
330
331
	/**
332
	 * Set whether to cover or fill the specified dimensions
333
	 *
334
	 * @param string $mode
335
	 *
336
	 * @return \OC\Preview
337
	 */
338
	public function setMode($mode) {
339
		$this->mode = $mode;
340
341
		return $this;
342
	}
343
344
	/**
345
	 * Sets whether we need to generate a preview which keeps the aspect ratio of the original file
346
	 *
347
	 * @param bool $keepAspect
348
	 *
349
	 * @return \OC\Preview
350
	 */
351
	public function setKeepAspect($keepAspect) {
352
		$this->keepAspect = $keepAspect;
353
354
		return $this;
355
	}
356
357
	/**
358
	 * Makes sure we were given a file to preview and that it exists in the filesystem
359
	 *
360
	 * @return bool
361
	 */
362
	public function isFileValid() {
363
		$file = $this->getFile();
364
		if ($file === null) {
365
			Util::writeLog('core', 'No filename passed', Util::DEBUG);
366
367
			return false;
368
		}
369
370
		if (!$this->getFileInfo() instanceof FileInfo) {
371
			Util::writeLog('core', 'File:"' . $file->getPath() . '" not found', Util::DEBUG);
372
373
			return false;
374
		}
375
376
		return true;
377
	}
378
379
	/**
380
	 * Deletes the preview of a file with specific width and height
381
	 *
382
	 * This should never delete the max preview, use deleteAllPreviews() instead
383
	 *
384
	 * @return bool
385
	 */
386
	public function deletePreview() {
387
		$fileInfo = $this->getFileInfo();
388
		if ($fileInfo !== null && $fileInfo !== false) {
389
			$previewPath = $this->buildCachePath();
390
			if (!\strpos($previewPath, 'max')) {
391
				return $this->userView->unlink($previewPath);
392
			}
393
		}
394
395
		return false;
396
	}
397
398
	/**
399
	 * Deletes all previews of a file
400
	 */
401
	public function deleteAllPreviews() {
402
		$thumbnailMount = $this->userView->getMount($this->getThumbnailsFolder());
403
		$propagator = $thumbnailMount->getStorage()->getPropagator();
404
		$propagator->beginBatch();
405
406
		$toDelete = $this->getChildren();
407
		$toDelete[] = $this->getFileInfo();
408
409
		foreach ($toDelete as $delete) {
410
			if ($delete instanceof FileInfo) {
411
				/** @var \OCP\Files\FileInfo $delete */
412
				$fileId = $delete->getId();
413
414
				// getId() might return null, e.g. when the file is a
415
				// .ocTransferId*.part file from chunked file upload.
416
				if (!empty($fileId)) {
417
					$previewPath = $this->getPreviewPath($fileId);
418
					$this->userView->rmdir($previewPath);
419
				}
420
			}
421
		}
422
423
		$propagator->commitBatch();
424
	}
425
426
	/**
427
	 * Checks if a preview matching the asked dimensions or a bigger version is already cached
428
	 *
429
	 *    * We first retrieve the size of the max preview since this is what we be used to create
430
	 * all our preview. If it doesn't exist we return false, so that it can be generated
431
	 *    * Using the dimensions of the max preview, we calculate what the size of the new
432
	 * thumbnail should be
433
	 *    * And finally, we look for a suitable candidate in the cache
434
	 *
435
	 * @return string|false path to the cached preview if it exists or false
436
	 */
437
	public function isCached() {
438
		$fileId = $this->getFileInfo()->getId();
439
		if ($fileId === null) {
440
			return false;
441
		}
442
443
		/**
444
		 * Phase 1: Looking for the max preview
445
		 */
446
		$previewPath = $this->getPreviewPath();
447
		// We currently can't look for a single file due to bugs related to #16478
448
		$allThumbnails = $this->userView->getDirectoryContent($previewPath);
449
		list($maxPreviewWidth, $maxPreviewHeight) = $this->getMaxPreviewSize($allThumbnails);
450
451
		// Only use the cache if we have a max preview
452
		if ($maxPreviewWidth !== null && $maxPreviewHeight !== null) {
453
454
			/**
455
			 * Phase 2: Calculating the size of the preview we need to send back
456
			 */
457
			$this->maxPreviewWidth = $maxPreviewWidth;
458
			$this->maxPreviewHeight = $maxPreviewHeight;
459
460
			list($previewWidth, $previewHeight) = $this->simulatePreviewDimensions();
461
			if (empty($previewWidth) || empty($previewHeight)) {
462
				return false;
463
			}
464
465
			$this->previewWidth = $previewWidth;
466
			$this->previewHeight = $previewHeight;
467
468
			/**
469
			 * Phase 3: We look for a preview of the exact size
470
			 */
471
			// This gives us a calculated path to a preview of asked dimensions
472
			// thumbnailFolder/fileId/<maxX>-<maxY>(-max|-with-aspect).png
473
			$preview = $this->buildCachePath($previewWidth, $previewHeight);
474
475
			// This checks if we have a preview of those exact dimensions in the cache
476
			if ($this->thumbnailSizeExists($allThumbnails, \basename($preview))) {
477
				return $preview;
478
			}
479
480
			/**
481
			 * Phase 4: We look for a larger preview, matching the aspect ratio
482
			 */
483
			if (($this->getMaxX() >= $maxPreviewWidth)
484
				&& ($this->getMaxY() >= $maxPreviewHeight)
485
			) {
486
				// The preview we-re looking for is the exact size or larger than the max preview,
487
				// so return that
488
				return $this->buildCachePath($maxPreviewWidth, $maxPreviewHeight);
489
			} else {
490
				// The last resort is to look for something bigger than what we've calculated,
491
				// but still smaller than the max preview
492
				return $this->isCachedBigger($allThumbnails);
493
			}
494
		}
495
496
		return false;
497
	}
498
499
	/**
500
	 * Returns the dimensions of the max preview
501
	 *
502
	 * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
503
	 *
504
	 * @return int[]
505
	 */
506
	private function getMaxPreviewSize($allThumbnails) {
507
		$maxPreviewX = null;
508
		$maxPreviewY = null;
509
510
		foreach ($allThumbnails as $thumbnail) {
511
			$name = $thumbnail['name'];
512
			if (\strpos($name, 'max')) {
513
				list($maxPreviewX, $maxPreviewY) = $this->getDimensionsFromFilename($name);
514
				break;
515
			}
516
		}
517
518
		return [$maxPreviewX, $maxPreviewY];
519
	}
520
521
	/**
522
	 * Check if a specific thumbnail size is cached
523
	 *
524
	 * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
525
	 * @param string $name
526
	 * @return bool
527
	 */
528
	private function thumbnailSizeExists(array $allThumbnails, $name) {
529
		foreach ($allThumbnails as $thumbnail) {
530
			if ($name === $thumbnail->getName()) {
531
				return true;
532
			}
533
		}
534
535
		return false;
536
	}
537
538
	/**
539
	 * Determines the size of the preview we should be looking for in the cache
540
	 *
541
	 * @return integer[]
542
	 */
543
	private function simulatePreviewDimensions() {
544
		$askedWidth = $this->getMaxX();
545
		$askedHeight = $this->getMaxY();
546
547
		if ($this->keepAspect) {
548
			list($newPreviewWidth, $newPreviewHeight) =
549
				$this->applyAspectRatio($askedWidth, $askedHeight);
550
		} else {
551
			list($newPreviewWidth, $newPreviewHeight) = $this->fixSize($askedWidth, $askedHeight);
552
		}
553
554
		return [(int)$newPreviewWidth, (int)$newPreviewHeight];
555
	}
556
557
	/**
558
	 * Resizes the boundaries to match the aspect ratio
559
	 *
560
	 * @param int $askedWidth
561
	 * @param int $askedHeight
562
	 *
563
	 * @param int $originalWidth
564
	 * @param int $originalHeight
565
	 * @return integer[]
566
	 */
567
	private function applyAspectRatio($askedWidth, $askedHeight, $originalWidth = 0, $originalHeight = 0) {
568
		if (!$originalWidth) {
569
			$originalWidth = $this->maxPreviewWidth;
570
		}
571
		if (!$originalHeight) {
572
			$originalHeight = $this->maxPreviewHeight;
573
		}
574
		$originalRatio = $originalWidth / $originalHeight;
575
		// Defines the box in which the preview has to fit
576
		$scaleFactor = $this->scalingUp ? $this->maxScaleFactor : 1;
577
		$askedWidth = \min($askedWidth, $originalWidth * $scaleFactor);
578
		$askedHeight = \min($askedHeight, $originalHeight * $scaleFactor);
579
580 View Code Duplication
		if ($askedWidth / $originalRatio < $askedHeight) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
581
			// width restricted
582
			$askedHeight = \round($askedWidth / $originalRatio);
583
		} else {
584
			$askedWidth = \round($askedHeight * $originalRatio);
585
		}
586
587
		return [(int)$askedWidth, (int)$askedHeight];
588
	}
589
590
	/**
591
	 * Resizes the boundaries to cover the area
592
	 *
593
	 * @param int $askedWidth
594
	 * @param int $askedHeight
595
	 * @param int $previewWidth
596
	 * @param int $previewHeight
597
	 * @return integer[]
598
	 */
599
	private function applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight) {
600
		$originalRatio = $previewWidth / $previewHeight;
601
		// Defines the box in which the preview has to fit
602
		$scaleFactor = $this->scalingUp ? $this->maxScaleFactor : 1;
603
		$askedWidth = \min($askedWidth, $previewWidth * $scaleFactor);
604
		$askedHeight = \min($askedHeight, $previewHeight * $scaleFactor);
605
606 View Code Duplication
		if ($askedWidth / $originalRatio > $askedHeight) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
607
			// height restricted
608
			$askedHeight = \round($askedWidth / $originalRatio);
609
		} else {
610
			$askedWidth = \round($askedHeight * $originalRatio);
611
		}
612
613
		return [(int)$askedWidth, (int)$askedHeight];
614
	}
615
616
	/**
617
	 * Makes sure an upscaled preview doesn't end up larger than the max dimensions defined in the
618
	 * config
619
	 *
620
	 * @param int $askedWidth
621
	 * @param int $askedHeight
622
	 *
623
	 * @return integer[]
624
	 */
625
	private function fixSize($askedWidth, $askedHeight) {
626
		if ($this->scalingUp) {
627
			$askedWidth = \min($this->configMaxWidth, $askedWidth);
628
			$askedHeight = \min($this->configMaxHeight, $askedHeight);
629
		}
630
631
		return [(int)$askedWidth, (int)$askedHeight];
632
	}
633
634
	/**
635
	 * Checks if a bigger version of a file preview is cached and if not
636
	 * return the preview of max allowed dimensions
637
	 *
638
	 * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
639
	 *
640
	 * @return string path to bigger thumbnail
641
	 */
642
	private function isCachedBigger($allThumbnails) {
643
		// This is used to eliminate any thumbnail narrower than what we need
644
		$maxX = $this->getMaxX();
645
646
		//array for usable cached thumbnails
647
		$possibleThumbnails = $this->getPossibleThumbnails($allThumbnails);
648
649
		foreach ($possibleThumbnails as $width => $path) {
650
			if ($width < $maxX) {
651
				continue;
652
			} else {
653
				return $path;
654
			}
655
		}
656
657
		// At this stage, we didn't find a preview, so we return the max preview
658
		return $this->buildCachePath($this->maxPreviewWidth, $this->maxPreviewHeight);
659
	}
660
661
	/**
662
	 * Get possible bigger thumbnails of the given image with the proper aspect ratio
663
	 *
664
	 * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
665
	 *
666
	 * @return string[] an array of paths to bigger thumbnails
667
	 */
668
	private function getPossibleThumbnails($allThumbnails) {
669
		if ($this->keepAspect) {
670
			$wantedAspectRatio = (float)($this->maxPreviewWidth / $this->maxPreviewHeight);
671
		} else {
672
			$wantedAspectRatio = (float)($this->getMaxX() / $this->getMaxY());
673
		}
674
675
		//array for usable cached thumbnails
676
		$possibleThumbnails = [];
677
		foreach ($allThumbnails as $thumbnail) {
678
			$name = \rtrim($thumbnail['name'], '.png');
679
			list($x, $y, $aspectRatio) = $this->getDimensionsFromFilename($name);
680
			if (\abs($aspectRatio - $wantedAspectRatio) >= 0.000001
681
				|| $this->unscalable($x, $y)
682
			) {
683
				continue;
684
			}
685
			$possibleThumbnails[$x] = $thumbnail['path'];
686
		}
687
688
		\ksort($possibleThumbnails);
689
690
		return $possibleThumbnails;
691
	}
692
693
	/**
694
	 * Looks at the preview filename from the cache and extracts the size of the preview
695
	 *
696
	 * @param string $name
697
	 *
698
	 * @return array<int,int,float>
0 ignored issues
show
Documentation introduced by
The doc-type array<int,int,float> could not be parsed: Expected ">" at position 5, but found ",". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
699
	 */
700
	private function getDimensionsFromFilename($name) {
701
		$size = \explode('-', $name);
702
		$x = (int)$size[0];
703
		$y = (int)$size[1];
704
		$aspectRatio = (float)($x / $y);
705
706
		return [$x, $y, $aspectRatio];
707
	}
708
709
	/**
710
	 * @param int $x
711
	 * @param int $y
712
	 *
713
	 * @return bool
714
	 */
715
	private function unscalable($x, $y) {
716
		$maxX = $this->getMaxX();
717
		$maxY = $this->getMaxY();
718
		$scalingUp = $this->getScalingUp();
719
		$maxScaleFactor = $this->getMaxScaleFactor();
720
721
		if ($x < $maxX || $y < $maxY) {
722
			if ($scalingUp) {
723
				$scaleFactor = $maxX / $x;
724
				if ($scaleFactor > $maxScaleFactor) {
725
					return true;
726
				}
727
			} else {
728
				return true;
729
			}
730
		}
731
732
		return false;
733
	}
734
735
	/**
736
	 * Returns a preview of a file
737
	 *
738
	 * The cache is searched first and if nothing usable was found then a preview is
739
	 * generated by one of the providers
740
	 *
741
	 * @return IImage
742
	 */
743
	public function getPreview() {
744
		if ($this->preview !== null && $this->preview->valid()) {
745
			return $this->preview;
746
		}
747
748
		$this->preview = null;
749
		$fileInfo = $this->getFileInfo();
750
		if ($fileInfo === null || $fileInfo === false) {
751
			return new \OC_Image();
752
		}
753
754
		$cached = $this->isCached();
755
		if ($cached) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cached of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
756
			$this->getCachedPreview($cached);
757
		}
758
759
		if ($this->preview === null) {
760
			$this->generatePreview();
761
		}
762
763
		// We still don't have a preview, so we send back an empty object
764
		if ($this->preview === null) {
765
			$this->preview = new \OC_Image();
766
		}
767
768
		return $this->preview;
769
	}
770
771
	/**
772
	 * Sends the preview, including the headers to client which requested it
773
	 *
774
	 * @param null|string $mimeTypeForHeaders the media type to use when sending back the reply
775
	 *
776
	 * @throws NotFoundException
777
	 */
778
	public function showPreview($mimeTypeForHeaders = null) {
779
		// Check if file is valid
780
		if ($this->isFileValid() === false) {
781
			throw new NotFoundException('File not found.');
782
		}
783
784
		if ($this->preview === null) {
785
			$this->getPreview();
786
		}
787
		if ($this->preview instanceof IImage) {
788
			if ($this->preview->valid()) {
789
				Response::enableCaching(3600 * 24); // 24 hours
790
			} else {
791
				$this->getMimeIcon();
792
			}
793
			$this->preview->show($mimeTypeForHeaders);
794
		}
795
	}
796
797
	/**
798
	 * Retrieves the preview from the cache and resizes it if necessary
799
	 *
800
	 * @param string $cached the path to the cached preview
801
	 */
802
	private function getCachedPreview($cached) {
803
		$stream = $this->userView->fopen($cached, 'r');
804
		$this->preview = null;
805
		if ($stream) {
806
			$image = new \OC_Image();
807
			$image->loadFromFileHandle($stream);
808
809
			$this->preview = $image->valid() ? $image : null;
810
811
			if ($this->preview !== null) {
812
				// Size of the preview we calculated
813
				$maxX = $this->previewWidth;
814
				$maxY = $this->previewHeight;
815
				// Size of the preview we retrieved from the cache
816
				$previewX = (int)$this->preview->width();
817
				$previewY = (int)$this->preview->height();
818
819
				// We don't have an exact match
820
				if ($previewX !== $maxX || $previewY !== $maxY) {
821
					$this->resizeAndStore();
822
				}
823
			}
824
825
			\fclose($stream);
826
		}
827
	}
828
829
	/**
830
	 * Resizes, crops, fixes orientation and stores in the cache
831
	 */
832
	private function resizeAndStore() {
833
		$image = $this->preview;
834
		if (!($image instanceof IImage)) {
835
			Util::writeLog(
836
				'core', '$this->preview is not an instance of \OCP\IImage', Util::DEBUG
837
			);
838
839
			return;
840
		}
841
		$previewWidth = (int)$image->width();
842
		$previewHeight = (int)$image->height();
843
		$askedWidth = $this->getMaxX();
844
		$askedHeight = $this->getMaxY();
845
846
		if ($this->mode === self::MODE_COVER) {
847
			list($askedWidth, $askedHeight) =
848
				$this->applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight);
849
		}
850
851
		/**
852
		 * Phase 1: If required, adjust boundaries to keep aspect ratio
853
		 */
854
		if ($this->keepAspect) {
855
			list($askedWidth, $askedHeight) =
856
				$this->applyAspectRatio($askedWidth, $askedHeight, $previewWidth, $previewHeight);
857
		}
858
859
		/**
860
		 * Phase 2: Resizes preview to try and match requirements.
861
		 * Takes the scaling ratio into consideration
862
		 */
863
		list($newPreviewWidth, $newPreviewHeight) = $this->scale(
864
			$image, $askedWidth, $askedHeight, $previewWidth, $previewHeight
865
		);
866
867
		// The preview has been resized and should now have the asked dimensions
868
		if ($newPreviewWidth === $askedWidth && $newPreviewHeight === $askedHeight) {
869
			$this->storePreview($newPreviewWidth, $newPreviewHeight);
870
871
			return;
872
		}
873
874
		/**
875
		 * Phase 3: We're still not there yet, so we're clipping and filling
876
		 * to match the asked dimensions
877
		 */
878
		// It turns out the scaled preview is now too big, so we crop the image
879
		if ($newPreviewWidth >= $askedWidth && $newPreviewHeight >= $askedHeight) {
880
			$this->crop($image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight);
881
			$this->storePreview($askedWidth, $askedHeight);
882
883
			return;
884
		}
885
886
		// At least one dimension of the scaled preview is too small,
887
		// so we fill the space with a transparent background
888
		if (($newPreviewWidth < $askedWidth || $newPreviewHeight < $askedHeight)) {
889
			$this->cropAndFill(
890
				$image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight
891
			);
892
			$this->storePreview($askedWidth, $askedHeight);
893
894
			return;
895
		}
896
897
		// The preview is smaller, but we can't touch it
898
		$this->storePreview($newPreviewWidth, $newPreviewHeight);
899
	}
900
901
	/**
902
	 * Calculates the new dimensions of the preview
903
	 *
904
	 * The new dimensions can be larger or smaller than the ones of the preview we have to resize
905
	 *
906
	 * @param IImage $image
907
	 * @param int $askedWidth
908
	 * @param int $askedHeight
909
	 * @param int $previewWidth
910
	 * @param int $previewHeight
911
	 *
912
	 * @return int[]
913
	 */
914
	private function scale($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) {
915
		$scalingUp = $this->getScalingUp();
916
		$maxScaleFactor = $this->getMaxScaleFactor();
917
918
		$factorX = $askedWidth / $previewWidth;
919
		$factorY = $askedHeight / $previewHeight;
920
921
		if ($factorX >= $factorY) {
922
			$factor = $factorX;
923
		} else {
924
			$factor = $factorY;
925
		}
926
927
		if ($scalingUp === false) {
928
			if ($factor > 1) {
929
				$factor = 1;
930
			}
931
		}
932
933
		// We cap when upscaling
934
		if ($maxScaleFactor !== null) {
935
			if ($factor > $maxScaleFactor) {
936
				Util::writeLog(
937
					'core', 'scale factor reduced from ' . $factor . ' to ' . $maxScaleFactor,
938
					Util::DEBUG
939
				);
940
				$factor = $maxScaleFactor;
941
			}
942
		}
943
944
		$newPreviewWidth = \round($previewWidth * $factor);
945
		$newPreviewHeight = \round($previewHeight * $factor);
946
947
		$image->preciseResize($newPreviewWidth, $newPreviewHeight);
948
		$this->preview = $image;
949
950
		return [$newPreviewWidth, $newPreviewHeight];
951
	}
952
953
	/**
954
	 * Crops a preview which is larger than the dimensions we've received
955
	 *
956
	 * @param IImage $image
957
	 * @param int $askedWidth
958
	 * @param int $askedHeight
959
	 * @param int $previewWidth
960
	 * @param int $previewHeight
961
	 */
962
	private function crop($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $previewHeight is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
963
		$cropX = \floor(\abs($askedWidth - $previewWidth) * 0.5);
964
		//don't crop previews on the Y axis, this sucks if it's a document.
965
		//$cropY = floor(abs($y - $newPreviewHeight) * 0.5);
966
		$cropY = 0;
967
		$image->crop($cropX, $cropY, $askedWidth, $askedHeight);
968
		$this->preview = $image;
969
	}
970
971
	/**
972
	 * Crops an image if it's larger than the dimensions we've received and fills the empty space
973
	 * with a transparent background
974
	 *
975
	 * @param IImage $image
976
	 * @param int $askedWidth
977
	 * @param int $askedHeight
978
	 * @param int $previewWidth
979
	 * @param int $previewHeight
980
	 */
981
	private function cropAndFill($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) {
982 View Code Duplication
		if ($previewWidth > $askedWidth) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
983
			$cropX = \floor(($previewWidth - $askedWidth) * 0.5);
984
			$image->crop($cropX, 0, $askedWidth, $previewHeight);
985
			$previewWidth = $askedWidth;
986
		}
987
988 View Code Duplication
		if ($previewHeight > $askedHeight) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
989
			$cropY = \floor(($previewHeight - $askedHeight) * 0.5);
990
			$image->crop(0, $cropY, $previewWidth, $askedHeight);
991
			$previewHeight = $askedHeight;
992
		}
993
994
		// Creates a transparent background
995
		$backgroundLayer = \imagecreatetruecolor($askedWidth, $askedHeight);
996
		\imagealphablending($backgroundLayer, false);
997
		$transparency = \imagecolorallocatealpha($backgroundLayer, 0, 0, 0, 127);
998
		\imagefill($backgroundLayer, 0, 0, $transparency);
999
		\imagesavealpha($backgroundLayer, true);
1000
1001
		$image = $image->resource();
1002
1003
		$mergeX = \floor(\abs($askedWidth - $previewWidth) * 0.5);
1004
		$mergeY = \floor(\abs($askedHeight - $previewHeight) * 0.5);
1005
1006
		// Pastes the preview on top of the background
1007
		\imagecopy(
1008
			$backgroundLayer, $image, $mergeX, $mergeY, 0, 0, $previewWidth,
1009
			$previewHeight
1010
		);
1011
1012
		$image = new \OC_Image($backgroundLayer);
1013
1014
		$this->preview = $image;
1015
	}
1016
1017
	/**
1018
	 * Saves a preview in the cache to speed up future calls
1019
	 *
1020
	 * Do not nullify the preview as it might send the whole process in a loop
1021
	 *
1022
	 * @param int $previewWidth
1023
	 * @param int $previewHeight
1024
	 */
1025
	private function storePreview($previewWidth, $previewHeight) {
1026
		if (empty($previewWidth) || empty($previewHeight)) {
1027
			Util::writeLog(
1028
				'core', 'Cannot save preview of dimension ' . $previewWidth . 'x' . $previewHeight,
1029
				Util::DEBUG
1030
			);
1031
		} else {
1032
			$cachePath = $this->buildCachePath($previewWidth, $previewHeight);
1033
			$this->userView->file_put_contents($cachePath, $this->preview->data());
1034
		}
1035
	}
1036
1037
	/**
1038
	 * Returns the path to a preview based on its dimensions and aspect
1039
	 *
1040
	 * @param int|null $maxX
1041
	 * @param int|null $maxY
1042
	 *
1043
	 * @return string
1044
	 */
1045
	private function buildCachePath($maxX = null, $maxY = null) {
1046
		if ($maxX === null) {
1047
			$maxX = $this->getMaxX();
1048
		}
1049
		if ($maxY === null) {
1050
			$maxY = $this->getMaxY();
1051
		}
1052
1053
		$previewPath = $this->getPreviewPath();
1054
		$previewPath = $previewPath . \strval($maxX) . '-' . \strval($maxY);
1055
		$isMaxPreview =
1056
			($maxX === $this->maxPreviewWidth && $maxY === $this->maxPreviewHeight) ? true : false;
1057
		if ($isMaxPreview) {
1058
			$previewPath .= '-max';
1059
		}
1060
		if ($this->keepAspect && !$isMaxPreview) {
1061
			$previewPath .= '-with-aspect';
1062
		}
1063
		if ($this->mode === self::MODE_COVER) {
1064
			$previewPath .= '-cover';
1065
		}
1066
		$previewPath .= '.png';
1067
1068
		return $previewPath;
1069
	}
1070
1071
	/**
1072
	 * Returns the path to the folder where the previews are stored, identified by the fileId
1073
	 *
1074
	 * @return string
1075
	 */
1076
	private function getPreviewPath($fileId = null) {
1077
		if ($fileId === null) {
1078
			$fileId = $this->getFileInfo()->getId();
1079
			if ($this->versionId !== null) {
1080
				$fileId .= '/';
1081
				$fileId .= $this->versionId;
1082
			}
1083
		}
1084
		return $this->getThumbnailsFolder() . '/' . $fileId . '/';
1085
	}
1086
1087
	/**
1088
	 * Asks the provider to send a preview of the file which respects the maximum dimensions
1089
	 * defined in the configuration and after saving it in the cache, it is then resized to the
1090
	 * asked dimensions
1091
	 *
1092
	 * This is only called once in order to generate a large PNG of dimensions defined in the
1093
	 * configuration file. We'll be able to quickly resize it later on.
1094
	 * We never upscale the original conversion as this will be done later by the resizing
1095
	 * operation
1096
	 *
1097
	 */
1098
	private function generatePreview() {
1099
		$file = $this->getFile();
1100
		$preview = null;
1101
1102
		$previewProviders = \OC::$server->getPreviewManager()
1103
			->getProviders();
1104
		foreach ($previewProviders as $supportedMimeType => $providers) {
1105
			if (!\preg_match($supportedMimeType, $this->mimeType)) {
1106
				continue;
1107
			}
1108
1109
			foreach ($providers as $closure) {
1110
				$provider = $closure();
1111
				if (!($provider instanceof IProvider2)) {
1112
					continue;
1113
				}
1114
1115
				Util::writeLog(
1116
					'core', 'Generating preview for "' . $file->getPath() . '" with "' . \get_class($provider)
1117
					. '"', Util::DEBUG
1118
				);
1119
1120
				$preview = $provider->getThumbnail($file, $this->configMaxWidth, $this->configMaxHeight, false);
1121
1122
				if (!($preview instanceof IImage)) {
1123
					continue;
1124
				}
1125
1126
				$this->preview = $preview;
1127
				$previewPath = $this->getPreviewPath();
1128
1129
				if ($this->userView->is_dir($this->getThumbnailsFolder() . '/') === false) {
1130
					$this->userView->mkdir($this->getThumbnailsFolder() . '/');
1131
				}
1132
1133
				if ($this->userView->is_dir($previewPath) === false) {
1134
					$this->userView->mkdir($previewPath);
1135
				}
1136
1137
				// This stores our large preview so that it can be used in subsequent resizing requests
1138
				$this->storeMaxPreview($previewPath);
1139
1140
				break 2;
1141
			}
1142
		}
1143
1144
		// The providers have been kind enough to give us a preview
1145
		if ($preview) {
1146
			$this->resizeAndStore();
1147
		}
1148
	}
1149
1150
	/**
1151
	 * Defines the media icon, for the media type of the original file, as the preview
1152
	 */
1153
	private function getMimeIcon() {
1154
		$image = new \OC_Image();
1155
		$mimeIconWebPath = \OC::$server->getMimeTypeDetector()->mimeTypeIcon($this->mimeType);
1156
		if (empty(\OC::$WEBROOT)) {
1157
			$mimeIconServerPath = \OC::$SERVERROOT . $mimeIconWebPath;
1158
		} else {
1159
			$mimeIconServerPath = \str_replace(\OC::$WEBROOT, \OC::$SERVERROOT, $mimeIconWebPath);
1160
		}
1161
		$image->loadFromFile($mimeIconServerPath);
1162
1163
		$this->preview = $image;
1164
	}
1165
1166
	/**
1167
	 * Stores the max preview in the cache
1168
	 *
1169
	 * @param string $previewPath path to the preview
1170
	 */
1171
	private function storeMaxPreview($previewPath) {
1172
		$maxPreviewExists = false;
1173
		$preview = $this->preview;
1174
1175
		$allThumbnails = $this->userView->getDirectoryContent($previewPath);
1176
		// This is so that the cache doesn't need emptying when upgrading
1177
		// Can be replaced by an upgrade script...
1178
		foreach ($allThumbnails as $thumbnail) {
1179
			$name = \rtrim($thumbnail['name'], '.png');
1180
			if (\strpos($name, 'max')) {
1181
				$maxPreviewExists = true;
1182
				break;
1183
			}
1184
		}
1185
		// We haven't found the max preview, so we create it
1186
		if (!$maxPreviewExists) {
1187
			$previewWidth = $preview->width();
1188
			$previewHeight = $preview->height();
1189
			$previewPath = $previewPath . \strval($previewWidth) . '-' . \strval($previewHeight);
1190
			$previewPath .= '-max.png';
1191
			$this->userView->file_put_contents($previewPath, $preview->data());
1192
			$this->maxPreviewWidth = $previewWidth;
1193
			$this->maxPreviewHeight = $previewHeight;
1194
		}
1195
	}
1196
1197
	/**
1198
	 * Limits a dimension to the maximum dimension provided as argument
1199
	 *
1200
	 * @param int $dim
1201
	 * @param int $maxDim
1202
	 * @param string $dimName
1203
	 *
1204
	 * @return integer
1205
	 */
1206
	private function limitMaxDim($dim, $maxDim, $dimName) {
1207
		if ($maxDim !== null) {
1208
			if ($dim > $maxDim) {
1209
				Util::writeLog(
1210
					'core', $dimName . ' reduced from ' . $dim . ' to ' . $maxDim, Util::DEBUG
1211
				);
1212
				$dim = $maxDim;
1213
			}
1214
		}
1215
1216
		return $dim;
1217
	}
1218
1219
	/**
1220
	 * @param array $args
1221
	 */
1222
	public static function post_write($args) {
1223
		self::post_delete($args, 'files/');
1224
	}
1225
1226
	/**
1227
	 * @param array $args
1228
	 */
1229
	public static function prepare_delete_files($args) {
1230
		self::prepare_delete($args, 'files/');
1231
	}
1232
1233
	/**
1234
	 * @param array $args
1235
	 * @param string $prefix
1236
	 */
1237
	public static function prepare_delete(array $args, $prefix = '') {
0 ignored issues
show
Unused Code introduced by
The parameter $prefix is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1238
		$path = Files\Filesystem::normalizePath($args['path']);
1239
		$user = isset($args['user']) ? $args['user'] : \OC_User::getUser();
1240
		if ($user === false) {
1241
			$user = Filesystem::getOwner($path);
1242
		}
1243
1244
		$userFolder = \OC::$server->getUserFolder($user);
1245
		if ($userFolder === null) {
1246
			return;
1247
		}
1248
1249
		$node = $userFolder->get($path);
1250
		self::addPathToDeleteFileMapper($path, $node);
1251
		if ($node->getType() === FileInfo::TYPE_FOLDER) {
1252
			$children = self::getAllChildren($node);
0 ignored issues
show
Compatibility introduced by
$node of type object<OCP\Files\Node> is not a sub-type of object<OCP\Files\Folder>. It seems like you assume a child interface of the interface OCP\Files\Node to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
1253
			self::$deleteChildrenMapper[$node->getPath()] = $children;
1254
		}
1255
	}
1256
1257
	/**
1258
	 * @param string $absolutePath
1259
	 * @param \OCP\Files\FileInfo $info
1260
	 */
1261
	private static function addPathToDeleteFileMapper($absolutePath, $info) {
1262
		self::$deleteFileMapper[$absolutePath] = $info;
1263
	}
1264
1265
	/**
1266
	 * @param Folder $node
1267
	 *
1268
	 * @return array
1269
	 */
1270
	private static function getAllChildren($node) {
1271
		$children = $node->getDirectoryListing();
1272
		$childrenFiles = [];
1273
1274
		foreach ($children as $child) {
1275
			if ($child->getType() === FileInfo::TYPE_FOLDER) {
1276
				$childrenFiles = \array_merge(
1277
					$childrenFiles,
1278
					self::getAllChildren($child)
0 ignored issues
show
Compatibility introduced by
$child of type object<OCP\Files\Node> is not a sub-type of object<OCP\Files\Folder>. It seems like you assume a child interface of the interface OCP\Files\Node to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
1279
				);
1280
			} else {
1281
				$childrenFiles[] = $child;
1282
			}
1283
		}
1284
1285
		return $childrenFiles;
1286
	}
1287
1288
	/**
1289
	 * @param array $args
1290
	 */
1291
	public static function post_delete_files($args) {
1292
		self::post_delete($args, 'files/');
1293
	}
1294
1295
	/**
1296
	 * @param array $args
1297
	 */
1298
	public static function post_delete_versions($args) {
1299
		self::post_delete($args, 'files/');
1300
	}
1301
1302
	/**
1303
	 * @param array $args
1304
	 * @param string $prefix
1305
	 */
1306
	public static function post_delete($args, $prefix = '') {
1307
		$path = Files\Filesystem::normalizePath($args['path']);
1308
		if (!isset(self::$deleteFileMapper[$path])) {
1309
			$user = isset($args['user']) ? $args['user'] : \OC_User::getUser();
1310
			if ($user === false) {
1311
				$user = Filesystem::getOwner($path);
1312
			}
1313
1314
			$userFolder = \OC::$server->getUserFolder($user);
1315
			if ($userFolder === null) {
1316
				return;
1317
			}
1318
1319
			$node = $userFolder->get($path);
1320
		} else {
1321
			/** @var FileInfo $node */
1322
			$node = self::$deleteFileMapper[$path];
1323
		}
1324
1325
		$preview = new Preview($node->getOwner()->getUID(), $prefix, $node);
0 ignored issues
show
Documentation introduced by
$node is of type object<OCP\Files\FileInfo>, but the function expects a null|object<OCP\Files\Node>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1326
		$preview->deleteAllPreviews();
1327
	}
1328
}
1329