Completed
Pull Request — master (#32044)
by Thomas
12:01
created

View::fopen()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
357
			}
358
			if ($this->is_dir($path)) {
359
				$result = $this->basicOperation('rmdir', $path, ['delete']);
360
			} else {
361
				$result = false;
362
			}
363
364 View Code Duplication
			if (!$result && !$this->file_exists($path)) { //clear ghost files from the cache on delete
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
365
				$storage = $mount->getStorage();
366
				$internalPath = $mount->getInternalPath($absolutePath);
367
				$storage->getUpdater()->remove($internalPath);
368
			}
369
370
			return $result;
371
		}, ['before' => ['path' => $this->getAbsolutePath($path)], 'after' => ['path' => $this->getAbsolutePath($path)]], 'file', 'delete');
372
	}
373
374
	/**
375
	 * @param string $path
376
	 * @return resource
377
	 */
378
	public function opendir($path) {
379
		return $this->basicOperation('opendir', $path, ['read']);
380
	}
381
382
	/**
383
	 * @param string $path
384
	 * @return bool|mixed
385
	 */
386
	public function is_dir($path) {
387
		if ($path == '/') {
388
			return true;
389
		}
390
		return $this->basicOperation('is_dir', $path);
391
	}
392
393
	/**
394
	 * @param string $path
395
	 * @return bool|mixed
396
	 */
397
	public function is_file($path) {
398
		if ($path == '/') {
399
			return false;
400
		}
401
		return $this->basicOperation('is_file', $path);
402
	}
403
404
	/**
405
	 * @param string $path
406
	 * @return mixed
407
	 */
408
	public function stat($path) {
409
		return $this->basicOperation('stat', $path);
410
	}
411
412
	/**
413
	 * @param string $path
414
	 * @return mixed
415
	 */
416
	public function filetype($path) {
417
		return $this->basicOperation('filetype', $path);
418
	}
419
420
	/**
421
	 * @param string $path
422
	 * @return mixed
423
	 */
424
	public function filesize($path) {
425
		return $this->basicOperation('filesize', $path);
426
	}
427
428
	/**
429
	 * @param string $path
430
	 * @return bool|mixed
431
	 * @throws \OCP\Files\InvalidPathException
432
	 */
433
	public function readFileToOutput($path) {
434
		$this->assertPathLength($path);
435
		@\ob_end_clean();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
436
		$handle = $this->readFile($path)->detach();
437
		if ($handle) {
438
			$chunkSize = 8192; // 8 kB chunks
439
			while (!\feof($handle)) {
440
				echo \fread($handle, $chunkSize);
441
				\flush();
442
			}
443
			$size = $this->filesize($path);
444
			return $size;
445
		}
446
		return false;
447
	}
448
449
	/**
450
	 * @param string $path
451
	 * @param int $from
452
	 * @param int $to
453
	 * @return bool|mixed
454
	 * @throws \OCP\Files\InvalidPathException
455
	 * @throws \OCP\Files\UnseekableException
456
	 */
457
	public function readfilePart($path, $from, $to) {
458
		$this->assertPathLength($path);
459
		@\ob_end_clean();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
460
		$handle = $this->readFile($path)->detach();
461
		if ($handle) {
462
			if (\fseek($handle, $from) === 0) {
463
				$chunkSize = 8192; // 8 kB chunks
464
				$end = $to + 1;
465
				while (!\feof($handle) && \ftell($handle) < $end) {
466
					$len = $end - \ftell($handle);
467
					if ($len > $chunkSize) {
468
						$len = $chunkSize;
469
					}
470
					echo \fread($handle, $len);
471
					\flush();
472
				}
473
				return \ftell($handle) - $from;
474
			}
475
476
			throw new \OCP\Files\UnseekableException('fseek error');
477
		}
478
		return false;
479
	}
480
481
	/**
482
	 * @param string $path
483
	 * @return mixed
484
	 */
485
	public function isCreatable($path) {
486
		return $this->basicOperation('isCreatable', $path);
487
	}
488
489
	/**
490
	 * @param string $path
491
	 * @return mixed
492
	 */
493
	public function isReadable($path) {
494
		return $this->basicOperation('isReadable', $path);
495
	}
496
497
	/**
498
	 * @param string $path
499
	 * @return mixed
500
	 */
501
	public function isUpdatable($path) {
502
		return $this->basicOperation('isUpdatable', $path);
503
	}
504
505
	/**
506
	 * @param string $path
507
	 * @return bool|mixed
508
	 */
509
	public function isDeletable($path) {
510
		$absolutePath = $this->getAbsolutePath($path);
511
		$mount = Filesystem::getMountManager()->find($absolutePath);
512
		if ($mount->getInternalPath($absolutePath) === '') {
513
			return $mount instanceof MoveableMount;
514
		}
515
		return $this->basicOperation('isDeletable', $path);
516
	}
517
518
	/**
519
	 * @param string $path
520
	 * @return mixed
521
	 */
522
	public function isSharable($path) {
523
		return $this->basicOperation('isSharable', $path);
524
	}
525
526
	/**
527
	 * @param string $path
528
	 * @return bool|mixed
529
	 */
530
	public function file_exists($path) {
531
		if ($path == '/') {
532
			return true;
533
		}
534
		return $this->basicOperation('file_exists', $path);
535
	}
536
537
	/**
538
	 * @param string $path
539
	 * @return mixed
540
	 */
541
	public function filemtime($path) {
542
		return $this->basicOperation('filemtime', $path);
543
	}
544
545
	/**
546
	 * @param string $path
547
	 * @param int|string $mtime
548
	 * @return bool
549
	 */
550
	public function touch($path, $mtime = null) {
551
		if ($mtime !== null and !\is_numeric($mtime)) {
552
			$mtime = \strtotime($mtime);
553
		}
554
555
		$hooks = ['touch'];
556
557
		if (!$this->file_exists($path)) {
558
			$hooks[] = 'create';
559
			$hooks[] = 'write';
560
		}
561
		$result = $this->basicOperation('touch', $path, $hooks, $mtime);
562
		if (!$result) {
563
			// If create file fails because of permissions on external storage like SMB folders,
564
			// check file exists and return false if not.
565
			if (!$this->file_exists($path)) {
566
				return false;
567
			}
568
			if ($mtime === null) {
569
				$mtime = \time();
570
			}
571
			//if native touch fails, we emulate it by changing the mtime in the cache
572
			$this->putFileInfo($path, ['mtime' => $mtime]);
573
		}
574
		return true;
575
	}
576
577
	/**
578
	 * @param string $path
579
	 * @return mixed
580
	 */
581
	public function file_get_contents($path) {
582
		return $this->emittingCall(function () use (&$path) {
583
			return $this->basicOperation('file_get_contents', $path, ['read']);
584
		}, ['before' => ['path' => $this->getAbsolutePath($path)], 'after' => ['path' => $this->getAbsolutePath($path)]], 'file', 'read');
585
	}
586
587
	public function readFile(string $path, array $options = []): StreamInterface {
588
		return $this->emittingCall(function () use ($path, $options) {
589
			return $this->basicOperation('readFile', $path, ['read'], $options);
590
		}, ['before' => ['path' => $this->getAbsolutePath($path)], 'after' => ['path' => $this->getAbsolutePath($path)]], 'file', 'read');
591
	}
592
593
	/**
594
	 * @param bool $exists
595
	 * @param string $path
596
	 * @param bool $run
597
	 */
598
	protected function emit_file_hooks_pre($exists, $path, &$run) {
599
		$event = new GenericEvent(null);
600
		if (!$exists) {
601
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
602
				Filesystem::signal_param_path => $this->getHookPath($path),
603
				Filesystem::signal_param_run => &$run,
604
			]);
605 View Code Duplication
			if ($run) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
606
				$event->setArgument('run', $run);
607
				$this->eventDispatcher->dispatch('file.beforeCreate', $event);
608
				if ($event->getArgument('run') === false) {
609
					$run = $event->getArgument('run');
610
				}
611
			}
612
		} else {
613
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
614
				Filesystem::signal_param_path => $this->getHookPath($path),
615
				Filesystem::signal_param_run => &$run,
616
			]);
617 View Code Duplication
			if ($run) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
618
				$event->setArgument('run', $run);
619
				$this->eventDispatcher->dispatch('file.beforeUpdate', $event);
620
				if ($event->getArgument('run') === false) {
621
					$run = $event->getArgument('run');
622
				}
623
			}
624
		}
625
626
		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
627
			Filesystem::signal_param_path => $this->getHookPath($path),
628
			Filesystem::signal_param_run => &$run,
629
		]);
630 View Code Duplication
		if ($run) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
631
			$event->setArgument('run', $run);
632
			$this->eventDispatcher->dispatch('file.beforeWrite', $event);
633
			if ($event->getArgument('run') === false) {
634
				$run = $event->getArgument('run');
635
			}
636
		}
637
	}
638
639
	/**
640
	 * @param bool $exists
641
	 * @param string $path
642
	 */
643
	protected function emit_file_hooks_post($exists, $path) {
644
		// A post event so no before event args required
645
		return $this->emittingCall(function () use (&$exists, &$path) {
646 View Code Duplication
			if (!$exists) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
647
				\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
648
					Filesystem::signal_param_path => $this->getHookPath($path),
649
				]);
650
			} else {
651
				\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
652
					Filesystem::signal_param_path => $this->getHookPath($path),
653
				]);
654
			}
655
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
656
				Filesystem::signal_param_path => $this->getHookPath($path),
657
			]);
658
		}, ['before' => ['path' => $path], 'after' => ['path' => $this->getAbsolutePath($path)]], 'file', 'create');
659
	}
660
661
	/**
662
	 * @param string $path
663
	 * @param mixed $data
664
	 * @return bool|mixed
665
	 * @throws \Exception
666
	 */
667
	public function file_put_contents($path, $data) {
668
		return $this->emittingCall(function () use (&$path, &$data) {
669
			if (\is_resource($data)) { //not having to deal with streams in file_put_contents makes life easier
670
				$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
671
				if (Filesystem::isValidPath($path)
672
					and !Filesystem::isForbiddenFileOrDir($path)
673
				) {
674
					$path = $this->getRelativePath($absolutePath);
675
676
					$this->lockFile($path, ILockingProvider::LOCK_SHARED);
677
678
					$exists = $this->file_exists($path);
679
					$run = true;
680
					if ($this->shouldEmitHooks($path)) {
681
						$this->emit_file_hooks_pre($exists, $path, $run);
682
					}
683
					if (!$run) {
684
						$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
685
						return false;
686
					}
687
688
					$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
689
690
					/** @var \OC\Files\Storage\Storage $storage */
691
					list($storage, $internalPath) = $this->resolvePath($path);
692
					$result = $storage->writeFile($internalPath, stream_for($data)) >= 0;
693
					if (\is_resource($data)) {
694
						\fclose($data);
695
					}
696
697
					$this->writeUpdate($storage, $internalPath);
698
699
					$this->changeLock($path, ILockingProvider::LOCK_SHARED);
700
701
					if ($this->shouldEmitHooks($path) && $result !== false) {
702
						$this->emit_file_hooks_post($exists, $path);
703
					}
704
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
705
					return $result;
706
				} else {
707
					return false;
708
				}
709
			} else {
710
				$hooks = ($this->file_exists($path)) ? ['update', 'write'] : ['create', 'write'];
711
				return $this->basicOperation('file_put_contents', $path, $hooks, $data);
712
			}
713
		}, ['before' => ['path' => $this->getAbsolutePath($path)], 'after' => ['path' => $this->getAbsolutePath($path)]], 'file', 'update');
714
	}
715
716
	public function writeFile(string $path, StreamInterface $stream): int {
717
		return $this->file_put_contents($path, $stream->detach());
718
	}
719
720
	/**
721
	 * @param string $path
722
	 * @return bool|mixed
723
	 */
724
	public function unlink($path) {
725
		return $this->emittingCall(function () use (&$path) {
726
			if ($path === '' || $path === '/') {
727
				// do not allow deleting the root
728
				return false;
729
			}
730
			$postFix = (\substr($path, -1, 1) === '/') ? '/' : '';
731
			$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
732
			$mount = Filesystem::getMountManager()->find($absolutePath . $postFix);
733
			if ($mount and $mount->getInternalPath($absolutePath) === '') {
734
				return $this->removeMount($mount, $absolutePath);
0 ignored issues
show
Documentation introduced by
$mount is of type object<OC\Files\Mount\MountPoint>, but the function expects a object<OC\Files\Mount\MoveableMount>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
735
			}
736
			if ($this->is_dir($path)) {
737
				$result = $this->basicOperation('rmdir', $path, ['delete']);
738
			} else {
739
				$result = $this->basicOperation('unlink', $path, ['delete']);
740
			}
741
742 View Code Duplication
			if (!$result && !$this->file_exists($path)) { //clear ghost files from the cache on delete
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
743
				$storage = $mount->getStorage();
744
				$internalPath = $mount->getInternalPath($absolutePath);
745
				$storage->getUpdater()->remove($internalPath);
746
				return true;
747
			} else {
748
				return $result;
749
			}
750
		}, ['before' => ['path' => $this->getAbsolutePath($path)], 'after' => ['path' => $this->getAbsolutePath($path)]], 'file', 'delete');
751
	}
752
753
	/**
754
	 * @param string $directory
755
	 * @return bool|mixed
756
	 */
757
	public function deleteAll($directory) {
758
		return $this->rmdir($directory);
759
	}
760
761
	/**
762
	 * Rename/move a file or folder from the source path to target path.
763
	 *
764
	 * @param string $path1 source path
765
	 * @param string $path2 target path
766
	 *
767
	 * @return bool|mixed
768
	 */
769
	public function rename($path1, $path2) {
770
		return $this->emittingCall(function () use (&$path1, &$path2) {
771
			$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($path1));
772
			$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($path2));
773
			$result = false;
774
			if (
775
				Filesystem::isValidPath($path2)
776
				and Filesystem::isValidPath($path1)
777
				and !Filesystem::isForbiddenFileOrDir($path2)
778
			) {
779
				$path1 = $this->getRelativePath($absolutePath1);
780
				$path2 = $this->getRelativePath($absolutePath2);
781
				$exists = $this->file_exists($path2);
782
783
				if ($path1 == null or $path2 == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $path1 of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
Bug introduced by
It seems like you are loosely comparing $path2 of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
784
					return false;
785
				}
786
787
				$this->lockFile($path1, ILockingProvider::LOCK_SHARED, true);
788
				try {
789
					$this->lockFile($path2, ILockingProvider::LOCK_SHARED, true);
790
				} catch (LockedException $e) {
791
					$this->unlockFile($path1, ILockingProvider::LOCK_SHARED);
792
					throw $e;
793
				}
794
795
				$run = true;
796
				if ($this->shouldEmitHooks($path1) && (Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2))) {
797
					// if it was a rename from a part file to a regular file it was a write and not a rename operation
798
					$this->emit_file_hooks_pre($exists, $path2, $run);
799
				} elseif ($this->shouldEmitHooks($path1)) {
800
					\OC_Hook::emit(
801
						Filesystem::CLASSNAME, Filesystem::signal_rename,
802
						[
803
							Filesystem::signal_param_oldpath => $this->getHookPath($path1),
804
							Filesystem::signal_param_newpath => $this->getHookPath($path2),
805
							Filesystem::signal_param_run => &$run
806
						]
807
					);
808
				}
809
				if ($run) {
810
					$this->verifyPath(\dirname($path2), \basename($path2));
811
812
					$manager = Filesystem::getMountManager();
813
					$mount1 = $this->getMount($path1);
814
					$mount2 = $this->getMount($path2);
815
					$storage1 = $mount1->getStorage();
816
					$storage2 = $mount2->getStorage();
817
					$internalPath1 = $mount1->getInternalPath($absolutePath1);
818
					$internalPath2 = $mount2->getInternalPath($absolutePath2);
819
820
					$this->changeLock($path1, ILockingProvider::LOCK_EXCLUSIVE, true);
821
					$this->changeLock($path2, ILockingProvider::LOCK_EXCLUSIVE, true);
822
823
					if ($internalPath1 === '' and $mount1 instanceof MoveableMount) {
824
						if ($this->canMove($mount1, $absolutePath2)) {
825
							/**
826
							 * @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
827
							 */
828
							$sourceMountPoint = $mount1->getMountPoint();
829
							$result = $mount1->moveMount($absolutePath2);
830
							$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
831
						} else {
832
							$result = false;
833
						}
834
						// moving a file/folder within the same mount point
835
					} elseif ($storage1 === $storage2) {
836
						if ($storage1) {
837
							$result = $storage1->rename($internalPath1, $internalPath2);
838
						} else {
839
							$result = false;
840
						}
841
						// moving a file/folder between storages (from $storage1 to $storage2)
842
					} else {
843
						$result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
844
					}
845
846
					if ((Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2)) && $result !== false) {
847
						// if it was a rename from a part file to a regular file it was a write and not a rename operation
848
849
						$this->writeUpdate($storage2, $internalPath2);
850
					} elseif ($result) {
851
						if ($internalPath1 !== '') { // don't do a cache update for moved mounts
852
							$this->renameUpdate($storage1, $storage2, $internalPath1, $internalPath2);
853
						}
854
					}
855
856
					$this->changeLock($path1, ILockingProvider::LOCK_SHARED, true);
857
					$this->changeLock($path2, ILockingProvider::LOCK_SHARED, true);
858
859
					if ((Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2)) && $result !== false) {
860
						if ($this->shouldEmitHooks()) {
861
							$this->emit_file_hooks_post($exists, $path2);
862
						}
863
					} elseif ($result) {
864
						if ($this->shouldEmitHooks($path1) and $this->shouldEmitHooks($path2)) {
865
							\OC_Hook::emit(
866
								Filesystem::CLASSNAME,
867
								Filesystem::signal_post_rename,
868
								[
869
									Filesystem::signal_param_oldpath => $this->getHookPath($path1),
870
									Filesystem::signal_param_newpath => $this->getHookPath($path2)
871
								]
872
							);
873
						}
874
					}
875
				}
876
				$this->unlockFile($path1, ILockingProvider::LOCK_SHARED, true);
877
				$this->unlockFile($path2, ILockingProvider::LOCK_SHARED, true);
878
			}
879
880
			return $result;
881
		}, [
882
			'before' => ['oldpath' => $this->getAbsolutePath($path1),
883
				'newpath' => $this->getAbsolutePath($path2)],
884
			'after' => ['oldpath' => $this->getAbsolutePath($path1),
885
				'newpath' => $this->getAbsolutePath($path2)]
886
		], 'file', 'rename');
887
	}
888
889
	/**
890
	 * Copy a file/folder from the source path to target path
891
	 *
892
	 * @param string $path1 source path
893
	 * @param string $path2 target path
894
	 * @param bool $preserveMtime whether to preserve mtime on the copy
895
	 *
896
	 * @return bool|mixed
897
	 */
898
899
	public function copy($path1, $path2, $preserveMtime = false) {
0 ignored issues
show
Unused Code introduced by
The parameter $preserveMtime is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
900
		$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($path1));
901
		$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($path2));
902
		return $this->emittingCall(function () use ($absolutePath1, $absolutePath2) {
903
			$result = false;
904
			if (
905
				Filesystem::isValidPath($absolutePath2)
906
				&& Filesystem::isValidPath($absolutePath1)
907
				&& !Filesystem::isForbiddenFileOrDir($absolutePath2)
908
			) {
909
				$path1 = $this->getRelativePath($absolutePath1);
910
				$path2 = $this->getRelativePath($absolutePath2);
911
912
				if ($path1 === null || $path2 === null) {
913
					return false;
914
				}
915
				$run = true;
916
917
				$this->lockFile($path2, ILockingProvider::LOCK_SHARED);
918
				$this->lockFile($path1, ILockingProvider::LOCK_SHARED);
919
				$lockTypePath1 = ILockingProvider::LOCK_SHARED;
920
				$lockTypePath2 = ILockingProvider::LOCK_SHARED;
921
922
				try {
923
					$exists = $this->file_exists($path2);
924 View Code Duplication
					if ($this->shouldEmitHooks()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
925
						\OC_Hook::emit(
926
							Filesystem::CLASSNAME,
927
							Filesystem::signal_copy,
928
							[
929
								Filesystem::signal_param_oldpath => $this->getHookPath($path1),
930
								Filesystem::signal_param_newpath => $this->getHookPath($path2),
931
								Filesystem::signal_param_run => &$run
932
							]
933
						);
934
						$this->emit_file_hooks_pre($exists, $path2, $run);
935
					}
936
					if ($run) {
937
						$mount1 = $this->getMount($path1);
938
						$mount2 = $this->getMount($path2);
939
						$storage1 = $mount1->getStorage();
940
						$internalPath1 = $mount1->getInternalPath($absolutePath1);
941
						$storage2 = $mount2->getStorage();
942
						$internalPath2 = $mount2->getInternalPath($absolutePath2);
943
944
						$this->changeLock($path2, ILockingProvider::LOCK_EXCLUSIVE);
945
						$lockTypePath2 = ILockingProvider::LOCK_EXCLUSIVE;
946
947
						if ($mount1->getMountPoint() === $mount2->getMountPoint()) {
948
							if ($storage1) {
949
								$result = $storage1->copy($internalPath1, $internalPath2);
950
							} else {
951
								$result = false;
952
							}
953
						} else {
954
							$result = $storage2->copyFromStorage($storage1, $internalPath1, $internalPath2);
955
						}
956
957
						$this->writeUpdate($storage2, $internalPath2);
958
959
						$this->changeLock($path2, ILockingProvider::LOCK_SHARED);
960
						$lockTypePath2 = ILockingProvider::LOCK_SHARED;
961
962 View Code Duplication
						if ($result !== false && $this->shouldEmitHooks()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
963
							\OC_Hook::emit(
964
								Filesystem::CLASSNAME,
965
								Filesystem::signal_post_copy,
966
								[
967
									Filesystem::signal_param_oldpath => $this->getHookPath($path1),
968
									Filesystem::signal_param_newpath => $this->getHookPath($path2)
969
								]
970
							);
971
							$this->emit_file_hooks_post($exists, $path2);
972
						}
973
					}
974
				} catch (\Exception $e) {
975
					$this->unlockFile($path2, $lockTypePath2);
976
					$this->unlockFile($path1, $lockTypePath1);
977
					throw $e;
978
				}
979
980
				$this->unlockFile($path2, $lockTypePath2);
981
				$this->unlockFile($path1, $lockTypePath1);
982
			}
983
			return $result;
984
		}, [
985
			'before' => [
986
				'oldpath' => $absolutePath1,
987
				'newpath' => $absolutePath2
988
			],
989
			'after' => [
990
				'oldpath' => $absolutePath1,
991
				'newpath' => $absolutePath2
992
			]
993
		], 'file', 'copy');
994
	}
995
996
	/**
997
	 * @param string $path
998
	 * @param string $mode
999
	 * @return resource
1000
	 * @deprecated 11.0.0
1001
	 */
1002
	public function fopen($path, $mode) {
0 ignored issues
show
Unused Code introduced by
The parameter $path is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $mode is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1003
		throw new \BadMethodCallException('fopen is no longer allowed to be called');
1004
	}
1005
1006
	/**
1007
	 * @param string $path
1008
	 * @return bool|string
1009
	 * @throws \OCP\Files\InvalidPathException
1010
	 */
1011
	public function toTmpFile($path) {
1012
		$this->assertPathLength($path);
1013
		if (Filesystem::isValidPath($path)) {
1014
			$source = $this->readFile($path, 'r')->detach();
0 ignored issues
show
Documentation introduced by
'r' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1015
			if ($source) {
1016
				$extension = \pathinfo($path, PATHINFO_EXTENSION);
1017
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($extension);
1018
				\file_put_contents($tmpFile, $source);
1019
				return $tmpFile;
1020
			} else {
1021
				return false;
1022
			}
1023
		} else {
1024
			return false;
1025
		}
1026
	}
1027
1028
	/**
1029
	 * @param string $tmpFile
1030
	 * @param string $path
1031
	 * @return bool|mixed
1032
	 * @throws \OCP\Files\InvalidPathException
1033
	 */
1034
	public function fromTmpFile($tmpFile, $path) {
1035
		$this->assertPathLength($path);
1036
		if (Filesystem::isValidPath($path)) {
1037
1038
			// Get directory that the file is going into
1039
			$filePath = \dirname($path);
1040
1041
			// Create the directories if any
1042
			if (!$this->file_exists($filePath)) {
1043
				$result = $this->createParentDirectories($filePath);
1044
				if ($result === false) {
1045
					return false;
1046
				}
1047
			}
1048
1049
			$source = \fopen($tmpFile, 'r');
1050
			if ($source) {
1051
				$result = $this->file_put_contents($path, $source);
1052
				// $this->file_put_contents() might have already closed
1053
				// the resource, so we check it, before trying to close it
1054
				// to avoid messages in the error log.
1055
				if (\is_resource($source)) {
1056
					\fclose($source);
1057
				}
1058
				\unlink($tmpFile);
1059
				return $result;
1060
			} else {
1061
				return false;
1062
			}
1063
		} else {
1064
			return false;
1065
		}
1066
	}
1067
1068
	/**
1069
	 * @param string $path
1070
	 * @return mixed
1071
	 * @throws \OCP\Files\InvalidPathException
1072
	 */
1073
	public function getMimeType($path) {
1074
		$this->assertPathLength($path);
1075
		return $this->basicOperation('getMimeType', $path);
1076
	}
1077
1078
	/**
1079
	 * @param string $type
1080
	 * @param string $path
1081
	 * @param bool $raw
1082
	 * @return bool|null|string
1083
	 */
1084
	public function hash($type, $path, $raw = false) {
1085
		$postFix = (\substr($path, -1, 1) === '/') ? '/' : '';
1086
		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
1087
		if (Filesystem::isValidPath($path)) {
1088
			$path = $this->getRelativePath($absolutePath);
1089
			if ($path == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $path of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
1090
				return false;
1091
			}
1092
			if ($this->shouldEmitHooks($path)) {
1093
				\OC_Hook::emit(
1094
					Filesystem::CLASSNAME,
1095
					Filesystem::signal_read,
1096
					[Filesystem::signal_param_path => $this->getHookPath($path)]
1097
				);
1098
			}
1099
1100
			list($storage, $internalPath) = Filesystem::resolvePath($absolutePath . $postFix);
1101
			if ($storage) {
1102
				$result = $storage->hash($type, $internalPath, $raw);
1103
				return $result;
1104
			}
1105
		}
1106
		return null;
1107
	}
1108
1109
	/**
1110
	 * @param string $path
1111
	 * @return mixed
1112
	 * @throws \OCP\Files\InvalidPathException
1113
	 */
1114
	public function free_space($path = '/') {
1115
		$this->assertPathLength($path);
1116
		return $this->basicOperation('free_space', $path);
1117
	}
1118
1119
	/**
1120
	 * abstraction layer for basic filesystem functions: wrapper for \OC\Files\Storage\Storage
1121
	 *
1122
	 * @param string $operation
1123
	 * @param string $path
1124
	 * @param array $hooks (optional)
1125
	 * @param mixed $extraParam (optional)
1126
	 * @return mixed
1127
	 * @throws \Exception
1128
	 *
1129
	 * This method takes requests for basic filesystem functions (e.g. reading & writing
1130
	 * files), processes hooks and proxies, sanitises paths, and finally passes them on to
1131
	 * \OC\Files\Storage\Storage for delegation to a storage backend for execution
1132
	 */
1133
	private function basicOperation($operation, $path, $hooks = [], $extraParam = null) {
1134
		$postFix = (\substr($path, -1, 1) === '/') ? '/' : '';
1135
		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
1136
		if (Filesystem::isValidPath($path)
1137
			and !Filesystem::isForbiddenFileOrDir($path)
1138
		) {
1139
			$path = $this->getRelativePath($absolutePath);
1140
			if ($path == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $path of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
1141
				return false;
1142
			}
1143
1144
			if (\in_array('write', $hooks) || \in_array('delete', $hooks) || \in_array('read', $hooks)) {
1145
				// always a shared lock during pre-hooks so the hook can read the file
1146
				$this->lockFile($path, ILockingProvider::LOCK_SHARED);
1147
			}
1148
1149
			$run = $this->runHooks($hooks, $path);
1150
			/** @var \OC\Files\Storage\Storage $storage */
1151
			list($storage, $internalPath) = Filesystem::resolvePath($absolutePath . $postFix);
1152
			if ($run and $storage) {
1153
				if (\in_array('write', $hooks) || \in_array('delete', $hooks)) {
1154
					$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
1155
				}
1156
				try {
1157
					if ($extraParam !== null) {
1158
						$result = $storage->$operation($internalPath, $extraParam);
1159
					} else {
1160
						$result = $storage->$operation($internalPath);
1161
					}
1162
				} catch (\Exception $e) {
1163 View Code Duplication
					if (\in_array('write', $hooks) || \in_array('delete', $hooks)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1164
						$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
1165
					} elseif (\in_array('read', $hooks)) {
1166
						$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1167
					}
1168
					throw $e;
1169
				}
1170
1171
				if (\in_array('delete', $hooks) and $result) {
1172
					$this->removeUpdate($storage, $internalPath);
1173
				}
1174
				if (\in_array('write', $hooks) and $operation !== 'fopen') {
1175
					$this->writeUpdate($storage, $internalPath);
1176
				}
1177
				if (\in_array('touch', $hooks)) {
1178
					$this->writeUpdate($storage, $internalPath, $extraParam);
1179
				}
1180
1181
				if ((\in_array('write', $hooks) || \in_array('delete', $hooks)) && ($operation !== 'fopen' || $result === false)) {
1182
					$this->changeLock($path, ILockingProvider::LOCK_SHARED);
1183
				}
1184
1185
				$unlockLater = false;
1186
				if ($this->lockingEnabled && $operation === 'fopen' && \is_resource($result)) {
1187
					$unlockLater = true;
1188
					// make sure our unlocking callback will still be called if connection is aborted
1189
					\ignore_user_abort(true);
1190
					$result = CallbackWrapper::wrap($result, null, null, function () use ($hooks, $path) {
1191
						if (\in_array('write', $hooks)) {
1192
							$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
1193
						} elseif (\in_array('read', $hooks)) {
1194
							$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1195
						}
1196
					});
1197
				}
1198
1199
				if ($this->shouldEmitHooks($path) && $result !== false) {
1200
					if ($operation != 'fopen') { //no post hooks for fopen, the file stream is still open
1201
						$this->runHooks($hooks, $path, true);
1202
					}
1203
				}
1204
1205 View Code Duplication
				if (!$unlockLater
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1206
					&& (\in_array('write', $hooks) || \in_array('delete', $hooks) || \in_array('read', $hooks))
1207
				) {
1208
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1209
				}
1210
				return $result;
1211
			} else {
1212
				$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1213
			}
1214
		}
1215
		return null;
1216
	}
1217
1218
	/**
1219
	 * get the path relative to the default root for hook usage
1220
	 *
1221
	 * @param string $path
1222
	 * @return string
1223
	 */
1224
	private function getHookPath($path) {
1225
		if (!Filesystem::getView()) {
1226
			return $path;
1227
		}
1228
		return Filesystem::getView()->getRelativePath($this->getAbsolutePath($path));
1229
	}
1230
1231
	private function shouldEmitHooks($path = '') {
1232
		if ($path && Cache\Scanner::isPartialFile($path)) {
1233
			return false;
1234
		}
1235
		if (!Filesystem::$loaded) {
1236
			return false;
1237
		}
1238
		$defaultRoot = Filesystem::getRoot();
1239
		if ($defaultRoot === null) {
1240
			return false;
1241
		}
1242
		if ($this->fakeRoot === $defaultRoot) {
1243
			return true;
1244
		}
1245
		$fullPath = $this->getAbsolutePath($path);
1246
1247
		if ($fullPath === $defaultRoot) {
1248
			return true;
1249
		}
1250
1251
		return (\strlen($fullPath) > \strlen($defaultRoot)) && (\substr($fullPath, 0, \strlen($defaultRoot) + 1) === $defaultRoot . '/');
1252
	}
1253
1254
	/**
1255
	 * @param string[] $hooks
1256
	 * @param string $path
1257
	 * @param bool $post
1258
	 * @return bool
1259
	 */
1260
	private function runHooks($hooks, $path, $post = false) {
1261
		if (empty($hooks)) {
1262
			return true;
1263
		}
1264
		$relativePath = $path;
1265
		$path = $this->getHookPath($path);
1266
		$prefix = ($post) ? 'post_' : '';
1267
		$run = true;
1268
		if ($this->shouldEmitHooks($relativePath)) {
1269
			foreach ($hooks as $hook) {
1270
				if ($hook != 'read') {
1271
					\OC_Hook::emit(
1272
						Filesystem::CLASSNAME,
1273
						$prefix . $hook,
1274
						[
1275
							Filesystem::signal_param_run => &$run,
1276
							Filesystem::signal_param_path => $path
1277
						]
1278
					);
1279
				} elseif (!$post) {
1280
					\OC_Hook::emit(
1281
						Filesystem::CLASSNAME,
1282
						$prefix . $hook,
1283
						[
1284
							Filesystem::signal_param_path => $path
1285
						]
1286
					);
1287
				}
1288
			}
1289
		}
1290
		return $run;
1291
	}
1292
1293
	/**
1294
	 * check if a file or folder has been updated since $time
1295
	 *
1296
	 * @param string $path
1297
	 * @param int $time
1298
	 * @return bool
1299
	 */
1300
	public function hasUpdated($path, $time) {
1301
		return $this->basicOperation('hasUpdated', $path, [], $time);
1302
	}
1303
1304
	/**
1305
	 * @param string $ownerId
1306
	 * @return IUser
1307
	 */
1308
	private function getUserObjectForOwner($ownerId) {
1309
		$owner = $this->userManager->get($ownerId);
1310
		if (!$owner instanceof IUser) {
1311
			return new RemoteUser($ownerId);
1312
		}
1313
1314
		return $owner;
1315
	}
1316
1317
	/**
1318
	 * Get file info from cache
1319
	 *
1320
	 * If the file is not in cached it will be scanned
1321
	 * If the file has changed on storage the cache will be updated
1322
	 *
1323
	 * @param \OC\Files\Storage\Storage $storage
1324
	 * @param string $internalPath
1325
	 * @param string $relativePath
1326
	 * @return array|bool
1327
	 */
1328
	private function getCacheEntry($storage, $internalPath, $relativePath) {
1329
		$cache = $storage->getCache($internalPath);
1330
		$data = $cache->get($internalPath);
1331
		$watcher = $storage->getWatcher($internalPath);
1332
1333
		try {
1334
			// if the file is not in the cache or needs to be updated, trigger the scanner and reload the data
1335
			if (!$data || (isset($data['size']) && ($data['size'] === -1))) {
1336
				$this->lockFile($relativePath, ILockingProvider::LOCK_SHARED);
1337
				if (!$storage->file_exists($internalPath)) {
1338
					$this->unlockFile($relativePath, ILockingProvider::LOCK_SHARED);
1339
					return false;
1340
				}
1341
				$scanner = $storage->getScanner($internalPath);
1342
				$scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW);
1343
				$data = $cache->get($internalPath);
1344
				$this->unlockFile($relativePath, ILockingProvider::LOCK_SHARED);
1345
			} elseif (!Cache\Scanner::isPartialFile($internalPath) && $watcher->needsUpdate($internalPath, $data)) {
1346
				$this->lockFile($relativePath, ILockingProvider::LOCK_SHARED);
1347
				$watcher->update($internalPath, $data);
1348
				$storage->getPropagator()->propagateChange($internalPath, \time());
1349
				$data = $cache->get($internalPath);
1350
				$this->unlockFile($relativePath, ILockingProvider::LOCK_SHARED);
1351
			}
1352
		} catch (LockedException $e) {
1353
			// if the file is locked we just use the old cache info
1354
		}
1355
1356
		return $data;
1357
	}
1358
1359
	/**
1360
	 * get the filesystem info
1361
	 *
1362
	 * @param string $path
1363
	 * @param boolean|string $includeMountPoints true to add mountpoint sizes,
1364
	 * 'ext' to add only ext storage mount point sizes. Defaults to true.
1365
	 * defaults to true
1366
	 * @return \OC\Files\FileInfo|false False if file does not exist
1367
	 */
1368
	public function getFileInfo($path, $includeMountPoints = true) {
1369
		$this->assertPathLength($path);
1370
		if (!Filesystem::isValidPath($path)) {
1371
			return false;
1372
		}
1373
		if (Cache\Scanner::isPartialFile($path)) {
1374
			return $this->getPartFileInfo($path);
1375
		}
1376
		$relativePath = $path;
1377
		$path = Filesystem::normalizePath($this->fakeRoot . '/' . $path);
1378
1379
		$mount = Filesystem::getMountManager()->find($path);
1380
		$storage = $mount->getStorage();
1381
		$internalPath = $mount->getInternalPath($path);
1382
		if ($storage) {
1383
			$data = $this->getCacheEntry($storage, $internalPath, $relativePath);
1384
1385
			if (!$data instanceof ICacheEntry) {
1386
				return false;
1387
			}
1388
1389
			if ($mount instanceof MoveableMount && $internalPath === '') {
1390
				$data['permissions'] |= \OCP\Constants::PERMISSION_DELETE;
1391
			}
1392
1393
			$owner = $this->getUserObjectForOwner($storage->getOwner($internalPath));
1394
			$info = new FileInfo($path, $storage, $internalPath, $data, $mount, $owner);
0 ignored issues
show
Bug introduced by
It seems like $mount defined by \OC\Files\Filesystem::ge...tManager()->find($path) on line 1379 can be null; however, OC\Files\FileInfo::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1395
1396
			if ($data and isset($data['fileid'])) {
1397
				if ($includeMountPoints and $data['mimetype'] === 'httpd/unix-directory') {
1398
					//add the sizes of other mount points to the folder
1399
					$extOnly = ($includeMountPoints === 'ext');
1400
					$mounts = Filesystem::getMountManager()->findIn($path);
1401
					foreach ($mounts as $mount) {
1402
						$subStorage = $mount->getStorage();
1403
						if ($subStorage) {
1404
							// exclude shared storage ?
1405
							if ($extOnly && $subStorage instanceof \OCA\Files_Sharing\SharedStorage) {
1406
								continue;
1407
							}
1408
							$subCache = $subStorage->getCache('');
1409
							$rootEntry = $subCache->get('');
1410
							$info->addSubEntry($rootEntry, $mount->getMountPoint());
1411
						}
1412
					}
1413
				}
1414
			}
1415
1416
			return $info;
1417
		}
1418
1419
		return false;
1420
	}
1421
1422
	/**
1423
	 * get the content of a directory
1424
	 *
1425
	 * @param string $directory path under datadirectory
1426
	 * @param string $mimetype_filter limit returned content to this mimetype or mimepart
1427
	 * @return FileInfo[]
1428
	 */
1429
	public function getDirectoryContent($directory, $mimetype_filter = '') {
1430
		$this->assertPathLength($directory);
1431
		if (!Filesystem::isValidPath($directory)) {
1432
			return [];
1433
		}
1434
		$path = $this->getAbsolutePath($directory);
1435
		$path = Filesystem::normalizePath($path);
1436
		$mount = $this->getMount($directory);
1437
		$storage = $mount->getStorage();
1438
		$internalPath = $mount->getInternalPath($path);
1439
		if ($storage) {
1440
			$cache = $storage->getCache($internalPath);
1441
			$user = \OC_User::getUser();
1442
1443
			$data = $this->getCacheEntry($storage, $internalPath, $directory);
1444
1445
			if (!$data instanceof ICacheEntry || !isset($data['fileid']) || !($data->getPermissions() && Constants::PERMISSION_READ)) {
1446
				return [];
1447
			}
1448
1449
			$folderId = $data['fileid'];
1450
			$contents = $cache->getFolderContentsById($folderId); //TODO: mimetype_filter
1451
1452
			$sharingDisabled = Util::isSharingDisabledForUser();
1453
			/**
1454
			 * @var \OC\Files\FileInfo[] $files
1455
			 */
1456
			$files = \array_filter($contents, function (ICacheEntry $content) {
1457
				return (!\OC\Files\Filesystem::isForbiddenFileOrDir($content['path']));
1458
			});
1459
			$files = \array_map(function (ICacheEntry $content) use ($path, $storage, $mount, $sharingDisabled) {
1460
				if ($sharingDisabled) {
1461
					$content['permissions'] = $content['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
1462
				}
1463
				$owner = $this->getUserObjectForOwner($storage->getOwner($content['path']));
1464
				return new FileInfo($path . '/' . $content['name'], $storage, $content['path'], $content, $mount, $owner);
0 ignored issues
show
Bug introduced by
It seems like $mount defined by $this->getMount($directory) on line 1436 can be null; however, OC\Files\FileInfo::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1465
			}, $files);
1466
1467
			//add a folder for any mountpoint in this directory and add the sizes of other mountpoints to the folders
1468
			$mounts = Filesystem::getMountManager()->findIn($path);
1469
			$dirLength = \strlen($path);
1470
			foreach ($mounts as $mount) {
1471
				$mountPoint = $mount->getMountPoint();
1472
				$subStorage = $mount->getStorage();
1473
				if ($subStorage) {
1474
					$subCache = $subStorage->getCache('');
1475
1476
					$rootEntry = $subCache->get('');
1477
					if (!$rootEntry) {
1478
						$subScanner = $subStorage->getScanner('');
1479
						try {
1480
							$subScanner->scanFile('');
1481
						} catch (\OCP\Files\StorageNotAvailableException $e) {
1482
							continue;
1483
						} catch (\OCP\Files\StorageInvalidException $e) {
1484
							continue;
1485
						} catch (\Exception $e) {
1486
							// sometimes when the storage is not available it can be any exception
1487
							Util::writeLog(
1488
								'core',
1489
								'Exception while scanning storage "' . $subStorage->getId() . '": ' .
1490
								\get_class($e) . ': ' . $e->getMessage(),
1491
								Util::ERROR
1492
							);
1493
							continue;
1494
						}
1495
						$rootEntry = $subCache->get('');
1496
					}
1497
1498
					if ($rootEntry && ($rootEntry->getPermissions() && Constants::PERMISSION_READ)) {
1499
						$relativePath = \trim(\substr($mountPoint, $dirLength), '/');
1500
						if ($pos = \strpos($relativePath, '/')) {
1501
							//mountpoint inside subfolder add size to the correct folder
1502
							$entryName = \substr($relativePath, 0, $pos);
1503
							foreach ($files as &$entry) {
1504
								if ($entry->getName() === $entryName) {
1505
									$entry->addSubEntry($rootEntry, $mountPoint);
1506
								}
1507
							}
1508
						} else { //mountpoint in this folder, add an entry for it
1509
							$rootEntry['name'] = $relativePath;
1510
							$rootEntry['type'] = $rootEntry['mimetype'] === 'httpd/unix-directory' ? 'dir' : 'file';
1511
							$permissions = $rootEntry['permissions'];
1512
							// do not allow renaming/deleting the mount point if they are not shared files/folders
1513
							// for shared files/folders we use the permissions given by the owner
1514
							if ($mount instanceof MoveableMount) {
1515
								$rootEntry['permissions'] = $permissions | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE;
1516
							} else {
1517
								$rootEntry['permissions'] = $permissions & (\OCP\Constants::PERMISSION_ALL - (\OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE));
1518
							}
1519
1520
							//remove any existing entry with the same name
1521
							foreach ($files as $i => $file) {
1522
								if ($file['name'] === $rootEntry['name']) {
1523
									unset($files[$i]);
1524
									break;
1525
								}
1526
							}
1527
							$rootEntry['path'] = \substr(Filesystem::normalizePath($path . '/' . $rootEntry['name']), \strlen($user) + 2); // full path without /$user/
1528
1529
							// if sharing was disabled for the user we remove the share permissions
1530
							if (Util::isSharingDisabledForUser()) {
1531
								$rootEntry['permissions'] = $rootEntry['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
1532
							}
1533
1534
							$owner = $this->getUserObjectForOwner($subStorage->getOwner(''));
1535
							$files[] = new FileInfo($path . '/' . $rootEntry['name'], $subStorage, '', $rootEntry, $mount, $owner);
1536
						}
1537
					}
1538
				}
1539
			}
1540
1541
			if ($mimetype_filter) {
1542
				$files = \array_filter($files, function (FileInfo $file) use ($mimetype_filter) {
1543
					if (\strpos($mimetype_filter, '/')) {
1544
						return $file->getMimetype() === $mimetype_filter;
1545
					} else {
1546
						return $file->getMimePart() === $mimetype_filter;
1547
					}
1548
				});
1549
			}
1550
1551
			return $files;
1552
		} else {
1553
			return [];
1554
		}
1555
	}
1556
1557
	/**
1558
	 * change file metadata
1559
	 *
1560
	 * @param string $path
1561
	 * @param array|\OCP\Files\FileInfo $data
1562
	 * @return int
1563
	 *
1564
	 * returns the fileid of the updated file
1565
	 */
1566
	public function putFileInfo($path, $data) {
1567
		$this->assertPathLength($path);
1568
		if ($data instanceof FileInfo) {
1569
			$data = $data->getData();
1570
		}
1571
		$path = Filesystem::normalizePath($this->fakeRoot . '/' . $path);
1572
		/**
1573
		 * @var \OC\Files\Storage\Storage $storage
1574
		 * @var string $internalPath
1575
		 */
1576
		list($storage, $internalPath) = Filesystem::resolvePath($path);
1577
		if ($storage) {
1578
			$cache = $storage->getCache($path);
1579
1580
			if (!$cache->inCache($internalPath)) {
1581
				$scanner = $storage->getScanner($internalPath);
1582
				$scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW);
1583
			}
1584
1585
			return $cache->put($internalPath, $data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 1566 can also be of type object<OCP\Files\FileInfo>; however, OC\Files\Cache\Cache::put() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1586
		} else {
1587
			return -1;
1588
		}
1589
	}
1590
1591
	/**
1592
	 * search for files with the name matching $query
1593
	 *
1594
	 * @param string $query
1595
	 * @return FileInfo[]
1596
	 */
1597
	public function search($query) {
1598
		return $this->searchCommon('search', ['%' . $query . '%']);
1599
	}
1600
1601
	/**
1602
	 * search for files with the name matching $query
1603
	 *
1604
	 * @param string $query
1605
	 * @return FileInfo[]
1606
	 */
1607
	public function searchRaw($query) {
1608
		return $this->searchCommon('search', [$query]);
1609
	}
1610
1611
	/**
1612
	 * search for files by mimetype
1613
	 *
1614
	 * @param string $mimetype
1615
	 * @return FileInfo[]
1616
	 */
1617
	public function searchByMime($mimetype) {
1618
		return $this->searchCommon('searchByMime', [$mimetype]);
1619
	}
1620
1621
	/**
1622
	 * search for files by tag
1623
	 *
1624
	 * @param string|int $tag name or tag id
1625
	 * @param string $userId owner of the tags
1626
	 * @return FileInfo[]
1627
	 */
1628
	public function searchByTag($tag, $userId) {
1629
		return $this->searchCommon('searchByTag', [$tag, $userId]);
1630
	}
1631
1632
	/**
1633
	 * @param string $method cache method
1634
	 * @param array $args
1635
	 * @return FileInfo[]
1636
	 */
1637
	private function searchCommon($method, $args) {
1638
		$files = [];
1639
		$rootLength = \strlen($this->fakeRoot);
1640
1641
		$mount = $this->getMount('');
1642
		$mountPoint = $mount->getMountPoint();
1643
		$storage = $mount->getStorage();
1644
		if ($storage) {
1645
			$cache = $storage->getCache('');
1646
1647
			$results = \call_user_func_array([$cache, $method], $args);
1648
			foreach ($results as $result) {
1649
				if (\substr($mountPoint . $result['path'], 0, $rootLength + 1) === $this->fakeRoot . '/') {
1650
					$internalPath = $result['path'];
1651
					$path = $mountPoint . $result['path'];
1652
					$result['path'] = \substr($mountPoint . $result['path'], $rootLength);
1653
					$owner = \OC::$server->getUserManager()->get($storage->getOwner($internalPath));
1654
					$files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner);
0 ignored issues
show
Bug introduced by
It seems like $mount defined by $this->getMount('') on line 1641 can be null; however, OC\Files\FileInfo::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1655
				}
1656
			}
1657
1658
			$mounts = Filesystem::getMountManager()->findIn($this->fakeRoot);
1659
			foreach ($mounts as $mount) {
1660
				$mountPoint = $mount->getMountPoint();
1661
				$storage = $mount->getStorage();
1662
				if ($storage) {
1663
					$cache = $storage->getCache('');
1664
1665
					$relativeMountPoint = \substr($mountPoint, $rootLength);
1666
					$results = \call_user_func_array([$cache, $method], $args);
1667
					if ($results) {
1668
						foreach ($results as $result) {
1669
							$internalPath = $result['path'];
1670
							$result['path'] = \rtrim($relativeMountPoint . $result['path'], '/');
1671
							$path = \rtrim($mountPoint . $internalPath, '/');
1672
							$owner = \OC::$server->getUserManager()->get($storage->getOwner($internalPath));
1673
							$files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner);
1674
						}
1675
					}
1676
				}
1677
			}
1678
		}
1679
		return $files;
1680
	}
1681
1682
	/**
1683
	 * Get the owner for a file or folder
1684
	 *
1685
	 * @param string $path
1686
	 * @return string the user id of the owner
1687
	 * @throws NotFoundException
1688
	 */
1689
	public function getOwner($path) {
1690
		$info = $this->getFileInfo($path);
1691
		if (!$info) {
1692
			throw new NotFoundException($path . ' not found while trying to get owner');
1693
		}
1694
		return $info->getOwner()->getUID();
1695
	}
1696
1697
	/**
1698
	 * get the ETag for a file or folder
1699
	 *
1700
	 * @param string $path
1701
	 * @return string
1702
	 */
1703
	public function getETag($path) {
1704
		/**
1705
		 * @var Storage\Storage $storage
1706
		 * @var string $internalPath
1707
		 */
1708
		list($storage, $internalPath) = $this->resolvePath($path);
1709
		if ($storage) {
1710
			return $storage->getETag($internalPath);
1711
		} else {
1712
			return null;
1713
		}
1714
	}
1715
1716
	/**
1717
	 * Get the path of a file by id, relative to the view
1718
	 *
1719
	 * Note that the resulting path is not guarantied to be unique for the id, multiple paths can point to the same file
1720
	 *
1721
	 * @param int $id
1722
	 * @param bool $includeShares whether to recurse into shared mounts
1723
	 * @throws NotFoundException
1724
	 * @return string
1725
	 */
1726
	public function getPath($id, $includeShares = true) {
1727
		$id = (int)$id;
1728
		$manager = Filesystem::getMountManager();
1729
		$mounts = $manager->findIn($this->fakeRoot);
1730
		$mounts[] = $manager->find($this->fakeRoot);
1731
		// reverse the array so we start with the storage this view is in
1732
		// which is the most likely to contain the file we're looking for
1733
		$mounts = \array_reverse($mounts);
1734
		foreach ($mounts as $mount) {
1735
			/**
1736
			 * @var \OC\Files\Mount\MountPoint $mount
1737
			 */
1738
			if (!$includeShares && $mount instanceof SharedMount) {
1739
				// prevent potential infinite loop when instantiating shared storages
1740
				// recursively
1741
				continue;
1742
			}
1743 View Code Duplication
			if ($mount->getStorage()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1744
				$cache = $mount->getStorage()->getCache();
1745
				$internalPath = $cache->getPathById($id);
1746
				if (\is_string($internalPath)) {
1747
					$fullPath = $mount->getMountPoint() . $internalPath;
1748
					if (($path = $this->getRelativePath($fullPath)) !== null) {
1749
						return $path;
1750
					}
1751
				}
1752
			}
1753
		}
1754
		throw new NotFoundException(\sprintf('File with id "%s" has not been found.', $id));
1755
	}
1756
1757
	/**
1758
	 * @param string $path
1759
	 * @throws InvalidPathException
1760
	 */
1761
	private function assertPathLength($path) {
1762
		$maxLen = \min(PHP_MAXPATHLEN, 4000);
1763
		// Check for the string length - performed using isset() instead of strlen()
1764
		// because isset() is about 5x-40x faster.
1765
		if (isset($path[$maxLen])) {
1766
			$pathLen = \strlen($path);
1767
			throw new \OCP\Files\InvalidPathException("Path length($pathLen) exceeds max path length($maxLen): $path");
1768
		}
1769
	}
1770
1771
	/**
1772
	 * check if it is allowed to move a mount point to a given target.
1773
	 * It is not allowed to move a mount point into a different mount point or
1774
	 * into an already shared folder
1775
	 *
1776
	 * @param MoveableMount $mount1 moveable mount
1777
	 * @param string $target absolute target path
1778
	 * @return boolean
1779
	 */
1780
	private function canMove(MoveableMount $mount1, $target) {
1781
		list($targetStorage, $targetInternalPath) = \OC\Files\Filesystem::resolvePath($target);
0 ignored issues
show
Unused Code introduced by
The assignment to $targetInternalPath is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1782
		if (!$targetStorage->instanceOfStorage('\OCP\Files\IHomeStorage')) {
1783
			Util::writeLog('files',
1784
				'It is not allowed to move one mount point into another one',
1785
				Util::DEBUG);
1786
			return false;
1787
		}
1788
1789
		return $mount1->isTargetAllowed($target);
1790
	}
1791
1792
	/**
1793
	 * Get a fileinfo object for files that are ignored in the cache (part files)
1794
	 *
1795
	 * @param string $path
1796
	 * @return \OCP\Files\FileInfo
1797
	 */
1798
	private function getPartFileInfo($path) {
1799
		$mount = $this->getMount($path);
1800
		$storage = $mount->getStorage();
1801
		$internalPath = $mount->getInternalPath($this->getAbsolutePath($path));
1802
		$owner = \OC::$server->getUserManager()->get($storage->getOwner($internalPath));
1803
		return new FileInfo(
1804
			$this->getAbsolutePath($path),
1805
			$storage,
1806
			$internalPath,
1807
			[
1808
				'fileid' => null,
1809
				'mimetype' => $storage->getMimeType($internalPath),
1810
				'name' => \basename($path),
1811
				'etag' => null,
1812
				'size' => $storage->filesize($internalPath),
1813
				'mtime' => $storage->filemtime($internalPath),
1814
				'encrypted' => false,
1815
				'permissions' => \OCP\Constants::PERMISSION_ALL
1816
			],
1817
			$mount,
0 ignored issues
show
Bug introduced by
It seems like $mount defined by $this->getMount($path) on line 1799 can be null; however, OC\Files\FileInfo::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1818
			$owner
1819
		);
1820
	}
1821
1822
	/**
1823
	 * @param string $path
1824
	 * @param string $fileName
1825
	 * @throws InvalidPathException
1826
	 */
1827
	public function verifyPath($path, $fileName) {
1828
		$l10n = \OC::$server->getL10N('lib');
1829
1830
		// verify empty and dot files
1831
		$trimmed = \trim($fileName);
1832
		if ($trimmed === '') {
1833
			throw new InvalidPathException($l10n->t('Empty filename is not allowed'));
1834
		}
1835
		if (\OC\Files\Filesystem::isIgnoredDir($trimmed)) {
1836
			throw new InvalidPathException($l10n->t('Dot files are not allowed'));
1837
		}
1838
1839
		$matches = [];
1840
1841
		if (\preg_match('/' . FileInfo::BLACKLIST_FILES_REGEX . '/', $fileName) !== 0) {
1842
			throw new InvalidPathException(
1843
				"Can`t upload files with extension {$matches[0]} because these extensions are reserved for internal use."
1844
			);
1845
		}
1846
1847
		if (!\OC::$server->getDatabaseConnection()->allows4ByteCharacters()) {
1848
			// verify database - e.g. mysql only 3-byte chars
1849
			if (\preg_match('%(?:
1850
      \xF0[\x90-\xBF][\x80-\xBF]{2}      # planes 1-3
1851
    | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
1852
    | \xF4[\x80-\x8F][\x80-\xBF]{2}      # plane 16
1853
)%xs', $fileName)) {
1854
				throw new InvalidPathException($l10n->t('4-byte characters are not supported in file names'));
1855
			}
1856
		}
1857
1858
		try {
1859
			/** @type \OCP\Files\Storage $storage */
1860
			list($storage, $internalPath) = $this->resolvePath($path);
1861
			$storage->verifyPath($internalPath, $fileName);
1862
		} catch (ReservedWordException $ex) {
1863
			throw new InvalidPathException($l10n->t('File name is a reserved word'));
1864
		} catch (InvalidCharacterInPathException $ex) {
1865
			throw new InvalidPathException($l10n->t('File name contains at least one invalid character'));
1866
		} catch (FileNameTooLongException $ex) {
1867
			throw new InvalidPathException($l10n->t('File name is too long'));
1868
		}
1869
	}
1870
1871
	/**
1872
	 * get all parent folders of $path
1873
	 *
1874
	 * @param string $path
1875
	 * @return string[]
1876
	 */
1877
	private function getParents($path) {
1878
		$path = \trim($path, '/');
1879
		if (!$path) {
1880
			return [];
1881
		}
1882
1883
		$parts = \explode('/', $path);
1884
1885
		// remove the single file
1886
		\array_pop($parts);
1887
		$result = ['/'];
1888
		$resultPath = '';
1889
		foreach ($parts as $part) {
1890
			if ($part) {
1891
				$resultPath .= '/' . $part;
1892
				$result[] = $resultPath;
1893
			}
1894
		}
1895
		return $result;
1896
	}
1897
1898
	/**
1899
	 * Returns the mount point for which to lock
1900
	 *
1901
	 * @param string $absolutePath absolute path
1902
	 * @param bool $useParentMount true to return parent mount instead of whatever
1903
	 * is mounted directly on the given path, false otherwise
1904
	 * @return \OC\Files\Mount\MountPoint mount point for which to apply locks
1905
	 */
1906
	private function getMountForLock($absolutePath, $useParentMount = false) {
1907
		$results = [];
1908
		$mount = Filesystem::getMountManager()->find($absolutePath);
1909
		if (!$mount) {
1910
			return $results;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $results; (array) is incompatible with the return type documented by OC\Files\View::getMountForLock of type OC\Files\Mount\MountPoint|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1911
		}
1912
1913
		if ($useParentMount) {
1914
			// find out if something is mounted directly on the path
1915
			$internalPath = $mount->getInternalPath($absolutePath);
1916
			if ($internalPath === '') {
1917
				// resolve the parent mount instead
1918
				$mount = Filesystem::getMountManager()->find(\dirname($absolutePath));
1919
			}
1920
		}
1921
1922
		return $mount;
1923
	}
1924
1925
	/**
1926
	 * Lock the given path
1927
	 *
1928
	 * @param string $path the path of the file to lock, relative to the view
1929
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
1930
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
1931
	 *
1932
	 * @return bool False if the path is excluded from locking, true otherwise
1933
	 * @throws \OCP\Lock\LockedException if the path is already locked
1934
	 */
1935 View Code Duplication
	private function lockPath($path, $type, $lockMountPoint = false) {
1936
		$absolutePath = $this->getAbsolutePath($path);
1937
		$absolutePath = Filesystem::normalizePath($absolutePath);
1938
		if (!$this->shouldLockFile($absolutePath)) {
1939
			return false;
1940
		}
1941
1942
		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
1943
		if ($mount) {
1944
			try {
1945
				$storage = $mount->getStorage();
1946
				if ($storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
1947
					$storage->acquireLock(
1948
						$mount->getInternalPath($absolutePath),
1949
						$type,
1950
						$this->lockingProvider
1951
					);
1952
				}
1953
			} catch (\OCP\Lock\LockedException $e) {
1954
				// rethrow with the a human-readable path
1955
				throw new \OCP\Lock\LockedException(
1956
					$this->getPathRelativeToFiles($absolutePath),
1957
					$e
1958
				);
1959
			}
1960
		}
1961
1962
		return true;
1963
	}
1964
1965
	/**
1966
	 * Change the lock type
1967
	 *
1968
	 * @param string $path the path of the file to lock, relative to the view
1969
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
1970
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
1971
	 *
1972
	 * @return bool False if the path is excluded from locking, true otherwise
1973
	 * @throws \OCP\Lock\LockedException if the path is already locked
1974
	 */
1975 View Code Duplication
	public function changeLock($path, $type, $lockMountPoint = false) {
1976
		$path = Filesystem::normalizePath($path);
1977
		$absolutePath = $this->getAbsolutePath($path);
1978
		$absolutePath = Filesystem::normalizePath($absolutePath);
1979
		if (!$this->shouldLockFile($absolutePath)) {
1980
			return false;
1981
		}
1982
1983
		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
1984
		if ($mount) {
1985
			try {
1986
				$storage = $mount->getStorage();
1987
				if ($storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
1988
					$storage->changeLock(
1989
						$mount->getInternalPath($absolutePath),
1990
						$type,
1991
						$this->lockingProvider
1992
					);
1993
				}
1994
			} catch (LockedException $e) {
1995
				// rethrow with the a human-readable path
1996
				throw new LockedException(
1997
					$this->getPathRelativeToFiles($absolutePath),
1998
					$e
1999
				);
2000
			}
2001
		}
2002
2003
		return true;
2004
	}
2005
2006
	/**
2007
	 * Unlock the given path
2008
	 *
2009
	 * @param string $path the path of the file to unlock, relative to the view
2010
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2011
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2012
	 *
2013
	 * @return bool False if the path is excluded from locking, true otherwise
2014
	 */
2015
	private function unlockPath($path, $type, $lockMountPoint = false) {
2016
		$absolutePath = $this->getAbsolutePath($path);
2017
		$absolutePath = Filesystem::normalizePath($absolutePath);
2018
		if (!$this->shouldLockFile($absolutePath)) {
2019
			return false;
2020
		}
2021
2022
		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
2023
		if ($mount) {
2024
			$storage = $mount->getStorage();
2025
			if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
2026
				$storage->releaseLock(
2027
					$mount->getInternalPath($absolutePath),
2028
					$type,
2029
					$this->lockingProvider
2030
				);
2031
			}
2032
		}
2033
2034
		return true;
2035
	}
2036
2037
	/**
2038
	 * Lock a path and all its parents up to the root of the view
2039
	 *
2040
	 * @param string $path the path of the file to lock relative to the view
2041
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2042
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2043
	 *
2044
	 * @return bool False if the path is excluded from locking, true otherwise
2045
	 */
2046 View Code Duplication
	public function lockFile($path, $type, $lockMountPoint = false) {
2047
		$absolutePath = $this->getAbsolutePath($path);
2048
		$absolutePath = Filesystem::normalizePath($absolutePath);
2049
		if (!$this->shouldLockFile($absolutePath)) {
2050
			return false;
2051
		}
2052
2053
		$this->lockPath($path, $type, $lockMountPoint);
2054
2055
		$parents = $this->getParents($path);
2056
		foreach ($parents as $parent) {
2057
			$this->lockPath($parent, ILockingProvider::LOCK_SHARED);
2058
		}
2059
2060
		return true;
2061
	}
2062
2063
	/**
2064
	 * Unlock a path and all its parents up to the root of the view
2065
	 *
2066
	 * @param string $path the path of the file to lock relative to the view
2067
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2068
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2069
	 *
2070
	 * @return bool False if the path is excluded from locking, true otherwise
2071
	 */
2072 View Code Duplication
	public function unlockFile($path, $type, $lockMountPoint = false) {
2073
		$absolutePath = $this->getAbsolutePath($path);
2074
		$absolutePath = Filesystem::normalizePath($absolutePath);
2075
		if (!$this->shouldLockFile($absolutePath)) {
2076
			return false;
2077
		}
2078
2079
		$this->unlockPath($path, $type, $lockMountPoint);
2080
2081
		$parents = $this->getParents($path);
2082
		foreach ($parents as $parent) {
2083
			$this->unlockPath($parent, ILockingProvider::LOCK_SHARED);
2084
		}
2085
2086
		return true;
2087
	}
2088
2089
	/**
2090
	 * Only lock files in data/user/files/
2091
	 *
2092
	 * @param string $path Absolute path to the file/folder we try to (un)lock
2093
	 * @return bool
2094
	 */
2095
	protected function shouldLockFile($path) {
2096
		$path = Filesystem::normalizePath($path);
2097
2098
		$pathSegments = \explode('/', $path);
2099
		if (isset($pathSegments[2])) {
2100
			// E.g.: /username/files/path-to-file
2101
			return ($pathSegments[2] === 'files') && (\count($pathSegments) > 3);
2102
		}
2103
2104
		return true;
2105
	}
2106
2107
	/**
2108
	 * Shortens the given absolute path to be relative to
2109
	 * "$user/files".
2110
	 *
2111
	 * @param string $absolutePath absolute path which is under "files"
2112
	 *
2113
	 * @return string path relative to "files" with trimmed slashes or null
2114
	 * if the path was NOT relative to files
2115
	 *
2116
	 * @throws \InvalidArgumentException if the given path was not under "files"
2117
	 * @since 8.1.0
2118
	 */
2119
	public function getPathRelativeToFiles($absolutePath) {
2120
		$path = Filesystem::normalizePath($absolutePath);
2121
		$parts = \explode('/', \trim($path, '/'), 3);
2122
		// "$user", "files", "path/to/dir"
2123
		if (!isset($parts[1]) || $parts[1] !== 'files') {
2124
			throw new \InvalidArgumentException('"' . $absolutePath . '" must be relative to "files"');
2125
		}
2126
		if (isset($parts[2])) {
2127
			return $parts[2];
2128
		}
2129
		return '';
2130
	}
2131
2132
	/**
2133
	 * @param string $filename
2134
	 * @return array
2135
	 * @throws \OC\User\NoUserException
2136
	 * @throws NotFoundException
2137
	 */
2138
	public function getUidAndFilename($filename) {
2139
		$info = $this->getFileInfo($filename);
2140
		if (!$info instanceof \OCP\Files\FileInfo) {
2141
			throw new NotFoundException($this->getAbsolutePath($filename) . ' not found');
2142
		}
2143
		$uid = $info->getOwner()->getUID();
2144
		if ($uid != \OCP\User::getUser()) {
2145
			Filesystem::initMountPoints($uid);
2146
			$ownerView = new View('/' . $uid . '/files');
2147
			try {
2148
				$filename = $ownerView->getPath($info['fileid']);
2149
			} catch (NotFoundException $e) {
2150
				throw new NotFoundException('File with id ' . $info['fileid'] . ' not found for user ' . $uid);
2151
			}
2152
		}
2153
		return [$uid, $filename];
2154
	}
2155
2156
	/**
2157
	 * Creates parent non-existing folders
2158
	 *
2159
	 * @param string $filePath
2160
	 * @return bool
2161
	 */
2162
	private function createParentDirectories($filePath) {
2163
		$parentDirectory = \dirname($filePath);
2164
		while (!$this->file_exists($parentDirectory)) {
2165
			$result = $this->createParentDirectories($parentDirectory);
2166
			if ($result === false) {
2167
				return false;
2168
			}
2169
		}
2170
		$this->mkdir($filePath);
2171
		return true;
2172
	}
2173
}
2174