Completed
Push — master ( 28c345...2d40e0 )
by Thomas
15:42
created

Trashbin   F

Complexity

Total Complexity 138

Size/Duplication

Total Lines 970
Duplicated Lines 1.24 %

Coupling/Cohesion

Components 1
Dependencies 27

Importance

Changes 0
Metric Value
dl 12
loc 970
rs 1.0434
c 0
b 0
f 0
wmc 138
lcom 1
cbo 27

35 Methods

Rating   Name   Duplication   Size   Complexity  
A ensureFileScannedHook() 0 7 2
B getUidAndFilename() 0 25 5
A getLocations() 0 14 3
A getLocation() 0 10 2
B setUpTrash() 0 15 5
A copyFilesToUser() 0 14 1
A insertTrashEntry() 0 7 2
A copyBackupForOwner() 0 18 2
F move2trash() 0 87 15
C retainVersions() 12 26 12
A move() 0 13 2
A copy() 0 13 2
B restore() 0 54 9
C restoreVersions() 0 35 8
B deleteAll() 0 38 4
A emitTrashbinPreDelete() 0 3 1
A emitTrashbinPostDelete() 0 3 1
B delete() 0 25 3
B deleteVersions() 0 20 6
A file_exists() 0 13 2
A deleteUser() 0 4 1
C calculateFreeSpace() 0 37 8
A resizeTrash() 0 10 2
A expire() 0 14 1
A scheduleExpire() 0 8 2
B deleteFiles() 0 19 5
A deleteExpiredFiles() 0 22 3
B copy_recursive() 0 28 6
B getVersionsFromTrash() 0 33 6
B getUniqueFilename() 0 25 4
B calculateSize() 0 24 4
A getTrashbinSize() 0 5 2
A registerHooks() 0 12 1
B isEmpty() 0 12 5
A preview_icon() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

1
<?php
2
/**
3
 * @author Bart Visscher <[email protected]>
4
 * @author Bastien Ho <[email protected]>
5
 * @author Björn Schießle <[email protected]>
6
 * @author Florin Peter <[email protected]>
7
 * @author Georg Ehrke <[email protected]>
8
 * @author Jörn Friedrich Dreyer <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Qingping Hou <[email protected]>
12
 * @author Robin Appelman <[email protected]>
13
 * @author Robin McCorkell <[email protected]>
14
 * @author Roeland Jago Douma <[email protected]>
15
 * @author Sjors van der Pluijm <[email protected]>
16
 * @author Steven Bühner <[email protected]>
17
 * @author Thomas Müller <[email protected]>
18
 * @author Victor Dubiniuk <[email protected]>
19
 * @author Vincent Petry <[email protected]>
20
 *
21
 * @copyright Copyright (c) 2017, ownCloud GmbH
22
 * @license AGPL-3.0
23
 *
24
 * This code is free software: you can redistribute it and/or modify
25
 * it under the terms of the GNU Affero General Public License, version 3,
26
 * as published by the Free Software Foundation.
27
 *
28
 * This program is distributed in the hope that it will be useful,
29
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31
 * GNU Affero General Public License for more details.
32
 *
33
 * You should have received a copy of the GNU Affero General Public License, version 3,
34
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
35
 *
36
 */
37
38
namespace OCA\Files_Trashbin;
39
40
use OC\Files\Filesystem;
41
use OC\Files\View;
42
use OCA\Files_Trashbin\AppInfo\Application;
43
use OCA\Files_Trashbin\Command\Expire;
44
use OCP\Files\NotFoundException;
45
use OCP\User;
46
47
class Trashbin {
48
49
	// unit: percentage; 50% of available disk space/quota
50
	const DEFAULTMAXSIZE = 50;
51
52
	/**
53
	 * Whether versions have already be rescanned during this PHP request
54
	 *
55
	 * @var bool
56
	 */
57
	private static $scannedVersions = false;
58
59
	/**
60
	 * Ensure we don't need to scan the file during the move to trash
61
	 * by triggering the scan in the pre-hook
62
	 *
63
	 * @param array $params
64
	 */
65
	public static function ensureFileScannedHook($params) {
66
		try {
67
			self::getUidAndFilename($params['path']);
68
		} catch (NotFoundException $e) {
69
			// nothing to scan for non existing files
70
		}
71
	}
72
73
	/**
74
	 * get the UID of the owner of the file and the path to the file relative to
75
	 * owners files folder
76
	 *
77
	 * @param string $filename
78
	 * @return array
79
	 * @throws \OC\User\NoUserException
80
	 */
81
	public static function getUidAndFilename($filename) {
82
		$uid = Filesystem::getOwner($filename);
83
		$userManager = \OC::$server->getUserManager();
84
		// if the user with the UID doesn't exists, e.g. because the UID points
85
		// to a remote user with a federated cloud ID we use the current logged-in
86
		// user. We need a valid local user to move the file to the right trash bin
87
		if (!$userManager->userExists($uid)) {
88
			$uid = User::getUser();
89
		}
90
		if (!$uid) {
91
			// no owner, usually because of share link from ext storage
92
			return [null, null];
93
		}
94
		Filesystem::initMountPoints($uid);
95
		if ($uid != User::getUser()) {
96
			$info = Filesystem::getFileInfo($filename);
97
			$ownerView = new View('/' . $uid . '/files');
98
			try {
99
				$filename = $ownerView->getPath($info['fileid']);
100
			} catch (NotFoundException $e) {
101
				$filename = null;
102
			}
103
		}
104
		return [$uid, $filename];
105
	}
106
107
	/**
108
	 * get original location of files for user
109
	 *
110
	 * @param string $user
111
	 * @return array (filename => array (timestamp => original location))
112
	 */
113
	public static function getLocations($user) {
114
		$query = \OC_DB::prepare('SELECT `id`, `timestamp`, `location`'
115
			. ' FROM `*PREFIX*files_trash` WHERE `user`=?');
116
		$result = $query->execute([$user]);
117
		$array = [];
118
		while ($row = $result->fetchRow()) {
119
			if (isset($array[$row['id']])) {
120
				$array[$row['id']][$row['timestamp']] = $row['location'];
121
			} else {
122
				$array[$row['id']] = [$row['timestamp'] => $row['location']];
123
			}
124
		}
125
		return $array;
126
	}
127
128
	/**
129
	 * get original location of file
130
	 *
131
	 * @param string $user
132
	 * @param string $filename
133
	 * @param string $timestamp
134
	 * @return string original location
135
	 */
136
	public static function getLocation($user, $filename, $timestamp) {
137
		$query = \OC_DB::prepare('SELECT `location` FROM `*PREFIX*files_trash`'
138
			. ' WHERE `user`=? AND `id`=? AND `timestamp`=?');
139
		$result = $query->execute([$user, $filename, $timestamp])->fetchAll();
140
		if (isset($result[0]['location'])) {
141
			return $result[0]['location'];
142
		} else {
143
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by OCA\Files_Trashbin\Trashbin::getLocation of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
144
		}
145
	}
146
147
	private static function setUpTrash($user) {
148
		$view = new View('/' . $user);
149
		if (!$view->is_dir('files_trashbin')) {
150
			$view->mkdir('files_trashbin');
151
		}
152
		if (!$view->is_dir('files_trashbin/files')) {
153
			$view->mkdir('files_trashbin/files');
154
		}
155
		if (!$view->is_dir('files_trashbin/versions')) {
156
			$view->mkdir('files_trashbin/versions');
157
		}
158
		if (!$view->is_dir('files_trashbin/keys')) {
159
			$view->mkdir('files_trashbin/keys');
160
		}
161
	}
162
163
164
	/**
165
	 * copy file to owners trash
166
	 *
167
	 * @param string $sourcePath
168
	 * @param string $owner
169
	 * @param string $targetPath
170
	 * @param $user
171
	 * @param integer $timestamp
172
	 */
173
	private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
174
		self::setUpTrash($owner);
175
176
		$targetFilename = basename($targetPath);
177
		$targetLocation = dirname($targetPath);
0 ignored issues
show
Unused Code introduced by
$targetLocation is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
178
179
		$sourceFilename = basename($sourcePath);
180
181
		$view = new View('/');
182
183
		$target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
184
		$source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
185
		self::copy_recursive($source, $target, $view);
186
	}
187
188
	/**
189
	 * Make a backup of a file into the trashbin for the owner
190
	 *
191
	 * @param string $ownerPath path relative to the owner's home folder and containing "files"
192
	 * @param string $owner user id of the owner
193
	 * @param int $timestamp deletion timestamp
194
	 */
195
	public static function copyBackupForOwner($ownerPath, $owner, $timestamp) {
196
		self::setUpTrash($owner);
197
198
		$targetFilename = basename($ownerPath);
199
		$targetLocation = dirname($ownerPath);
200
		$source = $owner . '/files/' . ltrim($ownerPath, '/');
201
		$target = $owner . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
202
203
		$view = new View('/');
204
		self::copy_recursive($source, $target, $view);
205
206
		self::retainVersions($targetFilename, $owner, $ownerPath, $timestamp, true);
207
208
		if ($view->file_exists($target)) {
209
			self::insertTrashEntry($owner, $targetFilename, $targetLocation, $timestamp);
210
			self::scheduleExpire($owner);
211
		}
212
	}
213
214
	/**
215
	 *
216
	 */
217
	public static function insertTrashEntry($user, $targetFilename, $targetLocation, $timestamp) {
218
		$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
219
		$result = $query->execute([$targetFilename, $timestamp, $targetLocation, $user]);
220
		if (!$result) {
221
			\OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated for the files owner', \OCP\Util::ERROR);
222
		}
223
	}
224
225
226
	/**
227
	 * move file to the trash bin
228
	 *
229
	 * @param string $file_path path to the deleted file/directory relative to the files root directory
230
	 * @return bool
231
	 */
232
	public static function move2trash($file_path) {
233
		// get the user for which the filesystem is setup
234
		$root = Filesystem::getRoot();
235
		list(, $user) = explode('/', $root);
236
		list($owner, $ownerPath) = self::getUidAndFilename($file_path);
237
238
		// if no owner found (ex: ext storage + share link), will use the current user's trashbin then
239
		if (is_null($owner)) {
240
			$owner = $user;
241
			$ownerPath = $file_path;
242
		}
243
244
		$ownerView = new View('/' . $owner);
245
		// file has been deleted in between
246
		if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
247
			return true;
248
		}
249
250
		self::setUpTrash($user);
251
		if ($owner !== $user) {
252
			// also setup for owner
253
			self::setUpTrash($owner);
254
		}
255
256
		$path_parts = pathinfo($ownerPath);
257
258
		$filename = $path_parts['basename'];
259
		$location = $path_parts['dirname'];
260
		$timestamp = time();
261
262
		$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
263
264
		/** @var \OC\Files\Storage\Storage $trashStorage */
265
		list($trashStorage, $trashInternalPath) = $ownerView->resolvePath($trashPath);
266
		/** @var \OC\Files\Storage\Storage $sourceStorage */
267
		list($sourceStorage, $sourceInternalPath) = $ownerView->resolvePath('/files/' . $ownerPath);
268
		try {
269
			$moveSuccessful = true;
270
			if ($trashStorage->file_exists($trashInternalPath)) {
271
				$trashStorage->unlink($trashInternalPath);
272
			}
273
			$trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
274
		} catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
275
			$moveSuccessful = false;
276
			if ($trashStorage->file_exists($trashInternalPath)) {
277
				$trashStorage->unlink($trashInternalPath);
278
			}
279
			\OCP\Util::writeLog('files_trashbin', 'Couldn\'t move ' . $file_path . ' to the trash bin', \OCP\Util::ERROR);
280
		}
281
282
		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
283
			if ($sourceStorage->is_dir($sourceInternalPath)) {
284
				$sourceStorage->rmdir($sourceInternalPath);
285
			} else {
286
				$sourceStorage->unlink($sourceInternalPath);
287
			}
288
			return false;
289
		}
290
291
		$trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
292
293
		if ($moveSuccessful) {
294
			$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
295
			$result = $query->execute([$filename, $timestamp, $location, $owner]);
296
			if (!$result) {
297
				\OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated', \OCP\Util::ERROR);
298
			}
299
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
300
				'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)]);
301
302
			self::retainVersions($filename, $owner, $ownerPath, $timestamp);
303
304
			// if owner !== user we need to also add a copy to the owners trash
305
			if ($user !== $owner) {
306
				self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
307
			}
308
		}
309
310
		self::scheduleExpire($user);
311
312
		// if owner !== user we also need to update the owners trash size
313
		if ($owner !== $user) {
314
			self::scheduleExpire($owner);
315
		}
316
317
		return $moveSuccessful;
318
	}
319
320
	/**
321
	 * Move file versions to trash so that they can be restored later
322
	 *
323
	 * @param string $filename of deleted file
324
	 * @param string $owner owner user id
325
	 * @param string $ownerPath path relative to the owner's home storage
326
	 * @param integer $timestamp when the file was deleted
327
	 * @param bool $forceCopy true to only make a copy of the versions into the trashbin
328
	 */
329
	private static function retainVersions($filename, $owner, $ownerPath, $timestamp, $forceCopy = false) {
330
		if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {
331
332
			$user = User::getUser();
333
			$rootView = new View('/');
334
335
			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
336 View Code Duplication
				if ($owner !== $user || $forceCopy) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
337
					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
338
				}
339 View Code Duplication
				if (!$forceCopy) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
340
					self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
341
				}
342
			} else if ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
343
344
				foreach ($versions as $v) {
345 View Code Duplication
					if ($owner !== $user || $forceCopy) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
346
						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
347
					}
348 View Code Duplication
					if (!$forceCopy) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
349
						self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
350
					}
351
				}
352
			}
353
		}
354
	}
355
356
	/**
357
	 * Move a file or folder on storage level
358
	 *
359
	 * @param View $view
360
	 * @param string $source
361
	 * @param string $target
362
	 * @return bool
363
	 */
364
	private static function move(View $view, $source, $target) {
365
		/** @var \OC\Files\Storage\Storage $sourceStorage */
366
		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
367
		/** @var \OC\Files\Storage\Storage $targetStorage */
368
		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
369
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
370
371
		$result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
372
		if ($result) {
373
			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
374
		}
375
		return $result;
376
	}
377
378
	/**
379
	 * Copy a file or folder on storage level
380
	 *
381
	 * @param View $view
382
	 * @param string $source
383
	 * @param string $target
384
	 * @return bool
385
	 */
386
	private static function copy(View $view, $source, $target) {
387
		/** @var \OC\Files\Storage\Storage $sourceStorage */
388
		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
389
		/** @var \OC\Files\Storage\Storage $targetStorage */
390
		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
391
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
392
393
		$result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
394
		if ($result) {
395
			$targetStorage->getUpdater()->update($targetInternalPath);
396
		}
397
		return $result;
398
	}
399
400
	/**
401
	 * Restore a file or folder from trash bin
402
	 *
403
	 * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
404
	 * including the timestamp suffix ".d12345678"
405
	 * @param string $filename name of the file/folder
406
	 * @param int $timestamp time when the file/folder was deleted
407
	 *
408
	 * @return bool true on success, false otherwise
409
	 */
410
	public static function restore($file, $filename, $timestamp) {
411
		$user = User::getUser();
412
		$view = new View('/' . $user);
413
414
		$location = '';
415
		if ($timestamp) {
416
			$location = self::getLocation($user, $filename, $timestamp);
417
			if ($location === false) {
418
				\OCP\Util::writeLog('files_trashbin', 'trash bin database inconsistent!', \OCP\Util::ERROR);
419
			} else {
420
				// if location no longer exists, restore file in the root directory
421
				if ($location !== '/' &&
422
					(!$view->is_dir('files/' . $location) ||
423
						!$view->isCreatable('files/' . $location))
424
				) {
425
					$location = '';
426
				}
427
			}
428
		}
429
430
		// we need a  extension in case a file/dir with the same name already exists
431
		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
432
433
		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
434
		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
435
		if (!$view->file_exists($source)) {
436
			return false;
437
		}
438
		$mtime = $view->filemtime($source);
439
440
		// restore file
441
		$restoreResult = $view->rename($source, $target);
442
443
		// handle the restore result
444
		if ($restoreResult) {
445
			$fakeRoot = $view->getRoot();
446
			$view->chroot('/' . $user . '/files');
447
			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
448
			$view->chroot($fakeRoot);
449
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
450
				'trashPath' => Filesystem::normalizePath($file)]);
451
452
			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
453
454
			if ($timestamp) {
455
				$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
456
				$query->execute([$user, $filename, $timestamp]);
457
			}
458
459
			return true;
460
		}
461
462
		return false;
463
	}
464
465
	/**
466
	 * restore versions from trash bin
467
	 *
468
	 * @param View $view file view
469
	 * @param string $file complete path to file
470
	 * @param string $filename name of file once it was deleted
471
	 * @param string $uniqueFilename new file name to restore the file without overwriting existing files
472
	 * @param string $location location if file
473
	 * @param int $timestamp deletion time
474
	 * @return false|null
475
	 */
476
	private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
477
478
		if (\OCP\App::isEnabled('files_versions')) {
479
480
			$user = User::getUser();
481
			$rootView = new View('/');
482
483
			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
484
485
			list($owner, $ownerPath) = self::getUidAndFilename($target);
486
487
			// file has been deleted in between
488
			if (empty($ownerPath)) {
489
				return false;
490
			}
491
492
			if ($timestamp) {
493
				$versionedFile = $filename;
494
			} else {
495
				$versionedFile = $file;
496
			}
497
498
			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
499
				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
500
			} else if ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
501
				foreach ($versions as $v) {
502
					if ($timestamp) {
503
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
504
					} else {
505
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
506
					}
507
				}
508
			}
509
		}
510
	}
511
512
	/**
513
	 * delete all files from the trash
514
	 */
515
	public static function deleteAll() {
516
		$user = User::getUser();
517
		$view = new View('/' . $user);
518
		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
519
520
		// Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
521
		$filePaths = [];
522
		foreach($fileInfos as $fileInfo){
523
			$filePaths[] = $view->getRelativePath($fileInfo->getPath());
524
		}
525
		unset($fileInfos); // save memory
526
527
		// Bulk PreDelete-Hook
528
		\OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
529
530
		// Single-File Hooks
531
		foreach($filePaths as $path){
532
			self::emitTrashbinPreDelete($path);
533
		}
534
535
		// actual file deletion
536
		$view->deleteAll('files_trashbin');
537
		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
538
		$query->execute([$user]);
539
540
		// Bulk PostDelete-Hook
541
		\OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
542
543
		// Single-File Hooks
544
		foreach($filePaths as $path){
545
			self::emitTrashbinPostDelete($path);
546
		}
547
548
		$view->mkdir('files_trashbin');
549
		$view->mkdir('files_trashbin/files');
550
551
		return true;
552
	}
553
554
	/**
555
	 * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
556
	 * @param string $path
557
	 */
558
	protected static function emitTrashbinPreDelete($path){
559
		\OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
560
	}
561
562
	/**
563
	 * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
564
	 * @param string $path
565
	 */
566
	protected static function emitTrashbinPostDelete($path){
567
		\OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
568
	}
569
570
	/**
571
	 * delete file from trash bin permanently
572
	 *
573
	 * @param string $filename path to the file
574
	 * @param string $user
575
	 * @param int $timestamp of deletion time
576
	 *
577
	 * @return int size of deleted files
578
	 */
579
	public static function delete($filename, $user, $timestamp = null) {
580
		$view = new View('/' . $user);
581
		$size = 0;
582
583
		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 zero. 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...
584
			$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
585
			$query->execute([$user, $filename, $timestamp]);
586
			$file = $filename . '.d' . $timestamp;
587
		} else {
588
			$file = $filename;
589
		}
590
591
		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
592
593
		if ($view->is_dir('/files_trashbin/files/' . $file)) {
594
			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
595
		} else {
596
			$size += $view->filesize('/files_trashbin/files/' . $file);
597
		}
598
		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
599
		$view->unlink('/files_trashbin/files/' . $file);
600
		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
601
602
		return $size;
603
	}
604
605
	/**
606
	 * @param View $view
607
	 * @param string $file
608
	 * @param string $filename
609
	 * @param integer|null $timestamp
610
	 * @param string $user
611
	 * @return int
612
	 */
613
	private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
614
		$size = 0;
615
		if (\OCP\App::isEnabled('files_versions')) {
616
			if ($view->is_dir('files_trashbin/versions/' . $file)) {
617
				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
618
				$view->unlink('files_trashbin/versions/' . $file);
619
			} else if ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
620
				foreach ($versions as $v) {
621
					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 zero. 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...
622
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
623
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
624
					} else {
625
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
626
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
627
					}
628
				}
629
			}
630
		}
631
		return $size;
632
	}
633
634
	/**
635
	 * check to see whether a file exists in trashbin
636
	 *
637
	 * @param string $filename path to the file
638
	 * @param int $timestamp of deletion time
639
	 * @return bool true if file exists, otherwise false
640
	 */
641
	public static function file_exists($filename, $timestamp = null) {
642
		$user = User::getUser();
643
		$view = new View('/' . $user);
644
645
		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 zero. 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...
646
			$filename = $filename . '.d' . $timestamp;
647
		} else {
648
			$filename = $filename;
0 ignored issues
show
Bug introduced by
Why assign $filename to itself?

This checks looks for cases where a variable has been assigned to itself.

This assignement can be removed without consequences.

Loading history...
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]);
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(); // 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;
709
	}
710
711
	/**
712
	 * resize trash bin if necessary after a new file was added to ownCloud
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
		$application = new Application();
753
		$expiration = $application->getContainer()->query('Expiration');
754
		if ($expiration->isEnabled()) {
755
			\OC::$server->getCommandBus()->push(new Expire($user));
756
		}
757
	}
758
759
	/**
760
	 * if the size limit for the trash bin is reached, we delete the oldest
761
	 * files in the trash bin until we meet the limit again
762
	 *
763
	 * @param array $files
764
	 * @param string $user
765
	 * @param int $availableSpace available disc space
766
	 * @return int size of deleted files
767
	 */
768
	protected static function deleteFiles($files, $user, $availableSpace) {
769
		$application = new Application();
770
		$expiration = $application->getContainer()->query('Expiration');
771
		$size = 0;
772
773
		if ($availableSpace < 0) {
774
			foreach ($files as $file) {
775
				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
776
					$tmp = self::delete($file['name'], $user, $file['mtime']);
777
					\OCP\Util::writeLog('files_trashbin', 'remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', \OCP\Util::INFO);
778
					$availableSpace += $tmp;
779
					$size += $tmp;
780
				} else {
781
					break;
782
				}
783
			}
784
		}
785
		return $size;
786
	}
787
788
	/**
789
	 * delete files older then max storage time
790
	 *
791
	 * @param array $files list of files sorted by mtime
792
	 * @param string $user
793
	 * @return integer[] size of deleted files and number of deleted files
794
	 */
795
	public static function deleteExpiredFiles($files, $user) {
796
		$application = new Application();
797
		$expiration = $application->getContainer()->query('Expiration');
798
		$size = 0;
799
		$count = 0;
800
		foreach ($files as $file) {
801
			$timestamp = $file['mtime'];
802
			$filename = $file['name'];
803
			if ($expiration->isExpired($timestamp)) {
804
				$count++;
805
				$size += self::delete($filename, $user, $timestamp);
806
				\OC::$server->getLogger()->info(
807
					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
808
					['app' => 'files_trashbin']
809
				);
810
			} else {
811
				break;
812
			}
813
		}
814
815
		return [$size, $count];
816
	}
817
818
	/**
819
	 * recursive copy to copy a whole directory
820
	 *
821
	 * @param string $source source path, relative to the users files directory
822
	 * @param string $destination destination path relative to the users root directoy
823
	 * @param View $view file view for the users root directory
824
	 * @return int
825
	 * @throws Exceptions\CopyRecursiveException
826
	 */
827
	private static function copy_recursive($source, $destination, View $view) {
828
		$size = 0;
829
		if ($view->is_dir($source)) {
830
			$view->mkdir($destination);
831
			$view->touch($destination, $view->filemtime($source));
832
			foreach ($view->getDirectoryContent($source) as $i) {
833
				$pathDir = $source . '/' . $i['name'];
834
				if ($view->is_dir($pathDir)) {
835
					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
836
				} else {
837
					$size += $view->filesize($pathDir);
838
					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
839
					if (!$result) {
840
						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
841
					}
842
					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
843
				}
844
			}
845
		} else {
846
			$size += $view->filesize($source);
847
			$result = $view->copy($source, $destination);
848
			if (!$result) {
849
				throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
850
			}
851
			$view->touch($destination, $view->filemtime($source));
852
		}
853
		return $size;
854
	}
855
856
	/**
857
	 * find all versions which belong to the file we want to restore
858
	 *
859
	 * @param string $filename name of the file which should be restored
860
	 * @param int $timestamp timestamp when the file was deleted
861
	 * @return array
862
	 */
863
	private static function getVersionsFromTrash($filename, $timestamp, $user) {
864
		$view = new View('/' . $user . '/files_trashbin/versions');
865
		$versions = [];
866
867
		//force rescan of versions, local storage may not have updated the cache
868
		if (!self::$scannedVersions) {
869
			/** @var \OC\Files\Storage\Storage $storage */
870
			list($storage,) = $view->resolvePath('/');
871
			$storage->getScanner()->scan('files_trashbin/versions');
872
			self::$scannedVersions = true;
873
		}
874
875
		if ($timestamp) {
876
			// fetch for old versions
877
			$matches = $view->searchRaw($filename . '.v%.d' . $timestamp);
878
			$offset = -strlen($timestamp) - 2;
879
		} else {
880
			$matches = $view->searchRaw($filename . '.v%');
881
		}
882
883
		if (is_array($matches)) {
884
			foreach ($matches as $ma) {
885
				if ($timestamp) {
886
					$parts = explode('.v', substr($ma['path'], 0, $offset));
0 ignored issues
show
Bug introduced by
The variable $offset does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

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