View   F
last analyzed

Complexity

Total Complexity 376

Size/Duplication

Total Lines 2072
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 964
c 0
b 0
f 0
dl 0
loc 2072
rs 1.6359
wmc 376

81 Methods

Rating   Name   Duplication   Size   Complexity  
A searchByTag() 0 2 1
A searchRaw() 0 2 1
A search() 0 2 1
A searchByMime() 0 2 1
A putFileInfo() 0 22 4
A hash() 0 22 6
B searchCommon() 0 44 8
A is_file() 0 5 2
A getOwner() 0 11 3
A getParents() 0 19 4
A file_exists() 0 5 2
A getHookPath() 0 6 2
A readfile() 0 16 4
A chroot() 0 7 3
A opendir() 0 2 1
C readfilePart() 0 44 15
C copy() 0 84 13
A deleteAll() 0 2 1
A getAbsolutePath() 0 12 4
A resolvePath() 0 4 1
A addSubMounts() 0 5 2
A emit_file_hooks_post() 0 12 2
A isReadable() 0 2 1
A unlockPath() 0 18 4
A stat() 0 2 1
A verifyPath() 0 20 6
A getRelativePath() 0 21 5
D getDirectoryContent() 0 125 23
A fromTmpFile() 0 33 6
A file_get_contents() 0 2 1
A runHooks() 0 28 6
A getMountForLock() 0 13 3
A filetype() 0 2 1
A isUpdatable() 0 2 1
A toTmpFile() 0 14 3
B unlink() 0 23 8
A isSharable() 0 2 1
A lockFile() 0 15 3
B getPath() 0 38 8
A getPathRelativeToFiles() 0 17 4
A mkdir() 0 2 1
A getMimeType() 0 3 1
A targetIsNotShared() 0 25 3
A __construct() 0 10 2
A getRoot() 0 2 1
A writeUpdate() 0 6 3
A getLocalFile() 0 8 4
A createParentDirectories() 0 15 4
A removeMount() 0 26 3
A filemtime() 0 2 1
A is_dir() 0 5 2
A assertPathLength() 0 7 2
C fopen() 0 38 15
A enableCacheUpdate() 0 2 1
A getUidAndFilename() 0 16 4
A filesize() 0 2 1
B shouldEmitHooks() 0 21 8
A getMountPoint() 0 2 1
C getFileInfo() 0 46 12
A isDeletable() 0 7 2
A removeUpdate() 0 3 2
A getUserObjectForOwner() 0 2 1
B getCacheEntry() 0 27 8
A getMount() 0 2 1
C file_put_contents() 0 58 12
B touch() 0 30 8
A isCreatable() 0 2 1
A unlockFile() 0 15 3
A emit_file_hooks_pre() 0 15 2
A lockPath() 0 27 5
A rmdir() 0 18 5
B changeLock() 0 36 6
A free_space() 0 7 2
A hasUpdated() 0 2 1
F basicOperation() 0 89 42
A renameUpdate() 0 3 2
A shouldLockFile() 0 10 3
A disableCacheUpdate() 0 2 1
A getETag() 0 6 2
F rename() 0 118 31
A getPartFileInfo() 0 21 1

How to fix   Complexity   

Complex Class

Complex classes like View often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use View, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Ashod Nakashian <[email protected]>
7
 * @author Bart Visscher <[email protected]>
8
 * @author Björn Schießle <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Florin Peter <[email protected]>
11
 * @author Jesús Macias <[email protected]>
12
 * @author Joas Schilling <[email protected]>
13
 * @author Jörn Friedrich Dreyer <[email protected]>
14
 * @author Julius Härtl <[email protected]>
15
 * @author karakayasemi <[email protected]>
16
 * @author Klaas Freitag <[email protected]>
17
 * @author korelstar <[email protected]>
18
 * @author Lukas Reschke <[email protected]>
19
 * @author Luke Policinski <[email protected]>
20
 * @author Michael Gapczynski <[email protected]>
21
 * @author Morris Jobke <[email protected]>
22
 * @author Piotr Filiciak <[email protected]>
23
 * @author Robin Appelman <[email protected]>
24
 * @author Robin McCorkell <[email protected]>
25
 * @author Roeland Jago Douma <[email protected]>
26
 * @author Sam Tuke <[email protected]>
27
 * @author Scott Dutton <[email protected]>
28
 * @author Thomas Müller <[email protected]>
29
 * @author Thomas Tanghus <[email protected]>
30
 * @author Vincent Petry <[email protected]>
31
 *
32
 * @license AGPL-3.0
33
 *
34
 * This code is free software: you can redistribute it and/or modify
35
 * it under the terms of the GNU Affero General Public License, version 3,
36
 * as published by the Free Software Foundation.
37
 *
38
 * This program is distributed in the hope that it will be useful,
39
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
40
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
41
 * GNU Affero General Public License for more details.
42
 *
43
 * You should have received a copy of the GNU Affero General Public License, version 3,
44
 * along with this program. If not, see <http://www.gnu.org/licenses/>
45
 *
46
 */
47
namespace OC\Files;
48
49
use Icewind\Streams\CallbackWrapper;
50
use OC\Files\Mount\MoveableMount;
51
use OC\Files\Storage\Storage;
52
use OC\User\LazyUser;
53
use OC\Share\Share;
54
use OC\User\User;
55
use OC\User\Manager as UserManager;
56
use OCA\Files_Sharing\SharedMount;
57
use OCP\Constants;
58
use OCP\Files\Cache\ICacheEntry;
59
use OCP\Files\EmptyFileNameException;
60
use OCP\Files\FileNameTooLongException;
61
use OCP\Files\InvalidCharacterInPathException;
62
use OCP\Files\InvalidDirectoryException;
63
use OCP\Files\InvalidPathException;
64
use OCP\Files\Mount\IMountPoint;
65
use OCP\Files\NotFoundException;
66
use OCP\Files\ReservedWordException;
67
use OCP\Files\Storage\IStorage;
68
use OCP\IUser;
69
use OCP\Lock\ILockingProvider;
70
use OCP\Lock\LockedException;
71
use Psr\Log\LoggerInterface;
72
73
/**
74
 * Class to provide access to ownCloud filesystem via a "view", and methods for
75
 * working with files within that view (e.g. read, write, delete, etc.). Each
76
 * view is restricted to a set of directories via a virtual root. The default view
77
 * uses the currently logged in user's data directory as root (parts of
78
 * OC_Filesystem are merely a wrapper for OC\Files\View).
79
 *
80
 * Apps that need to access files outside of the user data folders (to modify files
81
 * belonging to a user other than the one currently logged in, for example) should
82
 * use this class directly rather than using OC_Filesystem, or making use of PHP's
83
 * built-in file manipulation functions. This will ensure all hooks and proxies
84
 * are triggered correctly.
85
 *
86
 * Filesystem functions are not called directly; they are passed to the correct
87
 * \OC\Files\Storage\Storage object
88
 */
89
class View {
90
	private string $fakeRoot = '';
91
	private ILockingProvider $lockingProvider;
92
	private bool $lockingEnabled;
93
	private bool $updaterEnabled = true;
94
	private UserManager $userManager;
95
	private LoggerInterface $logger;
96
97
	/**
98
	 * @throws \Exception If $root contains an invalid path
99
	 */
100
	public function __construct(string $root = '') {
101
		if (!Filesystem::isValidPath($root)) {
102
			throw new \Exception();
103
		}
104
105
		$this->fakeRoot = $root;
106
		$this->lockingProvider = \OC::$server->getLockingProvider();
107
		$this->lockingEnabled = !($this->lockingProvider instanceof \OC\Lock\NoopLockingProvider);
108
		$this->userManager = \OC::$server->getUserManager();
109
		$this->logger = \OC::$server->get(LoggerInterface::class);
110
	}
111
112
	/**
113
	 * @param ?string $path
114
	 * @psalm-template S as string|null
115
	 * @psalm-param S $path
116
	 * @psalm-return (S is string ? string : null)
117
	 */
118
	public function getAbsolutePath($path = '/'): ?string {
119
		if ($path === null) {
120
			return null;
121
		}
122
		$this->assertPathLength($path);
123
		if ($path === '') {
124
			$path = '/';
125
		}
126
		if ($path[0] !== '/') {
127
			$path = '/' . $path;
128
		}
129
		return $this->fakeRoot . $path;
130
	}
131
132
	/**
133
	 * Change the root to a fake root
134
	 *
135
	 * @param string $fakeRoot
136
	 */
137
	public function chroot($fakeRoot): void {
138
		if (!$fakeRoot == '') {
139
			if ($fakeRoot[0] !== '/') {
140
				$fakeRoot = '/' . $fakeRoot;
141
			}
142
		}
143
		$this->fakeRoot = $fakeRoot;
144
	}
145
146
	/**
147
	 * Get the fake root
148
	 */
149
	public function getRoot(): string {
150
		return $this->fakeRoot;
151
	}
152
153
	/**
154
	 * get path relative to the root of the view
155
	 *
156
	 * @param string $path
157
	 */
158
	public function getRelativePath($path): ?string {
159
		$this->assertPathLength($path);
160
		if ($this->fakeRoot == '') {
161
			return $path;
162
		}
163
164
		if (rtrim($path, '/') === rtrim($this->fakeRoot, '/')) {
165
			return '/';
166
		}
167
168
		// missing slashes can cause wrong matches!
169
		$root = rtrim($this->fakeRoot, '/') . '/';
170
171
		if (!str_starts_with($path, $root)) {
172
			return null;
173
		} else {
174
			$path = substr($path, strlen($this->fakeRoot));
175
			if (strlen($path) === 0) {
176
				return '/';
177
			} else {
178
				return $path;
179
			}
180
		}
181
	}
182
183
	/**
184
	 * Get the mountpoint of the storage object for a path
185
	 * ( note: because a storage is not always mounted inside the fakeroot, the
186
	 * returned mountpoint is relative to the absolute root of the filesystem
187
	 * and does not take the chroot into account )
188
	 *
189
	 * @param string $path
190
	 */
191
	public function getMountPoint($path): string {
192
		return Filesystem::getMountPoint($this->getAbsolutePath($path));
193
	}
194
195
	/**
196
	 * Get the mountpoint of the storage object for a path
197
	 * ( note: because a storage is not always mounted inside the fakeroot, the
198
	 * returned mountpoint is relative to the absolute root of the filesystem
199
	 * and does not take the chroot into account )
200
	 *
201
	 * @param string $path
202
	 */
203
	public function getMount($path): IMountPoint {
204
		return Filesystem::getMountManager()->find($this->getAbsolutePath($path));
205
	}
206
207
	/**
208
	 * Resolve a path to a storage and internal path
209
	 *
210
	 * @param string $path
211
	 * @return array{?\OCP\Files\Storage\IStorage, string} an array consisting of the storage and the internal path
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{?\OCP\Files\Storage\IStorage, string} at position 2 could not be parsed: Expected ':' at position 2, but found '?\OCP\Files\Storage\IStorage'.
Loading history...
212
	 */
213
	public function resolvePath($path): array {
214
		$a = $this->getAbsolutePath($path);
215
		$p = Filesystem::normalizePath($a);
216
		return Filesystem::resolvePath($p);
217
	}
218
219
	/**
220
	 * Return the path to a local version of the file
221
	 * we need this because we can't know if a file is stored local or not from
222
	 * outside the filestorage and for some purposes a local file is needed
223
	 *
224
	 * @param string $path
225
	 */
226
	public function getLocalFile($path): string|false {
227
		$parent = substr($path, 0, strrpos($path, '/') ?: 0);
228
		$path = $this->getAbsolutePath($path);
229
		[$storage, $internalPath] = Filesystem::resolvePath($path);
230
		if (Filesystem::isValidPath($parent) && $storage) {
231
			return $storage->getLocalFile($internalPath);
232
		} else {
233
			return false;
234
		}
235
	}
236
237
	/**
238
	 * the following functions operate with arguments and return values identical
239
	 * to those of their PHP built-in equivalents. Mostly they are merely wrappers
240
	 * for \OC\Files\Storage\Storage via basicOperation().
241
	 */
242
	public function mkdir($path) {
243
		return $this->basicOperation('mkdir', $path, ['create', 'write']);
244
	}
245
246
	/**
247
	 * remove mount point
248
	 *
249
	 * @param IMountPoint $mount
250
	 * @param string $path relative to data/
251
	 */
252
	protected function removeMount($mount, $path): bool {
253
		if ($mount instanceof MoveableMount) {
254
			// cut of /user/files to get the relative path to data/user/files
255
			$pathParts = explode('/', $path, 4);
256
			$relPath = '/' . $pathParts[3];
257
			$this->lockFile($relPath, ILockingProvider::LOCK_SHARED, true);
258
			\OC_Hook::emit(
259
				Filesystem::CLASSNAME, "umount",
260
				[Filesystem::signal_param_path => $relPath]
261
			);
262
			$this->changeLock($relPath, ILockingProvider::LOCK_EXCLUSIVE, true);
263
			$result = $mount->removeMount();
264
			$this->changeLock($relPath, ILockingProvider::LOCK_SHARED, true);
265
			if ($result) {
266
				\OC_Hook::emit(
267
					Filesystem::CLASSNAME, "post_umount",
268
					[Filesystem::signal_param_path => $relPath]
269
				);
270
			}
271
			$this->unlockFile($relPath, ILockingProvider::LOCK_SHARED, true);
272
			return $result;
273
		} else {
274
			// do not allow deleting the storage's root / the mount point
275
			// because for some storages it might delete the whole contents
276
			// but isn't supposed to work that way
277
			return false;
278
		}
279
	}
280
281
	public function disableCacheUpdate(): void {
282
		$this->updaterEnabled = false;
283
	}
284
285
	public function enableCacheUpdate(): void {
286
		$this->updaterEnabled = true;
287
	}
288
289
	protected function writeUpdate(Storage $storage, string $internalPath, ?int $time = null): void {
290
		if ($this->updaterEnabled) {
291
			if (is_null($time)) {
292
				$time = time();
293
			}
294
			$storage->getUpdater()->update($internalPath, $time);
295
		}
296
	}
297
298
	protected function removeUpdate(Storage $storage, string $internalPath): void {
299
		if ($this->updaterEnabled) {
300
			$storage->getUpdater()->remove($internalPath);
301
		}
302
	}
303
304
	protected function renameUpdate(Storage $sourceStorage, Storage $targetStorage, string $sourceInternalPath, string $targetInternalPath): void {
305
		if ($this->updaterEnabled) {
306
			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
307
		}
308
	}
309
310
	/**
311
	 * @param string $path
312
	 * @return bool|mixed
313
	 */
314
	public function rmdir($path) {
315
		$absolutePath = $this->getAbsolutePath($path);
316
		$mount = Filesystem::getMountManager()->find($absolutePath);
317
		if ($mount->getInternalPath($absolutePath) === '') {
318
			return $this->removeMount($mount, $absolutePath);
319
		}
320
		if ($this->is_dir($path)) {
321
			$result = $this->basicOperation('rmdir', $path, ['delete']);
322
		} else {
323
			$result = false;
324
		}
325
326
		if (!$result && !$this->file_exists($path)) { //clear ghost files from the cache on delete
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
Bug Best Practice introduced by
The expression $this->file_exists($path) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
327
			$storage = $mount->getStorage();
328
			$internalPath = $mount->getInternalPath($absolutePath);
329
			$storage->getUpdater()->remove($internalPath);
330
		}
331
		return $result;
332
	}
333
334
	/**
335
	 * @param string $path
336
	 * @return resource|false
337
	 */
338
	public function opendir($path) {
339
		return $this->basicOperation('opendir', $path, ['read']);
340
	}
341
342
	/**
343
	 * @param string $path
344
	 * @return bool|mixed
345
	 */
346
	public function is_dir($path) {
347
		if ($path == '/') {
348
			return true;
349
		}
350
		return $this->basicOperation('is_dir', $path);
351
	}
352
353
	/**
354
	 * @param string $path
355
	 * @return bool|mixed
356
	 */
357
	public function is_file($path) {
358
		if ($path == '/') {
359
			return false;
360
		}
361
		return $this->basicOperation('is_file', $path);
362
	}
363
364
	/**
365
	 * @param string $path
366
	 * @return mixed
367
	 */
368
	public function stat($path) {
369
		return $this->basicOperation('stat', $path);
370
	}
371
372
	/**
373
	 * @param string $path
374
	 * @return mixed
375
	 */
376
	public function filetype($path) {
377
		return $this->basicOperation('filetype', $path);
378
	}
379
380
	/**
381
	 * @param string $path
382
	 * @return mixed
383
	 */
384
	public function filesize(string $path) {
385
		return $this->basicOperation('filesize', $path);
386
	}
387
388
	/**
389
	 * @param string $path
390
	 * @return bool|mixed
391
	 * @throws InvalidPathException
392
	 */
393
	public function readfile($path) {
394
		$this->assertPathLength($path);
395
		if (ob_get_level()) {
396
			ob_end_clean();
397
		}
398
		$handle = $this->fopen($path, 'rb');
399
		if ($handle) {
0 ignored issues
show
introduced by
$handle is of type resource, thus it always evaluated to false.
Loading history...
400
			$chunkSize = 524288; // 512 kB chunks
401
			while (!feof($handle)) {
402
				echo fread($handle, $chunkSize);
403
				flush();
404
			}
405
			fclose($handle);
406
			return $this->filesize($path);
407
		}
408
		return false;
409
	}
410
411
	/**
412
	 * @param string $path
413
	 * @param int $from
414
	 * @param int $to
415
	 * @return bool|mixed
416
	 * @throws InvalidPathException
417
	 * @throws \OCP\Files\UnseekableException
418
	 */
419
	public function readfilePart($path, $from, $to) {
420
		$this->assertPathLength($path);
421
		if (ob_get_level()) {
422
			ob_end_clean();
423
		}
424
		$handle = $this->fopen($path, 'rb');
425
		if ($handle) {
0 ignored issues
show
introduced by
$handle is of type resource, thus it always evaluated to false.
Loading history...
426
			$chunkSize = 524288; // 512 kB chunks
427
			$startReading = true;
428
429
			if ($from !== 0 && $from !== '0' && fseek($handle, $from) !== 0) {
430
				// forward file handle via chunked fread because fseek seem to have failed
431
432
				$end = $from + 1;
433
				while (!feof($handle) && ftell($handle) < $end && ftell($handle) !== $from) {
434
					$len = $from - ftell($handle);
435
					if ($len > $chunkSize) {
436
						$len = $chunkSize;
437
					}
438
					$result = fread($handle, $len);
439
440
					if ($result === false) {
441
						$startReading = false;
442
						break;
443
					}
444
				}
445
			}
446
447
			if ($startReading) {
448
				$end = $to + 1;
449
				while (!feof($handle) && ftell($handle) < $end) {
450
					$len = $end - ftell($handle);
451
					if ($len > $chunkSize) {
452
						$len = $chunkSize;
453
					}
454
					echo fread($handle, $len);
455
					flush();
456
				}
457
				return ftell($handle) - $from;
458
			}
459
460
			throw new \OCP\Files\UnseekableException('fseek error');
461
		}
462
		return false;
463
	}
464
465
	/**
466
	 * @param string $path
467
	 * @return mixed
468
	 */
469
	public function isCreatable($path) {
470
		return $this->basicOperation('isCreatable', $path);
471
	}
472
473
	/**
474
	 * @param string $path
475
	 * @return mixed
476
	 */
477
	public function isReadable($path) {
478
		return $this->basicOperation('isReadable', $path);
479
	}
480
481
	/**
482
	 * @param string $path
483
	 * @return mixed
484
	 */
485
	public function isUpdatable($path) {
486
		return $this->basicOperation('isUpdatable', $path);
487
	}
488
489
	/**
490
	 * @param string $path
491
	 * @return bool|mixed
492
	 */
493
	public function isDeletable($path) {
494
		$absolutePath = $this->getAbsolutePath($path);
495
		$mount = Filesystem::getMountManager()->find($absolutePath);
496
		if ($mount->getInternalPath($absolutePath) === '') {
497
			return $mount instanceof MoveableMount;
498
		}
499
		return $this->basicOperation('isDeletable', $path);
500
	}
501
502
	/**
503
	 * @param string $path
504
	 * @return mixed
505
	 */
506
	public function isSharable($path) {
507
		return $this->basicOperation('isSharable', $path);
508
	}
509
510
	/**
511
	 * @param string $path
512
	 * @return bool|mixed
513
	 */
514
	public function file_exists($path) {
515
		if ($path == '/') {
516
			return true;
517
		}
518
		return $this->basicOperation('file_exists', $path);
519
	}
520
521
	/**
522
	 * @param string $path
523
	 * @return mixed
524
	 */
525
	public function filemtime($path) {
526
		return $this->basicOperation('filemtime', $path);
527
	}
528
529
	/**
530
	 * @param string $path
531
	 * @param int|string $mtime
532
	 */
533
	public function touch($path, $mtime = null): bool {
534
		if (!is_null($mtime) && !is_numeric($mtime)) {
535
			$mtime = strtotime($mtime);
536
		}
537
538
		$hooks = ['touch'];
539
540
		if (!$this->file_exists($path)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->file_exists($path) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
541
			$hooks[] = 'create';
542
			$hooks[] = 'write';
543
		}
544
		try {
545
			$result = $this->basicOperation('touch', $path, $hooks, $mtime);
546
		} catch (\Exception $e) {
547
			$this->logger->info('Error while setting modified time', ['app' => 'core', 'exception' => $e]);
548
			$result = false;
549
		}
550
		if (!$result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
551
			// If create file fails because of permissions on external storage like SMB folders,
552
			// check file exists and return false if not.
553
			if (!$this->file_exists($path)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->file_exists($path) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
554
				return false;
555
			}
556
			if (is_null($mtime)) {
557
				$mtime = time();
558
			}
559
			//if native touch fails, we emulate it by changing the mtime in the cache
560
			$this->putFileInfo($path, ['mtime' => floor($mtime)]);
561
		}
562
		return true;
563
	}
564
565
	/**
566
	 * @param string $path
567
	 * @return string|false
568
	 * @throws LockedException
569
	 */
570
	public function file_get_contents($path) {
571
		return $this->basicOperation('file_get_contents', $path, ['read']);
572
	}
573
574
	protected function emit_file_hooks_pre(bool $exists, string $path, bool &$run): void {
575
		if (!$exists) {
576
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
577
				Filesystem::signal_param_path => $this->getHookPath($path),
578
				Filesystem::signal_param_run => &$run,
579
			]);
580
		} else {
581
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
582
				Filesystem::signal_param_path => $this->getHookPath($path),
583
				Filesystem::signal_param_run => &$run,
584
			]);
585
		}
586
		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
587
			Filesystem::signal_param_path => $this->getHookPath($path),
588
			Filesystem::signal_param_run => &$run,
589
		]);
590
	}
591
592
	protected function emit_file_hooks_post(bool $exists, string $path): void {
593
		if (!$exists) {
594
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
595
				Filesystem::signal_param_path => $this->getHookPath($path),
596
			]);
597
		} else {
598
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
599
				Filesystem::signal_param_path => $this->getHookPath($path),
600
			]);
601
		}
602
		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
603
			Filesystem::signal_param_path => $this->getHookPath($path),
604
		]);
605
	}
606
607
	/**
608
	 * @param string $path
609
	 * @param string|resource $data
610
	 * @return bool|mixed
611
	 * @throws LockedException
612
	 */
613
	public function file_put_contents($path, $data) {
614
		if (is_resource($data)) { //not having to deal with streams in file_put_contents makes life easier
615
			$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
616
			if (Filesystem::isValidPath($path)
617
				&& !Filesystem::isFileBlacklisted($path)
618
			) {
619
				$path = $this->getRelativePath($absolutePath);
620
				if ($path === null) {
621
					throw new InvalidPathException("Path $absolutePath is not in the expected root");
622
				}
623
624
				$this->lockFile($path, ILockingProvider::LOCK_SHARED);
625
626
				$exists = $this->file_exists($path);
627
				$run = true;
628
				if ($this->shouldEmitHooks($path)) {
629
					$this->emit_file_hooks_pre($exists, $path, $run);
630
				}
631
				if (!$run) {
632
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
633
					return false;
634
				}
635
636
				try {
637
					$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
638
				} catch (\Exception $e) {
639
					// Release the shared lock before throwing.
640
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
641
					throw $e;
642
				}
643
644
				/** @var Storage $storage */
645
				[$storage, $internalPath] = $this->resolvePath($path);
646
				$target = $storage->fopen($internalPath, 'w');
647
				if ($target) {
648
					[, $result] = \OC_Helper::streamCopy($data, $target);
649
					fclose($target);
650
					fclose($data);
651
652
					$this->writeUpdate($storage, $internalPath);
653
654
					$this->changeLock($path, ILockingProvider::LOCK_SHARED);
655
656
					if ($this->shouldEmitHooks($path) && $result !== false) {
0 ignored issues
show
introduced by
The condition $result !== false is always false.
Loading history...
657
						$this->emit_file_hooks_post($exists, $path);
658
					}
659
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
660
					return $result;
661
				} else {
662
					$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
663
					return false;
664
				}
665
			} else {
666
				return false;
667
			}
668
		} else {
669
			$hooks = $this->file_exists($path) ? ['update', 'write'] : ['create', 'write'];
670
			return $this->basicOperation('file_put_contents', $path, $hooks, $data);
671
		}
672
	}
673
674
	/**
675
	 * @param string $path
676
	 * @return bool|mixed
677
	 */
678
	public function unlink($path) {
679
		if ($path === '' || $path === '/') {
680
			// do not allow deleting the root
681
			return false;
682
		}
683
		$postFix = (substr($path, -1) === '/') ? '/' : '';
684
		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
685
		$mount = Filesystem::getMountManager()->find($absolutePath . $postFix);
686
		if ($mount->getInternalPath($absolutePath) === '') {
687
			return $this->removeMount($mount, $absolutePath);
688
		}
689
		if ($this->is_dir($path)) {
690
			$result = $this->basicOperation('rmdir', $path, ['delete']);
691
		} else {
692
			$result = $this->basicOperation('unlink', $path, ['delete']);
693
		}
694
		if (!$result && !$this->file_exists($path)) { //clear ghost files from the cache on delete
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
Bug Best Practice introduced by
The expression $this->file_exists($path) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
695
			$storage = $mount->getStorage();
696
			$internalPath = $mount->getInternalPath($absolutePath);
697
			$storage->getUpdater()->remove($internalPath);
698
			return true;
699
		} else {
700
			return $result;
701
		}
702
	}
703
704
	/**
705
	 * @param string $directory
706
	 * @return bool|mixed
707
	 */
708
	public function deleteAll($directory) {
709
		return $this->rmdir($directory);
710
	}
711
712
	/**
713
	 * Rename/move a file or folder from the source path to target path.
714
	 *
715
	 * @param string $source source path
716
	 * @param string $target target path
717
	 *
718
	 * @return bool|mixed
719
	 * @throws LockedException
720
	 */
721
	public function rename($source, $target) {
722
		$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source));
723
		$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
724
		$result = false;
725
		if (
726
			Filesystem::isValidPath($target)
727
			&& Filesystem::isValidPath($source)
728
			&& !Filesystem::isFileBlacklisted($target)
729
		) {
730
			$source = $this->getRelativePath($absolutePath1);
731
			$target = $this->getRelativePath($absolutePath2);
732
			$exists = $this->file_exists($target);
733
734
			if ($source == null || $target == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $target of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
Bug introduced by
It seems like you are loosely comparing $source of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
735
				return false;
736
			}
737
738
			$this->lockFile($source, ILockingProvider::LOCK_SHARED, true);
739
			try {
740
				$this->lockFile($target, ILockingProvider::LOCK_SHARED, true);
741
742
				$run = true;
743
				if ($this->shouldEmitHooks($source) && (Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target))) {
744
					// if it was a rename from a part file to a regular file it was a write and not a rename operation
745
					$this->emit_file_hooks_pre($exists, $target, $run);
746
				} elseif ($this->shouldEmitHooks($source)) {
747
					\OC_Hook::emit(
748
						Filesystem::CLASSNAME, Filesystem::signal_rename,
749
						[
750
							Filesystem::signal_param_oldpath => $this->getHookPath($source),
751
							Filesystem::signal_param_newpath => $this->getHookPath($target),
752
							Filesystem::signal_param_run => &$run
753
						]
754
					);
755
				}
756
				if ($run) {
757
					$this->verifyPath(dirname($target), basename($target));
758
759
					$manager = Filesystem::getMountManager();
760
					$mount1 = $this->getMount($source);
761
					$mount2 = $this->getMount($target);
762
					$storage1 = $mount1->getStorage();
763
					$storage2 = $mount2->getStorage();
764
					$internalPath1 = $mount1->getInternalPath($absolutePath1);
765
					$internalPath2 = $mount2->getInternalPath($absolutePath2);
766
767
					$this->changeLock($source, ILockingProvider::LOCK_EXCLUSIVE, true);
768
					try {
769
						$this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE, true);
770
771
						if ($internalPath1 === '') {
772
							if ($mount1 instanceof MoveableMount) {
773
								$sourceParentMount = $this->getMount(dirname($source));
774
								if ($sourceParentMount === $mount2 && $this->targetIsNotShared($storage2, $internalPath2)) {
775
									/**
776
									 * @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
777
									 */
778
									$sourceMountPoint = $mount1->getMountPoint();
0 ignored issues
show
Bug introduced by
The method getMountPoint() does not exist on OC\Files\Mount\MoveableMount. Since it exists in all sub-types, consider adding an abstract or default implementation to OC\Files\Mount\MoveableMount. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

778
									/** @scrutinizer ignore-call */ 
779
         $sourceMountPoint = $mount1->getMountPoint();
Loading history...
779
									$result = $mount1->moveMount($absolutePath2);
0 ignored issues
show
Bug introduced by
The method moveMount() does not exist on OC\Files\Mount\MountPoint. It seems like you code against a sub-type of said class. However, the method does not exist in OCA\Files_External\Config\ExternalMountPoint or OCA\Files_External\Config\SystemMountPoint. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

779
									/** @scrutinizer ignore-call */ 
780
         $result = $mount1->moveMount($absolutePath2);
Loading history...
780
									$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
781
								} else {
782
									$result = false;
783
								}
784
							} else {
785
								$result = false;
786
							}
787
						// moving a file/folder within the same mount point
788
						} elseif ($storage1 === $storage2) {
789
							if ($storage1) {
790
								$result = $storage1->rename($internalPath1, $internalPath2);
791
							} else {
792
								$result = false;
793
							}
794
						// moving a file/folder between storages (from $storage1 to $storage2)
795
						} else {
796
							$result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
797
						}
798
799
						if ((Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target)) && $result !== false) {
800
							// if it was a rename from a part file to a regular file it was a write and not a rename operation
801
							$this->writeUpdate($storage2, $internalPath2);
802
						} elseif ($result) {
803
							if ($internalPath1 !== '') { // don't do a cache update for moved mounts
804
								$this->renameUpdate($storage1, $storage2, $internalPath1, $internalPath2);
805
							}
806
						}
807
					} catch (\Exception $e) {
808
						throw $e;
809
					} finally {
810
						$this->changeLock($source, ILockingProvider::LOCK_SHARED, true);
811
						$this->changeLock($target, ILockingProvider::LOCK_SHARED, true);
812
					}
813
814
					if ((Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target)) && $result !== false) {
815
						if ($this->shouldEmitHooks()) {
816
							$this->emit_file_hooks_post($exists, $target);
817
						}
818
					} elseif ($result) {
819
						if ($this->shouldEmitHooks($source) && $this->shouldEmitHooks($target)) {
820
							\OC_Hook::emit(
821
								Filesystem::CLASSNAME,
822
								Filesystem::signal_post_rename,
823
								[
824
									Filesystem::signal_param_oldpath => $this->getHookPath($source),
825
									Filesystem::signal_param_newpath => $this->getHookPath($target)
826
								]
827
							);
828
						}
829
					}
830
				}
831
			} catch (\Exception $e) {
832
				throw $e;
833
			} finally {
834
				$this->unlockFile($source, ILockingProvider::LOCK_SHARED, true);
835
				$this->unlockFile($target, ILockingProvider::LOCK_SHARED, true);
836
			}
837
		}
838
		return $result;
839
	}
840
841
	/**
842
	 * Copy a file/folder from the source path to target path
843
	 *
844
	 * @param string $source source path
845
	 * @param string $target target path
846
	 * @param bool $preserveMtime whether to preserve mtime on the copy
847
	 *
848
	 * @return bool|mixed
849
	 */
850
	public function copy($source, $target, $preserveMtime = false) {
851
		$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source));
852
		$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
853
		$result = false;
854
		if (
855
			Filesystem::isValidPath($target)
856
			&& Filesystem::isValidPath($source)
857
			&& !Filesystem::isFileBlacklisted($target)
858
		) {
859
			$source = $this->getRelativePath($absolutePath1);
860
			$target = $this->getRelativePath($absolutePath2);
861
862
			if ($source == null || $target == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $target of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
Bug introduced by
It seems like you are loosely comparing $source of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
863
				return false;
864
			}
865
			$run = true;
866
867
			$this->lockFile($target, ILockingProvider::LOCK_SHARED);
868
			$this->lockFile($source, ILockingProvider::LOCK_SHARED);
869
			$lockTypePath1 = ILockingProvider::LOCK_SHARED;
870
			$lockTypePath2 = ILockingProvider::LOCK_SHARED;
871
872
			try {
873
				$exists = $this->file_exists($target);
874
				if ($this->shouldEmitHooks()) {
875
					\OC_Hook::emit(
876
						Filesystem::CLASSNAME,
877
						Filesystem::signal_copy,
878
						[
879
							Filesystem::signal_param_oldpath => $this->getHookPath($source),
880
							Filesystem::signal_param_newpath => $this->getHookPath($target),
881
							Filesystem::signal_param_run => &$run
882
						]
883
					);
884
					$this->emit_file_hooks_pre($exists, $target, $run);
885
				}
886
				if ($run) {
887
					$mount1 = $this->getMount($source);
888
					$mount2 = $this->getMount($target);
889
					$storage1 = $mount1->getStorage();
890
					$internalPath1 = $mount1->getInternalPath($absolutePath1);
891
					$storage2 = $mount2->getStorage();
892
					$internalPath2 = $mount2->getInternalPath($absolutePath2);
893
894
					$this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE);
895
					$lockTypePath2 = ILockingProvider::LOCK_EXCLUSIVE;
896
897
					if ($mount1->getMountPoint() == $mount2->getMountPoint()) {
898
						if ($storage1) {
899
							$result = $storage1->copy($internalPath1, $internalPath2);
900
						} else {
901
							$result = false;
902
						}
903
					} else {
904
						$result = $storage2->copyFromStorage($storage1, $internalPath1, $internalPath2);
905
					}
906
907
					$this->writeUpdate($storage2, $internalPath2);
908
909
					$this->changeLock($target, ILockingProvider::LOCK_SHARED);
910
					$lockTypePath2 = ILockingProvider::LOCK_SHARED;
911
912
					if ($this->shouldEmitHooks() && $result !== false) {
913
						\OC_Hook::emit(
914
							Filesystem::CLASSNAME,
915
							Filesystem::signal_post_copy,
916
							[
917
								Filesystem::signal_param_oldpath => $this->getHookPath($source),
918
								Filesystem::signal_param_newpath => $this->getHookPath($target)
919
							]
920
						);
921
						$this->emit_file_hooks_post($exists, $target);
922
					}
923
				}
924
			} catch (\Exception $e) {
925
				$this->unlockFile($target, $lockTypePath2);
926
				$this->unlockFile($source, $lockTypePath1);
927
				throw $e;
928
			}
929
930
			$this->unlockFile($target, $lockTypePath2);
931
			$this->unlockFile($source, $lockTypePath1);
932
		}
933
		return $result;
934
	}
935
936
	/**
937
	 * @param string $path
938
	 * @param string $mode 'r' or 'w'
939
	 * @return resource|false
940
	 * @throws LockedException
941
	 */
942
	public function fopen($path, $mode) {
943
		$mode = str_replace('b', '', $mode); // the binary flag is a windows only feature which we do not support
944
		$hooks = [];
945
		switch ($mode) {
946
			case 'r':
947
				$hooks[] = 'read';
948
				break;
949
			case 'r+':
950
			case 'w+':
951
			case 'x+':
952
			case 'a+':
953
				$hooks[] = 'read';
954
				$hooks[] = 'write';
955
				break;
956
			case 'w':
957
			case 'x':
958
			case 'a':
959
				$hooks[] = 'write';
960
				break;
961
			default:
962
				$this->logger->error('invalid mode (' . $mode . ') for ' . $path, ['app' => 'core']);
963
		}
964
965
		if ($mode !== 'r' && $mode !== 'w') {
966
			$this->logger->info('Trying to open a file with a mode other than "r" or "w" can cause severe performance issues with some backends', ['app' => 'core']);
967
		}
968
969
		$handle = $this->basicOperation('fopen', $path, $hooks, $mode);
970
		if (!is_resource($handle) && $mode === 'r') {
971
			// trying to read a file that isn't on disk, check if the cache is out of sync and rescan if needed
972
			$mount = $this->getMount($path);
973
			$internalPath = $mount->getInternalPath($this->getAbsolutePath($path));
974
			$storage = $mount->getStorage();
975
			if ($storage->getCache()->inCache($internalPath) && !$storage->file_exists($path)) {
976
				$this->writeUpdate($storage, $internalPath);
977
			}
978
		}
979
		return $handle;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $handle also could return the type boolean which is incompatible with the documented return type false|resource.
Loading history...
980
	}
981
982
	/**
983
	 * @param string $path
984
	 * @throws InvalidPathException
985
	 */
986
	public function toTmpFile($path): string|false {
987
		$this->assertPathLength($path);
988
		if (Filesystem::isValidPath($path)) {
989
			$source = $this->fopen($path, 'r');
990
			if ($source) {
0 ignored issues
show
introduced by
$source is of type resource, thus it always evaluated to false.
Loading history...
991
				$extension = pathinfo($path, PATHINFO_EXTENSION);
992
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($extension);
993
				file_put_contents($tmpFile, $source);
994
				return $tmpFile;
995
			} else {
996
				return false;
997
			}
998
		} else {
999
			return false;
1000
		}
1001
	}
1002
1003
	/**
1004
	 * @param string $tmpFile
1005
	 * @param string $path
1006
	 * @return bool|mixed
1007
	 * @throws InvalidPathException
1008
	 */
1009
	public function fromTmpFile($tmpFile, $path) {
1010
		$this->assertPathLength($path);
1011
		if (Filesystem::isValidPath($path)) {
1012
			// Get directory that the file is going into
1013
			$filePath = dirname($path);
1014
1015
			// Create the directories if any
1016
			if (!$this->file_exists($filePath)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->file_exists($filePath) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
1017
				$result = $this->createParentDirectories($filePath);
1018
				if ($result === false) {
1019
					return false;
1020
				}
1021
			}
1022
1023
			$source = fopen($tmpFile, 'r');
1024
			if ($source) {
0 ignored issues
show
introduced by
$source is of type resource, thus it always evaluated to false.
Loading history...
1025
				$result = $this->file_put_contents($path, $source);
1026
				/**
1027
				 * $this->file_put_contents() might have already closed
1028
				 * the resource, so we check it, before trying to close it
1029
				 * to avoid messages in the error log.
1030
				 * @psalm-suppress RedundantCondition false-positive
1031
				 */
1032
				if (is_resource($source)) {
1033
					fclose($source);
1034
				}
1035
				unlink($tmpFile);
1036
				return $result;
1037
			} else {
1038
				return false;
1039
			}
1040
		} else {
1041
			return false;
1042
		}
1043
	}
1044
1045
1046
	/**
1047
	 * @param string $path
1048
	 * @return mixed
1049
	 * @throws InvalidPathException
1050
	 */
1051
	public function getMimeType($path) {
1052
		$this->assertPathLength($path);
1053
		return $this->basicOperation('getMimeType', $path);
1054
	}
1055
1056
	/**
1057
	 * @param string $type
1058
	 * @param string $path
1059
	 * @param bool $raw
1060
	 */
1061
	public function hash($type, $path, $raw = false): string|bool {
1062
		$postFix = (substr($path, -1) === '/') ? '/' : '';
1063
		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
1064
		if (Filesystem::isValidPath($path)) {
1065
			$path = $this->getRelativePath($absolutePath);
1066
			if ($path == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $path of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
1067
				return false;
1068
			}
1069
			if ($this->shouldEmitHooks($path)) {
1070
				\OC_Hook::emit(
1071
					Filesystem::CLASSNAME,
1072
					Filesystem::signal_read,
1073
					[Filesystem::signal_param_path => $this->getHookPath($path)]
1074
				);
1075
			}
1076
			/** @var Storage|null $storage */
1077
			[$storage, $internalPath] = Filesystem::resolvePath($absolutePath . $postFix);
1078
			if ($storage) {
0 ignored issues
show
introduced by
$storage is of type OC\Files\Storage\Storage, thus it always evaluated to true.
Loading history...
1079
				return $storage->hash($type, $internalPath, $raw);
1080
			}
1081
		}
1082
		return false;
1083
	}
1084
1085
	/**
1086
	 * @param string $path
1087
	 * @return mixed
1088
	 * @throws InvalidPathException
1089
	 */
1090
	public function free_space($path = '/') {
1091
		$this->assertPathLength($path);
1092
		$result = $this->basicOperation('free_space', $path);
1093
		if ($result === null) {
1094
			throw new InvalidPathException();
1095
		}
1096
		return $result;
1097
	}
1098
1099
	/**
1100
	 * abstraction layer for basic filesystem functions: wrapper for \OC\Files\Storage\Storage
1101
	 *
1102
	 * @param mixed $extraParam (optional)
1103
	 * @return mixed
1104
	 * @throws LockedException
1105
	 *
1106
	 * This method takes requests for basic filesystem functions (e.g. reading & writing
1107
	 * files), processes hooks and proxies, sanitises paths, and finally passes them on to
1108
	 * \OC\Files\Storage\Storage for delegation to a storage backend for execution
1109
	 */
1110
	private function basicOperation(string $operation, string $path, array $hooks = [], $extraParam = null) {
1111
		$postFix = (substr($path, -1) === '/') ? '/' : '';
1112
		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
1113
		if (Filesystem::isValidPath($path)
1114
			&& !Filesystem::isFileBlacklisted($path)
1115
		) {
1116
			$path = $this->getRelativePath($absolutePath);
1117
			if ($path == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $path of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
1118
				return false;
1119
			}
1120
1121
			if (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks)) {
1122
				// always a shared lock during pre-hooks so the hook can read the file
1123
				$this->lockFile($path, ILockingProvider::LOCK_SHARED);
1124
			}
1125
1126
			$run = $this->runHooks($hooks, $path);
1127
			[$storage, $internalPath] = Filesystem::resolvePath($absolutePath . $postFix);
1128
			if ($run && $storage) {
1129
				/** @var Storage $storage */
1130
				if (in_array('write', $hooks) || in_array('delete', $hooks)) {
1131
					try {
1132
						$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
1133
					} catch (LockedException $e) {
1134
						// release the shared lock we acquired before quitting
1135
						$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1136
						throw $e;
1137
					}
1138
				}
1139
				try {
1140
					if (!is_null($extraParam)) {
1141
						$result = $storage->$operation($internalPath, $extraParam);
1142
					} else {
1143
						$result = $storage->$operation($internalPath);
1144
					}
1145
				} catch (\Exception $e) {
1146
					if (in_array('write', $hooks) || in_array('delete', $hooks)) {
1147
						$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
1148
					} elseif (in_array('read', $hooks)) {
1149
						$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1150
					}
1151
					throw $e;
1152
				}
1153
1154
				if ($result !== false && in_array('delete', $hooks)) {
1155
					$this->removeUpdate($storage, $internalPath);
1156
				}
1157
				if ($result !== false && in_array('write', $hooks, true) && $operation !== 'fopen' && $operation !== 'touch') {
1158
					$this->writeUpdate($storage, $internalPath);
1159
				}
1160
				if ($result !== false && in_array('touch', $hooks)) {
1161
					$this->writeUpdate($storage, $internalPath, $extraParam);
1162
				}
1163
1164
				if ((in_array('write', $hooks) || in_array('delete', $hooks)) && ($operation !== 'fopen' || $result === false)) {
1165
					$this->changeLock($path, ILockingProvider::LOCK_SHARED);
1166
				}
1167
1168
				$unlockLater = false;
1169
				if ($this->lockingEnabled && $operation === 'fopen' && is_resource($result)) {
1170
					$unlockLater = true;
1171
					// make sure our unlocking callback will still be called if connection is aborted
1172
					ignore_user_abort(true);
1173
					$result = CallbackWrapper::wrap($result, null, null, function () use ($hooks, $path) {
1174
						if (in_array('write', $hooks)) {
1175
							$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
1176
						} elseif (in_array('read', $hooks)) {
1177
							$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1178
						}
1179
					});
1180
				}
1181
1182
				if ($this->shouldEmitHooks($path) && $result !== false) {
1183
					if ($operation != 'fopen') { //no post hooks for fopen, the file stream is still open
1184
						$this->runHooks($hooks, $path, true);
1185
					}
1186
				}
1187
1188
				if (!$unlockLater
1189
					&& (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks))
1190
				) {
1191
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1192
				}
1193
				return $result;
1194
			} else {
1195
				$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1196
			}
1197
		}
1198
		return null;
1199
	}
1200
1201
	/**
1202
	 * get the path relative to the default root for hook usage
1203
	 *
1204
	 * @param string $path
1205
	 * @return ?string
1206
	 */
1207
	private function getHookPath($path): ?string {
1208
		$view = Filesystem::getView();
1209
		if (!$view) {
1210
			return $path;
1211
		}
1212
		return $view->getRelativePath($this->getAbsolutePath($path));
1213
	}
1214
1215
	private function shouldEmitHooks(string $path = ''): bool {
1216
		if ($path && Cache\Scanner::isPartialFile($path)) {
1217
			return false;
1218
		}
1219
		if (!Filesystem::$loaded) {
1220
			return false;
1221
		}
1222
		$defaultRoot = Filesystem::getRoot();
1223
		if ($defaultRoot === null) {
1224
			return false;
1225
		}
1226
		if ($this->fakeRoot === $defaultRoot) {
1227
			return true;
1228
		}
1229
		$fullPath = $this->getAbsolutePath($path);
1230
1231
		if ($fullPath === $defaultRoot) {
1232
			return true;
1233
		}
1234
1235
		return (strlen($fullPath) > strlen($defaultRoot)) && (substr($fullPath, 0, strlen($defaultRoot) + 1) === $defaultRoot . '/');
1236
	}
1237
1238
	/**
1239
	 * @param string[] $hooks
1240
	 * @param string $path
1241
	 * @param bool $post
1242
	 * @return bool
1243
	 */
1244
	private function runHooks($hooks, $path, $post = false) {
1245
		$relativePath = $path;
1246
		$path = $this->getHookPath($path);
1247
		$prefix = $post ? 'post_' : '';
1248
		$run = true;
1249
		if ($this->shouldEmitHooks($relativePath)) {
1250
			foreach ($hooks as $hook) {
1251
				if ($hook != 'read') {
1252
					\OC_Hook::emit(
1253
						Filesystem::CLASSNAME,
1254
						$prefix . $hook,
1255
						[
1256
							Filesystem::signal_param_run => &$run,
1257
							Filesystem::signal_param_path => $path
1258
						]
1259
					);
1260
				} elseif (!$post) {
1261
					\OC_Hook::emit(
1262
						Filesystem::CLASSNAME,
1263
						$prefix . $hook,
1264
						[
1265
							Filesystem::signal_param_path => $path
1266
						]
1267
					);
1268
				}
1269
			}
1270
		}
1271
		return $run;
1272
	}
1273
1274
	/**
1275
	 * check if a file or folder has been updated since $time
1276
	 *
1277
	 * @param string $path
1278
	 * @param int $time
1279
	 * @return bool
1280
	 */
1281
	public function hasUpdated($path, $time) {
1282
		return $this->basicOperation('hasUpdated', $path, [], $time);
1283
	}
1284
1285
	/**
1286
	 * @param string $ownerId
1287
	 * @return IUser
1288
	 */
1289
	private function getUserObjectForOwner(string $ownerId) {
1290
		return new LazyUser($ownerId, $this->userManager);
1291
	}
1292
1293
	/**
1294
	 * Get file info from cache
1295
	 *
1296
	 * If the file is not in cached it will be scanned
1297
	 * If the file has changed on storage the cache will be updated
1298
	 *
1299
	 * @param Storage $storage
1300
	 * @param string $internalPath
1301
	 * @param string $relativePath
1302
	 * @return ICacheEntry|bool
1303
	 */
1304
	private function getCacheEntry($storage, $internalPath, $relativePath) {
1305
		$cache = $storage->getCache($internalPath);
1306
		$data = $cache->get($internalPath);
1307
		$watcher = $storage->getWatcher($internalPath);
1308
1309
		try {
1310
			// if the file is not in the cache or needs to be updated, trigger the scanner and reload the data
1311
			if (!$data || (isset($data['size']) && $data['size'] === -1)) {
1312
				if (!$storage->file_exists($internalPath)) {
1313
					return false;
1314
				}
1315
				// don't need to get a lock here since the scanner does it's own locking
1316
				$scanner = $storage->getScanner($internalPath);
1317
				$scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW);
1318
				$data = $cache->get($internalPath);
1319
			} elseif (!Cache\Scanner::isPartialFile($internalPath) && $watcher->needsUpdate($internalPath, $data)) {
1320
				$this->lockFile($relativePath, ILockingProvider::LOCK_SHARED);
1321
				$watcher->update($internalPath, $data);
1322
				$storage->getPropagator()->propagateChange($internalPath, time());
1323
				$data = $cache->get($internalPath);
1324
				$this->unlockFile($relativePath, ILockingProvider::LOCK_SHARED);
1325
			}
1326
		} catch (LockedException $e) {
1327
			// if the file is locked we just use the old cache info
1328
		}
1329
1330
		return $data;
1331
	}
1332
1333
	/**
1334
	 * get the filesystem info
1335
	 *
1336
	 * @param string $path
1337
	 * @param bool|string $includeMountPoints true to add mountpoint sizes,
1338
	 * 'ext' to add only ext storage mount point sizes. Defaults to true.
1339
	 * @return \OC\Files\FileInfo|false False if file does not exist
1340
	 */
1341
	public function getFileInfo($path, $includeMountPoints = true) {
1342
		$this->assertPathLength($path);
1343
		if (!Filesystem::isValidPath($path)) {
1344
			return false;
1345
		}
1346
		if (Cache\Scanner::isPartialFile($path)) {
1347
			return $this->getPartFileInfo($path);
1348
		}
1349
		$relativePath = $path;
1350
		$path = Filesystem::normalizePath($this->fakeRoot . '/' . $path);
1351
1352
		$mount = Filesystem::getMountManager()->find($path);
1353
		$storage = $mount->getStorage();
1354
		$internalPath = $mount->getInternalPath($path);
1355
		if ($storage) {
1356
			$data = $this->getCacheEntry($storage, $internalPath, $relativePath);
1357
1358
			if (!$data instanceof ICacheEntry) {
1359
				return false;
1360
			}
1361
1362
			if ($mount instanceof MoveableMount && $internalPath === '') {
1363
				$data['permissions'] |= \OCP\Constants::PERMISSION_DELETE;
1364
			}
1365
			$ownerId = $storage->getOwner($internalPath);
1366
			$owner = null;
1367
			if ($ownerId !== null && $ownerId !== false) {
1368
				// ownerId might be null if files are accessed with an access token without file system access
1369
				$owner = $this->getUserObjectForOwner($ownerId);
1370
			}
1371
			$info = new FileInfo($path, $storage, $internalPath, $data, $mount, $owner);
1372
1373
			if (isset($data['fileid'])) {
1374
				if ($includeMountPoints && $data['mimetype'] === 'httpd/unix-directory') {
1375
					//add the sizes of other mount points to the folder
1376
					$extOnly = ($includeMountPoints === 'ext');
1377
					$this->addSubMounts($info, $extOnly);
1378
				}
1379
			}
1380
1381
			return $info;
1382
		} else {
1383
			$this->logger->warning('Storage not valid for mountpoint: ' . $mount->getMountPoint(), ['app' => 'core']);
1384
		}
1385
1386
		return false;
1387
	}
1388
1389
	/**
1390
	 * Extend a FileInfo that was previously requested with `$includeMountPoints = false` to include the sub mounts
1391
	 */
1392
	public function addSubMounts(FileInfo $info, $extOnly = false): void {
1393
		$mounts = Filesystem::getMountManager()->findIn($info->getPath());
1394
		$info->setSubMounts(array_filter($mounts, function (IMountPoint $mount) use ($extOnly) {
1395
			$subStorage = $mount->getStorage();
1396
			return !($extOnly && $subStorage instanceof \OCA\Files_Sharing\SharedStorage);
1397
		}));
1398
	}
1399
1400
	/**
1401
	 * get the content of a directory
1402
	 *
1403
	 * @param string $directory path under datadirectory
1404
	 * @param string $mimetype_filter limit returned content to this mimetype or mimepart
1405
	 * @return FileInfo[]
1406
	 */
1407
	public function getDirectoryContent($directory, $mimetype_filter = '', \OCP\Files\FileInfo $directoryInfo = null) {
1408
		$this->assertPathLength($directory);
1409
		if (!Filesystem::isValidPath($directory)) {
1410
			return [];
1411
		}
1412
1413
		$path = $this->getAbsolutePath($directory);
1414
		$path = Filesystem::normalizePath($path);
1415
		$mount = $this->getMount($directory);
1416
		$storage = $mount->getStorage();
1417
		$internalPath = $mount->getInternalPath($path);
1418
		if (!$storage) {
1419
			return [];
1420
		}
1421
1422
		$cache = $storage->getCache($internalPath);
1423
		$user = \OC_User::getUser();
1424
1425
		if (!$directoryInfo) {
1426
			$data = $this->getCacheEntry($storage, $internalPath, $directory);
1427
			if (!$data instanceof ICacheEntry || !isset($data['fileid'])) {
1428
				return [];
1429
			}
1430
		} else {
1431
			$data = $directoryInfo;
1432
		}
1433
1434
		if (!($data->getPermissions() & Constants::PERMISSION_READ)) {
1435
			return [];
1436
		}
1437
1438
		$folderId = $data->getId();
1439
		$contents = $cache->getFolderContentsById($folderId); //TODO: mimetype_filter
1440
1441
		$sharingDisabled = \OCP\Util::isSharingDisabledForUser();
1442
1443
		$fileNames = array_map(function (ICacheEntry $content) {
1444
			return $content->getName();
1445
		}, $contents);
1446
		/**
1447
		 * @var \OC\Files\FileInfo[] $fileInfos
1448
		 */
1449
		$fileInfos = array_map(function (ICacheEntry $content) use ($path, $storage, $mount, $sharingDisabled) {
1450
			if ($sharingDisabled) {
1451
				$content['permissions'] = $content['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
1452
			}
1453
			$owner = $this->getUserObjectForOwner($storage->getOwner($content['path']));
1454
			return new FileInfo($path . '/' . $content['name'], $storage, $content['path'], $content, $mount, $owner);
1455
		}, $contents);
1456
		$files = array_combine($fileNames, $fileInfos);
1457
1458
		//add a folder for any mountpoint in this directory and add the sizes of other mountpoints to the folders
1459
		$mounts = Filesystem::getMountManager()->findIn($path);
1460
		$dirLength = strlen($path);
1461
		foreach ($mounts as $mount) {
1462
			$mountPoint = $mount->getMountPoint();
1463
			$subStorage = $mount->getStorage();
1464
			if ($subStorage) {
1465
				$subCache = $subStorage->getCache('');
1466
1467
				$rootEntry = $subCache->get('');
1468
				if (!$rootEntry) {
1469
					$subScanner = $subStorage->getScanner();
1470
					try {
1471
						$subScanner->scanFile('');
1472
					} catch (\OCP\Files\StorageNotAvailableException $e) {
1473
						continue;
1474
					} catch (\OCP\Files\StorageInvalidException $e) {
1475
						continue;
1476
					} catch (\Exception $e) {
1477
						// sometimes when the storage is not available it can be any exception
1478
						$this->logger->error('Exception while scanning storage "' . $subStorage->getId() . '"', [
1479
							'exception' => $e,
1480
							'app' => 'core',
1481
						]);
1482
						continue;
1483
					}
1484
					$rootEntry = $subCache->get('');
1485
				}
1486
1487
				if ($rootEntry && ($rootEntry->getPermissions() & Constants::PERMISSION_READ)) {
1488
					$relativePath = trim(substr($mountPoint, $dirLength), '/');
1489
					if ($pos = strpos($relativePath, '/')) {
1490
						//mountpoint inside subfolder add size to the correct folder
1491
						$entryName = substr($relativePath, 0, $pos);
1492
						if (isset($files[$entryName])) {
1493
							$files[$entryName]->addSubEntry($rootEntry, $mountPoint);
1494
						}
1495
					} else { //mountpoint in this folder, add an entry for it
1496
						$rootEntry['name'] = $relativePath;
1497
						$rootEntry['type'] = $rootEntry['mimetype'] === 'httpd/unix-directory' ? 'dir' : 'file';
1498
						$permissions = $rootEntry['permissions'];
1499
						// do not allow renaming/deleting the mount point if they are not shared files/folders
1500
						// for shared files/folders we use the permissions given by the owner
1501
						if ($mount instanceof MoveableMount) {
1502
							$rootEntry['permissions'] = $permissions | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE;
1503
						} else {
1504
							$rootEntry['permissions'] = $permissions & (\OCP\Constants::PERMISSION_ALL - (\OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE));
1505
						}
1506
1507
						$rootEntry['path'] = substr(Filesystem::normalizePath($path . '/' . $rootEntry['name']), strlen($user) + 2); // full path without /$user/
1508
1509
						// if sharing was disabled for the user we remove the share permissions
1510
						if (\OCP\Util::isSharingDisabledForUser()) {
1511
							$rootEntry['permissions'] = $rootEntry['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
1512
						}
1513
1514
						$owner = $this->getUserObjectForOwner($subStorage->getOwner(''));
1515
						$files[$rootEntry->getName()] = new FileInfo($path . '/' . $rootEntry['name'], $subStorage, '', $rootEntry, $mount, $owner);
1516
					}
1517
				}
1518
			}
1519
		}
1520
1521
		if ($mimetype_filter) {
1522
			$files = array_filter($files, function (FileInfo $file) use ($mimetype_filter) {
1523
				if (strpos($mimetype_filter, '/')) {
1524
					return $file->getMimetype() === $mimetype_filter;
1525
				} else {
1526
					return $file->getMimePart() === $mimetype_filter;
1527
				}
1528
			});
1529
		}
1530
1531
		return array_values($files);
1532
	}
1533
1534
	/**
1535
	 * change file metadata
1536
	 *
1537
	 * @param string $path
1538
	 * @param array|\OCP\Files\FileInfo $data
1539
	 * @return int
1540
	 *
1541
	 * returns the fileid of the updated file
1542
	 */
1543
	public function putFileInfo($path, $data) {
1544
		$this->assertPathLength($path);
1545
		if ($data instanceof FileInfo) {
1546
			$data = $data->getData();
1547
		}
1548
		$path = Filesystem::normalizePath($this->fakeRoot . '/' . $path);
1549
		/**
1550
		 * @var Storage $storage
1551
		 * @var string $internalPath
1552
		 */
1553
		[$storage, $internalPath] = Filesystem::resolvePath($path);
1554
		if ($storage) {
0 ignored issues
show
introduced by
$storage is of type OC\Files\Storage\Storage, thus it always evaluated to true.
Loading history...
1555
			$cache = $storage->getCache($path);
1556
1557
			if (!$cache->inCache($internalPath)) {
1558
				$scanner = $storage->getScanner($internalPath);
1559
				$scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW);
1560
			}
1561
1562
			return $cache->put($internalPath, $data);
1563
		} else {
1564
			return -1;
1565
		}
1566
	}
1567
1568
	/**
1569
	 * search for files with the name matching $query
1570
	 *
1571
	 * @param string $query
1572
	 * @return FileInfo[]
1573
	 */
1574
	public function search($query) {
1575
		return $this->searchCommon('search', ['%' . $query . '%']);
1576
	}
1577
1578
	/**
1579
	 * search for files with the name matching $query
1580
	 *
1581
	 * @param string $query
1582
	 * @return FileInfo[]
1583
	 */
1584
	public function searchRaw($query) {
1585
		return $this->searchCommon('search', [$query]);
1586
	}
1587
1588
	/**
1589
	 * search for files by mimetype
1590
	 *
1591
	 * @param string $mimetype
1592
	 * @return FileInfo[]
1593
	 */
1594
	public function searchByMime($mimetype) {
1595
		return $this->searchCommon('searchByMime', [$mimetype]);
1596
	}
1597
1598
	/**
1599
	 * search for files by tag
1600
	 *
1601
	 * @param string|int $tag name or tag id
1602
	 * @param string $userId owner of the tags
1603
	 * @return FileInfo[]
1604
	 */
1605
	public function searchByTag($tag, $userId) {
1606
		return $this->searchCommon('searchByTag', [$tag, $userId]);
1607
	}
1608
1609
	/**
1610
	 * @param string $method cache method
1611
	 * @param array $args
1612
	 * @return FileInfo[]
1613
	 */
1614
	private function searchCommon($method, $args) {
1615
		$files = [];
1616
		$rootLength = strlen($this->fakeRoot);
1617
1618
		$mount = $this->getMount('');
1619
		$mountPoint = $mount->getMountPoint();
1620
		$storage = $mount->getStorage();
1621
		$userManager = \OC::$server->getUserManager();
1622
		if ($storage) {
1623
			$cache = $storage->getCache('');
1624
1625
			$results = call_user_func_array([$cache, $method], $args);
1626
			foreach ($results as $result) {
1627
				if (substr($mountPoint . $result['path'], 0, $rootLength + 1) === $this->fakeRoot . '/') {
1628
					$internalPath = $result['path'];
1629
					$path = $mountPoint . $result['path'];
1630
					$result['path'] = substr($mountPoint . $result['path'], $rootLength);
1631
					$owner = $userManager->get($storage->getOwner($internalPath));
1632
					$files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner);
1633
				}
1634
			}
1635
1636
			$mounts = Filesystem::getMountManager()->findIn($this->fakeRoot);
1637
			foreach ($mounts as $mount) {
1638
				$mountPoint = $mount->getMountPoint();
1639
				$storage = $mount->getStorage();
1640
				if ($storage) {
1641
					$cache = $storage->getCache('');
1642
1643
					$relativeMountPoint = substr($mountPoint, $rootLength);
1644
					$results = call_user_func_array([$cache, $method], $args);
1645
					if ($results) {
1646
						foreach ($results as $result) {
1647
							$internalPath = $result['path'];
1648
							$result['path'] = rtrim($relativeMountPoint . $result['path'], '/');
1649
							$path = rtrim($mountPoint . $internalPath, '/');
1650
							$owner = $userManager->get($storage->getOwner($internalPath));
1651
							$files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner);
1652
						}
1653
					}
1654
				}
1655
			}
1656
		}
1657
		return $files;
1658
	}
1659
1660
	/**
1661
	 * Get the owner for a file or folder
1662
	 *
1663
	 * @param string $path
1664
	 * @return string the user id of the owner
1665
	 * @throws NotFoundException
1666
	 */
1667
	public function getOwner($path) {
1668
		$info = $this->getFileInfo($path);
1669
		if (!$info) {
1670
			throw new NotFoundException($path . ' not found while trying to get owner');
1671
		}
1672
1673
		if ($info->getOwner() === null) {
1674
			throw new NotFoundException($path . ' has no owner');
1675
		}
1676
1677
		return $info->getOwner()->getUID();
1678
	}
1679
1680
	/**
1681
	 * get the ETag for a file or folder
1682
	 *
1683
	 * @param string $path
1684
	 * @return string|false
1685
	 */
1686
	public function getETag($path) {
1687
		[$storage, $internalPath] = $this->resolvePath($path);
1688
		if ($storage) {
1689
			return $storage->getETag($internalPath);
1690
		} else {
1691
			return false;
1692
		}
1693
	}
1694
1695
	/**
1696
	 * Get the path of a file by id, relative to the view
1697
	 *
1698
	 * Note that the resulting path is not guaranteed to be unique for the id, multiple paths can point to the same file
1699
	 *
1700
	 * @param int $id
1701
	 * @param int|null $storageId
1702
	 * @return string
1703
	 * @throws NotFoundException
1704
	 */
1705
	public function getPath($id, int $storageId = null) {
1706
		$id = (int)$id;
1707
		$manager = Filesystem::getMountManager();
1708
		$mounts = $manager->findIn($this->fakeRoot);
1709
		$mounts[] = $manager->find($this->fakeRoot);
1710
		$mounts = array_filter($mounts);
1711
		// reverse the array, so we start with the storage this view is in
1712
		// which is the most likely to contain the file we're looking for
1713
		$mounts = array_reverse($mounts);
1714
1715
		// put non-shared mounts in front of the shared mount
1716
		// this prevents unneeded recursion into shares
1717
		usort($mounts, function (IMountPoint $a, IMountPoint $b) {
1718
			return $a instanceof SharedMount && (!$b instanceof SharedMount) ? 1 : -1;
1719
		});
1720
1721
		if (!is_null($storageId)) {
1722
			$mounts = array_filter($mounts, function (IMountPoint $mount) use ($storageId) {
1723
				return $mount->getNumericStorageId() === $storageId;
1724
			});
1725
		}
1726
1727
		foreach ($mounts as $mount) {
1728
			/**
1729
			 * @var \OC\Files\Mount\MountPoint $mount
1730
			 */
1731
			if ($mount->getStorage()) {
1732
				$cache = $mount->getStorage()->getCache();
1733
				$internalPath = $cache->getPathById($id);
1734
				if (is_string($internalPath)) {
1735
					$fullPath = $mount->getMountPoint() . $internalPath;
1736
					if (!is_null($path = $this->getRelativePath($fullPath))) {
1737
						return $path;
1738
					}
1739
				}
1740
			}
1741
		}
1742
		throw new NotFoundException(sprintf('File with id "%s" has not been found.', $id));
1743
	}
1744
1745
	/**
1746
	 * @param string $path
1747
	 * @throws InvalidPathException
1748
	 */
1749
	private function assertPathLength($path): void {
1750
		$maxLen = min(PHP_MAXPATHLEN, 4000);
1751
		// Check for the string length - performed using isset() instead of strlen()
1752
		// because isset() is about 5x-40x faster.
1753
		if (isset($path[$maxLen])) {
1754
			$pathLen = strlen($path);
1755
			throw new InvalidPathException("Path length($pathLen) exceeds max path length($maxLen): $path");
1756
		}
1757
	}
1758
1759
	/**
1760
	 * check if it is allowed to move a mount point to a given target.
1761
	 * It is not allowed to move a mount point into a different mount point or
1762
	 * into an already shared folder
1763
	 */
1764
	private function targetIsNotShared(IStorage $targetStorage, string $targetInternalPath): bool {
1765
		// note: cannot use the view because the target is already locked
1766
		$fileId = $targetStorage->getCache()->getId($targetInternalPath);
1767
		if ($fileId === -1) {
1768
			// target might not exist, need to check parent instead
1769
			$fileId = $targetStorage->getCache()->getId(dirname($targetInternalPath));
1770
		}
1771
1772
		// check if any of the parents were shared by the current owner (include collections)
1773
		$shares = Share::getItemShared(
1774
			'folder',
1775
			(string)$fileId,
1776
			\OC\Share\Constants::FORMAT_NONE,
1777
			null,
1778
			true
1779
		);
1780
1781
		if (count($shares) > 0) {
1782
			$this->logger->debug(
1783
				'It is not allowed to move one mount point into a shared folder',
1784
				['app' => 'files']);
1785
			return false;
1786
		}
1787
1788
		return true;
1789
	}
1790
1791
	/**
1792
	 * Get a fileinfo object for files that are ignored in the cache (part files)
1793
	 */
1794
	private function getPartFileInfo(string $path): \OC\Files\FileInfo {
1795
		$mount = $this->getMount($path);
1796
		$storage = $mount->getStorage();
1797
		$internalPath = $mount->getInternalPath($this->getAbsolutePath($path));
1798
		$owner = \OC::$server->getUserManager()->get($storage->getOwner($internalPath));
1799
		return new FileInfo(
1800
			$this->getAbsolutePath($path),
1801
			$storage,
1802
			$internalPath,
1803
			[
1804
				'fileid' => null,
1805
				'mimetype' => $storage->getMimeType($internalPath),
1806
				'name' => basename($path),
1807
				'etag' => null,
1808
				'size' => $storage->filesize($internalPath),
1809
				'mtime' => $storage->filemtime($internalPath),
1810
				'encrypted' => false,
1811
				'permissions' => \OCP\Constants::PERMISSION_ALL
1812
			],
1813
			$mount,
1814
			$owner
1815
		);
1816
	}
1817
1818
	/**
1819
	 * @param string $path
1820
	 * @param string $fileName
1821
	 * @throws InvalidPathException
1822
	 */
1823
	public function verifyPath($path, $fileName): void {
1824
		try {
1825
			/** @type \OCP\Files\Storage $storage */
1826
			[$storage, $internalPath] = $this->resolvePath($path);
1827
			$storage->verifyPath($internalPath, $fileName);
1828
		} catch (ReservedWordException $ex) {
1829
			$l = \OC::$server->getL10N('lib');
1830
			throw new InvalidPathException($l->t('File name is a reserved word'));
1831
		} catch (InvalidCharacterInPathException $ex) {
1832
			$l = \OC::$server->getL10N('lib');
1833
			throw new InvalidPathException($l->t('File name contains at least one invalid character'));
1834
		} catch (FileNameTooLongException $ex) {
1835
			$l = \OC::$server->getL10N('lib');
1836
			throw new InvalidPathException($l->t('File name is too long'));
1837
		} catch (InvalidDirectoryException $ex) {
1838
			$l = \OC::$server->getL10N('lib');
1839
			throw new InvalidPathException($l->t('Dot files are not allowed'));
1840
		} catch (EmptyFileNameException $ex) {
1841
			$l = \OC::$server->getL10N('lib');
1842
			throw new InvalidPathException($l->t('Empty filename is not allowed'));
1843
		}
1844
	}
1845
1846
	/**
1847
	 * get all parent folders of $path
1848
	 *
1849
	 * @param string $path
1850
	 * @return string[]
1851
	 */
1852
	private function getParents($path) {
1853
		$path = trim($path, '/');
1854
		if (!$path) {
1855
			return [];
1856
		}
1857
1858
		$parts = explode('/', $path);
1859
1860
		// remove the single file
1861
		array_pop($parts);
1862
		$result = ['/'];
1863
		$resultPath = '';
1864
		foreach ($parts as $part) {
1865
			if ($part) {
1866
				$resultPath .= '/' . $part;
1867
				$result[] = $resultPath;
1868
			}
1869
		}
1870
		return $result;
1871
	}
1872
1873
	/**
1874
	 * Returns the mount point for which to lock
1875
	 *
1876
	 * @param string $absolutePath absolute path
1877
	 * @param bool $useParentMount true to return parent mount instead of whatever
1878
	 * is mounted directly on the given path, false otherwise
1879
	 * @return IMountPoint mount point for which to apply locks
1880
	 */
1881
	private function getMountForLock(string $absolutePath, bool $useParentMount = false): IMountPoint {
1882
		$mount = Filesystem::getMountManager()->find($absolutePath);
1883
1884
		if ($useParentMount) {
1885
			// find out if something is mounted directly on the path
1886
			$internalPath = $mount->getInternalPath($absolutePath);
1887
			if ($internalPath === '') {
1888
				// resolve the parent mount instead
1889
				$mount = Filesystem::getMountManager()->find(dirname($absolutePath));
1890
			}
1891
		}
1892
1893
		return $mount;
1894
	}
1895
1896
	/**
1897
	 * Lock the given path
1898
	 *
1899
	 * @param string $path the path of the file to lock, relative to the view
1900
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
1901
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
1902
	 *
1903
	 * @return bool False if the path is excluded from locking, true otherwise
1904
	 * @throws LockedException if the path is already locked
1905
	 */
1906
	private function lockPath($path, $type, $lockMountPoint = false) {
1907
		$absolutePath = $this->getAbsolutePath($path);
1908
		$absolutePath = Filesystem::normalizePath($absolutePath);
1909
		if (!$this->shouldLockFile($absolutePath)) {
1910
			return false;
1911
		}
1912
1913
		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
1914
		try {
1915
			$storage = $mount->getStorage();
1916
			if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
1917
				$storage->acquireLock(
0 ignored issues
show
Bug introduced by
The method acquireLock() does not exist on OCP\Files\Storage\IStorage. It seems like you code against a sub-type of said class. However, the method does not exist in OCP\Files\Storage\IDisableEncryptionStorage or OCA\Files_Sharing\ISharedStorage or OCP\Files\IHomeStorage or OCP\Files\Storage\IReliableEtagStorage or OCP\Files\Storage\IWriteStreamStorage or OCP\Files\Storage\IChunkedFileWrite. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1917
				$storage->/** @scrutinizer ignore-call */ 
1918
              acquireLock(
Loading history...
1918
					$mount->getInternalPath($absolutePath),
1919
					$type,
1920
					$this->lockingProvider
1921
				);
1922
			}
1923
		} catch (LockedException $e) {
1924
			// rethrow with the a human-readable path
1925
			throw new LockedException(
1926
				$this->getPathRelativeToFiles($absolutePath),
1927
				$e,
1928
				$e->getExistingLock()
1929
			);
1930
		}
1931
1932
		return true;
1933
	}
1934
1935
	/**
1936
	 * Change the lock type
1937
	 *
1938
	 * @param string $path the path of the file to lock, relative to the view
1939
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
1940
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
1941
	 *
1942
	 * @return bool False if the path is excluded from locking, true otherwise
1943
	 * @throws LockedException if the path is already locked
1944
	 */
1945
	public function changeLock($path, $type, $lockMountPoint = false) {
1946
		$path = Filesystem::normalizePath($path);
1947
		$absolutePath = $this->getAbsolutePath($path);
1948
		$absolutePath = Filesystem::normalizePath($absolutePath);
1949
		if (!$this->shouldLockFile($absolutePath)) {
1950
			return false;
1951
		}
1952
1953
		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
1954
		try {
1955
			$storage = $mount->getStorage();
1956
			if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
1957
				$storage->changeLock(
0 ignored issues
show
Bug introduced by
The method changeLock() does not exist on OCP\Files\Storage\IStorage. It seems like you code against a sub-type of said class. However, the method does not exist in OCP\Files\Storage\IDisableEncryptionStorage or OCA\Files_Sharing\ISharedStorage or OCP\Files\IHomeStorage or OCP\Files\Storage\IReliableEtagStorage or OCP\Files\Storage\IWriteStreamStorage or OCP\Files\Storage\IChunkedFileWrite. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1957
				$storage->/** @scrutinizer ignore-call */ 
1958
              changeLock(
Loading history...
1958
					$mount->getInternalPath($absolutePath),
1959
					$type,
1960
					$this->lockingProvider
1961
				);
1962
			}
1963
		} catch (LockedException $e) {
1964
			try {
1965
				// rethrow with the a human-readable path
1966
				throw new LockedException(
1967
					$this->getPathRelativeToFiles($absolutePath),
1968
					$e,
1969
					$e->getExistingLock()
1970
				);
1971
			} catch (\InvalidArgumentException $ex) {
1972
				throw new LockedException(
1973
					$absolutePath,
1974
					$ex,
1975
					$e->getExistingLock()
1976
				);
1977
			}
1978
		}
1979
1980
		return true;
1981
	}
1982
1983
	/**
1984
	 * Unlock the given path
1985
	 *
1986
	 * @param string $path the path of the file to unlock, relative to the view
1987
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
1988
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
1989
	 *
1990
	 * @return bool False if the path is excluded from locking, true otherwise
1991
	 * @throws LockedException
1992
	 */
1993
	private function unlockPath($path, $type, $lockMountPoint = false) {
1994
		$absolutePath = $this->getAbsolutePath($path);
1995
		$absolutePath = Filesystem::normalizePath($absolutePath);
1996
		if (!$this->shouldLockFile($absolutePath)) {
1997
			return false;
1998
		}
1999
2000
		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
2001
		$storage = $mount->getStorage();
2002
		if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
2003
			$storage->releaseLock(
0 ignored issues
show
Bug introduced by
The method releaseLock() does not exist on OCP\Files\Storage\IStorage. It seems like you code against a sub-type of said class. However, the method does not exist in OCP\Files\Storage\IDisableEncryptionStorage or OCA\Files_Sharing\ISharedStorage or OCP\Files\IHomeStorage or OCP\Files\Storage\IReliableEtagStorage or OCP\Files\Storage\IWriteStreamStorage or OCP\Files\Storage\IChunkedFileWrite. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2003
			$storage->/** @scrutinizer ignore-call */ 
2004
             releaseLock(
Loading history...
2004
				$mount->getInternalPath($absolutePath),
2005
				$type,
2006
				$this->lockingProvider
2007
			);
2008
		}
2009
2010
		return true;
2011
	}
2012
2013
	/**
2014
	 * Lock a path and all its parents up to the root of the view
2015
	 *
2016
	 * @param string $path the path of the file to lock relative to the view
2017
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2018
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2019
	 *
2020
	 * @return bool False if the path is excluded from locking, true otherwise
2021
	 * @throws LockedException
2022
	 */
2023
	public function lockFile($path, $type, $lockMountPoint = false) {
2024
		$absolutePath = $this->getAbsolutePath($path);
2025
		$absolutePath = Filesystem::normalizePath($absolutePath);
2026
		if (!$this->shouldLockFile($absolutePath)) {
2027
			return false;
2028
		}
2029
2030
		$this->lockPath($path, $type, $lockMountPoint);
2031
2032
		$parents = $this->getParents($path);
2033
		foreach ($parents as $parent) {
2034
			$this->lockPath($parent, ILockingProvider::LOCK_SHARED);
2035
		}
2036
2037
		return true;
2038
	}
2039
2040
	/**
2041
	 * Unlock a path and all its parents up to the root of the view
2042
	 *
2043
	 * @param string $path the path of the file to lock relative to the view
2044
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2045
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2046
	 *
2047
	 * @return bool False if the path is excluded from locking, true otherwise
2048
	 * @throws LockedException
2049
	 */
2050
	public function unlockFile($path, $type, $lockMountPoint = false) {
2051
		$absolutePath = $this->getAbsolutePath($path);
2052
		$absolutePath = Filesystem::normalizePath($absolutePath);
2053
		if (!$this->shouldLockFile($absolutePath)) {
2054
			return false;
2055
		}
2056
2057
		$this->unlockPath($path, $type, $lockMountPoint);
2058
2059
		$parents = $this->getParents($path);
2060
		foreach ($parents as $parent) {
2061
			$this->unlockPath($parent, ILockingProvider::LOCK_SHARED);
2062
		}
2063
2064
		return true;
2065
	}
2066
2067
	/**
2068
	 * Only lock files in data/user/files/
2069
	 *
2070
	 * @param string $path Absolute path to the file/folder we try to (un)lock
2071
	 * @return bool
2072
	 */
2073
	protected function shouldLockFile($path) {
2074
		$path = Filesystem::normalizePath($path);
2075
2076
		$pathSegments = explode('/', $path);
2077
		if (isset($pathSegments[2])) {
2078
			// E.g.: /username/files/path-to-file
2079
			return ($pathSegments[2] === 'files') && (count($pathSegments) > 3);
2080
		}
2081
2082
		return !str_starts_with($path, '/appdata_');
2083
	}
2084
2085
	/**
2086
	 * Shortens the given absolute path to be relative to
2087
	 * "$user/files".
2088
	 *
2089
	 * @param string $absolutePath absolute path which is under "files"
2090
	 *
2091
	 * @return string path relative to "files" with trimmed slashes or null
2092
	 * if the path was NOT relative to files
2093
	 *
2094
	 * @throws \InvalidArgumentException if the given path was not under "files"
2095
	 * @since 8.1.0
2096
	 */
2097
	public function getPathRelativeToFiles($absolutePath) {
2098
		$path = Filesystem::normalizePath($absolutePath);
2099
		$parts = explode('/', trim($path, '/'), 3);
2100
		// "$user", "files", "path/to/dir"
2101
		if (!isset($parts[1]) || $parts[1] !== 'files') {
2102
			$this->logger->error(
2103
				'$absolutePath must be relative to "files", value is "{absolutePath}"',
2104
				[
2105
					'absolutePath' => $absolutePath,
2106
				]
2107
			);
2108
			throw new \InvalidArgumentException('$absolutePath must be relative to "files"');
2109
		}
2110
		if (isset($parts[2])) {
2111
			return $parts[2];
2112
		}
2113
		return '';
2114
	}
2115
2116
	/**
2117
	 * @param string $filename
2118
	 * @return array
2119
	 * @throws \OC\User\NoUserException
2120
	 * @throws NotFoundException
2121
	 */
2122
	public function getUidAndFilename($filename) {
2123
		$info = $this->getFileInfo($filename);
2124
		if (!$info instanceof \OCP\Files\FileInfo) {
2125
			throw new NotFoundException($this->getAbsolutePath($filename) . ' not found');
2126
		}
2127
		$uid = $info->getOwner()->getUID();
2128
		if ($uid != \OC_User::getUser()) {
2129
			Filesystem::initMountPoints($uid);
2130
			$ownerView = new View('/' . $uid . '/files');
2131
			try {
2132
				$filename = $ownerView->getPath($info['fileid']);
2133
			} catch (NotFoundException $e) {
2134
				throw new NotFoundException('File with id ' . $info['fileid'] . ' not found for user ' . $uid);
2135
			}
2136
		}
2137
		return [$uid, $filename];
2138
	}
2139
2140
	/**
2141
	 * Creates parent non-existing folders
2142
	 *
2143
	 * @param string $filePath
2144
	 * @return bool
2145
	 */
2146
	private function createParentDirectories($filePath) {
2147
		$directoryParts = explode('/', $filePath);
2148
		$directoryParts = array_filter($directoryParts);
2149
		foreach ($directoryParts as $key => $part) {
2150
			$currentPathElements = array_slice($directoryParts, 0, $key);
2151
			$currentPath = '/' . implode('/', $currentPathElements);
2152
			if ($this->is_file($currentPath)) {
2153
				return false;
2154
			}
2155
			if (!$this->file_exists($currentPath)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->file_exists($currentPath) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
2156
				$this->mkdir($currentPath);
2157
			}
2158
		}
2159
2160
		return true;
2161
	}
2162
}
2163