Passed
Push — master ( 02a671...a719b4 )
by John
19:43 queued 14s
created

Trashbin::getConfiguredTrashbinSize()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 11
rs 10
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bart Visscher <[email protected]>
6
 * @author Bastien Ho <[email protected]>
7
 * @author Bjoern Schiessle <[email protected]>
8
 * @author Björn Schießle <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Daniel Kesselberg <[email protected]>
11
 * @author Florin Peter <[email protected]>
12
 * @author Georg Ehrke <[email protected]>
13
 * @author Joas Schilling <[email protected]>
14
 * @author Jörn Friedrich Dreyer <[email protected]>
15
 * @author Juan Pablo Villafáñez <[email protected]>
16
 * @author Julius Härtl <[email protected]>
17
 * @author Lars Knickrehm <[email protected]>
18
 * @author Lukas Reschke <[email protected]>
19
 * @author Morris Jobke <[email protected]>
20
 * @author Qingping Hou <[email protected]>
21
 * @author Robin Appelman <[email protected]>
22
 * @author Robin McCorkell <[email protected]>
23
 * @author Roeland Jago Douma <[email protected]>
24
 * @author Sjors van der Pluijm <[email protected]>
25
 * @author Steven Bühner <[email protected]>
26
 * @author Thomas Müller <[email protected]>
27
 * @author Victor Dubiniuk <[email protected]>
28
 * @author Vincent Petry <[email protected]>
29
 *
30
 * @license AGPL-3.0
31
 *
32
 * This code is free software: you can redistribute it and/or modify
33
 * it under the terms of the GNU Affero General Public License, version 3,
34
 * as published by the Free Software Foundation.
35
 *
36
 * This program is distributed in the hope that it will be useful,
37
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
38
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39
 * GNU Affero General Public License for more details.
40
 *
41
 * You should have received a copy of the GNU Affero General Public License, version 3,
42
 * along with this program. If not, see <http://www.gnu.org/licenses/>
43
 *
44
 */
45
namespace OCA\Files_Trashbin;
46
47
use OC_User;
48
use OC\Files\Cache\Cache;
49
use OC\Files\Cache\CacheEntry;
50
use OC\Files\Cache\CacheQueryBuilder;
51
use OC\Files\Filesystem;
52
use OC\Files\ObjectStore\ObjectStoreStorage;
53
use OC\Files\View;
54
use OCA\Files_Trashbin\AppInfo\Application;
55
use OCA\Files_Trashbin\Command\Expire;
56
use OCP\AppFramework\Utility\ITimeFactory;
57
use OCP\App\IAppManager;
58
use OCP\Files\File;
59
use OCP\Files\Folder;
60
use OCP\Files\NotFoundException;
61
use OCP\Files\NotPermittedException;
62
use OCP\IConfig;
63
use OCP\Lock\ILockingProvider;
64
use OCP\Lock\LockedException;
65
use Psr\Log\LoggerInterface;
66
67
class Trashbin {
68
	// unit: percentage; 50% of available disk space/quota
69
	public const DEFAULTMAXSIZE = 50;
70
71
	/**
72
	 * Ensure we don't need to scan the file during the move to trash
73
	 * by triggering the scan in the pre-hook
74
	 *
75
	 * @param array $params
76
	 */
77
	public static function ensureFileScannedHook($params) {
78
		try {
79
			self::getUidAndFilename($params['path']);
80
		} catch (NotFoundException $e) {
81
			// nothing to scan for non existing files
82
		}
83
	}
84
85
	/**
86
	 * get the UID of the owner of the file and the path to the file relative to
87
	 * owners files folder
88
	 *
89
	 * @param string $filename
90
	 * @return array
91
	 * @throws \OC\User\NoUserException
92
	 */
93
	public static function getUidAndFilename($filename) {
94
		$uid = Filesystem::getOwner($filename);
95
		$userManager = \OC::$server->getUserManager();
96
		// if the user with the UID doesn't exists, e.g. because the UID points
97
		// to a remote user with a federated cloud ID we use the current logged-in
98
		// user. We need a valid local user to move the file to the right trash bin
99
		if (!$userManager->userExists($uid)) {
100
			$uid = OC_User::getUser();
101
		}
102
		if (!$uid) {
103
			// no owner, usually because of share link from ext storage
104
			return [null, null];
105
		}
106
		Filesystem::initMountPoints($uid);
107
		if ($uid !== OC_User::getUser()) {
108
			$info = Filesystem::getFileInfo($filename);
109
			$ownerView = new View('/' . $uid . '/files');
110
			try {
111
				$filename = $ownerView->getPath($info['fileid']);
112
			} catch (NotFoundException $e) {
113
				$filename = null;
114
			}
115
		}
116
		return [$uid, $filename];
117
	}
118
119
	/**
120
	 * get original location of files for user
121
	 *
122
	 * @param string $user
123
	 * @return array (filename => array (timestamp => original location))
124
	 */
125
	public static function getLocations($user) {
126
		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
127
		$query->select('id', 'timestamp', 'location')
128
			->from('files_trash')
129
			->where($query->expr()->eq('user', $query->createNamedParameter($user)));
130
		$result = $query->executeQuery();
131
		$array = [];
132
		while ($row = $result->fetch()) {
133
			if (isset($array[$row['id']])) {
134
				$array[$row['id']][$row['timestamp']] = $row['location'];
135
			} else {
136
				$array[$row['id']] = [$row['timestamp'] => $row['location']];
137
			}
138
		}
139
		$result->closeCursor();
140
		return $array;
141
	}
142
143
	/**
144
	 * get original location of file
145
	 *
146
	 * @param string $user
147
	 * @param string $filename
148
	 * @param string $timestamp
149
	 * @return string original location
150
	 */
151
	public static function getLocation($user, $filename, $timestamp) {
152
		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
153
		$query->select('location')
154
			->from('files_trash')
155
			->where($query->expr()->eq('user', $query->createNamedParameter($user)))
156
			->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
157
			->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
158
159
		$result = $query->executeQuery();
160
		$row = $result->fetch();
161
		$result->closeCursor();
162
163
		if (isset($row['location'])) {
164
			return $row['location'];
165
		} else {
166
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
167
		}
168
	}
169
170
	private static function setUpTrash($user) {
171
		$view = new View('/' . $user);
172
		if (!$view->is_dir('files_trashbin')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $view->is_dir('files_trashbin') of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
173
			$view->mkdir('files_trashbin');
174
		}
175
		if (!$view->is_dir('files_trashbin/files')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $view->is_dir('files_trashbin/files') of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
176
			$view->mkdir('files_trashbin/files');
177
		}
178
		if (!$view->is_dir('files_trashbin/versions')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $view->is_dir('files_trashbin/versions') of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
179
			$view->mkdir('files_trashbin/versions');
180
		}
181
		if (!$view->is_dir('files_trashbin/keys')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $view->is_dir('files_trashbin/keys') of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
182
			$view->mkdir('files_trashbin/keys');
183
		}
184
	}
185
186
187
	/**
188
	 * copy file to owners trash
189
	 *
190
	 * @param string $sourcePath
191
	 * @param string $owner
192
	 * @param string $targetPath
193
	 * @param $user
194
	 * @param int $timestamp
195
	 */
196
	private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
197
		self::setUpTrash($owner);
198
199
		$targetFilename = basename($targetPath);
200
		$targetLocation = dirname($targetPath);
201
202
		$sourceFilename = basename($sourcePath);
203
204
		$view = new View('/');
205
206
		$target = $user . '/files_trashbin/files/' . static::getTrashFilename($targetFilename, $timestamp);
207
		$source = $owner . '/files_trashbin/files/' . static::getTrashFilename($sourceFilename, $timestamp);
208
		$free = $view->free_space($target);
209
		$isUnknownOrUnlimitedFreeSpace = $free < 0;
210
		$isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
211
		if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) {
212
			self::copy_recursive($source, $target, $view);
213
		}
214
215
216
		if ($view->file_exists($target)) {
217
			$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
218
			$query->insert('files_trash')
219
				->setValue('id', $query->createNamedParameter($targetFilename))
220
				->setValue('timestamp', $query->createNamedParameter($timestamp))
221
				->setValue('location', $query->createNamedParameter($targetLocation))
222
				->setValue('user', $query->createNamedParameter($user));
223
			$result = $query->executeStatement();
224
			if (!$result) {
225
				\OC::$server->get(LoggerInterface::class)->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']);
226
			}
227
		}
228
	}
229
230
231
	/**
232
	 * move file to the trash bin
233
	 *
234
	 * @param string $file_path path to the deleted file/directory relative to the files root directory
235
	 * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
236
	 *
237
	 * @return bool
238
	 */
239
	public static function move2trash($file_path, $ownerOnly = false) {
240
		// get the user for which the filesystem is setup
241
		$root = Filesystem::getRoot();
242
		[, $user] = explode('/', $root);
243
		[$owner, $ownerPath] = self::getUidAndFilename($file_path);
244
245
		// if no owner found (ex: ext storage + share link), will use the current user's trashbin then
246
		if (is_null($owner)) {
247
			$owner = $user;
248
			$ownerPath = $file_path;
249
		}
250
251
		$ownerView = new View('/' . $owner);
252
253
		// file has been deleted in between
254
		if (is_null($ownerPath) || $ownerPath === '') {
255
			return true;
256
		}
257
258
		$sourceInfo = $ownerView->getFileInfo('/files/' . $ownerPath);
259
260
		if ($sourceInfo === false) {
261
			return true;
262
		}
263
264
		self::setUpTrash($user);
265
		if ($owner !== $user) {
266
			// also setup for owner
267
			self::setUpTrash($owner);
268
		}
269
270
		$path_parts = pathinfo($ownerPath);
271
272
		$filename = $path_parts['basename'];
273
		$location = $path_parts['dirname'];
274
		/** @var ITimeFactory $timeFactory */
275
		$timeFactory = \OC::$server->query(ITimeFactory::class);
276
		$timestamp = $timeFactory->getTime();
277
278
		$lockingProvider = \OC::$server->getLockingProvider();
279
280
		// disable proxy to prevent recursive calls
281
		$trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
282
		$gotLock = false;
283
284
		while (!$gotLock) {
285
			try {
286
				/** @var \OC\Files\Storage\Storage $trashStorage */
287
				[$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath);
288
289
				$trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
290
				$gotLock = true;
291
			} catch (LockedException $e) {
292
				// a file with the same name is being deleted concurrently
293
				// nudge the timestamp a bit to resolve the conflict
294
295
				$timestamp = $timestamp + 1;
296
297
				$trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
298
			}
299
		}
300
301
		$sourceStorage = $sourceInfo->getStorage();
302
		$sourceInternalPath = $sourceInfo->getInternalPath();
303
304
		if ($trashStorage->file_exists($trashInternalPath)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $trashStorage does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $trashInternalPath does not seem to be defined for all execution paths leading up to this point.
Loading history...
305
			$trashStorage->unlink($trashInternalPath);
306
		}
307
308
		$configuredTrashbinSize = static::getConfiguredTrashbinSize($owner);
309
		if ($configuredTrashbinSize >= 0 && $sourceInfo->getSize() >= $configuredTrashbinSize) {
310
			return false;
311
		}
312
313
		$trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
314
315
		try {
316
			$moveSuccessful = true;
317
318
			// when moving within the same object store, the cache update done above is enough to move the file
319
			if (!($trashStorage->instanceOfStorage(ObjectStoreStorage::class) && $trashStorage->getId() === $sourceStorage->getId())) {
320
				$trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
321
			}
322
		} catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
323
			$moveSuccessful = false;
324
			if ($trashStorage->file_exists($trashInternalPath)) {
325
				$trashStorage->unlink($trashInternalPath);
326
			}
327
			\OC::$server->get(LoggerInterface::class)->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
328
		}
329
330
		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
331
			if ($sourceStorage->is_dir($sourceInternalPath)) {
332
				$sourceStorage->rmdir($sourceInternalPath);
333
			} else {
334
				$sourceStorage->unlink($sourceInternalPath);
335
			}
336
337
			if ($sourceStorage->file_exists($sourceInternalPath)) {
338
				// undo the cache move
339
				$sourceStorage->getUpdater()->renameFromStorage($trashStorage, $trashInternalPath, $sourceInternalPath);
340
			} else {
341
				$trashStorage->getUpdater()->remove($trashInternalPath);
342
			}
343
			return false;
344
		}
345
346
		if ($moveSuccessful) {
347
			$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
348
			$query->insert('files_trash')
349
				->setValue('id', $query->createNamedParameter($filename))
350
				->setValue('timestamp', $query->createNamedParameter($timestamp))
351
				->setValue('location', $query->createNamedParameter($location))
352
				->setValue('user', $query->createNamedParameter($owner));
353
			$result = $query->executeStatement();
354
			if (!$result) {
355
				\OC::$server->get(LoggerInterface::class)->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
356
			}
357
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
358
				'trashPath' => Filesystem::normalizePath(static::getTrashFilename($filename, $timestamp))]);
359
360
			self::retainVersions($filename, $owner, $ownerPath, $timestamp);
361
362
			// if owner !== user we need to also add a copy to the users trash
363
			if ($user !== $owner && $ownerOnly === false) {
364
				self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
365
			}
366
		}
367
368
		$trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
369
370
		self::scheduleExpire($user);
371
372
		// if owner !== user we also need to update the owners trash size
373
		if ($owner !== $user) {
374
			self::scheduleExpire($owner);
375
		}
376
377
		return $moveSuccessful;
378
	}
379
380
	private static function getConfiguredTrashbinSize(string $user): int|float {
381
		$config = \OC::$server->get(IConfig::class);
382
		$userTrashbinSize = $config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
383
		if (is_numeric($userTrashbinSize) && ($userTrashbinSize > -1)) {
384
			return \OCP\Util::numericToNumber($userTrashbinSize);
385
		}
386
		$systemTrashbinSize = $config->getAppValue('files_trashbin', 'trashbin_size', '-1');
387
		if (is_numeric($systemTrashbinSize)) {
388
			return \OCP\Util::numericToNumber($systemTrashbinSize);
389
		}
390
		return -1;
391
	}
392
393
	/**
394
	 * Move file versions to trash so that they can be restored later
395
	 *
396
	 * @param string $filename of deleted file
397
	 * @param string $owner owner user id
398
	 * @param string $ownerPath path relative to the owner's home storage
399
	 * @param int $timestamp when the file was deleted
400
	 */
401
	private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
402
		if (\OCP\Server::get(IAppManager::class)->isEnabledForUser('files_versions') && !empty($ownerPath)) {
403
			$user = OC_User::getUser();
404
			$rootView = new View('/');
405
406
			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
407
				if ($owner !== $user) {
408
					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . static::getTrashFilename(basename($ownerPath), $timestamp), $rootView);
409
				}
410
				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . static::getTrashFilename($filename, $timestamp));
0 ignored issues
show
Bug introduced by
Are you sure $user of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

410
				self::move($rootView, $owner . '/files_versions/' . $ownerPath, /** @scrutinizer ignore-type */ $user . '/files_trashbin/versions/' . static::getTrashFilename($filename, $timestamp));
Loading history...
411
			} elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
412
				foreach ($versions as $v) {
413
					if ($owner !== $user) {
414
						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . static::getTrashFilename($v['name'] . '.v' . $v['version'], $timestamp));
415
					}
416
					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v['version'], $timestamp));
417
				}
418
			}
419
		}
420
	}
421
422
	/**
423
	 * Move a file or folder on storage level
424
	 *
425
	 * @param View $view
426
	 * @param string $source
427
	 * @param string $target
428
	 * @return bool
429
	 */
430
	private static function move(View $view, $source, $target) {
431
		/** @var \OC\Files\Storage\Storage $sourceStorage */
432
		[$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
433
		/** @var \OC\Files\Storage\Storage $targetStorage */
434
		[$targetStorage, $targetInternalPath] = $view->resolvePath($target);
435
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
436
437
		$result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
438
		if ($result) {
439
			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
440
		}
441
		return $result;
442
	}
443
444
	/**
445
	 * Copy a file or folder on storage level
446
	 *
447
	 * @param View $view
448
	 * @param string $source
449
	 * @param string $target
450
	 * @return bool
451
	 */
452
	private static function copy(View $view, $source, $target) {
453
		/** @var \OC\Files\Storage\Storage $sourceStorage */
454
		[$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
455
		/** @var \OC\Files\Storage\Storage $targetStorage */
456
		[$targetStorage, $targetInternalPath] = $view->resolvePath($target);
457
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
458
459
		$result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
460
		if ($result) {
461
			$targetStorage->getUpdater()->update($targetInternalPath);
462
		}
463
		return $result;
464
	}
465
466
	/**
467
	 * Restore a file or folder from trash bin
468
	 *
469
	 * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
470
	 * including the timestamp suffix ".d12345678"
471
	 * @param string $filename name of the file/folder
472
	 * @param int $timestamp time when the file/folder was deleted
473
	 *
474
	 * @return bool true on success, false otherwise
475
	 */
476
	public static function restore($file, $filename, $timestamp) {
477
		$user = OC_User::getUser();
478
		$view = new View('/' . $user);
0 ignored issues
show
Bug introduced by
Are you sure $user of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

478
		$view = new View('/' . /** @scrutinizer ignore-type */ $user);
Loading history...
479
480
		$location = '';
481
		if ($timestamp) {
482
			$location = self::getLocation($user, $filename, $timestamp);
483
			if ($location === false) {
0 ignored issues
show
introduced by
The condition $location === false is always false.
Loading history...
484
				\OC::$server->get(LoggerInterface::class)->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
485
			} else {
486
				// if location no longer exists, restore file in the root directory
487
				if ($location !== '/' &&
488
					(!$view->is_dir('files/' . $location) ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $view->is_dir('files/' . $location) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
489
						!$view->isCreatable('files/' . $location))
0 ignored issues
show
Bug Best Practice introduced by
The expression $view->isCreatable('files/' . $location) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
490
				) {
491
					$location = '';
492
				}
493
			}
494
		}
495
496
		// we need a  extension in case a file/dir with the same name already exists
497
		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
498
499
		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
500
		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
501
		if (!$view->file_exists($source)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $view->file_exists($source) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
502
			return false;
503
		}
504
		$mtime = $view->filemtime($source);
505
506
		// restore file
507
		if (!$view->isCreatable(dirname($target))) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $view->isCreatable(dirname($target)) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
508
			throw new NotPermittedException("Can't restore trash item because the target folder is not writable");
509
		}
510
		$restoreResult = $view->rename($source, $target);
511
512
		// handle the restore result
513
		if ($restoreResult) {
514
			$fakeRoot = $view->getRoot();
515
			$view->chroot('/' . $user . '/files');
516
			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
517
			$view->chroot($fakeRoot);
518
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
519
				'trashPath' => Filesystem::normalizePath($file)]);
520
521
			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
522
523
			if ($timestamp) {
524
				$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
525
				$query->delete('files_trash')
526
					->where($query->expr()->eq('user', $query->createNamedParameter($user)))
527
					->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
528
					->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
529
				$query->executeStatement();
530
			}
531
532
			return true;
533
		}
534
535
		return false;
536
	}
537
538
	/**
539
	 * restore versions from trash bin
540
	 *
541
	 * @param View $view file view
542
	 * @param string $file complete path to file
543
	 * @param string $filename name of file once it was deleted
544
	 * @param string $uniqueFilename new file name to restore the file without overwriting existing files
545
	 * @param string $location location if file
546
	 * @param int $timestamp deletion time
547
	 * @return false|null
548
	 */
549
	private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
550
		if (\OCP\Server::get(IAppManager::class)->isEnabledForUser('files_versions')) {
551
			$user = OC_User::getUser();
552
			$rootView = new View('/');
553
554
			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
555
556
			[$owner, $ownerPath] = self::getUidAndFilename($target);
557
558
			// file has been deleted in between
559
			if (empty($ownerPath)) {
560
				return false;
561
			}
562
563
			if ($timestamp) {
564
				$versionedFile = $filename;
565
			} else {
566
				$versionedFile = $file;
567
			}
568
569
			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
570
				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
0 ignored issues
show
Bug introduced by
Are you sure $user of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

570
				$rootView->rename(Filesystem::normalizePath(/** @scrutinizer ignore-type */ $user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
Loading history...
571
			} elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
572
				foreach ($versions as $v) {
573
					if ($timestamp) {
574
						$rootView->rename($user . '/files_trashbin/versions/' . static::getTrashFilename($versionedFile . '.v' . $v, $timestamp), $owner . '/files_versions/' . $ownerPath . '.v' . $v);
575
					} else {
576
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
577
					}
578
				}
579
			}
580
		}
581
	}
582
583
	/**
584
	 * delete all files from the trash
585
	 */
586
	public static function deleteAll() {
587
		$user = OC_User::getUser();
588
		$userRoot = \OC::$server->getUserFolder($user)->getParent();
589
		$view = new View('/' . $user);
0 ignored issues
show
Bug introduced by
Are you sure $user of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

589
		$view = new View('/' . /** @scrutinizer ignore-type */ $user);
Loading history...
590
		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
591
592
		try {
593
			$trash = $userRoot->get('files_trashbin');
594
		} catch (NotFoundException $e) {
595
			return false;
596
		}
597
598
		// Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
599
		$filePaths = [];
600
		foreach ($fileInfos as $fileInfo) {
601
			$filePaths[] = $view->getRelativePath($fileInfo->getPath());
602
		}
603
		unset($fileInfos); // save memory
604
605
		// Bulk PreDelete-Hook
606
		\OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
607
608
		// Single-File Hooks
609
		foreach ($filePaths as $path) {
610
			self::emitTrashbinPreDelete($path);
611
		}
612
613
		// actual file deletion
614
		$trash->delete();
615
616
		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
617
		$query->delete('files_trash')
618
			->where($query->expr()->eq('user', $query->createNamedParameter($user)));
619
		$query->executeStatement();
620
621
		// Bulk PostDelete-Hook
622
		\OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
623
624
		// Single-File Hooks
625
		foreach ($filePaths as $path) {
626
			self::emitTrashbinPostDelete($path);
627
		}
628
629
		$trash = $userRoot->newFolder('files_trashbin');
630
		$trash->newFolder('files');
631
632
		return true;
633
	}
634
635
	/**
636
	 * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
637
	 *
638
	 * @param string $path
639
	 */
640
	protected static function emitTrashbinPreDelete($path) {
641
		\OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
642
	}
643
644
	/**
645
	 * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
646
	 *
647
	 * @param string $path
648
	 */
649
	protected static function emitTrashbinPostDelete($path) {
650
		\OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
651
	}
652
653
	/**
654
	 * delete file from trash bin permanently
655
	 *
656
	 * @param string $filename path to the file
657
	 * @param string $user
658
	 * @param int $timestamp of deletion time
659
	 *
660
	 * @return int|float size of deleted files
661
	 */
662
	public static function delete($filename, $user, $timestamp = null) {
663
		$userRoot = \OC::$server->getUserFolder($user)->getParent();
664
		$view = new View('/' . $user);
665
		$size = 0;
666
667
		if ($timestamp) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $timestamp of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

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

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
668
			$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
669
			$query->delete('files_trash')
670
				->where($query->expr()->eq('user', $query->createNamedParameter($user)))
671
				->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
672
				->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
673
			$query->executeStatement();
674
675
			$file = static::getTrashFilename($filename, $timestamp);
676
		} else {
677
			$file = $filename;
678
		}
679
680
		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
681
682
		try {
683
			$node = $userRoot->get('/files_trashbin/files/' . $file);
684
		} catch (NotFoundException $e) {
685
			return $size;
686
		}
687
688
		if ($node instanceof Folder) {
689
			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
690
		} elseif ($node instanceof File) {
691
			$size += $view->filesize('/files_trashbin/files/' . $file);
692
		}
693
694
		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
695
		$node->delete();
696
		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
697
698
		return $size;
699
	}
700
701
	/**
702
	 * @param string $file
703
	 * @param string $filename
704
	 * @param ?int $timestamp
705
	 */
706
	private static function deleteVersions(View $view, $file, $filename, $timestamp, string $user): int|float {
707
		$size = 0;
708
		if (\OCP\Server::get(IAppManager::class)->isEnabledForUser('files_versions')) {
709
			if ($view->is_dir('files_trashbin/versions/' . $file)) {
710
				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
711
				$view->unlink('files_trashbin/versions/' . $file);
712
			} elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
713
				foreach ($versions as $v) {
714
					if ($timestamp) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $timestamp of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

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

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
715
						$size += $view->filesize('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
716
						$view->unlink('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
717
					} else {
718
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
719
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
720
					}
721
				}
722
			}
723
		}
724
		return $size;
725
	}
726
727
	/**
728
	 * check to see whether a file exists in trashbin
729
	 *
730
	 * @param string $filename path to the file
731
	 * @param int $timestamp of deletion time
732
	 * @return bool true if file exists, otherwise false
733
	 */
734
	public static function file_exists($filename, $timestamp = null) {
735
		$user = OC_User::getUser();
736
		$view = new View('/' . $user);
0 ignored issues
show
Bug introduced by
Are you sure $user of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

736
		$view = new View('/' . /** @scrutinizer ignore-type */ $user);
Loading history...
737
738
		if ($timestamp) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $timestamp of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

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

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
739
			$filename = static::getTrashFilename($filename, $timestamp);
740
		}
741
742
		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
743
		return $view->file_exists($target);
744
	}
745
746
	/**
747
	 * deletes used space for trash bin in db if user was deleted
748
	 *
749
	 * @param string $uid id of deleted user
750
	 * @return bool result of db delete operation
751
	 */
752
	public static function deleteUser($uid) {
753
		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
754
		$query->delete('files_trash')
755
			->where($query->expr()->eq('user', $query->createNamedParameter($uid)));
756
		return (bool) $query->executeStatement();
757
	}
758
759
	/**
760
	 * calculate remaining free space for trash bin
761
	 *
762
	 * @param int|float $trashbinSize current size of the trash bin
763
	 * @param string $user
764
	 * @return int|float available free space for trash bin
765
	 */
766
	private static function calculateFreeSpace(int|float $trashbinSize, string $user): int|float {
767
		$configuredTrashbinSize = static::getConfiguredTrashbinSize($user);
768
		if ($configuredTrashbinSize > -1) {
769
			return $configuredTrashbinSize - $trashbinSize;
770
		}
771
772
		$userObject = \OC::$server->getUserManager()->get($user);
773
		if (is_null($userObject)) {
774
			return 0;
775
		}
776
		$softQuota = true;
777
		$quota = $userObject->getQuota();
778
		if ($quota === null || $quota === 'none') {
779
			$quota = Filesystem::free_space('/');
780
			$softQuota = false;
781
			// inf or unknown free space
782
			if ($quota < 0) {
783
				$quota = PHP_INT_MAX;
784
			}
785
		} else {
786
			$quota = \OCP\Util::computerFileSize($quota);
787
			// invalid quota
788
			if ($quota === false) {
789
				$quota = PHP_INT_MAX;
790
			}
791
		}
792
793
		// calculate available space for trash bin
794
		// subtract size of files and current trash bin size from quota
795
		if ($softQuota) {
796
			$userFolder = \OC::$server->getUserFolder($user);
797
			if (is_null($userFolder)) {
798
				return 0;
799
			}
800
			$free = $quota - $userFolder->getSize(false); // remaining free space for user
801
			if ($free > 0) {
802
				$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
803
			} else {
804
				$availableSpace = $free - $trashbinSize;
805
			}
806
		} else {
807
			$availableSpace = $quota;
808
		}
809
810
		return \OCP\Util::numericToNumber($availableSpace);
811
	}
812
813
	/**
814
	 * resize trash bin if necessary after a new file was added to Nextcloud
815
	 *
816
	 * @param string $user user id
817
	 */
818
	public static function resizeTrash($user) {
819
		$size = self::getTrashbinSize($user);
820
821
		$freeSpace = self::calculateFreeSpace($size, $user);
822
823
		if ($freeSpace < 0) {
824
			self::scheduleExpire($user);
825
		}
826
	}
827
828
	/**
829
	 * clean up the trash bin
830
	 *
831
	 * @param string $user
832
	 */
833
	public static function expire($user) {
834
		$trashBinSize = self::getTrashbinSize($user);
835
		$availableSpace = self::calculateFreeSpace($trashBinSize, $user);
836
837
		$dirContent = Helper::getTrashFiles('/', $user, 'mtime');
838
839
		// delete all files older then $retention_obligation
840
		[$delSize, $count] = self::deleteExpiredFiles($dirContent, $user);
841
842
		$availableSpace += $delSize;
843
844
		// delete files from trash until we meet the trash bin size limit again
845
		self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
846
	}
847
848
	/**
849
	 * @param string $user
850
	 */
851
	private static function scheduleExpire($user) {
852
		// let the admin disable auto expire
853
		/** @var Application $application */
854
		$application = \OC::$server->query(Application::class);
855
		$expiration = $application->getContainer()->query('Expiration');
856
		if ($expiration->isEnabled()) {
857
			\OC::$server->getCommandBus()->push(new Expire($user));
858
		}
859
	}
860
861
	/**
862
	 * if the size limit for the trash bin is reached, we delete the oldest
863
	 * files in the trash bin until we meet the limit again
864
	 *
865
	 * @param array $files
866
	 * @param string $user
867
	 * @param int|float $availableSpace available disc space
868
	 * @return int|float size of deleted files
869
	 */
870
	protected static function deleteFiles(array $files, string $user, int|float $availableSpace): int|float {
871
		/** @var Application $application */
872
		$application = \OC::$server->query(Application::class);
873
		$expiration = $application->getContainer()->query('Expiration');
874
		$size = 0;
875
876
		if ($availableSpace < 0) {
877
			foreach ($files as $file) {
878
				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
879
					$tmp = self::delete($file['name'], $user, $file['mtime']);
880
					\OC::$server->get(LoggerInterface::class)->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
881
					$availableSpace += $tmp;
882
					$size += $tmp;
883
				} else {
884
					break;
885
				}
886
			}
887
		}
888
		return $size;
889
	}
890
891
	/**
892
	 * delete files older then max storage time
893
	 *
894
	 * @param array $files list of files sorted by mtime
895
	 * @param string $user
896
	 * @return array{int|float, int} size of deleted files and number of deleted files
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{int|float, int} at position 2 could not be parsed: Expected ':' at position 2, but found 'int'.
Loading history...
897
	 */
898
	public static function deleteExpiredFiles($files, $user) {
899
		/** @var Expiration $expiration */
900
		$expiration = \OC::$server->query(Expiration::class);
901
		$size = 0;
902
		$count = 0;
903
		foreach ($files as $file) {
904
			$timestamp = $file['mtime'];
905
			$filename = $file['name'];
906
			if ($expiration->isExpired($timestamp)) {
907
				try {
908
					$size += self::delete($filename, $user, $timestamp);
909
					$count++;
910
				} catch (\OCP\Files\NotPermittedException $e) {
911
					\OC::$server->get(LoggerInterface::class)->warning('Removing "' . $filename . '" from trashbin failed.',
912
						[
913
							'exception' => $e,
914
							'app' => 'files_trashbin',
915
						]
916
					);
917
				}
918
				\OC::$server->get(LoggerInterface::class)->info(
919
					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
920
					['app' => 'files_trashbin']
921
				);
922
			} else {
923
				break;
924
			}
925
		}
926
927
		return [$size, $count];
928
	}
929
930
	/**
931
	 * recursive copy to copy a whole directory
932
	 *
933
	 * @param string $source source path, relative to the users files directory
934
	 * @param string $destination destination path relative to the users root directory
935
	 * @param View $view file view for the users root directory
936
	 * @return int|float
937
	 * @throws Exceptions\CopyRecursiveException
938
	 */
939
	private static function copy_recursive($source, $destination, View $view): int|float {
940
		$size = 0;
941
		if ($view->is_dir($source)) {
942
			$view->mkdir($destination);
943
			$view->touch($destination, $view->filemtime($source));
944
			foreach ($view->getDirectoryContent($source) as $i) {
945
				$pathDir = $source . '/' . $i['name'];
946
				if ($view->is_dir($pathDir)) {
947
					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
948
				} else {
949
					$size += $view->filesize($pathDir);
950
					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
951
					if (!$result) {
952
						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
953
					}
954
					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
955
				}
956
			}
957
		} else {
958
			$size += $view->filesize($source);
959
			$result = $view->copy($source, $destination);
960
			if (!$result) {
961
				throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
962
			}
963
			$view->touch($destination, $view->filemtime($source));
964
		}
965
		return $size;
966
	}
967
968
	/**
969
	 * find all versions which belong to the file we want to restore
970
	 *
971
	 * @param string $filename name of the file which should be restored
972
	 * @param int $timestamp timestamp when the file was deleted
973
	 */
974
	private static function getVersionsFromTrash($filename, $timestamp, string $user): array {
975
		$view = new View('/' . $user . '/files_trashbin/versions');
976
		$versions = [];
977
978
		/** @var \OC\Files\Storage\Storage $storage */
979
		[$storage,] = $view->resolvePath('/');
980
981
		$pattern = \OC::$server->getDatabaseConnection()->escapeLikeParameter(basename($filename));
982
		if ($timestamp) {
983
			// fetch for old versions
984
			$escapedTimestamp = \OC::$server->getDatabaseConnection()->escapeLikeParameter($timestamp);
985
			$pattern .= '.v%.d' . $escapedTimestamp;
986
			$offset = -strlen($escapedTimestamp) - 2;
987
		} else {
988
			$pattern .= '.v%';
989
		}
990
991
		// Manually fetch all versions from the file cache to be able to filter them by their parent
992
		$cache = $storage->getCache('');
993
		$query = new CacheQueryBuilder(
994
			\OC::$server->getDatabaseConnection(),
995
			\OC::$server->getSystemConfig(),
996
			\OC::$server->get(LoggerInterface::class)
997
		);
998
		$normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/');
999
		$parentId = $cache->getId($normalizedParentPath);
1000
		if ($parentId === -1) {
1001
			return [];
1002
		}
1003
1004
		$query->selectFileCache()
1005
			->whereStorageId($cache->getNumericStorageId())
1006
			->andWhere($query->expr()->eq('parent', $query->createNamedParameter($parentId)))
1007
			->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
1008
1009
		$result = $query->executeQuery();
1010
		$entries = $result->fetchAll();
1011
		$result->closeCursor();
1012
1013
		/** @var CacheEntry[] $matches */
1014
		$matches = array_map(function (array $data) {
1015
			return Cache::cacheEntryFromData($data, \OC::$server->getMimeTypeLoader());
1016
		}, $entries);
1017
1018
		foreach ($matches as $ma) {
1019
			if ($timestamp) {
1020
				$parts = explode('.v', substr($ma['path'], 0, $offset));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $offset does not seem to be defined for all execution paths leading up to this point.
Loading history...
1021
				$versions[] = end($parts);
1022
			} else {
1023
				$parts = explode('.v', $ma['path']);
1024
				$versions[] = end($parts);
1025
			}
1026
		}
1027
1028
		return $versions;
1029
	}
1030
1031
	/**
1032
	 * find unique extension for restored file if a file with the same name already exists
1033
	 *
1034
	 * @param string $location where the file should be restored
1035
	 * @param string $filename name of the file
1036
	 * @param View $view filesystem view relative to users root directory
1037
	 * @return string with unique extension
1038
	 */
1039
	private static function getUniqueFilename($location, $filename, View $view) {
1040
		$ext = pathinfo($filename, PATHINFO_EXTENSION);
1041
		$name = pathinfo($filename, PATHINFO_FILENAME);
1042
		$l = \OC::$server->getL10N('files_trashbin');
1043
1044
		$location = '/' . trim($location, '/');
1045
1046
		// if extension is not empty we set a dot in front of it
1047
		if ($ext !== '') {
1048
			$ext = '.' . $ext;
0 ignored issues
show
Bug introduced by
Are you sure $ext of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1048
			$ext = '.' . /** @scrutinizer ignore-type */ $ext;
Loading history...
1049
		}
1050
1051
		if ($view->file_exists('files' . $location . '/' . $filename)) {
1052
			$i = 2;
1053
			$uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
0 ignored issues
show
Bug introduced by
Are you sure $name of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1053
			$uniqueName = /** @scrutinizer ignore-type */ $name . " (" . $l->t("restored") . ")" . $ext;
Loading history...
1054
			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
1055
				$uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
1056
				$i++;
1057
			}
1058
1059
			return $uniqueName;
1060
		}
1061
1062
		return $filename;
1063
	}
1064
1065
	/**
1066
	 * get the size from a given root folder
1067
	 *
1068
	 * @param View $view file view on the root folder
1069
	 * @return int|float size of the folder
1070
	 */
1071
	private static function calculateSize(View $view): int|float {
1072
		$root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
1073
		if (!file_exists($root)) {
1074
			return 0;
1075
		}
1076
		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
1077
		$size = 0;
1078
1079
		/**
1080
		 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
1081
		 * This bug is fixed in PHP 5.5.9 or before
1082
		 * See #8376
1083
		 */
1084
		$iterator->rewind();
1085
		while ($iterator->valid()) {
1086
			$path = $iterator->current();
1087
			$relpath = substr($path, strlen($root) - 1);
1088
			if (!$view->is_dir($relpath)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $view->is_dir($relpath) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
1089
				$size += $view->filesize($relpath);
1090
			}
1091
			$iterator->next();
1092
		}
1093
		return $size;
1094
	}
1095
1096
	/**
1097
	 * get current size of trash bin from a given user
1098
	 *
1099
	 * @param string $user user who owns the trash bin
1100
	 * @return int|float trash bin size
1101
	 */
1102
	private static function getTrashbinSize(string $user): int|float {
1103
		$view = new View('/' . $user);
1104
		$fileInfo = $view->getFileInfo('/files_trashbin');
1105
		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1106
	}
1107
1108
	/**
1109
	 * check if trash bin is empty for a given user
1110
	 *
1111
	 * @param string $user
1112
	 * @return bool
1113
	 */
1114
	public static function isEmpty($user) {
1115
		$view = new View('/' . $user . '/files_trashbin');
1116
		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1117
			while ($file = readdir($dh)) {
1118
				if (!Filesystem::isIgnoredDir($file)) {
1119
					return false;
1120
				}
1121
			}
1122
		}
1123
		return true;
1124
	}
1125
1126
	/**
1127
	 * @param $path
1128
	 * @return string
1129
	 */
1130
	public static function preview_icon($path) {
1131
		return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]);
1132
	}
1133
1134
	/**
1135
	 * Return the filename used in the trash bin
1136
	 */
1137
	public static function getTrashFilename(string $filename, int $timestamp): string {
1138
		$trashFilename = $filename . '.d' . $timestamp;
1139
		$length = strlen($trashFilename);
1140
		// oc_filecache `name` column has a limit of 250 chars
1141
		$maxLength = 250;
1142
		if ($length > $maxLength) {
1143
			$trashFilename = substr_replace(
1144
				$trashFilename,
1145
				'',
1146
				$maxLength / 2,
1147
				$length - $maxLength
1148
			);
1149
		}
1150
		return $trashFilename;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $trashFilename could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
1151
	}
1152
}
1153