Completed
Push — stable9 ( 485cb1...e094cf )
by Lukas
26:41 queued 26:23
created

Trashbin   F

Complexity

Total Complexity 124

Size/Duplication

Total Lines 881
Duplicated Lines 2.38 %

Coupling/Cohesion

Components 1
Dependencies 26

Importance

Changes 0
Metric Value
wmc 124
c 0
b 0
f 0
lcom 1
cbo 26
dl 21
loc 881
rs 1.0434

31 Methods

Rating   Name   Duplication   Size   Complexity  
A ensureFileScannedHook() 0 7 2
A getUidAndFilename() 21 21 4
A getLocations() 0 14 3
A getLocation() 0 10 2
B setUpTrash() 0 15 5
A copyFilesToUser() 0 23 3
D move2trash() 0 78 13
C retainVersions() 0 22 8
A move() 0 13 2
A copy() 0 13 2
B restore() 0 54 9
C restoreVersions() 0 35 8
A deleteAll() 0 11 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
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bart Visscher <[email protected]>
6
 * @author Bastien Ho <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Florin Peter <[email protected]>
9
 * @author Georg Ehrke <[email protected]>
10
 * @author Jörn Friedrich Dreyer <[email protected]>
11
 * @author Lukas Reschke <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Qingping Hou <[email protected]>
14
 * @author Robin Appelman <[email protected]>
15
 * @author Robin McCorkell <[email protected]>
16
 * @author Roeland Jago Douma <[email protected]>
17
 * @author Sjors van der Pluijm <[email protected]>
18
 * @author Thomas Müller <[email protected]>
19
 * @author Victor Dubiniuk <[email protected]>
20
 * @author Vincent Petry <[email protected]>
21
 *
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 dont 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 View Code Duplication
	public static function getUidAndFilename($filename) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
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();
0 ignored issues
show
Deprecated Code introduced by
The method OCP\User::getUser() has been deprecated with message: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID()

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

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

Loading history...
89
		}
90
		Filesystem::initMountPoints($uid);
91
		if ($uid != User::getUser()) {
0 ignored issues
show
Deprecated Code introduced by
The method OCP\User::getUser() has been deprecated with message: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID()

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

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

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

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

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

Loading history...
291
			$rootView = new View('/');
292
293
			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
294
				if ($owner !== $user) {
295
					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
296
				}
297
				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
298
			} else if ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
299
300
				foreach ($versions as $v) {
301
					if ($owner !== $user) {
302
						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
303
					}
304
					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
305
				}
306
			}
307
		}
308
	}
309
310
	/**
311
	 * Move a file or folder on storage level
312
	 *
313
	 * @param View $view
314
	 * @param string $source
315
	 * @param string $target
316
	 * @return bool
317
	 */
318
	private static function move(View $view, $source, $target) {
319
		/** @var \OC\Files\Storage\Storage $sourceStorage */
320
		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
321
		/** @var \OC\Files\Storage\Storage $targetStorage */
322
		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
323
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
324
325
		$result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
326
		if ($result) {
327
			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
328
		}
329
		return $result;
330
	}
331
332
	/**
333
	 * Copy a file or folder on storage level
334
	 *
335
	 * @param View $view
336
	 * @param string $source
337
	 * @param string $target
338
	 * @return bool
339
	 */
340
	private static function copy(View $view, $source, $target) {
341
		/** @var \OC\Files\Storage\Storage $sourceStorage */
342
		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
343
		/** @var \OC\Files\Storage\Storage $targetStorage */
344
		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
345
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
346
347
		$result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
348
		if ($result) {
349
			$targetStorage->getUpdater()->update($targetInternalPath);
350
		}
351
		return $result;
352
	}
353
354
	/**
355
	 * Restore a file or folder from trash bin
356
	 *
357
	 * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
358
	 * including the timestamp suffix ".d12345678"
359
	 * @param string $filename name of the file/folder
360
	 * @param int $timestamp time when the file/folder was deleted
361
	 *
362
	 * @return bool true on success, false otherwise
363
	 */
364
	public static function restore($file, $filename, $timestamp) {
365
		$user = User::getUser();
0 ignored issues
show
Deprecated Code introduced by
The method OCP\User::getUser() has been deprecated with message: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID()

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

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

Loading history...
366
		$view = new View('/' . $user);
367
368
		$location = '';
369
		if ($timestamp) {
370
			$location = self::getLocation($user, $filename, $timestamp);
371
			if ($location === false) {
372
				\OCP\Util::writeLog('files_trashbin', 'trash bin database inconsistent!', \OCP\Util::ERROR);
373
			} else {
374
				// if location no longer exists, restore file in the root directory
375
				if ($location !== '/' &&
376
					(!$view->is_dir('files/' . $location) ||
377
						!$view->isCreatable('files/' . $location))
378
				) {
379
					$location = '';
380
				}
381
			}
382
		}
383
384
		// we need a  extension in case a file/dir with the same name already exists
385
		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
386
387
		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
388
		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
389
		if (!$view->file_exists($source)) {
390
			return false;
391
		}
392
		$mtime = $view->filemtime($source);
393
394
		// restore file
395
		$restoreResult = $view->rename($source, $target);
396
397
		// handle the restore result
398
		if ($restoreResult) {
399
			$fakeRoot = $view->getRoot();
400
			$view->chroot('/' . $user . '/files');
401
			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
402
			$view->chroot($fakeRoot);
403
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', array('filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
404
				'trashPath' => Filesystem::normalizePath($file)));
405
406
			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
407
408
			if ($timestamp) {
409
				$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
410
				$query->execute(array($user, $filename, $timestamp));
411
			}
412
413
			return true;
414
		}
415
416
		return false;
417
	}
418
419
	/**
420
	 * restore versions from trash bin
421
	 *
422
	 * @param View $view file view
423
	 * @param string $file complete path to file
424
	 * @param string $filename name of file once it was deleted
425
	 * @param string $uniqueFilename new file name to restore the file without overwriting existing files
426
	 * @param string $location location if file
427
	 * @param int $timestamp deletion time
428
	 * @return false|null
429
	 */
430
	private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
431
432
		if (\OCP\App::isEnabled('files_versions')) {
433
434
			$user = User::getUser();
0 ignored issues
show
Deprecated Code introduced by
The method OCP\User::getUser() has been deprecated with message: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID()

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

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

Loading history...
435
			$rootView = new View('/');
436
437
			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
438
439
			list($owner, $ownerPath) = self::getUidAndFilename($target);
440
441
			// file has been deleted in between
442
			if (empty($ownerPath)) {
443
				return false;
444
			}
445
446
			if ($timestamp) {
447
				$versionedFile = $filename;
448
			} else {
449
				$versionedFile = $file;
450
			}
451
452
			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
453
				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
454
			} else if ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
455
				foreach ($versions as $v) {
456
					if ($timestamp) {
457
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
458
					} else {
459
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
460
					}
461
				}
462
			}
463
		}
464
	}
465
466
	/**
467
	 * delete all files from the trash
468
	 */
469
	public static function deleteAll() {
470
		$user = User::getUser();
0 ignored issues
show
Deprecated Code introduced by
The method OCP\User::getUser() has been deprecated with message: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID()

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

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

Loading history...
471
		$view = new View('/' . $user);
472
		$view->deleteAll('files_trashbin');
473
		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
474
		$query->execute(array($user));
475
		$view->mkdir('files_trashbin');
476
		$view->mkdir('files_trashbin/files');
477
478
		return true;
479
	}
480
481
	/**
482
	 * delete file from trash bin permanently
483
	 *
484
	 * @param string $filename path to the file
485
	 * @param string $user
486
	 * @param int $timestamp of deletion time
487
	 *
488
	 * @return int size of deleted files
489
	 */
490
	public static function delete($filename, $user, $timestamp = null) {
491
		$view = new View('/' . $user);
492
		$size = 0;
493
494
		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...
495
			$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
496
			$query->execute(array($user, $filename, $timestamp));
497
			$file = $filename . '.d' . $timestamp;
498
		} else {
499
			$file = $filename;
500
		}
501
502
		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
503
504
		if ($view->is_dir('/files_trashbin/files/' . $file)) {
505
			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
506
		} else {
507
			$size += $view->filesize('/files_trashbin/files/' . $file);
508
		}
509
		\OC_Hook::emit('\OCP\Trashbin', 'preDelete', array('path' => '/files_trashbin/files/' . $file));
510
		$view->unlink('/files_trashbin/files/' . $file);
511
		\OC_Hook::emit('\OCP\Trashbin', 'delete', array('path' => '/files_trashbin/files/' . $file));
512
513
		return $size;
514
	}
515
516
	/**
517
	 * @param View $view
518
	 * @param string $file
519
	 * @param string $filename
520
	 * @param integer|null $timestamp
521
	 * @param string $user
522
	 * @return int
523
	 */
524
	private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
525
		$size = 0;
526
		if (\OCP\App::isEnabled('files_versions')) {
527
			if ($view->is_dir('files_trashbin/versions/' . $file)) {
528
				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
529
				$view->unlink('files_trashbin/versions/' . $file);
530
			} else if ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
531
				foreach ($versions as $v) {
532
					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...
533
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
534
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
535
					} else {
536
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
537
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
538
					}
539
				}
540
			}
541
		}
542
		return $size;
543
	}
544
545
	/**
546
	 * check to see whether a file exists in trashbin
547
	 *
548
	 * @param string $filename path to the file
549
	 * @param int $timestamp of deletion time
550
	 * @return bool true if file exists, otherwise false
551
	 */
552
	public static function file_exists($filename, $timestamp = null) {
553
		$user = User::getUser();
0 ignored issues
show
Deprecated Code introduced by
The method OCP\User::getUser() has been deprecated with message: 8.0.0 Use \OC::$server->getUserSession()->getUser()->getUID()

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

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

Loading history...
554
		$view = new View('/' . $user);
555
556
		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...
557
			$filename = $filename . '.d' . $timestamp;
558
		} else {
559
			$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...
560
		}
561
562
		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
563
		return $view->file_exists($target);
564
	}
565
566
	/**
567
	 * deletes used space for trash bin in db if user was deleted
568
	 *
569
	 * @param string $uid id of deleted user
570
	 * @return bool result of db delete operation
571
	 */
572
	public static function deleteUser($uid) {
573
		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
574
		return $query->execute(array($uid));
575
	}
576
577
	/**
578
	 * calculate remaining free space for trash bin
579
	 *
580
	 * @param integer $trashbinSize current size of the trash bin
581
	 * @param string $user
582
	 * @return int available free space for trash bin
583
	 */
584
	private static function calculateFreeSpace($trashbinSize, $user) {
585
		$softQuota = true;
586
		$userObject = \OC::$server->getUserManager()->get($user);
587
		if(is_null($userObject)) {
588
			return 0;
589
		}
590
		$quota = $userObject->getQuota();
591
		if ($quota === null || $quota === 'none') {
592
			$quota = Filesystem::free_space('/');
593
			$softQuota = false;
594
			// inf or unknown free space
595
			if ($quota < 0) {
596
				$quota = PHP_INT_MAX;
597
			}
598
		} else {
599
			$quota = \OCP\Util::computerFileSize($quota);
600
		}
601
602
		// calculate available space for trash bin
603
		// subtract size of files and current trash bin size from quota
604
		if ($softQuota) {
605
			$userFolder = \OC::$server->getUserFolder($user);
606
			if(is_null($userFolder)) {
607
				return 0;
608
			}
609
			$free = $quota - $userFolder->getSize(); // remaining free space for user
610
			if ($free > 0) {
611
				$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
612
			} else {
613
				$availableSpace = $free - $trashbinSize;
614
			}
615
		} else {
616
			$availableSpace = $quota;
617
		}
618
619
		return $availableSpace;
620
	}
621
622
	/**
623
	 * resize trash bin if necessary after a new file was added to ownCloud
624
	 *
625
	 * @param string $user user id
626
	 */
627
	public static function resizeTrash($user) {
628
629
		$size = self::getTrashbinSize($user);
630
631
		$freeSpace = self::calculateFreeSpace($size, $user);
632
633
		if ($freeSpace < 0) {
634
			self::scheduleExpire($user);
635
		}
636
	}
637
638
	/**
639
	 * clean up the trash bin
640
	 *
641
	 * @param string $user
642
	 */
643
	public static function expire($user) {
644
		$trashBinSize = self::getTrashbinSize($user);
645
		$availableSpace = self::calculateFreeSpace($trashBinSize, $user);
646
647
		$dirContent = Helper::getTrashFiles('/', $user, 'mtime');
648
649
		// delete all files older then $retention_obligation
650
		list($delSize, $count) = self::deleteExpiredFiles($dirContent, $user);
651
652
		$availableSpace += $delSize;
653
654
		// delete files from trash until we meet the trash bin size limit again
655
		self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
656
	}
657
658
	/**
659
	 * @param string $user
660
	 */
661
	private static function scheduleExpire($user) {
662
		// let the admin disable auto expire
663
		$application = new Application();
664
		$expiration = $application->getContainer()->query('Expiration');
665
		if ($expiration->isEnabled()) {
666
			\OC::$server->getCommandBus()->push(new Expire($user));
667
		}
668
	}
669
670
	/**
671
	 * if the size limit for the trash bin is reached, we delete the oldest
672
	 * files in the trash bin until we meet the limit again
673
	 *
674
	 * @param array $files
675
	 * @param string $user
676
	 * @param int $availableSpace available disc space
677
	 * @return int size of deleted files
678
	 */
679
	protected static function deleteFiles($files, $user, $availableSpace) {
680
		$application = new Application();
681
		$expiration = $application->getContainer()->query('Expiration');
682
		$size = 0;
683
684
		if ($availableSpace < 0) {
685
			foreach ($files as $file) {
686
				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
687
					$tmp = self::delete($file['name'], $user, $file['mtime']);
688
					\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);
689
					$availableSpace += $tmp;
690
					$size += $tmp;
691
				} else {
692
					break;
693
				}
694
			}
695
		}
696
		return $size;
697
	}
698
699
	/**
700
	 * delete files older then max storage time
701
	 *
702
	 * @param array $files list of files sorted by mtime
703
	 * @param string $user
704
	 * @return integer[] size of deleted files and number of deleted files
705
	 */
706
	public static function deleteExpiredFiles($files, $user) {
707
		$application = new Application();
708
		$expiration = $application->getContainer()->query('Expiration');
709
		$size = 0;
710
		$count = 0;
711
		foreach ($files as $file) {
712
			$timestamp = $file['mtime'];
713
			$filename = $file['name'];
714
			if ($expiration->isExpired($timestamp)) {
715
				$count++;
716
				$size += self::delete($filename, $user, $timestamp);
717
				\OC::$server->getLogger()->info(
718
					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
719
					['app' => 'files_trashbin']
720
				);
721
			} else {
722
				break;
723
			}
724
		}
725
726
		return array($size, $count);
727
	}
728
729
	/**
730
	 * recursive copy to copy a whole directory
731
	 *
732
	 * @param string $source source path, relative to the users files directory
733
	 * @param string $destination destination path relative to the users root directoy
734
	 * @param View $view file view for the users root directory
735
	 * @return int
736
	 * @throws Exceptions\CopyRecursiveException
737
	 */
738
	private static function copy_recursive($source, $destination, View $view) {
739
		$size = 0;
740
		if ($view->is_dir($source)) {
741
			$view->mkdir($destination);
742
			$view->touch($destination, $view->filemtime($source));
743
			foreach ($view->getDirectoryContent($source) as $i) {
744
				$pathDir = $source . '/' . $i['name'];
745
				if ($view->is_dir($pathDir)) {
746
					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
747
				} else {
748
					$size += $view->filesize($pathDir);
749
					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
750
					if (!$result) {
751
						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
752
					}
753
					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
754
				}
755
			}
756
		} else {
757
			$size += $view->filesize($source);
758
			$result = $view->copy($source, $destination);
759
			if (!$result) {
760
				throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
761
			}
762
			$view->touch($destination, $view->filemtime($source));
763
		}
764
		return $size;
765
	}
766
767
	/**
768
	 * find all versions which belong to the file we want to restore
769
	 *
770
	 * @param string $filename name of the file which should be restored
771
	 * @param int $timestamp timestamp when the file was deleted
772
	 * @return array
773
	 */
774
	private static function getVersionsFromTrash($filename, $timestamp, $user) {
775
		$view = new View('/' . $user . '/files_trashbin/versions');
776
		$versions = array();
777
778
		//force rescan of versions, local storage may not have updated the cache
779
		if (!self::$scannedVersions) {
780
			/** @var \OC\Files\Storage\Storage $storage */
781
			list($storage,) = $view->resolvePath('/');
782
			$storage->getScanner()->scan('files_trashbin/versions');
783
			self::$scannedVersions = true;
784
		}
785
786
		if ($timestamp) {
787
			// fetch for old versions
788
			$matches = $view->searchRaw($filename . '.v%.d' . $timestamp);
789
			$offset = -strlen($timestamp) - 2;
790
		} else {
791
			$matches = $view->searchRaw($filename . '.v%');
792
		}
793
794
		if (is_array($matches)) {
795
			foreach ($matches as $ma) {
796
				if ($timestamp) {
797
					$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...
798
					$versions[] = (end($parts));
799
				} else {
800
					$parts = explode('.v', $ma);
801
					$versions[] = (end($parts));
802
				}
803
			}
804
		}
805
		return $versions;
806
	}
807
808
	/**
809
	 * find unique extension for restored file if a file with the same name already exists
810
	 *
811
	 * @param string $location where the file should be restored
812
	 * @param string $filename name of the file
813
	 * @param View $view filesystem view relative to users root directory
814
	 * @return string with unique extension
815
	 */
816
	private static function getUniqueFilename($location, $filename, View $view) {
817
		$ext = pathinfo($filename, PATHINFO_EXTENSION);
818
		$name = pathinfo($filename, PATHINFO_FILENAME);
819
		$l = \OC::$server->getL10N('files_trashbin');
820
821
		$location = '/' . trim($location, '/');
822
823
		// if extension is not empty we set a dot in front of it
824
		if ($ext !== '') {
825
			$ext = '.' . $ext;
826
		}
827
828
		if ($view->file_exists('files' . $location . '/' . $filename)) {
829
			$i = 2;
830
			$uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
831
			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
832
				$uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
833
				$i++;
834
			}
835
836
			return $uniqueName;
837
		}
838
839
		return $filename;
840
	}
841
842
	/**
843
	 * get the size from a given root folder
844
	 *
845
	 * @param View $view file view on the root folder
846
	 * @return integer size of the folder
847
	 */
848
	private static function calculateSize($view) {
849
		$root = \OC::$server->getConfig()->getSystemValue('datadirectory') . $view->getAbsolutePath('');
850
		if (!file_exists($root)) {
851
			return 0;
852
		}
853
		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
854
		$size = 0;
855
856
		/**
857
		 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
858
		 * This bug is fixed in PHP 5.5.9 or before
859
		 * See #8376
860
		 */
861
		$iterator->rewind();
862
		while ($iterator->valid()) {
863
			$path = $iterator->current();
864
			$relpath = substr($path, strlen($root) - 1);
865
			if (!$view->is_dir($relpath)) {
866
				$size += $view->filesize($relpath);
867
			}
868
			$iterator->next();
869
		}
870
		return $size;
871
	}
872
873
	/**
874
	 * get current size of trash bin from a given user
875
	 *
876
	 * @param string $user user who owns the trash bin
877
	 * @return integer trash bin size
878
	 */
879
	private static function getTrashbinSize($user) {
880
		$view = new View('/' . $user);
881
		$fileInfo = $view->getFileInfo('/files_trashbin');
882
		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
883
	}
884
885
	/**
886
	 * register hooks
887
	 */
888
	public static function registerHooks() {
889
		// create storage wrapper on setup
890
		\OCP\Util::connectHook('OC_Filesystem', 'preSetup', 'OCA\Files_Trashbin\Storage', 'setupStorage');
891
		//Listen to delete user signal
892
		\OCP\Util::connectHook('OC_User', 'pre_deleteUser', 'OCA\Files_Trashbin\Hooks', 'deleteUser_hook');
893
		//Listen to post write hook
894
		\OCP\Util::connectHook('OC_Filesystem', 'post_write', 'OCA\Files_Trashbin\Hooks', 'post_write_hook');
895
		// pre and post-rename, disable trash logic for the copy+unlink case
896
		\OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Trashbin\Trashbin', 'ensureFileScannedHook');
897
		\OCP\Util::connectHook('OC_Filesystem', 'rename', 'OCA\Files_Trashbin\Storage', 'preRenameHook');
898
		\OCP\Util::connectHook('OC_Filesystem', 'post_rename', 'OCA\Files_Trashbin\Storage', 'postRenameHook');
899
	}
900
901
	/**
902
	 * check if trash bin is empty for a given user
903
	 *
904
	 * @param string $user
905
	 * @return bool
906
	 */
907
	public static function isEmpty($user) {
908
909
		$view = new View('/' . $user . '/files_trashbin');
910
		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
911
			while ($file = readdir($dh)) {
912
				if (!Filesystem::isIgnoredDir($file)) {
913
					return false;
914
				}
915
			}
916
		}
917
		return true;
918
	}
919
920
	/**
921
	 * @param $path
922
	 * @return string
923
	 */
924
	public static function preview_icon($path) {
925
		return \OCP\Util::linkToRoute('core_ajax_trashbin_preview', array('x' => 32, 'y' => 32, 'file' => $path));
0 ignored issues
show
Deprecated Code introduced by
The method OCP\Util::linkToRoute() has been deprecated with message: 8.1.0 Use \OC::$server->getURLGenerator()->linkToRoute($route, $parameters)

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

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

Loading history...
926
	}
927
}
928