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