Passed
Push — master ( 62403d...0c3e2f )
by Joas
14:50 queued 14s
created

Trashbin::preview_icon()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
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 Florin Peter <[email protected]>
11
 * @author Georg Ehrke <[email protected]>
12
 * @author Joas Schilling <[email protected]>
13
 * @author Jörn Friedrich Dreyer <[email protected]>
14
 * @author Juan Pablo Villafáñez <[email protected]>
15
 * @author Lars Knickrehm <[email protected]>
16
 * @author Lukas Reschke <[email protected]>
17
 * @author Morris Jobke <[email protected]>
18
 * @author Qingping Hou <[email protected]>
19
 * @author Robin Appelman <[email protected]>
20
 * @author Robin McCorkell <[email protected]>
21
 * @author Roeland Jago Douma <[email protected]>
22
 * @author Sjors van der Pluijm <[email protected]>
23
 * @author Steven Bühner <[email protected]>
24
 * @author Thomas Müller <[email protected]>
25
 * @author Victor Dubiniuk <[email protected]>
26
 * @author Vincent Petry <[email protected]>
27
 *
28
 * @license AGPL-3.0
29
 *
30
 * This code is free software: you can redistribute it and/or modify
31
 * it under the terms of the GNU Affero General Public License, version 3,
32
 * as published by the Free Software Foundation.
33
 *
34
 * This program is distributed in the hope that it will be useful,
35
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
36
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
37
 * GNU Affero General Public License for more details.
38
 *
39
 * You should have received a copy of the GNU Affero General Public License, version 3,
40
 * along with this program. If not, see <http://www.gnu.org/licenses/>
41
 *
42
 */
43
44
namespace OCA\Files_Trashbin;
45
46
use OC\Files\Filesystem;
47
use OC\Files\View;
48
use OCA\Files_Trashbin\AppInfo\Application;
49
use OCA\Files_Trashbin\Command\Expire;
50
use OCP\Files\File;
51
use OCP\Files\Folder;
52
use OCP\Files\NotFoundException;
53
use OCP\Files\NotPermittedException;
54
use OCP\User;
55
56
class Trashbin {
57
58
	// unit: percentage; 50% of available disk space/quota
59
	const DEFAULTMAXSIZE = 50;
60
61
	/**
62
	 * Whether versions have already be rescanned during this PHP request
63
	 *
64
	 * @var bool
65
	 */
66
	private static $scannedVersions = false;
67
68
	/**
69
	 * Ensure we don't need to scan the file during the move to trash
70
	 * by triggering the scan in the pre-hook
71
	 *
72
	 * @param array $params
73
	 */
74
	public static function ensureFileScannedHook($params) {
75
		try {
76
			self::getUidAndFilename($params['path']);
77
		} catch (NotFoundException $e) {
78
			// nothing to scan for non existing files
79
		}
80
	}
81
82
	/**
83
	 * get the UID of the owner of the file and the path to the file relative to
84
	 * owners files folder
85
	 *
86
	 * @param string $filename
87
	 * @return array
88
	 * @throws \OC\User\NoUserException
89
	 */
90
	public static function getUidAndFilename($filename) {
91
		$uid = Filesystem::getOwner($filename);
92
		$userManager = \OC::$server->getUserManager();
93
		// if the user with the UID doesn't exists, e.g. because the UID points
94
		// to a remote user with a federated cloud ID we use the current logged-in
95
		// user. We need a valid local user to move the file to the right trash bin
96
		if (!$userManager->userExists($uid)) {
97
			$uid = User::getUser();
0 ignored issues
show
Deprecated Code introduced by
The function OCP\User::getUser() has been deprecated: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID() ( Ignorable by Annotation )

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

97
			$uid = /** @scrutinizer ignore-deprecated */ User::getUser();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
98
		}
99
		if (!$uid) {
100
			// no owner, usually because of share link from ext storage
101
			return [null, null];
102
		}
103
		Filesystem::initMountPoints($uid);
104
		if ($uid !== User::getUser()) {
0 ignored issues
show
Deprecated Code introduced by
The function OCP\User::getUser() has been deprecated: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID() ( Ignorable by Annotation )

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

104
		if ($uid !== /** @scrutinizer ignore-deprecated */ User::getUser()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
105
			$info = Filesystem::getFileInfo($filename);
106
			$ownerView = new View('/' . $uid . '/files');
107
			try {
108
				$filename = $ownerView->getPath($info['fileid']);
109
			} catch (NotFoundException $e) {
110
				$filename = null;
111
			}
112
		}
113
		return [$uid, $filename];
114
	}
115
116
	/**
117
	 * get original location of files for user
118
	 *
119
	 * @param string $user
120
	 * @return array (filename => array (timestamp => original location))
121
	 */
122
	public static function getLocations($user) {
123
		$query = \OC_DB::prepare('SELECT `id`, `timestamp`, `location`'
124
			. ' FROM `*PREFIX*files_trash` WHERE `user`=?');
125
		$result = $query->execute([$user]);
126
		$array = [];
127
		while ($row = $result->fetchRow()) {
128
			if (isset($array[$row['id']])) {
129
				$array[$row['id']][$row['timestamp']] = $row['location'];
130
			} else {
131
				$array[$row['id']] = [$row['timestamp'] => $row['location']];
132
			}
133
		}
134
		return $array;
135
	}
136
137
	/**
138
	 * get original location of file
139
	 *
140
	 * @param string $user
141
	 * @param string $filename
142
	 * @param string $timestamp
143
	 * @return string original location
144
	 */
145
	public static function getLocation($user, $filename, $timestamp) {
146
		$query = \OC_DB::prepare('SELECT `location` FROM `*PREFIX*files_trash`'
147
			. ' WHERE `user`=? AND `id`=? AND `timestamp`=?');
148
		$result = $query->execute([$user, $filename, $timestamp])->fetchAll();
149
		if (isset($result[0]['location'])) {
150
			return $result[0]['location'];
151
		} else {
152
			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...
153
		}
154
	}
155
156
	private static function setUpTrash($user) {
157
		$view = new View('/' . $user);
158
		if (!$view->is_dir('files_trashbin')) {
159
			$view->mkdir('files_trashbin');
160
		}
161
		if (!$view->is_dir('files_trashbin/files')) {
162
			$view->mkdir('files_trashbin/files');
163
		}
164
		if (!$view->is_dir('files_trashbin/versions')) {
165
			$view->mkdir('files_trashbin/versions');
166
		}
167
		if (!$view->is_dir('files_trashbin/keys')) {
168
			$view->mkdir('files_trashbin/keys');
169
		}
170
	}
171
172
173
	/**
174
	 * copy file to owners trash
175
	 *
176
	 * @param string $sourcePath
177
	 * @param string $owner
178
	 * @param string $targetPath
179
	 * @param $user
180
	 * @param integer $timestamp
181
	 */
182
	private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
183
		self::setUpTrash($owner);
184
185
		$targetFilename = basename($targetPath);
186
		$targetLocation = dirname($targetPath);
187
188
		$sourceFilename = basename($sourcePath);
189
190
		$view = new View('/');
191
192
		$target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
193
		$source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
194
		$free = $view->free_space($target);
195
		$isUnknownOrUnlimitedFreeSpace = $free < 0;
196
		$isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
197
		if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) {
198
			self::copy_recursive($source, $target, $view);
199
		}
200
201
202
		if ($view->file_exists($target)) {
203
			$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
204
			$result = $query->execute([$targetFilename, $timestamp, $targetLocation, $user]);
205
			if (!$result) {
206
				\OC::$server->getLogger()->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']);
207
			}
208
		}
209
	}
210
211
212
	/**
213
	 * move file to the trash bin
214
	 *
215
	 * @param string $file_path path to the deleted file/directory relative to the files root directory
216
	 * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
217
	 *
218
	 * @return bool
219
	 */
220
	public static function move2trash($file_path, $ownerOnly = false) {
221
		// get the user for which the filesystem is setup
222
		$root = Filesystem::getRoot();
223
		list(, $user) = explode('/', $root);
224
		list($owner, $ownerPath) = self::getUidAndFilename($file_path);
225
226
		// if no owner found (ex: ext storage + share link), will use the current user's trashbin then
227
		if (is_null($owner)) {
228
			$owner = $user;
229
			$ownerPath = $file_path;
230
		}
231
232
		$ownerView = new View('/' . $owner);
233
		// file has been deleted in between
234
		if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
235
			return true;
236
		}
237
238
		self::setUpTrash($user);
239
		if ($owner !== $user) {
240
			// also setup for owner
241
			self::setUpTrash($owner);
242
		}
243
244
		$path_parts = pathinfo($ownerPath);
245
246
		$filename = $path_parts['basename'];
247
		$location = $path_parts['dirname'];
248
		$timestamp = time();
249
250
		// disable proxy to prevent recursive calls
251
		$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
252
253
		/** @var \OC\Files\Storage\Storage $trashStorage */
254
		list($trashStorage, $trashInternalPath) = $ownerView->resolvePath($trashPath);
255
		/** @var \OC\Files\Storage\Storage $sourceStorage */
256
		list($sourceStorage, $sourceInternalPath) = $ownerView->resolvePath('/files/' . $ownerPath);
257
		try {
258
			$moveSuccessful = true;
259
			if ($trashStorage->file_exists($trashInternalPath)) {
260
				$trashStorage->unlink($trashInternalPath);
261
			}
262
			$trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
263
		} catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
264
			$moveSuccessful = false;
265
			if ($trashStorage->file_exists($trashInternalPath)) {
266
				$trashStorage->unlink($trashInternalPath);
267
			}
268
			\OC::$server->getLogger()->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
269
		}
270
271
		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
272
			if ($sourceStorage->is_dir($sourceInternalPath)) {
273
				$sourceStorage->rmdir($sourceInternalPath);
274
			} else {
275
				$sourceStorage->unlink($sourceInternalPath);
276
			}
277
			return false;
278
		}
279
280
		$trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
281
282
		if ($moveSuccessful) {
283
			$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
284
			$result = $query->execute([$filename, $timestamp, $location, $owner]);
285
			if (!$result) {
286
				\OC::$server->getLogger()->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
287
			}
288
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
289
				'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)]);
290
291
			self::retainVersions($filename, $owner, $ownerPath, $timestamp);
292
293
			// if owner !== user we need to also add a copy to the users trash
294
			if ($user !== $owner && $ownerOnly === false) {
295
				self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
296
			}
297
		}
298
299
		self::scheduleExpire($user);
300
301
		// if owner !== user we also need to update the owners trash size
302
		if ($owner !== $user) {
303
			self::scheduleExpire($owner);
304
		}
305
306
		return $moveSuccessful;
307
	}
308
309
	/**
310
	 * Move file versions to trash so that they can be restored later
311
	 *
312
	 * @param string $filename of deleted file
313
	 * @param string $owner owner user id
314
	 * @param string $ownerPath path relative to the owner's home storage
315
	 * @param integer $timestamp when the file was deleted
316
	 */
317
	private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
318
		if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {
0 ignored issues
show
Deprecated Code introduced by
The function OCP\App::isEnabled() has been deprecated: 13.0.0 use \OC::$server->getAppManager()->isEnabledForUser($appId) ( Ignorable by Annotation )

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

318
		if (/** @scrutinizer ignore-deprecated */ \OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
319
320
			$user = User::getUser();
0 ignored issues
show
Deprecated Code introduced by
The function OCP\User::getUser() has been deprecated: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID() ( Ignorable by Annotation )

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

320
			$user = /** @scrutinizer ignore-deprecated */ User::getUser();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
321
			$rootView = new View('/');
322
323
			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
324
				if ($owner !== $user) {
325
					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
326
				}
327
				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
328
			} else if ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
329
330
				foreach ($versions as $v) {
331
					if ($owner !== $user) {
332
						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
333
					}
334
					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
335
				}
336
			}
337
		}
338
	}
339
340
	/**
341
	 * Move a file or folder on storage level
342
	 *
343
	 * @param View $view
344
	 * @param string $source
345
	 * @param string $target
346
	 * @return bool
347
	 */
348
	private static function move(View $view, $source, $target) {
349
		/** @var \OC\Files\Storage\Storage $sourceStorage */
350
		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
351
		/** @var \OC\Files\Storage\Storage $targetStorage */
352
		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
353
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
354
355
		$result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
356
		if ($result) {
357
			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
358
		}
359
		return $result;
360
	}
361
362
	/**
363
	 * Copy a file or folder on storage level
364
	 *
365
	 * @param View $view
366
	 * @param string $source
367
	 * @param string $target
368
	 * @return bool
369
	 */
370
	private static function copy(View $view, $source, $target) {
371
		/** @var \OC\Files\Storage\Storage $sourceStorage */
372
		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
373
		/** @var \OC\Files\Storage\Storage $targetStorage */
374
		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
375
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
376
377
		$result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
378
		if ($result) {
379
			$targetStorage->getUpdater()->update($targetInternalPath);
380
		}
381
		return $result;
382
	}
383
384
	/**
385
	 * Restore a file or folder from trash bin
386
	 *
387
	 * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
388
	 * including the timestamp suffix ".d12345678"
389
	 * @param string $filename name of the file/folder
390
	 * @param int $timestamp time when the file/folder was deleted
391
	 *
392
	 * @return bool true on success, false otherwise
393
	 */
394
	public static function restore($file, $filename, $timestamp) {
395
		$user = User::getUser();
0 ignored issues
show
Deprecated Code introduced by
The function OCP\User::getUser() has been deprecated: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID() ( Ignorable by Annotation )

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

395
		$user = /** @scrutinizer ignore-deprecated */ User::getUser();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
396
		$view = new View('/' . $user);
397
398
		$location = '';
399
		if ($timestamp) {
400
			$location = self::getLocation($user, $filename, $timestamp);
401
			if ($location === false) {
0 ignored issues
show
introduced by
The condition $location === false is always false.
Loading history...
402
				\OC::$server->getLogger()->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
403
			} else {
404
				// if location no longer exists, restore file in the root directory
405
				if ($location !== '/' &&
406
					(!$view->is_dir('files/' . $location) ||
407
						!$view->isCreatable('files/' . $location))
408
				) {
409
					$location = '';
410
				}
411
			}
412
		}
413
414
		// we need a  extension in case a file/dir with the same name already exists
415
		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
416
417
		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
418
		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
419
		if (!$view->file_exists($source)) {
420
			return false;
421
		}
422
		$mtime = $view->filemtime($source);
423
424
		// restore file
425
		if (!$view->isCreatable(dirname($target))) {
426
			throw new NotPermittedException("Can't restore trash item because the target folder is not writable");
427
		}
428
		$restoreResult = $view->rename($source, $target);
429
430
		// handle the restore result
431
		if ($restoreResult) {
432
			$fakeRoot = $view->getRoot();
433
			$view->chroot('/' . $user . '/files');
434
			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
435
			$view->chroot($fakeRoot);
436
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
437
				'trashPath' => Filesystem::normalizePath($file)]);
438
439
			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
440
441
			if ($timestamp) {
442
				$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
443
				$query->execute([$user, $filename, $timestamp]);
444
			}
445
446
			return true;
447
		}
448
449
		return false;
450
	}
451
452
	/**
453
	 * restore versions from trash bin
454
	 *
455
	 * @param View $view file view
456
	 * @param string $file complete path to file
457
	 * @param string $filename name of file once it was deleted
458
	 * @param string $uniqueFilename new file name to restore the file without overwriting existing files
459
	 * @param string $location location if file
460
	 * @param int $timestamp deletion time
461
	 * @return false|null
462
	 */
463
	private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
464
465
		if (\OCP\App::isEnabled('files_versions')) {
0 ignored issues
show
Deprecated Code introduced by
The function OCP\App::isEnabled() has been deprecated: 13.0.0 use \OC::$server->getAppManager()->isEnabledForUser($appId) ( Ignorable by Annotation )

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

465
		if (/** @scrutinizer ignore-deprecated */ \OCP\App::isEnabled('files_versions')) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
466
467
			$user = User::getUser();
0 ignored issues
show
Deprecated Code introduced by
The function OCP\User::getUser() has been deprecated: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID() ( Ignorable by Annotation )

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

467
			$user = /** @scrutinizer ignore-deprecated */ User::getUser();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
468
			$rootView = new View('/');
469
470
			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
471
472
			list($owner, $ownerPath) = self::getUidAndFilename($target);
473
474
			// file has been deleted in between
475
			if (empty($ownerPath)) {
476
				return false;
477
			}
478
479
			if ($timestamp) {
480
				$versionedFile = $filename;
481
			} else {
482
				$versionedFile = $file;
483
			}
484
485
			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
486
				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
487
			} else if ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
488
				foreach ($versions as $v) {
489
					if ($timestamp) {
490
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
491
					} else {
492
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
493
					}
494
				}
495
			}
496
		}
497
	}
498
499
	/**
500
	 * delete all files from the trash
501
	 */
502
	public static function deleteAll() {
503
		$user = User::getUser();
0 ignored issues
show
Deprecated Code introduced by
The function OCP\User::getUser() has been deprecated: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID() ( Ignorable by Annotation )

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

503
		$user = /** @scrutinizer ignore-deprecated */ User::getUser();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
504
		$userRoot = \OC::$server->getUserFolder($user)->getParent();
505
		$view = new View('/' . $user);
506
		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
507
508
		try {
509
			$trash = $userRoot->get('files_trashbin');
510
		} catch (NotFoundException $e) {
511
			return false;
512
		}
513
514
		// Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
515
		$filePaths = [];
516
		foreach($fileInfos as $fileInfo){
517
			$filePaths[] = $view->getRelativePath($fileInfo->getPath());
518
		}
519
		unset($fileInfos); // save memory
520
521
		// Bulk PreDelete-Hook
522
		\OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
523
524
		// Single-File Hooks
525
		foreach($filePaths as $path){
526
			self::emitTrashbinPreDelete($path);
527
		}
528
529
		// actual file deletion
530
		$trash->delete();
531
		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
532
		$query->execute([$user]);
533
534
		// Bulk PostDelete-Hook
535
		\OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
536
537
		// Single-File Hooks
538
		foreach($filePaths as $path){
539
			self::emitTrashbinPostDelete($path);
540
		}
541
542
		$trash = $userRoot->newFolder('files_trashbin');
543
		$trash->newFolder('files');
544
545
		return true;
546
	}
547
548
	/**
549
	 * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
550
	 * @param string $path
551
	 */
552
	protected static function emitTrashbinPreDelete($path){
553
		\OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
554
	}
555
556
	/**
557
	 * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
558
	 * @param string $path
559
	 */
560
	protected static function emitTrashbinPostDelete($path){
561
		\OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
562
	}
563
564
	/**
565
	 * delete file from trash bin permanently
566
	 *
567
	 * @param string $filename path to the file
568
	 * @param string $user
569
	 * @param int $timestamp of deletion time
570
	 *
571
	 * @return int size of deleted files
572
	 */
573
	public static function delete($filename, $user, $timestamp = null) {
574
		$userRoot = \OC::$server->getUserFolder($user)->getParent();
575
		$view = new View('/' . $user);
576
		$size = 0;
577
578
		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...
579
			$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
580
			$query->execute([$user, $filename, $timestamp]);
581
			$file = $filename . '.d' . $timestamp;
582
		} else {
583
			$file = $filename;
584
		}
585
586
		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
587
588
		try {
589
			$node = $userRoot->get('/files_trashbin/files/' . $file);
590
		} catch (NotFoundException $e) {
591
			return $size;
592
		}
593
594
		if ($node instanceof Folder) {
595
			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
596
		} else if ($node instanceof File) {
597
			$size += $view->filesize('/files_trashbin/files/' . $file);
598
		}
599
600
		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
601
		$node->delete();
602
		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
603
604
		return $size;
605
	}
606
607
	/**
608
	 * @param View $view
609
	 * @param string $file
610
	 * @param string $filename
611
	 * @param integer|null $timestamp
612
	 * @param string $user
613
	 * @return int
614
	 */
615
	private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
616
		$size = 0;
617
		if (\OCP\App::isEnabled('files_versions')) {
0 ignored issues
show
Deprecated Code introduced by
The function OCP\App::isEnabled() has been deprecated: 13.0.0 use \OC::$server->getAppManager()->isEnabledForUser($appId) ( Ignorable by Annotation )

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

617
		if (/** @scrutinizer ignore-deprecated */ \OCP\App::isEnabled('files_versions')) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
618
			if ($view->is_dir('files_trashbin/versions/' . $file)) {
619
				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
620
				$view->unlink('files_trashbin/versions/' . $file);
621
			} else if ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
622
				foreach ($versions as $v) {
623
					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...
624
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
625
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
626
					} else {
627
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
628
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
629
					}
630
				}
631
			}
632
		}
633
		return $size;
634
	}
635
636
	/**
637
	 * check to see whether a file exists in trashbin
638
	 *
639
	 * @param string $filename path to the file
640
	 * @param int $timestamp of deletion time
641
	 * @return bool true if file exists, otherwise false
642
	 */
643
	public static function file_exists($filename, $timestamp = null) {
644
		$user = User::getUser();
0 ignored issues
show
Deprecated Code introduced by
The function OCP\User::getUser() has been deprecated: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID() ( Ignorable by Annotation )

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

644
		$user = /** @scrutinizer ignore-deprecated */ User::getUser();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
645
		$view = new View('/' . $user);
646
647
		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...
648
			$filename = $filename . '.d' . $timestamp;
649
		}
650
651
		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
652
		return $view->file_exists($target);
653
	}
654
655
	/**
656
	 * deletes used space for trash bin in db if user was deleted
657
	 *
658
	 * @param string $uid id of deleted user
659
	 * @return bool result of db delete operation
660
	 */
661
	public static function deleteUser($uid) {
662
		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
663
		return $query->execute([$uid]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->execute(array($uid)) also could return the type OC_DB_StatementWrapper|integer which is incompatible with the documented return type boolean.
Loading history...
664
	}
665
666
	/**
667
	 * calculate remaining free space for trash bin
668
	 *
669
	 * @param integer $trashbinSize current size of the trash bin
670
	 * @param string $user
671
	 * @return int available free space for trash bin
672
	 */
673
	private static function calculateFreeSpace($trashbinSize, $user) {
674
		$softQuota = true;
675
		$userObject = \OC::$server->getUserManager()->get($user);
676
		if(is_null($userObject)) {
677
			return 0;
678
		}
679
		$quota = $userObject->getQuota();
680
		if ($quota === null || $quota === 'none') {
681
			$quota = Filesystem::free_space('/');
682
			$softQuota = false;
683
			// inf or unknown free space
684
			if ($quota < 0) {
685
				$quota = PHP_INT_MAX;
686
			}
687
		} else {
688
			$quota = \OCP\Util::computerFileSize($quota);
689
		}
690
691
		// calculate available space for trash bin
692
		// subtract size of files and current trash bin size from quota
693
		if ($softQuota) {
694
			$userFolder = \OC::$server->getUserFolder($user);
695
			if(is_null($userFolder)) {
696
				return 0;
697
			}
698
			$free = $quota - $userFolder->getSize(false); // remaining free space for user
699
			if ($free > 0) {
700
				$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
701
			} else {
702
				$availableSpace = $free - $trashbinSize;
703
			}
704
		} else {
705
			$availableSpace = $quota;
706
		}
707
708
		return $availableSpace;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $availableSpace could also return false which is incompatible with the documented return type integer. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
709
	}
710
711
	/**
712
	 * resize trash bin if necessary after a new file was added to Nextcloud
713
	 *
714
	 * @param string $user user id
715
	 */
716
	public static function resizeTrash($user) {
717
718
		$size = self::getTrashbinSize($user);
719
720
		$freeSpace = self::calculateFreeSpace($size, $user);
721
722
		if ($freeSpace < 0) {
723
			self::scheduleExpire($user);
724
		}
725
	}
726
727
	/**
728
	 * clean up the trash bin
729
	 *
730
	 * @param string $user
731
	 */
732
	public static function expire($user) {
733
		$trashBinSize = self::getTrashbinSize($user);
734
		$availableSpace = self::calculateFreeSpace($trashBinSize, $user);
735
736
		$dirContent = Helper::getTrashFiles('/', $user, 'mtime');
737
738
		// delete all files older then $retention_obligation
739
		list($delSize, $count) = self::deleteExpiredFiles($dirContent, $user);
740
741
		$availableSpace += $delSize;
742
743
		// delete files from trash until we meet the trash bin size limit again
744
		self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
745
	}
746
747
	/**
748
	 * @param string $user
749
	 */
750
	private static function scheduleExpire($user) {
751
		// let the admin disable auto expire
752
		/** @var Application $application */
753
		$application = \OC::$server->query(Application::class);
754
		$expiration = $application->getContainer()->query('Expiration');
755
		if ($expiration->isEnabled()) {
756
			\OC::$server->getCommandBus()->push(new Expire($user));
757
		}
758
	}
759
760
	/**
761
	 * if the size limit for the trash bin is reached, we delete the oldest
762
	 * files in the trash bin until we meet the limit again
763
	 *
764
	 * @param array $files
765
	 * @param string $user
766
	 * @param int $availableSpace available disc space
767
	 * @return int size of deleted files
768
	 */
769
	protected static function deleteFiles($files, $user, $availableSpace) {
770
		/** @var Application $application */
771
		$application = \OC::$server->query(Application::class);
772
		$expiration = $application->getContainer()->query('Expiration');
773
		$size = 0;
774
775
		if ($availableSpace < 0) {
776
			foreach ($files as $file) {
777
				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
778
					$tmp = self::delete($file['name'], $user, $file['mtime']);
779
					\OC::$server->getLogger()->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
780
					$availableSpace += $tmp;
781
					$size += $tmp;
782
				} else {
783
					break;
784
				}
785
			}
786
		}
787
		return $size;
788
	}
789
790
	/**
791
	 * delete files older then max storage time
792
	 *
793
	 * @param array $files list of files sorted by mtime
794
	 * @param string $user
795
	 * @return integer[] size of deleted files and number of deleted files
796
	 */
797
	public static function deleteExpiredFiles($files, $user) {
798
		/** @var Expiration $expiration */
799
		$expiration = \OC::$server->query(Expiration::class);
800
		$size = 0;
801
		$count = 0;
802
		foreach ($files as $file) {
803
			$timestamp = $file['mtime'];
804
			$filename = $file['name'];
805
			if ($expiration->isExpired($timestamp)) {
806
				try {
807
					$size += self::delete($filename, $user, $timestamp);
808
					$count++;
809
				} catch (\OCP\Files\NotPermittedException $e) {
810
					\OC::$server->getLogger()->logException($e, ['app' => 'files_trashbin', 'level' => \OCP\ILogger::WARN, 'message' => 'Removing "' . $filename . '" from trashbin failed.']);
811
				}
812
				\OC::$server->getLogger()->info(
813
					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
814
					['app' => 'files_trashbin']
815
				);
816
			} else {
817
				break;
818
			}
819
		}
820
821
		return [$size, $count];
822
	}
823
824
	/**
825
	 * recursive copy to copy a whole directory
826
	 *
827
	 * @param string $source source path, relative to the users files directory
828
	 * @param string $destination destination path relative to the users root directoy
829
	 * @param View $view file view for the users root directory
830
	 * @return int
831
	 * @throws Exceptions\CopyRecursiveException
832
	 */
833
	private static function copy_recursive($source, $destination, View $view) {
834
		$size = 0;
835
		if ($view->is_dir($source)) {
836
			$view->mkdir($destination);
837
			$view->touch($destination, $view->filemtime($source));
838
			foreach ($view->getDirectoryContent($source) as $i) {
839
				$pathDir = $source . '/' . $i['name'];
840
				if ($view->is_dir($pathDir)) {
841
					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
842
				} else {
843
					$size += $view->filesize($pathDir);
844
					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
845
					if (!$result) {
846
						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
847
					}
848
					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
849
				}
850
			}
851
		} else {
852
			$size += $view->filesize($source);
853
			$result = $view->copy($source, $destination);
854
			if (!$result) {
855
				throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
856
			}
857
			$view->touch($destination, $view->filemtime($source));
858
		}
859
		return $size;
860
	}
861
862
	/**
863
	 * find all versions which belong to the file we want to restore
864
	 *
865
	 * @param string $filename name of the file which should be restored
866
	 * @param int $timestamp timestamp when the file was deleted
867
	 * @return array
868
	 */
869
	private static function getVersionsFromTrash($filename, $timestamp, $user) {
870
		$view = new View('/' . $user . '/files_trashbin/versions');
871
		$versions = [];
872
873
		//force rescan of versions, local storage may not have updated the cache
874
		if (!self::$scannedVersions) {
875
			/** @var \OC\Files\Storage\Storage $storage */
876
			list($storage,) = $view->resolvePath('/');
877
			$storage->getScanner()->scan('files_trashbin/versions');
878
			self::$scannedVersions = true;
879
		}
880
881
		if ($timestamp) {
882
			// fetch for old versions
883
			$matches = $view->searchRaw($filename . '.v%.d' . $timestamp);
884
			$offset = -strlen($timestamp) - 2;
885
		} else {
886
			$matches = $view->searchRaw($filename . '.v%');
887
		}
888
889
		if (is_array($matches)) {
0 ignored issues
show
introduced by
The condition is_array($matches) is always true.
Loading history...
890
			foreach ($matches as $ma) {
891
				if ($timestamp) {
892
					$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...
893
					$versions[] = end($parts);
894
				} else {
895
					$parts = explode('.v', $ma);
0 ignored issues
show
Bug introduced by
$ma of type OC\Files\FileInfo is incompatible with the type string expected by parameter $string of explode(). ( Ignorable by Annotation )

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

895
					$parts = explode('.v', /** @scrutinizer ignore-type */ $ma);
Loading history...
896
					$versions[] = end($parts);
897
				}
898
			}
899
		}
900
		return $versions;
901
	}
902
903
	/**
904
	 * find unique extension for restored file if a file with the same name already exists
905
	 *
906
	 * @param string $location where the file should be restored
907
	 * @param string $filename name of the file
908
	 * @param View $view filesystem view relative to users root directory
909
	 * @return string with unique extension
910
	 */
911
	private static function getUniqueFilename($location, $filename, View $view) {
912
		$ext = pathinfo($filename, PATHINFO_EXTENSION);
913
		$name = pathinfo($filename, PATHINFO_FILENAME);
914
		$l = \OC::$server->getL10N('files_trashbin');
915
916
		$location = '/' . trim($location, '/');
917
918
		// if extension is not empty we set a dot in front of it
919
		if ($ext !== '') {
920
			$ext = '.' . $ext;
921
		}
922
923
		if ($view->file_exists('files' . $location . '/' . $filename)) {
924
			$i = 2;
925
			$uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
926
			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
927
				$uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
928
				$i++;
929
			}
930
931
			return $uniqueName;
932
		}
933
934
		return $filename;
935
	}
936
937
	/**
938
	 * get the size from a given root folder
939
	 *
940
	 * @param View $view file view on the root folder
941
	 * @return integer size of the folder
942
	 */
943
	private static function calculateSize($view) {
944
		$root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
945
		if (!file_exists($root)) {
946
			return 0;
947
		}
948
		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
949
		$size = 0;
950
951
		/**
952
		 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
953
		 * This bug is fixed in PHP 5.5.9 or before
954
		 * See #8376
955
		 */
956
		$iterator->rewind();
957
		while ($iterator->valid()) {
958
			$path = $iterator->current();
959
			$relpath = substr($path, strlen($root) - 1);
960
			if (!$view->is_dir($relpath)) {
961
				$size += $view->filesize($relpath);
962
			}
963
			$iterator->next();
964
		}
965
		return $size;
966
	}
967
968
	/**
969
	 * get current size of trash bin from a given user
970
	 *
971
	 * @param string $user user who owns the trash bin
972
	 * @return integer trash bin size
973
	 */
974
	private static function getTrashbinSize($user) {
975
		$view = new View('/' . $user);
976
		$fileInfo = $view->getFileInfo('/files_trashbin');
977
		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
978
	}
979
980
	/**
981
	 * register hooks
982
	 */
983
	public static function registerHooks() {
984
		// create storage wrapper on setup
985
		\OCP\Util::connectHook('OC_Filesystem', 'preSetup', 'OCA\Files_Trashbin\Storage', 'setupStorage');
986
		//Listen to delete user signal
987
		\OCP\Util::connectHook('OC_User', 'pre_deleteUser', 'OCA\Files_Trashbin\Hooks', 'deleteUser_hook');
988
		//Listen to post write hook
989
		\OCP\Util::connectHook('OC_Filesystem', 'post_write', 'OCA\Files_Trashbin\Hooks', 'post_write_hook');
990
		// pre and post-rename, disable trash logic for the copy+unlink case
991
		\OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Trashbin\Trashbin', 'ensureFileScannedHook');
992
	}
993
994
	/**
995
	 * check if trash bin is empty for a given user
996
	 *
997
	 * @param string $user
998
	 * @return bool
999
	 */
1000
	public static function isEmpty($user) {
1001
1002
		$view = new View('/' . $user . '/files_trashbin');
1003
		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1004
			while ($file = readdir($dh)) {
1005
				if (!Filesystem::isIgnoredDir($file)) {
1006
					return false;
1007
				}
1008
			}
1009
		}
1010
		return true;
1011
	}
1012
1013
	/**
1014
	 * @param $path
1015
	 * @return string
1016
	 */
1017
	public static function preview_icon($path) {
1018
		return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]);
1019
	}
1020
}
1021