Passed
Push — master ( b88dc5...9f34d1 )
by Robin
14:21 queued 11s
created

View::getDirectoryContent()   F

Complexity

Conditions 24
Paths 121

Size

Total Lines 128
Code Lines 83

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 24
eloc 83
nc 121
nop 3
dl 0
loc 128
rs 3.9916
c 4
b 1
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

259
			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...
260
		} else {
261
			return null;
262
		}
263
	}
264
265
	/**
266
	 * the following functions operate with arguments and return values identical
267
	 * to those of their PHP built-in equivalents. Mostly they are merely wrappers
268
	 * for \OC\Files\Storage\Storage via basicOperation().
269
	 */
270
	public function mkdir($path) {
271
		return $this->basicOperation('mkdir', $path, ['create', 'write']);
272
	}
273
274
	/**
275
	 * remove mount point
276
	 *
277
	 * @param IMountPoint $mount
278
	 * @param string $path relative to data/
279
	 * @return boolean
280
	 */
281
	protected function removeMount($mount, $path) {
282
		if ($mount instanceof MoveableMount) {
283
			// cut of /user/files to get the relative path to data/user/files
284
			$pathParts = explode('/', $path, 4);
285
			$relPath = '/' . $pathParts[3];
286
			$this->lockFile($relPath, ILockingProvider::LOCK_SHARED, true);
287
			\OC_Hook::emit(
288
				Filesystem::CLASSNAME, "umount",
289
				[Filesystem::signal_param_path => $relPath]
290
			);
291
			$this->changeLock($relPath, ILockingProvider::LOCK_EXCLUSIVE, true);
292
			$result = $mount->removeMount();
293
			$this->changeLock($relPath, ILockingProvider::LOCK_SHARED, true);
294
			if ($result) {
295
				\OC_Hook::emit(
296
					Filesystem::CLASSNAME, "post_umount",
297
					[Filesystem::signal_param_path => $relPath]
298
				);
299
			}
300
			$this->unlockFile($relPath, ILockingProvider::LOCK_SHARED, true);
301
			return $result;
302
		} else {
303
			// do not allow deleting the storage's root / the mount point
304
			// because for some storages it might delete the whole contents
305
			// but isn't supposed to work that way
306
			return false;
307
		}
308
	}
309
310
	public function disableCacheUpdate() {
311
		$this->updaterEnabled = false;
312
	}
313
314
	public function enableCacheUpdate() {
315
		$this->updaterEnabled = true;
316
	}
317
318
	protected function writeUpdate(Storage $storage, $internalPath, $time = null) {
319
		if ($this->updaterEnabled) {
320
			if (is_null($time)) {
321
				$time = time();
322
			}
323
			$storage->getUpdater()->update($internalPath, $time);
324
		}
325
	}
326
327
	protected function removeUpdate(Storage $storage, $internalPath) {
328
		if ($this->updaterEnabled) {
329
			$storage->getUpdater()->remove($internalPath);
330
		}
331
	}
332
333
	protected function renameUpdate(Storage $sourceStorage, Storage $targetStorage, $sourceInternalPath, $targetInternalPath) {
334
		if ($this->updaterEnabled) {
335
			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
336
		}
337
	}
338
339
	/**
340
	 * @param string $path
341
	 * @return bool|mixed
342
	 */
343
	public function rmdir($path) {
344
		$absolutePath = $this->getAbsolutePath($path);
345
		$mount = Filesystem::getMountManager()->find($absolutePath);
346
		if ($mount->getInternalPath($absolutePath) === '') {
347
			return $this->removeMount($mount, $absolutePath);
348
		}
349
		if ($this->is_dir($path)) {
350
			$result = $this->basicOperation('rmdir', $path, ['delete']);
351
		} else {
352
			$result = false;
353
		}
354
355
		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...
356
			$storage = $mount->getStorage();
357
			$internalPath = $mount->getInternalPath($absolutePath);
358
			$storage->getUpdater()->remove($internalPath);
359
		}
360
		return $result;
361
	}
362
363
	/**
364
	 * @param string $path
365
	 * @return resource
366
	 */
367
	public function opendir($path) {
368
		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...
369
	}
370
371
	/**
372
	 * @param string $path
373
	 * @return bool|mixed
374
	 */
375
	public function is_dir($path) {
376
		if ($path == '/') {
377
			return true;
378
		}
379
		return $this->basicOperation('is_dir', $path);
380
	}
381
382
	/**
383
	 * @param string $path
384
	 * @return bool|mixed
385
	 */
386
	public function is_file($path) {
387
		if ($path == '/') {
388
			return false;
389
		}
390
		return $this->basicOperation('is_file', $path);
391
	}
392
393
	/**
394
	 * @param string $path
395
	 * @return mixed
396
	 */
397
	public function stat($path) {
398
		return $this->basicOperation('stat', $path);
399
	}
400
401
	/**
402
	 * @param string $path
403
	 * @return mixed
404
	 */
405
	public function filetype($path) {
406
		return $this->basicOperation('filetype', $path);
407
	}
408
409
	/**
410
	 * @param string $path
411
	 * @return mixed
412
	 */
413
	public function filesize($path) {
414
		return $this->basicOperation('filesize', $path);
415
	}
416
417
	/**
418
	 * @param string $path
419
	 * @return bool|mixed
420
	 * @throws \OCP\Files\InvalidPathException
421
	 */
422
	public function readfile($path) {
423
		$this->assertPathLength($path);
424
		if (ob_get_level()) {
425
			ob_end_clean();
426
		}
427
		$handle = $this->fopen($path, 'rb');
428
		if ($handle) {
0 ignored issues
show
introduced by
$handle is of type resource, thus it always evaluated to false.
Loading history...
429
			$chunkSize = 524288; // 512 kB chunks
430
			while (!feof($handle)) {
431
				echo fread($handle, $chunkSize);
432
				flush();
433
			}
434
			fclose($handle);
435
			return $this->filesize($path);
436
		}
437
		return false;
438
	}
439
440
	/**
441
	 * @param string $path
442
	 * @param int $from
443
	 * @param int $to
444
	 * @return bool|mixed
445
	 * @throws \OCP\Files\InvalidPathException
446
	 * @throws \OCP\Files\UnseekableException
447
	 */
448
	public function readfilePart($path, $from, $to) {
449
		$this->assertPathLength($path);
450
		if (ob_get_level()) {
451
			ob_end_clean();
452
		}
453
		$handle = $this->fopen($path, 'rb');
454
		if ($handle) {
0 ignored issues
show
introduced by
$handle is of type resource, thus it always evaluated to false.
Loading history...
455
			$chunkSize = 524288; // 512 kB chunks
456
			$startReading = true;
457
458
			if ($from !== 0 && $from !== '0' && fseek($handle, $from) !== 0) {
459
				// forward file handle via chunked fread because fseek seem to have failed
460
461
				$end = $from + 1;
462
				while (!feof($handle) && ftell($handle) < $end && ftell($handle) !== $from) {
463
					$len = $from - ftell($handle);
464
					if ($len > $chunkSize) {
465
						$len = $chunkSize;
466
					}
467
					$result = fread($handle, $len);
468
469
					if ($result === false) {
470
						$startReading = false;
471
						break;
472
					}
473
				}
474
			}
475
476
			if ($startReading) {
477
				$end = $to + 1;
478
				while (!feof($handle) && ftell($handle) < $end) {
479
					$len = $end - ftell($handle);
480
					if ($len > $chunkSize) {
481
						$len = $chunkSize;
482
					}
483
					echo fread($handle, $len);
484
					flush();
485
				}
486
				return ftell($handle) - $from;
487
			}
488
489
			throw new \OCP\Files\UnseekableException('fseek error');
490
		}
491
		return false;
492
	}
493
494
	/**
495
	 * @param string $path
496
	 * @return mixed
497
	 */
498
	public function isCreatable($path) {
499
		return $this->basicOperation('isCreatable', $path);
500
	}
501
502
	/**
503
	 * @param string $path
504
	 * @return mixed
505
	 */
506
	public function isReadable($path) {
507
		return $this->basicOperation('isReadable', $path);
508
	}
509
510
	/**
511
	 * @param string $path
512
	 * @return mixed
513
	 */
514
	public function isUpdatable($path) {
515
		return $this->basicOperation('isUpdatable', $path);
516
	}
517
518
	/**
519
	 * @param string $path
520
	 * @return bool|mixed
521
	 */
522
	public function isDeletable($path) {
523
		$absolutePath = $this->getAbsolutePath($path);
524
		$mount = Filesystem::getMountManager()->find($absolutePath);
525
		if ($mount->getInternalPath($absolutePath) === '') {
526
			return $mount instanceof MoveableMount;
527
		}
528
		return $this->basicOperation('isDeletable', $path);
529
	}
530
531
	/**
532
	 * @param string $path
533
	 * @return mixed
534
	 */
535
	public function isSharable($path) {
536
		return $this->basicOperation('isSharable', $path);
537
	}
538
539
	/**
540
	 * @param string $path
541
	 * @return bool|mixed
542
	 */
543
	public function file_exists($path) {
544
		if ($path == '/') {
545
			return true;
546
		}
547
		return $this->basicOperation('file_exists', $path);
548
	}
549
550
	/**
551
	 * @param string $path
552
	 * @return mixed
553
	 */
554
	public function filemtime($path) {
555
		return $this->basicOperation('filemtime', $path);
556
	}
557
558
	/**
559
	 * @param string $path
560
	 * @param int|string $mtime
561
	 * @return bool
562
	 */
563
	public function touch($path, $mtime = null) {
564
		if (!is_null($mtime) and !is_numeric($mtime)) {
565
			$mtime = strtotime($mtime);
566
		}
567
568
		$hooks = ['touch'];
569
570
		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...
571
			$hooks[] = 'create';
572
			$hooks[] = 'write';
573
		}
574
		try {
575
			$result = $this->basicOperation('touch', $path, $hooks, $mtime);
576
		} catch (\Exception $e) {
577
			$this->logger->logException($e, ['level' => ILogger::INFO, 'message' => 'Error while setting modified time']);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::INFO has been deprecated: 20.0.0 ( Ignorable by Annotation )

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

577
			$this->logger->logException($e, ['level' => /** @scrutinizer ignore-deprecated */ ILogger::INFO, 'message' => 'Error while setting modified time']);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
578
			$result = false;
579
		}
580
		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...
581
			// If create file fails because of permissions on external storage like SMB folders,
582
			// check file exists and return false if not.
583
			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...
584
				return false;
585
			}
586
			if (is_null($mtime)) {
587
				$mtime = time();
588
			}
589
			//if native touch fails, we emulate it by changing the mtime in the cache
590
			$this->putFileInfo($path, ['mtime' => floor($mtime)]);
591
		}
592
		return true;
593
	}
594
595
	/**
596
	 * @param string $path
597
	 * @return mixed
598
	 * @throws LockedException
599
	 */
600
	public function file_get_contents($path) {
601
		return $this->basicOperation('file_get_contents', $path, ['read']);
602
	}
603
604
	/**
605
	 * @param bool $exists
606
	 * @param string $path
607
	 * @param bool $run
608
	 */
609
	protected function emit_file_hooks_pre($exists, $path, &$run) {
610
		if (!$exists) {
611
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
612
				Filesystem::signal_param_path => $this->getHookPath($path),
613
				Filesystem::signal_param_run => &$run,
614
			]);
615
		} else {
616
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
617
				Filesystem::signal_param_path => $this->getHookPath($path),
618
				Filesystem::signal_param_run => &$run,
619
			]);
620
		}
621
		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
622
			Filesystem::signal_param_path => $this->getHookPath($path),
623
			Filesystem::signal_param_run => &$run,
624
		]);
625
	}
626
627
	/**
628
	 * @param bool $exists
629
	 * @param string $path
630
	 */
631
	protected function emit_file_hooks_post($exists, $path) {
632
		if (!$exists) {
633
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
634
				Filesystem::signal_param_path => $this->getHookPath($path),
635
			]);
636
		} else {
637
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
638
				Filesystem::signal_param_path => $this->getHookPath($path),
639
			]);
640
		}
641
		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
642
			Filesystem::signal_param_path => $this->getHookPath($path),
643
		]);
644
	}
645
646
	/**
647
	 * @param string $path
648
	 * @param string|resource $data
649
	 * @return bool|mixed
650
	 * @throws LockedException
651
	 */
652
	public function file_put_contents($path, $data) {
653
		if (is_resource($data)) { //not having to deal with streams in file_put_contents makes life easier
654
			$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
655
			if (Filesystem::isValidPath($path)
656
				and !Filesystem::isFileBlacklisted($path)
657
			) {
658
				$path = $this->getRelativePath($absolutePath);
659
660
				$this->lockFile($path, ILockingProvider::LOCK_SHARED);
661
662
				$exists = $this->file_exists($path);
663
				$run = true;
664
				if ($this->shouldEmitHooks($path)) {
665
					$this->emit_file_hooks_pre($exists, $path, $run);
666
				}
667
				if (!$run) {
668
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
669
					return false;
670
				}
671
672
				try {
673
					$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
674
				} catch (\Exception $e) {
675
					// Release the shared lock before throwing.
676
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
677
					throw $e;
678
				}
679
680
				/** @var \OC\Files\Storage\Storage $storage */
681
				[$storage, $internalPath] = $this->resolvePath($path);
682
				$target = $storage->fopen($internalPath, 'w');
683
				if ($target) {
684
					[, $result] = \OC_Helper::streamCopy($data, $target);
685
					fclose($target);
686
					fclose($data);
687
688
					$this->writeUpdate($storage, $internalPath);
689
690
					$this->changeLock($path, ILockingProvider::LOCK_SHARED);
691
692
					if ($this->shouldEmitHooks($path) && $result !== false) {
0 ignored issues
show
introduced by
The condition $result !== false is always false.
Loading history...
693
						$this->emit_file_hooks_post($exists, $path);
694
					}
695
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
696
					return $result;
697
				} else {
698
					$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
699
					return false;
700
				}
701
			} else {
702
				return false;
703
			}
704
		} else {
705
			$hooks = $this->file_exists($path) ? ['update', 'write'] : ['create', 'write'];
706
			return $this->basicOperation('file_put_contents', $path, $hooks, $data);
707
		}
708
	}
709
710
	/**
711
	 * @param string $path
712
	 * @return bool|mixed
713
	 */
714
	public function unlink($path) {
715
		if ($path === '' || $path === '/') {
716
			// do not allow deleting the root
717
			return false;
718
		}
719
		$postFix = (substr($path, -1) === '/') ? '/' : '';
720
		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
721
		$mount = Filesystem::getMountManager()->find($absolutePath . $postFix);
722
		if ($mount->getInternalPath($absolutePath) === '') {
723
			return $this->removeMount($mount, $absolutePath);
724
		}
725
		if ($this->is_dir($path)) {
726
			$result = $this->basicOperation('rmdir', $path, ['delete']);
727
		} else {
728
			$result = $this->basicOperation('unlink', $path, ['delete']);
729
		}
730
		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...
731
			$storage = $mount->getStorage();
732
			$internalPath = $mount->getInternalPath($absolutePath);
733
			$storage->getUpdater()->remove($internalPath);
734
			return true;
735
		} else {
736
			return $result;
737
		}
738
	}
739
740
	/**
741
	 * @param string $directory
742
	 * @return bool|mixed
743
	 */
744
	public function deleteAll($directory) {
745
		return $this->rmdir($directory);
746
	}
747
748
	/**
749
	 * Rename/move a file or folder from the source path to target path.
750
	 *
751
	 * @param string $path1 source path
752
	 * @param string $path2 target path
753
	 *
754
	 * @return bool|mixed
755
	 * @throws LockedException
756
	 */
757
	public function rename($path1, $path2) {
758
		$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($path1));
759
		$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($path2));
760
		$result = false;
761
		if (
762
			Filesystem::isValidPath($path2)
763
			and Filesystem::isValidPath($path1)
764
			and !Filesystem::isFileBlacklisted($path2)
765
		) {
766
			$path1 = $this->getRelativePath($absolutePath1);
767
			$path2 = $this->getRelativePath($absolutePath2);
768
			$exists = $this->file_exists($path2);
769
770
			if ($path1 == null or $path2 == null) {
771
				return false;
772
			}
773
774
			$this->lockFile($path1, ILockingProvider::LOCK_SHARED, true);
775
			try {
776
				$this->lockFile($path2, ILockingProvider::LOCK_SHARED, true);
777
778
				$run = true;
779
				if ($this->shouldEmitHooks($path1) && (Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2))) {
780
					// if it was a rename from a part file to a regular file it was a write and not a rename operation
781
					$this->emit_file_hooks_pre($exists, $path2, $run);
782
				} elseif ($this->shouldEmitHooks($path1)) {
783
					\OC_Hook::emit(
784
						Filesystem::CLASSNAME, Filesystem::signal_rename,
785
						[
786
							Filesystem::signal_param_oldpath => $this->getHookPath($path1),
787
							Filesystem::signal_param_newpath => $this->getHookPath($path2),
788
							Filesystem::signal_param_run => &$run
789
						]
790
					);
791
				}
792
				if ($run) {
793
					$this->verifyPath(dirname($path2), basename($path2));
794
795
					$manager = Filesystem::getMountManager();
796
					$mount1 = $this->getMount($path1);
797
					$mount2 = $this->getMount($path2);
798
					$storage1 = $mount1->getStorage();
799
					$storage2 = $mount2->getStorage();
800
					$internalPath1 = $mount1->getInternalPath($absolutePath1);
801
					$internalPath2 = $mount2->getInternalPath($absolutePath2);
802
803
					$this->changeLock($path1, ILockingProvider::LOCK_EXCLUSIVE, true);
804
					try {
805
						$this->changeLock($path2, ILockingProvider::LOCK_EXCLUSIVE, true);
806
807
						if ($internalPath1 === '') {
808
							if ($mount1 instanceof MoveableMount) {
809
								$sourceParentMount = $this->getMount(dirname($path1));
810
								if ($sourceParentMount === $mount2 && $this->targetIsNotShared($storage2, $internalPath2)) {
811
									/**
812
									 * @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
813
									 */
814
									$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

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

815
									/** @scrutinizer ignore-call */ 
816
         $result = $mount1->moveMount($absolutePath2);
Loading history...
816
									$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
817
								} else {
818
									$result = false;
819
								}
820
							} else {
821
								$result = false;
822
							}
823
							// moving a file/folder within the same mount point
824
						} elseif ($storage1 === $storage2) {
825
							if ($storage1) {
826
								$result = $storage1->rename($internalPath1, $internalPath2);
827
							} else {
828
								$result = false;
829
							}
830
							// moving a file/folder between storages (from $storage1 to $storage2)
831
						} else {
832
							$result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
833
						}
834
835
						if ((Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2)) && $result !== false) {
836
							// if it was a rename from a part file to a regular file it was a write and not a rename operation
837
							$this->writeUpdate($storage2, $internalPath2);
838
						} elseif ($result) {
839
							if ($internalPath1 !== '') { // don't do a cache update for moved mounts
840
								$this->renameUpdate($storage1, $storage2, $internalPath1, $internalPath2);
841
							}
842
						}
843
					} catch (\Exception $e) {
844
						throw $e;
845
					} finally {
846
						$this->changeLock($path1, ILockingProvider::LOCK_SHARED, true);
847
						$this->changeLock($path2, ILockingProvider::LOCK_SHARED, true);
848
					}
849
850
					if ((Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2)) && $result !== false) {
851
						if ($this->shouldEmitHooks()) {
852
							$this->emit_file_hooks_post($exists, $path2);
853
						}
854
					} elseif ($result) {
855
						if ($this->shouldEmitHooks($path1) and $this->shouldEmitHooks($path2)) {
856
							\OC_Hook::emit(
857
								Filesystem::CLASSNAME,
858
								Filesystem::signal_post_rename,
859
								[
860
									Filesystem::signal_param_oldpath => $this->getHookPath($path1),
861
									Filesystem::signal_param_newpath => $this->getHookPath($path2)
862
								]
863
							);
864
						}
865
					}
866
				}
867
			} catch (\Exception $e) {
868
				throw $e;
869
			} finally {
870
				$this->unlockFile($path1, ILockingProvider::LOCK_SHARED, true);
871
				$this->unlockFile($path2, ILockingProvider::LOCK_SHARED, true);
872
			}
873
		}
874
		return $result;
875
	}
876
877
	/**
878
	 * Copy a file/folder from the source path to target path
879
	 *
880
	 * @param string $path1 source path
881
	 * @param string $path2 target path
882
	 * @param bool $preserveMtime whether to preserve mtime on the copy
883
	 *
884
	 * @return bool|mixed
885
	 */
886
	public function copy($path1, $path2, $preserveMtime = false) {
887
		$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($path1));
888
		$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($path2));
889
		$result = false;
890
		if (
891
			Filesystem::isValidPath($path2)
892
			and Filesystem::isValidPath($path1)
893
			and !Filesystem::isFileBlacklisted($path2)
894
		) {
895
			$path1 = $this->getRelativePath($absolutePath1);
896
			$path2 = $this->getRelativePath($absolutePath2);
897
898
			if ($path1 == null or $path2 == null) {
899
				return false;
900
			}
901
			$run = true;
902
903
			$this->lockFile($path2, ILockingProvider::LOCK_SHARED);
904
			$this->lockFile($path1, ILockingProvider::LOCK_SHARED);
905
			$lockTypePath1 = ILockingProvider::LOCK_SHARED;
906
			$lockTypePath2 = ILockingProvider::LOCK_SHARED;
907
908
			try {
909
				$exists = $this->file_exists($path2);
910
				if ($this->shouldEmitHooks()) {
911
					\OC_Hook::emit(
912
						Filesystem::CLASSNAME,
913
						Filesystem::signal_copy,
914
						[
915
							Filesystem::signal_param_oldpath => $this->getHookPath($path1),
916
							Filesystem::signal_param_newpath => $this->getHookPath($path2),
917
							Filesystem::signal_param_run => &$run
918
						]
919
					);
920
					$this->emit_file_hooks_pre($exists, $path2, $run);
921
				}
922
				if ($run) {
923
					$mount1 = $this->getMount($path1);
924
					$mount2 = $this->getMount($path2);
925
					$storage1 = $mount1->getStorage();
926
					$internalPath1 = $mount1->getInternalPath($absolutePath1);
927
					$storage2 = $mount2->getStorage();
928
					$internalPath2 = $mount2->getInternalPath($absolutePath2);
929
930
					$this->changeLock($path2, ILockingProvider::LOCK_EXCLUSIVE);
931
					$lockTypePath2 = ILockingProvider::LOCK_EXCLUSIVE;
932
933
					if ($mount1->getMountPoint() == $mount2->getMountPoint()) {
934
						if ($storage1) {
935
							$result = $storage1->copy($internalPath1, $internalPath2);
936
						} else {
937
							$result = false;
938
						}
939
					} else {
940
						$result = $storage2->copyFromStorage($storage1, $internalPath1, $internalPath2);
941
					}
942
943
					$this->writeUpdate($storage2, $internalPath2);
944
945
					$this->changeLock($path2, ILockingProvider::LOCK_SHARED);
946
					$lockTypePath2 = ILockingProvider::LOCK_SHARED;
947
948
					if ($this->shouldEmitHooks() && $result !== false) {
949
						\OC_Hook::emit(
950
							Filesystem::CLASSNAME,
951
							Filesystem::signal_post_copy,
952
							[
953
								Filesystem::signal_param_oldpath => $this->getHookPath($path1),
954
								Filesystem::signal_param_newpath => $this->getHookPath($path2)
955
							]
956
						);
957
						$this->emit_file_hooks_post($exists, $path2);
958
					}
959
				}
960
			} catch (\Exception $e) {
961
				$this->unlockFile($path2, $lockTypePath2);
962
				$this->unlockFile($path1, $lockTypePath1);
963
				throw $e;
964
			}
965
966
			$this->unlockFile($path2, $lockTypePath2);
967
			$this->unlockFile($path1, $lockTypePath1);
968
		}
969
		return $result;
970
	}
971
972
	/**
973
	 * @param string $path
974
	 * @param string $mode 'r' or 'w'
975
	 * @return resource
976
	 * @throws LockedException
977
	 */
978
	public function fopen($path, $mode) {
979
		$mode = str_replace('b', '', $mode); // the binary flag is a windows only feature which we do not support
980
		$hooks = [];
981
		switch ($mode) {
982
			case 'r':
983
				$hooks[] = 'read';
984
				break;
985
			case 'r+':
986
			case 'w+':
987
			case 'x+':
988
			case 'a+':
989
				$hooks[] = 'read';
990
				$hooks[] = 'write';
991
				break;
992
			case 'w':
993
			case 'x':
994
			case 'a':
995
				$hooks[] = 'write';
996
				break;
997
			default:
998
				\OCP\Util::writeLog('core', 'invalid mode (' . $mode . ') for ' . $path, ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

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

998
				\OCP\Util::writeLog('core', 'invalid mode (' . $mode . ') for ' . $path, /** @scrutinizer ignore-deprecated */ ILogger::ERROR);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
999
		}
1000
1001
		if ($mode !== 'r' && $mode !== 'w') {
1002
			\OC::$server->getLogger()->info('Trying to open a file with a mode other than "r" or "w" can cause severe performance issues with some backends');
1003
		}
1004
1005
		return $this->basicOperation('fopen', $path, $hooks, $mode);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->basicOpera..., $path, $hooks, $mode) also could return the type boolean which is incompatible with the documented return type resource.
Loading history...
1006
	}
1007
1008
	/**
1009
	 * @param string $path
1010
	 * @return bool|string
1011
	 * @throws \OCP\Files\InvalidPathException
1012
	 */
1013
	public function toTmpFile($path) {
1014
		$this->assertPathLength($path);
1015
		if (Filesystem::isValidPath($path)) {
1016
			$source = $this->fopen($path, 'r');
1017
			if ($source) {
0 ignored issues
show
introduced by
$source is of type resource, thus it always evaluated to false.
Loading history...
1018
				$extension = pathinfo($path, PATHINFO_EXTENSION);
1019
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($extension);
1020
				file_put_contents($tmpFile, $source);
1021
				return $tmpFile;
1022
			} else {
1023
				return false;
1024
			}
1025
		} else {
1026
			return false;
1027
		}
1028
	}
1029
1030
	/**
1031
	 * @param string $tmpFile
1032
	 * @param string $path
1033
	 * @return bool|mixed
1034
	 * @throws \OCP\Files\InvalidPathException
1035
	 */
1036
	public function fromTmpFile($tmpFile, $path) {
1037
		$this->assertPathLength($path);
1038
		if (Filesystem::isValidPath($path)) {
1039
1040
			// Get directory that the file is going into
1041
			$filePath = dirname($path);
1042
1043
			// Create the directories if any
1044
			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...
1045
				$result = $this->createParentDirectories($filePath);
1046
				if ($result === false) {
1047
					return false;
1048
				}
1049
			}
1050
1051
			$source = fopen($tmpFile, 'r');
1052
			if ($source) {
0 ignored issues
show
introduced by
$source is of type resource, thus it always evaluated to false.
Loading history...
1053
				$result = $this->file_put_contents($path, $source);
1054
				// $this->file_put_contents() might have already closed
1055
				// the resource, so we check it, before trying to close it
1056
				// to avoid messages in the error log.
1057
				if (is_resource($source)) {
1058
					fclose($source);
1059
				}
1060
				unlink($tmpFile);
1061
				return $result;
1062
			} else {
1063
				return false;
1064
			}
1065
		} else {
1066
			return false;
1067
		}
1068
	}
1069
1070
1071
	/**
1072
	 * @param string $path
1073
	 * @return mixed
1074
	 * @throws \OCP\Files\InvalidPathException
1075
	 */
1076
	public function getMimeType($path) {
1077
		$this->assertPathLength($path);
1078
		return $this->basicOperation('getMimeType', $path);
1079
	}
1080
1081
	/**
1082
	 * @param string $type
1083
	 * @param string $path
1084
	 * @param bool $raw
1085
	 * @return bool|string
1086
	 */
1087
	public function hash($type, $path, $raw = false) {
1088
		$postFix = (substr($path, -1) === '/') ? '/' : '';
1089
		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
1090
		if (Filesystem::isValidPath($path)) {
1091
			$path = $this->getRelativePath($absolutePath);
1092
			if ($path == null) {
1093
				return false;
1094
			}
1095
			if ($this->shouldEmitHooks($path)) {
1096
				\OC_Hook::emit(
1097
					Filesystem::CLASSNAME,
1098
					Filesystem::signal_read,
1099
					[Filesystem::signal_param_path => $this->getHookPath($path)]
1100
				);
1101
			}
1102
			/** @var Storage|null $storage */
1103
			[$storage, $internalPath] = Filesystem::resolvePath($absolutePath . $postFix);
1104
			if ($storage) {
0 ignored issues
show
introduced by
$storage is of type OC\Files\Storage\Storage, thus it always evaluated to true.
Loading history...
1105
				return $storage->hash($type, $internalPath, $raw);
1106
			}
1107
		}
1108
		return false;
1109
	}
1110
1111
	/**
1112
	 * @param string $path
1113
	 * @return mixed
1114
	 * @throws \OCP\Files\InvalidPathException
1115
	 */
1116
	public function free_space($path = '/') {
1117
		$this->assertPathLength($path);
1118
		$result = $this->basicOperation('free_space', $path);
1119
		if ($result === null) {
1120
			throw new InvalidPathException();
1121
		}
1122
		return $result;
1123
	}
1124
1125
	/**
1126
	 * abstraction layer for basic filesystem functions: wrapper for \OC\Files\Storage\Storage
1127
	 *
1128
	 * @param string $operation
1129
	 * @param string $path
1130
	 * @param array $hooks (optional)
1131
	 * @param mixed $extraParam (optional)
1132
	 * @return mixed
1133
	 * @throws LockedException
1134
	 *
1135
	 * This method takes requests for basic filesystem functions (e.g. reading & writing
1136
	 * files), processes hooks and proxies, sanitises paths, and finally passes them on to
1137
	 * \OC\Files\Storage\Storage for delegation to a storage backend for execution
1138
	 */
1139
	private function basicOperation($operation, $path, $hooks = [], $extraParam = null) {
1140
		$postFix = (substr($path, -1) === '/') ? '/' : '';
1141
		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
1142
		if (Filesystem::isValidPath($path)
1143
			and !Filesystem::isFileBlacklisted($path)
1144
		) {
1145
			$path = $this->getRelativePath($absolutePath);
1146
			if ($path == null) {
1147
				return false;
1148
			}
1149
1150
			if (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks)) {
1151
				// always a shared lock during pre-hooks so the hook can read the file
1152
				$this->lockFile($path, ILockingProvider::LOCK_SHARED);
1153
			}
1154
1155
			$run = $this->runHooks($hooks, $path);
1156
			/** @var \OC\Files\Storage\Storage $storage */
1157
			[$storage, $internalPath] = Filesystem::resolvePath($absolutePath . $postFix);
1158
			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...
1159
				if (in_array('write', $hooks) || in_array('delete', $hooks)) {
1160
					try {
1161
						$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
1162
					} catch (LockedException $e) {
1163
						// release the shared lock we acquired before quiting
1164
						$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1165
						throw $e;
1166
					}
1167
				}
1168
				try {
1169
					if (!is_null($extraParam)) {
1170
						$result = $storage->$operation($internalPath, $extraParam);
1171
					} else {
1172
						$result = $storage->$operation($internalPath);
1173
					}
1174
				} catch (\Exception $e) {
1175
					if (in_array('write', $hooks) || in_array('delete', $hooks)) {
1176
						$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
1177
					} elseif (in_array('read', $hooks)) {
1178
						$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1179
					}
1180
					throw $e;
1181
				}
1182
1183
				if ($result && in_array('delete', $hooks)) {
1184
					$this->removeUpdate($storage, $internalPath);
1185
				}
1186
				if ($result && in_array('write', $hooks, true) && $operation !== 'fopen' && $operation !== 'touch') {
1187
					$this->writeUpdate($storage, $internalPath);
1188
				}
1189
				if ($result && in_array('touch', $hooks)) {
1190
					$this->writeUpdate($storage, $internalPath, $extraParam);
1191
				}
1192
1193
				if ((in_array('write', $hooks) || in_array('delete', $hooks)) && ($operation !== 'fopen' || $result === false)) {
1194
					$this->changeLock($path, ILockingProvider::LOCK_SHARED);
1195
				}
1196
1197
				$unlockLater = false;
1198
				if ($this->lockingEnabled && $operation === 'fopen' && is_resource($result)) {
1199
					$unlockLater = true;
1200
					// make sure our unlocking callback will still be called if connection is aborted
1201
					ignore_user_abort(true);
1202
					$result = CallbackWrapper::wrap($result, null, null, function () use ($hooks, $path) {
1203
						if (in_array('write', $hooks)) {
1204
							$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
1205
						} elseif (in_array('read', $hooks)) {
1206
							$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1207
						}
1208
					});
1209
				}
1210
1211
				if ($this->shouldEmitHooks($path) && $result !== false) {
1212
					if ($operation != 'fopen') { //no post hooks for fopen, the file stream is still open
1213
						$this->runHooks($hooks, $path, true);
1214
					}
1215
				}
1216
1217
				if (!$unlockLater
1218
					&& (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks))
1219
				) {
1220
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1221
				}
1222
				return $result;
1223
			} else {
1224
				$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1225
			}
1226
		}
1227
		return null;
1228
	}
1229
1230
	/**
1231
	 * get the path relative to the default root for hook usage
1232
	 *
1233
	 * @param string $path
1234
	 * @return string
1235
	 */
1236
	private function getHookPath($path) {
1237
		if (!Filesystem::getView()) {
1238
			return $path;
1239
		}
1240
		return Filesystem::getView()->getRelativePath($this->getAbsolutePath($path));
1241
	}
1242
1243
	private function shouldEmitHooks($path = '') {
1244
		if ($path && Cache\Scanner::isPartialFile($path)) {
1245
			return false;
1246
		}
1247
		if (!Filesystem::$loaded) {
1248
			return false;
1249
		}
1250
		$defaultRoot = Filesystem::getRoot();
1251
		if ($defaultRoot === null) {
0 ignored issues
show
introduced by
The condition $defaultRoot === null is always false.
Loading history...
1252
			return false;
1253
		}
1254
		if ($this->fakeRoot === $defaultRoot) {
1255
			return true;
1256
		}
1257
		$fullPath = $this->getAbsolutePath($path);
1258
1259
		if ($fullPath === $defaultRoot) {
1260
			return true;
1261
		}
1262
1263
		return (strlen($fullPath) > strlen($defaultRoot)) && (substr($fullPath, 0, strlen($defaultRoot) + 1) === $defaultRoot . '/');
1264
	}
1265
1266
	/**
1267
	 * @param string[] $hooks
1268
	 * @param string $path
1269
	 * @param bool $post
1270
	 * @return bool
1271
	 */
1272
	private function runHooks($hooks, $path, $post = false) {
1273
		$relativePath = $path;
1274
		$path = $this->getHookPath($path);
1275
		$prefix = $post ? 'post_' : '';
1276
		$run = true;
1277
		if ($this->shouldEmitHooks($relativePath)) {
1278
			foreach ($hooks as $hook) {
1279
				if ($hook != 'read') {
1280
					\OC_Hook::emit(
1281
						Filesystem::CLASSNAME,
1282
						$prefix . $hook,
1283
						[
1284
							Filesystem::signal_param_run => &$run,
1285
							Filesystem::signal_param_path => $path
1286
						]
1287
					);
1288
				} elseif (!$post) {
1289
					\OC_Hook::emit(
1290
						Filesystem::CLASSNAME,
1291
						$prefix . $hook,
1292
						[
1293
							Filesystem::signal_param_path => $path
1294
						]
1295
					);
1296
				}
1297
			}
1298
		}
1299
		return $run;
1300
	}
1301
1302
	/**
1303
	 * check if a file or folder has been updated since $time
1304
	 *
1305
	 * @param string $path
1306
	 * @param int $time
1307
	 * @return bool
1308
	 */
1309
	public function hasUpdated($path, $time) {
1310
		return $this->basicOperation('hasUpdated', $path, [], $time);
1311
	}
1312
1313
	/**
1314
	 * @param string $ownerId
1315
	 * @return \OC\User\User
1316
	 */
1317
	private function getUserObjectForOwner($ownerId) {
1318
		$owner = $this->userManager->get($ownerId);
1319
		if ($owner instanceof IUser) {
1320
			return $owner;
1321
		} else {
1322
			return new User($ownerId, null, \OC::$server->getEventDispatcher());
1323
		}
1324
	}
1325
1326
	/**
1327
	 * Get file info from cache
1328
	 *
1329
	 * If the file is not in cached it will be scanned
1330
	 * If the file has changed on storage the cache will be updated
1331
	 *
1332
	 * @param \OC\Files\Storage\Storage $storage
1333
	 * @param string $internalPath
1334
	 * @param string $relativePath
1335
	 * @return ICacheEntry|bool
1336
	 */
1337
	private function getCacheEntry($storage, $internalPath, $relativePath) {
1338
		$cache = $storage->getCache($internalPath);
1339
		$data = $cache->get($internalPath);
1340
		$watcher = $storage->getWatcher($internalPath);
1341
1342
		try {
1343
			// if the file is not in the cache or needs to be updated, trigger the scanner and reload the data
1344
			if (!$data || (isset($data['size']) && $data['size'] === -1)) {
1345
				if (!$storage->file_exists($internalPath)) {
1346
					return false;
1347
				}
1348
				// don't need to get a lock here since the scanner does it's own locking
1349
				$scanner = $storage->getScanner($internalPath);
1350
				$scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW);
1351
				$data = $cache->get($internalPath);
1352
			} elseif (!Cache\Scanner::isPartialFile($internalPath) && $watcher->needsUpdate($internalPath, $data)) {
1353
				$this->lockFile($relativePath, ILockingProvider::LOCK_SHARED);
1354
				$watcher->update($internalPath, $data);
1355
				$storage->getPropagator()->propagateChange($internalPath, time());
1356
				$data = $cache->get($internalPath);
1357
				$this->unlockFile($relativePath, ILockingProvider::LOCK_SHARED);
1358
			}
1359
		} catch (LockedException $e) {
1360
			// if the file is locked we just use the old cache info
1361
		}
1362
1363
		return $data;
1364
	}
1365
1366
	/**
1367
	 * get the filesystem info
1368
	 *
1369
	 * @param string $path
1370
	 * @param boolean|string $includeMountPoints true to add mountpoint sizes,
1371
	 * 'ext' to add only ext storage mount point sizes. Defaults to true.
1372
	 * defaults to true
1373
	 * @return \OC\Files\FileInfo|false False if file does not exist
1374
	 */
1375
	public function getFileInfo($path, $includeMountPoints = true) {
1376
		$this->assertPathLength($path);
1377
		if (!Filesystem::isValidPath($path)) {
1378
			return false;
1379
		}
1380
		if (Cache\Scanner::isPartialFile($path)) {
1381
			return $this->getPartFileInfo($path);
1382
		}
1383
		$relativePath = $path;
1384
		$path = Filesystem::normalizePath($this->fakeRoot . '/' . $path);
1385
1386
		$mount = Filesystem::getMountManager()->find($path);
1387
		$storage = $mount->getStorage();
1388
		$internalPath = $mount->getInternalPath($path);
1389
		if ($storage) {
1390
			$data = $this->getCacheEntry($storage, $internalPath, $relativePath);
1391
1392
			if (!$data instanceof ICacheEntry) {
1393
				return false;
1394
			}
1395
1396
			if ($mount instanceof MoveableMount && $internalPath === '') {
1397
				$data['permissions'] |= \OCP\Constants::PERMISSION_DELETE;
1398
			}
1399
			$ownerId = $storage->getOwner($internalPath);
1400
			$owner = null;
1401
			if ($ownerId !== null && $ownerId !== false) {
1402
				// ownerId might be null if files are accessed with an access token without file system access
1403
				$owner = $this->getUserObjectForOwner($ownerId);
1404
			}
1405
			$info = new FileInfo($path, $storage, $internalPath, $data, $mount, $owner);
1406
1407
			if (isset($data['fileid'])) {
1408
				if ($includeMountPoints and $data['mimetype'] === 'httpd/unix-directory') {
1409
					//add the sizes of other mount points to the folder
1410
					$extOnly = ($includeMountPoints === 'ext');
1411
					$mounts = Filesystem::getMountManager()->findIn($path);
1412
					$info->setSubMounts(array_filter($mounts, function (IMountPoint $mount) use ($extOnly) {
1413
						$subStorage = $mount->getStorage();
1414
						return !($extOnly && $subStorage instanceof \OCA\Files_Sharing\SharedStorage);
1415
					}));
1416
				}
1417
			}
1418
1419
			return $info;
1420
		} else {
1421
			\OC::$server->getLogger()->warning('Storage not valid for mountpoint: ' . $mount->getMountPoint());
1422
		}
1423
1424
		return false;
1425
	}
1426
1427
	/**
1428
	 * get the content of a directory
1429
	 *
1430
	 * @param string $directory path under datadirectory
1431
	 * @param string $mimetype_filter limit returned content to this mimetype or mimepart
1432
	 * @return FileInfo[]
1433
	 */
1434
	public function getDirectoryContent($directory, $mimetype_filter = '', \OCP\Files\FileInfo $directoryInfo = null) {
1435
		$this->assertPathLength($directory);
1436
		if (!Filesystem::isValidPath($directory)) {
1437
			return [];
1438
		}
1439
1440
		$path = $this->getAbsolutePath($directory);
1441
		$path = Filesystem::normalizePath($path);
1442
		$mount = $this->getMount($directory);
1443
		$storage = $mount->getStorage();
1444
		$internalPath = $mount->getInternalPath($path);
1445
		if (!$storage) {
1446
			return [];
1447
		}
1448
1449
		$cache = $storage->getCache($internalPath);
1450
		$user = \OC_User::getUser();
1451
1452
		if (!$directoryInfo) {
1453
			$data = $this->getCacheEntry($storage, $internalPath, $directory);
1454
			if (!$data instanceof ICacheEntry || !isset($data['fileid'])) {
1455
				return [];
1456
			}
1457
		} else {
1458
			$data = $directoryInfo;
1459
		}
1460
1461
		if (!($data->getPermissions() & Constants::PERMISSION_READ)) {
1462
			return [];
1463
		}
1464
1465
		$folderId = $data->getId();
1466
		$contents = $cache->getFolderContentsById($folderId); //TODO: mimetype_filter
1467
1468
		$sharingDisabled = \OCP\Util::isSharingDisabledForUser();
1469
1470
		$fileNames = array_map(function (ICacheEntry $content) {
1471
			return $content->getName();
1472
		}, $contents);
1473
		/**
1474
		 * @var \OC\Files\FileInfo[] $fileInfos
1475
		 */
1476
		$fileInfos = array_map(function (ICacheEntry $content) use ($path, $storage, $mount, $sharingDisabled) {
1477
			if ($sharingDisabled) {
1478
				$content['permissions'] = $content['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
1479
			}
1480
			$owner = $this->getUserObjectForOwner($storage->getOwner($content['path']));
1481
			return new FileInfo($path . '/' . $content['name'], $storage, $content['path'], $content, $mount, $owner);
1482
		}, $contents);
1483
		$files = array_combine($fileNames, $fileInfos);
1484
1485
		//add a folder for any mountpoint in this directory and add the sizes of other mountpoints to the folders
1486
		$mounts = Filesystem::getMountManager()->findIn($path);
1487
		$dirLength = strlen($path);
1488
		foreach ($mounts as $mount) {
1489
			$mountPoint = $mount->getMountPoint();
1490
			$subStorage = $mount->getStorage();
1491
			if ($subStorage) {
1492
				$subCache = $subStorage->getCache('');
1493
1494
				$rootEntry = $subCache->get('');
1495
				if (!$rootEntry) {
1496
					$subScanner = $subStorage->getScanner();
1497
					try {
1498
						$subScanner->scanFile('');
1499
					} catch (\OCP\Files\StorageNotAvailableException $e) {
1500
						continue;
1501
					} catch (\OCP\Files\StorageInvalidException $e) {
1502
						continue;
1503
					} catch (\Exception $e) {
1504
						// sometimes when the storage is not available it can be any exception
1505
						\OC::$server->getLogger()->logException($e, [
1506
							'message' => 'Exception while scanning storage "' . $subStorage->getId() . '"',
1507
							'level' => ILogger::ERROR,
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

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

1507
							'level' => /** @scrutinizer ignore-deprecated */ ILogger::ERROR,

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1508
							'app' => 'lib',
1509
						]);
1510
						continue;
1511
					}
1512
					$rootEntry = $subCache->get('');
1513
				}
1514
1515
				if ($rootEntry && ($rootEntry->getPermissions() & Constants::PERMISSION_READ)) {
1516
					$relativePath = trim(substr($mountPoint, $dirLength), '/');
1517
					if ($pos = strpos($relativePath, '/')) {
1518
						//mountpoint inside subfolder add size to the correct folder
1519
						$entryName = substr($relativePath, 0, $pos);
1520
						foreach ($files as &$entry) {
1521
							if ($entry->getName() === $entryName) {
1522
								$entry->addSubEntry($rootEntry, $mountPoint);
1523
							}
1524
						}
1525
					} else { //mountpoint in this folder, add an entry for it
1526
						$rootEntry['name'] = $relativePath;
1527
						$rootEntry['type'] = $rootEntry['mimetype'] === 'httpd/unix-directory' ? 'dir' : 'file';
1528
						$permissions = $rootEntry['permissions'];
1529
						// do not allow renaming/deleting the mount point if they are not shared files/folders
1530
						// for shared files/folders we use the permissions given by the owner
1531
						if ($mount instanceof MoveableMount) {
1532
							$rootEntry['permissions'] = $permissions | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE;
1533
						} else {
1534
							$rootEntry['permissions'] = $permissions & (\OCP\Constants::PERMISSION_ALL - (\OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE));
1535
						}
1536
1537
						$rootEntry['path'] = substr(Filesystem::normalizePath($path . '/' . $rootEntry['name']), strlen($user) + 2); // full path without /$user/
1538
1539
						// if sharing was disabled for the user we remove the share permissions
1540
						if (\OCP\Util::isSharingDisabledForUser()) {
1541
							$rootEntry['permissions'] = $rootEntry['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
1542
						}
1543
1544
						$owner = $this->getUserObjectForOwner($subStorage->getOwner(''));
1545
						$files[$rootEntry->getName()] = new FileInfo($path . '/' . $rootEntry['name'], $subStorage, '', $rootEntry, $mount, $owner);
1546
					}
1547
				}
1548
			}
1549
		}
1550
1551
		if ($mimetype_filter) {
1552
			$files = array_filter($files, function (FileInfo $file) use ($mimetype_filter) {
1553
				if (strpos($mimetype_filter, '/')) {
1554
					return $file->getMimetype() === $mimetype_filter;
1555
				} else {
1556
					return $file->getMimePart() === $mimetype_filter;
1557
				}
1558
			});
1559
		}
1560
1561
		return array_values($files);
1562
	}
1563
1564
	/**
1565
	 * change file metadata
1566
	 *
1567
	 * @param string $path
1568
	 * @param array|\OCP\Files\FileInfo $data
1569
	 * @return int
1570
	 *
1571
	 * returns the fileid of the updated file
1572
	 */
1573
	public function putFileInfo($path, $data) {
1574
		$this->assertPathLength($path);
1575
		if ($data instanceof FileInfo) {
1576
			$data = $data->getData();
1577
		}
1578
		$path = Filesystem::normalizePath($this->fakeRoot . '/' . $path);
1579
		/**
1580
		 * @var \OC\Files\Storage\Storage $storage
1581
		 * @var string $internalPath
1582
		 */
1583
		[$storage, $internalPath] = Filesystem::resolvePath($path);
1584
		if ($storage) {
0 ignored issues
show
introduced by
$storage is of type OC\Files\Storage\Storage, thus it always evaluated to true.
Loading history...
1585
			$cache = $storage->getCache($path);
1586
1587
			if (!$cache->inCache($internalPath)) {
1588
				$scanner = $storage->getScanner($internalPath);
1589
				$scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW);
1590
			}
1591
1592
			return $cache->put($internalPath, $data);
1593
		} else {
1594
			return -1;
1595
		}
1596
	}
1597
1598
	/**
1599
	 * search for files with the name matching $query
1600
	 *
1601
	 * @param string $query
1602
	 * @return FileInfo[]
1603
	 */
1604
	public function search($query) {
1605
		return $this->searchCommon('search', ['%' . $query . '%']);
1606
	}
1607
1608
	/**
1609
	 * search for files with the name matching $query
1610
	 *
1611
	 * @param string $query
1612
	 * @return FileInfo[]
1613
	 */
1614
	public function searchRaw($query) {
1615
		return $this->searchCommon('search', [$query]);
1616
	}
1617
1618
	/**
1619
	 * search for files by mimetype
1620
	 *
1621
	 * @param string $mimetype
1622
	 * @return FileInfo[]
1623
	 */
1624
	public function searchByMime($mimetype) {
1625
		return $this->searchCommon('searchByMime', [$mimetype]);
1626
	}
1627
1628
	/**
1629
	 * search for files by tag
1630
	 *
1631
	 * @param string|int $tag name or tag id
1632
	 * @param string $userId owner of the tags
1633
	 * @return FileInfo[]
1634
	 */
1635
	public function searchByTag($tag, $userId) {
1636
		return $this->searchCommon('searchByTag', [$tag, $userId]);
1637
	}
1638
1639
	/**
1640
	 * @param string $method cache method
1641
	 * @param array $args
1642
	 * @return FileInfo[]
1643
	 */
1644
	private function searchCommon($method, $args) {
1645
		$files = [];
1646
		$rootLength = strlen($this->fakeRoot);
1647
1648
		$mount = $this->getMount('');
1649
		$mountPoint = $mount->getMountPoint();
1650
		$storage = $mount->getStorage();
1651
		$userManager = \OC::$server->getUserManager();
1652
		if ($storage) {
1653
			$cache = $storage->getCache('');
1654
1655
			$results = call_user_func_array([$cache, $method], $args);
1656
			foreach ($results as $result) {
1657
				if (substr($mountPoint . $result['path'], 0, $rootLength + 1) === $this->fakeRoot . '/') {
1658
					$internalPath = $result['path'];
1659
					$path = $mountPoint . $result['path'];
1660
					$result['path'] = substr($mountPoint . $result['path'], $rootLength);
1661
					$owner = $userManager->get($storage->getOwner($internalPath));
1662
					$files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner);
1663
				}
1664
			}
1665
1666
			$mounts = Filesystem::getMountManager()->findIn($this->fakeRoot);
1667
			foreach ($mounts as $mount) {
1668
				$mountPoint = $mount->getMountPoint();
1669
				$storage = $mount->getStorage();
1670
				if ($storage) {
1671
					$cache = $storage->getCache('');
1672
1673
					$relativeMountPoint = substr($mountPoint, $rootLength);
1674
					$results = call_user_func_array([$cache, $method], $args);
1675
					if ($results) {
1676
						foreach ($results as $result) {
1677
							$internalPath = $result['path'];
1678
							$result['path'] = rtrim($relativeMountPoint . $result['path'], '/');
1679
							$path = rtrim($mountPoint . $internalPath, '/');
1680
							$owner = $userManager->get($storage->getOwner($internalPath));
1681
							$files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner);
1682
						}
1683
					}
1684
				}
1685
			}
1686
		}
1687
		return $files;
1688
	}
1689
1690
	/**
1691
	 * Get the owner for a file or folder
1692
	 *
1693
	 * @param string $path
1694
	 * @return string the user id of the owner
1695
	 * @throws NotFoundException
1696
	 */
1697
	public function getOwner($path) {
1698
		$info = $this->getFileInfo($path);
1699
		if (!$info) {
1700
			throw new NotFoundException($path . ' not found while trying to get owner');
1701
		}
1702
1703
		if ($info->getOwner() === null) {
1704
			throw new NotFoundException($path . ' has no owner');
1705
		}
1706
1707
		return $info->getOwner()->getUID();
1708
	}
1709
1710
	/**
1711
	 * get the ETag for a file or folder
1712
	 *
1713
	 * @param string $path
1714
	 * @return string
1715
	 */
1716
	public function getETag($path) {
1717
		/**
1718
		 * @var Storage\Storage $storage
1719
		 * @var string $internalPath
1720
		 */
1721
		[$storage, $internalPath] = $this->resolvePath($path);
1722
		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...
1723
			return $storage->getETag($internalPath);
1724
		} else {
1725
			return null;
1726
		}
1727
	}
1728
1729
	/**
1730
	 * Get the path of a file by id, relative to the view
1731
	 *
1732
	 * Note that the resulting path is not guarantied to be unique for the id, multiple paths can point to the same file
1733
	 *
1734
	 * @param int $id
1735
	 * @param int|null $storageId
1736
	 * @return string
1737
	 * @throws NotFoundException
1738
	 */
1739
	public function getPath($id, int $storageId = null) {
1740
		$id = (int)$id;
1741
		$manager = Filesystem::getMountManager();
1742
		$mounts = $manager->findIn($this->fakeRoot);
1743
		$mounts[] = $manager->find($this->fakeRoot);
1744
		$mounts = array_filter($mounts);
1745
		// reverse the array, so we start with the storage this view is in
1746
		// which is the most likely to contain the file we're looking for
1747
		$mounts = array_reverse($mounts);
1748
1749
		// put non-shared mounts in front of the shared mount
1750
		// this prevents unneeded recursion into shares
1751
		usort($mounts, function (IMountPoint $a, IMountPoint $b) {
1752
			return $a instanceof SharedMount && (!$b instanceof SharedMount) ? 1 : -1;
1753
		});
1754
1755
		if (!is_null($storageId)) {
1756
			$mounts = array_filter($mounts, function (IMountPoint $mount) use ($storageId) {
1757
				return $mount->getNumericStorageId() === $storageId;
1758
			});
1759
		}
1760
1761
		foreach ($mounts as $mount) {
1762
			/**
1763
			 * @var \OC\Files\Mount\MountPoint $mount
1764
			 */
1765
			if ($mount->getStorage()) {
1766
				$cache = $mount->getStorage()->getCache();
1767
				$internalPath = $cache->getPathById($id);
1768
				if (is_string($internalPath)) {
1769
					$fullPath = $mount->getMountPoint() . $internalPath;
1770
					if (!is_null($path = $this->getRelativePath($fullPath))) {
1771
						return $path;
1772
					}
1773
				}
1774
			}
1775
		}
1776
		throw new NotFoundException(sprintf('File with id "%s" has not been found.', $id));
1777
	}
1778
1779
	/**
1780
	 * @param string $path
1781
	 * @throws InvalidPathException
1782
	 */
1783
	private function assertPathLength($path) {
1784
		$maxLen = min(PHP_MAXPATHLEN, 4000);
1785
		// Check for the string length - performed using isset() instead of strlen()
1786
		// because isset() is about 5x-40x faster.
1787
		if (isset($path[$maxLen])) {
1788
			$pathLen = strlen($path);
1789
			throw new \OCP\Files\InvalidPathException("Path length($pathLen) exceeds max path length($maxLen): $path");
1790
		}
1791
	}
1792
1793
	/**
1794
	 * check if it is allowed to move a mount point to a given target.
1795
	 * It is not allowed to move a mount point into a different mount point or
1796
	 * into an already shared folder
1797
	 *
1798
	 * @param IStorage $targetStorage
1799
	 * @param string $targetInternalPath
1800
	 * @return boolean
1801
	 */
1802
	private function targetIsNotShared(IStorage $targetStorage, string $targetInternalPath) {
1803
1804
		// note: cannot use the view because the target is already locked
1805
		$fileId = (int)$targetStorage->getCache()->getId($targetInternalPath);
1806
		if ($fileId === -1) {
1807
			// target might not exist, need to check parent instead
1808
			$fileId = (int)$targetStorage->getCache()->getId(dirname($targetInternalPath));
1809
		}
1810
1811
		// check if any of the parents were shared by the current owner (include collections)
1812
		$shares = \OCP\Share::getItemShared(
1813
			'folder',
1814
			$fileId,
1815
			\OCP\Share::FORMAT_NONE,
1816
			null,
1817
			true
1818
		);
1819
1820
		if (count($shares) > 0) {
1821
			\OCP\Util::writeLog('files',
1822
				'It is not allowed to move one mount point into a shared folder',
1823
				ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::DEBUG has been deprecated: 20.0.0 ( Ignorable by Annotation )

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

1823
				/** @scrutinizer ignore-deprecated */ ILogger::DEBUG);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1824
			return false;
1825
		}
1826
1827
		return true;
1828
	}
1829
1830
	/**
1831
	 * Get a fileinfo object for files that are ignored in the cache (part files)
1832
	 *
1833
	 * @param string $path
1834
	 * @return \OCP\Files\FileInfo
1835
	 */
1836
	private function getPartFileInfo($path) {
1837
		$mount = $this->getMount($path);
1838
		$storage = $mount->getStorage();
1839
		$internalPath = $mount->getInternalPath($this->getAbsolutePath($path));
1840
		$owner = \OC::$server->getUserManager()->get($storage->getOwner($internalPath));
1841
		return new FileInfo(
1842
			$this->getAbsolutePath($path),
1843
			$storage,
1844
			$internalPath,
1845
			[
1846
				'fileid' => null,
1847
				'mimetype' => $storage->getMimeType($internalPath),
1848
				'name' => basename($path),
1849
				'etag' => null,
1850
				'size' => $storage->filesize($internalPath),
1851
				'mtime' => $storage->filemtime($internalPath),
1852
				'encrypted' => false,
1853
				'permissions' => \OCP\Constants::PERMISSION_ALL
1854
			],
1855
			$mount,
1856
			$owner
1857
		);
1858
	}
1859
1860
	/**
1861
	 * @param string $path
1862
	 * @param string $fileName
1863
	 * @throws InvalidPathException
1864
	 */
1865
	public function verifyPath($path, $fileName) {
1866
		try {
1867
			/** @type \OCP\Files\Storage $storage */
1868
			[$storage, $internalPath] = $this->resolvePath($path);
1869
			$storage->verifyPath($internalPath, $fileName);
1870
		} catch (ReservedWordException $ex) {
1871
			$l = \OC::$server->getL10N('lib');
1872
			throw new InvalidPathException($l->t('File name is a reserved word'));
1873
		} catch (InvalidCharacterInPathException $ex) {
1874
			$l = \OC::$server->getL10N('lib');
1875
			throw new InvalidPathException($l->t('File name contains at least one invalid character'));
1876
		} catch (FileNameTooLongException $ex) {
1877
			$l = \OC::$server->getL10N('lib');
1878
			throw new InvalidPathException($l->t('File name is too long'));
1879
		} catch (InvalidDirectoryException $ex) {
1880
			$l = \OC::$server->getL10N('lib');
1881
			throw new InvalidPathException($l->t('Dot files are not allowed'));
1882
		} catch (EmptyFileNameException $ex) {
1883
			$l = \OC::$server->getL10N('lib');
1884
			throw new InvalidPathException($l->t('Empty filename is not allowed'));
1885
		}
1886
	}
1887
1888
	/**
1889
	 * get all parent folders of $path
1890
	 *
1891
	 * @param string $path
1892
	 * @return string[]
1893
	 */
1894
	private function getParents($path) {
1895
		$path = trim($path, '/');
1896
		if (!$path) {
1897
			return [];
1898
		}
1899
1900
		$parts = explode('/', $path);
1901
1902
		// remove the single file
1903
		array_pop($parts);
1904
		$result = ['/'];
1905
		$resultPath = '';
1906
		foreach ($parts as $part) {
1907
			if ($part) {
1908
				$resultPath .= '/' . $part;
1909
				$result[] = $resultPath;
1910
			}
1911
		}
1912
		return $result;
1913
	}
1914
1915
	/**
1916
	 * Returns the mount point for which to lock
1917
	 *
1918
	 * @param string $absolutePath absolute path
1919
	 * @param bool $useParentMount true to return parent mount instead of whatever
1920
	 * is mounted directly on the given path, false otherwise
1921
	 * @return IMountPoint mount point for which to apply locks
1922
	 */
1923
	private function getMountForLock($absolutePath, $useParentMount = false) {
1924
		$mount = Filesystem::getMountManager()->find($absolutePath);
1925
1926
		if ($useParentMount) {
1927
			// find out if something is mounted directly on the path
1928
			$internalPath = $mount->getInternalPath($absolutePath);
1929
			if ($internalPath === '') {
1930
				// resolve the parent mount instead
1931
				$mount = Filesystem::getMountManager()->find(dirname($absolutePath));
1932
			}
1933
		}
1934
1935
		return $mount;
1936
	}
1937
1938
	/**
1939
	 * Lock the given path
1940
	 *
1941
	 * @param string $path the path of the file to lock, relative to the view
1942
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
1943
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
1944
	 *
1945
	 * @return bool False if the path is excluded from locking, true otherwise
1946
	 * @throws LockedException if the path is already locked
1947
	 */
1948
	private function lockPath($path, $type, $lockMountPoint = false) {
1949
		$absolutePath = $this->getAbsolutePath($path);
1950
		$absolutePath = Filesystem::normalizePath($absolutePath);
1951
		if (!$this->shouldLockFile($absolutePath)) {
1952
			return false;
1953
		}
1954
1955
		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
1956
		if ($mount) {
0 ignored issues
show
introduced by
$mount is of type OCP\Files\Mount\IMountPoint, thus it always evaluated to true.
Loading history...
1957
			try {
1958
				$storage = $mount->getStorage();
1959
				if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
1960
					$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\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

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

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

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