Passed
Push — master ( 9979b6...02fa6a )
by Morris
17:32 queued 10s
created
apps/files_trashbin/lib/Trashbin.php 2 patches
Indentation   +1024 added lines, -1024 removed lines patch added patch discarded remove patch
@@ -63,1028 +63,1028 @@
 block discarded – undo
63 63
 
64 64
 class Trashbin {
65 65
 
66
-	// unit: percentage; 50% of available disk space/quota
67
-	public const DEFAULTMAXSIZE = 50;
68
-
69
-	/**
70
-	 * Whether versions have already be rescanned during this PHP request
71
-	 *
72
-	 * @var bool
73
-	 */
74
-	private static $scannedVersions = false;
75
-
76
-	/**
77
-	 * Ensure we don't need to scan the file during the move to trash
78
-	 * by triggering the scan in the pre-hook
79
-	 *
80
-	 * @param array $params
81
-	 */
82
-	public static function ensureFileScannedHook($params) {
83
-		try {
84
-			self::getUidAndFilename($params['path']);
85
-		} catch (NotFoundException $e) {
86
-			// nothing to scan for non existing files
87
-		}
88
-	}
89
-
90
-	/**
91
-	 * get the UID of the owner of the file and the path to the file relative to
92
-	 * owners files folder
93
-	 *
94
-	 * @param string $filename
95
-	 * @return array
96
-	 * @throws \OC\User\NoUserException
97
-	 */
98
-	public static function getUidAndFilename($filename) {
99
-		$uid = Filesystem::getOwner($filename);
100
-		$userManager = \OC::$server->getUserManager();
101
-		// if the user with the UID doesn't exists, e.g. because the UID points
102
-		// to a remote user with a federated cloud ID we use the current logged-in
103
-		// user. We need a valid local user to move the file to the right trash bin
104
-		if (!$userManager->userExists($uid)) {
105
-			$uid = User::getUser();
106
-		}
107
-		if (!$uid) {
108
-			// no owner, usually because of share link from ext storage
109
-			return [null, null];
110
-		}
111
-		Filesystem::initMountPoints($uid);
112
-		if ($uid !== User::getUser()) {
113
-			$info = Filesystem::getFileInfo($filename);
114
-			$ownerView = new View('/' . $uid . '/files');
115
-			try {
116
-				$filename = $ownerView->getPath($info['fileid']);
117
-			} catch (NotFoundException $e) {
118
-				$filename = null;
119
-			}
120
-		}
121
-		return [$uid, $filename];
122
-	}
123
-
124
-	/**
125
-	 * get original location of files for user
126
-	 *
127
-	 * @param string $user
128
-	 * @return array (filename => array (timestamp => original location))
129
-	 */
130
-	public static function getLocations($user) {
131
-		$query = \OC_DB::prepare('SELECT `id`, `timestamp`, `location`'
132
-			. ' FROM `*PREFIX*files_trash` WHERE `user`=?');
133
-		$result = $query->execute([$user]);
134
-		$array = [];
135
-		while ($row = $result->fetchRow()) {
136
-			if (isset($array[$row['id']])) {
137
-				$array[$row['id']][$row['timestamp']] = $row['location'];
138
-			} else {
139
-				$array[$row['id']] = [$row['timestamp'] => $row['location']];
140
-			}
141
-		}
142
-		return $array;
143
-	}
144
-
145
-	/**
146
-	 * get original location of file
147
-	 *
148
-	 * @param string $user
149
-	 * @param string $filename
150
-	 * @param string $timestamp
151
-	 * @return string original location
152
-	 */
153
-	public static function getLocation($user, $filename, $timestamp) {
154
-		$query = \OC_DB::prepare('SELECT `location` FROM `*PREFIX*files_trash`'
155
-			. ' WHERE `user`=? AND `id`=? AND `timestamp`=?');
156
-		$result = $query->execute([$user, $filename, $timestamp])->fetchAll();
157
-		if (isset($result[0]['location'])) {
158
-			return $result[0]['location'];
159
-		} else {
160
-			return false;
161
-		}
162
-	}
163
-
164
-	private static function setUpTrash($user) {
165
-		$view = new View('/' . $user);
166
-		if (!$view->is_dir('files_trashbin')) {
167
-			$view->mkdir('files_trashbin');
168
-		}
169
-		if (!$view->is_dir('files_trashbin/files')) {
170
-			$view->mkdir('files_trashbin/files');
171
-		}
172
-		if (!$view->is_dir('files_trashbin/versions')) {
173
-			$view->mkdir('files_trashbin/versions');
174
-		}
175
-		if (!$view->is_dir('files_trashbin/keys')) {
176
-			$view->mkdir('files_trashbin/keys');
177
-		}
178
-	}
179
-
180
-
181
-	/**
182
-	 * copy file to owners trash
183
-	 *
184
-	 * @param string $sourcePath
185
-	 * @param string $owner
186
-	 * @param string $targetPath
187
-	 * @param $user
188
-	 * @param integer $timestamp
189
-	 */
190
-	private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
191
-		self::setUpTrash($owner);
192
-
193
-		$targetFilename = basename($targetPath);
194
-		$targetLocation = dirname($targetPath);
195
-
196
-		$sourceFilename = basename($sourcePath);
197
-
198
-		$view = new View('/');
199
-
200
-		$target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
201
-		$source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
202
-		$free = $view->free_space($target);
203
-		$isUnknownOrUnlimitedFreeSpace = $free < 0;
204
-		$isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
205
-		if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) {
206
-			self::copy_recursive($source, $target, $view);
207
-		}
208
-
209
-
210
-		if ($view->file_exists($target)) {
211
-			$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
212
-			$result = $query->execute([$targetFilename, $timestamp, $targetLocation, $user]);
213
-			if (!$result) {
214
-				\OC::$server->getLogger()->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']);
215
-			}
216
-		}
217
-	}
218
-
219
-
220
-	/**
221
-	 * move file to the trash bin
222
-	 *
223
-	 * @param string $file_path path to the deleted file/directory relative to the files root directory
224
-	 * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
225
-	 *
226
-	 * @return bool
227
-	 */
228
-	public static function move2trash($file_path, $ownerOnly = false) {
229
-		// get the user for which the filesystem is setup
230
-		$root = Filesystem::getRoot();
231
-		[, $user] = explode('/', $root);
232
-		[$owner, $ownerPath] = self::getUidAndFilename($file_path);
233
-
234
-		// if no owner found (ex: ext storage + share link), will use the current user's trashbin then
235
-		if (is_null($owner)) {
236
-			$owner = $user;
237
-			$ownerPath = $file_path;
238
-		}
239
-
240
-		$ownerView = new View('/' . $owner);
241
-		// file has been deleted in between
242
-		if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
243
-			return true;
244
-		}
245
-
246
-		self::setUpTrash($user);
247
-		if ($owner !== $user) {
248
-			// also setup for owner
249
-			self::setUpTrash($owner);
250
-		}
251
-
252
-		$path_parts = pathinfo($ownerPath);
253
-
254
-		$filename = $path_parts['basename'];
255
-		$location = $path_parts['dirname'];
256
-		/** @var ITimeFactory $timeFactory */
257
-		$timeFactory = \OC::$server->query(ITimeFactory::class);
258
-		$timestamp = $timeFactory->getTime();
259
-
260
-		$lockingProvider = \OC::$server->getLockingProvider();
261
-
262
-		// disable proxy to prevent recursive calls
263
-		$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
264
-		$gotLock = false;
265
-
266
-		while (!$gotLock) {
267
-			try {
268
-				/** @var \OC\Files\Storage\Storage $trashStorage */
269
-				[$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath);
270
-
271
-				$trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
272
-				$gotLock = true;
273
-			} catch (LockedException $e) {
274
-				// a file with the same name is being deleted concurrently
275
-				// nudge the timestamp a bit to resolve the conflict
276
-
277
-				$timestamp = $timestamp + 1;
278
-
279
-				$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
280
-			}
281
-		}
282
-
283
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
284
-		[$sourceStorage, $sourceInternalPath] = $ownerView->resolvePath('/files/' . $ownerPath);
285
-
286
-
287
-		if ($trashStorage->file_exists($trashInternalPath)) {
288
-			$trashStorage->unlink($trashInternalPath);
289
-		}
290
-
291
-		$config = \OC::$server->getConfig();
292
-		$systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
293
-		$userTrashbinSize = (int)$config->getUserValue($owner, 'files_trashbin', 'trashbin_size', '-1');
294
-		$configuredTrashbinSize = ($userTrashbinSize < 0) ? $systemTrashbinSize : $userTrashbinSize;
295
-		if ($configuredTrashbinSize >= 0 && $sourceStorage->filesize($sourceInternalPath) >= $configuredTrashbinSize) {
296
-			return false;
297
-		}
298
-
299
-		$trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
300
-
301
-		try {
302
-			$moveSuccessful = true;
303
-
304
-			// when moving within the same object store, the cache update done above is enough to move the file
305
-			if (!($trashStorage->instanceOfStorage(ObjectStoreStorage::class) && $trashStorage->getId() === $sourceStorage->getId())) {
306
-				$trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
307
-			}
308
-		} catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
309
-			$moveSuccessful = false;
310
-			if ($trashStorage->file_exists($trashInternalPath)) {
311
-				$trashStorage->unlink($trashInternalPath);
312
-			}
313
-			\OC::$server->getLogger()->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
314
-		}
315
-
316
-		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
317
-			if ($sourceStorage->is_dir($sourceInternalPath)) {
318
-				$sourceStorage->rmdir($sourceInternalPath);
319
-			} else {
320
-				$sourceStorage->unlink($sourceInternalPath);
321
-			}
322
-
323
-			if ($sourceStorage->file_exists($sourceInternalPath)) {
324
-				// undo the cache move
325
-				$sourceStorage->getUpdater()->renameFromStorage($trashStorage, $trashInternalPath, $sourceInternalPath);
326
-			} else {
327
-				$trashStorage->getUpdater()->remove($trashInternalPath);
328
-			}
329
-			return false;
330
-		}
331
-
332
-		if ($moveSuccessful) {
333
-			$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
334
-			$result = $query->execute([$filename, $timestamp, $location, $owner]);
335
-			if (!$result) {
336
-				\OC::$server->getLogger()->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
337
-			}
338
-			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
339
-				'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)]);
340
-
341
-			self::retainVersions($filename, $owner, $ownerPath, $timestamp);
342
-
343
-			// if owner !== user we need to also add a copy to the users trash
344
-			if ($user !== $owner && $ownerOnly === false) {
345
-				self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
346
-			}
347
-		}
348
-
349
-		$trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
350
-
351
-		self::scheduleExpire($user);
352
-
353
-		// if owner !== user we also need to update the owners trash size
354
-		if ($owner !== $user) {
355
-			self::scheduleExpire($owner);
356
-		}
357
-
358
-		return $moveSuccessful;
359
-	}
360
-
361
-	/**
362
-	 * Move file versions to trash so that they can be restored later
363
-	 *
364
-	 * @param string $filename of deleted file
365
-	 * @param string $owner owner user id
366
-	 * @param string $ownerPath path relative to the owner's home storage
367
-	 * @param integer $timestamp when the file was deleted
368
-	 */
369
-	private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
370
-		if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {
371
-			$user = User::getUser();
372
-			$rootView = new View('/');
373
-
374
-			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
375
-				if ($owner !== $user) {
376
-					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
377
-				}
378
-				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
379
-			} elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
380
-				foreach ($versions as $v) {
381
-					if ($owner !== $user) {
382
-						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
383
-					}
384
-					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
385
-				}
386
-			}
387
-		}
388
-	}
389
-
390
-	/**
391
-	 * Move a file or folder on storage level
392
-	 *
393
-	 * @param View $view
394
-	 * @param string $source
395
-	 * @param string $target
396
-	 * @return bool
397
-	 */
398
-	private static function move(View $view, $source, $target) {
399
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
400
-		[$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
401
-		/** @var \OC\Files\Storage\Storage $targetStorage */
402
-		[$targetStorage, $targetInternalPath] = $view->resolvePath($target);
403
-		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
404
-
405
-		$result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
406
-		if ($result) {
407
-			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
408
-		}
409
-		return $result;
410
-	}
411
-
412
-	/**
413
-	 * Copy a file or folder on storage level
414
-	 *
415
-	 * @param View $view
416
-	 * @param string $source
417
-	 * @param string $target
418
-	 * @return bool
419
-	 */
420
-	private static function copy(View $view, $source, $target) {
421
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
422
-		[$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
423
-		/** @var \OC\Files\Storage\Storage $targetStorage */
424
-		[$targetStorage, $targetInternalPath] = $view->resolvePath($target);
425
-		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
426
-
427
-		$result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
428
-		if ($result) {
429
-			$targetStorage->getUpdater()->update($targetInternalPath);
430
-		}
431
-		return $result;
432
-	}
433
-
434
-	/**
435
-	 * Restore a file or folder from trash bin
436
-	 *
437
-	 * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
438
-	 * including the timestamp suffix ".d12345678"
439
-	 * @param string $filename name of the file/folder
440
-	 * @param int $timestamp time when the file/folder was deleted
441
-	 *
442
-	 * @return bool true on success, false otherwise
443
-	 */
444
-	public static function restore($file, $filename, $timestamp) {
445
-		$user = User::getUser();
446
-		$view = new View('/' . $user);
447
-
448
-		$location = '';
449
-		if ($timestamp) {
450
-			$location = self::getLocation($user, $filename, $timestamp);
451
-			if ($location === false) {
452
-				\OC::$server->getLogger()->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
453
-			} else {
454
-				// if location no longer exists, restore file in the root directory
455
-				if ($location !== '/' &&
456
-					(!$view->is_dir('files/' . $location) ||
457
-						!$view->isCreatable('files/' . $location))
458
-				) {
459
-					$location = '';
460
-				}
461
-			}
462
-		}
463
-
464
-		// we need a  extension in case a file/dir with the same name already exists
465
-		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
466
-
467
-		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
468
-		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
469
-		if (!$view->file_exists($source)) {
470
-			return false;
471
-		}
472
-		$mtime = $view->filemtime($source);
473
-
474
-		// restore file
475
-		if (!$view->isCreatable(dirname($target))) {
476
-			throw new NotPermittedException("Can't restore trash item because the target folder is not writable");
477
-		}
478
-		$restoreResult = $view->rename($source, $target);
479
-
480
-		// handle the restore result
481
-		if ($restoreResult) {
482
-			$fakeRoot = $view->getRoot();
483
-			$view->chroot('/' . $user . '/files');
484
-			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
485
-			$view->chroot($fakeRoot);
486
-			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
487
-				'trashPath' => Filesystem::normalizePath($file)]);
488
-
489
-			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
490
-
491
-			if ($timestamp) {
492
-				$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
493
-				$query->execute([$user, $filename, $timestamp]);
494
-			}
495
-
496
-			return true;
497
-		}
498
-
499
-		return false;
500
-	}
501
-
502
-	/**
503
-	 * restore versions from trash bin
504
-	 *
505
-	 * @param View $view file view
506
-	 * @param string $file complete path to file
507
-	 * @param string $filename name of file once it was deleted
508
-	 * @param string $uniqueFilename new file name to restore the file without overwriting existing files
509
-	 * @param string $location location if file
510
-	 * @param int $timestamp deletion time
511
-	 * @return false|null
512
-	 */
513
-	private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
514
-		if (\OCP\App::isEnabled('files_versions')) {
515
-			$user = User::getUser();
516
-			$rootView = new View('/');
517
-
518
-			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
519
-
520
-			[$owner, $ownerPath] = self::getUidAndFilename($target);
521
-
522
-			// file has been deleted in between
523
-			if (empty($ownerPath)) {
524
-				return false;
525
-			}
526
-
527
-			if ($timestamp) {
528
-				$versionedFile = $filename;
529
-			} else {
530
-				$versionedFile = $file;
531
-			}
532
-
533
-			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
534
-				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
535
-			} elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
536
-				foreach ($versions as $v) {
537
-					if ($timestamp) {
538
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
539
-					} else {
540
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
541
-					}
542
-				}
543
-			}
544
-		}
545
-	}
546
-
547
-	/**
548
-	 * delete all files from the trash
549
-	 */
550
-	public static function deleteAll() {
551
-		$user = User::getUser();
552
-		$userRoot = \OC::$server->getUserFolder($user)->getParent();
553
-		$view = new View('/' . $user);
554
-		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
555
-
556
-		try {
557
-			$trash = $userRoot->get('files_trashbin');
558
-		} catch (NotFoundException $e) {
559
-			return false;
560
-		}
561
-
562
-		// Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
563
-		$filePaths = [];
564
-		foreach ($fileInfos as $fileInfo) {
565
-			$filePaths[] = $view->getRelativePath($fileInfo->getPath());
566
-		}
567
-		unset($fileInfos); // save memory
568
-
569
-		// Bulk PreDelete-Hook
570
-		\OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
571
-
572
-		// Single-File Hooks
573
-		foreach ($filePaths as $path) {
574
-			self::emitTrashbinPreDelete($path);
575
-		}
576
-
577
-		// actual file deletion
578
-		$trash->delete();
579
-		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
580
-		$query->execute([$user]);
581
-
582
-		// Bulk PostDelete-Hook
583
-		\OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
584
-
585
-		// Single-File Hooks
586
-		foreach ($filePaths as $path) {
587
-			self::emitTrashbinPostDelete($path);
588
-		}
589
-
590
-		$trash = $userRoot->newFolder('files_trashbin');
591
-		$trash->newFolder('files');
592
-
593
-		return true;
594
-	}
595
-
596
-	/**
597
-	 * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
598
-	 *
599
-	 * @param string $path
600
-	 */
601
-	protected static function emitTrashbinPreDelete($path) {
602
-		\OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
603
-	}
604
-
605
-	/**
606
-	 * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
607
-	 *
608
-	 * @param string $path
609
-	 */
610
-	protected static function emitTrashbinPostDelete($path) {
611
-		\OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
612
-	}
613
-
614
-	/**
615
-	 * delete file from trash bin permanently
616
-	 *
617
-	 * @param string $filename path to the file
618
-	 * @param string $user
619
-	 * @param int $timestamp of deletion time
620
-	 *
621
-	 * @return int size of deleted files
622
-	 */
623
-	public static function delete($filename, $user, $timestamp = null) {
624
-		$userRoot = \OC::$server->getUserFolder($user)->getParent();
625
-		$view = new View('/' . $user);
626
-		$size = 0;
627
-
628
-		if ($timestamp) {
629
-			$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
630
-			$query->execute([$user, $filename, $timestamp]);
631
-			$file = $filename . '.d' . $timestamp;
632
-		} else {
633
-			$file = $filename;
634
-		}
635
-
636
-		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
637
-
638
-		try {
639
-			$node = $userRoot->get('/files_trashbin/files/' . $file);
640
-		} catch (NotFoundException $e) {
641
-			return $size;
642
-		}
643
-
644
-		if ($node instanceof Folder) {
645
-			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
646
-		} elseif ($node instanceof File) {
647
-			$size += $view->filesize('/files_trashbin/files/' . $file);
648
-		}
649
-
650
-		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
651
-		$node->delete();
652
-		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
653
-
654
-		return $size;
655
-	}
656
-
657
-	/**
658
-	 * @param View $view
659
-	 * @param string $file
660
-	 * @param string $filename
661
-	 * @param integer|null $timestamp
662
-	 * @param string $user
663
-	 * @return int
664
-	 */
665
-	private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
666
-		$size = 0;
667
-		if (\OCP\App::isEnabled('files_versions')) {
668
-			if ($view->is_dir('files_trashbin/versions/' . $file)) {
669
-				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
670
-				$view->unlink('files_trashbin/versions/' . $file);
671
-			} elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
672
-				foreach ($versions as $v) {
673
-					if ($timestamp) {
674
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
675
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
676
-					} else {
677
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
678
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
679
-					}
680
-				}
681
-			}
682
-		}
683
-		return $size;
684
-	}
685
-
686
-	/**
687
-	 * check to see whether a file exists in trashbin
688
-	 *
689
-	 * @param string $filename path to the file
690
-	 * @param int $timestamp of deletion time
691
-	 * @return bool true if file exists, otherwise false
692
-	 */
693
-	public static function file_exists($filename, $timestamp = null) {
694
-		$user = User::getUser();
695
-		$view = new View('/' . $user);
696
-
697
-		if ($timestamp) {
698
-			$filename = $filename . '.d' . $timestamp;
699
-		}
700
-
701
-		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
702
-		return $view->file_exists($target);
703
-	}
704
-
705
-	/**
706
-	 * deletes used space for trash bin in db if user was deleted
707
-	 *
708
-	 * @param string $uid id of deleted user
709
-	 * @return bool result of db delete operation
710
-	 */
711
-	public static function deleteUser($uid) {
712
-		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
713
-		return $query->execute([$uid]);
714
-	}
715
-
716
-	/**
717
-	 * calculate remaining free space for trash bin
718
-	 *
719
-	 * @param integer $trashbinSize current size of the trash bin
720
-	 * @param string $user
721
-	 * @return int available free space for trash bin
722
-	 */
723
-	private static function calculateFreeSpace($trashbinSize, $user) {
724
-		$config = \OC::$server->getConfig();
725
-		$userTrashbinSize = (int)$config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
726
-		if ($userTrashbinSize > -1) {
727
-			return $userTrashbinSize - $trashbinSize;
728
-		}
729
-		$systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
730
-		if ($systemTrashbinSize > -1) {
731
-			return $systemTrashbinSize - $trashbinSize;
732
-		}
733
-
734
-		$softQuota = true;
735
-		$userObject = \OC::$server->getUserManager()->get($user);
736
-		if (is_null($userObject)) {
737
-			return 0;
738
-		}
739
-		$quota = $userObject->getQuota();
740
-		if ($quota === null || $quota === 'none') {
741
-			$quota = Filesystem::free_space('/');
742
-			$softQuota = false;
743
-			// inf or unknown free space
744
-			if ($quota < 0) {
745
-				$quota = PHP_INT_MAX;
746
-			}
747
-		} else {
748
-			$quota = \OCP\Util::computerFileSize($quota);
749
-		}
750
-
751
-		// calculate available space for trash bin
752
-		// subtract size of files and current trash bin size from quota
753
-		if ($softQuota) {
754
-			$userFolder = \OC::$server->getUserFolder($user);
755
-			if (is_null($userFolder)) {
756
-				return 0;
757
-			}
758
-			$free = $quota - $userFolder->getSize(false); // remaining free space for user
759
-			if ($free > 0) {
760
-				$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
761
-			} else {
762
-				$availableSpace = $free - $trashbinSize;
763
-			}
764
-		} else {
765
-			$availableSpace = $quota;
766
-		}
767
-
768
-		return $availableSpace;
769
-	}
770
-
771
-	/**
772
-	 * resize trash bin if necessary after a new file was added to Nextcloud
773
-	 *
774
-	 * @param string $user user id
775
-	 */
776
-	public static function resizeTrash($user) {
777
-		$size = self::getTrashbinSize($user);
778
-
779
-		$freeSpace = self::calculateFreeSpace($size, $user);
780
-
781
-		if ($freeSpace < 0) {
782
-			self::scheduleExpire($user);
783
-		}
784
-	}
785
-
786
-	/**
787
-	 * clean up the trash bin
788
-	 *
789
-	 * @param string $user
790
-	 */
791
-	public static function expire($user) {
792
-		$trashBinSize = self::getTrashbinSize($user);
793
-		$availableSpace = self::calculateFreeSpace($trashBinSize, $user);
794
-
795
-		$dirContent = Helper::getTrashFiles('/', $user, 'mtime');
796
-
797
-		// delete all files older then $retention_obligation
798
-		[$delSize, $count] = self::deleteExpiredFiles($dirContent, $user);
799
-
800
-		$availableSpace += $delSize;
801
-
802
-		// delete files from trash until we meet the trash bin size limit again
803
-		self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
804
-	}
805
-
806
-	/**
807
-	 * @param string $user
808
-	 */
809
-	private static function scheduleExpire($user) {
810
-		// let the admin disable auto expire
811
-		/** @var Application $application */
812
-		$application = \OC::$server->query(Application::class);
813
-		$expiration = $application->getContainer()->query('Expiration');
814
-		if ($expiration->isEnabled()) {
815
-			\OC::$server->getCommandBus()->push(new Expire($user));
816
-		}
817
-	}
818
-
819
-	/**
820
-	 * if the size limit for the trash bin is reached, we delete the oldest
821
-	 * files in the trash bin until we meet the limit again
822
-	 *
823
-	 * @param array $files
824
-	 * @param string $user
825
-	 * @param int $availableSpace available disc space
826
-	 * @return int size of deleted files
827
-	 */
828
-	protected static function deleteFiles($files, $user, $availableSpace) {
829
-		/** @var Application $application */
830
-		$application = \OC::$server->query(Application::class);
831
-		$expiration = $application->getContainer()->query('Expiration');
832
-		$size = 0;
833
-
834
-		if ($availableSpace < 0) {
835
-			foreach ($files as $file) {
836
-				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
837
-					$tmp = self::delete($file['name'], $user, $file['mtime']);
838
-					\OC::$server->getLogger()->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
839
-					$availableSpace += $tmp;
840
-					$size += $tmp;
841
-				} else {
842
-					break;
843
-				}
844
-			}
845
-		}
846
-		return $size;
847
-	}
848
-
849
-	/**
850
-	 * delete files older then max storage time
851
-	 *
852
-	 * @param array $files list of files sorted by mtime
853
-	 * @param string $user
854
-	 * @return integer[] size of deleted files and number of deleted files
855
-	 */
856
-	public static function deleteExpiredFiles($files, $user) {
857
-		/** @var Expiration $expiration */
858
-		$expiration = \OC::$server->query(Expiration::class);
859
-		$size = 0;
860
-		$count = 0;
861
-		foreach ($files as $file) {
862
-			$timestamp = $file['mtime'];
863
-			$filename = $file['name'];
864
-			if ($expiration->isExpired($timestamp)) {
865
-				try {
866
-					$size += self::delete($filename, $user, $timestamp);
867
-					$count++;
868
-				} catch (\OCP\Files\NotPermittedException $e) {
869
-					\OC::$server->getLogger()->logException($e, ['app' => 'files_trashbin', 'level' => \OCP\ILogger::WARN, 'message' => 'Removing "' . $filename . '" from trashbin failed.']);
870
-				}
871
-				\OC::$server->getLogger()->info(
872
-					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
873
-					['app' => 'files_trashbin']
874
-				);
875
-			} else {
876
-				break;
877
-			}
878
-		}
879
-
880
-		return [$size, $count];
881
-	}
882
-
883
-	/**
884
-	 * recursive copy to copy a whole directory
885
-	 *
886
-	 * @param string $source source path, relative to the users files directory
887
-	 * @param string $destination destination path relative to the users root directoy
888
-	 * @param View $view file view for the users root directory
889
-	 * @return int
890
-	 * @throws Exceptions\CopyRecursiveException
891
-	 */
892
-	private static function copy_recursive($source, $destination, View $view) {
893
-		$size = 0;
894
-		if ($view->is_dir($source)) {
895
-			$view->mkdir($destination);
896
-			$view->touch($destination, $view->filemtime($source));
897
-			foreach ($view->getDirectoryContent($source) as $i) {
898
-				$pathDir = $source . '/' . $i['name'];
899
-				if ($view->is_dir($pathDir)) {
900
-					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
901
-				} else {
902
-					$size += $view->filesize($pathDir);
903
-					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
904
-					if (!$result) {
905
-						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
906
-					}
907
-					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
908
-				}
909
-			}
910
-		} else {
911
-			$size += $view->filesize($source);
912
-			$result = $view->copy($source, $destination);
913
-			if (!$result) {
914
-				throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
915
-			}
916
-			$view->touch($destination, $view->filemtime($source));
917
-		}
918
-		return $size;
919
-	}
920
-
921
-	/**
922
-	 * find all versions which belong to the file we want to restore
923
-	 *
924
-	 * @param string $filename name of the file which should be restored
925
-	 * @param int $timestamp timestamp when the file was deleted
926
-	 * @return array
927
-	 */
928
-	private static function getVersionsFromTrash($filename, $timestamp, $user) {
929
-		$view = new View('/' . $user . '/files_trashbin/versions');
930
-		$versions = [];
931
-
932
-		/** @var \OC\Files\Storage\Storage $storage */
933
-		[$storage,] = $view->resolvePath('/');
934
-
935
-		//force rescan of versions, local storage may not have updated the cache
936
-		if (!self::$scannedVersions) {
937
-			$storage->getScanner()->scan('files_trashbin/versions');
938
-			self::$scannedVersions = true;
939
-		}
940
-
941
-		$pattern = \OC::$server->getDatabaseConnection()->escapeLikeParameter(basename($filename));
942
-		if ($timestamp) {
943
-			// fetch for old versions
944
-			$escapedTimestamp = \OC::$server->getDatabaseConnection()->escapeLikeParameter($timestamp);
945
-			$pattern .= '.v%.d' . $escapedTimestamp;
946
-			$offset = -strlen($escapedTimestamp) - 2;
947
-		} else {
948
-			$pattern .= '.v%';
949
-		}
950
-
951
-		// Manually fetch all versions from the file cache to be able to filter them by their parent
952
-		$cache = $storage->getCache('');
953
-		$query = new CacheQueryBuilder(
954
-			\OC::$server->getDatabaseConnection(),
955
-			\OC::$server->getSystemConfig(),
956
-			\OC::$server->getLogger(),
957
-			$cache
958
-		);
959
-		$normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/');
960
-		$parentId = $cache->getId($normalizedParentPath);
961
-		if ($parentId === -1) {
962
-			return [];
963
-		}
964
-
965
-		$query->selectFileCache()
966
-			->whereStorageId()
967
-			->andWhere($query->expr()->eq('parent', $query->createNamedParameter($parentId)))
968
-			->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
969
-
970
-		/** @var CacheEntry[] $matches */
971
-		$matches = array_map(function (array $data) {
972
-			return Cache::cacheEntryFromData($data, \OC::$server->getMimeTypeLoader());
973
-		}, $query->execute()->fetchAll());
974
-
975
-		foreach ($matches as $ma) {
976
-			if ($timestamp) {
977
-				$parts = explode('.v', substr($ma['path'], 0, $offset));
978
-				$versions[] = end($parts);
979
-			} else {
980
-				$parts = explode('.v', $ma['path']);
981
-				$versions[] = end($parts);
982
-			}
983
-		}
984
-
985
-		return $versions;
986
-	}
987
-
988
-	/**
989
-	 * find unique extension for restored file if a file with the same name already exists
990
-	 *
991
-	 * @param string $location where the file should be restored
992
-	 * @param string $filename name of the file
993
-	 * @param View $view filesystem view relative to users root directory
994
-	 * @return string with unique extension
995
-	 */
996
-	private static function getUniqueFilename($location, $filename, View $view) {
997
-		$ext = pathinfo($filename, PATHINFO_EXTENSION);
998
-		$name = pathinfo($filename, PATHINFO_FILENAME);
999
-		$l = \OC::$server->getL10N('files_trashbin');
1000
-
1001
-		$location = '/' . trim($location, '/');
1002
-
1003
-		// if extension is not empty we set a dot in front of it
1004
-		if ($ext !== '') {
1005
-			$ext = '.' . $ext;
1006
-		}
1007
-
1008
-		if ($view->file_exists('files' . $location . '/' . $filename)) {
1009
-			$i = 2;
1010
-			$uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
1011
-			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
1012
-				$uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
1013
-				$i++;
1014
-			}
1015
-
1016
-			return $uniqueName;
1017
-		}
1018
-
1019
-		return $filename;
1020
-	}
1021
-
1022
-	/**
1023
-	 * get the size from a given root folder
1024
-	 *
1025
-	 * @param View $view file view on the root folder
1026
-	 * @return integer size of the folder
1027
-	 */
1028
-	private static function calculateSize($view) {
1029
-		$root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
1030
-		if (!file_exists($root)) {
1031
-			return 0;
1032
-		}
1033
-		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
1034
-		$size = 0;
1035
-
1036
-		/**
1037
-		 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
1038
-		 * This bug is fixed in PHP 5.5.9 or before
1039
-		 * See #8376
1040
-		 */
1041
-		$iterator->rewind();
1042
-		while ($iterator->valid()) {
1043
-			$path = $iterator->current();
1044
-			$relpath = substr($path, strlen($root) - 1);
1045
-			if (!$view->is_dir($relpath)) {
1046
-				$size += $view->filesize($relpath);
1047
-			}
1048
-			$iterator->next();
1049
-		}
1050
-		return $size;
1051
-	}
1052
-
1053
-	/**
1054
-	 * get current size of trash bin from a given user
1055
-	 *
1056
-	 * @param string $user user who owns the trash bin
1057
-	 * @return integer trash bin size
1058
-	 */
1059
-	private static function getTrashbinSize($user) {
1060
-		$view = new View('/' . $user);
1061
-		$fileInfo = $view->getFileInfo('/files_trashbin');
1062
-		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1063
-	}
1064
-
1065
-	/**
1066
-	 * check if trash bin is empty for a given user
1067
-	 *
1068
-	 * @param string $user
1069
-	 * @return bool
1070
-	 */
1071
-	public static function isEmpty($user) {
1072
-		$view = new View('/' . $user . '/files_trashbin');
1073
-		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1074
-			while ($file = readdir($dh)) {
1075
-				if (!Filesystem::isIgnoredDir($file)) {
1076
-					return false;
1077
-				}
1078
-			}
1079
-		}
1080
-		return true;
1081
-	}
1082
-
1083
-	/**
1084
-	 * @param $path
1085
-	 * @return string
1086
-	 */
1087
-	public static function preview_icon($path) {
1088
-		return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]);
1089
-	}
66
+    // unit: percentage; 50% of available disk space/quota
67
+    public const DEFAULTMAXSIZE = 50;
68
+
69
+    /**
70
+     * Whether versions have already be rescanned during this PHP request
71
+     *
72
+     * @var bool
73
+     */
74
+    private static $scannedVersions = false;
75
+
76
+    /**
77
+     * Ensure we don't need to scan the file during the move to trash
78
+     * by triggering the scan in the pre-hook
79
+     *
80
+     * @param array $params
81
+     */
82
+    public static function ensureFileScannedHook($params) {
83
+        try {
84
+            self::getUidAndFilename($params['path']);
85
+        } catch (NotFoundException $e) {
86
+            // nothing to scan for non existing files
87
+        }
88
+    }
89
+
90
+    /**
91
+     * get the UID of the owner of the file and the path to the file relative to
92
+     * owners files folder
93
+     *
94
+     * @param string $filename
95
+     * @return array
96
+     * @throws \OC\User\NoUserException
97
+     */
98
+    public static function getUidAndFilename($filename) {
99
+        $uid = Filesystem::getOwner($filename);
100
+        $userManager = \OC::$server->getUserManager();
101
+        // if the user with the UID doesn't exists, e.g. because the UID points
102
+        // to a remote user with a federated cloud ID we use the current logged-in
103
+        // user. We need a valid local user to move the file to the right trash bin
104
+        if (!$userManager->userExists($uid)) {
105
+            $uid = User::getUser();
106
+        }
107
+        if (!$uid) {
108
+            // no owner, usually because of share link from ext storage
109
+            return [null, null];
110
+        }
111
+        Filesystem::initMountPoints($uid);
112
+        if ($uid !== User::getUser()) {
113
+            $info = Filesystem::getFileInfo($filename);
114
+            $ownerView = new View('/' . $uid . '/files');
115
+            try {
116
+                $filename = $ownerView->getPath($info['fileid']);
117
+            } catch (NotFoundException $e) {
118
+                $filename = null;
119
+            }
120
+        }
121
+        return [$uid, $filename];
122
+    }
123
+
124
+    /**
125
+     * get original location of files for user
126
+     *
127
+     * @param string $user
128
+     * @return array (filename => array (timestamp => original location))
129
+     */
130
+    public static function getLocations($user) {
131
+        $query = \OC_DB::prepare('SELECT `id`, `timestamp`, `location`'
132
+            . ' FROM `*PREFIX*files_trash` WHERE `user`=?');
133
+        $result = $query->execute([$user]);
134
+        $array = [];
135
+        while ($row = $result->fetchRow()) {
136
+            if (isset($array[$row['id']])) {
137
+                $array[$row['id']][$row['timestamp']] = $row['location'];
138
+            } else {
139
+                $array[$row['id']] = [$row['timestamp'] => $row['location']];
140
+            }
141
+        }
142
+        return $array;
143
+    }
144
+
145
+    /**
146
+     * get original location of file
147
+     *
148
+     * @param string $user
149
+     * @param string $filename
150
+     * @param string $timestamp
151
+     * @return string original location
152
+     */
153
+    public static function getLocation($user, $filename, $timestamp) {
154
+        $query = \OC_DB::prepare('SELECT `location` FROM `*PREFIX*files_trash`'
155
+            . ' WHERE `user`=? AND `id`=? AND `timestamp`=?');
156
+        $result = $query->execute([$user, $filename, $timestamp])->fetchAll();
157
+        if (isset($result[0]['location'])) {
158
+            return $result[0]['location'];
159
+        } else {
160
+            return false;
161
+        }
162
+    }
163
+
164
+    private static function setUpTrash($user) {
165
+        $view = new View('/' . $user);
166
+        if (!$view->is_dir('files_trashbin')) {
167
+            $view->mkdir('files_trashbin');
168
+        }
169
+        if (!$view->is_dir('files_trashbin/files')) {
170
+            $view->mkdir('files_trashbin/files');
171
+        }
172
+        if (!$view->is_dir('files_trashbin/versions')) {
173
+            $view->mkdir('files_trashbin/versions');
174
+        }
175
+        if (!$view->is_dir('files_trashbin/keys')) {
176
+            $view->mkdir('files_trashbin/keys');
177
+        }
178
+    }
179
+
180
+
181
+    /**
182
+     * copy file to owners trash
183
+     *
184
+     * @param string $sourcePath
185
+     * @param string $owner
186
+     * @param string $targetPath
187
+     * @param $user
188
+     * @param integer $timestamp
189
+     */
190
+    private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
191
+        self::setUpTrash($owner);
192
+
193
+        $targetFilename = basename($targetPath);
194
+        $targetLocation = dirname($targetPath);
195
+
196
+        $sourceFilename = basename($sourcePath);
197
+
198
+        $view = new View('/');
199
+
200
+        $target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
201
+        $source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
202
+        $free = $view->free_space($target);
203
+        $isUnknownOrUnlimitedFreeSpace = $free < 0;
204
+        $isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
205
+        if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) {
206
+            self::copy_recursive($source, $target, $view);
207
+        }
208
+
209
+
210
+        if ($view->file_exists($target)) {
211
+            $query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
212
+            $result = $query->execute([$targetFilename, $timestamp, $targetLocation, $user]);
213
+            if (!$result) {
214
+                \OC::$server->getLogger()->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']);
215
+            }
216
+        }
217
+    }
218
+
219
+
220
+    /**
221
+     * move file to the trash bin
222
+     *
223
+     * @param string $file_path path to the deleted file/directory relative to the files root directory
224
+     * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
225
+     *
226
+     * @return bool
227
+     */
228
+    public static function move2trash($file_path, $ownerOnly = false) {
229
+        // get the user for which the filesystem is setup
230
+        $root = Filesystem::getRoot();
231
+        [, $user] = explode('/', $root);
232
+        [$owner, $ownerPath] = self::getUidAndFilename($file_path);
233
+
234
+        // if no owner found (ex: ext storage + share link), will use the current user's trashbin then
235
+        if (is_null($owner)) {
236
+            $owner = $user;
237
+            $ownerPath = $file_path;
238
+        }
239
+
240
+        $ownerView = new View('/' . $owner);
241
+        // file has been deleted in between
242
+        if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
243
+            return true;
244
+        }
245
+
246
+        self::setUpTrash($user);
247
+        if ($owner !== $user) {
248
+            // also setup for owner
249
+            self::setUpTrash($owner);
250
+        }
251
+
252
+        $path_parts = pathinfo($ownerPath);
253
+
254
+        $filename = $path_parts['basename'];
255
+        $location = $path_parts['dirname'];
256
+        /** @var ITimeFactory $timeFactory */
257
+        $timeFactory = \OC::$server->query(ITimeFactory::class);
258
+        $timestamp = $timeFactory->getTime();
259
+
260
+        $lockingProvider = \OC::$server->getLockingProvider();
261
+
262
+        // disable proxy to prevent recursive calls
263
+        $trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
264
+        $gotLock = false;
265
+
266
+        while (!$gotLock) {
267
+            try {
268
+                /** @var \OC\Files\Storage\Storage $trashStorage */
269
+                [$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath);
270
+
271
+                $trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
272
+                $gotLock = true;
273
+            } catch (LockedException $e) {
274
+                // a file with the same name is being deleted concurrently
275
+                // nudge the timestamp a bit to resolve the conflict
276
+
277
+                $timestamp = $timestamp + 1;
278
+
279
+                $trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
280
+            }
281
+        }
282
+
283
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
284
+        [$sourceStorage, $sourceInternalPath] = $ownerView->resolvePath('/files/' . $ownerPath);
285
+
286
+
287
+        if ($trashStorage->file_exists($trashInternalPath)) {
288
+            $trashStorage->unlink($trashInternalPath);
289
+        }
290
+
291
+        $config = \OC::$server->getConfig();
292
+        $systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
293
+        $userTrashbinSize = (int)$config->getUserValue($owner, 'files_trashbin', 'trashbin_size', '-1');
294
+        $configuredTrashbinSize = ($userTrashbinSize < 0) ? $systemTrashbinSize : $userTrashbinSize;
295
+        if ($configuredTrashbinSize >= 0 && $sourceStorage->filesize($sourceInternalPath) >= $configuredTrashbinSize) {
296
+            return false;
297
+        }
298
+
299
+        $trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
300
+
301
+        try {
302
+            $moveSuccessful = true;
303
+
304
+            // when moving within the same object store, the cache update done above is enough to move the file
305
+            if (!($trashStorage->instanceOfStorage(ObjectStoreStorage::class) && $trashStorage->getId() === $sourceStorage->getId())) {
306
+                $trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
307
+            }
308
+        } catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
309
+            $moveSuccessful = false;
310
+            if ($trashStorage->file_exists($trashInternalPath)) {
311
+                $trashStorage->unlink($trashInternalPath);
312
+            }
313
+            \OC::$server->getLogger()->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
314
+        }
315
+
316
+        if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
317
+            if ($sourceStorage->is_dir($sourceInternalPath)) {
318
+                $sourceStorage->rmdir($sourceInternalPath);
319
+            } else {
320
+                $sourceStorage->unlink($sourceInternalPath);
321
+            }
322
+
323
+            if ($sourceStorage->file_exists($sourceInternalPath)) {
324
+                // undo the cache move
325
+                $sourceStorage->getUpdater()->renameFromStorage($trashStorage, $trashInternalPath, $sourceInternalPath);
326
+            } else {
327
+                $trashStorage->getUpdater()->remove($trashInternalPath);
328
+            }
329
+            return false;
330
+        }
331
+
332
+        if ($moveSuccessful) {
333
+            $query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
334
+            $result = $query->execute([$filename, $timestamp, $location, $owner]);
335
+            if (!$result) {
336
+                \OC::$server->getLogger()->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
337
+            }
338
+            \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
339
+                'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)]);
340
+
341
+            self::retainVersions($filename, $owner, $ownerPath, $timestamp);
342
+
343
+            // if owner !== user we need to also add a copy to the users trash
344
+            if ($user !== $owner && $ownerOnly === false) {
345
+                self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
346
+            }
347
+        }
348
+
349
+        $trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
350
+
351
+        self::scheduleExpire($user);
352
+
353
+        // if owner !== user we also need to update the owners trash size
354
+        if ($owner !== $user) {
355
+            self::scheduleExpire($owner);
356
+        }
357
+
358
+        return $moveSuccessful;
359
+    }
360
+
361
+    /**
362
+     * Move file versions to trash so that they can be restored later
363
+     *
364
+     * @param string $filename of deleted file
365
+     * @param string $owner owner user id
366
+     * @param string $ownerPath path relative to the owner's home storage
367
+     * @param integer $timestamp when the file was deleted
368
+     */
369
+    private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
370
+        if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {
371
+            $user = User::getUser();
372
+            $rootView = new View('/');
373
+
374
+            if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
375
+                if ($owner !== $user) {
376
+                    self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
377
+                }
378
+                self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
379
+            } elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
380
+                foreach ($versions as $v) {
381
+                    if ($owner !== $user) {
382
+                        self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
383
+                    }
384
+                    self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
385
+                }
386
+            }
387
+        }
388
+    }
389
+
390
+    /**
391
+     * Move a file or folder on storage level
392
+     *
393
+     * @param View $view
394
+     * @param string $source
395
+     * @param string $target
396
+     * @return bool
397
+     */
398
+    private static function move(View $view, $source, $target) {
399
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
400
+        [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
401
+        /** @var \OC\Files\Storage\Storage $targetStorage */
402
+        [$targetStorage, $targetInternalPath] = $view->resolvePath($target);
403
+        /** @var \OC\Files\Storage\Storage $ownerTrashStorage */
404
+
405
+        $result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
406
+        if ($result) {
407
+            $targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
408
+        }
409
+        return $result;
410
+    }
411
+
412
+    /**
413
+     * Copy a file or folder on storage level
414
+     *
415
+     * @param View $view
416
+     * @param string $source
417
+     * @param string $target
418
+     * @return bool
419
+     */
420
+    private static function copy(View $view, $source, $target) {
421
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
422
+        [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
423
+        /** @var \OC\Files\Storage\Storage $targetStorage */
424
+        [$targetStorage, $targetInternalPath] = $view->resolvePath($target);
425
+        /** @var \OC\Files\Storage\Storage $ownerTrashStorage */
426
+
427
+        $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
428
+        if ($result) {
429
+            $targetStorage->getUpdater()->update($targetInternalPath);
430
+        }
431
+        return $result;
432
+    }
433
+
434
+    /**
435
+     * Restore a file or folder from trash bin
436
+     *
437
+     * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
438
+     * including the timestamp suffix ".d12345678"
439
+     * @param string $filename name of the file/folder
440
+     * @param int $timestamp time when the file/folder was deleted
441
+     *
442
+     * @return bool true on success, false otherwise
443
+     */
444
+    public static function restore($file, $filename, $timestamp) {
445
+        $user = User::getUser();
446
+        $view = new View('/' . $user);
447
+
448
+        $location = '';
449
+        if ($timestamp) {
450
+            $location = self::getLocation($user, $filename, $timestamp);
451
+            if ($location === false) {
452
+                \OC::$server->getLogger()->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
453
+            } else {
454
+                // if location no longer exists, restore file in the root directory
455
+                if ($location !== '/' &&
456
+                    (!$view->is_dir('files/' . $location) ||
457
+                        !$view->isCreatable('files/' . $location))
458
+                ) {
459
+                    $location = '';
460
+                }
461
+            }
462
+        }
463
+
464
+        // we need a  extension in case a file/dir with the same name already exists
465
+        $uniqueFilename = self::getUniqueFilename($location, $filename, $view);
466
+
467
+        $source = Filesystem::normalizePath('files_trashbin/files/' . $file);
468
+        $target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
469
+        if (!$view->file_exists($source)) {
470
+            return false;
471
+        }
472
+        $mtime = $view->filemtime($source);
473
+
474
+        // restore file
475
+        if (!$view->isCreatable(dirname($target))) {
476
+            throw new NotPermittedException("Can't restore trash item because the target folder is not writable");
477
+        }
478
+        $restoreResult = $view->rename($source, $target);
479
+
480
+        // handle the restore result
481
+        if ($restoreResult) {
482
+            $fakeRoot = $view->getRoot();
483
+            $view->chroot('/' . $user . '/files');
484
+            $view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
485
+            $view->chroot($fakeRoot);
486
+            \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
487
+                'trashPath' => Filesystem::normalizePath($file)]);
488
+
489
+            self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
490
+
491
+            if ($timestamp) {
492
+                $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
493
+                $query->execute([$user, $filename, $timestamp]);
494
+            }
495
+
496
+            return true;
497
+        }
498
+
499
+        return false;
500
+    }
501
+
502
+    /**
503
+     * restore versions from trash bin
504
+     *
505
+     * @param View $view file view
506
+     * @param string $file complete path to file
507
+     * @param string $filename name of file once it was deleted
508
+     * @param string $uniqueFilename new file name to restore the file without overwriting existing files
509
+     * @param string $location location if file
510
+     * @param int $timestamp deletion time
511
+     * @return false|null
512
+     */
513
+    private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
514
+        if (\OCP\App::isEnabled('files_versions')) {
515
+            $user = User::getUser();
516
+            $rootView = new View('/');
517
+
518
+            $target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
519
+
520
+            [$owner, $ownerPath] = self::getUidAndFilename($target);
521
+
522
+            // file has been deleted in between
523
+            if (empty($ownerPath)) {
524
+                return false;
525
+            }
526
+
527
+            if ($timestamp) {
528
+                $versionedFile = $filename;
529
+            } else {
530
+                $versionedFile = $file;
531
+            }
532
+
533
+            if ($view->is_dir('/files_trashbin/versions/' . $file)) {
534
+                $rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
535
+            } elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
536
+                foreach ($versions as $v) {
537
+                    if ($timestamp) {
538
+                        $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
539
+                    } else {
540
+                        $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
541
+                    }
542
+                }
543
+            }
544
+        }
545
+    }
546
+
547
+    /**
548
+     * delete all files from the trash
549
+     */
550
+    public static function deleteAll() {
551
+        $user = User::getUser();
552
+        $userRoot = \OC::$server->getUserFolder($user)->getParent();
553
+        $view = new View('/' . $user);
554
+        $fileInfos = $view->getDirectoryContent('files_trashbin/files');
555
+
556
+        try {
557
+            $trash = $userRoot->get('files_trashbin');
558
+        } catch (NotFoundException $e) {
559
+            return false;
560
+        }
561
+
562
+        // Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
563
+        $filePaths = [];
564
+        foreach ($fileInfos as $fileInfo) {
565
+            $filePaths[] = $view->getRelativePath($fileInfo->getPath());
566
+        }
567
+        unset($fileInfos); // save memory
568
+
569
+        // Bulk PreDelete-Hook
570
+        \OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
571
+
572
+        // Single-File Hooks
573
+        foreach ($filePaths as $path) {
574
+            self::emitTrashbinPreDelete($path);
575
+        }
576
+
577
+        // actual file deletion
578
+        $trash->delete();
579
+        $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
580
+        $query->execute([$user]);
581
+
582
+        // Bulk PostDelete-Hook
583
+        \OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
584
+
585
+        // Single-File Hooks
586
+        foreach ($filePaths as $path) {
587
+            self::emitTrashbinPostDelete($path);
588
+        }
589
+
590
+        $trash = $userRoot->newFolder('files_trashbin');
591
+        $trash->newFolder('files');
592
+
593
+        return true;
594
+    }
595
+
596
+    /**
597
+     * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
598
+     *
599
+     * @param string $path
600
+     */
601
+    protected static function emitTrashbinPreDelete($path) {
602
+        \OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
603
+    }
604
+
605
+    /**
606
+     * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
607
+     *
608
+     * @param string $path
609
+     */
610
+    protected static function emitTrashbinPostDelete($path) {
611
+        \OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
612
+    }
613
+
614
+    /**
615
+     * delete file from trash bin permanently
616
+     *
617
+     * @param string $filename path to the file
618
+     * @param string $user
619
+     * @param int $timestamp of deletion time
620
+     *
621
+     * @return int size of deleted files
622
+     */
623
+    public static function delete($filename, $user, $timestamp = null) {
624
+        $userRoot = \OC::$server->getUserFolder($user)->getParent();
625
+        $view = new View('/' . $user);
626
+        $size = 0;
627
+
628
+        if ($timestamp) {
629
+            $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
630
+            $query->execute([$user, $filename, $timestamp]);
631
+            $file = $filename . '.d' . $timestamp;
632
+        } else {
633
+            $file = $filename;
634
+        }
635
+
636
+        $size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
637
+
638
+        try {
639
+            $node = $userRoot->get('/files_trashbin/files/' . $file);
640
+        } catch (NotFoundException $e) {
641
+            return $size;
642
+        }
643
+
644
+        if ($node instanceof Folder) {
645
+            $size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
646
+        } elseif ($node instanceof File) {
647
+            $size += $view->filesize('/files_trashbin/files/' . $file);
648
+        }
649
+
650
+        self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
651
+        $node->delete();
652
+        self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
653
+
654
+        return $size;
655
+    }
656
+
657
+    /**
658
+     * @param View $view
659
+     * @param string $file
660
+     * @param string $filename
661
+     * @param integer|null $timestamp
662
+     * @param string $user
663
+     * @return int
664
+     */
665
+    private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
666
+        $size = 0;
667
+        if (\OCP\App::isEnabled('files_versions')) {
668
+            if ($view->is_dir('files_trashbin/versions/' . $file)) {
669
+                $size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
670
+                $view->unlink('files_trashbin/versions/' . $file);
671
+            } elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
672
+                foreach ($versions as $v) {
673
+                    if ($timestamp) {
674
+                        $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
675
+                        $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
676
+                    } else {
677
+                        $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
678
+                        $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
679
+                    }
680
+                }
681
+            }
682
+        }
683
+        return $size;
684
+    }
685
+
686
+    /**
687
+     * check to see whether a file exists in trashbin
688
+     *
689
+     * @param string $filename path to the file
690
+     * @param int $timestamp of deletion time
691
+     * @return bool true if file exists, otherwise false
692
+     */
693
+    public static function file_exists($filename, $timestamp = null) {
694
+        $user = User::getUser();
695
+        $view = new View('/' . $user);
696
+
697
+        if ($timestamp) {
698
+            $filename = $filename . '.d' . $timestamp;
699
+        }
700
+
701
+        $target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
702
+        return $view->file_exists($target);
703
+    }
704
+
705
+    /**
706
+     * deletes used space for trash bin in db if user was deleted
707
+     *
708
+     * @param string $uid id of deleted user
709
+     * @return bool result of db delete operation
710
+     */
711
+    public static function deleteUser($uid) {
712
+        $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
713
+        return $query->execute([$uid]);
714
+    }
715
+
716
+    /**
717
+     * calculate remaining free space for trash bin
718
+     *
719
+     * @param integer $trashbinSize current size of the trash bin
720
+     * @param string $user
721
+     * @return int available free space for trash bin
722
+     */
723
+    private static function calculateFreeSpace($trashbinSize, $user) {
724
+        $config = \OC::$server->getConfig();
725
+        $userTrashbinSize = (int)$config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
726
+        if ($userTrashbinSize > -1) {
727
+            return $userTrashbinSize - $trashbinSize;
728
+        }
729
+        $systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
730
+        if ($systemTrashbinSize > -1) {
731
+            return $systemTrashbinSize - $trashbinSize;
732
+        }
733
+
734
+        $softQuota = true;
735
+        $userObject = \OC::$server->getUserManager()->get($user);
736
+        if (is_null($userObject)) {
737
+            return 0;
738
+        }
739
+        $quota = $userObject->getQuota();
740
+        if ($quota === null || $quota === 'none') {
741
+            $quota = Filesystem::free_space('/');
742
+            $softQuota = false;
743
+            // inf or unknown free space
744
+            if ($quota < 0) {
745
+                $quota = PHP_INT_MAX;
746
+            }
747
+        } else {
748
+            $quota = \OCP\Util::computerFileSize($quota);
749
+        }
750
+
751
+        // calculate available space for trash bin
752
+        // subtract size of files and current trash bin size from quota
753
+        if ($softQuota) {
754
+            $userFolder = \OC::$server->getUserFolder($user);
755
+            if (is_null($userFolder)) {
756
+                return 0;
757
+            }
758
+            $free = $quota - $userFolder->getSize(false); // remaining free space for user
759
+            if ($free > 0) {
760
+                $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
761
+            } else {
762
+                $availableSpace = $free - $trashbinSize;
763
+            }
764
+        } else {
765
+            $availableSpace = $quota;
766
+        }
767
+
768
+        return $availableSpace;
769
+    }
770
+
771
+    /**
772
+     * resize trash bin if necessary after a new file was added to Nextcloud
773
+     *
774
+     * @param string $user user id
775
+     */
776
+    public static function resizeTrash($user) {
777
+        $size = self::getTrashbinSize($user);
778
+
779
+        $freeSpace = self::calculateFreeSpace($size, $user);
780
+
781
+        if ($freeSpace < 0) {
782
+            self::scheduleExpire($user);
783
+        }
784
+    }
785
+
786
+    /**
787
+     * clean up the trash bin
788
+     *
789
+     * @param string $user
790
+     */
791
+    public static function expire($user) {
792
+        $trashBinSize = self::getTrashbinSize($user);
793
+        $availableSpace = self::calculateFreeSpace($trashBinSize, $user);
794
+
795
+        $dirContent = Helper::getTrashFiles('/', $user, 'mtime');
796
+
797
+        // delete all files older then $retention_obligation
798
+        [$delSize, $count] = self::deleteExpiredFiles($dirContent, $user);
799
+
800
+        $availableSpace += $delSize;
801
+
802
+        // delete files from trash until we meet the trash bin size limit again
803
+        self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
804
+    }
805
+
806
+    /**
807
+     * @param string $user
808
+     */
809
+    private static function scheduleExpire($user) {
810
+        // let the admin disable auto expire
811
+        /** @var Application $application */
812
+        $application = \OC::$server->query(Application::class);
813
+        $expiration = $application->getContainer()->query('Expiration');
814
+        if ($expiration->isEnabled()) {
815
+            \OC::$server->getCommandBus()->push(new Expire($user));
816
+        }
817
+    }
818
+
819
+    /**
820
+     * if the size limit for the trash bin is reached, we delete the oldest
821
+     * files in the trash bin until we meet the limit again
822
+     *
823
+     * @param array $files
824
+     * @param string $user
825
+     * @param int $availableSpace available disc space
826
+     * @return int size of deleted files
827
+     */
828
+    protected static function deleteFiles($files, $user, $availableSpace) {
829
+        /** @var Application $application */
830
+        $application = \OC::$server->query(Application::class);
831
+        $expiration = $application->getContainer()->query('Expiration');
832
+        $size = 0;
833
+
834
+        if ($availableSpace < 0) {
835
+            foreach ($files as $file) {
836
+                if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
837
+                    $tmp = self::delete($file['name'], $user, $file['mtime']);
838
+                    \OC::$server->getLogger()->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
839
+                    $availableSpace += $tmp;
840
+                    $size += $tmp;
841
+                } else {
842
+                    break;
843
+                }
844
+            }
845
+        }
846
+        return $size;
847
+    }
848
+
849
+    /**
850
+     * delete files older then max storage time
851
+     *
852
+     * @param array $files list of files sorted by mtime
853
+     * @param string $user
854
+     * @return integer[] size of deleted files and number of deleted files
855
+     */
856
+    public static function deleteExpiredFiles($files, $user) {
857
+        /** @var Expiration $expiration */
858
+        $expiration = \OC::$server->query(Expiration::class);
859
+        $size = 0;
860
+        $count = 0;
861
+        foreach ($files as $file) {
862
+            $timestamp = $file['mtime'];
863
+            $filename = $file['name'];
864
+            if ($expiration->isExpired($timestamp)) {
865
+                try {
866
+                    $size += self::delete($filename, $user, $timestamp);
867
+                    $count++;
868
+                } catch (\OCP\Files\NotPermittedException $e) {
869
+                    \OC::$server->getLogger()->logException($e, ['app' => 'files_trashbin', 'level' => \OCP\ILogger::WARN, 'message' => 'Removing "' . $filename . '" from trashbin failed.']);
870
+                }
871
+                \OC::$server->getLogger()->info(
872
+                    'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
873
+                    ['app' => 'files_trashbin']
874
+                );
875
+            } else {
876
+                break;
877
+            }
878
+        }
879
+
880
+        return [$size, $count];
881
+    }
882
+
883
+    /**
884
+     * recursive copy to copy a whole directory
885
+     *
886
+     * @param string $source source path, relative to the users files directory
887
+     * @param string $destination destination path relative to the users root directoy
888
+     * @param View $view file view for the users root directory
889
+     * @return int
890
+     * @throws Exceptions\CopyRecursiveException
891
+     */
892
+    private static function copy_recursive($source, $destination, View $view) {
893
+        $size = 0;
894
+        if ($view->is_dir($source)) {
895
+            $view->mkdir($destination);
896
+            $view->touch($destination, $view->filemtime($source));
897
+            foreach ($view->getDirectoryContent($source) as $i) {
898
+                $pathDir = $source . '/' . $i['name'];
899
+                if ($view->is_dir($pathDir)) {
900
+                    $size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
901
+                } else {
902
+                    $size += $view->filesize($pathDir);
903
+                    $result = $view->copy($pathDir, $destination . '/' . $i['name']);
904
+                    if (!$result) {
905
+                        throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
906
+                    }
907
+                    $view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
908
+                }
909
+            }
910
+        } else {
911
+            $size += $view->filesize($source);
912
+            $result = $view->copy($source, $destination);
913
+            if (!$result) {
914
+                throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
915
+            }
916
+            $view->touch($destination, $view->filemtime($source));
917
+        }
918
+        return $size;
919
+    }
920
+
921
+    /**
922
+     * find all versions which belong to the file we want to restore
923
+     *
924
+     * @param string $filename name of the file which should be restored
925
+     * @param int $timestamp timestamp when the file was deleted
926
+     * @return array
927
+     */
928
+    private static function getVersionsFromTrash($filename, $timestamp, $user) {
929
+        $view = new View('/' . $user . '/files_trashbin/versions');
930
+        $versions = [];
931
+
932
+        /** @var \OC\Files\Storage\Storage $storage */
933
+        [$storage,] = $view->resolvePath('/');
934
+
935
+        //force rescan of versions, local storage may not have updated the cache
936
+        if (!self::$scannedVersions) {
937
+            $storage->getScanner()->scan('files_trashbin/versions');
938
+            self::$scannedVersions = true;
939
+        }
940
+
941
+        $pattern = \OC::$server->getDatabaseConnection()->escapeLikeParameter(basename($filename));
942
+        if ($timestamp) {
943
+            // fetch for old versions
944
+            $escapedTimestamp = \OC::$server->getDatabaseConnection()->escapeLikeParameter($timestamp);
945
+            $pattern .= '.v%.d' . $escapedTimestamp;
946
+            $offset = -strlen($escapedTimestamp) - 2;
947
+        } else {
948
+            $pattern .= '.v%';
949
+        }
950
+
951
+        // Manually fetch all versions from the file cache to be able to filter them by their parent
952
+        $cache = $storage->getCache('');
953
+        $query = new CacheQueryBuilder(
954
+            \OC::$server->getDatabaseConnection(),
955
+            \OC::$server->getSystemConfig(),
956
+            \OC::$server->getLogger(),
957
+            $cache
958
+        );
959
+        $normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/');
960
+        $parentId = $cache->getId($normalizedParentPath);
961
+        if ($parentId === -1) {
962
+            return [];
963
+        }
964
+
965
+        $query->selectFileCache()
966
+            ->whereStorageId()
967
+            ->andWhere($query->expr()->eq('parent', $query->createNamedParameter($parentId)))
968
+            ->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
969
+
970
+        /** @var CacheEntry[] $matches */
971
+        $matches = array_map(function (array $data) {
972
+            return Cache::cacheEntryFromData($data, \OC::$server->getMimeTypeLoader());
973
+        }, $query->execute()->fetchAll());
974
+
975
+        foreach ($matches as $ma) {
976
+            if ($timestamp) {
977
+                $parts = explode('.v', substr($ma['path'], 0, $offset));
978
+                $versions[] = end($parts);
979
+            } else {
980
+                $parts = explode('.v', $ma['path']);
981
+                $versions[] = end($parts);
982
+            }
983
+        }
984
+
985
+        return $versions;
986
+    }
987
+
988
+    /**
989
+     * find unique extension for restored file if a file with the same name already exists
990
+     *
991
+     * @param string $location where the file should be restored
992
+     * @param string $filename name of the file
993
+     * @param View $view filesystem view relative to users root directory
994
+     * @return string with unique extension
995
+     */
996
+    private static function getUniqueFilename($location, $filename, View $view) {
997
+        $ext = pathinfo($filename, PATHINFO_EXTENSION);
998
+        $name = pathinfo($filename, PATHINFO_FILENAME);
999
+        $l = \OC::$server->getL10N('files_trashbin');
1000
+
1001
+        $location = '/' . trim($location, '/');
1002
+
1003
+        // if extension is not empty we set a dot in front of it
1004
+        if ($ext !== '') {
1005
+            $ext = '.' . $ext;
1006
+        }
1007
+
1008
+        if ($view->file_exists('files' . $location . '/' . $filename)) {
1009
+            $i = 2;
1010
+            $uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
1011
+            while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
1012
+                $uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
1013
+                $i++;
1014
+            }
1015
+
1016
+            return $uniqueName;
1017
+        }
1018
+
1019
+        return $filename;
1020
+    }
1021
+
1022
+    /**
1023
+     * get the size from a given root folder
1024
+     *
1025
+     * @param View $view file view on the root folder
1026
+     * @return integer size of the folder
1027
+     */
1028
+    private static function calculateSize($view) {
1029
+        $root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
1030
+        if (!file_exists($root)) {
1031
+            return 0;
1032
+        }
1033
+        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
1034
+        $size = 0;
1035
+
1036
+        /**
1037
+         * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
1038
+         * This bug is fixed in PHP 5.5.9 or before
1039
+         * See #8376
1040
+         */
1041
+        $iterator->rewind();
1042
+        while ($iterator->valid()) {
1043
+            $path = $iterator->current();
1044
+            $relpath = substr($path, strlen($root) - 1);
1045
+            if (!$view->is_dir($relpath)) {
1046
+                $size += $view->filesize($relpath);
1047
+            }
1048
+            $iterator->next();
1049
+        }
1050
+        return $size;
1051
+    }
1052
+
1053
+    /**
1054
+     * get current size of trash bin from a given user
1055
+     *
1056
+     * @param string $user user who owns the trash bin
1057
+     * @return integer trash bin size
1058
+     */
1059
+    private static function getTrashbinSize($user) {
1060
+        $view = new View('/' . $user);
1061
+        $fileInfo = $view->getFileInfo('/files_trashbin');
1062
+        return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1063
+    }
1064
+
1065
+    /**
1066
+     * check if trash bin is empty for a given user
1067
+     *
1068
+     * @param string $user
1069
+     * @return bool
1070
+     */
1071
+    public static function isEmpty($user) {
1072
+        $view = new View('/' . $user . '/files_trashbin');
1073
+        if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1074
+            while ($file = readdir($dh)) {
1075
+                if (!Filesystem::isIgnoredDir($file)) {
1076
+                    return false;
1077
+                }
1078
+            }
1079
+        }
1080
+        return true;
1081
+    }
1082
+
1083
+    /**
1084
+     * @param $path
1085
+     * @return string
1086
+     */
1087
+    public static function preview_icon($path) {
1088
+        return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]);
1089
+    }
1090 1090
 }
Please login to merge, or discard this patch.
Spacing   +73 added lines, -73 removed lines patch added patch discarded remove patch
@@ -111,7 +111,7 @@  discard block
 block discarded – undo
111 111
 		Filesystem::initMountPoints($uid);
112 112
 		if ($uid !== User::getUser()) {
113 113
 			$info = Filesystem::getFileInfo($filename);
114
-			$ownerView = new View('/' . $uid . '/files');
114
+			$ownerView = new View('/'.$uid.'/files');
115 115
 			try {
116 116
 				$filename = $ownerView->getPath($info['fileid']);
117 117
 			} catch (NotFoundException $e) {
@@ -162,7 +162,7 @@  discard block
 block discarded – undo
162 162
 	}
163 163
 
164 164
 	private static function setUpTrash($user) {
165
-		$view = new View('/' . $user);
165
+		$view = new View('/'.$user);
166 166
 		if (!$view->is_dir('files_trashbin')) {
167 167
 			$view->mkdir('files_trashbin');
168 168
 		}
@@ -197,8 +197,8 @@  discard block
 block discarded – undo
197 197
 
198 198
 		$view = new View('/');
199 199
 
200
-		$target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
201
-		$source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
200
+		$target = $user.'/files_trashbin/files/'.$targetFilename.'.d'.$timestamp;
201
+		$source = $owner.'/files_trashbin/files/'.$sourceFilename.'.d'.$timestamp;
202 202
 		$free = $view->free_space($target);
203 203
 		$isUnknownOrUnlimitedFreeSpace = $free < 0;
204 204
 		$isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
@@ -237,9 +237,9 @@  discard block
 block discarded – undo
237 237
 			$ownerPath = $file_path;
238 238
 		}
239 239
 
240
-		$ownerView = new View('/' . $owner);
240
+		$ownerView = new View('/'.$owner);
241 241
 		// file has been deleted in between
242
-		if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
242
+		if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/'.$ownerPath)) {
243 243
 			return true;
244 244
 		}
245 245
 
@@ -260,7 +260,7 @@  discard block
 block discarded – undo
260 260
 		$lockingProvider = \OC::$server->getLockingProvider();
261 261
 
262 262
 		// disable proxy to prevent recursive calls
263
-		$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
263
+		$trashPath = '/files_trashbin/files/'.$filename.'.d'.$timestamp;
264 264
 		$gotLock = false;
265 265
 
266 266
 		while (!$gotLock) {
@@ -276,12 +276,12 @@  discard block
 block discarded – undo
276 276
 
277 277
 				$timestamp = $timestamp + 1;
278 278
 
279
-				$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
279
+				$trashPath = '/files_trashbin/files/'.$filename.'.d'.$timestamp;
280 280
 			}
281 281
 		}
282 282
 
283 283
 		/** @var \OC\Files\Storage\Storage $sourceStorage */
284
-		[$sourceStorage, $sourceInternalPath] = $ownerView->resolvePath('/files/' . $ownerPath);
284
+		[$sourceStorage, $sourceInternalPath] = $ownerView->resolvePath('/files/'.$ownerPath);
285 285
 
286 286
 
287 287
 		if ($trashStorage->file_exists($trashInternalPath)) {
@@ -289,8 +289,8 @@  discard block
 block discarded – undo
289 289
 		}
290 290
 
291 291
 		$config = \OC::$server->getConfig();
292
-		$systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
293
-		$userTrashbinSize = (int)$config->getUserValue($owner, 'files_trashbin', 'trashbin_size', '-1');
292
+		$systemTrashbinSize = (int) $config->getAppValue('files_trashbin', 'trashbin_size', '-1');
293
+		$userTrashbinSize = (int) $config->getUserValue($owner, 'files_trashbin', 'trashbin_size', '-1');
294 294
 		$configuredTrashbinSize = ($userTrashbinSize < 0) ? $systemTrashbinSize : $userTrashbinSize;
295 295
 		if ($configuredTrashbinSize >= 0 && $sourceStorage->filesize($sourceInternalPath) >= $configuredTrashbinSize) {
296 296
 			return false;
@@ -310,7 +310,7 @@  discard block
 block discarded – undo
310 310
 			if ($trashStorage->file_exists($trashInternalPath)) {
311 311
 				$trashStorage->unlink($trashInternalPath);
312 312
 			}
313
-			\OC::$server->getLogger()->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
313
+			\OC::$server->getLogger()->error('Couldn\'t move '.$file_path.' to the trash bin', ['app' => 'files_trashbin']);
314 314
 		}
315 315
 
316 316
 		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
@@ -336,7 +336,7 @@  discard block
 block discarded – undo
336 336
 				\OC::$server->getLogger()->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
337 337
 			}
338 338
 			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
339
-				'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)]);
339
+				'trashPath' => Filesystem::normalizePath($filename.'.d'.$timestamp)]);
340 340
 
341 341
 			self::retainVersions($filename, $owner, $ownerPath, $timestamp);
342 342
 
@@ -371,17 +371,17 @@  discard block
 block discarded – undo
371 371
 			$user = User::getUser();
372 372
 			$rootView = new View('/');
373 373
 
374
-			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
374
+			if ($rootView->is_dir($owner.'/files_versions/'.$ownerPath)) {
375 375
 				if ($owner !== $user) {
376
-					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
376
+					self::copy_recursive($owner.'/files_versions/'.$ownerPath, $owner.'/files_trashbin/versions/'.basename($ownerPath).'.d'.$timestamp, $rootView);
377 377
 				}
378
-				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
378
+				self::move($rootView, $owner.'/files_versions/'.$ownerPath, $user.'/files_trashbin/versions/'.$filename.'.d'.$timestamp);
379 379
 			} elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
380 380
 				foreach ($versions as $v) {
381 381
 					if ($owner !== $user) {
382
-						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
382
+						self::copy($rootView, $owner.'/files_versions'.$v['path'].'.v'.$v['version'], $owner.'/files_trashbin/versions/'.$v['name'].'.v'.$v['version'].'.d'.$timestamp);
383 383
 					}
384
-					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
384
+					self::move($rootView, $owner.'/files_versions'.$v['path'].'.v'.$v['version'], $user.'/files_trashbin/versions/'.$filename.'.v'.$v['version'].'.d'.$timestamp);
385 385
 				}
386 386
 			}
387 387
 		}
@@ -443,18 +443,18 @@  discard block
 block discarded – undo
443 443
 	 */
444 444
 	public static function restore($file, $filename, $timestamp) {
445 445
 		$user = User::getUser();
446
-		$view = new View('/' . $user);
446
+		$view = new View('/'.$user);
447 447
 
448 448
 		$location = '';
449 449
 		if ($timestamp) {
450 450
 			$location = self::getLocation($user, $filename, $timestamp);
451 451
 			if ($location === false) {
452
-				\OC::$server->getLogger()->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
452
+				\OC::$server->getLogger()->error('trash bin database inconsistent! ($user: '.$user.' $filename: '.$filename.', $timestamp: '.$timestamp.')', ['app' => 'files_trashbin']);
453 453
 			} else {
454 454
 				// if location no longer exists, restore file in the root directory
455 455
 				if ($location !== '/' &&
456
-					(!$view->is_dir('files/' . $location) ||
457
-						!$view->isCreatable('files/' . $location))
456
+					(!$view->is_dir('files/'.$location) ||
457
+						!$view->isCreatable('files/'.$location))
458 458
 				) {
459 459
 					$location = '';
460 460
 				}
@@ -464,8 +464,8 @@  discard block
 block discarded – undo
464 464
 		// we need a  extension in case a file/dir with the same name already exists
465 465
 		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
466 466
 
467
-		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
468
-		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
467
+		$source = Filesystem::normalizePath('files_trashbin/files/'.$file);
468
+		$target = Filesystem::normalizePath('files/'.$location.'/'.$uniqueFilename);
469 469
 		if (!$view->file_exists($source)) {
470 470
 			return false;
471 471
 		}
@@ -480,10 +480,10 @@  discard block
 block discarded – undo
480 480
 		// handle the restore result
481 481
 		if ($restoreResult) {
482 482
 			$fakeRoot = $view->getRoot();
483
-			$view->chroot('/' . $user . '/files');
484
-			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
483
+			$view->chroot('/'.$user.'/files');
484
+			$view->touch('/'.$location.'/'.$uniqueFilename, $mtime);
485 485
 			$view->chroot($fakeRoot);
486
-			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
486
+			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/'.$location.'/'.$uniqueFilename),
487 487
 				'trashPath' => Filesystem::normalizePath($file)]);
488 488
 
489 489
 			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
@@ -515,7 +515,7 @@  discard block
 block discarded – undo
515 515
 			$user = User::getUser();
516 516
 			$rootView = new View('/');
517 517
 
518
-			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
518
+			$target = Filesystem::normalizePath('/'.$location.'/'.$uniqueFilename);
519 519
 
520 520
 			[$owner, $ownerPath] = self::getUidAndFilename($target);
521 521
 
@@ -530,14 +530,14 @@  discard block
 block discarded – undo
530 530
 				$versionedFile = $file;
531 531
 			}
532 532
 
533
-			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
534
-				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
533
+			if ($view->is_dir('/files_trashbin/versions/'.$file)) {
534
+				$rootView->rename(Filesystem::normalizePath($user.'/files_trashbin/versions/'.$file), Filesystem::normalizePath($owner.'/files_versions/'.$ownerPath));
535 535
 			} elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
536 536
 				foreach ($versions as $v) {
537 537
 					if ($timestamp) {
538
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
538
+						$rootView->rename($user.'/files_trashbin/versions/'.$versionedFile.'.v'.$v.'.d'.$timestamp, $owner.'/files_versions/'.$ownerPath.'.v'.$v);
539 539
 					} else {
540
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
540
+						$rootView->rename($user.'/files_trashbin/versions/'.$versionedFile.'.v'.$v, $owner.'/files_versions/'.$ownerPath.'.v'.$v);
541 541
 					}
542 542
 				}
543 543
 			}
@@ -550,7 +550,7 @@  discard block
 block discarded – undo
550 550
 	public static function deleteAll() {
551 551
 		$user = User::getUser();
552 552
 		$userRoot = \OC::$server->getUserFolder($user)->getParent();
553
-		$view = new View('/' . $user);
553
+		$view = new View('/'.$user);
554 554
 		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
555 555
 
556 556
 		try {
@@ -622,13 +622,13 @@  discard block
 block discarded – undo
622 622
 	 */
623 623
 	public static function delete($filename, $user, $timestamp = null) {
624 624
 		$userRoot = \OC::$server->getUserFolder($user)->getParent();
625
-		$view = new View('/' . $user);
625
+		$view = new View('/'.$user);
626 626
 		$size = 0;
627 627
 
628 628
 		if ($timestamp) {
629 629
 			$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
630 630
 			$query->execute([$user, $filename, $timestamp]);
631
-			$file = $filename . '.d' . $timestamp;
631
+			$file = $filename.'.d'.$timestamp;
632 632
 		} else {
633 633
 			$file = $filename;
634 634
 		}
@@ -636,20 +636,20 @@  discard block
 block discarded – undo
636 636
 		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
637 637
 
638 638
 		try {
639
-			$node = $userRoot->get('/files_trashbin/files/' . $file);
639
+			$node = $userRoot->get('/files_trashbin/files/'.$file);
640 640
 		} catch (NotFoundException $e) {
641 641
 			return $size;
642 642
 		}
643 643
 
644 644
 		if ($node instanceof Folder) {
645
-			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
645
+			$size += self::calculateSize(new View('/'.$user.'/files_trashbin/files/'.$file));
646 646
 		} elseif ($node instanceof File) {
647
-			$size += $view->filesize('/files_trashbin/files/' . $file);
647
+			$size += $view->filesize('/files_trashbin/files/'.$file);
648 648
 		}
649 649
 
650
-		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
650
+		self::emitTrashbinPreDelete('/files_trashbin/files/'.$file);
651 651
 		$node->delete();
652
-		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
652
+		self::emitTrashbinPostDelete('/files_trashbin/files/'.$file);
653 653
 
654 654
 		return $size;
655 655
 	}
@@ -665,17 +665,17 @@  discard block
 block discarded – undo
665 665
 	private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
666 666
 		$size = 0;
667 667
 		if (\OCP\App::isEnabled('files_versions')) {
668
-			if ($view->is_dir('files_trashbin/versions/' . $file)) {
669
-				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
670
-				$view->unlink('files_trashbin/versions/' . $file);
668
+			if ($view->is_dir('files_trashbin/versions/'.$file)) {
669
+				$size += self::calculateSize(new View('/'.$user.'/files_trashbin/versions/'.$file));
670
+				$view->unlink('files_trashbin/versions/'.$file);
671 671
 			} elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
672 672
 				foreach ($versions as $v) {
673 673
 					if ($timestamp) {
674
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
675
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
674
+						$size += $view->filesize('/files_trashbin/versions/'.$filename.'.v'.$v.'.d'.$timestamp);
675
+						$view->unlink('/files_trashbin/versions/'.$filename.'.v'.$v.'.d'.$timestamp);
676 676
 					} else {
677
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
678
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
677
+						$size += $view->filesize('/files_trashbin/versions/'.$filename.'.v'.$v);
678
+						$view->unlink('/files_trashbin/versions/'.$filename.'.v'.$v);
679 679
 					}
680 680
 				}
681 681
 			}
@@ -692,13 +692,13 @@  discard block
 block discarded – undo
692 692
 	 */
693 693
 	public static function file_exists($filename, $timestamp = null) {
694 694
 		$user = User::getUser();
695
-		$view = new View('/' . $user);
695
+		$view = new View('/'.$user);
696 696
 
697 697
 		if ($timestamp) {
698
-			$filename = $filename . '.d' . $timestamp;
698
+			$filename = $filename.'.d'.$timestamp;
699 699
 		}
700 700
 
701
-		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
701
+		$target = Filesystem::normalizePath('files_trashbin/files/'.$filename);
702 702
 		return $view->file_exists($target);
703 703
 	}
704 704
 
@@ -722,11 +722,11 @@  discard block
 block discarded – undo
722 722
 	 */
723 723
 	private static function calculateFreeSpace($trashbinSize, $user) {
724 724
 		$config = \OC::$server->getConfig();
725
-		$userTrashbinSize = (int)$config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
725
+		$userTrashbinSize = (int) $config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
726 726
 		if ($userTrashbinSize > -1) {
727 727
 			return $userTrashbinSize - $trashbinSize;
728 728
 		}
729
-		$systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
729
+		$systemTrashbinSize = (int) $config->getAppValue('files_trashbin', 'trashbin_size', '-1');
730 730
 		if ($systemTrashbinSize > -1) {
731 731
 			return $systemTrashbinSize - $trashbinSize;
732 732
 		}
@@ -835,7 +835,7 @@  discard block
 block discarded – undo
835 835
 			foreach ($files as $file) {
836 836
 				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
837 837
 					$tmp = self::delete($file['name'], $user, $file['mtime']);
838
-					\OC::$server->getLogger()->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
838
+					\OC::$server->getLogger()->info('remove "'.$file['name'].'" ('.$tmp.'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
839 839
 					$availableSpace += $tmp;
840 840
 					$size += $tmp;
841 841
 				} else {
@@ -866,10 +866,10 @@  discard block
 block discarded – undo
866 866
 					$size += self::delete($filename, $user, $timestamp);
867 867
 					$count++;
868 868
 				} catch (\OCP\Files\NotPermittedException $e) {
869
-					\OC::$server->getLogger()->logException($e, ['app' => 'files_trashbin', 'level' => \OCP\ILogger::WARN, 'message' => 'Removing "' . $filename . '" from trashbin failed.']);
869
+					\OC::$server->getLogger()->logException($e, ['app' => 'files_trashbin', 'level' => \OCP\ILogger::WARN, 'message' => 'Removing "'.$filename.'" from trashbin failed.']);
870 870
 				}
871 871
 				\OC::$server->getLogger()->info(
872
-					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
872
+					'Remove "'.$filename.'" from trashbin because it exceeds max retention obligation term.',
873 873
 					['app' => 'files_trashbin']
874 874
 				);
875 875
 			} else {
@@ -895,16 +895,16 @@  discard block
 block discarded – undo
895 895
 			$view->mkdir($destination);
896 896
 			$view->touch($destination, $view->filemtime($source));
897 897
 			foreach ($view->getDirectoryContent($source) as $i) {
898
-				$pathDir = $source . '/' . $i['name'];
898
+				$pathDir = $source.'/'.$i['name'];
899 899
 				if ($view->is_dir($pathDir)) {
900
-					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
900
+					$size += self::copy_recursive($pathDir, $destination.'/'.$i['name'], $view);
901 901
 				} else {
902 902
 					$size += $view->filesize($pathDir);
903
-					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
903
+					$result = $view->copy($pathDir, $destination.'/'.$i['name']);
904 904
 					if (!$result) {
905 905
 						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
906 906
 					}
907
-					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
907
+					$view->touch($destination.'/'.$i['name'], $view->filemtime($pathDir));
908 908
 				}
909 909
 			}
910 910
 		} else {
@@ -926,11 +926,11 @@  discard block
 block discarded – undo
926 926
 	 * @return array
927 927
 	 */
928 928
 	private static function getVersionsFromTrash($filename, $timestamp, $user) {
929
-		$view = new View('/' . $user . '/files_trashbin/versions');
929
+		$view = new View('/'.$user.'/files_trashbin/versions');
930 930
 		$versions = [];
931 931
 
932 932
 		/** @var \OC\Files\Storage\Storage $storage */
933
-		[$storage,] = $view->resolvePath('/');
933
+		[$storage, ] = $view->resolvePath('/');
934 934
 
935 935
 		//force rescan of versions, local storage may not have updated the cache
936 936
 		if (!self::$scannedVersions) {
@@ -942,7 +942,7 @@  discard block
 block discarded – undo
942 942
 		if ($timestamp) {
943 943
 			// fetch for old versions
944 944
 			$escapedTimestamp = \OC::$server->getDatabaseConnection()->escapeLikeParameter($timestamp);
945
-			$pattern .= '.v%.d' . $escapedTimestamp;
945
+			$pattern .= '.v%.d'.$escapedTimestamp;
946 946
 			$offset = -strlen($escapedTimestamp) - 2;
947 947
 		} else {
948 948
 			$pattern .= '.v%';
@@ -956,7 +956,7 @@  discard block
 block discarded – undo
956 956
 			\OC::$server->getLogger(),
957 957
 			$cache
958 958
 		);
959
-		$normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/');
959
+		$normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'.$filename)), '/');
960 960
 		$parentId = $cache->getId($normalizedParentPath);
961 961
 		if ($parentId === -1) {
962 962
 			return [];
@@ -968,7 +968,7 @@  discard block
 block discarded – undo
968 968
 			->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
969 969
 
970 970
 		/** @var CacheEntry[] $matches */
971
-		$matches = array_map(function (array $data) {
971
+		$matches = array_map(function(array $data) {
972 972
 			return Cache::cacheEntryFromData($data, \OC::$server->getMimeTypeLoader());
973 973
 		}, $query->execute()->fetchAll());
974 974
 
@@ -998,18 +998,18 @@  discard block
 block discarded – undo
998 998
 		$name = pathinfo($filename, PATHINFO_FILENAME);
999 999
 		$l = \OC::$server->getL10N('files_trashbin');
1000 1000
 
1001
-		$location = '/' . trim($location, '/');
1001
+		$location = '/'.trim($location, '/');
1002 1002
 
1003 1003
 		// if extension is not empty we set a dot in front of it
1004 1004
 		if ($ext !== '') {
1005
-			$ext = '.' . $ext;
1005
+			$ext = '.'.$ext;
1006 1006
 		}
1007 1007
 
1008
-		if ($view->file_exists('files' . $location . '/' . $filename)) {
1008
+		if ($view->file_exists('files'.$location.'/'.$filename)) {
1009 1009
 			$i = 2;
1010
-			$uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
1011
-			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
1012
-				$uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
1010
+			$uniqueName = $name." (".$l->t("restored").")".$ext;
1011
+			while ($view->file_exists('files'.$location.'/'.$uniqueName)) {
1012
+				$uniqueName = $name." (".$l->t("restored")." ".$i.")".$ext;
1013 1013
 				$i++;
1014 1014
 			}
1015 1015
 
@@ -1026,7 +1026,7 @@  discard block
 block discarded – undo
1026 1026
 	 * @return integer size of the folder
1027 1027
 	 */
1028 1028
 	private static function calculateSize($view) {
1029
-		$root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
1029
+		$root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT.'/data').$view->getAbsolutePath('');
1030 1030
 		if (!file_exists($root)) {
1031 1031
 			return 0;
1032 1032
 		}
@@ -1057,7 +1057,7 @@  discard block
 block discarded – undo
1057 1057
 	 * @return integer trash bin size
1058 1058
 	 */
1059 1059
 	private static function getTrashbinSize($user) {
1060
-		$view = new View('/' . $user);
1060
+		$view = new View('/'.$user);
1061 1061
 		$fileInfo = $view->getFileInfo('/files_trashbin');
1062 1062
 		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1063 1063
 	}
@@ -1069,7 +1069,7 @@  discard block
 block discarded – undo
1069 1069
 	 * @return bool
1070 1070
 	 */
1071 1071
 	public static function isEmpty($user) {
1072
-		$view = new View('/' . $user . '/files_trashbin');
1072
+		$view = new View('/'.$user.'/files_trashbin');
1073 1073
 		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1074 1074
 			while ($file = readdir($dh)) {
1075 1075
 				if (!Filesystem::isIgnoredDir($file)) {
Please login to merge, or discard this patch.