Completed
Push — master ( 81153d...2a6722 )
by Thomas
52s
created

Preview::getDimensionsFromFilename()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 1
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
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
	static public $deleteFileMapper = [];
87
	static public $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 (is_null($fileId)) {
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 (!is_null($maxPreviewWidth) && !is_null($maxPreviewHeight)) {
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
530
		foreach ($allThumbnails as $thumbnail) {
531
			if ($name === $thumbnail->getName()) {
532
				return true;
533
			}
534
		}
535
536
		return false;
537
	}
538
539
	/**
540
	 * Determines the size of the preview we should be looking for in the cache
541
	 *
542
	 * @return integer[]
543
	 */
544
	private function simulatePreviewDimensions() {
545
		$askedWidth = $this->getMaxX();
546
		$askedHeight = $this->getMaxY();
547
548
		if ($this->keepAspect) {
549
			list($newPreviewWidth, $newPreviewHeight) =
550
				$this->applyAspectRatio($askedWidth, $askedHeight);
551
		} else {
552
			list($newPreviewWidth, $newPreviewHeight) = $this->fixSize($askedWidth, $askedHeight);
553
		}
554
555
		return [(int)$newPreviewWidth, (int)$newPreviewHeight];
556
	}
557
558
	/**
559
	 * Resizes the boundaries to match the aspect ratio
560
	 *
561
	 * @param int $askedWidth
562
	 * @param int $askedHeight
563
	 *
564
	 * @param int $originalWidth
565
	 * @param int $originalHeight
566
	 * @return integer[]
567
	 */
568
	private function applyAspectRatio($askedWidth, $askedHeight, $originalWidth = 0, $originalHeight = 0) {
569
		if (!$originalWidth) {
570
			$originalWidth = $this->maxPreviewWidth;
571
		}
572
		if (!$originalHeight) {
573
			$originalHeight = $this->maxPreviewHeight;
574
		}
575
		$originalRatio = $originalWidth / $originalHeight;
576
		// Defines the box in which the preview has to fit
577
		$scaleFactor = $this->scalingUp ? $this->maxScaleFactor : 1;
578
		$askedWidth = min($askedWidth, $originalWidth * $scaleFactor);
579
		$askedHeight = min($askedHeight, $originalHeight * $scaleFactor);
580
581 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...
582
			// width restricted
583
			$askedHeight = round($askedWidth / $originalRatio);
584
		} else {
585
			$askedWidth = round($askedHeight * $originalRatio);
586
		}
587
588
		return [(int)$askedWidth, (int)$askedHeight];
589
	}
590
591
	/**
592
	 * Resizes the boundaries to cover the area
593
	 *
594
	 * @param int $askedWidth
595
	 * @param int $askedHeight
596
	 * @param int $previewWidth
597
	 * @param int $previewHeight
598
	 * @return integer[]
599
	 */
600
	private function applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight) {
601
		$originalRatio = $previewWidth / $previewHeight;
602
		// Defines the box in which the preview has to fit
603
		$scaleFactor = $this->scalingUp ? $this->maxScaleFactor : 1;
604
		$askedWidth = min($askedWidth, $previewWidth * $scaleFactor);
605
		$askedHeight = min($askedHeight, $previewHeight * $scaleFactor);
606
607 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...
608
			// height restricted
609
			$askedHeight = round($askedWidth / $originalRatio);
610
		} else {
611
			$askedWidth = round($askedHeight * $originalRatio);
612
		}
613
614
		return [(int)$askedWidth, (int)$askedHeight];
615
	}
616
617
	/**
618
	 * Makes sure an upscaled preview doesn't end up larger than the max dimensions defined in the
619
	 * config
620
	 *
621
	 * @param int $askedWidth
622
	 * @param int $askedHeight
623
	 *
624
	 * @return integer[]
625
	 */
626
	private function fixSize($askedWidth, $askedHeight) {
627
		if ($this->scalingUp) {
628
			$askedWidth = min($this->configMaxWidth, $askedWidth);
629
			$askedHeight = min($this->configMaxHeight, $askedHeight);
630
		}
631
632
		return [(int)$askedWidth, (int)$askedHeight];
633
	}
634
635
	/**
636
	 * Checks if a bigger version of a file preview is cached and if not
637
	 * return the preview of max allowed dimensions
638
	 *
639
	 * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
640
	 *
641
	 * @return string path to bigger thumbnail
642
	 */
643
	private function isCachedBigger($allThumbnails) {
644
		// This is used to eliminate any thumbnail narrower than what we need
645
		$maxX = $this->getMaxX();
646
647
		//array for usable cached thumbnails
648
		$possibleThumbnails = $this->getPossibleThumbnails($allThumbnails);
649
650
		foreach ($possibleThumbnails as $width => $path) {
651
			if ($width < $maxX) {
652
				continue;
653
			} else {
654
				return $path;
655
			}
656
		}
657
658
		// At this stage, we didn't find a preview, so we return the max preview
659
		return $this->buildCachePath($this->maxPreviewWidth, $this->maxPreviewHeight);
660
	}
661
662
	/**
663
	 * Get possible bigger thumbnails of the given image with the proper aspect ratio
664
	 *
665
	 * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
666
	 *
667
	 * @return string[] an array of paths to bigger thumbnails
668
	 */
669
	private function getPossibleThumbnails($allThumbnails) {
670
		if ($this->keepAspect) {
671
			$wantedAspectRatio = (float)($this->maxPreviewWidth / $this->maxPreviewHeight);
672
		} else {
673
			$wantedAspectRatio = (float)($this->getMaxX() / $this->getMaxY());
674
		}
675
676
		//array for usable cached thumbnails
677
		$possibleThumbnails = [];
678
		foreach ($allThumbnails as $thumbnail) {
679
			$name = rtrim($thumbnail['name'], '.png');
680
			list($x, $y, $aspectRatio) = $this->getDimensionsFromFilename($name);
681
			if (abs($aspectRatio - $wantedAspectRatio) >= 0.000001
682
				|| $this->unscalable($x, $y)
683
			) {
684
				continue;
685
			}
686
			$possibleThumbnails[$x] = $thumbnail['path'];
687
		}
688
689
		ksort($possibleThumbnails);
690
691
		return $possibleThumbnails;
692
	}
693
694
	/**
695
	 * Looks at the preview filename from the cache and extracts the size of the preview
696
	 *
697
	 * @param string $name
698
	 *
699
	 * @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...
700
	 */
701
	private function getDimensionsFromFilename($name) {
702
		$size = explode('-', $name);
703
		$x = (int)$size[0];
704
		$y = (int)$size[1];
705
		$aspectRatio = (float)($x / $y);
706
707
		return [$x, $y, $aspectRatio];
708
	}
709
710
	/**
711
	 * @param int $x
712
	 * @param int $y
713
	 *
714
	 * @return bool
715
	 */
716
	private function unscalable($x, $y) {
717
718
		$maxX = $this->getMaxX();
719
		$maxY = $this->getMaxY();
720
		$scalingUp = $this->getScalingUp();
721
		$maxScaleFactor = $this->getMaxScaleFactor();
722
723
		if ($x < $maxX || $y < $maxY) {
724
			if ($scalingUp) {
725
				$scaleFactor = $maxX / $x;
726
				if ($scaleFactor > $maxScaleFactor) {
727
					return true;
728
				}
729
			} else {
730
				return true;
731
			}
732
		}
733
734
		return false;
735
	}
736
737
	/**
738
	 * Returns a preview of a file
739
	 *
740
	 * The cache is searched first and if nothing usable was found then a preview is
741
	 * generated by one of the providers
742
	 *
743
	 * @return IImage
744
	 */
745
	public function getPreview() {
746
		if (!is_null($this->preview) && $this->preview->valid()) {
747
			return $this->preview;
748
		}
749
750
		$this->preview = null;
751
		$fileInfo = $this->getFileInfo();
752
		if ($fileInfo === null || $fileInfo === false) {
753
			return new \OC_Image();
754
		}
755
756
		$cached = $this->isCached();
757
		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...
758
			$this->getCachedPreview($cached);
759
		}
760
761
		if (is_null($this->preview)) {
762
			$this->generatePreview();
763
		}
764
765
		// We still don't have a preview, so we send back an empty object
766
		if (is_null($this->preview)) {
767
			$this->preview = new \OC_Image();
768
		}
769
770
		return $this->preview;
771
	}
772
773
	/**
774
	 * Sends the preview, including the headers to client which requested it
775
	 *
776
	 * @param null|string $mimeTypeForHeaders the media type to use when sending back the reply
777
	 *
778
	 * @throws NotFoundException
779
	 */
780
	public function showPreview($mimeTypeForHeaders = null) {
781
		// Check if file is valid
782
		if ($this->isFileValid() === false) {
783
			throw new NotFoundException('File not found.');
784
		}
785
786
		if (is_null($this->preview)) {
787
			$this->getPreview();
788
		}
789
		if ($this->preview instanceof IImage) {
790
			if ($this->preview->valid()) {
791
				Response::enableCaching(3600 * 24); // 24 hours
792
			} else {
793
				$this->getMimeIcon();
794
			}
795
			$this->preview->show($mimeTypeForHeaders);
796
		}
797
	}
798
799
	/**
800
	 * Retrieves the preview from the cache and resizes it if necessary
801
	 *
802
	 * @param string $cached the path to the cached preview
803
	 */
804
	private function getCachedPreview($cached) {
805
		$stream = $this->userView->fopen($cached, 'r');
806
		$this->preview = null;
807
		if ($stream) {
808
			$image = new \OC_Image();
809
			$image->loadFromFileHandle($stream);
810
811
			$this->preview = $image->valid() ? $image : null;
812
813
			if (!is_null($this->preview)) {
814
				// Size of the preview we calculated
815
				$maxX = $this->previewWidth;
816
				$maxY = $this->previewHeight;
817
				// Size of the preview we retrieved from the cache
818
				$previewX = (int)$this->preview->width();
819
				$previewY = (int)$this->preview->height();
820
821
				// We don't have an exact match
822
				if ($previewX !== $maxX || $previewY !== $maxY) {
823
					$this->resizeAndStore();
824
				}
825
			}
826
827
			fclose($stream);
828
		}
829
	}
830
831
	/**
832
	 * Resizes, crops, fixes orientation and stores in the cache
833
	 */
834
	private function resizeAndStore() {
835
		$image = $this->preview;
836
		if (!($image instanceof IImage)) {
837
			Util::writeLog(
838
				'core', '$this->preview is not an instance of \OCP\IImage', Util::DEBUG
839
			);
840
841
			return;
842
		}
843
		$previewWidth = (int)$image->width();
844
		$previewHeight = (int)$image->height();
845
		$askedWidth = $this->getMaxX();
846
		$askedHeight = $this->getMaxY();
847
848
		if ($this->mode === self::MODE_COVER) {
849
			list($askedWidth, $askedHeight) =
850
				$this->applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight);
851
		}
852
853
		/**
854
		 * Phase 1: If required, adjust boundaries to keep aspect ratio
855
		 */
856
		if ($this->keepAspect) {
857
			list($askedWidth, $askedHeight) =
858
				$this->applyAspectRatio($askedWidth, $askedHeight, $previewWidth, $previewHeight);
859
		}
860
861
		/**
862
		 * Phase 2: Resizes preview to try and match requirements.
863
		 * Takes the scaling ratio into consideration
864
		 */
865
		list($newPreviewWidth, $newPreviewHeight) = $this->scale(
866
			$image, $askedWidth, $askedHeight, $previewWidth, $previewHeight
867
		);
868
869
		// The preview has been resized and should now have the asked dimensions
870
		if ($newPreviewWidth === $askedWidth && $newPreviewHeight === $askedHeight) {
871
			$this->storePreview($newPreviewWidth, $newPreviewHeight);
872
873
			return;
874
		}
875
876
		/**
877
		 * Phase 3: We're still not there yet, so we're clipping and filling
878
		 * to match the asked dimensions
879
		 */
880
		// It turns out the scaled preview is now too big, so we crop the image
881
		if ($newPreviewWidth >= $askedWidth && $newPreviewHeight >= $askedHeight) {
882
			$this->crop($image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight);
883
			$this->storePreview($askedWidth, $askedHeight);
884
885
			return;
886
		}
887
888
		// At least one dimension of the scaled preview is too small,
889
		// so we fill the space with a transparent background
890
		if (($newPreviewWidth < $askedWidth || $newPreviewHeight < $askedHeight)) {
891
			$this->cropAndFill(
892
				$image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight
893
			);
894
			$this->storePreview($askedWidth, $askedHeight);
895
896
			return;
897
		}
898
899
		// The preview is smaller, but we can't touch it
900
		$this->storePreview($newPreviewWidth, $newPreviewHeight);
901
	}
902
903
	/**
904
	 * Calculates the new dimensions of the preview
905
	 *
906
	 * The new dimensions can be larger or smaller than the ones of the preview we have to resize
907
	 *
908
	 * @param IImage $image
909
	 * @param int $askedWidth
910
	 * @param int $askedHeight
911
	 * @param int $previewWidth
912
	 * @param int $previewHeight
913
	 *
914
	 * @return int[]
915
	 */
916
	private function scale($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) {
917
		$scalingUp = $this->getScalingUp();
918
		$maxScaleFactor = $this->getMaxScaleFactor();
919
920
		$factorX = $askedWidth / $previewWidth;
921
		$factorY = $askedHeight / $previewHeight;
922
923
		if ($factorX >= $factorY) {
924
			$factor = $factorX;
925
		} else {
926
			$factor = $factorY;
927
		}
928
929
		if ($scalingUp === false) {
930
			if ($factor > 1) {
931
				$factor = 1;
932
			}
933
		}
934
935
		// We cap when upscaling
936
		if (!is_null($maxScaleFactor)) {
937
			if ($factor > $maxScaleFactor) {
938
				Util::writeLog(
939
					'core', 'scale factor reduced from ' . $factor . ' to ' . $maxScaleFactor,
940
					Util::DEBUG
941
				);
942
				$factor = $maxScaleFactor;
943
			}
944
		}
945
946
		$newPreviewWidth = round($previewWidth * $factor);
947
		$newPreviewHeight = round($previewHeight * $factor);
948
949
		$image->preciseResize($newPreviewWidth, $newPreviewHeight);
950
		$this->preview = $image;
951
952
		return [$newPreviewWidth, $newPreviewHeight];
953
	}
954
955
	/**
956
	 * Crops a preview which is larger than the dimensions we've received
957
	 *
958
	 * @param IImage $image
959
	 * @param int $askedWidth
960
	 * @param int $askedHeight
961
	 * @param int $previewWidth
962
	 * @param int $previewHeight
963
	 */
964
	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...
965
		$cropX = floor(abs($askedWidth - $previewWidth) * 0.5);
966
		//don't crop previews on the Y axis, this sucks if it's a document.
967
		//$cropY = floor(abs($y - $newPreviewHeight) * 0.5);
968
		$cropY = 0;
969
		$image->crop($cropX, $cropY, $askedWidth, $askedHeight);
970
		$this->preview = $image;
971
	}
972
973
	/**
974
	 * Crops an image if it's larger than the dimensions we've received and fills the empty space
975
	 * with a transparent background
976
	 *
977
	 * @param IImage $image
978
	 * @param int $askedWidth
979
	 * @param int $askedHeight
980
	 * @param int $previewWidth
981
	 * @param int $previewHeight
982
	 */
983
	private function cropAndFill($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) {
984 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...
985
			$cropX = floor(($previewWidth - $askedWidth) * 0.5);
986
			$image->crop($cropX, 0, $askedWidth, $previewHeight);
987
			$previewWidth = $askedWidth;
988
		}
989
990 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...
991
			$cropY = floor(($previewHeight - $askedHeight) * 0.5);
992
			$image->crop(0, $cropY, $previewWidth, $askedHeight);
993
			$previewHeight = $askedHeight;
994
		}
995
996
		// Creates a transparent background
997
		$backgroundLayer = imagecreatetruecolor($askedWidth, $askedHeight);
998
		imagealphablending($backgroundLayer, false);
999
		$transparency = imagecolorallocatealpha($backgroundLayer, 0, 0, 0, 127);
1000
		imagefill($backgroundLayer, 0, 0, $transparency);
1001
		imagesavealpha($backgroundLayer, true);
1002
1003
		$image = $image->resource();
1004
1005
		$mergeX = floor(abs($askedWidth - $previewWidth) * 0.5);
1006
		$mergeY = floor(abs($askedHeight - $previewHeight) * 0.5);
1007
1008
		// Pastes the preview on top of the background
1009
		imagecopy(
1010
			$backgroundLayer, $image, $mergeX, $mergeY, 0, 0, $previewWidth,
1011
			$previewHeight
1012
		);
1013
1014
		$image = new \OC_Image($backgroundLayer);
1015
1016
		$this->preview = $image;
1017
	}
1018
1019
	/**
1020
	 * Saves a preview in the cache to speed up future calls
1021
	 *
1022
	 * Do not nullify the preview as it might send the whole process in a loop
1023
	 *
1024
	 * @param int $previewWidth
1025
	 * @param int $previewHeight
1026
	 */
1027
	private function storePreview($previewWidth, $previewHeight) {
1028
		if (empty($previewWidth) || empty($previewHeight)) {
1029
			Util::writeLog(
1030
				'core', 'Cannot save preview of dimension ' . $previewWidth . 'x' . $previewHeight,
1031
				Util::DEBUG
1032
			);
1033
1034
		} else {
1035
			$cachePath = $this->buildCachePath($previewWidth, $previewHeight);
1036
			$this->userView->file_put_contents($cachePath, $this->preview->data());
1037
		}
1038
	}
1039
1040
	/**
1041
	 * Returns the path to a preview based on its dimensions and aspect
1042
	 *
1043
	 * @param int|null $maxX
1044
	 * @param int|null $maxY
1045
	 *
1046
	 * @return string
1047
	 */
1048
	private function buildCachePath($maxX = null, $maxY = null) {
1049
		if (is_null($maxX)) {
1050
			$maxX = $this->getMaxX();
1051
		}
1052
		if (is_null($maxY)) {
1053
			$maxY = $this->getMaxY();
1054
		}
1055
1056
		$previewPath = $this->getPreviewPath();
1057
		$previewPath = $previewPath . strval($maxX) . '-' . strval($maxY);
1058
		$isMaxPreview =
1059
			($maxX === $this->maxPreviewWidth && $maxY === $this->maxPreviewHeight) ? true : false;
1060
		if ($isMaxPreview) {
1061
			$previewPath .= '-max';
1062
		}
1063
		if ($this->keepAspect && !$isMaxPreview) {
1064
			$previewPath .= '-with-aspect';
1065
		}
1066
		if ($this->mode === self::MODE_COVER) {
1067
			$previewPath .= '-cover';
1068
		}
1069
		$previewPath .= '.png';
1070
1071
		return $previewPath;
1072
	}
1073
1074
	/**
1075
	 * Returns the path to the folder where the previews are stored, identified by the fileId
1076
	 *
1077
	 * @return string
1078
	 */
1079
	private function getPreviewPath($fileId = null) {
1080
		if ($fileId === null) {
1081
			$fileId = $this->getFileInfo()->getId();
1082
			if ($this->versionId !== null) {
1083
				$fileId .= '/';
1084
				$fileId .= $this->versionId;
1085
			}
1086
		}
1087
		return $this->getThumbnailsFolder() . '/' . $fileId . '/';
1088
	}
1089
1090
	/**
1091
	 * Asks the provider to send a preview of the file which respects the maximum dimensions
1092
	 * defined in the configuration and after saving it in the cache, it is then resized to the
1093
	 * asked dimensions
1094
	 *
1095
	 * This is only called once in order to generate a large PNG of dimensions defined in the
1096
	 * configuration file. We'll be able to quickly resize it later on.
1097
	 * We never upscale the original conversion as this will be done later by the resizing
1098
	 * operation
1099
	 *
1100
	 */
1101
	private function generatePreview() {
1102
		$file = $this->getFile();
1103
		$preview = null;
1104
1105
		$previewProviders = \OC::$server->getPreviewManager()
1106
			->getProviders();
1107
		foreach ($previewProviders as $supportedMimeType => $providers) {
1108
			if (!preg_match($supportedMimeType, $this->mimeType)) {
1109
				continue;
1110
			}
1111
1112
			foreach ($providers as $closure) {
1113
				$provider = $closure();
1114
				if (!($provider instanceof IProvider2)) {
1115
					continue;
1116
				}
1117
1118
				Util::writeLog(
1119
					'core', 'Generating preview for "' . $file->getPath() . '" with "' . get_class($provider)
1120
					. '"', Util::DEBUG
1121
				);
1122
1123
				$preview = $provider->getThumbnail($file, $this->configMaxWidth, $this->configMaxHeight, false);
1124
1125
				if (!($preview instanceof IImage)) {
1126
					continue;
1127
				}
1128
1129
				$this->preview = $preview;
1130
				$previewPath = $this->getPreviewPath();
1131
1132
				if ($this->userView->is_dir($this->getThumbnailsFolder() . '/') === false) {
1133
					$this->userView->mkdir($this->getThumbnailsFolder() . '/');
1134
				}
1135
1136
				if ($this->userView->is_dir($previewPath) === false) {
1137
					$this->userView->mkdir($previewPath);
1138
				}
1139
1140
				// This stores our large preview so that it can be used in subsequent resizing requests
1141
				$this->storeMaxPreview($previewPath);
1142
1143
				break 2;
1144
			}
1145
		}
1146
1147
		// The providers have been kind enough to give us a preview
1148
		if ($preview) {
1149
			$this->resizeAndStore();
1150
		}
1151
	}
1152
1153
	/**
1154
	 * Defines the media icon, for the media type of the original file, as the preview
1155
	 */
1156
	private function getMimeIcon() {
1157
		$image = new \OC_Image();
1158
		$mimeIconWebPath = \OC::$server->getMimeTypeDetector()->mimeTypeIcon($this->mimeType);
1159
		if (empty(\OC::$WEBROOT)) {
1160
			$mimeIconServerPath = \OC::$SERVERROOT . $mimeIconWebPath;
1161
		} else {
1162
			$mimeIconServerPath = str_replace(\OC::$WEBROOT, \OC::$SERVERROOT, $mimeIconWebPath);
1163
		}
1164
		$image->loadFromFile($mimeIconServerPath);
1165
1166
		$this->preview = $image;
1167
	}
1168
1169
	/**
1170
	 * Stores the max preview in the cache
1171
	 *
1172
	 * @param string $previewPath path to the preview
1173
	 */
1174
	private function storeMaxPreview($previewPath) {
1175
		$maxPreviewExists = false;
1176
		$preview = $this->preview;
1177
1178
		$allThumbnails = $this->userView->getDirectoryContent($previewPath);
1179
		// This is so that the cache doesn't need emptying when upgrading
1180
		// Can be replaced by an upgrade script...
1181
		foreach ($allThumbnails as $thumbnail) {
1182
			$name = rtrim($thumbnail['name'], '.png');
1183
			if (strpos($name, 'max')) {
1184
				$maxPreviewExists = true;
1185
				break;
1186
			}
1187
		}
1188
		// We haven't found the max preview, so we create it
1189
		if (!$maxPreviewExists) {
1190
			$previewWidth = $preview->width();
1191
			$previewHeight = $preview->height();
1192
			$previewPath = $previewPath . strval($previewWidth) . '-' . strval($previewHeight);
1193
			$previewPath .= '-max.png';
1194
			$this->userView->file_put_contents($previewPath, $preview->data());
1195
			$this->maxPreviewWidth = $previewWidth;
1196
			$this->maxPreviewHeight = $previewHeight;
1197
		}
1198
	}
1199
1200
	/**
1201
	 * Limits a dimension to the maximum dimension provided as argument
1202
	 *
1203
	 * @param int $dim
1204
	 * @param int $maxDim
1205
	 * @param string $dimName
1206
	 *
1207
	 * @return integer
1208
	 */
1209
	private function limitMaxDim($dim, $maxDim, $dimName) {
1210
		if (!is_null($maxDim)) {
1211
			if ($dim > $maxDim) {
1212
				Util::writeLog(
1213
					'core', $dimName . ' reduced from ' . $dim . ' to ' . $maxDim, Util::DEBUG
1214
				);
1215
				$dim = $maxDim;
1216
			}
1217
		}
1218
1219
		return $dim;
1220
	}
1221
1222
	/**
1223
	 * @param array $args
1224
	 */
1225
	public static function post_write($args) {
1226
		self::post_delete($args, 'files/');
1227
	}
1228
1229
	/**
1230
	 * @param array $args
1231
	 */
1232
	public static function prepare_delete_files($args) {
1233
		self::prepare_delete($args, 'files/');
1234
	}
1235
1236
	/**
1237
	 * @param array $args
1238
	 * @param string $prefix
1239
	 */
1240
	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...
1241
		$path = Files\Filesystem::normalizePath($args['path']);
1242
		$user = isset($args['user']) ? $args['user'] : \OC_User::getUser();
1243
		if ($user === false) {
1244
			$user = Filesystem::getOwner($path);
1245
		}
1246
1247
		$userFolder = \OC::$server->getUserFolder($user);
1248
		if ($userFolder === null) {
1249
			return;
1250
		}
1251
1252
		$node = $userFolder->get($path);
1253
		self::addPathToDeleteFileMapper($path, $node);
1254
		if ($node->getType() === FileInfo::TYPE_FOLDER) {
1255
			$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...
1256
			self::$deleteChildrenMapper[$node->getPath()] = $children;
1257
		}
1258
	}
1259
1260
	/**
1261
	 * @param string $absolutePath
1262
	 * @param \OCP\Files\FileInfo $info
1263
	 */
1264
	private static function addPathToDeleteFileMapper($absolutePath, $info) {
1265
		self::$deleteFileMapper[$absolutePath] = $info;
1266
	}
1267
1268
	/**
1269
	 * @param Folder $node
1270
	 *
1271
	 * @return array
1272
	 */
1273
	private static function getAllChildren($node) {
1274
		$children = $node->getDirectoryListing();
1275
		$childrenFiles = [];
1276
1277
		foreach ($children as $child) {
1278
1279
			if ($child->getType() === FileInfo::TYPE_FOLDER) {
1280
				$childrenFiles = array_merge(
1281
					$childrenFiles,
1282
					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...
1283
				);
1284
			} else {
1285
				$childrenFiles[] = $child;
1286
			}
1287
		}
1288
1289
		return $childrenFiles;
1290
	}
1291
1292
	/**
1293
	 * @param array $args
1294
	 */
1295
	public static function post_delete_files($args) {
1296
		self::post_delete($args, 'files/');
1297
	}
1298
1299
	/**
1300
	 * @param array $args
1301
	 */
1302
	public static function post_delete_versions($args) {
1303
		self::post_delete($args, 'files/');
1304
	}
1305
1306
	/**
1307
	 * @param array $args
1308
	 * @param string $prefix
1309
	 */
1310
	public static function post_delete($args, $prefix = '') {
1311
		$path = Files\Filesystem::normalizePath($args['path']);
1312
		if (!isset(self::$deleteFileMapper[$path])) {
1313
			return;
1314
		}
1315
1316
		/** @var FileInfo $node */
1317
		$node = self::$deleteFileMapper[$path];
1318
		$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...
1319
		$preview->deleteAllPreviews();
1320
	}
1321
1322
}
1323