Completed
Push — master ( 99c28a...4c4331 )
by Robin
47:48
created

Trashbin::restore()   B

Complexity

Conditions 9
Paths 16

Size

Total Lines 54
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 9.0016
Metric Value
dl 0
loc 54
ccs 36
cts 37
cp 0.973
rs 7.2551
cc 9
eloc 33
nc 16
nop 3
crap 9.0016

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Martin Mattel <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Qingping Hou <[email protected]>
13
 * @author Robin Appelman <[email protected]>
14
 * @author Robin McCorkell <[email protected]>
15
 * @author Roeland Jago Douma <[email protected]>
16
 * @author Sjors van der Pluijm <[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) 2015, ownCloud, Inc.
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
46
class Trashbin {
47
48
	// unit: percentage; 50% of available disk space/quota
49
	const DEFAULTMAXSIZE = 50;
50
51
	/**
52
	 * Whether versions have already be rescanned during this PHP request
53
	 *
54
	 * @var bool
55
	 */
56
	private static $scannedVersions = false;
57
58
	/**
59
	 * Ensure we dont need to scan the file during the move to trash
60
	 * by triggering the scan in the pre-hook
61
	 *
62
	 * @param array $params
63
	 */
64 36
	public static function ensureFileScannedHook($params) {
65 36
		self::getUidAndFilename($params['path']);
66 36
	}
67
68
	/**
69
	 * @param string $filename
70
	 * @return array
71
	 * @throws \OC\User\NoUserException
72
	 */
73 56 View Code Duplication
	public static function getUidAndFilename($filename) {
74 56
		$uid = \OC\Files\Filesystem::getOwner($filename);
75 56
		\OC\Files\Filesystem::initMountPoints($uid);
76 56
		if ($uid != \OCP\User::getUser()) {
77 7
			$info = \OC\Files\Filesystem::getFileInfo($filename);
78 7
			$ownerView = new \OC\Files\View('/' . $uid . '/files');
79
			try {
80 7
				$filename = $ownerView->getPath($info['fileid']);
81 7
			} catch (NotFoundException $e) {
82
				$filename = null;
83
			}
84 7
		}
85 56
		return [$uid, $filename];
86
	}
87
88
	/**
89
	 * get original location of files for user
90
	 *
91
	 * @param string $user
92
	 * @return array (filename => array (timestamp => original location))
93
	 */
94 12
	public static function getLocations($user) {
95 12
		$query = \OC_DB::prepare('SELECT `id`, `timestamp`, `location`'
96 12
			. ' FROM `*PREFIX*files_trash` WHERE `user`=?');
97 12
		$result = $query->execute(array($user));
98 12
		$array = array();
99 12
		while ($row = $result->fetchRow()) {
100 12
			if (isset($array[$row['id']])) {
101
				$array[$row['id']][$row['timestamp']] = $row['location'];
102
			} else {
103 12
				$array[$row['id']] = array($row['timestamp'] => $row['location']);
104
			}
105 12
		}
106 12
		return $array;
107
	}
108
109
	/**
110
	 * get original location of file
111
	 *
112
	 * @param string $user
113
	 * @param string $filename
114
	 * @param string $timestamp
115
	 * @return string original location
116
	 */
117 8
	public static function getLocation($user, $filename, $timestamp) {
118 8
		$query = \OC_DB::prepare('SELECT `location` FROM `*PREFIX*files_trash`'
119 8
			. ' WHERE `user`=? AND `id`=? AND `timestamp`=?');
120 8
		$result = $query->execute(array($user, $filename, $timestamp))->fetchAll();
121 8
		if (isset($result[0]['location'])) {
122 6
			return $result[0]['location'];
123
		} else {
124 2
			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...
125
		}
126
	}
127
128 55
	private static function setUpTrash($user) {
129 55
		$view = new \OC\Files\View('/' . $user);
130 55
		if (!$view->is_dir('files_trashbin')) {
131 26
			$view->mkdir('files_trashbin');
132 26
		}
133 55
		if (!$view->is_dir('files_trashbin/files')) {
134 26
			$view->mkdir('files_trashbin/files');
135 26
		}
136 55
		if (!$view->is_dir('files_trashbin/versions')) {
137 26
			$view->mkdir('files_trashbin/versions');
138 26
		}
139 55
		if (!$view->is_dir('files_trashbin/keys')) {
140 26
			$view->mkdir('files_trashbin/keys');
141 26
		}
142 55
	}
143
144
145
	/**
146
	 * copy file to owners trash
147
	 *
148
	 * @param string $sourcePath
149
	 * @param string $owner
150
	 * @param $targetPath
151
	 * @param $user
152
	 * @param integer $timestamp
153
	 */
154 4
	private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
155 4
		self::setUpTrash($owner);
156
157 4
		$targetFilename = basename($targetPath);
158 4
		$targetLocation = dirname($targetPath);
159
160 4
		$sourceFilename = basename($sourcePath);
161
162 4
		$view = new \OC\Files\View('/');
163
164 4
		$target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
165 4
		$source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
166 4
		self::copy_recursive($source, $target, $view);
167
168
169 4
		if ($view->file_exists($target)) {
170 4
			$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
171 4
			$result = $query->execute(array($targetFilename, $timestamp, $targetLocation, $user));
172 4
			if (!$result) {
173
				\OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated for the files owner', \OCP\Util::ERROR);
174
			}
175 4
		}
176 4
	}
177
178
179
	/**
180
	 * move file to the trash bin
181
	 *
182
	 * @param string $file_path path to the deleted file/directory relative to the files root directory
183
	 * @return bool
184
	 */
185 55
	public static function move2trash($file_path) {
186
		// get the user for which the filesystem is setup
187 55
		$root = Filesystem::getRoot();
188 55
		list(, $user) = explode('/', $root);
189 55
		list($owner, $ownerPath) = self::getUidAndFilename($file_path);
190
191 55
		$ownerView = new \OC\Files\View('/' . $owner);
192
		// file has been deleted in between
193 55
		if (!$ownerView->file_exists('/files/' . $ownerPath)) {
194
			return true;
195
		}
196
197 55
		self::setUpTrash($user);
198 55
		if ($owner !== $user) {
199
			// also setup for owner
200 4
			self::setUpTrash($owner);
201 4
		}
202
203 55
		$path_parts = pathinfo($ownerPath);
204
205 55
		$filename = $path_parts['basename'];
206 55
		$location = $path_parts['dirname'];
207 55
		$timestamp = time();
208
209
		// disable proxy to prevent recursive calls
210 55
		$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
211
212
		/** @var \OC\Files\Storage\Storage $trashStorage */
213 55
		list($trashStorage, $trashInternalPath) = $ownerView->resolvePath($trashPath);
214
		/** @var \OC\Files\Storage\Storage $sourceStorage */
215 55
		list($sourceStorage, $sourceInternalPath) = $ownerView->resolvePath('/files/' . $ownerPath);
216
		try {
217 55
			$sizeOfAddedFiles = $sourceStorage->filesize($sourceInternalPath);
218 55
			if ($trashStorage->file_exists($trashInternalPath)) {
219
				$trashStorage->unlink($trashInternalPath);
220
			}
221 55
			$trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
222 55
		} catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
223
			$sizeOfAddedFiles = false;
224
			if ($trashStorage->file_exists($trashInternalPath)) {
225
				$trashStorage->unlink($trashInternalPath);
226
			}
227
			\OCP\Util::writeLog('files_trashbin', 'Couldn\'t move ' . $file_path . ' to the trash bin', \OCP\Util::ERROR);
228
		}
229
230 55
		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
231 1
			$sourceStorage->unlink($sourceInternalPath);
232 1
			return false;
233
		}
234
235 54
		$trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
236
237 54
		if ($sizeOfAddedFiles !== false) {
238 54
			$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
239 54
			$result = $query->execute(array($filename, $timestamp, $location, $owner));
240 54
			if (!$result) {
241
				\OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated', \OCP\Util::ERROR);
242
			}
243 54
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', array('filePath' => \OC\Files\Filesystem::normalizePath($file_path),
244 54
				'trashPath' => \OC\Files\Filesystem::normalizePath($filename . '.d' . $timestamp)));
245
246 54
			self::retainVersions($filename, $owner, $ownerPath, $timestamp);
247
248
			// if owner !== user we need to also add a copy to the owners trash
249 54
			if ($user !== $owner) {
250 4
				self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
251 4
			}
252 54
		}
253
254 54
		self::scheduleExpire($user);
255
256
		// if owner !== user we also need to update the owners trash size
257 54
		if ($owner !== $user) {
258 4
			self::scheduleExpire($owner);
259 4
		}
260
261 54
		return ($sizeOfAddedFiles === false) ? false : true;
262
	}
263
264
	/**
265
	 * Move file versions to trash so that they can be restored later
266
	 *
267
	 * @param string $filename of deleted file
268
	 * @param string $owner owner user id
269
	 * @param string $ownerPath path relative to the owner's home storage
270
	 * @param integer $timestamp when the file was deleted
271
	 *
272
	 * @return int size of stored versions
273
	 */
274 54
	private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
275 54
		$size = 0;
276 54
		if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {
277
278 54
			$user = \OCP\User::getUser();
279 54
			$rootView = new \OC\Files\View('/');
280
281 54
			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
282 2
				$size += self::calculateSize(new \OC\Files\View('/' . $owner . '/files_versions/' . $ownerPath));
283 2
				if ($owner !== $user) {
284 1
					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
285 1
				}
286 2
				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
287 54
			} else if ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
288
289 2
				foreach ($versions as $v) {
290 2
					$size += $rootView->filesize($owner . '/files_versions/' . $v['path'] . '.v' . $v['version']);
291 2
					if ($owner !== $user) {
292 1
						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
293 1
					}
294 2
					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
295 2
				}
296 2
			}
297 54
		}
298
299 54
		return $size;
300
	}
301
302
	/**
303
	 * Move a file or folder on storage level
304
	 *
305
	 * @param View $view
306
	 * @param string $source
307
	 * @param string $target
308
	 * @return bool
309
	 */
310 4
	private static function move(View $view, $source, $target) {
311
		/** @var \OC\Files\Storage\Storage $sourceStorage */
312 4
		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
313
		/** @var \OC\Files\Storage\Storage $targetStorage */
314 4
		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
315
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
316
317 4
		$result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
318 4
		if ($result) {
319 4
			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
320 4
		}
321 4
		return $result;
322
	}
323
324
	/**
325
	 * Copy a file or folder on storage level
326
	 *
327
	 * @param View $view
328
	 * @param string $source
329
	 * @param string $target
330
	 * @return bool
331
	 */
332 1
	private static function copy(View $view, $source, $target) {
333
		/** @var \OC\Files\Storage\Storage $sourceStorage */
334 1
		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
335
		/** @var \OC\Files\Storage\Storage $targetStorage */
336 1
		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
337
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
338
339 1
		$result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
340 1
		if ($result) {
341 1
			$targetStorage->getUpdater()->update($targetInternalPath);
342 1
		}
343 1
		return $result;
344
	}
345
346
	/**
347
	 * Restore a file or folder from trash bin
348
	 *
349
	 * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
350
	 * including the timestamp suffix ".d12345678"
351
	 * @param string $filename name of the file/folder
352
	 * @param int $timestamp time when the file/folder was deleted
353
	 *
354
	 * @return bool true on success, false otherwise
355
	 */
356 8
	public static function restore($file, $filename, $timestamp) {
357 8
		$user = \OCP\User::getUser();
358 8
		$view = new \OC\Files\View('/' . $user);
359
360 8
		$location = '';
361 8
		if ($timestamp) {
362 8
			$location = self::getLocation($user, $filename, $timestamp);
363 8
			if ($location === false) {
364 2
				\OCP\Util::writeLog('files_trashbin', 'trash bin database inconsistent!', \OCP\Util::ERROR);
365 2
			} else {
366
				// if location no longer exists, restore file in the root directory
367 6
				if ($location !== '/' &&
368 6
					(!$view->is_dir('files/' . $location) ||
369 5
						!$view->isCreatable('files/' . $location))
370 6
				) {
371 1
					$location = '';
372 1
				}
373
			}
374 8
		}
375
376
		// we need a  extension in case a file/dir with the same name already exists
377 8
		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
378
379 8
		$source = \OC\Files\Filesystem::normalizePath('files_trashbin/files/' . $file);
380 8
		$target = \OC\Files\Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
381 8
		if (!$view->file_exists($source)) {
382 1
			return false;
383
		}
384 7
		$mtime = $view->filemtime($source);
385
386
		// restore file
387 7
		$restoreResult = $view->rename($source, $target);
388
389
		// handle the restore result
390 7
		if ($restoreResult) {
391 7
			$fakeRoot = $view->getRoot();
392 7
			$view->chroot('/' . $user . '/files');
393 7
			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
394 7
			$view->chroot($fakeRoot);
395 7
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', array('filePath' => \OC\Files\Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
396 7
				'trashPath' => \OC\Files\Filesystem::normalizePath($file)));
397
398 7
			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
399
400 7
			if ($timestamp) {
401 7
				$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
402 7
				$query->execute(array($user, $filename, $timestamp));
403 7
			}
404
405 7
			return true;
406
		}
407
408
		return false;
409
	}
410
411
	/**
412
	 * restore versions from trash bin
413
	 *
414
	 * @param \OC\Files\View $view file view
415
	 * @param string $file complete path to file
416
	 * @param string $filename name of file once it was deleted
417
	 * @param string $uniqueFilename new file name to restore the file without overwriting existing files
418
	 * @param string $location location if file
419
	 * @param int $timestamp deletion time
420
	 * @return bool
421
	 */
422 7
	private static function restoreVersions(\OC\Files\View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
423
424 7
		if (\OCP\App::isEnabled('files_versions')) {
425
426 7
			$user = \OCP\User::getUser();
427 7
			$rootView = new \OC\Files\View('/');
428
429 7
			$target = \OC\Files\Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
430
431 7
			list($owner, $ownerPath) = self::getUidAndFilename($target);
432
433
			// file has been deleted in between
434 7
			if (empty($ownerPath)) {
435
				return false;
436
			}
437
438 7
			if ($timestamp) {
439 7
				$versionedFile = $filename;
440 7
			} else {
441
				$versionedFile = $file;
442
			}
443
444 7
			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
445
				$rootView->rename(\OC\Files\Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), \OC\Files\Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
446 7
			} else if ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
447
				foreach ($versions as $v) {
448
					if ($timestamp) {
449
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
450
					} else {
451
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
452
					}
453
				}
454
			}
455 7
		}
456 7
	}
457
458
	/**
459
	 * delete all files from the trash
460
	 */
461
	public static function deleteAll() {
462
		$user = \OCP\User::getUser();
463
		$view = new \OC\Files\View('/' . $user);
464
		$view->deleteAll('files_trashbin');
465
		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
466
		$query->execute(array($user));
467
		$view->mkdir('files_trashbin');
468
		$view->mkdir('files_trashbin/files');
469
470
		return true;
471
	}
472
473
	/**
474
	 * delete file from trash bin permanently
475
	 *
476
	 * @param string $filename path to the file
477
	 * @param string $user
478
	 * @param int $timestamp of deletion time
479
	 *
480
	 * @return int size of deleted files
481
	 */
482 3
	public static function delete($filename, $user, $timestamp = null) {
483 3
		$view = new \OC\Files\View('/' . $user);
484 3
		$size = 0;
485
486 3
		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...
487 3
			$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
488 3
			$query->execute(array($user, $filename, $timestamp));
489 3
			$file = $filename . '.d' . $timestamp;
490 3
		} else {
491
			$file = $filename;
492
		}
493
494 3
		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
495
496 3
		if ($view->is_dir('/files_trashbin/files/' . $file)) {
497
			$size += self::calculateSize(new \OC\Files\View('/' . $user . '/files_trashbin/files/' . $file));
498
		} else {
499 3
			$size += $view->filesize('/files_trashbin/files/' . $file);
500
		}
501 3
		\OC_Hook::emit('\OCP\Trashbin', 'preDelete', array('path' => '/files_trashbin/files/' . $file));
502 3
		$view->unlink('/files_trashbin/files/' . $file);
503 3
		\OC_Hook::emit('\OCP\Trashbin', 'delete', array('path' => '/files_trashbin/files/' . $file));
504
505 3
		return $size;
506
	}
507
508
	/**
509
	 * @param \OC\Files\View $view
510
	 * @param $file
511
	 * @param $filename
512
	 * @param $timestamp
513
	 * @return int
514
	 */
515 3
	private static function deleteVersions(\OC\Files\View $view, $file, $filename, $timestamp, $user) {
516 3
		$size = 0;
517 3
		if (\OCP\App::isEnabled('files_versions')) {
518 3
			if ($view->is_dir('files_trashbin/versions/' . $file)) {
519
				$size += self::calculateSize(new \OC\Files\view('/' . $user . '/files_trashbin/versions/' . $file));
520
				$view->unlink('files_trashbin/versions/' . $file);
521 3
			} else if ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
522
				foreach ($versions as $v) {
523
					if ($timestamp) {
524
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
525
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
526
					} else {
527
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
528
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
529
					}
530
				}
531
			}
532 3
		}
533 3
		return $size;
534
	}
535
536
	/**
537
	 * check to see whether a file exists in trashbin
538
	 *
539
	 * @param string $filename path to the file
540
	 * @param int $timestamp of deletion time
541
	 * @return bool true if file exists, otherwise false
542
	 */
543
	public static function file_exists($filename, $timestamp = null) {
544
		$user = \OCP\User::getUser();
545
		$view = new \OC\Files\View('/' . $user);
546
547
		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...
548
			$filename = $filename . '.d' . $timestamp;
549
		} else {
550
			$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...
551
		}
552
553
		$target = \OC\Files\Filesystem::normalizePath('files_trashbin/files/' . $filename);
554
		return $view->file_exists($target);
555
	}
556
557
	/**
558
	 * deletes used space for trash bin in db if user was deleted
559
	 *
560
	 * @param string $uid id of deleted user
561
	 * @return bool result of db delete operation
562
	 */
563 16
	public static function deleteUser($uid) {
564 16
		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
565 16
		return $query->execute(array($uid));
566
	}
567
568
	/**
569
	 * calculate remaining free space for trash bin
570
	 *
571
	 * @param integer $trashbinSize current size of the trash bin
572
	 * @param string $user
573
	 * @return int available free space for trash bin
574
	 */
575 44
	private static function calculateFreeSpace($trashbinSize, $user) {
576 44
		$config = \OC::$server->getConfig();
577
578 44
		$softQuota = true;
579 44
		$quota = $config->getUserValue($user, 'files', 'quota', null);
580 44
		$view = new \OC\Files\View('/' . $user);
581 44 View Code Duplication
		if ($quota === null || $quota === 'default') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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