Passed
Push — master ( 2b854c...ca958d )
by Blizzz
32:19 queued 19:48
created

View::copy()   C

Complexity

Conditions 13
Paths 70

Size

Total Lines 84
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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