Completed
Push — master ( 6848dc...c0dc17 )
by
unknown
40:28
created
apps/files_trashbin/tests/TrashbinTest.php 2 patches
Indentation   +666 added lines, -666 removed lines patch added patch discarded remove patch
@@ -37,686 +37,686 @@
 block discarded – undo
37 37
  * @group DB
38 38
  */
39 39
 class TrashbinTest extends \Test\TestCase {
40
-	public const TEST_TRASHBIN_USER1 = 'test-trashbin-user1';
41
-	public const TEST_TRASHBIN_USER2 = 'test-trashbin-user2';
42
-
43
-	private $trashRoot1;
44
-	private $trashRoot2;
45
-
46
-	private static $rememberRetentionObligation;
47
-	private static bool $trashBinStatus;
48
-	private View $rootView;
49
-
50
-	public static function setUpBeforeClass(): void {
51
-		parent::setUpBeforeClass();
52
-
53
-		$appManager = Server::get(IAppManager::class);
54
-		self::$trashBinStatus = $appManager->isEnabledForUser('files_trashbin');
55
-
56
-		// reset backend
57
-		Server::get(IUserManager::class)->clearBackends();
58
-		Server::get(IUserManager::class)->registerBackend(new Database());
59
-
60
-		// clear share hooks
61
-		\OC_Hook::clear('OCP\\Share');
62
-		\OC::registerShareHooks(Server::get(SystemConfig::class));
63
-
64
-		// init files sharing
65
-		new Application();
66
-
67
-		//disable encryption
68
-		Server::get(IAppManager::class)->disableApp('encryption');
69
-
70
-		$config = Server::get(IConfig::class);
71
-		//configure trashbin
72
-		self::$rememberRetentionObligation = (string)$config->getSystemValue('trashbin_retention_obligation', Expiration::DEFAULT_RETENTION_OBLIGATION);
73
-		/** @var Expiration $expiration */
74
-		$expiration = Server::get(Expiration::class);
75
-		$expiration->setRetentionObligation('auto, 2');
76
-
77
-		// register trashbin hooks
78
-		$trashbinApp = new TrashbinApplication();
79
-		$trashbinApp->boot(new BootContext(new DIContainer('', [], \OC::$server)));
80
-
81
-		// create test user
82
-		self::loginHelper(self::TEST_TRASHBIN_USER2, true);
83
-		self::loginHelper(self::TEST_TRASHBIN_USER1, true);
84
-	}
85
-
86
-
87
-	public static function tearDownAfterClass(): void {
88
-		// cleanup test user
89
-		$user = Server::get(IUserManager::class)->get(self::TEST_TRASHBIN_USER1);
90
-		if ($user !== null) {
91
-			$user->delete();
92
-		}
93
-
94
-		/** @var Expiration $expiration */
95
-		$expiration = Server::get(Expiration::class);
96
-		$expiration->setRetentionObligation(self::$rememberRetentionObligation);
97
-
98
-		\OC_Hook::clear();
99
-
100
-		Filesystem::getLoader()->removeStorageWrapper('oc_trashbin');
101
-
102
-		if (self::$trashBinStatus) {
103
-			Server::get(IAppManager::class)->enableApp('files_trashbin');
104
-		}
105
-
106
-		parent::tearDownAfterClass();
107
-	}
108
-
109
-	protected function setUp(): void {
110
-		parent::setUp();
111
-
112
-		Server::get(IAppManager::class)->enableApp('files_trashbin');
113
-		$config = Server::get(IConfig::class);
114
-		$mockConfig = $this->getMockBuilder(AllConfig::class)
115
-			->onlyMethods(['getSystemValue'])
116
-			->setConstructorArgs([Server::get(SystemConfig::class)])
117
-			->getMock();
118
-		$mockConfig->expects($this->any())
119
-			->method('getSystemValue')
120
-			->willReturnCallback(static function ($key, $default) use ($config) {
121
-				if ($key === 'filesystem_check_changes') {
122
-					return Watcher::CHECK_ONCE;
123
-				} else {
124
-					return $config->getSystemValue($key, $default);
125
-				}
126
-			});
127
-		$this->overwriteService(AllConfig::class, $mockConfig);
128
-
129
-		$this->trashRoot1 = '/' . self::TEST_TRASHBIN_USER1 . '/files_trashbin';
130
-		$this->trashRoot2 = '/' . self::TEST_TRASHBIN_USER2 . '/files_trashbin';
131
-		$this->rootView = new View();
132
-		self::loginHelper(self::TEST_TRASHBIN_USER1);
133
-	}
134
-
135
-	protected function tearDown(): void {
136
-		$this->restoreService(AllConfig::class);
137
-		// disable trashbin to be able to properly clean up
138
-		Server::get(IAppManager::class)->disableApp('files_trashbin');
139
-
140
-		$this->rootView->deleteAll('/' . self::TEST_TRASHBIN_USER1 . '/files');
141
-		$this->rootView->deleteAll('/' . self::TEST_TRASHBIN_USER2 . '/files');
142
-		$this->rootView->deleteAll($this->trashRoot1);
143
-		$this->rootView->deleteAll($this->trashRoot2);
144
-
145
-		// clear trash table
146
-		$connection = Server::get(IDBConnection::class);
147
-		$connection->executeUpdate('DELETE FROM `*PREFIX*files_trash`');
148
-
149
-		parent::tearDown();
150
-	}
151
-
152
-	/**
153
-	 * test expiration of files older then the max storage time defined for the trash
154
-	 */
155
-	public function testExpireOldFiles(): void {
156
-
157
-		/** @var ITimeFactory $time */
158
-		$time = Server::get(ITimeFactory::class);
159
-		$currentTime = $time->getTime();
160
-		$expireAt = $currentTime - 2 * 24 * 60 * 60;
161
-		$expiredDate = $currentTime - 3 * 24 * 60 * 60;
162
-
163
-		// create some files
164
-		Filesystem::file_put_contents('file1.txt', 'file1');
165
-		Filesystem::file_put_contents('file2.txt', 'file2');
166
-		Filesystem::file_put_contents('file3.txt', 'file3');
167
-
168
-		// delete them so that they end up in the trash bin
169
-		Filesystem::unlink('file1.txt');
170
-		Filesystem::unlink('file2.txt');
171
-		Filesystem::unlink('file3.txt');
172
-
173
-		//make sure that files are in the trash bin
174
-		$filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'name');
175
-		$this->assertSame(3, count($filesInTrash));
176
-
177
-		// every second file will get a date in the past so that it will get expired
178
-		$manipulatedList = $this->manipulateDeleteTime($filesInTrash, $this->trashRoot1, $expiredDate);
179
-
180
-		$testClass = new TrashbinForTesting();
181
-		[$sizeOfDeletedFiles, $count] = $testClass->dummyDeleteExpiredFiles($manipulatedList, $expireAt);
182
-
183
-		$this->assertSame(10, $sizeOfDeletedFiles);
184
-		$this->assertSame(2, $count);
185
-
186
-		// only file2.txt should be left
187
-		$remainingFiles = array_slice($manipulatedList, $count);
188
-		$this->assertCount(1, $remainingFiles);
189
-		$remainingFile = reset($remainingFiles);
190
-		// TODO: failing test
191
-		#$this->assertSame('file2.txt', $remainingFile['name']);
192
-
193
-		// check that file1.txt and file3.txt was really deleted
194
-		$newTrashContent = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1);
195
-		$this->assertCount(1, $newTrashContent);
196
-		$element = reset($newTrashContent);
197
-		// TODO: failing test
198
-		#$this->assertSame('file2.txt', $element['name']);
199
-	}
200
-
201
-	/**
202
-	 * test expiration of files older then the max storage time defined for the trash
203
-	 * in this test we delete a shared file and check if both trash bins, the one from
204
-	 * the owner of the file and the one from the user who deleted the file get expired
205
-	 * correctly
206
-	 */
207
-	public function testExpireOldFilesShared(): void {
208
-		$currentTime = time();
209
-		$folder = 'trashTest-' . $currentTime . '/';
210
-		$expiredDate = $currentTime - 3 * 24 * 60 * 60;
211
-
212
-		// create some files
213
-		Filesystem::mkdir($folder);
214
-		Filesystem::file_put_contents($folder . 'user1-1.txt', 'file1');
215
-		Filesystem::file_put_contents($folder . 'user1-2.txt', 'file2');
216
-		Filesystem::file_put_contents($folder . 'user1-3.txt', 'file3');
217
-		Filesystem::file_put_contents($folder . 'user1-4.txt', 'file4');
218
-
219
-		//share user1-4.txt with user2
220
-		$node = \OC::$server->getUserFolder(self::TEST_TRASHBIN_USER1)->get($folder);
221
-		$share = Server::get(\OCP\Share\IManager::class)->newShare();
222
-		$share->setShareType(IShare::TYPE_USER)
223
-			->setNode($node)
224
-			->setSharedBy(self::TEST_TRASHBIN_USER1)
225
-			->setSharedWith(self::TEST_TRASHBIN_USER2)
226
-			->setPermissions(Constants::PERMISSION_ALL);
227
-		$share = Server::get(\OCP\Share\IManager::class)->createShare($share);
228
-		Server::get(\OCP\Share\IManager::class)->acceptShare($share, self::TEST_TRASHBIN_USER2);
229
-
230
-		// delete them so that they end up in the trash bin
231
-		Filesystem::unlink($folder . 'user1-1.txt');
232
-		Filesystem::unlink($folder . 'user1-2.txt');
233
-		Filesystem::unlink($folder . 'user1-3.txt');
234
-
235
-		$filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'name');
236
-		$this->assertSame(3, count($filesInTrash));
237
-
238
-		// every second file will get a date in the past so that it will get expired
239
-		$this->manipulateDeleteTime($filesInTrash, $this->trashRoot1, $expiredDate);
240
-
241
-		// login as user2
242
-		self::loginHelper(self::TEST_TRASHBIN_USER2);
243
-
244
-		$this->assertTrue(Filesystem::file_exists($folder . 'user1-4.txt'));
245
-
246
-		// create some files
247
-		Filesystem::file_put_contents('user2-1.txt', 'file1');
248
-		Filesystem::file_put_contents('user2-2.txt', 'file2');
249
-
250
-		// delete them so that they end up in the trash bin
251
-		Filesystem::unlink('user2-1.txt');
252
-		Filesystem::unlink('user2-2.txt');
253
-
254
-		$filesInTrashUser2 = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER2, 'name');
255
-		$this->assertSame(2, count($filesInTrashUser2));
256
-
257
-		// every second file will get a date in the past so that it will get expired
258
-		$this->manipulateDeleteTime($filesInTrashUser2, $this->trashRoot2, $expiredDate);
259
-
260
-		Filesystem::unlink($folder . 'user1-4.txt');
261
-
262
-		$this->runCommands();
263
-
264
-		$filesInTrashUser2AfterDelete = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER2);
265
-
266
-		// user2-1.txt should have been expired
267
-		$this->verifyArray($filesInTrashUser2AfterDelete, ['user2-2.txt', 'user1-4.txt']);
268
-
269
-		self::loginHelper(self::TEST_TRASHBIN_USER1);
270
-
271
-		// user1-1.txt and user1-3.txt should have been expired
272
-		$filesInTrashUser1AfterDelete = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1);
273
-
274
-		$this->verifyArray($filesInTrashUser1AfterDelete, ['user1-2.txt', 'user1-4.txt']);
275
-	}
276
-
277
-	/**
278
-	 * verify that the array contains the expected results
279
-	 *
280
-	 * @param FileInfo[] $result
281
-	 * @param string[] $expected
282
-	 */
283
-	private function verifyArray(array $result, array $expected): void {
284
-		$this->assertCount(count($expected), $result);
285
-		foreach ($expected as $expectedFile) {
286
-			$found = false;
287
-			foreach ($result as $fileInTrash) {
288
-				if ($expectedFile === $fileInTrash['name']) {
289
-					$found = true;
290
-					break;
291
-				}
292
-			}
293
-			if (!$found) {
294
-				// if we didn't found the expected file, something went wrong
295
-				$this->assertTrue(false, "can't find expected file '" . $expectedFile . "' in trash bin");
296
-			}
297
-		}
298
-	}
299
-
300
-	/**
301
-	 * @param FileInfo[] $files
302
-	 */
303
-	private function manipulateDeleteTime(array $files, string $trashRoot, int $expireDate): array {
304
-		$counter = 0;
305
-		foreach ($files as &$file) {
306
-			// modify every second file
307
-			$counter = ($counter + 1) % 2;
308
-			if ($counter === 1) {
309
-				$source = $trashRoot . '/files/' . $file['name'] . '.d' . $file['mtime'];
310
-				$target = Filesystem::normalizePath($trashRoot . '/files/' . $file['name'] . '.d' . $expireDate);
311
-				$this->rootView->rename($source, $target);
312
-				$file['mtime'] = $expireDate;
313
-			}
314
-		}
315
-		return \OCA\Files\Helper::sortFiles($files, 'mtime');
316
-	}
317
-
318
-
319
-	/**
320
-	 * test expiration of old files in the trash bin until the max size
321
-	 * of the trash bin is met again
322
-	 */
323
-	public function testExpireOldFilesUtilLimitsAreMet(): void {
324
-
325
-		// create some files
326
-		Filesystem::file_put_contents('file1.txt', 'file1');
327
-		Filesystem::file_put_contents('file2.txt', 'file2');
328
-		Filesystem::file_put_contents('file3.txt', 'file3');
329
-
330
-		// delete them so that they end up in the trash bin
331
-		Filesystem::unlink('file3.txt');
332
-		sleep(1); // make sure that every file has a unique mtime
333
-		Filesystem::unlink('file2.txt');
334
-		sleep(1); // make sure that every file has a unique mtime
335
-		Filesystem::unlink('file1.txt');
336
-
337
-		//make sure that files are in the trash bin
338
-		$filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
339
-		$this->assertSame(3, count($filesInTrash));
340
-
341
-		$testClass = new TrashbinForTesting();
342
-		$sizeOfDeletedFiles = $testClass->dummyDeleteFiles($filesInTrash, -8);
343
-
344
-		// the two oldest files (file3.txt and file2.txt) should be deleted
345
-		$this->assertSame(10, $sizeOfDeletedFiles);
346
-
347
-		$newTrashContent = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1);
348
-		$this->assertSame(1, count($newTrashContent));
349
-		$element = reset($newTrashContent);
350
-		$this->assertSame('file1.txt', $element['name']);
351
-	}
352
-
353
-	/**
354
-	 * Test restoring a file
355
-	 */
356
-	public function testRestoreFileInRoot(): void {
357
-		$userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
358
-		$file = $userFolder->newFile('file1.txt');
359
-		$file->putContent('foo');
360
-
361
-		$this->assertTrue($userFolder->nodeExists('file1.txt'));
362
-
363
-		$file->delete();
364
-
365
-		$this->assertFalse($userFolder->nodeExists('file1.txt'));
366
-
367
-		$filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
368
-		$this->assertCount(1, $filesInTrash);
369
-
370
-		/** @var FileInfo */
371
-		$trashedFile = $filesInTrash[0];
372
-
373
-		$this->assertTrue(
374
-			Trashbin::restore(
375
-				'file1.txt.d' . $trashedFile->getMtime(),
376
-				$trashedFile->getName(),
377
-				$trashedFile->getMtime()
378
-			)
379
-		);
380
-
381
-		$file = $userFolder->get('file1.txt');
382
-		$this->assertEquals('foo', $file->getContent());
383
-	}
384
-
385
-	/**
386
-	 * Test restoring a file in subfolder
387
-	 */
388
-	public function testRestoreFileInSubfolder(): void {
389
-		$userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
390
-		$folder = $userFolder->newFolder('folder');
391
-		$file = $folder->newFile('file1.txt');
392
-		$file->putContent('foo');
393
-
394
-		$this->assertTrue($userFolder->nodeExists('folder/file1.txt'));
395
-
396
-		$file->delete();
397
-
398
-		$this->assertFalse($userFolder->nodeExists('folder/file1.txt'));
399
-
400
-		$filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
401
-		$this->assertCount(1, $filesInTrash);
402
-
403
-		/** @var FileInfo */
404
-		$trashedFile = $filesInTrash[0];
405
-
406
-		$this->assertTrue(
407
-			Trashbin::restore(
408
-				'file1.txt.d' . $trashedFile->getMtime(),
409
-				$trashedFile->getName(),
410
-				$trashedFile->getMtime()
411
-			)
412
-		);
413
-
414
-		$file = $userFolder->get('folder/file1.txt');
415
-		$this->assertEquals('foo', $file->getContent());
416
-	}
417
-
418
-	/**
419
-	 * Test restoring a folder
420
-	 */
421
-	public function testRestoreFolder(): void {
422
-		$userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
423
-		$folder = $userFolder->newFolder('folder');
424
-		$file = $folder->newFile('file1.txt');
425
-		$file->putContent('foo');
426
-
427
-		$this->assertTrue($userFolder->nodeExists('folder'));
428
-
429
-		$folder->delete();
430
-
431
-		$this->assertFalse($userFolder->nodeExists('folder'));
432
-
433
-		$filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
434
-		$this->assertCount(1, $filesInTrash);
40
+    public const TEST_TRASHBIN_USER1 = 'test-trashbin-user1';
41
+    public const TEST_TRASHBIN_USER2 = 'test-trashbin-user2';
42
+
43
+    private $trashRoot1;
44
+    private $trashRoot2;
45
+
46
+    private static $rememberRetentionObligation;
47
+    private static bool $trashBinStatus;
48
+    private View $rootView;
49
+
50
+    public static function setUpBeforeClass(): void {
51
+        parent::setUpBeforeClass();
52
+
53
+        $appManager = Server::get(IAppManager::class);
54
+        self::$trashBinStatus = $appManager->isEnabledForUser('files_trashbin');
55
+
56
+        // reset backend
57
+        Server::get(IUserManager::class)->clearBackends();
58
+        Server::get(IUserManager::class)->registerBackend(new Database());
59
+
60
+        // clear share hooks
61
+        \OC_Hook::clear('OCP\\Share');
62
+        \OC::registerShareHooks(Server::get(SystemConfig::class));
63
+
64
+        // init files sharing
65
+        new Application();
66
+
67
+        //disable encryption
68
+        Server::get(IAppManager::class)->disableApp('encryption');
69
+
70
+        $config = Server::get(IConfig::class);
71
+        //configure trashbin
72
+        self::$rememberRetentionObligation = (string)$config->getSystemValue('trashbin_retention_obligation', Expiration::DEFAULT_RETENTION_OBLIGATION);
73
+        /** @var Expiration $expiration */
74
+        $expiration = Server::get(Expiration::class);
75
+        $expiration->setRetentionObligation('auto, 2');
76
+
77
+        // register trashbin hooks
78
+        $trashbinApp = new TrashbinApplication();
79
+        $trashbinApp->boot(new BootContext(new DIContainer('', [], \OC::$server)));
80
+
81
+        // create test user
82
+        self::loginHelper(self::TEST_TRASHBIN_USER2, true);
83
+        self::loginHelper(self::TEST_TRASHBIN_USER1, true);
84
+    }
85
+
86
+
87
+    public static function tearDownAfterClass(): void {
88
+        // cleanup test user
89
+        $user = Server::get(IUserManager::class)->get(self::TEST_TRASHBIN_USER1);
90
+        if ($user !== null) {
91
+            $user->delete();
92
+        }
93
+
94
+        /** @var Expiration $expiration */
95
+        $expiration = Server::get(Expiration::class);
96
+        $expiration->setRetentionObligation(self::$rememberRetentionObligation);
97
+
98
+        \OC_Hook::clear();
99
+
100
+        Filesystem::getLoader()->removeStorageWrapper('oc_trashbin');
101
+
102
+        if (self::$trashBinStatus) {
103
+            Server::get(IAppManager::class)->enableApp('files_trashbin');
104
+        }
105
+
106
+        parent::tearDownAfterClass();
107
+    }
108
+
109
+    protected function setUp(): void {
110
+        parent::setUp();
111
+
112
+        Server::get(IAppManager::class)->enableApp('files_trashbin');
113
+        $config = Server::get(IConfig::class);
114
+        $mockConfig = $this->getMockBuilder(AllConfig::class)
115
+            ->onlyMethods(['getSystemValue'])
116
+            ->setConstructorArgs([Server::get(SystemConfig::class)])
117
+            ->getMock();
118
+        $mockConfig->expects($this->any())
119
+            ->method('getSystemValue')
120
+            ->willReturnCallback(static function ($key, $default) use ($config) {
121
+                if ($key === 'filesystem_check_changes') {
122
+                    return Watcher::CHECK_ONCE;
123
+                } else {
124
+                    return $config->getSystemValue($key, $default);
125
+                }
126
+            });
127
+        $this->overwriteService(AllConfig::class, $mockConfig);
128
+
129
+        $this->trashRoot1 = '/' . self::TEST_TRASHBIN_USER1 . '/files_trashbin';
130
+        $this->trashRoot2 = '/' . self::TEST_TRASHBIN_USER2 . '/files_trashbin';
131
+        $this->rootView = new View();
132
+        self::loginHelper(self::TEST_TRASHBIN_USER1);
133
+    }
134
+
135
+    protected function tearDown(): void {
136
+        $this->restoreService(AllConfig::class);
137
+        // disable trashbin to be able to properly clean up
138
+        Server::get(IAppManager::class)->disableApp('files_trashbin');
139
+
140
+        $this->rootView->deleteAll('/' . self::TEST_TRASHBIN_USER1 . '/files');
141
+        $this->rootView->deleteAll('/' . self::TEST_TRASHBIN_USER2 . '/files');
142
+        $this->rootView->deleteAll($this->trashRoot1);
143
+        $this->rootView->deleteAll($this->trashRoot2);
144
+
145
+        // clear trash table
146
+        $connection = Server::get(IDBConnection::class);
147
+        $connection->executeUpdate('DELETE FROM `*PREFIX*files_trash`');
148
+
149
+        parent::tearDown();
150
+    }
151
+
152
+    /**
153
+     * test expiration of files older then the max storage time defined for the trash
154
+     */
155
+    public function testExpireOldFiles(): void {
156
+
157
+        /** @var ITimeFactory $time */
158
+        $time = Server::get(ITimeFactory::class);
159
+        $currentTime = $time->getTime();
160
+        $expireAt = $currentTime - 2 * 24 * 60 * 60;
161
+        $expiredDate = $currentTime - 3 * 24 * 60 * 60;
162
+
163
+        // create some files
164
+        Filesystem::file_put_contents('file1.txt', 'file1');
165
+        Filesystem::file_put_contents('file2.txt', 'file2');
166
+        Filesystem::file_put_contents('file3.txt', 'file3');
167
+
168
+        // delete them so that they end up in the trash bin
169
+        Filesystem::unlink('file1.txt');
170
+        Filesystem::unlink('file2.txt');
171
+        Filesystem::unlink('file3.txt');
172
+
173
+        //make sure that files are in the trash bin
174
+        $filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'name');
175
+        $this->assertSame(3, count($filesInTrash));
176
+
177
+        // every second file will get a date in the past so that it will get expired
178
+        $manipulatedList = $this->manipulateDeleteTime($filesInTrash, $this->trashRoot1, $expiredDate);
179
+
180
+        $testClass = new TrashbinForTesting();
181
+        [$sizeOfDeletedFiles, $count] = $testClass->dummyDeleteExpiredFiles($manipulatedList, $expireAt);
182
+
183
+        $this->assertSame(10, $sizeOfDeletedFiles);
184
+        $this->assertSame(2, $count);
185
+
186
+        // only file2.txt should be left
187
+        $remainingFiles = array_slice($manipulatedList, $count);
188
+        $this->assertCount(1, $remainingFiles);
189
+        $remainingFile = reset($remainingFiles);
190
+        // TODO: failing test
191
+        #$this->assertSame('file2.txt', $remainingFile['name']);
192
+
193
+        // check that file1.txt and file3.txt was really deleted
194
+        $newTrashContent = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1);
195
+        $this->assertCount(1, $newTrashContent);
196
+        $element = reset($newTrashContent);
197
+        // TODO: failing test
198
+        #$this->assertSame('file2.txt', $element['name']);
199
+    }
200
+
201
+    /**
202
+     * test expiration of files older then the max storage time defined for the trash
203
+     * in this test we delete a shared file and check if both trash bins, the one from
204
+     * the owner of the file and the one from the user who deleted the file get expired
205
+     * correctly
206
+     */
207
+    public function testExpireOldFilesShared(): void {
208
+        $currentTime = time();
209
+        $folder = 'trashTest-' . $currentTime . '/';
210
+        $expiredDate = $currentTime - 3 * 24 * 60 * 60;
211
+
212
+        // create some files
213
+        Filesystem::mkdir($folder);
214
+        Filesystem::file_put_contents($folder . 'user1-1.txt', 'file1');
215
+        Filesystem::file_put_contents($folder . 'user1-2.txt', 'file2');
216
+        Filesystem::file_put_contents($folder . 'user1-3.txt', 'file3');
217
+        Filesystem::file_put_contents($folder . 'user1-4.txt', 'file4');
218
+
219
+        //share user1-4.txt with user2
220
+        $node = \OC::$server->getUserFolder(self::TEST_TRASHBIN_USER1)->get($folder);
221
+        $share = Server::get(\OCP\Share\IManager::class)->newShare();
222
+        $share->setShareType(IShare::TYPE_USER)
223
+            ->setNode($node)
224
+            ->setSharedBy(self::TEST_TRASHBIN_USER1)
225
+            ->setSharedWith(self::TEST_TRASHBIN_USER2)
226
+            ->setPermissions(Constants::PERMISSION_ALL);
227
+        $share = Server::get(\OCP\Share\IManager::class)->createShare($share);
228
+        Server::get(\OCP\Share\IManager::class)->acceptShare($share, self::TEST_TRASHBIN_USER2);
229
+
230
+        // delete them so that they end up in the trash bin
231
+        Filesystem::unlink($folder . 'user1-1.txt');
232
+        Filesystem::unlink($folder . 'user1-2.txt');
233
+        Filesystem::unlink($folder . 'user1-3.txt');
234
+
235
+        $filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'name');
236
+        $this->assertSame(3, count($filesInTrash));
237
+
238
+        // every second file will get a date in the past so that it will get expired
239
+        $this->manipulateDeleteTime($filesInTrash, $this->trashRoot1, $expiredDate);
240
+
241
+        // login as user2
242
+        self::loginHelper(self::TEST_TRASHBIN_USER2);
243
+
244
+        $this->assertTrue(Filesystem::file_exists($folder . 'user1-4.txt'));
245
+
246
+        // create some files
247
+        Filesystem::file_put_contents('user2-1.txt', 'file1');
248
+        Filesystem::file_put_contents('user2-2.txt', 'file2');
249
+
250
+        // delete them so that they end up in the trash bin
251
+        Filesystem::unlink('user2-1.txt');
252
+        Filesystem::unlink('user2-2.txt');
253
+
254
+        $filesInTrashUser2 = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER2, 'name');
255
+        $this->assertSame(2, count($filesInTrashUser2));
256
+
257
+        // every second file will get a date in the past so that it will get expired
258
+        $this->manipulateDeleteTime($filesInTrashUser2, $this->trashRoot2, $expiredDate);
259
+
260
+        Filesystem::unlink($folder . 'user1-4.txt');
261
+
262
+        $this->runCommands();
263
+
264
+        $filesInTrashUser2AfterDelete = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER2);
265
+
266
+        // user2-1.txt should have been expired
267
+        $this->verifyArray($filesInTrashUser2AfterDelete, ['user2-2.txt', 'user1-4.txt']);
268
+
269
+        self::loginHelper(self::TEST_TRASHBIN_USER1);
270
+
271
+        // user1-1.txt and user1-3.txt should have been expired
272
+        $filesInTrashUser1AfterDelete = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1);
273
+
274
+        $this->verifyArray($filesInTrashUser1AfterDelete, ['user1-2.txt', 'user1-4.txt']);
275
+    }
276
+
277
+    /**
278
+     * verify that the array contains the expected results
279
+     *
280
+     * @param FileInfo[] $result
281
+     * @param string[] $expected
282
+     */
283
+    private function verifyArray(array $result, array $expected): void {
284
+        $this->assertCount(count($expected), $result);
285
+        foreach ($expected as $expectedFile) {
286
+            $found = false;
287
+            foreach ($result as $fileInTrash) {
288
+                if ($expectedFile === $fileInTrash['name']) {
289
+                    $found = true;
290
+                    break;
291
+                }
292
+            }
293
+            if (!$found) {
294
+                // if we didn't found the expected file, something went wrong
295
+                $this->assertTrue(false, "can't find expected file '" . $expectedFile . "' in trash bin");
296
+            }
297
+        }
298
+    }
299
+
300
+    /**
301
+     * @param FileInfo[] $files
302
+     */
303
+    private function manipulateDeleteTime(array $files, string $trashRoot, int $expireDate): array {
304
+        $counter = 0;
305
+        foreach ($files as &$file) {
306
+            // modify every second file
307
+            $counter = ($counter + 1) % 2;
308
+            if ($counter === 1) {
309
+                $source = $trashRoot . '/files/' . $file['name'] . '.d' . $file['mtime'];
310
+                $target = Filesystem::normalizePath($trashRoot . '/files/' . $file['name'] . '.d' . $expireDate);
311
+                $this->rootView->rename($source, $target);
312
+                $file['mtime'] = $expireDate;
313
+            }
314
+        }
315
+        return \OCA\Files\Helper::sortFiles($files, 'mtime');
316
+    }
317
+
318
+
319
+    /**
320
+     * test expiration of old files in the trash bin until the max size
321
+     * of the trash bin is met again
322
+     */
323
+    public function testExpireOldFilesUtilLimitsAreMet(): void {
324
+
325
+        // create some files
326
+        Filesystem::file_put_contents('file1.txt', 'file1');
327
+        Filesystem::file_put_contents('file2.txt', 'file2');
328
+        Filesystem::file_put_contents('file3.txt', 'file3');
329
+
330
+        // delete them so that they end up in the trash bin
331
+        Filesystem::unlink('file3.txt');
332
+        sleep(1); // make sure that every file has a unique mtime
333
+        Filesystem::unlink('file2.txt');
334
+        sleep(1); // make sure that every file has a unique mtime
335
+        Filesystem::unlink('file1.txt');
336
+
337
+        //make sure that files are in the trash bin
338
+        $filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
339
+        $this->assertSame(3, count($filesInTrash));
340
+
341
+        $testClass = new TrashbinForTesting();
342
+        $sizeOfDeletedFiles = $testClass->dummyDeleteFiles($filesInTrash, -8);
343
+
344
+        // the two oldest files (file3.txt and file2.txt) should be deleted
345
+        $this->assertSame(10, $sizeOfDeletedFiles);
346
+
347
+        $newTrashContent = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1);
348
+        $this->assertSame(1, count($newTrashContent));
349
+        $element = reset($newTrashContent);
350
+        $this->assertSame('file1.txt', $element['name']);
351
+    }
352
+
353
+    /**
354
+     * Test restoring a file
355
+     */
356
+    public function testRestoreFileInRoot(): void {
357
+        $userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
358
+        $file = $userFolder->newFile('file1.txt');
359
+        $file->putContent('foo');
360
+
361
+        $this->assertTrue($userFolder->nodeExists('file1.txt'));
362
+
363
+        $file->delete();
364
+
365
+        $this->assertFalse($userFolder->nodeExists('file1.txt'));
366
+
367
+        $filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
368
+        $this->assertCount(1, $filesInTrash);
369
+
370
+        /** @var FileInfo */
371
+        $trashedFile = $filesInTrash[0];
372
+
373
+        $this->assertTrue(
374
+            Trashbin::restore(
375
+                'file1.txt.d' . $trashedFile->getMtime(),
376
+                $trashedFile->getName(),
377
+                $trashedFile->getMtime()
378
+            )
379
+        );
380
+
381
+        $file = $userFolder->get('file1.txt');
382
+        $this->assertEquals('foo', $file->getContent());
383
+    }
384
+
385
+    /**
386
+     * Test restoring a file in subfolder
387
+     */
388
+    public function testRestoreFileInSubfolder(): void {
389
+        $userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
390
+        $folder = $userFolder->newFolder('folder');
391
+        $file = $folder->newFile('file1.txt');
392
+        $file->putContent('foo');
393
+
394
+        $this->assertTrue($userFolder->nodeExists('folder/file1.txt'));
395
+
396
+        $file->delete();
397
+
398
+        $this->assertFalse($userFolder->nodeExists('folder/file1.txt'));
399
+
400
+        $filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
401
+        $this->assertCount(1, $filesInTrash);
402
+
403
+        /** @var FileInfo */
404
+        $trashedFile = $filesInTrash[0];
405
+
406
+        $this->assertTrue(
407
+            Trashbin::restore(
408
+                'file1.txt.d' . $trashedFile->getMtime(),
409
+                $trashedFile->getName(),
410
+                $trashedFile->getMtime()
411
+            )
412
+        );
413
+
414
+        $file = $userFolder->get('folder/file1.txt');
415
+        $this->assertEquals('foo', $file->getContent());
416
+    }
417
+
418
+    /**
419
+     * Test restoring a folder
420
+     */
421
+    public function testRestoreFolder(): void {
422
+        $userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
423
+        $folder = $userFolder->newFolder('folder');
424
+        $file = $folder->newFile('file1.txt');
425
+        $file->putContent('foo');
426
+
427
+        $this->assertTrue($userFolder->nodeExists('folder'));
428
+
429
+        $folder->delete();
430
+
431
+        $this->assertFalse($userFolder->nodeExists('folder'));
432
+
433
+        $filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
434
+        $this->assertCount(1, $filesInTrash);
435 435
 
436
-		/** @var FileInfo */
437
-		$trashedFolder = $filesInTrash[0];
436
+        /** @var FileInfo */
437
+        $trashedFolder = $filesInTrash[0];
438 438
 
439
-		$this->assertTrue(
440
-			Trashbin::restore(
441
-				'folder.d' . $trashedFolder->getMtime(),
442
-				$trashedFolder->getName(),
443
-				$trashedFolder->getMtime()
444
-			)
445
-		);
439
+        $this->assertTrue(
440
+            Trashbin::restore(
441
+                'folder.d' . $trashedFolder->getMtime(),
442
+                $trashedFolder->getName(),
443
+                $trashedFolder->getMtime()
444
+            )
445
+        );
446 446
 
447
-		$file = $userFolder->get('folder/file1.txt');
448
-		$this->assertEquals('foo', $file->getContent());
449
-	}
450
-
451
-	/**
452
-	 * Test restoring a file from inside a trashed folder
453
-	 */
454
-	public function testRestoreFileFromTrashedSubfolder(): void {
455
-		$userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
456
-		$folder = $userFolder->newFolder('folder');
457
-		$file = $folder->newFile('file1.txt');
458
-		$file->putContent('foo');
447
+        $file = $userFolder->get('folder/file1.txt');
448
+        $this->assertEquals('foo', $file->getContent());
449
+    }
450
+
451
+    /**
452
+     * Test restoring a file from inside a trashed folder
453
+     */
454
+    public function testRestoreFileFromTrashedSubfolder(): void {
455
+        $userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
456
+        $folder = $userFolder->newFolder('folder');
457
+        $file = $folder->newFile('file1.txt');
458
+        $file->putContent('foo');
459 459
 
460
-		$this->assertTrue($userFolder->nodeExists('folder'));
460
+        $this->assertTrue($userFolder->nodeExists('folder'));
461 461
 
462
-		$folder->delete();
463
-
464
-		$this->assertFalse($userFolder->nodeExists('folder'));
465
-
466
-		$filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
467
-		$this->assertCount(1, $filesInTrash);
468
-
469
-		/** @var FileInfo */
470
-		$trashedFile = $filesInTrash[0];
471
-
472
-		$this->assertTrue(
473
-			Trashbin::restore(
474
-				'folder.d' . $trashedFile->getMtime() . '/file1.txt',
475
-				'file1.txt',
476
-				$trashedFile->getMtime()
477
-			)
478
-		);
462
+        $folder->delete();
463
+
464
+        $this->assertFalse($userFolder->nodeExists('folder'));
465
+
466
+        $filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
467
+        $this->assertCount(1, $filesInTrash);
468
+
469
+        /** @var FileInfo */
470
+        $trashedFile = $filesInTrash[0];
471
+
472
+        $this->assertTrue(
473
+            Trashbin::restore(
474
+                'folder.d' . $trashedFile->getMtime() . '/file1.txt',
475
+                'file1.txt',
476
+                $trashedFile->getMtime()
477
+            )
478
+        );
479 479
 
480
-		$file = $userFolder->get('file1.txt');
481
-		$this->assertEquals('foo', $file->getContent());
482
-	}
480
+        $file = $userFolder->get('file1.txt');
481
+        $this->assertEquals('foo', $file->getContent());
482
+    }
483 483
 
484
-	/**
485
-	 * Test restoring a file whenever the source folder was removed.
486
-	 * The file should then land in the root.
487
-	 */
488
-	public function testRestoreFileWithMissingSourceFolder(): void {
489
-		$userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
490
-		$folder = $userFolder->newFolder('folder');
491
-		$file = $folder->newFile('file1.txt');
492
-		$file->putContent('foo');
484
+    /**
485
+     * Test restoring a file whenever the source folder was removed.
486
+     * The file should then land in the root.
487
+     */
488
+    public function testRestoreFileWithMissingSourceFolder(): void {
489
+        $userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
490
+        $folder = $userFolder->newFolder('folder');
491
+        $file = $folder->newFile('file1.txt');
492
+        $file->putContent('foo');
493 493
 
494
-		$this->assertTrue($userFolder->nodeExists('folder/file1.txt'));
494
+        $this->assertTrue($userFolder->nodeExists('folder/file1.txt'));
495 495
 
496
-		$file->delete();
496
+        $file->delete();
497 497
 
498
-		$this->assertFalse($userFolder->nodeExists('folder/file1.txt'));
499
-
500
-		$filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
501
-		$this->assertCount(1, $filesInTrash);
502
-
503
-		/** @var FileInfo */
504
-		$trashedFile = $filesInTrash[0];
505
-
506
-		// delete source folder
507
-		$folder->delete();
508
-
509
-		$this->assertTrue(
510
-			Trashbin::restore(
511
-				'file1.txt.d' . $trashedFile->getMtime(),
512
-				$trashedFile->getName(),
513
-				$trashedFile->getMtime()
514
-			)
515
-		);
516
-
517
-		$file = $userFolder->get('file1.txt');
518
-		$this->assertEquals('foo', $file->getContent());
519
-	}
520
-
521
-	/**
522
-	 * Test restoring a file in the root folder whenever there is another file
523
-	 * with the same name in the root folder
524
-	 */
525
-	public function testRestoreFileDoesNotOverwriteExistingInRoot(): void {
526
-		$userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
527
-		$file = $userFolder->newFile('file1.txt');
528
-		$file->putContent('foo');
529
-
530
-		$this->assertTrue($userFolder->nodeExists('file1.txt'));
531
-
532
-		$file->delete();
533
-
534
-		$this->assertFalse($userFolder->nodeExists('file1.txt'));
535
-
536
-		$filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
537
-		$this->assertCount(1, $filesInTrash);
538
-
539
-		/** @var FileInfo */
540
-		$trashedFile = $filesInTrash[0];
541
-
542
-		// create another file
543
-		$file = $userFolder->newFile('file1.txt');
544
-		$file->putContent('bar');
545
-
546
-		$this->assertTrue(
547
-			Trashbin::restore(
548
-				'file1.txt.d' . $trashedFile->getMtime(),
549
-				$trashedFile->getName(),
550
-				$trashedFile->getMtime()
551
-			)
552
-		);
553
-
554
-		$anotherFile = $userFolder->get('file1.txt');
555
-		$this->assertEquals('bar', $anotherFile->getContent());
556
-
557
-		$restoredFile = $userFolder->get('file1 (restored).txt');
558
-		$this->assertEquals('foo', $restoredFile->getContent());
559
-	}
560
-
561
-	/**
562
-	 * Test restoring a file whenever there is another file
563
-	 * with the same name in the source folder
564
-	 */
565
-	public function testRestoreFileDoesNotOverwriteExistingInSubfolder(): void {
566
-		$userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
567
-		$folder = $userFolder->newFolder('folder');
568
-		$file = $folder->newFile('file1.txt');
569
-		$file->putContent('foo');
570
-
571
-		$this->assertTrue($userFolder->nodeExists('folder/file1.txt'));
572
-
573
-		$file->delete();
574
-
575
-		$this->assertFalse($userFolder->nodeExists('folder/file1.txt'));
576
-
577
-		$filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
578
-		$this->assertCount(1, $filesInTrash);
579
-
580
-		/** @var FileInfo */
581
-		$trashedFile = $filesInTrash[0];
582
-
583
-		// create another file
584
-		$file = $folder->newFile('file1.txt');
585
-		$file->putContent('bar');
586
-
587
-		$this->assertTrue(
588
-			Trashbin::restore(
589
-				'file1.txt.d' . $trashedFile->getMtime(),
590
-				$trashedFile->getName(),
591
-				$trashedFile->getMtime()
592
-			)
593
-		);
594
-
595
-		$anotherFile = $userFolder->get('folder/file1.txt');
596
-		$this->assertEquals('bar', $anotherFile->getContent());
597
-
598
-		$restoredFile = $userFolder->get('folder/file1 (restored).txt');
599
-		$this->assertEquals('foo', $restoredFile->getContent());
600
-	}
601
-
602
-	/**
603
-	 * Test restoring a non-existing file from trashbin, returns false
604
-	 */
605
-	public function testRestoreUnexistingFile(): void {
606
-		$this->assertFalse(
607
-			Trashbin::restore(
608
-				'unexist.txt.d123456',
609
-				'unexist.txt',
610
-				'123456'
611
-			)
612
-		);
613
-	}
614
-
615
-	/**
616
-	 * Test restoring a file into a read-only folder, will restore
617
-	 * the file to root instead
618
-	 */
619
-	public function testRestoreFileIntoReadOnlySourceFolder(): void {
620
-		$userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
621
-		$folder = $userFolder->newFolder('folder');
622
-		$file = $folder->newFile('file1.txt');
623
-		$file->putContent('foo');
624
-
625
-		$this->assertTrue($userFolder->nodeExists('folder/file1.txt'));
626
-
627
-		$file->delete();
628
-
629
-		$this->assertFalse($userFolder->nodeExists('folder/file1.txt'));
630
-
631
-		$filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
632
-		$this->assertCount(1, $filesInTrash);
633
-
634
-		/** @var FileInfo */
635
-		$trashedFile = $filesInTrash[0];
636
-
637
-		// delete source folder
638
-		[$storage, $internalPath] = $this->rootView->resolvePath('/' . self::TEST_TRASHBIN_USER1 . '/files/folder');
639
-		if ($storage instanceof Local) {
640
-			$folderAbsPath = $storage->getSourcePath($internalPath);
641
-			// make folder read-only
642
-			chmod($folderAbsPath, 0555);
643
-
644
-			$this->assertTrue(
645
-				Trashbin::restore(
646
-					'file1.txt.d' . $trashedFile->getMtime(),
647
-					$trashedFile->getName(),
648
-					$trashedFile->getMtime()
649
-				)
650
-			);
651
-
652
-			$file = $userFolder->get('file1.txt');
653
-			$this->assertEquals('foo', $file->getContent());
654
-
655
-			chmod($folderAbsPath, 0755);
656
-		}
657
-	}
658
-
659
-	public function testTrashSizePropagation(): void {
660
-		$view = new View('/' . self::TEST_TRASHBIN_USER1 . '/files_trashbin/files');
661
-
662
-		$userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
663
-		$file1 = $userFolder->newFile('foo.txt');
664
-		$file1->putContent('1');
665
-
666
-		$this->assertTrue($userFolder->nodeExists('foo.txt'));
667
-		$file1->delete();
668
-		$this->assertFalse($userFolder->nodeExists('foo.txt'));
669
-		$this->assertEquals(1, $view->getFileInfo('')->getSize());
670
-
671
-		$folder = $userFolder->newFolder('bar');
672
-		$file2 = $folder->newFile('baz.txt');
673
-		$file2->putContent('22');
674
-
675
-		$this->assertTrue($userFolder->nodeExists('bar'));
676
-		$folder->delete();
677
-		$this->assertFalse($userFolder->nodeExists('bar'));
678
-		$this->assertEquals(3, $view->getFileInfo('')->getSize());
679
-	}
680
-
681
-	/**
682
-	 * @param string $user
683
-	 * @param bool $create
684
-	 */
685
-	public static function loginHelper($user, $create = false) {
686
-		if ($create) {
687
-			try {
688
-				Server::get(IUserManager::class)->createUser($user, $user);
689
-			} catch (\Exception $e) { // catch username is already being used from previous aborted runs
690
-			}
691
-		}
692
-
693
-		\OC_Util::tearDownFS();
694
-		\OC_User::setUserId('');
695
-		Filesystem::tearDown();
696
-		\OC_User::setUserId($user);
697
-		\OC_Util::setupFS($user);
698
-		Server::get(IRootFolder::class)->getUserFolder($user);
699
-	}
498
+        $this->assertFalse($userFolder->nodeExists('folder/file1.txt'));
499
+
500
+        $filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
501
+        $this->assertCount(1, $filesInTrash);
502
+
503
+        /** @var FileInfo */
504
+        $trashedFile = $filesInTrash[0];
505
+
506
+        // delete source folder
507
+        $folder->delete();
508
+
509
+        $this->assertTrue(
510
+            Trashbin::restore(
511
+                'file1.txt.d' . $trashedFile->getMtime(),
512
+                $trashedFile->getName(),
513
+                $trashedFile->getMtime()
514
+            )
515
+        );
516
+
517
+        $file = $userFolder->get('file1.txt');
518
+        $this->assertEquals('foo', $file->getContent());
519
+    }
520
+
521
+    /**
522
+     * Test restoring a file in the root folder whenever there is another file
523
+     * with the same name in the root folder
524
+     */
525
+    public function testRestoreFileDoesNotOverwriteExistingInRoot(): void {
526
+        $userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
527
+        $file = $userFolder->newFile('file1.txt');
528
+        $file->putContent('foo');
529
+
530
+        $this->assertTrue($userFolder->nodeExists('file1.txt'));
531
+
532
+        $file->delete();
533
+
534
+        $this->assertFalse($userFolder->nodeExists('file1.txt'));
535
+
536
+        $filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
537
+        $this->assertCount(1, $filesInTrash);
538
+
539
+        /** @var FileInfo */
540
+        $trashedFile = $filesInTrash[0];
541
+
542
+        // create another file
543
+        $file = $userFolder->newFile('file1.txt');
544
+        $file->putContent('bar');
545
+
546
+        $this->assertTrue(
547
+            Trashbin::restore(
548
+                'file1.txt.d' . $trashedFile->getMtime(),
549
+                $trashedFile->getName(),
550
+                $trashedFile->getMtime()
551
+            )
552
+        );
553
+
554
+        $anotherFile = $userFolder->get('file1.txt');
555
+        $this->assertEquals('bar', $anotherFile->getContent());
556
+
557
+        $restoredFile = $userFolder->get('file1 (restored).txt');
558
+        $this->assertEquals('foo', $restoredFile->getContent());
559
+    }
560
+
561
+    /**
562
+     * Test restoring a file whenever there is another file
563
+     * with the same name in the source folder
564
+     */
565
+    public function testRestoreFileDoesNotOverwriteExistingInSubfolder(): void {
566
+        $userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
567
+        $folder = $userFolder->newFolder('folder');
568
+        $file = $folder->newFile('file1.txt');
569
+        $file->putContent('foo');
570
+
571
+        $this->assertTrue($userFolder->nodeExists('folder/file1.txt'));
572
+
573
+        $file->delete();
574
+
575
+        $this->assertFalse($userFolder->nodeExists('folder/file1.txt'));
576
+
577
+        $filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
578
+        $this->assertCount(1, $filesInTrash);
579
+
580
+        /** @var FileInfo */
581
+        $trashedFile = $filesInTrash[0];
582
+
583
+        // create another file
584
+        $file = $folder->newFile('file1.txt');
585
+        $file->putContent('bar');
586
+
587
+        $this->assertTrue(
588
+            Trashbin::restore(
589
+                'file1.txt.d' . $trashedFile->getMtime(),
590
+                $trashedFile->getName(),
591
+                $trashedFile->getMtime()
592
+            )
593
+        );
594
+
595
+        $anotherFile = $userFolder->get('folder/file1.txt');
596
+        $this->assertEquals('bar', $anotherFile->getContent());
597
+
598
+        $restoredFile = $userFolder->get('folder/file1 (restored).txt');
599
+        $this->assertEquals('foo', $restoredFile->getContent());
600
+    }
601
+
602
+    /**
603
+     * Test restoring a non-existing file from trashbin, returns false
604
+     */
605
+    public function testRestoreUnexistingFile(): void {
606
+        $this->assertFalse(
607
+            Trashbin::restore(
608
+                'unexist.txt.d123456',
609
+                'unexist.txt',
610
+                '123456'
611
+            )
612
+        );
613
+    }
614
+
615
+    /**
616
+     * Test restoring a file into a read-only folder, will restore
617
+     * the file to root instead
618
+     */
619
+    public function testRestoreFileIntoReadOnlySourceFolder(): void {
620
+        $userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
621
+        $folder = $userFolder->newFolder('folder');
622
+        $file = $folder->newFile('file1.txt');
623
+        $file->putContent('foo');
624
+
625
+        $this->assertTrue($userFolder->nodeExists('folder/file1.txt'));
626
+
627
+        $file->delete();
628
+
629
+        $this->assertFalse($userFolder->nodeExists('folder/file1.txt'));
630
+
631
+        $filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'mtime');
632
+        $this->assertCount(1, $filesInTrash);
633
+
634
+        /** @var FileInfo */
635
+        $trashedFile = $filesInTrash[0];
636
+
637
+        // delete source folder
638
+        [$storage, $internalPath] = $this->rootView->resolvePath('/' . self::TEST_TRASHBIN_USER1 . '/files/folder');
639
+        if ($storage instanceof Local) {
640
+            $folderAbsPath = $storage->getSourcePath($internalPath);
641
+            // make folder read-only
642
+            chmod($folderAbsPath, 0555);
643
+
644
+            $this->assertTrue(
645
+                Trashbin::restore(
646
+                    'file1.txt.d' . $trashedFile->getMtime(),
647
+                    $trashedFile->getName(),
648
+                    $trashedFile->getMtime()
649
+                )
650
+            );
651
+
652
+            $file = $userFolder->get('file1.txt');
653
+            $this->assertEquals('foo', $file->getContent());
654
+
655
+            chmod($folderAbsPath, 0755);
656
+        }
657
+    }
658
+
659
+    public function testTrashSizePropagation(): void {
660
+        $view = new View('/' . self::TEST_TRASHBIN_USER1 . '/files_trashbin/files');
661
+
662
+        $userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
663
+        $file1 = $userFolder->newFile('foo.txt');
664
+        $file1->putContent('1');
665
+
666
+        $this->assertTrue($userFolder->nodeExists('foo.txt'));
667
+        $file1->delete();
668
+        $this->assertFalse($userFolder->nodeExists('foo.txt'));
669
+        $this->assertEquals(1, $view->getFileInfo('')->getSize());
670
+
671
+        $folder = $userFolder->newFolder('bar');
672
+        $file2 = $folder->newFile('baz.txt');
673
+        $file2->putContent('22');
674
+
675
+        $this->assertTrue($userFolder->nodeExists('bar'));
676
+        $folder->delete();
677
+        $this->assertFalse($userFolder->nodeExists('bar'));
678
+        $this->assertEquals(3, $view->getFileInfo('')->getSize());
679
+    }
680
+
681
+    /**
682
+     * @param string $user
683
+     * @param bool $create
684
+     */
685
+    public static function loginHelper($user, $create = false) {
686
+        if ($create) {
687
+            try {
688
+                Server::get(IUserManager::class)->createUser($user, $user);
689
+            } catch (\Exception $e) { // catch username is already being used from previous aborted runs
690
+            }
691
+        }
692
+
693
+        \OC_Util::tearDownFS();
694
+        \OC_User::setUserId('');
695
+        Filesystem::tearDown();
696
+        \OC_User::setUserId($user);
697
+        \OC_Util::setupFS($user);
698
+        Server::get(IRootFolder::class)->getUserFolder($user);
699
+    }
700 700
 }
701 701
 
702 702
 
703 703
 // just a dummy class to make protected methods available for testing
704 704
 class TrashbinForTesting extends Trashbin {
705 705
 
706
-	/**
707
-	 * @param FileInfo[] $files
708
-	 * @param integer $limit
709
-	 */
710
-	public function dummyDeleteExpiredFiles($files) {
711
-		// dummy value for $retention_obligation because it is not needed here
712
-		return parent::deleteExpiredFiles($files, TrashbinTest::TEST_TRASHBIN_USER1);
713
-	}
714
-
715
-	/**
716
-	 * @param FileInfo[] $files
717
-	 * @param integer $availableSpace
718
-	 */
719
-	public function dummyDeleteFiles($files, $availableSpace) {
720
-		return parent::deleteFiles($files, TrashbinTest::TEST_TRASHBIN_USER1, $availableSpace);
721
-	}
706
+    /**
707
+     * @param FileInfo[] $files
708
+     * @param integer $limit
709
+     */
710
+    public function dummyDeleteExpiredFiles($files) {
711
+        // dummy value for $retention_obligation because it is not needed here
712
+        return parent::deleteExpiredFiles($files, TrashbinTest::TEST_TRASHBIN_USER1);
713
+    }
714
+
715
+    /**
716
+     * @param FileInfo[] $files
717
+     * @param integer $availableSpace
718
+     */
719
+    public function dummyDeleteFiles($files, $availableSpace) {
720
+        return parent::deleteFiles($files, TrashbinTest::TEST_TRASHBIN_USER1, $availableSpace);
721
+    }
722 722
 }
Please login to merge, or discard this patch.
Spacing   +29 added lines, -29 removed lines patch added patch discarded remove patch
@@ -69,7 +69,7 @@  discard block
 block discarded – undo
69 69
 
70 70
 		$config = Server::get(IConfig::class);
71 71
 		//configure trashbin
72
-		self::$rememberRetentionObligation = (string)$config->getSystemValue('trashbin_retention_obligation', Expiration::DEFAULT_RETENTION_OBLIGATION);
72
+		self::$rememberRetentionObligation = (string) $config->getSystemValue('trashbin_retention_obligation', Expiration::DEFAULT_RETENTION_OBLIGATION);
73 73
 		/** @var Expiration $expiration */
74 74
 		$expiration = Server::get(Expiration::class);
75 75
 		$expiration->setRetentionObligation('auto, 2');
@@ -117,7 +117,7 @@  discard block
 block discarded – undo
117 117
 			->getMock();
118 118
 		$mockConfig->expects($this->any())
119 119
 			->method('getSystemValue')
120
-			->willReturnCallback(static function ($key, $default) use ($config) {
120
+			->willReturnCallback(static function($key, $default) use ($config) {
121 121
 				if ($key === 'filesystem_check_changes') {
122 122
 					return Watcher::CHECK_ONCE;
123 123
 				} else {
@@ -126,8 +126,8 @@  discard block
 block discarded – undo
126 126
 			});
127 127
 		$this->overwriteService(AllConfig::class, $mockConfig);
128 128
 
129
-		$this->trashRoot1 = '/' . self::TEST_TRASHBIN_USER1 . '/files_trashbin';
130
-		$this->trashRoot2 = '/' . self::TEST_TRASHBIN_USER2 . '/files_trashbin';
129
+		$this->trashRoot1 = '/'.self::TEST_TRASHBIN_USER1.'/files_trashbin';
130
+		$this->trashRoot2 = '/'.self::TEST_TRASHBIN_USER2.'/files_trashbin';
131 131
 		$this->rootView = new View();
132 132
 		self::loginHelper(self::TEST_TRASHBIN_USER1);
133 133
 	}
@@ -137,8 +137,8 @@  discard block
 block discarded – undo
137 137
 		// disable trashbin to be able to properly clean up
138 138
 		Server::get(IAppManager::class)->disableApp('files_trashbin');
139 139
 
140
-		$this->rootView->deleteAll('/' . self::TEST_TRASHBIN_USER1 . '/files');
141
-		$this->rootView->deleteAll('/' . self::TEST_TRASHBIN_USER2 . '/files');
140
+		$this->rootView->deleteAll('/'.self::TEST_TRASHBIN_USER1.'/files');
141
+		$this->rootView->deleteAll('/'.self::TEST_TRASHBIN_USER2.'/files');
142 142
 		$this->rootView->deleteAll($this->trashRoot1);
143 143
 		$this->rootView->deleteAll($this->trashRoot2);
144 144
 
@@ -206,15 +206,15 @@  discard block
 block discarded – undo
206 206
 	 */
207 207
 	public function testExpireOldFilesShared(): void {
208 208
 		$currentTime = time();
209
-		$folder = 'trashTest-' . $currentTime . '/';
209
+		$folder = 'trashTest-'.$currentTime.'/';
210 210
 		$expiredDate = $currentTime - 3 * 24 * 60 * 60;
211 211
 
212 212
 		// create some files
213 213
 		Filesystem::mkdir($folder);
214
-		Filesystem::file_put_contents($folder . 'user1-1.txt', 'file1');
215
-		Filesystem::file_put_contents($folder . 'user1-2.txt', 'file2');
216
-		Filesystem::file_put_contents($folder . 'user1-3.txt', 'file3');
217
-		Filesystem::file_put_contents($folder . 'user1-4.txt', 'file4');
214
+		Filesystem::file_put_contents($folder.'user1-1.txt', 'file1');
215
+		Filesystem::file_put_contents($folder.'user1-2.txt', 'file2');
216
+		Filesystem::file_put_contents($folder.'user1-3.txt', 'file3');
217
+		Filesystem::file_put_contents($folder.'user1-4.txt', 'file4');
218 218
 
219 219
 		//share user1-4.txt with user2
220 220
 		$node = \OC::$server->getUserFolder(self::TEST_TRASHBIN_USER1)->get($folder);
@@ -228,9 +228,9 @@  discard block
 block discarded – undo
228 228
 		Server::get(\OCP\Share\IManager::class)->acceptShare($share, self::TEST_TRASHBIN_USER2);
229 229
 
230 230
 		// delete them so that they end up in the trash bin
231
-		Filesystem::unlink($folder . 'user1-1.txt');
232
-		Filesystem::unlink($folder . 'user1-2.txt');
233
-		Filesystem::unlink($folder . 'user1-3.txt');
231
+		Filesystem::unlink($folder.'user1-1.txt');
232
+		Filesystem::unlink($folder.'user1-2.txt');
233
+		Filesystem::unlink($folder.'user1-3.txt');
234 234
 
235 235
 		$filesInTrash = Helper::getTrashFiles('/', self::TEST_TRASHBIN_USER1, 'name');
236 236
 		$this->assertSame(3, count($filesInTrash));
@@ -241,7 +241,7 @@  discard block
 block discarded – undo
241 241
 		// login as user2
242 242
 		self::loginHelper(self::TEST_TRASHBIN_USER2);
243 243
 
244
-		$this->assertTrue(Filesystem::file_exists($folder . 'user1-4.txt'));
244
+		$this->assertTrue(Filesystem::file_exists($folder.'user1-4.txt'));
245 245
 
246 246
 		// create some files
247 247
 		Filesystem::file_put_contents('user2-1.txt', 'file1');
@@ -257,7 +257,7 @@  discard block
 block discarded – undo
257 257
 		// every second file will get a date in the past so that it will get expired
258 258
 		$this->manipulateDeleteTime($filesInTrashUser2, $this->trashRoot2, $expiredDate);
259 259
 
260
-		Filesystem::unlink($folder . 'user1-4.txt');
260
+		Filesystem::unlink($folder.'user1-4.txt');
261 261
 
262 262
 		$this->runCommands();
263 263
 
@@ -292,7 +292,7 @@  discard block
 block discarded – undo
292 292
 			}
293 293
 			if (!$found) {
294 294
 				// if we didn't found the expected file, something went wrong
295
-				$this->assertTrue(false, "can't find expected file '" . $expectedFile . "' in trash bin");
295
+				$this->assertTrue(false, "can't find expected file '".$expectedFile."' in trash bin");
296 296
 			}
297 297
 		}
298 298
 	}
@@ -306,8 +306,8 @@  discard block
 block discarded – undo
306 306
 			// modify every second file
307 307
 			$counter = ($counter + 1) % 2;
308 308
 			if ($counter === 1) {
309
-				$source = $trashRoot . '/files/' . $file['name'] . '.d' . $file['mtime'];
310
-				$target = Filesystem::normalizePath($trashRoot . '/files/' . $file['name'] . '.d' . $expireDate);
309
+				$source = $trashRoot.'/files/'.$file['name'].'.d'.$file['mtime'];
310
+				$target = Filesystem::normalizePath($trashRoot.'/files/'.$file['name'].'.d'.$expireDate);
311 311
 				$this->rootView->rename($source, $target);
312 312
 				$file['mtime'] = $expireDate;
313 313
 			}
@@ -372,7 +372,7 @@  discard block
 block discarded – undo
372 372
 
373 373
 		$this->assertTrue(
374 374
 			Trashbin::restore(
375
-				'file1.txt.d' . $trashedFile->getMtime(),
375
+				'file1.txt.d'.$trashedFile->getMtime(),
376 376
 				$trashedFile->getName(),
377 377
 				$trashedFile->getMtime()
378 378
 			)
@@ -405,7 +405,7 @@  discard block
 block discarded – undo
405 405
 
406 406
 		$this->assertTrue(
407 407
 			Trashbin::restore(
408
-				'file1.txt.d' . $trashedFile->getMtime(),
408
+				'file1.txt.d'.$trashedFile->getMtime(),
409 409
 				$trashedFile->getName(),
410 410
 				$trashedFile->getMtime()
411 411
 			)
@@ -438,7 +438,7 @@  discard block
 block discarded – undo
438 438
 
439 439
 		$this->assertTrue(
440 440
 			Trashbin::restore(
441
-				'folder.d' . $trashedFolder->getMtime(),
441
+				'folder.d'.$trashedFolder->getMtime(),
442 442
 				$trashedFolder->getName(),
443 443
 				$trashedFolder->getMtime()
444 444
 			)
@@ -471,7 +471,7 @@  discard block
 block discarded – undo
471 471
 
472 472
 		$this->assertTrue(
473 473
 			Trashbin::restore(
474
-				'folder.d' . $trashedFile->getMtime() . '/file1.txt',
474
+				'folder.d'.$trashedFile->getMtime().'/file1.txt',
475 475
 				'file1.txt',
476 476
 				$trashedFile->getMtime()
477 477
 			)
@@ -508,7 +508,7 @@  discard block
 block discarded – undo
508 508
 
509 509
 		$this->assertTrue(
510 510
 			Trashbin::restore(
511
-				'file1.txt.d' . $trashedFile->getMtime(),
511
+				'file1.txt.d'.$trashedFile->getMtime(),
512 512
 				$trashedFile->getName(),
513 513
 				$trashedFile->getMtime()
514 514
 			)
@@ -545,7 +545,7 @@  discard block
 block discarded – undo
545 545
 
546 546
 		$this->assertTrue(
547 547
 			Trashbin::restore(
548
-				'file1.txt.d' . $trashedFile->getMtime(),
548
+				'file1.txt.d'.$trashedFile->getMtime(),
549 549
 				$trashedFile->getName(),
550 550
 				$trashedFile->getMtime()
551 551
 			)
@@ -586,7 +586,7 @@  discard block
 block discarded – undo
586 586
 
587 587
 		$this->assertTrue(
588 588
 			Trashbin::restore(
589
-				'file1.txt.d' . $trashedFile->getMtime(),
589
+				'file1.txt.d'.$trashedFile->getMtime(),
590 590
 				$trashedFile->getName(),
591 591
 				$trashedFile->getMtime()
592 592
 			)
@@ -635,7 +635,7 @@  discard block
 block discarded – undo
635 635
 		$trashedFile = $filesInTrash[0];
636 636
 
637 637
 		// delete source folder
638
-		[$storage, $internalPath] = $this->rootView->resolvePath('/' . self::TEST_TRASHBIN_USER1 . '/files/folder');
638
+		[$storage, $internalPath] = $this->rootView->resolvePath('/'.self::TEST_TRASHBIN_USER1.'/files/folder');
639 639
 		if ($storage instanceof Local) {
640 640
 			$folderAbsPath = $storage->getSourcePath($internalPath);
641 641
 			// make folder read-only
@@ -643,7 +643,7 @@  discard block
 block discarded – undo
643 643
 
644 644
 			$this->assertTrue(
645 645
 				Trashbin::restore(
646
-					'file1.txt.d' . $trashedFile->getMtime(),
646
+					'file1.txt.d'.$trashedFile->getMtime(),
647 647
 					$trashedFile->getName(),
648 648
 					$trashedFile->getMtime()
649 649
 				)
@@ -657,7 +657,7 @@  discard block
 block discarded – undo
657 657
 	}
658 658
 
659 659
 	public function testTrashSizePropagation(): void {
660
-		$view = new View('/' . self::TEST_TRASHBIN_USER1 . '/files_trashbin/files');
660
+		$view = new View('/'.self::TEST_TRASHBIN_USER1.'/files_trashbin/files');
661 661
 
662 662
 		$userFolder = Server::get(IRootFolder::class)->getUserFolder(self::TEST_TRASHBIN_USER1);
663 663
 		$file1 = $userFolder->newFile('foo.txt');
Please login to merge, or discard this patch.
apps/files_trashbin/lib/Trashbin.php 1 patch
Indentation   +1142 added lines, -1142 removed lines patch added patch discarded remove patch
@@ -51,1146 +51,1146 @@
 block discarded – undo
51 51
 
52 52
 /** @template-implements IEventListener<BeforeNodeDeletedEvent> */
53 53
 class Trashbin implements IEventListener {
54
-	// unit: percentage; 50% of available disk space/quota
55
-	public const DEFAULTMAXSIZE = 50;
56
-
57
-	/**
58
-	 * Ensure we don't need to scan the file during the move to trash
59
-	 * by triggering the scan in the pre-hook
60
-	 */
61
-	public static function ensureFileScannedHook(Node $node): void {
62
-		try {
63
-			self::getUidAndFilename($node->getPath());
64
-		} catch (NotFoundException $e) {
65
-			// Nothing to scan for non existing files
66
-		}
67
-	}
68
-
69
-	/**
70
-	 * get the UID of the owner of the file and the path to the file relative to
71
-	 * owners files folder
72
-	 *
73
-	 * @param string $filename
74
-	 * @return array
75
-	 * @throws NoUserException
76
-	 */
77
-	public static function getUidAndFilename($filename) {
78
-		$uid = Filesystem::getOwner($filename);
79
-		$userManager = Server::get(IUserManager::class);
80
-		// if the user with the UID doesn't exists, e.g. because the UID points
81
-		// to a remote user with a federated cloud ID we use the current logged-in
82
-		// user. We need a valid local user to move the file to the right trash bin
83
-		if (!$userManager->userExists($uid)) {
84
-			$uid = OC_User::getUser();
85
-		}
86
-		if (!$uid) {
87
-			// no owner, usually because of share link from ext storage
88
-			return [null, null];
89
-		}
90
-		Filesystem::initMountPoints($uid);
91
-		if ($uid !== OC_User::getUser()) {
92
-			$info = Filesystem::getFileInfo($filename);
93
-			$ownerView = new View('/' . $uid . '/files');
94
-			try {
95
-				$filename = $ownerView->getPath($info['fileid']);
96
-			} catch (NotFoundException $e) {
97
-				$filename = null;
98
-			}
99
-		}
100
-		return [$uid, $filename];
101
-	}
102
-
103
-	/**
104
-	 * get original location and deleted by of files for user
105
-	 *
106
-	 * @param string $user
107
-	 * @return array<string, array<string, array{location: string, deletedBy: string}>>
108
-	 */
109
-	public static function getExtraData($user) {
110
-		$query = Server::get(IDBConnection::class)->getQueryBuilder();
111
-		$query->select('id', 'timestamp', 'location', 'deleted_by')
112
-			->from('files_trash')
113
-			->where($query->expr()->eq('user', $query->createNamedParameter($user)));
114
-		$result = $query->executeQuery();
115
-		$array = [];
116
-		while ($row = $result->fetch()) {
117
-			$array[$row['id']][$row['timestamp']] = [
118
-				'location' => (string)$row['location'],
119
-				'deletedBy' => (string)$row['deleted_by'],
120
-			];
121
-		}
122
-		$result->closeCursor();
123
-		return $array;
124
-	}
125
-
126
-	/**
127
-	 * get original location of file
128
-	 *
129
-	 * @param string $user
130
-	 * @param string $filename
131
-	 * @param string $timestamp
132
-	 * @return string|false original location
133
-	 */
134
-	public static function getLocation($user, $filename, $timestamp) {
135
-		$query = Server::get(IDBConnection::class)->getQueryBuilder();
136
-		$query->select('location')
137
-			->from('files_trash')
138
-			->where($query->expr()->eq('user', $query->createNamedParameter($user)))
139
-			->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
140
-			->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
141
-
142
-		$result = $query->executeQuery();
143
-		$row = $result->fetch();
144
-		$result->closeCursor();
145
-
146
-		if (isset($row['location'])) {
147
-			return $row['location'];
148
-		} else {
149
-			return false;
150
-		}
151
-	}
152
-
153
-	/** @param string $user */
154
-	private static function setUpTrash($user): void {
155
-		$view = new View('/' . $user);
156
-		if (!$view->is_dir('files_trashbin')) {
157
-			$view->mkdir('files_trashbin');
158
-		}
159
-		if (!$view->is_dir('files_trashbin/files')) {
160
-			$view->mkdir('files_trashbin/files');
161
-		}
162
-		if (!$view->is_dir('files_trashbin/versions')) {
163
-			$view->mkdir('files_trashbin/versions');
164
-		}
165
-		if (!$view->is_dir('files_trashbin/keys')) {
166
-			$view->mkdir('files_trashbin/keys');
167
-		}
168
-	}
169
-
170
-
171
-	/**
172
-	 * copy file to owners trash
173
-	 *
174
-	 * @param string $sourcePath
175
-	 * @param string $owner
176
-	 * @param string $targetPath
177
-	 * @param string $user
178
-	 * @param int $timestamp
179
-	 */
180
-	private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp): void {
181
-		self::setUpTrash($owner);
182
-
183
-		$targetFilename = basename($targetPath);
184
-		$targetLocation = dirname($targetPath);
185
-
186
-		$sourceFilename = basename($sourcePath);
187
-
188
-		$view = new View('/');
189
-
190
-		$target = $user . '/files_trashbin/files/' . static::getTrashFilename($targetFilename, $timestamp);
191
-		$source = $owner . '/files_trashbin/files/' . static::getTrashFilename($sourceFilename, $timestamp);
192
-		$free = $view->free_space($target);
193
-		$isUnknownOrUnlimitedFreeSpace = $free < 0;
194
-		$isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
195
-		if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) {
196
-			self::copy_recursive($source, $target, $view);
197
-		}
198
-
199
-
200
-		if ($view->file_exists($target)) {
201
-			$query = Server::get(IDBConnection::class)->getQueryBuilder();
202
-			$query->insert('files_trash')
203
-				->setValue('id', $query->createNamedParameter($targetFilename))
204
-				->setValue('timestamp', $query->createNamedParameter($timestamp))
205
-				->setValue('location', $query->createNamedParameter($targetLocation))
206
-				->setValue('user', $query->createNamedParameter($user))
207
-				->setValue('deleted_by', $query->createNamedParameter($user));
208
-			$result = $query->executeStatement();
209
-			if (!$result) {
210
-				Server::get(LoggerInterface::class)->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']);
211
-			}
212
-		}
213
-	}
214
-
215
-
216
-	/**
217
-	 * move file to the trash bin
218
-	 *
219
-	 * @param string $file_path path to the deleted file/directory relative to the files root directory
220
-	 * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
221
-	 *
222
-	 * @return bool
223
-	 */
224
-	public static function move2trash($file_path, $ownerOnly = false) {
225
-		// get the user for which the filesystem is setup
226
-		$root = Filesystem::getRoot();
227
-		[, $user] = explode('/', $root);
228
-		[$owner, $ownerPath] = self::getUidAndFilename($file_path);
229
-
230
-		// if no owner found (ex: ext storage + share link), will use the current user's trashbin then
231
-		if (is_null($owner)) {
232
-			$owner = $user;
233
-			$ownerPath = $file_path;
234
-		}
235
-
236
-		$ownerView = new View('/' . $owner);
237
-
238
-		// file has been deleted in between
239
-		if (is_null($ownerPath) || $ownerPath === '') {
240
-			return true;
241
-		}
242
-
243
-		$sourceInfo = $ownerView->getFileInfo('/files/' . $ownerPath);
244
-
245
-		if ($sourceInfo === false) {
246
-			return true;
247
-		}
248
-
249
-		self::setUpTrash($user);
250
-		if ($owner !== $user) {
251
-			// also setup for owner
252
-			self::setUpTrash($owner);
253
-		}
254
-
255
-		$path_parts = pathinfo($ownerPath);
256
-
257
-		$filename = $path_parts['basename'];
258
-		$location = $path_parts['dirname'];
259
-		/** @var ITimeFactory $timeFactory */
260
-		$timeFactory = Server::get(ITimeFactory::class);
261
-		$timestamp = $timeFactory->getTime();
262
-
263
-		$lockingProvider = Server::get(ILockingProvider::class);
264
-
265
-		// disable proxy to prevent recursive calls
266
-		$trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
267
-		$gotLock = false;
268
-
269
-		do {
270
-			/** @var ILockingStorage & IStorage $trashStorage */
271
-			[$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath);
272
-			try {
273
-				$trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
274
-				$gotLock = true;
275
-			} catch (LockedException $e) {
276
-				// a file with the same name is being deleted concurrently
277
-				// nudge the timestamp a bit to resolve the conflict
278
-
279
-				$timestamp = $timestamp + 1;
280
-
281
-				$trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
282
-			}
283
-		} while (!$gotLock);
284
-
285
-		$sourceStorage = $sourceInfo->getStorage();
286
-		$sourceInternalPath = $sourceInfo->getInternalPath();
287
-
288
-		if ($trashStorage->file_exists($trashInternalPath)) {
289
-			$trashStorage->unlink($trashInternalPath);
290
-		}
291
-
292
-		$configuredTrashbinSize = static::getConfiguredTrashbinSize($owner);
293
-		if ($configuredTrashbinSize >= 0 && $sourceInfo->getSize() >= $configuredTrashbinSize) {
294
-			return false;
295
-		}
296
-
297
-		try {
298
-			$moveSuccessful = true;
299
-
300
-			$inCache = $sourceStorage->getCache()->inCache($sourceInternalPath);
301
-			$trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
302
-			if ($inCache) {
303
-				$trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
304
-			}
305
-		} catch (CopyRecursiveException $e) {
306
-			$moveSuccessful = false;
307
-			if ($trashStorage->file_exists($trashInternalPath)) {
308
-				$trashStorage->unlink($trashInternalPath);
309
-			}
310
-			Server::get(LoggerInterface::class)->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
311
-		}
312
-
313
-		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
314
-			if ($sourceStorage->is_dir($sourceInternalPath)) {
315
-				$sourceStorage->rmdir($sourceInternalPath);
316
-			} else {
317
-				$sourceStorage->unlink($sourceInternalPath);
318
-			}
319
-
320
-			if ($sourceStorage->file_exists($sourceInternalPath)) {
321
-				// undo the cache move
322
-				$sourceStorage->getUpdater()->renameFromStorage($trashStorage, $trashInternalPath, $sourceInternalPath);
323
-			} else {
324
-				$trashStorage->getUpdater()->remove($trashInternalPath);
325
-			}
326
-			return false;
327
-		}
328
-
329
-		if ($moveSuccessful) {
330
-			$query = Server::get(IDBConnection::class)->getQueryBuilder();
331
-			$query->insert('files_trash')
332
-				->setValue('id', $query->createNamedParameter($filename))
333
-				->setValue('timestamp', $query->createNamedParameter($timestamp))
334
-				->setValue('location', $query->createNamedParameter($location))
335
-				->setValue('user', $query->createNamedParameter($owner))
336
-				->setValue('deleted_by', $query->createNamedParameter($user));
337
-			$result = $query->executeStatement();
338
-			if (!$result) {
339
-				Server::get(LoggerInterface::class)->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
340
-			}
341
-			Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
342
-				'trashPath' => Filesystem::normalizePath(static::getTrashFilename($filename, $timestamp))]);
343
-
344
-			self::retainVersions($filename, $owner, $ownerPath, $timestamp);
345
-
346
-			// if owner !== user we need to also add a copy to the users trash
347
-			if ($user !== $owner && $ownerOnly === false) {
348
-				self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
349
-			}
350
-		}
351
-
352
-		$trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
353
-
354
-		self::scheduleExpire($user);
355
-
356
-		// if owner !== user we also need to update the owners trash size
357
-		if ($owner !== $user) {
358
-			self::scheduleExpire($owner);
359
-		}
360
-
361
-		return $moveSuccessful;
362
-	}
363
-
364
-	private static function getConfiguredTrashbinSize(string $user): int|float {
365
-		$config = Server::get(IConfig::class);
366
-		$userTrashbinSize = $config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
367
-		if (is_numeric($userTrashbinSize) && ($userTrashbinSize > -1)) {
368
-			return Util::numericToNumber($userTrashbinSize);
369
-		}
370
-		$systemTrashbinSize = $config->getAppValue('files_trashbin', 'trashbin_size', '-1');
371
-		if (is_numeric($systemTrashbinSize)) {
372
-			return Util::numericToNumber($systemTrashbinSize);
373
-		}
374
-		return -1;
375
-	}
376
-
377
-	/**
378
-	 * Move file versions to trash so that they can be restored later
379
-	 *
380
-	 * @param string $filename of deleted file
381
-	 * @param string $owner owner user id
382
-	 * @param string $ownerPath path relative to the owner's home storage
383
-	 * @param int $timestamp when the file was deleted
384
-	 */
385
-	private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
386
-		if (Server::get(IAppManager::class)->isEnabledForUser('files_versions') && !empty($ownerPath)) {
387
-			$user = OC_User::getUser();
388
-			$rootView = new View('/');
389
-
390
-			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
391
-				if ($owner !== $user) {
392
-					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . static::getTrashFilename(basename($ownerPath), $timestamp), $rootView);
393
-				}
394
-				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . static::getTrashFilename($filename, $timestamp));
395
-			} elseif ($versions = Storage::getVersions($owner, $ownerPath)) {
396
-				foreach ($versions as $v) {
397
-					if ($owner !== $user) {
398
-						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . static::getTrashFilename($v['name'] . '.v' . $v['version'], $timestamp));
399
-					}
400
-					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v['version'], $timestamp));
401
-				}
402
-			}
403
-		}
404
-	}
405
-
406
-	/**
407
-	 * Move a file or folder on storage level
408
-	 *
409
-	 * @param View $view
410
-	 * @param string $source
411
-	 * @param string $target
412
-	 * @return bool
413
-	 */
414
-	private static function move(View $view, $source, $target) {
415
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
416
-		[$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
417
-		/** @var \OC\Files\Storage\Storage $targetStorage */
418
-		[$targetStorage, $targetInternalPath] = $view->resolvePath($target);
419
-		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
420
-
421
-		$result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
422
-		if ($result) {
423
-			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
424
-		}
425
-		return $result;
426
-	}
427
-
428
-	/**
429
-	 * Copy a file or folder on storage level
430
-	 *
431
-	 * @param View $view
432
-	 * @param string $source
433
-	 * @param string $target
434
-	 * @return bool
435
-	 */
436
-	private static function copy(View $view, $source, $target) {
437
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
438
-		[$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
439
-		/** @var \OC\Files\Storage\Storage $targetStorage */
440
-		[$targetStorage, $targetInternalPath] = $view->resolvePath($target);
441
-		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
442
-
443
-		$result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
444
-		if ($result) {
445
-			$targetStorage->getUpdater()->update($targetInternalPath);
446
-		}
447
-		return $result;
448
-	}
449
-
450
-	/**
451
-	 * Restore a file or folder from trash bin
452
-	 *
453
-	 * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
454
-	 *                     including the timestamp suffix ".d12345678"
455
-	 * @param string $filename name of the file/folder
456
-	 * @param int $timestamp time when the file/folder was deleted
457
-	 *
458
-	 * @return bool true on success, false otherwise
459
-	 */
460
-	public static function restore($file, $filename, $timestamp) {
461
-		$user = OC_User::getUser();
462
-		if (!$user) {
463
-			throw new \Exception('Tried to restore a file while not logged in');
464
-		}
465
-		$view = new View('/' . $user);
466
-
467
-		$location = '';
468
-		if ($timestamp) {
469
-			$location = self::getLocation($user, $filename, $timestamp);
470
-			if ($location === false) {
471
-				Server::get(LoggerInterface::class)->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
472
-			} else {
473
-				// if location no longer exists, restore file in the root directory
474
-				if ($location !== '/'
475
-					&& (!$view->is_dir('files/' . $location)
476
-						|| !$view->isCreatable('files/' . $location))
477
-				) {
478
-					$location = '';
479
-				}
480
-			}
481
-		}
482
-
483
-		// we need a  extension in case a file/dir with the same name already exists
484
-		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
485
-
486
-		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
487
-		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
488
-		if (!$view->file_exists($source)) {
489
-			return false;
490
-		}
491
-		$mtime = $view->filemtime($source);
492
-
493
-		// restore file
494
-		if (!$view->isCreatable(dirname($target))) {
495
-			throw new NotPermittedException("Can't restore trash item because the target folder is not writable");
496
-		}
497
-
498
-		$sourcePath = Filesystem::normalizePath($file);
499
-		$targetPath = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
500
-
501
-		$sourceNode = self::getNodeForPath($user, $sourcePath);
502
-		$targetNode = self::getNodeForPath($user, $targetPath, 'files');
503
-		$run = true;
504
-		$event = new BeforeNodeRestoredEvent($sourceNode, $targetNode, $run);
505
-		$dispatcher = Server::get(IEventDispatcher::class);
506
-		$dispatcher->dispatchTyped($event);
507
-
508
-		if (!$run) {
509
-			return false;
510
-		}
511
-
512
-		$restoreResult = $view->rename($source, $target);
513
-
514
-		// handle the restore result
515
-		if ($restoreResult) {
516
-			$fakeRoot = $view->getRoot();
517
-			$view->chroot('/' . $user . '/files');
518
-			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
519
-			$view->chroot($fakeRoot);
520
-			Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => $targetPath, 'trashPath' => $sourcePath]);
521
-
522
-			$sourceNode = self::getNodeForPath($user, $sourcePath);
523
-			$targetNode = self::getNodeForPath($user, $targetPath, 'files');
524
-			$event = new NodeRestoredEvent($sourceNode, $targetNode);
525
-			$dispatcher = Server::get(IEventDispatcher::class);
526
-			$dispatcher->dispatchTyped($event);
527
-
528
-			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
529
-
530
-			if ($timestamp) {
531
-				$query = Server::get(IDBConnection::class)->getQueryBuilder();
532
-				$query->delete('files_trash')
533
-					->where($query->expr()->eq('user', $query->createNamedParameter($user)))
534
-					->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
535
-					->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
536
-				$query->executeStatement();
537
-			}
538
-
539
-			return true;
540
-		}
541
-
542
-		return false;
543
-	}
544
-
545
-	/**
546
-	 * restore versions from trash bin
547
-	 *
548
-	 * @param View $view file view
549
-	 * @param string $file complete path to file
550
-	 * @param string $filename name of file once it was deleted
551
-	 * @param string $uniqueFilename new file name to restore the file without overwriting existing files
552
-	 * @param string $location location if file
553
-	 * @param int $timestamp deletion time
554
-	 * @return false|null
555
-	 */
556
-	private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
557
-		if (Server::get(IAppManager::class)->isEnabledForUser('files_versions')) {
558
-			$user = OC_User::getUser();
559
-			$rootView = new View('/');
560
-
561
-			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
562
-
563
-			[$owner, $ownerPath] = self::getUidAndFilename($target);
564
-
565
-			// file has been deleted in between
566
-			if (empty($ownerPath)) {
567
-				return false;
568
-			}
569
-
570
-			if ($timestamp) {
571
-				$versionedFile = $filename;
572
-			} else {
573
-				$versionedFile = $file;
574
-			}
575
-
576
-			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
577
-				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
578
-			} elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
579
-				foreach ($versions as $v) {
580
-					if ($timestamp) {
581
-						$rootView->rename($user . '/files_trashbin/versions/' . static::getTrashFilename($versionedFile . '.v' . $v, $timestamp), $owner . '/files_versions/' . $ownerPath . '.v' . $v);
582
-					} else {
583
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
584
-					}
585
-				}
586
-			}
587
-		}
588
-	}
589
-
590
-	/**
591
-	 * delete all files from the trash
592
-	 */
593
-	public static function deleteAll() {
594
-		$user = OC_User::getUser();
595
-		$userRoot = \OC::$server->getUserFolder($user)->getParent();
596
-		$view = new View('/' . $user);
597
-		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
598
-
599
-		try {
600
-			$trash = $userRoot->get('files_trashbin');
601
-		} catch (NotFoundException $e) {
602
-			return false;
603
-		}
604
-
605
-		// Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
606
-		$filePaths = [];
607
-		foreach ($fileInfos as $fileInfo) {
608
-			$filePaths[] = $view->getRelativePath($fileInfo->getPath());
609
-		}
610
-		unset($fileInfos); // save memory
611
-
612
-		// Bulk PreDelete-Hook
613
-		\OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
614
-
615
-		// Single-File Hooks
616
-		foreach ($filePaths as $path) {
617
-			self::emitTrashbinPreDelete($path);
618
-		}
619
-
620
-		// actual file deletion
621
-		$trash->delete();
622
-
623
-		$query = Server::get(IDBConnection::class)->getQueryBuilder();
624
-		$query->delete('files_trash')
625
-			->where($query->expr()->eq('user', $query->createNamedParameter($user)));
626
-		$query->executeStatement();
627
-
628
-		// Bulk PostDelete-Hook
629
-		\OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
630
-
631
-		// Single-File Hooks
632
-		foreach ($filePaths as $path) {
633
-			self::emitTrashbinPostDelete($path);
634
-		}
635
-
636
-		$trash = $userRoot->newFolder('files_trashbin');
637
-		$trash->newFolder('files');
638
-
639
-		return true;
640
-	}
641
-
642
-	/**
643
-	 * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
644
-	 *
645
-	 * @param string $path
646
-	 */
647
-	protected static function emitTrashbinPreDelete($path) {
648
-		\OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
649
-	}
650
-
651
-	/**
652
-	 * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
653
-	 *
654
-	 * @param string $path
655
-	 */
656
-	protected static function emitTrashbinPostDelete($path) {
657
-		\OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
658
-	}
659
-
660
-	/**
661
-	 * delete file from trash bin permanently
662
-	 *
663
-	 * @param string $filename path to the file
664
-	 * @param string $user
665
-	 * @param int $timestamp of deletion time
666
-	 *
667
-	 * @return int|float size of deleted files
668
-	 */
669
-	public static function delete($filename, $user, $timestamp = null) {
670
-		$userRoot = \OC::$server->getUserFolder($user)->getParent();
671
-		$view = new View('/' . $user);
672
-		$size = 0;
673
-
674
-		if ($timestamp) {
675
-			$query = Server::get(IDBConnection::class)->getQueryBuilder();
676
-			$query->delete('files_trash')
677
-				->where($query->expr()->eq('user', $query->createNamedParameter($user)))
678
-				->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
679
-				->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
680
-			$query->executeStatement();
681
-
682
-			$file = static::getTrashFilename($filename, $timestamp);
683
-		} else {
684
-			$file = $filename;
685
-		}
686
-
687
-		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
688
-
689
-		try {
690
-			$node = $userRoot->get('/files_trashbin/files/' . $file);
691
-		} catch (NotFoundException $e) {
692
-			return $size;
693
-		}
694
-
695
-		if ($node instanceof Folder) {
696
-			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
697
-		} elseif ($node instanceof File) {
698
-			$size += $view->filesize('/files_trashbin/files/' . $file);
699
-		}
700
-
701
-		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
702
-		$node->delete();
703
-		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
704
-
705
-		return $size;
706
-	}
707
-
708
-	/**
709
-	 * @param string $file
710
-	 * @param string $filename
711
-	 * @param ?int $timestamp
712
-	 */
713
-	private static function deleteVersions(View $view, $file, $filename, $timestamp, string $user): int|float {
714
-		$size = 0;
715
-		if (Server::get(IAppManager::class)->isEnabledForUser('files_versions')) {
716
-			if ($view->is_dir('files_trashbin/versions/' . $file)) {
717
-				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
718
-				$view->unlink('files_trashbin/versions/' . $file);
719
-			} elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
720
-				foreach ($versions as $v) {
721
-					if ($timestamp) {
722
-						$size += $view->filesize('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
723
-						$view->unlink('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
724
-					} else {
725
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
726
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
727
-					}
728
-				}
729
-			}
730
-		}
731
-		return $size;
732
-	}
733
-
734
-	/**
735
-	 * check to see whether a file exists in trashbin
736
-	 *
737
-	 * @param string $filename path to the file
738
-	 * @param int $timestamp of deletion time
739
-	 * @return bool true if file exists, otherwise false
740
-	 */
741
-	public static function file_exists($filename, $timestamp = null) {
742
-		$user = OC_User::getUser();
743
-		$view = new View('/' . $user);
744
-
745
-		if ($timestamp) {
746
-			$filename = static::getTrashFilename($filename, $timestamp);
747
-		}
748
-
749
-		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
750
-		return $view->file_exists($target);
751
-	}
752
-
753
-	/**
754
-	 * deletes used space for trash bin in db if user was deleted
755
-	 *
756
-	 * @param string $uid id of deleted user
757
-	 * @return bool result of db delete operation
758
-	 */
759
-	public static function deleteUser($uid) {
760
-		$query = Server::get(IDBConnection::class)->getQueryBuilder();
761
-		$query->delete('files_trash')
762
-			->where($query->expr()->eq('user', $query->createNamedParameter($uid)));
763
-		return (bool)$query->executeStatement();
764
-	}
765
-
766
-	/**
767
-	 * calculate remaining free space for trash bin
768
-	 *
769
-	 * @param int|float $trashbinSize current size of the trash bin
770
-	 * @param string $user
771
-	 * @return int|float available free space for trash bin
772
-	 */
773
-	private static function calculateFreeSpace(int|float $trashbinSize, string $user): int|float {
774
-		$configuredTrashbinSize = static::getConfiguredTrashbinSize($user);
775
-		if ($configuredTrashbinSize > -1) {
776
-			return $configuredTrashbinSize - $trashbinSize;
777
-		}
778
-
779
-		$userObject = Server::get(IUserManager::class)->get($user);
780
-		if (is_null($userObject)) {
781
-			return 0;
782
-		}
783
-		$softQuota = true;
784
-		$quota = $userObject->getQuota();
785
-		if ($quota === null || $quota === 'none') {
786
-			$quota = Filesystem::free_space('/');
787
-			$softQuota = false;
788
-			// inf or unknown free space
789
-			if ($quota < 0) {
790
-				$quota = PHP_INT_MAX;
791
-			}
792
-		} else {
793
-			$quota = Util::computerFileSize($quota);
794
-			// invalid quota
795
-			if ($quota === false) {
796
-				$quota = PHP_INT_MAX;
797
-			}
798
-		}
799
-
800
-		// calculate available space for trash bin
801
-		// subtract size of files and current trash bin size from quota
802
-		if ($softQuota) {
803
-			$userFolder = \OC::$server->getUserFolder($user);
804
-			if (is_null($userFolder)) {
805
-				return 0;
806
-			}
807
-			$free = $quota - $userFolder->getSize(false); // remaining free space for user
808
-			if ($free > 0) {
809
-				$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
810
-			} else {
811
-				$availableSpace = $free - $trashbinSize;
812
-			}
813
-		} else {
814
-			$availableSpace = $quota;
815
-		}
816
-
817
-		return Util::numericToNumber($availableSpace);
818
-	}
819
-
820
-	/**
821
-	 * resize trash bin if necessary after a new file was added to Nextcloud
822
-	 *
823
-	 * @param string $user user id
824
-	 */
825
-	public static function resizeTrash($user) {
826
-		$size = self::getTrashbinSize($user);
827
-
828
-		$freeSpace = self::calculateFreeSpace($size, $user);
829
-
830
-		if ($freeSpace < 0) {
831
-			self::scheduleExpire($user);
832
-		}
833
-	}
834
-
835
-	/**
836
-	 * clean up the trash bin
837
-	 *
838
-	 * @param string $user
839
-	 */
840
-	public static function expire($user) {
841
-		$trashBinSize = self::getTrashbinSize($user);
842
-		$availableSpace = self::calculateFreeSpace($trashBinSize, $user);
843
-
844
-		$dirContent = Helper::getTrashFiles('/', $user, 'mtime');
845
-
846
-		// delete all files older then $retention_obligation
847
-		[$delSize, $count] = self::deleteExpiredFiles($dirContent, $user);
848
-
849
-		$availableSpace += $delSize;
850
-
851
-		// delete files from trash until we meet the trash bin size limit again
852
-		self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
853
-	}
854
-
855
-	/**
856
-	 * @param string $user
857
-	 */
858
-	private static function scheduleExpire($user) {
859
-		// let the admin disable auto expire
860
-		/** @var Application $application */
861
-		$application = Server::get(Application::class);
862
-		$expiration = $application->getContainer()->query('Expiration');
863
-		if ($expiration->isEnabled()) {
864
-			Server::get(IBus::class)->push(new Expire($user));
865
-		}
866
-	}
867
-
868
-	/**
869
-	 * if the size limit for the trash bin is reached, we delete the oldest
870
-	 * files in the trash bin until we meet the limit again
871
-	 *
872
-	 * @param array $files
873
-	 * @param string $user
874
-	 * @param int|float $availableSpace available disc space
875
-	 * @return int|float size of deleted files
876
-	 */
877
-	protected static function deleteFiles(array $files, string $user, int|float $availableSpace): int|float {
878
-		/** @var Application $application */
879
-		$application = Server::get(Application::class);
880
-		$expiration = $application->getContainer()->query('Expiration');
881
-		$size = 0;
882
-
883
-		if ($availableSpace < 0) {
884
-			foreach ($files as $file) {
885
-				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
886
-					$tmp = self::delete($file['name'], $user, $file['mtime']);
887
-					Server::get(LoggerInterface::class)->info(
888
-						'remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota) for user "{user}"',
889
-						[
890
-							'app' => 'files_trashbin',
891
-							'user' => $user,
892
-						]
893
-					);
894
-					$availableSpace += $tmp;
895
-					$size += $tmp;
896
-				} else {
897
-					break;
898
-				}
899
-			}
900
-		}
901
-		return $size;
902
-	}
903
-
904
-	/**
905
-	 * delete files older then max storage time
906
-	 *
907
-	 * @param array $files list of files sorted by mtime
908
-	 * @param string $user
909
-	 * @return array{int|float, int} size of deleted files and number of deleted files
910
-	 */
911
-	public static function deleteExpiredFiles($files, $user) {
912
-		/** @var Expiration $expiration */
913
-		$expiration = Server::get(Expiration::class);
914
-		$size = 0;
915
-		$count = 0;
916
-		foreach ($files as $file) {
917
-			$timestamp = $file['mtime'];
918
-			$filename = $file['name'];
919
-			if ($expiration->isExpired($timestamp)) {
920
-				try {
921
-					$size += self::delete($filename, $user, $timestamp);
922
-					$count++;
923
-				} catch (NotPermittedException $e) {
924
-					Server::get(LoggerInterface::class)->warning('Removing "' . $filename . '" from trashbin failed for user "{user}"',
925
-						[
926
-							'exception' => $e,
927
-							'app' => 'files_trashbin',
928
-							'user' => $user,
929
-						]
930
-					);
931
-				}
932
-				Server::get(LoggerInterface::class)->info(
933
-					'Remove "' . $filename . '" from trashbin for user "{user}" because it exceeds max retention obligation term.',
934
-					[
935
-						'app' => 'files_trashbin',
936
-						'user' => $user,
937
-					],
938
-				);
939
-			} else {
940
-				break;
941
-			}
942
-		}
943
-
944
-		return [$size, $count];
945
-	}
946
-
947
-	/**
948
-	 * recursive copy to copy a whole directory
949
-	 *
950
-	 * @param string $source source path, relative to the users files directory
951
-	 * @param string $destination destination path relative to the users root directory
952
-	 * @param View $view file view for the users root directory
953
-	 * @return int|float
954
-	 * @throws Exceptions\CopyRecursiveException
955
-	 */
956
-	private static function copy_recursive($source, $destination, View $view): int|float {
957
-		$size = 0;
958
-		if ($view->is_dir($source)) {
959
-			$view->mkdir($destination);
960
-			$view->touch($destination, $view->filemtime($source));
961
-			foreach ($view->getDirectoryContent($source) as $i) {
962
-				$pathDir = $source . '/' . $i['name'];
963
-				if ($view->is_dir($pathDir)) {
964
-					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
965
-				} else {
966
-					$size += $view->filesize($pathDir);
967
-					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
968
-					if (!$result) {
969
-						throw new CopyRecursiveException();
970
-					}
971
-					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
972
-				}
973
-			}
974
-		} else {
975
-			$size += $view->filesize($source);
976
-			$result = $view->copy($source, $destination);
977
-			if (!$result) {
978
-				throw new CopyRecursiveException();
979
-			}
980
-			$view->touch($destination, $view->filemtime($source));
981
-		}
982
-		return $size;
983
-	}
984
-
985
-	/**
986
-	 * find all versions which belong to the file we want to restore
987
-	 *
988
-	 * @param string $filename name of the file which should be restored
989
-	 * @param int $timestamp timestamp when the file was deleted
990
-	 */
991
-	private static function getVersionsFromTrash($filename, $timestamp, string $user): array {
992
-		$view = new View('/' . $user . '/files_trashbin/versions');
993
-		$versions = [];
994
-
995
-		/** @var \OC\Files\Storage\Storage $storage */
996
-		[$storage,] = $view->resolvePath('/');
997
-
998
-		$pattern = Server::get(IDBConnection::class)->escapeLikeParameter(basename($filename));
999
-		if ($timestamp) {
1000
-			// fetch for old versions
1001
-			$escapedTimestamp = Server::get(IDBConnection::class)->escapeLikeParameter((string)$timestamp);
1002
-			$pattern .= '.v%.d' . $escapedTimestamp;
1003
-			$offset = -strlen($escapedTimestamp) - 2;
1004
-		} else {
1005
-			$pattern .= '.v%';
1006
-		}
1007
-
1008
-		// Manually fetch all versions from the file cache to be able to filter them by their parent
1009
-		$cache = $storage->getCache('');
1010
-		$query = new CacheQueryBuilder(
1011
-			Server::get(IDBConnection::class)->getQueryBuilder(),
1012
-			Server::get(IFilesMetadataManager::class),
1013
-		);
1014
-		$normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/' . $filename)), '/');
1015
-		$parentId = $cache->getId($normalizedParentPath);
1016
-		if ($parentId === -1) {
1017
-			return [];
1018
-		}
1019
-
1020
-		$query->selectFileCache()
1021
-			->whereStorageId($cache->getNumericStorageId())
1022
-			->andWhere($query->expr()->eq('parent', $query->createNamedParameter($parentId)))
1023
-			->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
1024
-
1025
-		$result = $query->executeQuery();
1026
-		$entries = $result->fetchAll();
1027
-		$result->closeCursor();
1028
-
1029
-		/** @var CacheEntry[] $matches */
1030
-		$matches = array_map(function (array $data) {
1031
-			return Cache::cacheEntryFromData($data, Server::get(IMimeTypeLoader::class));
1032
-		}, $entries);
1033
-
1034
-		foreach ($matches as $ma) {
1035
-			if ($timestamp) {
1036
-				$parts = explode('.v', substr($ma['path'], 0, $offset));
1037
-				$versions[] = end($parts);
1038
-			} else {
1039
-				$parts = explode('.v', $ma['path']);
1040
-				$versions[] = end($parts);
1041
-			}
1042
-		}
1043
-
1044
-		return $versions;
1045
-	}
1046
-
1047
-	/**
1048
-	 * find unique extension for restored file if a file with the same name already exists
1049
-	 *
1050
-	 * @param string $location where the file should be restored
1051
-	 * @param string $filename name of the file
1052
-	 * @param View $view filesystem view relative to users root directory
1053
-	 * @return string with unique extension
1054
-	 */
1055
-	private static function getUniqueFilename($location, $filename, View $view) {
1056
-		$ext = pathinfo($filename, PATHINFO_EXTENSION);
1057
-		$name = pathinfo($filename, PATHINFO_FILENAME);
1058
-		$l = Util::getL10N('files_trashbin');
1059
-
1060
-		$location = '/' . trim($location, '/');
1061
-
1062
-		// if extension is not empty we set a dot in front of it
1063
-		if ($ext !== '') {
1064
-			$ext = '.' . $ext;
1065
-		}
1066
-
1067
-		if ($view->file_exists('files' . $location . '/' . $filename)) {
1068
-			$i = 2;
1069
-			$uniqueName = $name . ' (' . $l->t('restored') . ')' . $ext;
1070
-			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
1071
-				$uniqueName = $name . ' (' . $l->t('restored') . ' ' . $i . ')' . $ext;
1072
-				$i++;
1073
-			}
1074
-
1075
-			return $uniqueName;
1076
-		}
1077
-
1078
-		return $filename;
1079
-	}
1080
-
1081
-	/**
1082
-	 * get the size from a given root folder
1083
-	 *
1084
-	 * @param View $view file view on the root folder
1085
-	 * @return int|float size of the folder
1086
-	 */
1087
-	private static function calculateSize(View $view): int|float {
1088
-		$root = Server::get(IConfig::class)->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
1089
-		if (!file_exists($root)) {
1090
-			return 0;
1091
-		}
1092
-		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
1093
-		$size = 0;
1094
-
1095
-		/**
1096
-		 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
1097
-		 * This bug is fixed in PHP 5.5.9 or before
1098
-		 * See #8376
1099
-		 */
1100
-		$iterator->rewind();
1101
-		while ($iterator->valid()) {
1102
-			$path = $iterator->current();
1103
-			$relpath = substr($path, strlen($root) - 1);
1104
-			if (!$view->is_dir($relpath)) {
1105
-				$size += $view->filesize($relpath);
1106
-			}
1107
-			$iterator->next();
1108
-		}
1109
-		return $size;
1110
-	}
1111
-
1112
-	/**
1113
-	 * get current size of trash bin from a given user
1114
-	 *
1115
-	 * @param string $user user who owns the trash bin
1116
-	 * @return int|float trash bin size
1117
-	 */
1118
-	private static function getTrashbinSize(string $user): int|float {
1119
-		$view = new View('/' . $user);
1120
-		$fileInfo = $view->getFileInfo('/files_trashbin');
1121
-		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1122
-	}
1123
-
1124
-	/**
1125
-	 * check if trash bin is empty for a given user
1126
-	 *
1127
-	 * @param string $user
1128
-	 * @return bool
1129
-	 */
1130
-	public static function isEmpty($user) {
1131
-		$view = new View('/' . $user . '/files_trashbin');
1132
-		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1133
-			while (($file = readdir($dh)) !== false) {
1134
-				if (!Filesystem::isIgnoredDir($file)) {
1135
-					return false;
1136
-				}
1137
-			}
1138
-		}
1139
-		return true;
1140
-	}
1141
-
1142
-	/**
1143
-	 * @param $path
1144
-	 * @return string
1145
-	 */
1146
-	public static function preview_icon($path) {
1147
-		return Server::get(IURLGenerator::class)->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]);
1148
-	}
1149
-
1150
-	/**
1151
-	 * Return the filename used in the trash bin
1152
-	 */
1153
-	public static function getTrashFilename(string $filename, int $timestamp): string {
1154
-		$trashFilename = $filename . '.d' . $timestamp;
1155
-		$length = strlen($trashFilename);
1156
-		// oc_filecache `name` column has a limit of 250 chars
1157
-		$maxLength = 250;
1158
-		if ($length > $maxLength) {
1159
-			$trashFilename = substr_replace(
1160
-				$trashFilename,
1161
-				'',
1162
-				$maxLength / 2,
1163
-				$length - $maxLength
1164
-			);
1165
-		}
1166
-		return $trashFilename;
1167
-	}
1168
-
1169
-	private static function getNodeForPath(string $user, string $path, string $baseDir = 'files_trashbin/files'): Node {
1170
-		$rootFolder = Server::get(IRootFolder::class);
1171
-		$path = ltrim($path, '/');
1172
-
1173
-		$userFolder = $rootFolder->getUserFolder($user);
1174
-		/** @var Folder $trashFolder */
1175
-		$trashFolder = $userFolder->getParent()->get($baseDir);
1176
-		try {
1177
-			return $trashFolder->get($path);
1178
-		} catch (NotFoundException $ex) {
1179
-		}
1180
-
1181
-		$view = Server::get(View::class);
1182
-		$fullPath = '/' . $user . '/' . $baseDir . '/' . $path;
1183
-
1184
-		if (Filesystem::is_dir($path)) {
1185
-			return new NonExistingFolder($rootFolder, $view, $fullPath);
1186
-		} else {
1187
-			return new NonExistingFile($rootFolder, $view, $fullPath);
1188
-		}
1189
-	}
1190
-
1191
-	public function handle(Event $event): void {
1192
-		if ($event instanceof BeforeNodeDeletedEvent) {
1193
-			self::ensureFileScannedHook($event->getNode());
1194
-		}
1195
-	}
54
+    // unit: percentage; 50% of available disk space/quota
55
+    public const DEFAULTMAXSIZE = 50;
56
+
57
+    /**
58
+     * Ensure we don't need to scan the file during the move to trash
59
+     * by triggering the scan in the pre-hook
60
+     */
61
+    public static function ensureFileScannedHook(Node $node): void {
62
+        try {
63
+            self::getUidAndFilename($node->getPath());
64
+        } catch (NotFoundException $e) {
65
+            // Nothing to scan for non existing files
66
+        }
67
+    }
68
+
69
+    /**
70
+     * get the UID of the owner of the file and the path to the file relative to
71
+     * owners files folder
72
+     *
73
+     * @param string $filename
74
+     * @return array
75
+     * @throws NoUserException
76
+     */
77
+    public static function getUidAndFilename($filename) {
78
+        $uid = Filesystem::getOwner($filename);
79
+        $userManager = Server::get(IUserManager::class);
80
+        // if the user with the UID doesn't exists, e.g. because the UID points
81
+        // to a remote user with a federated cloud ID we use the current logged-in
82
+        // user. We need a valid local user to move the file to the right trash bin
83
+        if (!$userManager->userExists($uid)) {
84
+            $uid = OC_User::getUser();
85
+        }
86
+        if (!$uid) {
87
+            // no owner, usually because of share link from ext storage
88
+            return [null, null];
89
+        }
90
+        Filesystem::initMountPoints($uid);
91
+        if ($uid !== OC_User::getUser()) {
92
+            $info = Filesystem::getFileInfo($filename);
93
+            $ownerView = new View('/' . $uid . '/files');
94
+            try {
95
+                $filename = $ownerView->getPath($info['fileid']);
96
+            } catch (NotFoundException $e) {
97
+                $filename = null;
98
+            }
99
+        }
100
+        return [$uid, $filename];
101
+    }
102
+
103
+    /**
104
+     * get original location and deleted by of files for user
105
+     *
106
+     * @param string $user
107
+     * @return array<string, array<string, array{location: string, deletedBy: string}>>
108
+     */
109
+    public static function getExtraData($user) {
110
+        $query = Server::get(IDBConnection::class)->getQueryBuilder();
111
+        $query->select('id', 'timestamp', 'location', 'deleted_by')
112
+            ->from('files_trash')
113
+            ->where($query->expr()->eq('user', $query->createNamedParameter($user)));
114
+        $result = $query->executeQuery();
115
+        $array = [];
116
+        while ($row = $result->fetch()) {
117
+            $array[$row['id']][$row['timestamp']] = [
118
+                'location' => (string)$row['location'],
119
+                'deletedBy' => (string)$row['deleted_by'],
120
+            ];
121
+        }
122
+        $result->closeCursor();
123
+        return $array;
124
+    }
125
+
126
+    /**
127
+     * get original location of file
128
+     *
129
+     * @param string $user
130
+     * @param string $filename
131
+     * @param string $timestamp
132
+     * @return string|false original location
133
+     */
134
+    public static function getLocation($user, $filename, $timestamp) {
135
+        $query = Server::get(IDBConnection::class)->getQueryBuilder();
136
+        $query->select('location')
137
+            ->from('files_trash')
138
+            ->where($query->expr()->eq('user', $query->createNamedParameter($user)))
139
+            ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
140
+            ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
141
+
142
+        $result = $query->executeQuery();
143
+        $row = $result->fetch();
144
+        $result->closeCursor();
145
+
146
+        if (isset($row['location'])) {
147
+            return $row['location'];
148
+        } else {
149
+            return false;
150
+        }
151
+    }
152
+
153
+    /** @param string $user */
154
+    private static function setUpTrash($user): void {
155
+        $view = new View('/' . $user);
156
+        if (!$view->is_dir('files_trashbin')) {
157
+            $view->mkdir('files_trashbin');
158
+        }
159
+        if (!$view->is_dir('files_trashbin/files')) {
160
+            $view->mkdir('files_trashbin/files');
161
+        }
162
+        if (!$view->is_dir('files_trashbin/versions')) {
163
+            $view->mkdir('files_trashbin/versions');
164
+        }
165
+        if (!$view->is_dir('files_trashbin/keys')) {
166
+            $view->mkdir('files_trashbin/keys');
167
+        }
168
+    }
169
+
170
+
171
+    /**
172
+     * copy file to owners trash
173
+     *
174
+     * @param string $sourcePath
175
+     * @param string $owner
176
+     * @param string $targetPath
177
+     * @param string $user
178
+     * @param int $timestamp
179
+     */
180
+    private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp): void {
181
+        self::setUpTrash($owner);
182
+
183
+        $targetFilename = basename($targetPath);
184
+        $targetLocation = dirname($targetPath);
185
+
186
+        $sourceFilename = basename($sourcePath);
187
+
188
+        $view = new View('/');
189
+
190
+        $target = $user . '/files_trashbin/files/' . static::getTrashFilename($targetFilename, $timestamp);
191
+        $source = $owner . '/files_trashbin/files/' . static::getTrashFilename($sourceFilename, $timestamp);
192
+        $free = $view->free_space($target);
193
+        $isUnknownOrUnlimitedFreeSpace = $free < 0;
194
+        $isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
195
+        if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) {
196
+            self::copy_recursive($source, $target, $view);
197
+        }
198
+
199
+
200
+        if ($view->file_exists($target)) {
201
+            $query = Server::get(IDBConnection::class)->getQueryBuilder();
202
+            $query->insert('files_trash')
203
+                ->setValue('id', $query->createNamedParameter($targetFilename))
204
+                ->setValue('timestamp', $query->createNamedParameter($timestamp))
205
+                ->setValue('location', $query->createNamedParameter($targetLocation))
206
+                ->setValue('user', $query->createNamedParameter($user))
207
+                ->setValue('deleted_by', $query->createNamedParameter($user));
208
+            $result = $query->executeStatement();
209
+            if (!$result) {
210
+                Server::get(LoggerInterface::class)->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']);
211
+            }
212
+        }
213
+    }
214
+
215
+
216
+    /**
217
+     * move file to the trash bin
218
+     *
219
+     * @param string $file_path path to the deleted file/directory relative to the files root directory
220
+     * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
221
+     *
222
+     * @return bool
223
+     */
224
+    public static function move2trash($file_path, $ownerOnly = false) {
225
+        // get the user for which the filesystem is setup
226
+        $root = Filesystem::getRoot();
227
+        [, $user] = explode('/', $root);
228
+        [$owner, $ownerPath] = self::getUidAndFilename($file_path);
229
+
230
+        // if no owner found (ex: ext storage + share link), will use the current user's trashbin then
231
+        if (is_null($owner)) {
232
+            $owner = $user;
233
+            $ownerPath = $file_path;
234
+        }
235
+
236
+        $ownerView = new View('/' . $owner);
237
+
238
+        // file has been deleted in between
239
+        if (is_null($ownerPath) || $ownerPath === '') {
240
+            return true;
241
+        }
242
+
243
+        $sourceInfo = $ownerView->getFileInfo('/files/' . $ownerPath);
244
+
245
+        if ($sourceInfo === false) {
246
+            return true;
247
+        }
248
+
249
+        self::setUpTrash($user);
250
+        if ($owner !== $user) {
251
+            // also setup for owner
252
+            self::setUpTrash($owner);
253
+        }
254
+
255
+        $path_parts = pathinfo($ownerPath);
256
+
257
+        $filename = $path_parts['basename'];
258
+        $location = $path_parts['dirname'];
259
+        /** @var ITimeFactory $timeFactory */
260
+        $timeFactory = Server::get(ITimeFactory::class);
261
+        $timestamp = $timeFactory->getTime();
262
+
263
+        $lockingProvider = Server::get(ILockingProvider::class);
264
+
265
+        // disable proxy to prevent recursive calls
266
+        $trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
267
+        $gotLock = false;
268
+
269
+        do {
270
+            /** @var ILockingStorage & IStorage $trashStorage */
271
+            [$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath);
272
+            try {
273
+                $trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
274
+                $gotLock = true;
275
+            } catch (LockedException $e) {
276
+                // a file with the same name is being deleted concurrently
277
+                // nudge the timestamp a bit to resolve the conflict
278
+
279
+                $timestamp = $timestamp + 1;
280
+
281
+                $trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
282
+            }
283
+        } while (!$gotLock);
284
+
285
+        $sourceStorage = $sourceInfo->getStorage();
286
+        $sourceInternalPath = $sourceInfo->getInternalPath();
287
+
288
+        if ($trashStorage->file_exists($trashInternalPath)) {
289
+            $trashStorage->unlink($trashInternalPath);
290
+        }
291
+
292
+        $configuredTrashbinSize = static::getConfiguredTrashbinSize($owner);
293
+        if ($configuredTrashbinSize >= 0 && $sourceInfo->getSize() >= $configuredTrashbinSize) {
294
+            return false;
295
+        }
296
+
297
+        try {
298
+            $moveSuccessful = true;
299
+
300
+            $inCache = $sourceStorage->getCache()->inCache($sourceInternalPath);
301
+            $trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
302
+            if ($inCache) {
303
+                $trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
304
+            }
305
+        } catch (CopyRecursiveException $e) {
306
+            $moveSuccessful = false;
307
+            if ($trashStorage->file_exists($trashInternalPath)) {
308
+                $trashStorage->unlink($trashInternalPath);
309
+            }
310
+            Server::get(LoggerInterface::class)->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
311
+        }
312
+
313
+        if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
314
+            if ($sourceStorage->is_dir($sourceInternalPath)) {
315
+                $sourceStorage->rmdir($sourceInternalPath);
316
+            } else {
317
+                $sourceStorage->unlink($sourceInternalPath);
318
+            }
319
+
320
+            if ($sourceStorage->file_exists($sourceInternalPath)) {
321
+                // undo the cache move
322
+                $sourceStorage->getUpdater()->renameFromStorage($trashStorage, $trashInternalPath, $sourceInternalPath);
323
+            } else {
324
+                $trashStorage->getUpdater()->remove($trashInternalPath);
325
+            }
326
+            return false;
327
+        }
328
+
329
+        if ($moveSuccessful) {
330
+            $query = Server::get(IDBConnection::class)->getQueryBuilder();
331
+            $query->insert('files_trash')
332
+                ->setValue('id', $query->createNamedParameter($filename))
333
+                ->setValue('timestamp', $query->createNamedParameter($timestamp))
334
+                ->setValue('location', $query->createNamedParameter($location))
335
+                ->setValue('user', $query->createNamedParameter($owner))
336
+                ->setValue('deleted_by', $query->createNamedParameter($user));
337
+            $result = $query->executeStatement();
338
+            if (!$result) {
339
+                Server::get(LoggerInterface::class)->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
340
+            }
341
+            Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
342
+                'trashPath' => Filesystem::normalizePath(static::getTrashFilename($filename, $timestamp))]);
343
+
344
+            self::retainVersions($filename, $owner, $ownerPath, $timestamp);
345
+
346
+            // if owner !== user we need to also add a copy to the users trash
347
+            if ($user !== $owner && $ownerOnly === false) {
348
+                self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
349
+            }
350
+        }
351
+
352
+        $trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
353
+
354
+        self::scheduleExpire($user);
355
+
356
+        // if owner !== user we also need to update the owners trash size
357
+        if ($owner !== $user) {
358
+            self::scheduleExpire($owner);
359
+        }
360
+
361
+        return $moveSuccessful;
362
+    }
363
+
364
+    private static function getConfiguredTrashbinSize(string $user): int|float {
365
+        $config = Server::get(IConfig::class);
366
+        $userTrashbinSize = $config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
367
+        if (is_numeric($userTrashbinSize) && ($userTrashbinSize > -1)) {
368
+            return Util::numericToNumber($userTrashbinSize);
369
+        }
370
+        $systemTrashbinSize = $config->getAppValue('files_trashbin', 'trashbin_size', '-1');
371
+        if (is_numeric($systemTrashbinSize)) {
372
+            return Util::numericToNumber($systemTrashbinSize);
373
+        }
374
+        return -1;
375
+    }
376
+
377
+    /**
378
+     * Move file versions to trash so that they can be restored later
379
+     *
380
+     * @param string $filename of deleted file
381
+     * @param string $owner owner user id
382
+     * @param string $ownerPath path relative to the owner's home storage
383
+     * @param int $timestamp when the file was deleted
384
+     */
385
+    private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
386
+        if (Server::get(IAppManager::class)->isEnabledForUser('files_versions') && !empty($ownerPath)) {
387
+            $user = OC_User::getUser();
388
+            $rootView = new View('/');
389
+
390
+            if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
391
+                if ($owner !== $user) {
392
+                    self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . static::getTrashFilename(basename($ownerPath), $timestamp), $rootView);
393
+                }
394
+                self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . static::getTrashFilename($filename, $timestamp));
395
+            } elseif ($versions = Storage::getVersions($owner, $ownerPath)) {
396
+                foreach ($versions as $v) {
397
+                    if ($owner !== $user) {
398
+                        self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . static::getTrashFilename($v['name'] . '.v' . $v['version'], $timestamp));
399
+                    }
400
+                    self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v['version'], $timestamp));
401
+                }
402
+            }
403
+        }
404
+    }
405
+
406
+    /**
407
+     * Move a file or folder on storage level
408
+     *
409
+     * @param View $view
410
+     * @param string $source
411
+     * @param string $target
412
+     * @return bool
413
+     */
414
+    private static function move(View $view, $source, $target) {
415
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
416
+        [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
417
+        /** @var \OC\Files\Storage\Storage $targetStorage */
418
+        [$targetStorage, $targetInternalPath] = $view->resolvePath($target);
419
+        /** @var \OC\Files\Storage\Storage $ownerTrashStorage */
420
+
421
+        $result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
422
+        if ($result) {
423
+            $targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
424
+        }
425
+        return $result;
426
+    }
427
+
428
+    /**
429
+     * Copy a file or folder on storage level
430
+     *
431
+     * @param View $view
432
+     * @param string $source
433
+     * @param string $target
434
+     * @return bool
435
+     */
436
+    private static function copy(View $view, $source, $target) {
437
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
438
+        [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
439
+        /** @var \OC\Files\Storage\Storage $targetStorage */
440
+        [$targetStorage, $targetInternalPath] = $view->resolvePath($target);
441
+        /** @var \OC\Files\Storage\Storage $ownerTrashStorage */
442
+
443
+        $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
444
+        if ($result) {
445
+            $targetStorage->getUpdater()->update($targetInternalPath);
446
+        }
447
+        return $result;
448
+    }
449
+
450
+    /**
451
+     * Restore a file or folder from trash bin
452
+     *
453
+     * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
454
+     *                     including the timestamp suffix ".d12345678"
455
+     * @param string $filename name of the file/folder
456
+     * @param int $timestamp time when the file/folder was deleted
457
+     *
458
+     * @return bool true on success, false otherwise
459
+     */
460
+    public static function restore($file, $filename, $timestamp) {
461
+        $user = OC_User::getUser();
462
+        if (!$user) {
463
+            throw new \Exception('Tried to restore a file while not logged in');
464
+        }
465
+        $view = new View('/' . $user);
466
+
467
+        $location = '';
468
+        if ($timestamp) {
469
+            $location = self::getLocation($user, $filename, $timestamp);
470
+            if ($location === false) {
471
+                Server::get(LoggerInterface::class)->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
472
+            } else {
473
+                // if location no longer exists, restore file in the root directory
474
+                if ($location !== '/'
475
+                    && (!$view->is_dir('files/' . $location)
476
+                        || !$view->isCreatable('files/' . $location))
477
+                ) {
478
+                    $location = '';
479
+                }
480
+            }
481
+        }
482
+
483
+        // we need a  extension in case a file/dir with the same name already exists
484
+        $uniqueFilename = self::getUniqueFilename($location, $filename, $view);
485
+
486
+        $source = Filesystem::normalizePath('files_trashbin/files/' . $file);
487
+        $target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
488
+        if (!$view->file_exists($source)) {
489
+            return false;
490
+        }
491
+        $mtime = $view->filemtime($source);
492
+
493
+        // restore file
494
+        if (!$view->isCreatable(dirname($target))) {
495
+            throw new NotPermittedException("Can't restore trash item because the target folder is not writable");
496
+        }
497
+
498
+        $sourcePath = Filesystem::normalizePath($file);
499
+        $targetPath = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
500
+
501
+        $sourceNode = self::getNodeForPath($user, $sourcePath);
502
+        $targetNode = self::getNodeForPath($user, $targetPath, 'files');
503
+        $run = true;
504
+        $event = new BeforeNodeRestoredEvent($sourceNode, $targetNode, $run);
505
+        $dispatcher = Server::get(IEventDispatcher::class);
506
+        $dispatcher->dispatchTyped($event);
507
+
508
+        if (!$run) {
509
+            return false;
510
+        }
511
+
512
+        $restoreResult = $view->rename($source, $target);
513
+
514
+        // handle the restore result
515
+        if ($restoreResult) {
516
+            $fakeRoot = $view->getRoot();
517
+            $view->chroot('/' . $user . '/files');
518
+            $view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
519
+            $view->chroot($fakeRoot);
520
+            Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => $targetPath, 'trashPath' => $sourcePath]);
521
+
522
+            $sourceNode = self::getNodeForPath($user, $sourcePath);
523
+            $targetNode = self::getNodeForPath($user, $targetPath, 'files');
524
+            $event = new NodeRestoredEvent($sourceNode, $targetNode);
525
+            $dispatcher = Server::get(IEventDispatcher::class);
526
+            $dispatcher->dispatchTyped($event);
527
+
528
+            self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
529
+
530
+            if ($timestamp) {
531
+                $query = Server::get(IDBConnection::class)->getQueryBuilder();
532
+                $query->delete('files_trash')
533
+                    ->where($query->expr()->eq('user', $query->createNamedParameter($user)))
534
+                    ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
535
+                    ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
536
+                $query->executeStatement();
537
+            }
538
+
539
+            return true;
540
+        }
541
+
542
+        return false;
543
+    }
544
+
545
+    /**
546
+     * restore versions from trash bin
547
+     *
548
+     * @param View $view file view
549
+     * @param string $file complete path to file
550
+     * @param string $filename name of file once it was deleted
551
+     * @param string $uniqueFilename new file name to restore the file without overwriting existing files
552
+     * @param string $location location if file
553
+     * @param int $timestamp deletion time
554
+     * @return false|null
555
+     */
556
+    private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
557
+        if (Server::get(IAppManager::class)->isEnabledForUser('files_versions')) {
558
+            $user = OC_User::getUser();
559
+            $rootView = new View('/');
560
+
561
+            $target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
562
+
563
+            [$owner, $ownerPath] = self::getUidAndFilename($target);
564
+
565
+            // file has been deleted in between
566
+            if (empty($ownerPath)) {
567
+                return false;
568
+            }
569
+
570
+            if ($timestamp) {
571
+                $versionedFile = $filename;
572
+            } else {
573
+                $versionedFile = $file;
574
+            }
575
+
576
+            if ($view->is_dir('/files_trashbin/versions/' . $file)) {
577
+                $rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
578
+            } elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
579
+                foreach ($versions as $v) {
580
+                    if ($timestamp) {
581
+                        $rootView->rename($user . '/files_trashbin/versions/' . static::getTrashFilename($versionedFile . '.v' . $v, $timestamp), $owner . '/files_versions/' . $ownerPath . '.v' . $v);
582
+                    } else {
583
+                        $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
584
+                    }
585
+                }
586
+            }
587
+        }
588
+    }
589
+
590
+    /**
591
+     * delete all files from the trash
592
+     */
593
+    public static function deleteAll() {
594
+        $user = OC_User::getUser();
595
+        $userRoot = \OC::$server->getUserFolder($user)->getParent();
596
+        $view = new View('/' . $user);
597
+        $fileInfos = $view->getDirectoryContent('files_trashbin/files');
598
+
599
+        try {
600
+            $trash = $userRoot->get('files_trashbin');
601
+        } catch (NotFoundException $e) {
602
+            return false;
603
+        }
604
+
605
+        // Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
606
+        $filePaths = [];
607
+        foreach ($fileInfos as $fileInfo) {
608
+            $filePaths[] = $view->getRelativePath($fileInfo->getPath());
609
+        }
610
+        unset($fileInfos); // save memory
611
+
612
+        // Bulk PreDelete-Hook
613
+        \OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
614
+
615
+        // Single-File Hooks
616
+        foreach ($filePaths as $path) {
617
+            self::emitTrashbinPreDelete($path);
618
+        }
619
+
620
+        // actual file deletion
621
+        $trash->delete();
622
+
623
+        $query = Server::get(IDBConnection::class)->getQueryBuilder();
624
+        $query->delete('files_trash')
625
+            ->where($query->expr()->eq('user', $query->createNamedParameter($user)));
626
+        $query->executeStatement();
627
+
628
+        // Bulk PostDelete-Hook
629
+        \OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
630
+
631
+        // Single-File Hooks
632
+        foreach ($filePaths as $path) {
633
+            self::emitTrashbinPostDelete($path);
634
+        }
635
+
636
+        $trash = $userRoot->newFolder('files_trashbin');
637
+        $trash->newFolder('files');
638
+
639
+        return true;
640
+    }
641
+
642
+    /**
643
+     * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
644
+     *
645
+     * @param string $path
646
+     */
647
+    protected static function emitTrashbinPreDelete($path) {
648
+        \OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
649
+    }
650
+
651
+    /**
652
+     * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
653
+     *
654
+     * @param string $path
655
+     */
656
+    protected static function emitTrashbinPostDelete($path) {
657
+        \OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
658
+    }
659
+
660
+    /**
661
+     * delete file from trash bin permanently
662
+     *
663
+     * @param string $filename path to the file
664
+     * @param string $user
665
+     * @param int $timestamp of deletion time
666
+     *
667
+     * @return int|float size of deleted files
668
+     */
669
+    public static function delete($filename, $user, $timestamp = null) {
670
+        $userRoot = \OC::$server->getUserFolder($user)->getParent();
671
+        $view = new View('/' . $user);
672
+        $size = 0;
673
+
674
+        if ($timestamp) {
675
+            $query = Server::get(IDBConnection::class)->getQueryBuilder();
676
+            $query->delete('files_trash')
677
+                ->where($query->expr()->eq('user', $query->createNamedParameter($user)))
678
+                ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
679
+                ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
680
+            $query->executeStatement();
681
+
682
+            $file = static::getTrashFilename($filename, $timestamp);
683
+        } else {
684
+            $file = $filename;
685
+        }
686
+
687
+        $size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
688
+
689
+        try {
690
+            $node = $userRoot->get('/files_trashbin/files/' . $file);
691
+        } catch (NotFoundException $e) {
692
+            return $size;
693
+        }
694
+
695
+        if ($node instanceof Folder) {
696
+            $size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
697
+        } elseif ($node instanceof File) {
698
+            $size += $view->filesize('/files_trashbin/files/' . $file);
699
+        }
700
+
701
+        self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
702
+        $node->delete();
703
+        self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
704
+
705
+        return $size;
706
+    }
707
+
708
+    /**
709
+     * @param string $file
710
+     * @param string $filename
711
+     * @param ?int $timestamp
712
+     */
713
+    private static function deleteVersions(View $view, $file, $filename, $timestamp, string $user): int|float {
714
+        $size = 0;
715
+        if (Server::get(IAppManager::class)->isEnabledForUser('files_versions')) {
716
+            if ($view->is_dir('files_trashbin/versions/' . $file)) {
717
+                $size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
718
+                $view->unlink('files_trashbin/versions/' . $file);
719
+            } elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
720
+                foreach ($versions as $v) {
721
+                    if ($timestamp) {
722
+                        $size += $view->filesize('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
723
+                        $view->unlink('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
724
+                    } else {
725
+                        $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
726
+                        $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
727
+                    }
728
+                }
729
+            }
730
+        }
731
+        return $size;
732
+    }
733
+
734
+    /**
735
+     * check to see whether a file exists in trashbin
736
+     *
737
+     * @param string $filename path to the file
738
+     * @param int $timestamp of deletion time
739
+     * @return bool true if file exists, otherwise false
740
+     */
741
+    public static function file_exists($filename, $timestamp = null) {
742
+        $user = OC_User::getUser();
743
+        $view = new View('/' . $user);
744
+
745
+        if ($timestamp) {
746
+            $filename = static::getTrashFilename($filename, $timestamp);
747
+        }
748
+
749
+        $target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
750
+        return $view->file_exists($target);
751
+    }
752
+
753
+    /**
754
+     * deletes used space for trash bin in db if user was deleted
755
+     *
756
+     * @param string $uid id of deleted user
757
+     * @return bool result of db delete operation
758
+     */
759
+    public static function deleteUser($uid) {
760
+        $query = Server::get(IDBConnection::class)->getQueryBuilder();
761
+        $query->delete('files_trash')
762
+            ->where($query->expr()->eq('user', $query->createNamedParameter($uid)));
763
+        return (bool)$query->executeStatement();
764
+    }
765
+
766
+    /**
767
+     * calculate remaining free space for trash bin
768
+     *
769
+     * @param int|float $trashbinSize current size of the trash bin
770
+     * @param string $user
771
+     * @return int|float available free space for trash bin
772
+     */
773
+    private static function calculateFreeSpace(int|float $trashbinSize, string $user): int|float {
774
+        $configuredTrashbinSize = static::getConfiguredTrashbinSize($user);
775
+        if ($configuredTrashbinSize > -1) {
776
+            return $configuredTrashbinSize - $trashbinSize;
777
+        }
778
+
779
+        $userObject = Server::get(IUserManager::class)->get($user);
780
+        if (is_null($userObject)) {
781
+            return 0;
782
+        }
783
+        $softQuota = true;
784
+        $quota = $userObject->getQuota();
785
+        if ($quota === null || $quota === 'none') {
786
+            $quota = Filesystem::free_space('/');
787
+            $softQuota = false;
788
+            // inf or unknown free space
789
+            if ($quota < 0) {
790
+                $quota = PHP_INT_MAX;
791
+            }
792
+        } else {
793
+            $quota = Util::computerFileSize($quota);
794
+            // invalid quota
795
+            if ($quota === false) {
796
+                $quota = PHP_INT_MAX;
797
+            }
798
+        }
799
+
800
+        // calculate available space for trash bin
801
+        // subtract size of files and current trash bin size from quota
802
+        if ($softQuota) {
803
+            $userFolder = \OC::$server->getUserFolder($user);
804
+            if (is_null($userFolder)) {
805
+                return 0;
806
+            }
807
+            $free = $quota - $userFolder->getSize(false); // remaining free space for user
808
+            if ($free > 0) {
809
+                $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
810
+            } else {
811
+                $availableSpace = $free - $trashbinSize;
812
+            }
813
+        } else {
814
+            $availableSpace = $quota;
815
+        }
816
+
817
+        return Util::numericToNumber($availableSpace);
818
+    }
819
+
820
+    /**
821
+     * resize trash bin if necessary after a new file was added to Nextcloud
822
+     *
823
+     * @param string $user user id
824
+     */
825
+    public static function resizeTrash($user) {
826
+        $size = self::getTrashbinSize($user);
827
+
828
+        $freeSpace = self::calculateFreeSpace($size, $user);
829
+
830
+        if ($freeSpace < 0) {
831
+            self::scheduleExpire($user);
832
+        }
833
+    }
834
+
835
+    /**
836
+     * clean up the trash bin
837
+     *
838
+     * @param string $user
839
+     */
840
+    public static function expire($user) {
841
+        $trashBinSize = self::getTrashbinSize($user);
842
+        $availableSpace = self::calculateFreeSpace($trashBinSize, $user);
843
+
844
+        $dirContent = Helper::getTrashFiles('/', $user, 'mtime');
845
+
846
+        // delete all files older then $retention_obligation
847
+        [$delSize, $count] = self::deleteExpiredFiles($dirContent, $user);
848
+
849
+        $availableSpace += $delSize;
850
+
851
+        // delete files from trash until we meet the trash bin size limit again
852
+        self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
853
+    }
854
+
855
+    /**
856
+     * @param string $user
857
+     */
858
+    private static function scheduleExpire($user) {
859
+        // let the admin disable auto expire
860
+        /** @var Application $application */
861
+        $application = Server::get(Application::class);
862
+        $expiration = $application->getContainer()->query('Expiration');
863
+        if ($expiration->isEnabled()) {
864
+            Server::get(IBus::class)->push(new Expire($user));
865
+        }
866
+    }
867
+
868
+    /**
869
+     * if the size limit for the trash bin is reached, we delete the oldest
870
+     * files in the trash bin until we meet the limit again
871
+     *
872
+     * @param array $files
873
+     * @param string $user
874
+     * @param int|float $availableSpace available disc space
875
+     * @return int|float size of deleted files
876
+     */
877
+    protected static function deleteFiles(array $files, string $user, int|float $availableSpace): int|float {
878
+        /** @var Application $application */
879
+        $application = Server::get(Application::class);
880
+        $expiration = $application->getContainer()->query('Expiration');
881
+        $size = 0;
882
+
883
+        if ($availableSpace < 0) {
884
+            foreach ($files as $file) {
885
+                if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
886
+                    $tmp = self::delete($file['name'], $user, $file['mtime']);
887
+                    Server::get(LoggerInterface::class)->info(
888
+                        'remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota) for user "{user}"',
889
+                        [
890
+                            'app' => 'files_trashbin',
891
+                            'user' => $user,
892
+                        ]
893
+                    );
894
+                    $availableSpace += $tmp;
895
+                    $size += $tmp;
896
+                } else {
897
+                    break;
898
+                }
899
+            }
900
+        }
901
+        return $size;
902
+    }
903
+
904
+    /**
905
+     * delete files older then max storage time
906
+     *
907
+     * @param array $files list of files sorted by mtime
908
+     * @param string $user
909
+     * @return array{int|float, int} size of deleted files and number of deleted files
910
+     */
911
+    public static function deleteExpiredFiles($files, $user) {
912
+        /** @var Expiration $expiration */
913
+        $expiration = Server::get(Expiration::class);
914
+        $size = 0;
915
+        $count = 0;
916
+        foreach ($files as $file) {
917
+            $timestamp = $file['mtime'];
918
+            $filename = $file['name'];
919
+            if ($expiration->isExpired($timestamp)) {
920
+                try {
921
+                    $size += self::delete($filename, $user, $timestamp);
922
+                    $count++;
923
+                } catch (NotPermittedException $e) {
924
+                    Server::get(LoggerInterface::class)->warning('Removing "' . $filename . '" from trashbin failed for user "{user}"',
925
+                        [
926
+                            'exception' => $e,
927
+                            'app' => 'files_trashbin',
928
+                            'user' => $user,
929
+                        ]
930
+                    );
931
+                }
932
+                Server::get(LoggerInterface::class)->info(
933
+                    'Remove "' . $filename . '" from trashbin for user "{user}" because it exceeds max retention obligation term.',
934
+                    [
935
+                        'app' => 'files_trashbin',
936
+                        'user' => $user,
937
+                    ],
938
+                );
939
+            } else {
940
+                break;
941
+            }
942
+        }
943
+
944
+        return [$size, $count];
945
+    }
946
+
947
+    /**
948
+     * recursive copy to copy a whole directory
949
+     *
950
+     * @param string $source source path, relative to the users files directory
951
+     * @param string $destination destination path relative to the users root directory
952
+     * @param View $view file view for the users root directory
953
+     * @return int|float
954
+     * @throws Exceptions\CopyRecursiveException
955
+     */
956
+    private static function copy_recursive($source, $destination, View $view): int|float {
957
+        $size = 0;
958
+        if ($view->is_dir($source)) {
959
+            $view->mkdir($destination);
960
+            $view->touch($destination, $view->filemtime($source));
961
+            foreach ($view->getDirectoryContent($source) as $i) {
962
+                $pathDir = $source . '/' . $i['name'];
963
+                if ($view->is_dir($pathDir)) {
964
+                    $size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
965
+                } else {
966
+                    $size += $view->filesize($pathDir);
967
+                    $result = $view->copy($pathDir, $destination . '/' . $i['name']);
968
+                    if (!$result) {
969
+                        throw new CopyRecursiveException();
970
+                    }
971
+                    $view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
972
+                }
973
+            }
974
+        } else {
975
+            $size += $view->filesize($source);
976
+            $result = $view->copy($source, $destination);
977
+            if (!$result) {
978
+                throw new CopyRecursiveException();
979
+            }
980
+            $view->touch($destination, $view->filemtime($source));
981
+        }
982
+        return $size;
983
+    }
984
+
985
+    /**
986
+     * find all versions which belong to the file we want to restore
987
+     *
988
+     * @param string $filename name of the file which should be restored
989
+     * @param int $timestamp timestamp when the file was deleted
990
+     */
991
+    private static function getVersionsFromTrash($filename, $timestamp, string $user): array {
992
+        $view = new View('/' . $user . '/files_trashbin/versions');
993
+        $versions = [];
994
+
995
+        /** @var \OC\Files\Storage\Storage $storage */
996
+        [$storage,] = $view->resolvePath('/');
997
+
998
+        $pattern = Server::get(IDBConnection::class)->escapeLikeParameter(basename($filename));
999
+        if ($timestamp) {
1000
+            // fetch for old versions
1001
+            $escapedTimestamp = Server::get(IDBConnection::class)->escapeLikeParameter((string)$timestamp);
1002
+            $pattern .= '.v%.d' . $escapedTimestamp;
1003
+            $offset = -strlen($escapedTimestamp) - 2;
1004
+        } else {
1005
+            $pattern .= '.v%';
1006
+        }
1007
+
1008
+        // Manually fetch all versions from the file cache to be able to filter them by their parent
1009
+        $cache = $storage->getCache('');
1010
+        $query = new CacheQueryBuilder(
1011
+            Server::get(IDBConnection::class)->getQueryBuilder(),
1012
+            Server::get(IFilesMetadataManager::class),
1013
+        );
1014
+        $normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/' . $filename)), '/');
1015
+        $parentId = $cache->getId($normalizedParentPath);
1016
+        if ($parentId === -1) {
1017
+            return [];
1018
+        }
1019
+
1020
+        $query->selectFileCache()
1021
+            ->whereStorageId($cache->getNumericStorageId())
1022
+            ->andWhere($query->expr()->eq('parent', $query->createNamedParameter($parentId)))
1023
+            ->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
1024
+
1025
+        $result = $query->executeQuery();
1026
+        $entries = $result->fetchAll();
1027
+        $result->closeCursor();
1028
+
1029
+        /** @var CacheEntry[] $matches */
1030
+        $matches = array_map(function (array $data) {
1031
+            return Cache::cacheEntryFromData($data, Server::get(IMimeTypeLoader::class));
1032
+        }, $entries);
1033
+
1034
+        foreach ($matches as $ma) {
1035
+            if ($timestamp) {
1036
+                $parts = explode('.v', substr($ma['path'], 0, $offset));
1037
+                $versions[] = end($parts);
1038
+            } else {
1039
+                $parts = explode('.v', $ma['path']);
1040
+                $versions[] = end($parts);
1041
+            }
1042
+        }
1043
+
1044
+        return $versions;
1045
+    }
1046
+
1047
+    /**
1048
+     * find unique extension for restored file if a file with the same name already exists
1049
+     *
1050
+     * @param string $location where the file should be restored
1051
+     * @param string $filename name of the file
1052
+     * @param View $view filesystem view relative to users root directory
1053
+     * @return string with unique extension
1054
+     */
1055
+    private static function getUniqueFilename($location, $filename, View $view) {
1056
+        $ext = pathinfo($filename, PATHINFO_EXTENSION);
1057
+        $name = pathinfo($filename, PATHINFO_FILENAME);
1058
+        $l = Util::getL10N('files_trashbin');
1059
+
1060
+        $location = '/' . trim($location, '/');
1061
+
1062
+        // if extension is not empty we set a dot in front of it
1063
+        if ($ext !== '') {
1064
+            $ext = '.' . $ext;
1065
+        }
1066
+
1067
+        if ($view->file_exists('files' . $location . '/' . $filename)) {
1068
+            $i = 2;
1069
+            $uniqueName = $name . ' (' . $l->t('restored') . ')' . $ext;
1070
+            while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
1071
+                $uniqueName = $name . ' (' . $l->t('restored') . ' ' . $i . ')' . $ext;
1072
+                $i++;
1073
+            }
1074
+
1075
+            return $uniqueName;
1076
+        }
1077
+
1078
+        return $filename;
1079
+    }
1080
+
1081
+    /**
1082
+     * get the size from a given root folder
1083
+     *
1084
+     * @param View $view file view on the root folder
1085
+     * @return int|float size of the folder
1086
+     */
1087
+    private static function calculateSize(View $view): int|float {
1088
+        $root = Server::get(IConfig::class)->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
1089
+        if (!file_exists($root)) {
1090
+            return 0;
1091
+        }
1092
+        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
1093
+        $size = 0;
1094
+
1095
+        /**
1096
+         * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
1097
+         * This bug is fixed in PHP 5.5.9 or before
1098
+         * See #8376
1099
+         */
1100
+        $iterator->rewind();
1101
+        while ($iterator->valid()) {
1102
+            $path = $iterator->current();
1103
+            $relpath = substr($path, strlen($root) - 1);
1104
+            if (!$view->is_dir($relpath)) {
1105
+                $size += $view->filesize($relpath);
1106
+            }
1107
+            $iterator->next();
1108
+        }
1109
+        return $size;
1110
+    }
1111
+
1112
+    /**
1113
+     * get current size of trash bin from a given user
1114
+     *
1115
+     * @param string $user user who owns the trash bin
1116
+     * @return int|float trash bin size
1117
+     */
1118
+    private static function getTrashbinSize(string $user): int|float {
1119
+        $view = new View('/' . $user);
1120
+        $fileInfo = $view->getFileInfo('/files_trashbin');
1121
+        return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1122
+    }
1123
+
1124
+    /**
1125
+     * check if trash bin is empty for a given user
1126
+     *
1127
+     * @param string $user
1128
+     * @return bool
1129
+     */
1130
+    public static function isEmpty($user) {
1131
+        $view = new View('/' . $user . '/files_trashbin');
1132
+        if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1133
+            while (($file = readdir($dh)) !== false) {
1134
+                if (!Filesystem::isIgnoredDir($file)) {
1135
+                    return false;
1136
+                }
1137
+            }
1138
+        }
1139
+        return true;
1140
+    }
1141
+
1142
+    /**
1143
+     * @param $path
1144
+     * @return string
1145
+     */
1146
+    public static function preview_icon($path) {
1147
+        return Server::get(IURLGenerator::class)->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]);
1148
+    }
1149
+
1150
+    /**
1151
+     * Return the filename used in the trash bin
1152
+     */
1153
+    public static function getTrashFilename(string $filename, int $timestamp): string {
1154
+        $trashFilename = $filename . '.d' . $timestamp;
1155
+        $length = strlen($trashFilename);
1156
+        // oc_filecache `name` column has a limit of 250 chars
1157
+        $maxLength = 250;
1158
+        if ($length > $maxLength) {
1159
+            $trashFilename = substr_replace(
1160
+                $trashFilename,
1161
+                '',
1162
+                $maxLength / 2,
1163
+                $length - $maxLength
1164
+            );
1165
+        }
1166
+        return $trashFilename;
1167
+    }
1168
+
1169
+    private static function getNodeForPath(string $user, string $path, string $baseDir = 'files_trashbin/files'): Node {
1170
+        $rootFolder = Server::get(IRootFolder::class);
1171
+        $path = ltrim($path, '/');
1172
+
1173
+        $userFolder = $rootFolder->getUserFolder($user);
1174
+        /** @var Folder $trashFolder */
1175
+        $trashFolder = $userFolder->getParent()->get($baseDir);
1176
+        try {
1177
+            return $trashFolder->get($path);
1178
+        } catch (NotFoundException $ex) {
1179
+        }
1180
+
1181
+        $view = Server::get(View::class);
1182
+        $fullPath = '/' . $user . '/' . $baseDir . '/' . $path;
1183
+
1184
+        if (Filesystem::is_dir($path)) {
1185
+            return new NonExistingFolder($rootFolder, $view, $fullPath);
1186
+        } else {
1187
+            return new NonExistingFile($rootFolder, $view, $fullPath);
1188
+        }
1189
+    }
1190
+
1191
+    public function handle(Event $event): void {
1192
+        if ($event instanceof BeforeNodeDeletedEvent) {
1193
+            self::ensureFileScannedHook($event->getNode());
1194
+        }
1195
+    }
1196 1196
 }
Please login to merge, or discard this patch.