Passed
Push — master ( 524db1...d9cd8b )
by Julius
14:35 queued 12s
created

View::isDeletable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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 OCA\Files_Sharing\SharedMount;
56
use OCP\Constants;
57
use OCP\Files\Cache\ICacheEntry;
58
use OCP\Files\EmptyFileNameException;
59
use OCP\Files\FileNameTooLongException;
60
use OCP\Files\InvalidCharacterInPathException;
61
use OCP\Files\InvalidDirectoryException;
62
use OCP\Files\InvalidPathException;
63
use OCP\Files\Mount\IMountPoint;
64
use OCP\Files\NotFoundException;
65
use OCP\Files\ReservedWordException;
66
use OCP\Files\Storage\IStorage;
67
use OCP\IUser;
68
use OCP\Lock\ILockingProvider;
69
use OCP\Lock\LockedException;
70
use Psr\Log\LoggerInterface;
71
72
/**
73
 * Class to provide access to ownCloud filesystem via a "view", and methods for
74
 * working with files within that view (e.g. read, write, delete, etc.). Each
75
 * view is restricted to a set of directories via a virtual root. The default view
76
 * uses the currently logged in user's data directory as root (parts of
77
 * OC_Filesystem are merely a wrapper for OC\Files\View).
78
 *
79
 * Apps that need to access files outside of the user data folders (to modify files
80
 * belonging to a user other than the one currently logged in, for example) should
81
 * use this class directly rather than using OC_Filesystem, or making use of PHP's
82
 * built-in file manipulation functions. This will ensure all hooks and proxies
83
 * are triggered correctly.
84
 *
85
 * Filesystem functions are not called directly; they are passed to the correct
86
 * \OC\Files\Storage\Storage object
87
 */
88
class View {
89
	/** @var string */
90
	private $fakeRoot = '';
91
92
	/**
93
	 * @var \OCP\Lock\ILockingProvider
94
	 */
95
	protected $lockingProvider;
96
97
	private $lockingEnabled;
98
99
	private $updaterEnabled = true;
100
101
	/** @var \OC\User\Manager */
102
	private $userManager;
103
104
	private LoggerInterface $logger;
105
106
	/**
107
	 * @param string $root
108
	 * @throws \Exception If $root contains an invalid path
109
	 */
110
	public function __construct($root = '') {
111
		if (is_null($root)) {
0 ignored issues
show
introduced by
The condition is_null($root) is always false.
Loading history...
112
			throw new \InvalidArgumentException('Root can\'t be null');
113
		}
114
		if (!Filesystem::isValidPath($root)) {
115
			throw new \Exception();
116
		}
117
118
		$this->fakeRoot = $root;
119
		$this->lockingProvider = \OC::$server->getLockingProvider();
120
		$this->lockingEnabled = !($this->lockingProvider instanceof \OC\Lock\NoopLockingProvider);
121
		$this->userManager = \OC::$server->getUserManager();
122
		$this->logger = \OC::$server->get(LoggerInterface::class);
123
	}
124
125
	public function getAbsolutePath($path = '/') {
126
		if ($path === null) {
127
			return null;
128
		}
129
		$this->assertPathLength($path);
130
		if ($path === '') {
131
			$path = '/';
132
		}
133
		if ($path[0] !== '/') {
134
			$path = '/' . $path;
135
		}
136
		return $this->fakeRoot . $path;
137
	}
138
139
	/**
140
	 * change the root to a fake root
141
	 *
142
	 * @param string $fakeRoot
143
	 * @return boolean|null
144
	 */
145
	public function chroot($fakeRoot) {
146
		if (!$fakeRoot == '') {
147
			if ($fakeRoot[0] !== '/') {
148
				$fakeRoot = '/' . $fakeRoot;
149
			}
150
		}
151
		$this->fakeRoot = $fakeRoot;
152
	}
153
154
	/**
155
	 * get the fake root
156
	 *
157
	 * @return string
158
	 */
159
	public function getRoot() {
160
		return $this->fakeRoot;
161
	}
162
163
	/**
164
	 * get path relative to the root of the view
165
	 *
166
	 * @param string $path
167
	 * @return string
168
	 */
169
	public function getRelativePath($path) {
170
		$this->assertPathLength($path);
171
		if ($this->fakeRoot == '') {
172
			return $path;
173
		}
174
175
		if (rtrim($path, '/') === rtrim($this->fakeRoot, '/')) {
176
			return '/';
177
		}
178
179
		// missing slashes can cause wrong matches!
180
		$root = rtrim($this->fakeRoot, '/') . '/';
181
182
		if (strpos($path, $root) !== 0) {
183
			return null;
184
		} else {
185
			$path = substr($path, strlen($this->fakeRoot));
186
			if (strlen($path) === 0) {
187
				return '/';
188
			} else {
189
				return $path;
190
			}
191
		}
192
	}
193
194
	/**
195
	 * get the mountpoint of the storage object for a path
196
	 * ( note: because a storage is not always mounted inside the fakeroot, the
197
	 * returned mountpoint is relative to the absolute root of the filesystem
198
	 * and does not take the chroot into account )
199
	 *
200
	 * @param string $path
201
	 * @return string
202
	 */
203
	public function getMountPoint($path) {
204
		return Filesystem::getMountPoint($this->getAbsolutePath($path));
205
	}
206
207
	/**
208
	 * get the mountpoint of the storage object for a path
209
	 * ( note: because a storage is not always mounted inside the fakeroot, the
210
	 * returned mountpoint is relative to the absolute root of the filesystem
211
	 * and does not take the chroot into account )
212
	 *
213
	 * @param string $path
214
	 * @return \OCP\Files\Mount\IMountPoint
215
	 */
216
	public function getMount($path) {
217
		return Filesystem::getMountManager()->find($this->getAbsolutePath($path));
218
	}
219
220
	/**
221
	 * resolve a path to a storage and internal path
222
	 *
223
	 * @param string $path
224
	 * @return array an array consisting of the storage and the internal path
225
	 */
226
	public function resolvePath($path) {
227
		$a = $this->getAbsolutePath($path);
228
		$p = Filesystem::normalizePath($a);
229
		return Filesystem::resolvePath($p);
230
	}
231
232
	/**
233
	 * return the path to a local version of the file
234
	 * we need this because we can't know if a file is stored local or not from
235
	 * outside the filestorage and for some purposes a local file is needed
236
	 *
237
	 * @param string $path
238
	 * @return string
239
	 */
240
	public function getLocalFile($path) {
241
		$parent = substr($path, 0, strrpos($path, '/'));
242
		$path = $this->getAbsolutePath($path);
243
		[$storage, $internalPath] = Filesystem::resolvePath($path);
244
		if (Filesystem::isValidPath($parent) and $storage) {
245
			return $storage->getLocalFile($internalPath);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $storage->getLocalFile($internalPath) also could return the type boolean which is incompatible with the documented return type string.
Loading history...
246
		} else {
247
			return null;
248
		}
249
	}
250
251
	/**
252
	 * @param string $path
253
	 * @return string
254
	 */
255
	public function getLocalFolder($path) {
256
		$parent = substr($path, 0, strrpos($path, '/'));
257
		$path = $this->getAbsolutePath($path);
258
		[$storage, $internalPath] = Filesystem::resolvePath($path);
259
		if (Filesystem::isValidPath($parent) and $storage) {
260
			return $storage->getLocalFolder($internalPath);
0 ignored issues
show
Bug introduced by
The method getLocalFolder() does not exist on OCP\Files\Storage\IStorage. Did you maybe mean getLocalFile()? ( Ignorable by Annotation )

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

260
			return $storage->/** @scrutinizer ignore-call */ getLocalFolder($internalPath);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
261
		} else {
262
			return null;
263
		}
264
	}
265
266
	/**
267
	 * the following functions operate with arguments and return values identical
268
	 * to those of their PHP built-in equivalents. Mostly they are merely wrappers
269
	 * for \OC\Files\Storage\Storage via basicOperation().
270
	 */
271
	public function mkdir($path) {
272
		return $this->basicOperation('mkdir', $path, ['create', 'write']);
273
	}
274
275
	/**
276
	 * remove mount point
277
	 *
278
	 * @param IMountPoint $mount
279
	 * @param string $path relative to data/
280
	 * @return boolean
281
	 */
282
	protected function removeMount($mount, $path) {
283
		if ($mount instanceof MoveableMount) {
284
			// cut of /user/files to get the relative path to data/user/files
285
			$pathParts = explode('/', $path, 4);
286
			$relPath = '/' . $pathParts[3];
287
			$this->lockFile($relPath, ILockingProvider::LOCK_SHARED, true);
288
			\OC_Hook::emit(
289
				Filesystem::CLASSNAME, "umount",
290
				[Filesystem::signal_param_path => $relPath]
291
			);
292
			$this->changeLock($relPath, ILockingProvider::LOCK_EXCLUSIVE, true);
293
			$result = $mount->removeMount();
294
			$this->changeLock($relPath, ILockingProvider::LOCK_SHARED, true);
295
			if ($result) {
296
				\OC_Hook::emit(
297
					Filesystem::CLASSNAME, "post_umount",
298
					[Filesystem::signal_param_path => $relPath]
299
				);
300
			}
301
			$this->unlockFile($relPath, ILockingProvider::LOCK_SHARED, true);
302
			return $result;
303
		} else {
304
			// do not allow deleting the storage's root / the mount point
305
			// because for some storages it might delete the whole contents
306
			// but isn't supposed to work that way
307
			return false;
308
		}
309
	}
310
311
	public function disableCacheUpdate() {
312
		$this->updaterEnabled = false;
313
	}
314
315
	public function enableCacheUpdate() {
316
		$this->updaterEnabled = true;
317
	}
318
319
	protected function writeUpdate(Storage $storage, $internalPath, $time = null) {
320
		if ($this->updaterEnabled) {
321
			if (is_null($time)) {
322
				$time = time();
323
			}
324
			$storage->getUpdater()->update($internalPath, $time);
325
		}
326
	}
327
328
	protected function removeUpdate(Storage $storage, $internalPath) {
329
		if ($this->updaterEnabled) {
330
			$storage->getUpdater()->remove($internalPath);
331
		}
332
	}
333
334
	protected function renameUpdate(Storage $sourceStorage, Storage $targetStorage, $sourceInternalPath, $targetInternalPath) {
335
		if ($this->updaterEnabled) {
336
			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
337
		}
338
	}
339
340
	/**
341
	 * @param string $path
342
	 * @return bool|mixed
343
	 */
344
	public function rmdir($path) {
345
		$absolutePath = $this->getAbsolutePath($path);
346
		$mount = Filesystem::getMountManager()->find($absolutePath);
347
		if ($mount->getInternalPath($absolutePath) === '') {
348
			return $this->removeMount($mount, $absolutePath);
349
		}
350
		if ($this->is_dir($path)) {
351
			$result = $this->basicOperation('rmdir', $path, ['delete']);
352
		} else {
353
			$result = false;
354
		}
355
356
		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 $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...
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...
357
			$storage = $mount->getStorage();
358
			$internalPath = $mount->getInternalPath($absolutePath);
359
			$storage->getUpdater()->remove($internalPath);
360
		}
361
		return $result;
362
	}
363
364
	/**
365
	 * @param string $path
366
	 * @return resource
367
	 */
368
	public function opendir($path) {
369
		return $this->basicOperation('opendir', $path, ['read']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->basicOpera..., $path, array('read')) also could return the type boolean which is incompatible with the documented return type resource.
Loading history...
370
	}
371
372
	/**
373
	 * @param string $path
374
	 * @return bool|mixed
375
	 */
376
	public function is_dir($path) {
377
		if ($path == '/') {
378
			return true;
379
		}
380
		return $this->basicOperation('is_dir', $path);
381
	}
382
383
	/**
384
	 * @param string $path
385
	 * @return bool|mixed
386
	 */
387
	public function is_file($path) {
388
		if ($path == '/') {
389
			return false;
390
		}
391
		return $this->basicOperation('is_file', $path);
392
	}
393
394
	/**
395
	 * @param string $path
396
	 * @return mixed
397
	 */
398
	public function stat($path) {
399
		return $this->basicOperation('stat', $path);
400
	}
401
402
	/**
403
	 * @param string $path
404
	 * @return mixed
405
	 */
406
	public function filetype($path) {
407
		return $this->basicOperation('filetype', $path);
408
	}
409
410
	/**
411
	 * @param string $path
412
	 * @return mixed
413
	 */
414
	public function filesize(string $path) {
415
		return $this->basicOperation('filesize', $path);
416
	}
417
418
	/**
419
	 * @param string $path
420
	 * @return bool|mixed
421
	 * @throws \OCP\Files\InvalidPathException
422
	 */
423
	public function readfile($path) {
424
		$this->assertPathLength($path);
425
		if (ob_get_level()) {
426
			ob_end_clean();
427
		}
428
		$handle = $this->fopen($path, 'rb');
429
		if ($handle) {
0 ignored issues
show
introduced by
$handle is of type resource, thus it always evaluated to false.
Loading history...
430
			$chunkSize = 524288; // 512 kB chunks
431
			while (!feof($handle)) {
432
				echo fread($handle, $chunkSize);
433
				flush();
434
			}
435
			fclose($handle);
436
			return $this->filesize($path);
437
		}
438
		return false;
439
	}
440
441
	/**
442
	 * @param string $path
443
	 * @param int $from
444
	 * @param int $to
445
	 * @return bool|mixed
446
	 * @throws \OCP\Files\InvalidPathException
447
	 * @throws \OCP\Files\UnseekableException
448
	 */
449
	public function readfilePart($path, $from, $to) {
450
		$this->assertPathLength($path);
451
		if (ob_get_level()) {
452
			ob_end_clean();
453
		}
454
		$handle = $this->fopen($path, 'rb');
455
		if ($handle) {
0 ignored issues
show
introduced by
$handle is of type resource, thus it always evaluated to false.
Loading history...
456
			$chunkSize = 524288; // 512 kB chunks
457
			$startReading = true;
458
459
			if ($from !== 0 && $from !== '0' && fseek($handle, $from) !== 0) {
460
				// forward file handle via chunked fread because fseek seem to have failed
461
462
				$end = $from + 1;
463
				while (!feof($handle) && ftell($handle) < $end && ftell($handle) !== $from) {
464
					$len = $from - ftell($handle);
465
					if ($len > $chunkSize) {
466
						$len = $chunkSize;
467
					}
468
					$result = fread($handle, $len);
469
470
					if ($result === false) {
471
						$startReading = false;
472
						break;
473
					}
474
				}
475
			}
476
477
			if ($startReading) {
478
				$end = $to + 1;
479
				while (!feof($handle) && ftell($handle) < $end) {
480
					$len = $end - ftell($handle);
481
					if ($len > $chunkSize) {
482
						$len = $chunkSize;
483
					}
484
					echo fread($handle, $len);
485
					flush();
486
				}
487
				return ftell($handle) - $from;
488
			}
489
490
			throw new \OCP\Files\UnseekableException('fseek error');
491
		}
492
		return false;
493
	}
494
495
	/**
496
	 * @param string $path
497
	 * @return mixed
498
	 */
499
	public function isCreatable($path) {
500
		return $this->basicOperation('isCreatable', $path);
501
	}
502
503
	/**
504
	 * @param string $path
505
	 * @return mixed
506
	 */
507
	public function isReadable($path) {
508
		return $this->basicOperation('isReadable', $path);
509
	}
510
511
	/**
512
	 * @param string $path
513
	 * @return mixed
514
	 */
515
	public function isUpdatable($path) {
516
		return $this->basicOperation('isUpdatable', $path);
517
	}
518
519
	/**
520
	 * @param string $path
521
	 * @return bool|mixed
522
	 */
523
	public function isDeletable($path) {
524
		$absolutePath = $this->getAbsolutePath($path);
525
		$mount = Filesystem::getMountManager()->find($absolutePath);
526
		if ($mount->getInternalPath($absolutePath) === '') {
527
			return $mount instanceof MoveableMount;
528
		}
529
		return $this->basicOperation('isDeletable', $path);
530
	}
531
532
	/**
533
	 * @param string $path
534
	 * @return mixed
535
	 */
536
	public function isSharable($path) {
537
		return $this->basicOperation('isSharable', $path);
538
	}
539
540
	/**
541
	 * @param string $path
542
	 * @return bool|mixed
543
	 */
544
	public function file_exists($path) {
545
		if ($path == '/') {
546
			return true;
547
		}
548
		return $this->basicOperation('file_exists', $path);
549
	}
550
551
	/**
552
	 * @param string $path
553
	 * @return mixed
554
	 */
555
	public function filemtime($path) {
556
		return $this->basicOperation('filemtime', $path);
557
	}
558
559
	/**
560
	 * @param string $path
561
	 * @param int|string $mtime
562
	 * @return bool
563
	 */
564
	public function touch($path, $mtime = null) {
565
		if (!is_null($mtime) and !is_numeric($mtime)) {
566
			$mtime = strtotime($mtime);
567
		}
568
569
		$hooks = ['touch'];
570
571
		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...
572
			$hooks[] = 'create';
573
			$hooks[] = 'write';
574
		}
575
		try {
576
			$result = $this->basicOperation('touch', $path, $hooks, $mtime);
577
		} catch (\Exception $e) {
578
			$this->logger->info('Error while setting modified time', ['app' => 'core', 'exception' => $e]);
579
			$result = false;
580
		}
581
		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...
582
			// If create file fails because of permissions on external storage like SMB folders,
583
			// check file exists and return false if not.
584
			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...
585
				return false;
586
			}
587
			if (is_null($mtime)) {
588
				$mtime = time();
589
			}
590
			//if native touch fails, we emulate it by changing the mtime in the cache
591
			$this->putFileInfo($path, ['mtime' => floor($mtime)]);
592
		}
593
		return true;
594
	}
595
596
	/**
597
	 * @param string $path
598
	 * @return mixed
599
	 * @throws LockedException
600
	 */
601
	public function file_get_contents($path) {
602
		return $this->basicOperation('file_get_contents', $path, ['read']);
603
	}
604
605
	/**
606
	 * @param bool $exists
607
	 * @param string $path
608
	 * @param bool $run
609
	 */
610
	protected function emit_file_hooks_pre($exists, $path, &$run) {
611
		if (!$exists) {
612
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
613
				Filesystem::signal_param_path => $this->getHookPath($path),
614
				Filesystem::signal_param_run => &$run,
615
			]);
616
		} else {
617
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
618
				Filesystem::signal_param_path => $this->getHookPath($path),
619
				Filesystem::signal_param_run => &$run,
620
			]);
621
		}
622
		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
623
			Filesystem::signal_param_path => $this->getHookPath($path),
624
			Filesystem::signal_param_run => &$run,
625
		]);
626
	}
627
628
	/**
629
	 * @param bool $exists
630
	 * @param string $path
631
	 */
632
	protected function emit_file_hooks_post($exists, $path) {
633
		if (!$exists) {
634
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
635
				Filesystem::signal_param_path => $this->getHookPath($path),
636
			]);
637
		} else {
638
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
639
				Filesystem::signal_param_path => $this->getHookPath($path),
640
			]);
641
		}
642
		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
643
			Filesystem::signal_param_path => $this->getHookPath($path),
644
		]);
645
	}
646
647
	/**
648
	 * @param string $path
649
	 * @param string|resource $data
650
	 * @return bool|mixed
651
	 * @throws LockedException
652
	 */
653
	public function file_put_contents($path, $data) {
654
		if (is_resource($data)) { //not having to deal with streams in file_put_contents makes life easier
655
			$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
656
			if (Filesystem::isValidPath($path)
657
				and !Filesystem::isFileBlacklisted($path)
658
			) {
659
				$path = $this->getRelativePath($absolutePath);
660
661
				$this->lockFile($path, ILockingProvider::LOCK_SHARED);
662
663
				$exists = $this->file_exists($path);
664
				$run = true;
665
				if ($this->shouldEmitHooks($path)) {
666
					$this->emit_file_hooks_pre($exists, $path, $run);
667
				}
668
				if (!$run) {
669
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
670
					return false;
671
				}
672
673
				try {
674
					$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
675
				} catch (\Exception $e) {
676
					// Release the shared lock before throwing.
677
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
678
					throw $e;
679
				}
680
681
				/** @var \OC\Files\Storage\Storage $storage */
682
				[$storage, $internalPath] = $this->resolvePath($path);
683
				$target = $storage->fopen($internalPath, 'w');
684
				if ($target) {
685
					[, $result] = \OC_Helper::streamCopy($data, $target);
686
					fclose($target);
687
					fclose($data);
688
689
					$this->writeUpdate($storage, $internalPath);
690
691
					$this->changeLock($path, ILockingProvider::LOCK_SHARED);
692
693
					if ($this->shouldEmitHooks($path) && $result !== false) {
0 ignored issues
show
introduced by
The condition $result !== false is always false.
Loading history...
694
						$this->emit_file_hooks_post($exists, $path);
695
					}
696
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
697
					return $result;
698
				} else {
699
					$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
700
					return false;
701
				}
702
			} else {
703
				return false;
704
			}
705
		} else {
706
			$hooks = $this->file_exists($path) ? ['update', 'write'] : ['create', 'write'];
707
			return $this->basicOperation('file_put_contents', $path, $hooks, $data);
708
		}
709
	}
710
711
	/**
712
	 * @param string $path
713
	 * @return bool|mixed
714
	 */
715
	public function unlink($path) {
716
		if ($path === '' || $path === '/') {
717
			// do not allow deleting the root
718
			return false;
719
		}
720
		$postFix = (substr($path, -1) === '/') ? '/' : '';
721
		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
722
		$mount = Filesystem::getMountManager()->find($absolutePath . $postFix);
723
		if ($mount->getInternalPath($absolutePath) === '') {
724
			return $this->removeMount($mount, $absolutePath);
725
		}
726
		if ($this->is_dir($path)) {
727
			$result = $this->basicOperation('rmdir', $path, ['delete']);
728
		} else {
729
			$result = $this->basicOperation('unlink', $path, ['delete']);
730
		}
731
		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...
732
			$storage = $mount->getStorage();
733
			$internalPath = $mount->getInternalPath($absolutePath);
734
			$storage->getUpdater()->remove($internalPath);
735
			return true;
736
		} else {
737
			return $result;
738
		}
739
	}
740
741
	/**
742
	 * @param string $directory
743
	 * @return bool|mixed
744
	 */
745
	public function deleteAll($directory) {
746
		return $this->rmdir($directory);
747
	}
748
749
	/**
750
	 * Rename/move a file or folder from the source path to target path.
751
	 *
752
	 * @param string $source source path
753
	 * @param string $target target path
754
	 *
755
	 * @return bool|mixed
756
	 * @throws LockedException
757
	 */
758
	public function rename($source, $target) {
759
		$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source));
760
		$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
761
		$result = false;
762
		if (
763
			Filesystem::isValidPath($target)
764
			and Filesystem::isValidPath($source)
765
			and !Filesystem::isFileBlacklisted($target)
766
		) {
767
			$source = $this->getRelativePath($absolutePath1);
768
			$target = $this->getRelativePath($absolutePath2);
769
			$exists = $this->file_exists($target);
770
771
			if ($source == null or $target == null) {
772
				return false;
773
			}
774
775
			$this->lockFile($source, ILockingProvider::LOCK_SHARED, true);
776
			try {
777
				$this->lockFile($target, ILockingProvider::LOCK_SHARED, true);
778
779
				$run = true;
780
				if ($this->shouldEmitHooks($source) && (Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target))) {
781
					// if it was a rename from a part file to a regular file it was a write and not a rename operation
782
					$this->emit_file_hooks_pre($exists, $target, $run);
783
				} elseif ($this->shouldEmitHooks($source)) {
784
					\OC_Hook::emit(
785
						Filesystem::CLASSNAME, Filesystem::signal_rename,
786
						[
787
							Filesystem::signal_param_oldpath => $this->getHookPath($source),
788
							Filesystem::signal_param_newpath => $this->getHookPath($target),
789
							Filesystem::signal_param_run => &$run
790
						]
791
					);
792
				}
793
				if ($run) {
794
					$this->verifyPath(dirname($target), basename($target));
795
796
					$manager = Filesystem::getMountManager();
797
					$mount1 = $this->getMount($source);
798
					$mount2 = $this->getMount($target);
799
					$storage1 = $mount1->getStorage();
800
					$storage2 = $mount2->getStorage();
801
					$internalPath1 = $mount1->getInternalPath($absolutePath1);
802
					$internalPath2 = $mount2->getInternalPath($absolutePath2);
803
804
					$this->changeLock($source, ILockingProvider::LOCK_EXCLUSIVE, true);
805
					try {
806
						$this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE, true);
807
808
						if ($internalPath1 === '') {
809
							if ($mount1 instanceof MoveableMount) {
810
								$sourceParentMount = $this->getMount(dirname($source));
811
								if ($sourceParentMount === $mount2 && $this->targetIsNotShared($storage2, $internalPath2)) {
812
									/**
813
									 * @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
814
									 */
815
									$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

815
									/** @scrutinizer ignore-call */ 
816
         $sourceMountPoint = $mount1->getMountPoint();
Loading history...
816
									$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

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

1967
					$storage->/** @scrutinizer ignore-call */ 
1968
               acquireLock(
Loading history...
1968
						$mount->getInternalPath($absolutePath),
1969
						$type,
1970
						$this->lockingProvider
1971
					);
1972
				}
1973
			} catch (LockedException $e) {
1974
				// rethrow with the a human-readable path
1975
				throw new LockedException(
1976
					$this->getPathRelativeToFiles($absolutePath),
1977
					$e,
1978
					$e->getExistingLock()
1979
				);
1980
			}
1981
		}
1982
1983
		return true;
1984
	}
1985
1986
	/**
1987
	 * Change the lock type
1988
	 *
1989
	 * @param string $path the path of the file to lock, relative to the view
1990
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
1991
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
1992
	 *
1993
	 * @return bool False if the path is excluded from locking, true otherwise
1994
	 * @throws LockedException if the path is already locked
1995
	 */
1996
	public function changeLock($path, $type, $lockMountPoint = false) {
1997
		$path = Filesystem::normalizePath($path);
1998
		$absolutePath = $this->getAbsolutePath($path);
1999
		$absolutePath = Filesystem::normalizePath($absolutePath);
2000
		if (!$this->shouldLockFile($absolutePath)) {
2001
			return false;
2002
		}
2003
2004
		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
2005
		if ($mount) {
0 ignored issues
show
introduced by
$mount is of type OCP\Files\Mount\IMountPoint, thus it always evaluated to true.
Loading history...
2006
			try {
2007
				$storage = $mount->getStorage();
2008
				if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
2009
					$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. 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

2009
					$storage->/** @scrutinizer ignore-call */ 
2010
               changeLock(
Loading history...
2010
						$mount->getInternalPath($absolutePath),
2011
						$type,
2012
						$this->lockingProvider
2013
					);
2014
				}
2015
			} catch (LockedException $e) {
2016
				try {
2017
					// rethrow with the a human-readable path
2018
					throw new LockedException(
2019
						$this->getPathRelativeToFiles($absolutePath),
2020
						$e,
2021
						$e->getExistingLock()
2022
					);
2023
				} catch (\InvalidArgumentException $ex) {
2024
					throw new LockedException(
2025
						$absolutePath,
2026
						$ex,
2027
						$e->getExistingLock()
2028
					);
2029
				}
2030
			}
2031
		}
2032
2033
		return true;
2034
	}
2035
2036
	/**
2037
	 * Unlock the given path
2038
	 *
2039
	 * @param string $path the path of the file to unlock, relative to the view
2040
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2041
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2042
	 *
2043
	 * @return bool False if the path is excluded from locking, true otherwise
2044
	 * @throws LockedException
2045
	 */
2046
	private function unlockPath($path, $type, $lockMountPoint = false) {
2047
		$absolutePath = $this->getAbsolutePath($path);
2048
		$absolutePath = Filesystem::normalizePath($absolutePath);
2049
		if (!$this->shouldLockFile($absolutePath)) {
2050
			return false;
2051
		}
2052
2053
		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
2054
		if ($mount) {
0 ignored issues
show
introduced by
$mount is of type OCP\Files\Mount\IMountPoint, thus it always evaluated to true.
Loading history...
2055
			$storage = $mount->getStorage();
2056
			if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
2057
				$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. 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

2057
				$storage->/** @scrutinizer ignore-call */ 
2058
              releaseLock(
Loading history...
2058
					$mount->getInternalPath($absolutePath),
2059
					$type,
2060
					$this->lockingProvider
2061
				);
2062
			}
2063
		}
2064
2065
		return true;
2066
	}
2067
2068
	/**
2069
	 * Lock a path and all its parents up to the root of the view
2070
	 *
2071
	 * @param string $path the path of the file to lock relative to the view
2072
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2073
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2074
	 *
2075
	 * @return bool False if the path is excluded from locking, true otherwise
2076
	 * @throws LockedException
2077
	 */
2078
	public function lockFile($path, $type, $lockMountPoint = false) {
2079
		$absolutePath = $this->getAbsolutePath($path);
2080
		$absolutePath = Filesystem::normalizePath($absolutePath);
2081
		if (!$this->shouldLockFile($absolutePath)) {
2082
			return false;
2083
		}
2084
2085
		$this->lockPath($path, $type, $lockMountPoint);
2086
2087
		$parents = $this->getParents($path);
2088
		foreach ($parents as $parent) {
2089
			$this->lockPath($parent, ILockingProvider::LOCK_SHARED);
2090
		}
2091
2092
		return true;
2093
	}
2094
2095
	/**
2096
	 * Unlock a path and all its parents up to the root of the view
2097
	 *
2098
	 * @param string $path the path of the file to lock relative to the view
2099
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2100
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2101
	 *
2102
	 * @return bool False if the path is excluded from locking, true otherwise
2103
	 * @throws LockedException
2104
	 */
2105
	public function unlockFile($path, $type, $lockMountPoint = false) {
2106
		$absolutePath = $this->getAbsolutePath($path);
2107
		$absolutePath = Filesystem::normalizePath($absolutePath);
2108
		if (!$this->shouldLockFile($absolutePath)) {
2109
			return false;
2110
		}
2111
2112
		$this->unlockPath($path, $type, $lockMountPoint);
2113
2114
		$parents = $this->getParents($path);
2115
		foreach ($parents as $parent) {
2116
			$this->unlockPath($parent, ILockingProvider::LOCK_SHARED);
2117
		}
2118
2119
		return true;
2120
	}
2121
2122
	/**
2123
	 * Only lock files in data/user/files/
2124
	 *
2125
	 * @param string $path Absolute path to the file/folder we try to (un)lock
2126
	 * @return bool
2127
	 */
2128
	protected function shouldLockFile($path) {
2129
		$path = Filesystem::normalizePath($path);
2130
2131
		$pathSegments = explode('/', $path);
2132
		if (isset($pathSegments[2])) {
2133
			// E.g.: /username/files/path-to-file
2134
			return ($pathSegments[2] === 'files') && (count($pathSegments) > 3);
2135
		}
2136
2137
		return strpos($path, '/appdata_') !== 0;
2138
	}
2139
2140
	/**
2141
	 * Shortens the given absolute path to be relative to
2142
	 * "$user/files".
2143
	 *
2144
	 * @param string $absolutePath absolute path which is under "files"
2145
	 *
2146
	 * @return string path relative to "files" with trimmed slashes or null
2147
	 * if the path was NOT relative to files
2148
	 *
2149
	 * @throws \InvalidArgumentException if the given path was not under "files"
2150
	 * @since 8.1.0
2151
	 */
2152
	public function getPathRelativeToFiles($absolutePath) {
2153
		$path = Filesystem::normalizePath($absolutePath);
2154
		$parts = explode('/', trim($path, '/'), 3);
2155
		// "$user", "files", "path/to/dir"
2156
		if (!isset($parts[1]) || $parts[1] !== 'files') {
2157
			$this->logger->error(
2158
				'$absolutePath must be relative to "files", value is "{absolutePath}"',
2159
				[
2160
					'absolutePath' => $absolutePath,
2161
				]
2162
			);
2163
			throw new \InvalidArgumentException('$absolutePath must be relative to "files"');
2164
		}
2165
		if (isset($parts[2])) {
2166
			return $parts[2];
2167
		}
2168
		return '';
2169
	}
2170
2171
	/**
2172
	 * @param string $filename
2173
	 * @return array
2174
	 * @throws \OC\User\NoUserException
2175
	 * @throws NotFoundException
2176
	 */
2177
	public function getUidAndFilename($filename) {
2178
		$info = $this->getFileInfo($filename);
2179
		if (!$info instanceof \OCP\Files\FileInfo) {
2180
			throw new NotFoundException($this->getAbsolutePath($filename) . ' not found');
2181
		}
2182
		$uid = $info->getOwner()->getUID();
2183
		if ($uid != \OC_User::getUser()) {
2184
			Filesystem::initMountPoints($uid);
2185
			$ownerView = new View('/' . $uid . '/files');
2186
			try {
2187
				$filename = $ownerView->getPath($info['fileid']);
2188
			} catch (NotFoundException $e) {
2189
				throw new NotFoundException('File with id ' . $info['fileid'] . ' not found for user ' . $uid);
2190
			}
2191
		}
2192
		return [$uid, $filename];
2193
	}
2194
2195
	/**
2196
	 * Creates parent non-existing folders
2197
	 *
2198
	 * @param string $filePath
2199
	 * @return bool
2200
	 */
2201
	private function createParentDirectories($filePath) {
2202
		$directoryParts = explode('/', $filePath);
2203
		$directoryParts = array_filter($directoryParts);
2204
		foreach ($directoryParts as $key => $part) {
2205
			$currentPathElements = array_slice($directoryParts, 0, $key);
2206
			$currentPath = '/' . implode('/', $currentPathElements);
2207
			if ($this->is_file($currentPath)) {
2208
				return false;
2209
			}
2210
			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...
2211
				$this->mkdir($currentPath);
2212
			}
2213
		}
2214
2215
		return true;
2216
	}
2217
}
2218