Completed
Push — master ( 06e969...2f488c )
by Lukas
56:47 queued 47:01
created

Trashbin::ensureFileScannedHook()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bart Visscher <[email protected]>
6
 * @author Bastien Ho <[email protected]>
7
 * @author 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 Stefan Weil <[email protected]>
19
 * @author Thomas Müller <[email protected]>
20
 * @author Victor Dubiniuk <[email protected]>
21
 * @author Vincent Petry <[email protected]>
22
 *
23
 * @license AGPL-3.0
24
 *
25
 * This code is free software: you can redistribute it and/or modify
26
 * it under the terms of the GNU Affero General Public License, version 3,
27
 * as published by the Free Software Foundation.
28
 *
29
 * This program is distributed in the hope that it will be useful,
30
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32
 * GNU Affero General Public License for more details.
33
 *
34
 * You should have received a copy of the GNU Affero General Public License, version 3,
35
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
36
 *
37
 */
38
39
namespace OCA\Files_Trashbin;
40
41
use OC\Files\Filesystem;
42
use OC\Files\View;
43
use OCA\Files_Trashbin\AppInfo\Application;
44
use OCA\Files_Trashbin\Command\Expire;
45
use OCP\Files\NotFoundException;
46
use OCP\User;
47
48
class Trashbin {
49
50
	// unit: percentage; 50% of available disk space/quota
51
	const DEFAULTMAXSIZE = 50;
52
53
	/**
54
	 * Whether versions have already be rescanned during this PHP request
55
	 *
56
	 * @var bool
57
	 */
58
	private static $scannedVersions = false;
59
60
	/**
61
	 * Ensure we don't need to scan the file during the move to trash
62
	 * by triggering the scan in the pre-hook
63
	 *
64
	 * @param array $params
65
	 */
66
	public static function ensureFileScannedHook($params) {
67
		try {
68
			self::getUidAndFilename($params['path']);
69
		} catch (NotFoundException $e) {
70
			// nothing to scan for non existing files
71
		}
72
	}
73
74
	/**
75
	 * get the UID of the owner of the file and the path to the file relative to
76
	 * owners files folder
77
	 *
78
	 * @param string $filename
79
	 * @return array
80
	 * @throws \OC\User\NoUserException
81
	 */
82 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...
83
		$uid = Filesystem::getOwner($filename);
84
		$userManager = \OC::$server->getUserManager();
85
		// if the user with the UID doesn't exists, e.g. because the UID points
86
		// to a remote user with a federated cloud ID we use the current logged-in
87
		// user. We need a valid local user to move the file to the right trash bin
88
		if (!$userManager->userExists($uid)) {
89
			$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...
90
		}
91
		Filesystem::initMountPoints($uid);
92
		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...
93
			$info = Filesystem::getFileInfo($filename);
94
			$ownerView = new View('/' . $uid . '/files');
95
			try {
96
				$filename = $ownerView->getPath($info['fileid']);
97
			} catch (NotFoundException $e) {
98
				$filename = null;
99
			}
100
		}
101
		return [$uid, $filename];
102
	}
103
104
	/**
105
	 * get original location of files for user
106
	 *
107
	 * @param string $user
108
	 * @return array (filename => array (timestamp => original location))
109
	 */
110
	public static function getLocations($user) {
111
		$query = \OC_DB::prepare('SELECT `id`, `timestamp`, `location`'
112
			. ' FROM `*PREFIX*files_trash` WHERE `user`=?');
113
		$result = $query->execute(array($user));
114
		$array = array();
115
		while ($row = $result->fetchRow()) {
116
			if (isset($array[$row['id']])) {
117
				$array[$row['id']][$row['timestamp']] = $row['location'];
118
			} else {
119
				$array[$row['id']] = array($row['timestamp'] => $row['location']);
120
			}
121
		}
122
		return $array;
123
	}
124
125
	/**
126
	 * get original location of file
127
	 *
128
	 * @param string $user
129
	 * @param string $filename
130
	 * @param string $timestamp
131
	 * @return string original location
132
	 */
133
	public static function getLocation($user, $filename, $timestamp) {
134
		$query = \OC_DB::prepare('SELECT `location` FROM `*PREFIX*files_trash`'
135
			. ' WHERE `user`=? AND `id`=? AND `timestamp`=?');
136
		$result = $query->execute(array($user, $filename, $timestamp))->fetchAll();
137
		if (isset($result[0]['location'])) {
138
			return $result[0]['location'];
139
		} else {
140
			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...
141
		}
142
	}
143
144
	private static function setUpTrash($user) {
145
		$view = new View('/' . $user);
146
		if (!$view->is_dir('files_trashbin')) {
147
			$view->mkdir('files_trashbin');
148
		}
149
		if (!$view->is_dir('files_trashbin/files')) {
150
			$view->mkdir('files_trashbin/files');
151
		}
152
		if (!$view->is_dir('files_trashbin/versions')) {
153
			$view->mkdir('files_trashbin/versions');
154
		}
155
		if (!$view->is_dir('files_trashbin/keys')) {
156
			$view->mkdir('files_trashbin/keys');
157
		}
158
	}
159
160
161
	/**
162
	 * copy file to owners trash
163
	 *
164
	 * @param string $sourcePath
165
	 * @param string $owner
166
	 * @param string $targetPath
167
	 * @param $user
168
	 * @param integer $timestamp
169
	 */
170
	private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
171
		self::setUpTrash($owner);
172
173
		$targetFilename = basename($targetPath);
174
		$targetLocation = dirname($targetPath);
175
176
		$sourceFilename = basename($sourcePath);
177
178
		$view = new View('/');
179
180
		$target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
181
		$source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
182
		self::copy_recursive($source, $target, $view);
183
184
185
		if ($view->file_exists($target)) {
186
			$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
187
			$result = $query->execute(array($targetFilename, $timestamp, $targetLocation, $user));
188
			if (!$result) {
189
				\OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated for the files owner', \OCP\Util::ERROR);
190
			}
191
		}
192
	}
193
194
195
	/**
196
	 * move file to the trash bin
197
	 *
198
	 * @param string $file_path path to the deleted file/directory relative to the files root directory
199
	 * @return bool
200
	 */
201
	public static function move2trash($file_path) {
202
		// get the user for which the filesystem is setup
203
		$root = Filesystem::getRoot();
204
		list(, $user) = explode('/', $root);
205
		list($owner, $ownerPath) = self::getUidAndFilename($file_path);
206
207
		$ownerView = new View('/' . $owner);
208
		// file has been deleted in between
209
		if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
210
			return true;
211
		}
212
213
		self::setUpTrash($user);
214
		if ($owner !== $user) {
215
			// also setup for owner
216
			self::setUpTrash($owner);
217
		}
218
219
		$path_parts = pathinfo($ownerPath);
220
221
		$filename = $path_parts['basename'];
222
		$location = $path_parts['dirname'];
223
		$timestamp = time();
224
225
		// disable proxy to prevent recursive calls
226
		$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
227
228
		/** @var \OC\Files\Storage\Storage $trashStorage */
229
		list($trashStorage, $trashInternalPath) = $ownerView->resolvePath($trashPath);
230
		/** @var \OC\Files\Storage\Storage $sourceStorage */
231
		list($sourceStorage, $sourceInternalPath) = $ownerView->resolvePath('/files/' . $ownerPath);
232
		try {
233
			$moveSuccessful = true;
234
			if ($trashStorage->file_exists($trashInternalPath)) {
235
				$trashStorage->unlink($trashInternalPath);
236
			}
237
			$trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
238
		} catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
239
			$moveSuccessful = false;
240
			if ($trashStorage->file_exists($trashInternalPath)) {
241
				$trashStorage->unlink($trashInternalPath);
242
			}
243
			\OCP\Util::writeLog('files_trashbin', 'Couldn\'t move ' . $file_path . ' to the trash bin', \OCP\Util::ERROR);
244
		}
245
246
		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
247
			$sourceStorage->unlink($sourceInternalPath);
248
			return false;
249
		}
250
251
		$trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
252
253
		if ($moveSuccessful) {
254
			$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
255
			$result = $query->execute(array($filename, $timestamp, $location, $owner));
256
			if (!$result) {
257
				\OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated', \OCP\Util::ERROR);
258
			}
259
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', array('filePath' => Filesystem::normalizePath($file_path),
260
				'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)));
261
262
			self::retainVersions($filename, $owner, $ownerPath, $timestamp);
263
264
			// if owner !== user we need to also add a copy to the owners trash
265
			if ($user !== $owner) {
266
				self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
267
			}
268
		}
269
270
		self::scheduleExpire($user);
271
272
		// if owner !== user we also need to update the owners trash size
273
		if ($owner !== $user) {
274
			self::scheduleExpire($owner);
275
		}
276
277
		return $moveSuccessful;
278
	}
279
280
	/**
281
	 * Move file versions to trash so that they can be restored later
282
	 *
283
	 * @param string $filename of deleted file
284
	 * @param string $owner owner user id
285
	 * @param string $ownerPath path relative to the owner's home storage
286
	 * @param integer $timestamp when the file was deleted
287
	 */
288
	private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
289
		if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {
290
291
			$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...
292
			$rootView = new View('/');
293
294
			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
295
				if ($owner !== $user) {
296
					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
297
				}
298
				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
299
			} else if ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
300
301
				foreach ($versions as $v) {
302
					if ($owner !== $user) {
303
						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
304
					}
305
					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
306
				}
307
			}
308
		}
309
	}
310
311
	/**
312
	 * Move a file or folder on storage level
313
	 *
314
	 * @param View $view
315
	 * @param string $source
316
	 * @param string $target
317
	 * @return bool
318
	 */
319
	private static function move(View $view, $source, $target) {
320
		/** @var \OC\Files\Storage\Storage $sourceStorage */
321
		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
322
		/** @var \OC\Files\Storage\Storage $targetStorage */
323
		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
324
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
325
326
		$result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
327
		if ($result) {
328
			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
329
		}
330
		return $result;
331
	}
332
333
	/**
334
	 * Copy a file or folder on storage level
335
	 *
336
	 * @param View $view
337
	 * @param string $source
338
	 * @param string $target
339
	 * @return bool
340
	 */
341
	private static function copy(View $view, $source, $target) {
342
		/** @var \OC\Files\Storage\Storage $sourceStorage */
343
		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
344
		/** @var \OC\Files\Storage\Storage $targetStorage */
345
		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
346
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
347
348
		$result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
349
		if ($result) {
350
			$targetStorage->getUpdater()->update($targetInternalPath);
351
		}
352
		return $result;
353
	}
354
355
	/**
356
	 * Restore a file or folder from trash bin
357
	 *
358
	 * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
359
	 * including the timestamp suffix ".d12345678"
360
	 * @param string $filename name of the file/folder
361
	 * @param int $timestamp time when the file/folder was deleted
362
	 *
363
	 * @return bool true on success, false otherwise
364
	 */
365
	public static function restore($file, $filename, $timestamp) {
366
		$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...
367
		$view = new View('/' . $user);
368
369
		$location = '';
370
		if ($timestamp) {
371
			$location = self::getLocation($user, $filename, $timestamp);
372
			if ($location === false) {
373
				\OCP\Util::writeLog('files_trashbin', 'trash bin database inconsistent!', \OCP\Util::ERROR);
374
			} else {
375
				// if location no longer exists, restore file in the root directory
376
				if ($location !== '/' &&
377
					(!$view->is_dir('files/' . $location) ||
378
						!$view->isCreatable('files/' . $location))
379
				) {
380
					$location = '';
381
				}
382
			}
383
		}
384
385
		// we need a  extension in case a file/dir with the same name already exists
386
		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
387
388
		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
389
		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
390
		if (!$view->file_exists($source)) {
391
			return false;
392
		}
393
		$mtime = $view->filemtime($source);
394
395
		// restore file
396
		$restoreResult = $view->rename($source, $target);
397
398
		// handle the restore result
399
		if ($restoreResult) {
400
			$fakeRoot = $view->getRoot();
401
			$view->chroot('/' . $user . '/files');
402
			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
403
			$view->chroot($fakeRoot);
404
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', array('filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
405
				'trashPath' => Filesystem::normalizePath($file)));
406
407
			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
408
409
			if ($timestamp) {
410
				$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
411
				$query->execute(array($user, $filename, $timestamp));
412
			}
413
414
			return true;
415
		}
416
417
		return false;
418
	}
419
420
	/**
421
	 * restore versions from trash bin
422
	 *
423
	 * @param View $view file view
424
	 * @param string $file complete path to file
425
	 * @param string $filename name of file once it was deleted
426
	 * @param string $uniqueFilename new file name to restore the file without overwriting existing files
427
	 * @param string $location location if file
428
	 * @param int $timestamp deletion time
429
	 * @return false|null
430
	 */
431
	private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
432
433
		if (\OCP\App::isEnabled('files_versions')) {
434
435
			$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...
436
			$rootView = new View('/');
437
438
			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
439
440
			list($owner, $ownerPath) = self::getUidAndFilename($target);
441
442
			// file has been deleted in between
443
			if (empty($ownerPath)) {
444
				return false;
445
			}
446
447
			if ($timestamp) {
448
				$versionedFile = $filename;
449
			} else {
450
				$versionedFile = $file;
451
			}
452
453
			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
454
				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
455
			} else if ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
456
				foreach ($versions as $v) {
457
					if ($timestamp) {
458
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
459
					} else {
460
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
461
					}
462
				}
463
			}
464
		}
465
	}
466
467
	/**
468
	 * delete all files from the trash
469
	 */
470
	public static function deleteAll() {
471
		$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...
472
		$view = new View('/' . $user);
473
		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
474
475
		// Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
476
		$filePaths = array();
477
		foreach($fileInfos as $fileInfo){
478
			$filePaths[] = $view->getRelativePath($fileInfo->getPath());
479
		}
480
		unset($fileInfos); // save memory
481
482
		// Bulk PreDelete-Hook
483
		\OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', array('paths' => $filePaths));
484
485
		// Single-File Hooks
486
		foreach($filePaths as $path){
487
			self::emitTrashbinPreDelete($path);
488
		}
489
490
		// actual file deletion
491
		$view->deleteAll('files_trashbin');
492
		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
493
		$query->execute(array($user));
494
495
		// Bulk PostDelete-Hook
496
		\OC_Hook::emit('\OCP\Trashbin', 'deleteAll', array('paths' => $filePaths));
497
498
		// Single-File Hooks
499
		foreach($filePaths as $path){
500
			self::emitTrashbinPostDelete($path);
501
		}
502
503
		$view->mkdir('files_trashbin');
504
		$view->mkdir('files_trashbin/files');
505
506
		return true;
507
	}
508
509
	/**
510
	 * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
511
	 * @param string $path
512
	 */
513
	protected static function emitTrashbinPreDelete($path){
514
		\OC_Hook::emit('\OCP\Trashbin', 'preDelete', array('path' => $path));
515
	}
516
517
	/**
518
	 * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
519
	 * @param string $path
520
	 */
521
	protected static function emitTrashbinPostDelete($path){
522
		\OC_Hook::emit('\OCP\Trashbin', 'delete', array('path' => $path));
523
	}
524
525
	/**
526
	 * delete file from trash bin permanently
527
	 *
528
	 * @param string $filename path to the file
529
	 * @param string $user
530
	 * @param int $timestamp of deletion time
531
	 *
532
	 * @return int size of deleted files
533
	 */
534
	public static function delete($filename, $user, $timestamp = null) {
535
		$view = new View('/' . $user);
536
		$size = 0;
537
538
		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...
539
			$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
540
			$query->execute(array($user, $filename, $timestamp));
541
			$file = $filename . '.d' . $timestamp;
542
		} else {
543
			$file = $filename;
544
		}
545
546
		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
547
548
		if ($view->is_dir('/files_trashbin/files/' . $file)) {
549
			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
550
		} else {
551
			$size += $view->filesize('/files_trashbin/files/' . $file);
552
		}
553
		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
554
		$view->unlink('/files_trashbin/files/' . $file);
555
		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
556
557
		return $size;
558
	}
559
560
	/**
561
	 * @param View $view
562
	 * @param string $file
563
	 * @param string $filename
564
	 * @param integer|null $timestamp
565
	 * @param string $user
566
	 * @return int
567
	 */
568
	private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
569
		$size = 0;
570
		if (\OCP\App::isEnabled('files_versions')) {
571
			if ($view->is_dir('files_trashbin/versions/' . $file)) {
572
				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
573
				$view->unlink('files_trashbin/versions/' . $file);
574
			} else if ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
575
				foreach ($versions as $v) {
576
					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...
577
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
578
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
579
					} else {
580
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
581
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
582
					}
583
				}
584
			}
585
		}
586
		return $size;
587
	}
588
589
	/**
590
	 * check to see whether a file exists in trashbin
591
	 *
592
	 * @param string $filename path to the file
593
	 * @param int $timestamp of deletion time
594
	 * @return bool true if file exists, otherwise false
595
	 */
596
	public static function file_exists($filename, $timestamp = null) {
597
		$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...
598
		$view = new View('/' . $user);
599
600
		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...
601
			$filename = $filename . '.d' . $timestamp;
602
		} else {
603
			$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...
604
		}
605
606
		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
607
		return $view->file_exists($target);
608
	}
609
610
	/**
611
	 * deletes used space for trash bin in db if user was deleted
612
	 *
613
	 * @param string $uid id of deleted user
614
	 * @return bool result of db delete operation
615
	 */
616
	public static function deleteUser($uid) {
617
		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
618
		return $query->execute(array($uid));
619
	}
620
621
	/**
622
	 * calculate remaining free space for trash bin
623
	 *
624
	 * @param integer $trashbinSize current size of the trash bin
625
	 * @param string $user
626
	 * @return int available free space for trash bin
627
	 */
628
	private static function calculateFreeSpace($trashbinSize, $user) {
629
		$softQuota = true;
630
		$userObject = \OC::$server->getUserManager()->get($user);
631
		if(is_null($userObject)) {
632
			return 0;
633
		}
634
		$quota = $userObject->getQuota();
635
		if ($quota === null || $quota === 'none') {
636
			$quota = Filesystem::free_space('/');
637
			$softQuota = false;
638
			// inf or unknown free space
639
			if ($quota < 0) {
640
				$quota = PHP_INT_MAX;
641
			}
642
		} else {
643
			$quota = \OCP\Util::computerFileSize($quota);
644
		}
645
646
		// calculate available space for trash bin
647
		// subtract size of files and current trash bin size from quota
648
		if ($softQuota) {
649
			$userFolder = \OC::$server->getUserFolder($user);
650
			if(is_null($userFolder)) {
651
				return 0;
652
			}
653
			$free = $quota - $userFolder->getSize(); // remaining free space for user
654
			if ($free > 0) {
655
				$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
656
			} else {
657
				$availableSpace = $free - $trashbinSize;
658
			}
659
		} else {
660
			$availableSpace = $quota;
661
		}
662
663
		return $availableSpace;
664
	}
665
666
	/**
667
	 * resize trash bin if necessary after a new file was added to ownCloud
668
	 *
669
	 * @param string $user user id
670
	 */
671
	public static function resizeTrash($user) {
672
673
		$size = self::getTrashbinSize($user);
674
675
		$freeSpace = self::calculateFreeSpace($size, $user);
676
677
		if ($freeSpace < 0) {
678
			self::scheduleExpire($user);
679
		}
680
	}
681
682
	/**
683
	 * clean up the trash bin
684
	 *
685
	 * @param string $user
686
	 */
687
	public static function expire($user) {
688
		$trashBinSize = self::getTrashbinSize($user);
689
		$availableSpace = self::calculateFreeSpace($trashBinSize, $user);
690
691
		$dirContent = Helper::getTrashFiles('/', $user, 'mtime');
692
693
		// delete all files older then $retention_obligation
694
		list($delSize, $count) = self::deleteExpiredFiles($dirContent, $user);
695
696
		$availableSpace += $delSize;
697
698
		// delete files from trash until we meet the trash bin size limit again
699
		self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
700
	}
701
702
	/**
703
	 * @param string $user
704
	 */
705
	private static function scheduleExpire($user) {
706
		// let the admin disable auto expire
707
		$application = new Application();
708
		$expiration = $application->getContainer()->query('Expiration');
709
		if ($expiration->isEnabled()) {
710
			\OC::$server->getCommandBus()->push(new Expire($user));
711
		}
712
	}
713
714
	/**
715
	 * if the size limit for the trash bin is reached, we delete the oldest
716
	 * files in the trash bin until we meet the limit again
717
	 *
718
	 * @param array $files
719
	 * @param string $user
720
	 * @param int $availableSpace available disc space
721
	 * @return int size of deleted files
722
	 */
723
	protected static function deleteFiles($files, $user, $availableSpace) {
724
		$application = new Application();
725
		$expiration = $application->getContainer()->query('Expiration');
726
		$size = 0;
727
728
		if ($availableSpace < 0) {
729
			foreach ($files as $file) {
730
				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
731
					$tmp = self::delete($file['name'], $user, $file['mtime']);
732
					\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);
733
					$availableSpace += $tmp;
734
					$size += $tmp;
735
				} else {
736
					break;
737
				}
738
			}
739
		}
740
		return $size;
741
	}
742
743
	/**
744
	 * delete files older then max storage time
745
	 *
746
	 * @param array $files list of files sorted by mtime
747
	 * @param string $user
748
	 * @return integer[] size of deleted files and number of deleted files
749
	 */
750
	public static function deleteExpiredFiles($files, $user) {
751
		$application = new Application();
752
		$expiration = $application->getContainer()->query('Expiration');
753
		$size = 0;
754
		$count = 0;
755
		foreach ($files as $file) {
756
			$timestamp = $file['mtime'];
757
			$filename = $file['name'];
758
			if ($expiration->isExpired($timestamp)) {
759
				$count++;
760
				$size += self::delete($filename, $user, $timestamp);
761
				\OC::$server->getLogger()->info(
762
					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
763
					['app' => 'files_trashbin']
764
				);
765
			} else {
766
				break;
767
			}
768
		}
769
770
		return array($size, $count);
771
	}
772
773
	/**
774
	 * recursive copy to copy a whole directory
775
	 *
776
	 * @param string $source source path, relative to the users files directory
777
	 * @param string $destination destination path relative to the users root directoy
778
	 * @param View $view file view for the users root directory
779
	 * @return int
780
	 * @throws Exceptions\CopyRecursiveException
781
	 */
782
	private static function copy_recursive($source, $destination, View $view) {
783
		$size = 0;
784
		if ($view->is_dir($source)) {
785
			$view->mkdir($destination);
786
			$view->touch($destination, $view->filemtime($source));
787
			foreach ($view->getDirectoryContent($source) as $i) {
788
				$pathDir = $source . '/' . $i['name'];
789
				if ($view->is_dir($pathDir)) {
790
					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
791
				} else {
792
					$size += $view->filesize($pathDir);
793
					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
794
					if (!$result) {
795
						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
796
					}
797
					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
798
				}
799
			}
800
		} else {
801
			$size += $view->filesize($source);
802
			$result = $view->copy($source, $destination);
803
			if (!$result) {
804
				throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
805
			}
806
			$view->touch($destination, $view->filemtime($source));
807
		}
808
		return $size;
809
	}
810
811
	/**
812
	 * find all versions which belong to the file we want to restore
813
	 *
814
	 * @param string $filename name of the file which should be restored
815
	 * @param int $timestamp timestamp when the file was deleted
816
	 * @return array
817
	 */
818
	private static function getVersionsFromTrash($filename, $timestamp, $user) {
819
		$view = new View('/' . $user . '/files_trashbin/versions');
820
		$versions = array();
821
822
		//force rescan of versions, local storage may not have updated the cache
823
		if (!self::$scannedVersions) {
824
			/** @var \OC\Files\Storage\Storage $storage */
825
			list($storage,) = $view->resolvePath('/');
826
			$storage->getScanner()->scan('files_trashbin/versions');
827
			self::$scannedVersions = true;
828
		}
829
830
		if ($timestamp) {
831
			// fetch for old versions
832
			$matches = $view->searchRaw($filename . '.v%.d' . $timestamp);
833
			$offset = -strlen($timestamp) - 2;
834
		} else {
835
			$matches = $view->searchRaw($filename . '.v%');
836
		}
837
838
		if (is_array($matches)) {
839
			foreach ($matches as $ma) {
840
				if ($timestamp) {
841
					$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...
842
					$versions[] = (end($parts));
843
				} else {
844
					$parts = explode('.v', $ma);
845
					$versions[] = (end($parts));
846
				}
847
			}
848
		}
849
		return $versions;
850
	}
851
852
	/**
853
	 * find unique extension for restored file if a file with the same name already exists
854
	 *
855
	 * @param string $location where the file should be restored
856
	 * @param string $filename name of the file
857
	 * @param View $view filesystem view relative to users root directory
858
	 * @return string with unique extension
859
	 */
860
	private static function getUniqueFilename($location, $filename, View $view) {
861
		$ext = pathinfo($filename, PATHINFO_EXTENSION);
862
		$name = pathinfo($filename, PATHINFO_FILENAME);
863
		$l = \OC::$server->getL10N('files_trashbin');
864
865
		$location = '/' . trim($location, '/');
866
867
		// if extension is not empty we set a dot in front of it
868
		if ($ext !== '') {
869
			$ext = '.' . $ext;
870
		}
871
872
		if ($view->file_exists('files' . $location . '/' . $filename)) {
873
			$i = 2;
874
			$uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
875
			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
876
				$uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
877
				$i++;
878
			}
879
880
			return $uniqueName;
881
		}
882
883
		return $filename;
884
	}
885
886
	/**
887
	 * get the size from a given root folder
888
	 *
889
	 * @param View $view file view on the root folder
890
	 * @return integer size of the folder
891
	 */
892
	private static function calculateSize($view) {
893
		$root = \OC::$server->getConfig()->getSystemValue('datadirectory') . $view->getAbsolutePath('');
894
		if (!file_exists($root)) {
895
			return 0;
896
		}
897
		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
898
		$size = 0;
899
900
		/**
901
		 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
902
		 * This bug is fixed in PHP 5.5.9 or before
903
		 * See #8376
904
		 */
905
		$iterator->rewind();
906
		while ($iterator->valid()) {
907
			$path = $iterator->current();
908
			$relpath = substr($path, strlen($root) - 1);
909
			if (!$view->is_dir($relpath)) {
910
				$size += $view->filesize($relpath);
911
			}
912
			$iterator->next();
913
		}
914
		return $size;
915
	}
916
917
	/**
918
	 * get current size of trash bin from a given user
919
	 *
920
	 * @param string $user user who owns the trash bin
921
	 * @return integer trash bin size
922
	 */
923
	private static function getTrashbinSize($user) {
924
		$view = new View('/' . $user);
925
		$fileInfo = $view->getFileInfo('/files_trashbin');
926
		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
927
	}
928
929
	/**
930
	 * register hooks
931
	 */
932
	public static function registerHooks() {
933
		// create storage wrapper on setup
934
		\OCP\Util::connectHook('OC_Filesystem', 'preSetup', 'OCA\Files_Trashbin\Storage', 'setupStorage');
935
		//Listen to delete user signal
936
		\OCP\Util::connectHook('OC_User', 'pre_deleteUser', 'OCA\Files_Trashbin\Hooks', 'deleteUser_hook');
937
		//Listen to post write hook
938
		\OCP\Util::connectHook('OC_Filesystem', 'post_write', 'OCA\Files_Trashbin\Hooks', 'post_write_hook');
939
		// pre and post-rename, disable trash logic for the copy+unlink case
940
		\OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Trashbin\Trashbin', 'ensureFileScannedHook');
941
		\OCP\Util::connectHook('OC_Filesystem', 'rename', 'OCA\Files_Trashbin\Storage', 'preRenameHook');
942
		\OCP\Util::connectHook('OC_Filesystem', 'post_rename', 'OCA\Files_Trashbin\Storage', 'postRenameHook');
943
	}
944
945
	/**
946
	 * check if trash bin is empty for a given user
947
	 *
948
	 * @param string $user
949
	 * @return bool
950
	 */
951
	public static function isEmpty($user) {
952
953
		$view = new View('/' . $user . '/files_trashbin');
954
		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
955
			while ($file = readdir($dh)) {
956
				if (!Filesystem::isIgnoredDir($file)) {
957
					return false;
958
				}
959
			}
960
		}
961
		return true;
962
	}
963
964
	/**
965
	 * @param $path
966
	 * @return string
967
	 */
968
	public static function preview_icon($path) {
969
		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...
970
	}
971
}
972