Completed
Push — master ( 921667...4f4873 )
by Thomas
10:52
created

Trashbin::resolvePrivateLink()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 14

Duplication

Lines 9
Ratio 40.91 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 4
nop 2
dl 9
loc 22
rs 8.9197
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Bart Visscher <[email protected]>
4
 * @author Bastien Ho <[email protected]>
5
 * @author Björn Schießle <[email protected]>
6
 * @author Florin Peter <[email protected]>
7
 * @author Georg Ehrke <[email protected]>
8
 * @author Jörn Friedrich Dreyer <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Qingping Hou <[email protected]>
12
 * @author Robin Appelman <[email protected]>
13
 * @author Robin McCorkell <[email protected]>
14
 * @author Roeland Jago Douma <[email protected]>
15
 * @author Sjors van der Pluijm <[email protected]>
16
 * @author Steven Bühner <[email protected]>
17
 * @author Thomas Müller <[email protected]>
18
 * @author Viktar Dubiniuk <[email protected]>
19
 * @author Vincent Petry <[email protected]>
20
 *
21
 * @copyright Copyright (c) 2018, ownCloud GmbH
22
 * @license AGPL-3.0
23
 *
24
 * This code is free software: you can redistribute it and/or modify
25
 * it under the terms of the GNU Affero General Public License, version 3,
26
 * as published by the Free Software Foundation.
27
 *
28
 * This program is distributed in the hope that it will be useful,
29
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31
 * GNU Affero General Public License for more details.
32
 *
33
 * You should have received a copy of the GNU Affero General Public License, version 3,
34
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
35
 *
36
 */
37
38
namespace OCA\Files_Trashbin;
39
40
use OC\Files\Filesystem;
41
use OC\Files\View;
42
use OCA\Files_Trashbin\AppInfo\Application;
43
use OCA\Files_Trashbin\Command\Expire;
44
use OCP\Files\NotFoundException;
45
use OCP\User;
46
use Symfony\Component\EventDispatcher\GenericEvent;
47
use OCP\Files\Folder;
48
use OCP\Files\IRootFolder;
49
use OCP\IURLGenerator;
50
use Symfony\Component\EventDispatcher\EventDispatcher;
51
52
class Trashbin {
53
54
	/**
55
	 * @var IURLGenerator
56
	 */
57
	private $urlGenerator;
58
59
	/**
60
	 * @var IRootFolder
61
	 */
62
	private $rootFolder;
63
64
	/**
65
	 * @var EventDispatcher
66
	 */
67
	private $eventDispatcher;
68
69
	public function __construct(
70
		IRootFolder $rootFolder,
71
		IUrlGenerator $urlGenerator,
72
		EventDispatcher $eventDispatcher
73
	) {
74
		$this->rootFolder = $rootFolder;
75
		$this->urlGenerator = $urlGenerator;
76
		$this->eventDispatcher = $eventDispatcher;
77
	}
78
79
	/**
80
	 * Whether versions have already be rescanned during this PHP request
81
	 *
82
	 * @var bool
83
	 */
84
	private static $scannedVersions = false;
85
86
	/**
87
	 * Ensure we don't need to scan the file during the move to trash
88
	 * by triggering the scan in the pre-hook
89
	 *
90
	 * @param array $params
91
	 */
92
	public static function ensureFileScannedHook($params) {
93
		try {
94
			self::getUidAndFilename($params['path']);
95
		} catch (NotFoundException $e) {
96
			// nothing to scan for non existing files
97
		}
98
	}
99
100
	/**
101
	 * get the UID of the owner of the file and the path to the file relative to
102
	 * owners files folder
103
	 *
104
	 * @param string $filename
105
	 * @return array
106
	 * @throws \OC\User\NoUserException
107
	 */
108
	public static function getUidAndFilename($filename) {
109
		$uid = Filesystem::getOwner($filename);
110
		$userManager = \OC::$server->getUserManager();
111
		// if the user with the UID doesn't exists, e.g. because the UID points
112
		// to a remote user with a federated cloud ID we use the current logged-in
113
		// user. We need a valid local user to move the file to the right trash bin
114
		if (!$userManager->userExists($uid)) {
115
			$uid = User::getUser();
116
		}
117
		if (!$uid) {
118
			// no owner, usually because of share link from ext storage
119
			return [null, null];
120
		}
121
		Filesystem::initMountPoints($uid);
122
		if ($uid != User::getUser()) {
123
			$info = Filesystem::getFileInfo($filename);
124
			$ownerView = new View('/' . $uid . '/files');
125
			try {
126
				$filename = $ownerView->getPath($info['fileid']);
127
			} catch (NotFoundException $e) {
128
				$filename = null;
129
			}
130
		}
131
		return [$uid, $filename];
132
	}
133
134
	/**
135
	 * get original location of files for user
136
	 *
137
	 * @param string $user
138
	 * @return array (filename => array (timestamp => original location))
139
	 */
140
	public static function getLocations($user) {
141
		$query = \OC_DB::prepare('SELECT `id`, `timestamp`, `location`'
142
			. ' FROM `*PREFIX*files_trash` WHERE `user`=?');
143
		$result = $query->execute([$user]);
144
		$array = [];
145
		while ($row = $result->fetchRow()) {
146
			if (isset($array[$row['id']])) {
147
				$array[$row['id']][$row['timestamp']] = $row['location'];
148
			} else {
149
				$array[$row['id']] = [$row['timestamp'] => $row['location']];
150
			}
151
		}
152
		return $array;
153
	}
154
155
	/**
156
	 * get original location of file
157
	 *
158
	 * @param string $user
159
	 * @param string $filename
160
	 * @param string $timestamp
161
	 * @return string original location
162
	 */
163
	public static function getLocation($user, $filename, $timestamp) {
164
		$query = \OC_DB::prepare('SELECT `location` FROM `*PREFIX*files_trash`'
165
			. ' WHERE `user`=? AND `id`=? AND `timestamp`=?');
166
		$result = $query->execute([$user, $filename, $timestamp])->fetchAll();
167
		if (isset($result[0]['location'])) {
168
			return $result[0]['location'];
169
		} else {
170
			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...
171
		}
172
	}
173
174
	/**
175
	 * Sets up the trashbin for the given user
176
	 *
177
	 * @param string $user user id
178
	 * @return bool true if trashbin is setup and usable, false otherwise
179
	 */
180
	private static function setUpTrash($user) {
181
		$view = new View('/' . $user);
182
		if (!$view->is_dir('files_trashbin')) {
183
			$view->mkdir('files_trashbin');
184
		}
185
186
		if (!$view->isUpdatable('files_trashbin')) {
187
			// no trashbin access or denied
188
			return false;
189
		}
190
191
		if (!$view->is_dir('files_trashbin/files')) {
192
			$view->mkdir('files_trashbin/files');
193
		}
194
		if (!$view->is_dir('files_trashbin/versions')) {
195
			$view->mkdir('files_trashbin/versions');
196
		}
197
		if (!$view->is_dir('files_trashbin/keys')) {
198
			$view->mkdir('files_trashbin/keys');
199
		}
200
201
		return true;
202
	}
203
204
205
	/**
206
	 * copy file to owners trash
207
	 *
208
	 * @param string $sourcePath
209
	 * @param string $owner
210
	 * @param string $targetPath
211
	 * @param $user
212
	 * @param integer $timestamp
213
	 */
214
	private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
215
		self::setUpTrash($owner);
216
217
		$targetFilename = basename($targetPath);
218
		$targetLocation = dirname($targetPath);
219
220
		$sourceFilename = basename($sourcePath);
221
222
		$view = new View('/');
223
224
		$target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
225
		$source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
226
		self::copy_recursive($source, $target, $view);
227
228
		if ($view->file_exists($target)) {
229
			self::insertTrashEntry($user, $targetFilename, $targetLocation, $timestamp);
230
			self::scheduleExpire($user);
231
		}
232
	}
233
234
	/**
235
	 * Make a backup of a file into the trashbin for the owner
236
	 *
237
	 * @param string $ownerPath path relative to the owner's home folder and containing "files"
238
	 * @param string $owner user id of the owner
239
	 * @param int $timestamp deletion timestamp
240
	 */
241
	public static function copyBackupForOwner($ownerPath, $owner, $timestamp) {
242
		self::setUpTrash($owner);
243
244
		$targetFilename = basename($ownerPath);
245
		$targetLocation = dirname($ownerPath);
246
		$source = $owner . '/files/' . ltrim($ownerPath, '/');
247
		$target = $owner . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
248
249
		$view = new View('/');
250
		self::copy_recursive($source, $target, $view);
251
252
		self::retainVersions($targetFilename, $owner, $ownerPath, $timestamp, null, true);
253
254
		if ($view->file_exists($target)) {
255
			self::insertTrashEntry($owner, $targetFilename, $targetLocation, $timestamp);
256
			self::scheduleExpire($owner);
257
		}
258
	}
259
260
	/**
261
	 *
262
	 */
263
	public static function insertTrashEntry($user, $targetFilename, $targetLocation, $timestamp) {
264
		$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
265
		$result = $query->execute([$targetFilename, $timestamp, $targetLocation, $user]);
266
		if (!$result) {
267
			\OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated for the files owner', \OCP\Util::ERROR);
268
		}
269
	}
270
271
272
	/**
273
	 * move file to the trash bin
274
	 *
275
	 * @param string $file_path path to the deleted file/directory relative to the files root directory
276
	 * @return bool
277
	 */
278
	public static function move2trash($file_path) {
279
		// get the user for which the filesystem is setup
280
		$root = Filesystem::getRoot();
281
		list(, $user) = explode('/', $root);
282
		list($owner, $ownerPath) = self::getUidAndFilename($file_path);
283
284
		// if no owner found (ex: ext storage + share link), will use the current user's trashbin then
285
		if (is_null($owner)) {
286
			$owner = $user;
287
			$ownerPath = $file_path;
288
		}
289
290
		$ownerView = new View('/' . $owner);
291
		// file has been deleted in between
292
		if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
293
			return true;
294
		}
295
296
		if (!self::setUpTrash($user)) {
297
			// trashbin not usable for user (ex: guest), switch to owner only
298
			$user = $owner;
299
			if (!self::setUpTrash($owner)) {
300
				// nothing to do as no trash is available anywheree
301
				return true;
302
			}
303
		}
304
		if ($owner !== $user) {
305
			// also setup for owner
306
			self::setUpTrash($owner);
307
		}
308
309
		$path_parts = pathinfo($ownerPath);
310
311
		$filename = $path_parts['basename'];
312
		$location = $path_parts['dirname'];
313
		$timestamp = time();
314
315
		$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
316
317
		/** @var \OC\Files\Storage\Storage $trashStorage */
318
		list($trashStorage, $trashInternalPath) = $ownerView->resolvePath($trashPath);
319
		/** @var \OC\Files\Storage\Storage $sourceStorage */
320
		list($sourceStorage, $sourceInternalPath) = $ownerView->resolvePath('/files/' . $ownerPath);
321
		try {
322
			$moveSuccessful = true;
323
			if ($trashStorage->file_exists($trashInternalPath)) {
324
				$trashStorage->unlink($trashInternalPath);
325
			}
326
			$trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
327
		} catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
328
			$moveSuccessful = false;
329
			if ($trashStorage->file_exists($trashInternalPath)) {
330
				$trashStorage->unlink($trashInternalPath);
331
			}
332
			\OCP\Util::writeLog('files_trashbin', 'Couldn\'t move ' . $file_path . ' to the trash bin', \OCP\Util::ERROR);
333
		}
334
335
		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
336
			if ($sourceStorage->is_dir($sourceInternalPath)) {
337
				$sourceStorage->rmdir($sourceInternalPath);
338
			} else {
339
				$sourceStorage->unlink($sourceInternalPath);
340
			}
341
			return false;
342
		}
343
344
		$trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
345
346
		if ($moveSuccessful) {
347
			$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
348
			$result = $query->execute([$filename, $timestamp, $location, $owner]);
349
			if (!$result) {
350
				\OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated', \OCP\Util::ERROR);
351
			}
352
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
353
				'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)]);
354
355
			self::retainVersions($filename, $owner, $ownerPath, $timestamp, $sourceStorage);
356
357
			// if owner !== user we need to also add a copy to the owners trash
358
			if ($user !== $owner) {
359
				self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
360
			}
361
		}
362
363
		self::scheduleExpire($user);
364
365
		// if owner !== user we also need to update the owners trash size
366
		if ($owner !== $user) {
367
			self::scheduleExpire($owner);
368
		}
369
370
		return $moveSuccessful;
371
	}
372
373
	/**
374
	 * Move file versions to trash so that they can be restored later
375
	 *
376
	 * @param string $filename of deleted file
377
	 * @param string $owner owner user id
378
	 * @param string $ownerPath path relative to the owner's home storage
379
	 * @param integer $timestamp when the file was deleted
380
	 * @param bool $forceCopy true to only make a copy of the versions into the trashbin
381
	 */
382
	private static function retainVersions($filename, $owner, $ownerPath, $timestamp, $sourceStorage = null, $forceCopy = false) {
383
		if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {
384
385
			$copyKeysResult = false;
386
387
			/**
388
			 * In case if encryption is enabled then we need to retain the keys which were
389
			 * deleted due to move operation to trashbin.
390
			 */
391
			if ($sourceStorage !== null) {
392
				$copyKeysResult = $sourceStorage->retainKeys($filename, $owner, $ownerPath, $timestamp, $sourceStorage);
393
			}
394
395
			$user = User::getUser();
396
			$rootView = new View('/');
397
398
			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
399 View Code Duplication
				if ($owner !== $user || $forceCopy) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

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

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

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

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

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

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

Loading history...
412
						self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
413
					}
414
				}
415
			}
416
417
			if ($copyKeysResult === true) {
418
				$sourceStorage->deleteAllFileKeys($filename);
419
			}
420
		}
421
	}
422
423
	/**
424
	 * Move a file or folder on storage level
425
	 *
426
	 * @param View $view
427
	 * @param string $source
428
	 * @param string $target
429
	 * @return bool
430
	 */
431
	private static function move(View $view, $source, $target) {
432
		/** @var \OC\Files\Storage\Storage $sourceStorage */
433
		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
434
		/** @var \OC\Files\Storage\Storage $targetStorage */
435
		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
436
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
437
438
		$result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
439
		if ($result) {
440
			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
441
		}
442
		return $result;
443
	}
444
445
	/**
446
	 * Copy a file or folder on storage level
447
	 *
448
	 * @param View $view
449
	 * @param string $source
450
	 * @param string $target
451
	 * @return bool
452
	 */
453
	private static function copy(View $view, $source, $target) {
454
		/** @var \OC\Files\Storage\Storage $sourceStorage */
455
		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
456
		/** @var \OC\Files\Storage\Storage $targetStorage */
457
		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
458
		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
459
460
		$result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
461
		if ($result) {
462
			$targetStorage->getUpdater()->update($targetInternalPath);
463
		}
464
		return $result;
465
	}
466
467
	/**
468
	 * Restore a file or folder from trash bin
469
	 *
470
	 * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
471
	 * including the timestamp suffix ".d12345678"
472
	 * @param string $filename name of the file/folder
473
	 * @param int $timestamp time when the file/folder was deleted
474
	 *
475
	 * @return bool true on success, false otherwise
476
	 */
477
	public static function restore($file, $filename, $timestamp) {
478
		$user = User::getUser();
479
		$view = new View('/' . $user);
480
481
		$location = '';
482
		if ($timestamp) {
483
			$location = self::getLocation($user, $filename, $timestamp);
484
			if ($location === false) {
485
				\OCP\Util::writeLog('files_trashbin', 'Original location of file ' . $filename .
486
					' not found in database, hence restoring into user\'s root instead', \OCP\Util::DEBUG);
487
			} else {
488
				// if location no longer exists, restore file in the root directory
489
				if ($location !== '/' &&
490
					(!$view->is_dir('files/' . $location) ||
491
						!$view->isCreatable('files/' . $location))
492
				) {
493
					$location = '';
494
				}
495
			}
496
		}
497
498
		// we need a  extension in case a file/dir with the same name already exists
499
		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
500
501
		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
502
		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
503
		if (!$view->file_exists($source)) {
504
			return false;
505
		}
506
		$mtime = $view->filemtime($source);
507
508
		// restore file
509
		$restoreResult = $view->rename($source, $target);
510
511
		// handle the restore result
512
		if ($restoreResult) {
513
			$fakeRoot = $view->getRoot();
514
			$view->chroot('/' . $user . '/files');
515
			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
516
			$view->chroot($fakeRoot);
517
			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
518
				'trashPath' => Filesystem::normalizePath($file)]);
519
520
			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
521
522
			if ($timestamp) {
523
				$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
524
				$query->execute([$user, $filename, $timestamp]);
525
			}
526
527
			return true;
528
		}
529
530
		return false;
531
	}
532
533
	/**
534
	 * restore versions from trash bin
535
	 *
536
	 * @param View $view file view
537
	 * @param string $file complete path to file
538
	 * @param string $filename name of file once it was deleted
539
	 * @param string $uniqueFilename new file name to restore the file without overwriting existing files
540
	 * @param string $location location if file
541
	 * @param int $timestamp deletion time
542
	 * @return false|null
543
	 */
544
	private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
545
546
		if (\OCP\App::isEnabled('files_versions')) {
547
548
			$user = User::getUser();
549
			$rootView = new View('/');
550
551
			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
552
553
			list($owner, $ownerPath) = self::getUidAndFilename($target);
554
555
			// file has been deleted in between
556
			if (empty($ownerPath)) {
557
				return false;
558
			}
559
560
			if ($timestamp) {
561
				$versionedFile = $filename;
562
			} else {
563
				$versionedFile = $file;
564
			}
565
566
			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
567
				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
568
			} else if ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
569
				foreach ($versions as $v) {
570
					if ($timestamp) {
571
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
572
					} else {
573
						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
574
					}
575
				}
576
			}
577
		}
578
	}
579
580
	/**
581
	 * delete all files from the trash
582
	 */
583
	public static function deleteAll() {
584
		$user = User::getUser();
585
		$view = new View('/' . $user);
586
		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
587
588
		// Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
589
		$filePaths = [];
590
		foreach($fileInfos as $fileInfo){
591
			$filePaths[] = $view->getRelativePath($fileInfo->getPath());
592
		}
593
		unset($fileInfos); // save memory
594
595
		// Bulk PreDelete-Hook
596
		\OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
597
598
		// Single-File Hooks
599
		foreach($filePaths as $path){
600
			self::emitTrashbinPreDelete($path);
601
		}
602
603
		// actual file deletion
604
		$view->deleteAll('files_trashbin');
605
		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
606
		$query->execute([$user]);
607
608
		// Bulk PostDelete-Hook
609
		\OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
610
611
		// Single-File Hooks
612
		foreach($filePaths as $path){
613
			self::emitTrashbinPostDelete($path);
614
		}
615
616
		$view->mkdir('files_trashbin');
617
		$view->mkdir('files_trashbin/files');
618
619
		return true;
620
	}
621
622
	/**
623
	 * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
624
	 * @param string $path
625
	 */
626
	protected static function emitTrashbinPreDelete($path){
627
		\OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
628
	}
629
630
	/**
631
	 * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
632
	 * @param string $path
633
	 */
634
	protected static function emitTrashbinPostDelete($path){
635
		\OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
636
	}
637
638
	/**
639
	 * delete file from trash bin permanently
640
	 *
641
	 * @param string $filename path to the file
642
	 * @param string $user
643
	 * @param int $timestamp of deletion time
644
	 *
645
	 * @return int size of deleted files
646
	 */
647
	public static function delete($filename, $user, $timestamp = null) {
648
		$view = new View('/' . $user);
649
		$size = 0;
650
651
		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...
652
			$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
653
			$query->execute([$user, $filename, $timestamp]);
654
			$file = $filename . '.d' . $timestamp;
655
		} else {
656
			$file = $filename;
657
		}
658
659
		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
660
661
		if ($view->is_dir('/files_trashbin/files/' . $file)) {
662
			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
663
		} else {
664
			$size += $view->filesize('/files_trashbin/files/' . $file);
665
		}
666
		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
667
		$view->unlink('/files_trashbin/files/' . $file);
668
		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
669
670
		return $size;
671
	}
672
673
	/**
674
	 * @param View $view
675
	 * @param string $file
676
	 * @param string $filename
677
	 * @param integer|null $timestamp
678
	 * @param string $user
679
	 * @return int
680
	 */
681
	private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
682
		$size = 0;
683
		if (\OCP\App::isEnabled('files_versions')) {
684
			if ($view->is_dir('files_trashbin/versions/' . $file)) {
685
				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
686
				$view->unlink('files_trashbin/versions/' . $file);
687
			} else if ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
688
				foreach ($versions as $v) {
689
					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...
690
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
691
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
692
					} else {
693
						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
694
						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
695
					}
696
				}
697
			}
698
		}
699
		return $size;
700
	}
701
702
	/**
703
	 * check to see whether a file exists in trashbin
704
	 *
705
	 * @param string $filename path to the file
706
	 * @param int $timestamp of deletion time
707
	 * @return bool true if file exists, otherwise false
708
	 */
709
	public static function file_exists($filename, $timestamp = null) {
710
		$user = User::getUser();
711
		$view = new View('/' . $user);
712
713
		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...
714
			$filename = $filename . '.d' . $timestamp;
715
		}
716
717
		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
718
		return $view->file_exists($target);
719
	}
720
721
	/**
722
	 * deletes used space for trash bin in db if user was deleted
723
	 *
724
	 * @param string $uid id of deleted user
725
	 * @return bool result of db delete operation
726
	 */
727
	public static function deleteUser($uid) {
728
		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
729
		return $query->execute([$uid]);
730
	}
731
732
	/**
733
	 * resize trash bin if necessary after a new file was added to ownCloud
734
	 *
735
	 * @param string $user user id
736
	 */
737
	public static function resizeTrash($user) {
738
		$size = self::getTrashbinSize($user);
739
		$freeSpace = self::getQuota()->calculateFreeSpace($size, $user);
740
741
		if ($freeSpace < 0) {
742
			self::scheduleExpire($user);
743
		}
744
	}
745
746
	/**
747
	 * clean up the trash bin
748
	 *
749
	 * @param string $user
750
	 */
751
	public static function expire($user) {
752
		$trashBinSize = self::getTrashbinSize($user);
753
		$availableSpace = self::getQuota()->calculateFreeSpace($trashBinSize, $user);
754
755
		$dirContent = Helper::getTrashFiles('/', $user, 'mtime');
756
757
		// delete all files older then $retention_obligation
758
		list($delSize, $count) = self::deleteExpiredFiles($dirContent, $user);
759
760
		$availableSpace += $delSize;
761
762
		// delete files from trash until we meet the trash bin size limit again
763
		self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
764
	}
765
766
	/**
767
	 * @return Quota
768
	 */
769
	protected static function getQuota() {
770
		$application = new Application();
771
		return $application->getContainer()->query('Quota');
772
	}
773
774
	/**
775
	 * @param string $user
776
	 */
777
	private static function scheduleExpire($user) {
778
		// let the admin disable auto expire
779
		$application = new Application();
780
		$expiration = $application->getContainer()->query('Expiration');
781
		if ($expiration->isEnabled()) {
782
			\OC::$server->getCommandBus()->push(new Expire($user));
783
		}
784
	}
785
786
	/**
787
	 * if the size limit for the trash bin is reached, we delete the oldest
788
	 * files in the trash bin until we meet the limit again
789
	 *
790
	 * @param array $files
791
	 * @param string $user
792
	 * @param int $availableSpace available disc space
793
	 * @return int size of deleted files
794
	 */
795
	protected static function deleteFiles($files, $user, $availableSpace) {
796
		$application = new Application();
797
		$expiration = $application->getContainer()->query('Expiration');
798
		$size = 0;
799
800
		if ($availableSpace < 0) {
801
			foreach ($files as $file) {
802
				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
803
					$tmp = self::delete($file['name'], $user, $file['mtime']);
804
					$message = sprintf(
805
						'remove "%s" (%dB) to meet the limit of trash bin size (%d%% of available quota)',
806
						$file['name'],
807
						$tmp,
808
						self::getQuota()->getPurgeLimit()
809
					);
810
					\OCP\Util::writeLog('files_trashbin', $message, \OCP\Util::INFO);
811
					$availableSpace += $tmp;
812
					$size += $tmp;
813
				} else {
814
					break;
815
				}
816
			}
817
		}
818
		return $size;
819
	}
820
821
	/**
822
	 * delete files older then max storage time
823
	 *
824
	 * @param array $files list of files sorted by mtime
825
	 * @param string $user
826
	 * @return integer[] size of deleted files and number of deleted files
827
	 */
828
	public static function deleteExpiredFiles($files, $user) {
829
		$application = new Application();
830
		$expiration = $application->getContainer()->query('Expiration');
831
		$size = 0;
832
		$count = 0;
833
		foreach ($files as $file) {
834
			$timestamp = $file['mtime'];
835
			$filename = $file['name'];
836
			if ($expiration->isExpired($timestamp)) {
837
				$count++;
838
				$size += self::delete($filename, $user, $timestamp);
839
				\OC::$server->getLogger()->info(
840
					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
841
					['app' => 'files_trashbin']
842
				);
843
			} else {
844
				break;
845
			}
846
		}
847
848
		return [$size, $count];
849
	}
850
851
	/**
852
	 * recursive copy to copy a whole directory
853
	 *
854
	 * @param string $source source path, relative to the users files directory
855
	 * @param string $destination destination path relative to the users root directoy
856
	 * @param View $view file view for the users root directory
857
	 * @return int
858
	 * @throws Exceptions\CopyRecursiveException
859
	 */
860
	private static function copy_recursive($source, $destination, View $view) {
861
		$size = 0;
862
		if ($view->is_dir($source)) {
863
			$view->mkdir($destination);
864
			$view->touch($destination, $view->filemtime($source));
865
			foreach ($view->getDirectoryContent($source) as $i) {
866
				$pathDir = $source . '/' . $i['name'];
867
				if ($view->is_dir($pathDir)) {
868
					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
869
				} else {
870
					$size += $view->filesize($pathDir);
871
					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
872
					if (!$result) {
873
						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
874
					}
875
					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
876
				}
877
			}
878
		} else {
879
			$size += $view->filesize($source);
880
			$result = $view->copy($source, $destination);
881
			if (!$result) {
882
				throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
883
			}
884
			$view->touch($destination, $view->filemtime($source));
885
		}
886
		return $size;
887
	}
888
889
	/**
890
	 * find all versions which belong to the file we want to restore
891
	 *
892
	 * @param string $filename name of the file which should be restored
893
	 * @param int $timestamp timestamp when the file was deleted
894
	 * @return array
895
	 */
896
	private static function getVersionsFromTrash($filename, $timestamp, $user) {
897
		$view = new View('/' . $user . '/files_trashbin/versions');
898
		$versions = [];
899
900
		//force rescan of versions, local storage may not have updated the cache
901
		if (!self::$scannedVersions) {
902
			/** @var \OC\Files\Storage\Storage $storage */
903
			list($storage,) = $view->resolvePath('/');
904
			$storage->getScanner()->scan('files_trashbin/versions');
905
			self::$scannedVersions = true;
906
		}
907
908
		if ($timestamp) {
909
			// fetch for old versions
910
			$matches = $view->searchRaw($filename . '.v%.d' . $timestamp);
911
			$offset = -strlen($timestamp) - 2;
912
		} else {
913
			$matches = $view->searchRaw($filename . '.v%');
914
		}
915
916
		if (is_array($matches)) {
917
			foreach ($matches as $ma) {
918
				if ($timestamp) {
919
					$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...
920
					$versions[] = (end($parts));
921
				} else {
922
					$parts = explode('.v', $ma);
923
					$versions[] = (end($parts));
924
				}
925
			}
926
		}
927
		return $versions;
928
	}
929
930
	/**
931
	 * find unique extension for restored file if a file with the same name already exists
932
	 *
933
	 * @param string $location where the file should be restored
934
	 * @param string $filename name of the file
935
	 * @param View $view filesystem view relative to users root directory
936
	 * @return string with unique extension
937
	 */
938
	private static function getUniqueFilename($location, $filename, View $view) {
939
		$ext = pathinfo($filename, PATHINFO_EXTENSION);
940
		$name = pathinfo($filename, PATHINFO_FILENAME);
941
		$l = \OC::$server->getL10N('files_trashbin');
942
943
		$location = '/' . trim($location, '/');
944
945
		// if extension is not empty we set a dot in front of it
946
		if ($ext !== '') {
947
			$ext = '.' . $ext;
948
		}
949
950
		if ($view->file_exists('files' . $location . '/' . $filename)) {
951
			$i = 2;
952
			$uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
953
			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
954
				$uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
955
				$i++;
956
			}
957
958
			return $uniqueName;
959
		}
960
961
		return $filename;
962
	}
963
964
	/**
965
	 * get the size from a given root folder
966
	 *
967
	 * @param View $view file view on the root folder
968
	 * @return integer size of the folder
969
	 */
970
	private static function calculateSize($view) {
971
		$root = \OC::$server->getConfig()->getSystemValue('datadirectory') . $view->getAbsolutePath('');
972
		if (!file_exists($root)) {
973
			return 0;
974
		}
975
		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
976
		$size = 0;
977
978
		/**
979
		 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
980
		 * This bug is fixed in PHP 5.5.9 or before
981
		 * See #8376
982
		 */
983
		$iterator->rewind();
984
		while ($iterator->valid()) {
985
			$path = $iterator->current();
986
			$relpath = substr($path, strlen($root) - 1);
987
			if (!$view->is_dir($relpath)) {
988
				$size += $view->filesize($relpath);
989
			}
990
			$iterator->next();
991
		}
992
		return $size;
993
	}
994
995
	/**
996
	 * get current size of trash bin from a given user
997
	 *
998
	 * @param string $user user who owns the trash bin
999
	 * @return integer trash bin size
1000
	 */
1001
	private static function getTrashbinSize($user) {
1002
		$view = new View('/' . $user);
1003
		$fileInfo = $view->getFileInfo('/files_trashbin');
1004
		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1005
	}
1006
1007
	/**
1008
	 * Register listeners
1009
	 */
1010
	public function registerListeners() {
1011
		$this->eventDispatcher->addListener(
1012
			'files.resolvePrivateLink',
1013
			function(GenericEvent $event) {
1014
				$uid = $event->getArgument('uid');
1015
				$fileId = $event->getArgument('fileid');
1016
1017
				$link = $this->resolvePrivateLink($uid, $fileId);
1018
1019
				if ($link !== null) {
1020
					$event->setArgument('resolvedWebLink', $link);
1021
				}
1022
			}
1023
		);
1024
	}
1025
1026
	/**
1027
	 * register hooks
1028
	 */
1029
	public static function registerHooks() {
1030
		// create storage wrapper on setup
1031
		\OCP\Util::connectHook('OC_Filesystem', 'preSetup', 'OCA\Files_Trashbin\Storage', 'setupStorage');
1032
		//Listen to delete user signal
1033
		\OCP\Util::connectHook('OC_User', 'pre_deleteUser', 'OCA\Files_Trashbin\Hooks', 'deleteUser_hook');
1034
		//Listen to post write hook
1035
		\OCP\Util::connectHook('OC_Filesystem', 'post_write', 'OCA\Files_Trashbin\Hooks', 'post_write_hook');
1036
		// pre and post-rename, disable trash logic for the copy+unlink case
1037
		\OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Trashbin\Trashbin', 'ensureFileScannedHook');
1038
		\OCP\Util::connectHook('OC_Filesystem', 'rename', 'OCA\Files_Trashbin\Storage', 'preRenameHook');
1039
		\OCP\Util::connectHook('OC_Filesystem', 'post_rename', 'OCA\Files_Trashbin\Storage', 'postRenameHook');
1040
	}
1041
1042
	/**
1043
	 * Resolves web URL that points to the trashbin view of the given file
1044
	 *
1045
	 * @param string $uid user id
1046
	 * @param string $fileId file id
1047
	 * @return string|null view URL or null if the file is not found or not accessible
1048
	 */
1049
	public function resolvePrivateLink($uid, $fileId) {
1050
		if ($this->rootFolder->nodeExists($uid . '/files_trashbin/files/')) {
1051
			$baseFolder = $this->rootFolder->get($uid . '/files_trashbin/files/');
1052
			$files = $baseFolder->getById($fileId);
1053
			if (!empty($files)) {
1054
				$params['view'] = 'trashbin';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$params was never initialized. Although not strictly required by PHP, it is generally a good practice to add $params = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1055
				$file = current($files);
1056 View Code Duplication
				if ($file instanceof Folder) {
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...
1057
					// set the full path to enter the folder
1058
					$params['dir'] = $baseFolder->getRelativePath($file->getPath());
1059
				} else {
1060
					// set parent path as dir
1061
					$params['dir'] = $baseFolder->getRelativePath($file->getParent()->getPath());
1062
					// and scroll to the entry
1063
					$params['scrollto'] = $file->getName();
1064
				}
1065
				return $this->urlGenerator->linkToRoute('files.view.index', $params);
1066
			}
1067
		}
1068
1069
		return null;
1070
	}
1071
1072
	/**
1073
	 * check if trash bin is empty for a given user
1074
	 *
1075
	 * @param string $user
1076
	 * @return bool
1077
	 */
1078
	public static function isEmpty($user) {
1079
1080
		$view = new View('/' . $user . '/files_trashbin');
1081
		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1082
			while ($file = readdir($dh)) {
1083
				if (!Filesystem::isIgnoredDir($file)) {
1084
					return false;
1085
				}
1086
			}
1087
		}
1088
		return true;
1089
	}
1090
1091
	/**
1092
	 * @param $path
1093
	 * @return string
1094
	 */
1095
	public static function preview_icon($path) {
1096
		return \OCP\Util::linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]);
1097
	}
1098
}
1099