Completed
Push — master ( 81153d...2a6722 )
by Thomas
52s
created

View::unlockFile()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 16
Ratio 100 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 3
nop 3
dl 16
loc 16
rs 9.4285
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
47
namespace OC\Files;
48
49
use Icewind\Streams\CallbackWrapper;
50
use OC\Files\Mount\MoveableMount;
51
use OC\Files\Storage\Storage;
52
use OC\User\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 Symfony\Component\EventDispatcher\GenericEvent;
67
68
/**
69
 * Class to provide access to ownCloud filesystem via a "view", and methods for
70
 * working with files within that view (e.g. read, write, delete, etc.). Each
71
 * view is restricted to a set of directories via a virtual root. The default view
72
 * uses the currently logged in user's data directory as root (parts of
73
 * OC_Filesystem are merely a wrapper for OC\Files\View).
74
 *
75
 * Apps that need to access files outside of the user data folders (to modify files
76
 * belonging to a user other than the one currently logged in, for example) should
77
 * use this class directly rather than using OC_Filesystem, or making use of PHP's
78
 * built-in file manipulation functions. This will ensure all hooks and proxies
79
 * are triggered correctly.
80
 *
81
 * Filesystem functions are not called directly; they are passed to the correct
82
 * \OC\Files\Storage\Storage object
83
 */
84
class View {
85
	use EventEmitterTrait;
86
	/** @var string */
87
	private $fakeRoot = '';
88
89
	/**
90
	 * @var \OCP\Lock\ILockingProvider
91
	 */
92
	private $lockingProvider;
93
94
	/** @var bool  */
95
	private $lockingEnabled;
96
97
	/** @var bool  */
98
	private $updaterEnabled = true;
99
100
	/** @var \OC\User\Manager  */
101
	private $userManager;
102
103
	/** @var \OCP\ILogger  */
104
	private $logger;
105
106
	private $eventDispatcher;
107
108
109
	/**
110
	 * @param string $root
111
	 * @throws \Exception If $root contains an invalid path
112
	 */
113
	public function __construct($root = '') {
114
		if (is_null($root)) {
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 (is_null($time)) {
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
360
				$result = $this->basicOperation('rmdir', $path, ['delete']);
361
			} else {
362
				$result = false;
363
			}
364
365 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...
366
				$storage = $mount->getStorage();
367
				$internalPath = $mount->getInternalPath($absolutePath);
368
				$storage->getUpdater()->remove($internalPath);
369
			}
370
371
			return $result;
372
		}, ['before' => ['path' => $this->getAbsolutePath($path)], 'after' => ['path' => $this->getAbsolutePath($path)]], 'file', 'delete');
373
	}
374
375
	/**
376
	 * @param string $path
377
	 * @return resource
378
	 */
379
	public function opendir($path) {
380
		return $this->basicOperation('opendir', $path, ['read']);
381
	}
382
383
	/**
384
	 * @param string $path
385
	 * @return bool|mixed
386
	 */
387
	public function is_dir($path) {
388
		if ($path == '/') {
389
			return true;
390
		}
391
		return $this->basicOperation('is_dir', $path);
392
	}
393
394
	/**
395
	 * @param string $path
396
	 * @return bool|mixed
397
	 */
398
	public function is_file($path) {
399
		if ($path == '/') {
400
			return false;
401
		}
402
		return $this->basicOperation('is_file', $path);
403
	}
404
405
	/**
406
	 * @param string $path
407
	 * @return mixed
408
	 */
409
	public function stat($path) {
410
		return $this->basicOperation('stat', $path);
411
	}
412
413
	/**
414
	 * @param string $path
415
	 * @return mixed
416
	 */
417
	public function filetype($path) {
418
		return $this->basicOperation('filetype', $path);
419
	}
420
421
	/**
422
	 * @param string $path
423
	 * @return mixed
424
	 */
425
	public function filesize($path) {
426
		return $this->basicOperation('filesize', $path);
427
	}
428
429
	/**
430
	 * @param string $path
431
	 * @return bool|mixed
432
	 * @throws \OCP\Files\InvalidPathException
433
	 */
434
	public function readfile($path) {
435
		$this->assertPathLength($path);
436
		@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...
437
		$handle = $this->fopen($path, 'rb');
438
		if ($handle) {
439
			$chunkSize = 8192; // 8 kB chunks
440
			while (!feof($handle)) {
441
				echo fread($handle, $chunkSize);
442
				flush();
443
			}
444
			$size = $this->filesize($path);
445
			return $size;
446
		}
447
		return false;
448
	}
449
450
	/**
451
	 * @param string $path
452
	 * @param int $from
453
	 * @param int $to
454
	 * @return bool|mixed
455
	 * @throws \OCP\Files\InvalidPathException
456
	 * @throws \OCP\Files\UnseekableException
457
	 */
458
	public function readfilePart($path, $from, $to) {
459
		$this->assertPathLength($path);
460
		@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...
461
		$handle = $this->fopen($path, 'rb');
462
		if ($handle) {
463
			if (fseek($handle, $from) === 0) {
464
				$chunkSize = 8192; // 8 kB chunks
465
				$end = $to + 1;
466
				while (!feof($handle) && ftell($handle) < $end) {
467
					$len = $end - ftell($handle);
468
					if ($len > $chunkSize) {
469
						$len = $chunkSize;
470
					}
471
					echo fread($handle, $len);
472
					flush();
473
				}
474
				$size = ftell($handle) - $from;
475
				return $size;
476
			}
477
478
			throw new \OCP\Files\UnseekableException('fseek error');
479
		}
480
		return false;
481
	}
482
483
	/**
484
	 * @param string $path
485
	 * @return mixed
486
	 */
487
	public function isCreatable($path) {
488
		return $this->basicOperation('isCreatable', $path);
489
	}
490
491
	/**
492
	 * @param string $path
493
	 * @return mixed
494
	 */
495
	public function isReadable($path) {
496
		return $this->basicOperation('isReadable', $path);
497
	}
498
499
	/**
500
	 * @param string $path
501
	 * @return mixed
502
	 */
503
	public function isUpdatable($path) {
504
		return $this->basicOperation('isUpdatable', $path);
505
	}
506
507
	/**
508
	 * @param string $path
509
	 * @return bool|mixed
510
	 */
511
	public function isDeletable($path) {
512
		$absolutePath = $this->getAbsolutePath($path);
513
		$mount = Filesystem::getMountManager()->find($absolutePath);
514
		if ($mount->getInternalPath($absolutePath) === '') {
515
			return $mount instanceof MoveableMount;
516
		}
517
		return $this->basicOperation('isDeletable', $path);
518
	}
519
520
	/**
521
	 * @param string $path
522
	 * @return mixed
523
	 */
524
	public function isSharable($path) {
525
		return $this->basicOperation('isSharable', $path);
526
	}
527
528
	/**
529
	 * @param string $path
530
	 * @return bool|mixed
531
	 */
532
	public function file_exists($path) {
533
		if ($path == '/') {
534
			return true;
535
		}
536
		return $this->basicOperation('file_exists', $path);
537
	}
538
539
	/**
540
	 * @param string $path
541
	 * @return mixed
542
	 */
543
	public function filemtime($path) {
544
		return $this->basicOperation('filemtime', $path);
545
	}
546
547
	/**
548
	 * @param string $path
549
	 * @param int|string $mtime
550
	 * @return bool
551
	 */
552
	public function touch($path, $mtime = null) {
553
		if (!is_null($mtime) and !is_numeric($mtime)) {
554
			$mtime = strtotime($mtime);
555
		}
556
557
		$hooks = ['touch'];
558
559
		if (!$this->file_exists($path)) {
560
			$hooks[] = 'create';
561
			$hooks[] = 'write';
562
		}
563
		$result = $this->basicOperation('touch', $path, $hooks, $mtime);
564
		if (!$result) {
565
			// If create file fails because of permissions on external storage like SMB folders,
566
			// check file exists and return false if not.
567
			if (!$this->file_exists($path)) {
568
				return false;
569
			}
570
			if (is_null($mtime)) {
571
				$mtime = time();
572
			}
573
			//if native touch fails, we emulate it by changing the mtime in the cache
574
			$this->putFileInfo($path, ['mtime' => $mtime]);
575
		}
576
		return true;
577
	}
578
579
	/**
580
	 * @param string $path
581
	 * @return mixed
582
	 */
583
	public function file_get_contents($path) {
584
		return $this->emittingCall(function () use (&$path) {
585
			return $this->basicOperation('file_get_contents', $path, ['read']);
586
		}, ['before' => ['path' => $this->getAbsolutePath($path)], 'after' => ['path' => $this->getAbsolutePath($path)]], 'file', 'read');
587
	}
588
589
	/**
590
	 * @param bool $exists
591
	 * @param string $path
592
	 * @param bool $run
593
	 */
594
	protected function emit_file_hooks_pre($exists, $path, &$run) {
595
		$event = new GenericEvent(null);
596
		if (!$exists) {
597
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
598
				Filesystem::signal_param_path => $this->getHookPath($path),
599
				Filesystem::signal_param_run => &$run,
600
			]);
601 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...
602
				$event->setArgument('run', $run);
603
				$this->eventDispatcher->dispatch('file.beforeCreate', $event);
604
				if ($event->getArgument('run') === false) {
605
					$run = $event->getArgument('run');
606
				}
607
			}
608
		} else {
609
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
610
				Filesystem::signal_param_path => $this->getHookPath($path),
611
				Filesystem::signal_param_run => &$run,
612
			]);
613 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...
614
				$event->setArgument('run', $run);
615
				$this->eventDispatcher->dispatch('file.beforeUpdate', $event);
616
				if ($event->getArgument('run') === false) {
617
					$run = $event->getArgument('run');
618
				}
619
			}
620
		}
621
622
		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
623
			Filesystem::signal_param_path => $this->getHookPath($path),
624
			Filesystem::signal_param_run => &$run,
625
		]);
626 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...
627
			$event->setArgument('run', $run);
628
			$this->eventDispatcher->dispatch('file.beforeWrite', $event);
629
			if ($event->getArgument('run') === false) {
630
				$run = $event->getArgument('run');
631
			}
632
		}
633
	}
634
635
	/**
636
	 * @param bool $exists
637
	 * @param string $path
638
	 */
639
	protected function emit_file_hooks_post($exists, $path) {
640
		// A post event so no before event args required
641
		return $this->emittingCall(function () use (&$exists, &$path){
642 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...
643
				\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
644
					Filesystem::signal_param_path => $this->getHookPath($path),
645
				]);
646
647
			} else {
648
				\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
649
					Filesystem::signal_param_path => $this->getHookPath($path),
650
				]);
651
			}
652
			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
653
				Filesystem::signal_param_path => $this->getHookPath($path),
654
			]);
655
		}, ['before' => ['path' => $path], 'after' => ['path' => $this->getAbsolutePath($path)]], 'file', 'create');
656
	}
657
658
	/**
659
	 * @param string $path
660
	 * @param mixed $data
661
	 * @return bool|mixed
662
	 * @throws \Exception
663
	 */
664
	public function file_put_contents($path, $data) {
665
		return $this->emittingCall(function () use (&$path, &$data) {
666
			if (is_resource($data)) { //not having to deal with streams in file_put_contents makes life easier
667
				$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
668
				if (Filesystem::isValidPath($path)
669
					and !Filesystem::isForbiddenFileOrDir($path)
670
				) {
671
					$path = $this->getRelativePath($absolutePath);
672
673
					$this->lockFile($path, ILockingProvider::LOCK_SHARED);
674
675
					$exists = $this->file_exists($path);
676
					$run = true;
677
					if ($this->shouldEmitHooks($path)) {
678
						$this->emit_file_hooks_pre($exists, $path, $run);
679
					}
680
					if (!$run) {
681
						$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
682
						return false;
683
					}
684
685
					$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
686
687
					/** @var \OC\Files\Storage\Storage $storage */
688
					list($storage, $internalPath) = $this->resolvePath($path);
689
					$target = $storage->fopen($internalPath, 'w');
690
					if ($target) {
691
						list (, $result) = \OC_Helper::streamCopy($data, $target);
692
						fclose($target);
693
						fclose($data);
694
695
						$this->writeUpdate($storage, $internalPath);
696
697
						$this->changeLock($path, ILockingProvider::LOCK_SHARED);
698
699
						if ($this->shouldEmitHooks($path) && $result !== false) {
700
							$this->emit_file_hooks_post($exists, $path);
701
						}
702
						$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
703
						return $result;
704
					} else {
705
						$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
706
						return false;
707
					}
708
				} else {
709
					return false;
710
				}
711
			} else {
712
				$hooks = ($this->file_exists($path)) ? ['update', 'write'] : ['create', 'write'];
713
				return $this->basicOperation('file_put_contents', $path, $hooks, $data);
714
			}
715
		}, ['before' => ['path' => $this->getAbsolutePath($path)], 'after' => ['path' => $this->getAbsolutePath($path)]], 'file', 'update');
716
	}
717
718
	/**
719
	 * @param string $path
720
	 * @return bool|mixed
721
	 */
722
	public function unlink($path) {
723
		return $this->emittingCall(function () use (&$path) {
724
			if ($path === '' || $path === '/') {
725
				// do not allow deleting the root
726
				return false;
727
			}
728
			$postFix = (substr($path, -1, 1) === '/') ? '/' : '';
729
			$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
730
			$mount = Filesystem::getMountManager()->find($absolutePath . $postFix);
731
			if ($mount and $mount->getInternalPath($absolutePath) === '') {
732
				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...
733
			}
734
			if ($this->is_dir($path)) {
735
				$result = $this->basicOperation('rmdir', $path, array('delete'));
736
			} else {
737
				$result = $this->basicOperation('unlink', $path, array('delete'));
738
			}
739
740 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...
741
				$storage = $mount->getStorage();
742
				$internalPath = $mount->getInternalPath($absolutePath);
743
				$storage->getUpdater()->remove($internalPath);
744
				return true;
745
			} else {
746
				return $result;
747
			}
748
		}, ['before' => ['path' => $this->getAbsolutePath($path)], 'after' => ['path' => $this->getAbsolutePath($path)]], 'file', 'delete');
749
	}
750
751
	/**
752
	 * @param string $directory
753
	 * @return bool|mixed
754
	 */
755
	public function deleteAll($directory) {
756
		return $this->rmdir($directory);
757
	}
758
759
	/**
760
	 * Rename/move a file or folder from the source path to target path.
761
	 *
762
	 * @param string $path1 source path
763
	 * @param string $path2 target path
764
	 *
765
	 * @return bool|mixed
766
	 */
767
	public function rename($path1, $path2) {
768
		return $this->emittingCall(function () use (&$path1, &$path2) {
769
			$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($path1));
770
			$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($path2));
771
			$result = false;
772
			if (
773
				Filesystem::isValidPath($path2)
774
				and Filesystem::isValidPath($path1)
775
				and !Filesystem::isForbiddenFileOrDir($path2)
776
			) {
777
				$path1 = $this->getRelativePath($absolutePath1);
778
				$path2 = $this->getRelativePath($absolutePath2);
779
				$exists = $this->file_exists($path2);
780
781
				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...
782
					return false;
783
				}
784
785
				$this->lockFile($path1, ILockingProvider::LOCK_SHARED, true);
786
				try {
787
					$this->lockFile($path2, ILockingProvider::LOCK_SHARED, true);
788
				} catch (LockedException $e) {
789
					$this->unlockFile($path1, ILockingProvider::LOCK_SHARED);
790
					throw $e;
791
				}
792
793
				$run = true;
794
				if ($this->shouldEmitHooks($path1) && (Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2))) {
795
					// if it was a rename from a part file to a regular file it was a write and not a rename operation
796
					$this->emit_file_hooks_pre($exists, $path2, $run);
797
				} elseif ($this->shouldEmitHooks($path1)) {
798
					\OC_Hook::emit(
799
						Filesystem::CLASSNAME, Filesystem::signal_rename,
800
						[
801
							Filesystem::signal_param_oldpath => $this->getHookPath($path1),
802
							Filesystem::signal_param_newpath => $this->getHookPath($path2),
803
							Filesystem::signal_param_run => &$run
804
						]
805
					);
806
				}
807
				if ($run) {
808
					$this->verifyPath(dirname($path2), basename($path2));
809
810
					$manager = Filesystem::getMountManager();
811
					$mount1 = $this->getMount($path1);
812
					$mount2 = $this->getMount($path2);
813
					$storage1 = $mount1->getStorage();
814
					$storage2 = $mount2->getStorage();
815
					$internalPath1 = $mount1->getInternalPath($absolutePath1);
816
					$internalPath2 = $mount2->getInternalPath($absolutePath2);
817
818
					$this->changeLock($path1, ILockingProvider::LOCK_EXCLUSIVE, true);
819
					$this->changeLock($path2, ILockingProvider::LOCK_EXCLUSIVE, true);
820
821
					if ($internalPath1 === '' and $mount1 instanceof MoveableMount) {
822
						if ($this->canMove($mount1, $absolutePath2)) {
823
							/**
824
							 * @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
825
							 */
826
							$sourceMountPoint = $mount1->getMountPoint();
827
							$result = $mount1->moveMount($absolutePath2);
828
							$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
829
						} else {
830
							$result = false;
831
						}
832
						// moving a file/folder within the same mount point
833
					} elseif ($storage1 === $storage2) {
834
						if ($storage1) {
835
							$result = $storage1->rename($internalPath1, $internalPath2);
836
						} else {
837
							$result = false;
838
						}
839
						// moving a file/folder between storages (from $storage1 to $storage2)
840
					} else {
841
						$result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
842
					}
843
844
					if ((Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2)) && $result !== false) {
845
						// if it was a rename from a part file to a regular file it was a write and not a rename operation
846
847
						$this->writeUpdate($storage2, $internalPath2);
848
					} else if ($result) {
849
						if ($internalPath1 !== '') { // don't do a cache update for moved mounts
850
							$this->renameUpdate($storage1, $storage2, $internalPath1, $internalPath2);
851
						}
852
					}
853
854
					$this->changeLock($path1, ILockingProvider::LOCK_SHARED, true);
855
					$this->changeLock($path2, ILockingProvider::LOCK_SHARED, true);
856
857
					if ((Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2)) && $result !== false) {
858
						if ($this->shouldEmitHooks()) {
859
							$this->emit_file_hooks_post($exists, $path2);
860
						}
861
					} elseif ($result) {
862
						if ($this->shouldEmitHooks($path1) and $this->shouldEmitHooks($path2)) {
863
							\OC_Hook::emit(
864
								Filesystem::CLASSNAME,
865
								Filesystem::signal_post_rename,
866
								[
867
									Filesystem::signal_param_oldpath => $this->getHookPath($path1),
868
									Filesystem::signal_param_newpath => $this->getHookPath($path2)
869
								]
870
							);
871
						}
872
					}
873
				}
874
				$this->unlockFile($path1, ILockingProvider::LOCK_SHARED, true);
875
				$this->unlockFile($path2, ILockingProvider::LOCK_SHARED, true);
876
			}
877
878
			return $result;
879
		}, [
880
			'before' => ['oldpath' => $this->getAbsolutePath($path1),
881
				'newpath' => $this->getAbsolutePath($path2)],
882
			'after' => ['oldpath' => $this->getAbsolutePath($path1),
883
				'newpath' => $this->getAbsolutePath($path2)]
884
		], 'file', 'rename');
885
	}
886
887
	/**
888
	 * Copy a file/folder from the source path to target path
889
	 *
890
	 * @param string $path1 source path
891
	 * @param string $path2 target path
892
	 * @param bool $preserveMtime whether to preserve mtime on the copy
893
	 *
894
	 * @return bool|mixed
895
	 */
896
	public function copy($path1, $path2, $preserveMtime = false) {
897
		return $this->emittingCall(function () use (&$path1, &$path2, &$preserveMtime) {
898
			$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($path1));
899
			$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($path2));
900
			$result = false;
901
			if (
902
				Filesystem::isValidPath($path2)
903
				and Filesystem::isValidPath($path1)
904
				and !Filesystem::isForbiddenFileOrDir($path2)
905
			) {
906
				$path1 = $this->getRelativePath($absolutePath1);
907
				$path2 = $this->getRelativePath($absolutePath2);
908
909
				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...
910
					return false;
911
				}
912
				$run = true;
913
914
				$this->lockFile($path2, ILockingProvider::LOCK_SHARED);
915
				$this->lockFile($path1, ILockingProvider::LOCK_SHARED);
916
				$lockTypePath1 = ILockingProvider::LOCK_SHARED;
917
				$lockTypePath2 = ILockingProvider::LOCK_SHARED;
918
919
				try {
920
921
					$exists = $this->file_exists($path2);
922 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...
923
						\OC_Hook::emit(
924
							Filesystem::CLASSNAME,
925
							Filesystem::signal_copy,
926
							[
927
								Filesystem::signal_param_oldpath => $this->getHookPath($path1),
928
								Filesystem::signal_param_newpath => $this->getHookPath($path2),
929
								Filesystem::signal_param_run => &$run
930
							]
931
						);
932
						$this->emit_file_hooks_pre($exists, $path2, $run);
933
					}
934
					if ($run) {
935
						$mount1 = $this->getMount($path1);
936
						$mount2 = $this->getMount($path2);
937
						$storage1 = $mount1->getStorage();
938
						$internalPath1 = $mount1->getInternalPath($absolutePath1);
939
						$storage2 = $mount2->getStorage();
940
						$internalPath2 = $mount2->getInternalPath($absolutePath2);
941
942
						$this->changeLock($path2, ILockingProvider::LOCK_EXCLUSIVE);
943
						$lockTypePath2 = ILockingProvider::LOCK_EXCLUSIVE;
944
945
						if ($mount1->getMountPoint() == $mount2->getMountPoint()) {
946
							if ($storage1) {
947
								$result = $storage1->copy($internalPath1, $internalPath2);
948
							} else {
949
								$result = false;
950
							}
951
						} else {
952
							$result = $storage2->copyFromStorage($storage1, $internalPath1, $internalPath2);
953
						}
954
955
						$this->writeUpdate($storage2, $internalPath2);
956
957
						$this->changeLock($path2, ILockingProvider::LOCK_SHARED);
958
						$lockTypePath2 = ILockingProvider::LOCK_SHARED;
959
960 View Code Duplication
						if ($this->shouldEmitHooks() && $result !== false) {
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...
961
							\OC_Hook::emit(
962
								Filesystem::CLASSNAME,
963
								Filesystem::signal_post_copy,
964
								[
965
									Filesystem::signal_param_oldpath => $this->getHookPath($path1),
966
									Filesystem::signal_param_newpath => $this->getHookPath($path2)
967
								]
968
							);
969
							$this->emit_file_hooks_post($exists, $path2);
970
						}
971
972
					}
973
				} catch (\Exception $e) {
974
					$this->unlockFile($path2, $lockTypePath2);
975
					$this->unlockFile($path1, $lockTypePath1);
976
					throw $e;
977
				}
978
979
				$this->unlockFile($path2, $lockTypePath2);
980
				$this->unlockFile($path1, $lockTypePath1);
981
982
			}
983
984
			return $result;
985
		}, ['before' => [
986
				'oldpath' => $this->getAbsolutePath($path1),
987
				'newpath' => $this->getAbsolutePath($path2)],
988
			'after' => [
989
				'oldpath' => $this->getAbsolutePath($path1),
990
				'newpath' => $this->getAbsolutePath($path2)
991
			]], 'file', 'copy');
992
	}
993
994
	/**
995
	 * @param string $path
996
	 * @param string $mode
997
	 * @return resource
998
	 */
999
	public function fopen($path, $mode) {
1000
		$hooks = [];
1001
		switch ($mode) {
1002
			case 'r':
1003
			case 'rb':
1004
				$hooks[] = 'read';
1005
				break;
1006
			case 'r+':
1007
			case 'rb+':
1008
			case 'w+':
1009
			case 'wb+':
1010
			case 'x+':
1011
			case 'xb+':
1012
			case 'a+':
1013
			case 'ab+':
1014
				$hooks[] = 'read';
1015
				$hooks[] = 'write';
1016
				break;
1017
			case 'w':
1018
			case 'wb':
1019
			case 'x':
1020
			case 'xb':
1021
			case 'a':
1022
			case 'ab':
1023
				$hooks[] = 'write';
1024
				break;
1025
			default:
1026
				Util::writeLog('core', 'invalid mode (' . $mode . ') for ' . $path, Util::ERROR);
1027
		}
1028
1029
		return $this->basicOperation('fopen', $path, $hooks, $mode);
1030
	}
1031
1032
	/**
1033
	 * @param string $path
1034
	 * @return bool|string
1035
	 * @throws \OCP\Files\InvalidPathException
1036
	 */
1037
	public function toTmpFile($path) {
1038
		$this->assertPathLength($path);
1039
		if (Filesystem::isValidPath($path)) {
1040
			$source = $this->fopen($path, 'r');
1041
			if ($source) {
1042
				$extension = pathinfo($path, PATHINFO_EXTENSION);
1043
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($extension);
1044
				file_put_contents($tmpFile, $source);
1045
				return $tmpFile;
1046
			} else {
1047
				return false;
1048
			}
1049
		} else {
1050
			return false;
1051
		}
1052
	}
1053
1054
	/**
1055
	 * @param string $tmpFile
1056
	 * @param string $path
1057
	 * @return bool|mixed
1058
	 * @throws \OCP\Files\InvalidPathException
1059
	 */
1060
	public function fromTmpFile($tmpFile, $path) {
1061
		$this->assertPathLength($path);
1062
		if (Filesystem::isValidPath($path)) {
1063
1064
			// Get directory that the file is going into
1065
			$filePath = dirname($path);
1066
1067
			// Create the directories if any
1068
			if (!$this->file_exists($filePath)) {
1069
				$result = $this->createParentDirectories($filePath);
1070
				if($result === false) {
1071
					return false;
1072
				}
1073
			}
1074
1075
			$source = fopen($tmpFile, 'r');
1076
			if ($source) {
1077
				$result = $this->file_put_contents($path, $source);
1078
				// $this->file_put_contents() might have already closed
1079
				// the resource, so we check it, before trying to close it
1080
				// to avoid messages in the error log.
1081
				if (is_resource($source)) {
1082
					fclose($source);
1083
				}
1084
				unlink($tmpFile);
1085
				return $result;
1086
			} else {
1087
				return false;
1088
			}
1089
		} else {
1090
			return false;
1091
		}
1092
	}
1093
1094
1095
	/**
1096
	 * @param string $path
1097
	 * @return mixed
1098
	 * @throws \OCP\Files\InvalidPathException
1099
	 */
1100
	public function getMimeType($path) {
1101
		$this->assertPathLength($path);
1102
		return $this->basicOperation('getMimeType', $path);
1103
	}
1104
1105
	/**
1106
	 * @param string $type
1107
	 * @param string $path
1108
	 * @param bool $raw
1109
	 * @return bool|null|string
1110
	 */
1111
	public function hash($type, $path, $raw = false) {
1112
		$postFix = (substr($path, -1, 1) === '/') ? '/' : '';
1113
		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
1114
		if (Filesystem::isValidPath($path)) {
1115
			$path = $this->getRelativePath($absolutePath);
1116
			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...
1117
				return false;
1118
			}
1119
			if ($this->shouldEmitHooks($path)) {
1120
				\OC_Hook::emit(
1121
					Filesystem::CLASSNAME,
1122
					Filesystem::signal_read,
1123
					[Filesystem::signal_param_path => $this->getHookPath($path)]
1124
				);
1125
			}
1126
1127
			list($storage, $internalPath) = Filesystem::resolvePath($absolutePath . $postFix);
1128
			if ($storage) {
1129
				$result = $storage->hash($type, $internalPath, $raw);
1130
				return $result;
1131
			}
1132
		}
1133
		return null;
1134
	}
1135
1136
	/**
1137
	 * @param string $path
1138
	 * @return mixed
1139
	 * @throws \OCP\Files\InvalidPathException
1140
	 */
1141
	public function free_space($path = '/') {
1142
		$this->assertPathLength($path);
1143
		return $this->basicOperation('free_space', $path);
1144
	}
1145
1146
	/**
1147
	 * abstraction layer for basic filesystem functions: wrapper for \OC\Files\Storage\Storage
1148
	 *
1149
	 * @param string $operation
1150
	 * @param string $path
1151
	 * @param array $hooks (optional)
1152
	 * @param mixed $extraParam (optional)
1153
	 * @return mixed
1154
	 * @throws \Exception
1155
	 *
1156
	 * This method takes requests for basic filesystem functions (e.g. reading & writing
1157
	 * files), processes hooks and proxies, sanitises paths, and finally passes them on to
1158
	 * \OC\Files\Storage\Storage for delegation to a storage backend for execution
1159
	 */
1160
	private function basicOperation($operation, $path, $hooks = [], $extraParam = null) {
1161
		$postFix = (substr($path, -1, 1) === '/') ? '/' : '';
1162
		$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
1163
		if (Filesystem::isValidPath($path)
1164
			and !Filesystem::isForbiddenFileOrDir($path)
1165
		) {
1166
			$path = $this->getRelativePath($absolutePath);
1167
			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...
1168
				return false;
1169
			}
1170
1171
			if (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks)) {
1172
				// always a shared lock during pre-hooks so the hook can read the file
1173
				$this->lockFile($path, ILockingProvider::LOCK_SHARED);
1174
			}
1175
1176
			$run = $this->runHooks($hooks, $path);
1177
			/** @var \OC\Files\Storage\Storage $storage */
1178
			list($storage, $internalPath) = Filesystem::resolvePath($absolutePath . $postFix);
1179
			if ($run and $storage) {
1180
				if (in_array('write', $hooks) || in_array('delete', $hooks)) {
1181
					$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
1182
				}
1183
				try {
1184
					if (!is_null($extraParam)) {
1185
						$result = $storage->$operation($internalPath, $extraParam);
1186
					} else {
1187
						$result = $storage->$operation($internalPath);
1188
					}
1189
				} catch (\Exception $e) {
1190 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...
1191
						$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
1192
					} else if (in_array('read', $hooks)) {
1193
						$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1194
					}
1195
					throw $e;
1196
				}
1197
1198
				if (in_array('delete', $hooks) and $result) {
1199
					$this->removeUpdate($storage, $internalPath);
1200
				}
1201
				if (in_array('write', $hooks) and $operation !== 'fopen') {
1202
					$this->writeUpdate($storage, $internalPath);
1203
				}
1204
				if (in_array('touch', $hooks)) {
1205
					$this->writeUpdate($storage, $internalPath, $extraParam);
1206
				}
1207
1208
				if ((in_array('write', $hooks) || in_array('delete', $hooks)) && ($operation !== 'fopen' || $result === false)) {
1209
					$this->changeLock($path, ILockingProvider::LOCK_SHARED);
1210
				}
1211
1212
				$unlockLater = false;
1213
				if ($this->lockingEnabled && $operation === 'fopen' && is_resource($result)) {
1214
					$unlockLater = true;
1215
					// make sure our unlocking callback will still be called if connection is aborted
1216
					ignore_user_abort(true);
1217
					$result = CallbackWrapper::wrap($result, null, null, function () use ($hooks, $path) {
1218 View Code Duplication
						if (in_array('write', $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...
1219
							$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
1220
						} else if (in_array('read', $hooks)) {
1221
							$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1222
						}
1223
					});
1224
				}
1225
1226
				if ($this->shouldEmitHooks($path) && $result !== false) {
1227
					if ($operation != 'fopen') { //no post hooks for fopen, the file stream is still open
1228
						$this->runHooks($hooks, $path, true);
1229
					}
1230
				}
1231
1232 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...
1233
					&& (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks))
1234
				) {
1235
					$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1236
				}
1237
				return $result;
1238
			} else {
1239
				$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
1240
			}
1241
		}
1242
		return null;
1243
	}
1244
1245
	/**
1246
	 * get the path relative to the default root for hook usage
1247
	 *
1248
	 * @param string $path
1249
	 * @return string
1250
	 */
1251
	private function getHookPath($path) {
1252
		if (!Filesystem::getView()) {
1253
			return $path;
1254
		}
1255
		return Filesystem::getView()->getRelativePath($this->getAbsolutePath($path));
1256
	}
1257
1258
	private function shouldEmitHooks($path = '') {
1259
		if ($path && Cache\Scanner::isPartialFile($path)) {
1260
			return false;
1261
		}
1262
		if (!Filesystem::$loaded) {
1263
			return false;
1264
		}
1265
		$defaultRoot = Filesystem::getRoot();
1266
		if ($defaultRoot === null) {
1267
			return false;
1268
		}
1269
		if ($this->fakeRoot === $defaultRoot) {
1270
			return true;
1271
		}
1272
		$fullPath = $this->getAbsolutePath($path);
1273
1274
		if ($fullPath === $defaultRoot) {
1275
			return true;
1276
		}
1277
1278
		return (strlen($fullPath) > strlen($defaultRoot)) && (substr($fullPath, 0, strlen($defaultRoot) + 1) === $defaultRoot . '/');
1279
	}
1280
1281
	/**
1282
	 * @param string[] $hooks
1283
	 * @param string $path
1284
	 * @param bool $post
1285
	 * @return bool
1286
	 */
1287
	private function runHooks($hooks, $path, $post = false) {
1288
		if (empty($hooks)) {
1289
			return true;
1290
		}
1291
		$relativePath = $path;
1292
		$path = $this->getHookPath($path);
1293
		$prefix = ($post) ? 'post_' : '';
1294
		$run = true;
1295
		if ($this->shouldEmitHooks($relativePath)) {
1296
			foreach ($hooks as $hook) {
1297
				if ($hook != 'read') {
1298
					\OC_Hook::emit(
1299
						Filesystem::CLASSNAME,
1300
						$prefix . $hook,
1301
						[
1302
							Filesystem::signal_param_run => &$run,
1303
							Filesystem::signal_param_path => $path
1304
						]
1305
					);
1306
				} elseif (!$post) {
1307
					\OC_Hook::emit(
1308
						Filesystem::CLASSNAME,
1309
						$prefix . $hook,
1310
						[
1311
							Filesystem::signal_param_path => $path
1312
						]
1313
					);
1314
				}
1315
			}
1316
		}
1317
		return $run;
1318
	}
1319
1320
	/**
1321
	 * check if a file or folder has been updated since $time
1322
	 *
1323
	 * @param string $path
1324
	 * @param int $time
1325
	 * @return bool
1326
	 */
1327
	public function hasUpdated($path, $time) {
1328
		return $this->basicOperation('hasUpdated', $path, [], $time);
1329
	}
1330
1331
	/**
1332
	 * @param string $ownerId
1333
	 * @return IUser
1334
	 */
1335
	private function getUserObjectForOwner($ownerId) {
1336
		$owner = $this->userManager->get($ownerId);
1337
		if (!$owner instanceof IUser) {
1338
			return new RemoteUser($ownerId);
1339
		}
1340
1341
		return $owner;
1342
	}
1343
1344
	/**
1345
	 * Get file info from cache
1346
	 *
1347
	 * If the file is not in cached it will be scanned
1348
	 * If the file has changed on storage the cache will be updated
1349
	 *
1350
	 * @param \OC\Files\Storage\Storage $storage
1351
	 * @param string $internalPath
1352
	 * @param string $relativePath
1353
	 * @return array|bool
1354
	 */
1355
	private function getCacheEntry($storage, $internalPath, $relativePath) {
1356
		$cache = $storage->getCache($internalPath);
1357
		$data = $cache->get($internalPath);
1358
		$watcher = $storage->getWatcher($internalPath);
1359
1360
		try {
1361
			// if the file is not in the cache or needs to be updated, trigger the scanner and reload the data
1362
			if (!$data || (isset($data['size']) && ($data['size'] === -1))) {
1363
				$this->lockFile($relativePath, ILockingProvider::LOCK_SHARED);
1364
				if (!$storage->file_exists($internalPath)) {
1365
					$this->unlockFile($relativePath, ILockingProvider::LOCK_SHARED);
1366
					return false;
1367
				}
1368
				$scanner = $storage->getScanner($internalPath);
1369
				$scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW);
1370
				$data = $cache->get($internalPath);
1371
				$this->unlockFile($relativePath, ILockingProvider::LOCK_SHARED);
1372
			} else if (!Cache\Scanner::isPartialFile($internalPath) && $watcher->needsUpdate($internalPath, $data)) {
1373
				$this->lockFile($relativePath, ILockingProvider::LOCK_SHARED);
1374
				$watcher->update($internalPath, $data);
1375
				$storage->getPropagator()->propagateChange($internalPath, time());
1376
				$data = $cache->get($internalPath);
1377
				$this->unlockFile($relativePath, ILockingProvider::LOCK_SHARED);
1378
			}
1379
		} catch (LockedException $e) {
1380
			// if the file is locked we just use the old cache info
1381
		}
1382
1383
		return $data;
1384
	}
1385
1386
	/**
1387
	 * get the filesystem info
1388
	 *
1389
	 * @param string $path
1390
	 * @param boolean|string $includeMountPoints true to add mountpoint sizes,
1391
	 * 'ext' to add only ext storage mount point sizes. Defaults to true.
1392
	 * defaults to true
1393
	 * @return \OC\Files\FileInfo|false False if file does not exist
1394
	 */
1395
	public function getFileInfo($path, $includeMountPoints = true) {
1396
		$this->assertPathLength($path);
1397
		if (!Filesystem::isValidPath($path)) {
1398
			return false;
1399
		}
1400
		if (Cache\Scanner::isPartialFile($path)) {
1401
			return $this->getPartFileInfo($path);
1402
		}
1403
		$relativePath = $path;
1404
		$path = Filesystem::normalizePath($this->fakeRoot . '/' . $path);
1405
1406
		$mount = Filesystem::getMountManager()->find($path);
1407
		$storage = $mount->getStorage();
1408
		$internalPath = $mount->getInternalPath($path);
1409
		if ($storage) {
1410
			$data = $this->getCacheEntry($storage, $internalPath, $relativePath);
1411
1412
			if (!$data instanceof ICacheEntry) {
1413
				return false;
1414
			}
1415
1416
			if ($mount instanceof MoveableMount && $internalPath === '') {
1417
				$data['permissions'] |= \OCP\Constants::PERMISSION_DELETE;
1418
			}
1419
1420
			$owner = $this->getUserObjectForOwner($storage->getOwner($internalPath));
1421
			$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 1406 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...
1422
1423
			if ($data and isset($data['fileid'])) {
1424
				if ($includeMountPoints and $data['mimetype'] === 'httpd/unix-directory') {
1425
					//add the sizes of other mount points to the folder
1426
					$extOnly = ($includeMountPoints === 'ext');
1427
					$mounts = Filesystem::getMountManager()->findIn($path);
1428
					foreach ($mounts as $mount) {
1429
						$subStorage = $mount->getStorage();
1430
						if ($subStorage) {
1431
							// exclude shared storage ?
1432
							if ($extOnly && $subStorage instanceof \OCA\Files_Sharing\SharedStorage) {
1433
								continue;
1434
							}
1435
							$subCache = $subStorage->getCache('');
1436
							$rootEntry = $subCache->get('');
1437
							$info->addSubEntry($rootEntry, $mount->getMountPoint());
1438
						}
1439
					}
1440
				}
1441
			}
1442
1443
			return $info;
1444
		}
1445
1446
		return false;
1447
	}
1448
1449
	/**
1450
	 * get the content of a directory
1451
	 *
1452
	 * @param string $directory path under datadirectory
1453
	 * @param string $mimetype_filter limit returned content to this mimetype or mimepart
1454
	 * @return FileInfo[]
1455
	 */
1456
	public function getDirectoryContent($directory, $mimetype_filter = '') {
1457
		$this->assertPathLength($directory);
1458
		if (!Filesystem::isValidPath($directory)) {
1459
			return [];
1460
		}
1461
		$path = $this->getAbsolutePath($directory);
1462
		$path = Filesystem::normalizePath($path);
1463
		$mount = $this->getMount($directory);
1464
		$storage = $mount->getStorage();
1465
		$internalPath = $mount->getInternalPath($path);
1466
		if ($storage) {
1467
			$cache = $storage->getCache($internalPath);
1468
			$user = \OC_User::getUser();
1469
1470
			$data = $this->getCacheEntry($storage, $internalPath, $directory);
1471
1472
			if (!$data instanceof ICacheEntry || !isset($data['fileid']) || !($data->getPermissions() && Constants::PERMISSION_READ)) {
1473
				return [];
1474
			}
1475
1476
			$folderId = $data['fileid'];
1477
			$contents = $cache->getFolderContentsById($folderId); //TODO: mimetype_filter
1478
1479
			$sharingDisabled = Util::isSharingDisabledForUser();
1480
			/**
1481
			 * @var \OC\Files\FileInfo[] $files
1482
			 */
1483
			$files = array_filter($contents, function(ICacheEntry $content) {
1484
				return (!\OC\Files\Filesystem::isForbiddenFileOrDir($content['path']));
1485
			});
1486
			$files = array_map(function (ICacheEntry $content) use ($path, $storage, $mount, $sharingDisabled) {
1487
				if ($sharingDisabled) {
1488
					$content['permissions'] = $content['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
1489
				}
1490
				$owner = $this->getUserObjectForOwner($storage->getOwner($content['path']));
1491
				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 1463 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...
1492
			}, $files);
1493
1494
			//add a folder for any mountpoint in this directory and add the sizes of other mountpoints to the folders
1495
			$mounts = Filesystem::getMountManager()->findIn($path);
1496
			$dirLength = strlen($path);
1497
			foreach ($mounts as $mount) {
1498
				$mountPoint = $mount->getMountPoint();
1499
				$subStorage = $mount->getStorage();
1500
				if ($subStorage) {
1501
					$subCache = $subStorage->getCache('');
1502
1503
					$rootEntry = $subCache->get('');
1504
					if (!$rootEntry) {
1505
						$subScanner = $subStorage->getScanner('');
1506
						try {
1507
							$subScanner->scanFile('');
1508
						} catch (\OCP\Files\StorageNotAvailableException $e) {
1509
							continue;
1510
						} catch (\OCP\Files\StorageInvalidException $e) {
1511
							continue;
1512
						} catch (\Exception $e) {
1513
							// sometimes when the storage is not available it can be any exception
1514
							Util::writeLog(
1515
								'core',
1516
								'Exception while scanning storage "' . $subStorage->getId() . '": ' .
1517
								get_class($e) . ': ' . $e->getMessage(),
1518
								Util::ERROR
1519
							);
1520
							continue;
1521
						}
1522
						$rootEntry = $subCache->get('');
1523
					}
1524
1525
					if ($rootEntry && ($rootEntry->getPermissions() && Constants::PERMISSION_READ)) {
1526
						$relativePath = trim(substr($mountPoint, $dirLength), '/');
1527
						if ($pos = strpos($relativePath, '/')) {
1528
							//mountpoint inside subfolder add size to the correct folder
1529
							$entryName = substr($relativePath, 0, $pos);
1530
							foreach ($files as &$entry) {
1531
								if ($entry->getName() === $entryName) {
1532
									$entry->addSubEntry($rootEntry, $mountPoint);
1533
								}
1534
							}
1535
						} else { //mountpoint in this folder, add an entry for it
1536
							$rootEntry['name'] = $relativePath;
1537
							$rootEntry['type'] = $rootEntry['mimetype'] === 'httpd/unix-directory' ? 'dir' : 'file';
1538
							$permissions = $rootEntry['permissions'];
1539
							// do not allow renaming/deleting the mount point if they are not shared files/folders
1540
							// for shared files/folders we use the permissions given by the owner
1541
							if ($mount instanceof MoveableMount) {
1542
								$rootEntry['permissions'] = $permissions | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE;
1543
							} else {
1544
								$rootEntry['permissions'] = $permissions & (\OCP\Constants::PERMISSION_ALL - (\OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE));
1545
							}
1546
1547
							//remove any existing entry with the same name
1548
							foreach ($files as $i => $file) {
1549
								if ($file['name'] === $rootEntry['name']) {
1550
									unset($files[$i]);
1551
									break;
1552
								}
1553
							}
1554
							$rootEntry['path'] = substr(Filesystem::normalizePath($path . '/' . $rootEntry['name']), strlen($user) + 2); // full path without /$user/
1555
1556
							// if sharing was disabled for the user we remove the share permissions
1557
							if (Util::isSharingDisabledForUser()) {
1558
								$rootEntry['permissions'] = $rootEntry['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
1559
							}
1560
1561
							$owner = $this->getUserObjectForOwner($subStorage->getOwner(''));
1562
							$files[] = new FileInfo($path . '/' . $rootEntry['name'], $subStorage, '', $rootEntry, $mount, $owner);
1563
						}
1564
					}
1565
				}
1566
			}
1567
1568
			if ($mimetype_filter) {
1569
				$files = array_filter($files, function (FileInfo $file) use ($mimetype_filter) {
1570
					if (strpos($mimetype_filter, '/')) {
1571
						return $file->getMimetype() === $mimetype_filter;
1572
					} else {
1573
						return $file->getMimePart() === $mimetype_filter;
1574
					}
1575
				});
1576
			}
1577
1578
			return $files;
1579
		} else {
1580
			return [];
1581
		}
1582
	}
1583
1584
	/**
1585
	 * change file metadata
1586
	 *
1587
	 * @param string $path
1588
	 * @param array|\OCP\Files\FileInfo $data
1589
	 * @return int
1590
	 *
1591
	 * returns the fileid of the updated file
1592
	 */
1593
	public function putFileInfo($path, $data) {
1594
		$this->assertPathLength($path);
1595
		if ($data instanceof FileInfo) {
1596
			$data = $data->getData();
1597
		}
1598
		$path = Filesystem::normalizePath($this->fakeRoot . '/' . $path);
1599
		/**
1600
		 * @var \OC\Files\Storage\Storage $storage
1601
		 * @var string $internalPath
1602
		 */
1603
		list($storage, $internalPath) = Filesystem::resolvePath($path);
1604
		if ($storage) {
1605
			$cache = $storage->getCache($path);
1606
1607
			if (!$cache->inCache($internalPath)) {
1608
				$scanner = $storage->getScanner($internalPath);
1609
				$scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW);
1610
			}
1611
1612
			return $cache->put($internalPath, $data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 1593 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...
1613
		} else {
1614
			return -1;
1615
		}
1616
	}
1617
1618
	/**
1619
	 * search for files with the name matching $query
1620
	 *
1621
	 * @param string $query
1622
	 * @return FileInfo[]
1623
	 */
1624
	public function search($query) {
1625
		return $this->searchCommon('search', ['%' . $query . '%']);
1626
	}
1627
1628
	/**
1629
	 * search for files with the name matching $query
1630
	 *
1631
	 * @param string $query
1632
	 * @return FileInfo[]
1633
	 */
1634
	public function searchRaw($query) {
1635
		return $this->searchCommon('search', [$query]);
1636
	}
1637
1638
	/**
1639
	 * search for files by mimetype
1640
	 *
1641
	 * @param string $mimetype
1642
	 * @return FileInfo[]
1643
	 */
1644
	public function searchByMime($mimetype) {
1645
		return $this->searchCommon('searchByMime', [$mimetype]);
1646
	}
1647
1648
	/**
1649
	 * search for files by tag
1650
	 *
1651
	 * @param string|int $tag name or tag id
1652
	 * @param string $userId owner of the tags
1653
	 * @return FileInfo[]
1654
	 */
1655
	public function searchByTag($tag, $userId) {
1656
		return $this->searchCommon('searchByTag', [$tag, $userId]);
1657
	}
1658
1659
	/**
1660
	 * @param string $method cache method
1661
	 * @param array $args
1662
	 * @return FileInfo[]
1663
	 */
1664
	private function searchCommon($method, $args) {
1665
		$files = [];
1666
		$rootLength = strlen($this->fakeRoot);
1667
1668
		$mount = $this->getMount('');
1669
		$mountPoint = $mount->getMountPoint();
1670
		$storage = $mount->getStorage();
1671
		if ($storage) {
1672
			$cache = $storage->getCache('');
1673
1674
			$results = call_user_func_array([$cache, $method], $args);
1675
			foreach ($results as $result) {
1676
				if (substr($mountPoint . $result['path'], 0, $rootLength + 1) === $this->fakeRoot . '/') {
1677
					$internalPath = $result['path'];
1678
					$path = $mountPoint . $result['path'];
1679
					$result['path'] = substr($mountPoint . $result['path'], $rootLength);
1680
					$owner = \OC::$server->getUserManager()->get($storage->getOwner($internalPath));
1681
					$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 1668 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...
1682
				}
1683
			}
1684
1685
			$mounts = Filesystem::getMountManager()->findIn($this->fakeRoot);
1686
			foreach ($mounts as $mount) {
1687
				$mountPoint = $mount->getMountPoint();
1688
				$storage = $mount->getStorage();
1689
				if ($storage) {
1690
					$cache = $storage->getCache('');
1691
1692
					$relativeMountPoint = substr($mountPoint, $rootLength);
1693
					$results = call_user_func_array([$cache, $method], $args);
1694
					if ($results) {
1695
						foreach ($results as $result) {
1696
							$internalPath = $result['path'];
1697
							$result['path'] = rtrim($relativeMountPoint . $result['path'], '/');
1698
							$path = rtrim($mountPoint . $internalPath, '/');
1699
							$owner = \OC::$server->getUserManager()->get($storage->getOwner($internalPath));
1700
							$files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner);
1701
						}
1702
					}
1703
				}
1704
			}
1705
		}
1706
		return $files;
1707
	}
1708
1709
	/**
1710
	 * Get the owner for a file or folder
1711
	 *
1712
	 * @param string $path
1713
	 * @return string the user id of the owner
1714
	 * @throws NotFoundException
1715
	 */
1716
	public function getOwner($path) {
1717
		$info = $this->getFileInfo($path);
1718
		if (!$info) {
1719
			throw new NotFoundException($path . ' not found while trying to get owner');
1720
		}
1721
		return $info->getOwner()->getUID();
1722
	}
1723
1724
	/**
1725
	 * get the ETag for a file or folder
1726
	 *
1727
	 * @param string $path
1728
	 * @return string
1729
	 */
1730
	public function getETag($path) {
1731
		/**
1732
		 * @var Storage\Storage $storage
1733
		 * @var string $internalPath
1734
		 */
1735
		list($storage, $internalPath) = $this->resolvePath($path);
1736
		if ($storage) {
1737
			return $storage->getETag($internalPath);
1738
		} else {
1739
			return null;
1740
		}
1741
	}
1742
1743
	/**
1744
	 * Get the path of a file by id, relative to the view
1745
	 *
1746
	 * Note that the resulting path is not guarantied to be unique for the id, multiple paths can point to the same file
1747
	 *
1748
	 * @param int $id
1749
	 * @param bool $includeShares whether to recurse into shared mounts
1750
	 * @throws NotFoundException
1751
	 * @return string
1752
	 */
1753
	public function getPath($id, $includeShares = true) {
1754
		$id = (int)$id;
1755
		$manager = Filesystem::getMountManager();
1756
		$mounts = $manager->findIn($this->fakeRoot);
1757
		$mounts[] = $manager->find($this->fakeRoot);
1758
		// reverse the array so we start with the storage this view is in
1759
		// which is the most likely to contain the file we're looking for
1760
		$mounts = array_reverse($mounts);
1761
		foreach ($mounts as $mount) {
1762
			/**
1763
			 * @var \OC\Files\Mount\MountPoint $mount
1764
			 */
1765
			if (!$includeShares && $mount instanceof SharedMount) {
1766
				// prevent potential infinite loop when instantiating shared storages
1767
				// recursively
1768
				continue;
1769
			}
1770
			if ($mount->getStorage()) {
1771
				$cache = $mount->getStorage()->getCache();
1772
				$internalPath = $cache->getPathById($id);
1773
				if (is_string($internalPath)) {
1774
					$fullPath = $mount->getMountPoint() . $internalPath;
1775
					if (!is_null($path = $this->getRelativePath($fullPath))) {
1776
						return $path;
1777
					}
1778
				}
1779
			}
1780
		}
1781
		throw new NotFoundException(sprintf('File with id "%s" has not been found.', $id));
1782
	}
1783
1784
	/**
1785
	 * @param string $path
1786
	 * @throws InvalidPathException
1787
	 */
1788
	private function assertPathLength($path) {
1789
		$maxLen = min(PHP_MAXPATHLEN, 4000);
1790
		// Check for the string length - performed using isset() instead of strlen()
1791
		// because isset() is about 5x-40x faster.
1792
		if (isset($path[$maxLen])) {
1793
			$pathLen = strlen($path);
1794
			throw new \OCP\Files\InvalidPathException("Path length($pathLen) exceeds max path length($maxLen): $path");
1795
		}
1796
	}
1797
1798
	/**
1799
	 * check if it is allowed to move a mount point to a given target.
1800
	 * It is not allowed to move a mount point into a different mount point or
1801
	 * into an already shared folder
1802
	 *
1803
	 * @param MoveableMount $mount1 moveable mount
1804
	 * @param string $target absolute target path
1805
	 * @return boolean
1806
	 */
1807
	private function canMove(MoveableMount $mount1, $target) {
1808
1809
		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...
1810
		if (!$targetStorage->instanceOfStorage('\OCP\Files\IHomeStorage')) {
1811
			Util::writeLog('files',
1812
				'It is not allowed to move one mount point into another one',
1813
				Util::DEBUG);
1814
			return false;
1815
		}
1816
1817
		return $mount1->isTargetAllowed($target);
1818
	}
1819
1820
	/**
1821
	 * Get a fileinfo object for files that are ignored in the cache (part files)
1822
	 *
1823
	 * @param string $path
1824
	 * @return \OCP\Files\FileInfo
1825
	 */
1826
	private function getPartFileInfo($path) {
1827
		$mount = $this->getMount($path);
1828
		$storage = $mount->getStorage();
1829
		$internalPath = $mount->getInternalPath($this->getAbsolutePath($path));
1830
		$owner = \OC::$server->getUserManager()->get($storage->getOwner($internalPath));
1831
		return new FileInfo(
1832
			$this->getAbsolutePath($path),
1833
			$storage,
1834
			$internalPath,
1835
			[
1836
				'fileid' => null,
1837
				'mimetype' => $storage->getMimeType($internalPath),
1838
				'name' => basename($path),
1839
				'etag' => null,
1840
				'size' => $storage->filesize($internalPath),
1841
				'mtime' => $storage->filemtime($internalPath),
1842
				'encrypted' => false,
1843
				'permissions' => \OCP\Constants::PERMISSION_ALL
1844
			],
1845
			$mount,
0 ignored issues
show
Bug introduced by
It seems like $mount defined by $this->getMount($path) on line 1827 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...
1846
			$owner
1847
		);
1848
	}
1849
1850
	/**
1851
	 * @param string $path
1852
	 * @param string $fileName
1853
	 * @throws InvalidPathException
1854
	 */
1855
	public function verifyPath($path, $fileName) {
1856
1857
		$l10n = \OC::$server->getL10N('lib');
1858
1859
		// verify empty and dot files
1860
		$trimmed = trim($fileName);
1861
		if ($trimmed === '') {
1862
			throw new InvalidPathException($l10n->t('Empty filename is not allowed'));
1863
		}
1864
		if (\OC\Files\Filesystem::isIgnoredDir($trimmed)) {
1865
			throw new InvalidPathException($l10n->t('Dot files are not allowed'));
1866
		}
1867
1868
		$matches = [];
1869
1870
		if (preg_match('/' . FileInfo::BLACKLIST_FILES_REGEX . '/', $fileName) !== 0) {
1871
			throw new InvalidPathException(
1872
				"Can`t upload files with extension {$matches[0]} because these extensions are reserved for internal use."
1873
			);
1874
		}
1875
1876
		if (!\OC::$server->getDatabaseConnection()->allows4ByteCharacters()) {
1877
			// verify database - e.g. mysql only 3-byte chars
1878
			if (preg_match('%(?:
1879
      \xF0[\x90-\xBF][\x80-\xBF]{2}      # planes 1-3
1880
    | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
1881
    | \xF4[\x80-\x8F][\x80-\xBF]{2}      # plane 16
1882
)%xs', $fileName)) {
1883
				throw new InvalidPathException($l10n->t('4-byte characters are not supported in file names'));
1884
			}
1885
		}
1886
1887
		try {
1888
			/** @type \OCP\Files\Storage $storage */
1889
			list($storage, $internalPath) = $this->resolvePath($path);
1890
			$storage->verifyPath($internalPath, $fileName);
1891
		} catch (ReservedWordException $ex) {
1892
			throw new InvalidPathException($l10n->t('File name is a reserved word'));
1893
		} catch (InvalidCharacterInPathException $ex) {
1894
			throw new InvalidPathException($l10n->t('File name contains at least one invalid character'));
1895
		} catch (FileNameTooLongException $ex) {
1896
			throw new InvalidPathException($l10n->t('File name is too long'));
1897
		}
1898
	}
1899
1900
	/**
1901
	 * get all parent folders of $path
1902
	 *
1903
	 * @param string $path
1904
	 * @return string[]
1905
	 */
1906
	private function getParents($path) {
1907
		$path = trim($path, '/');
1908
		if (!$path) {
1909
			return [];
1910
		}
1911
1912
		$parts = explode('/', $path);
1913
1914
		// remove the single file
1915
		array_pop($parts);
1916
		$result = ['/'];
1917
		$resultPath = '';
1918
		foreach ($parts as $part) {
1919
			if ($part) {
1920
				$resultPath .= '/' . $part;
1921
				$result[] = $resultPath;
1922
			}
1923
		}
1924
		return $result;
1925
	}
1926
1927
	/**
1928
	 * Returns the mount point for which to lock
1929
	 *
1930
	 * @param string $absolutePath absolute path
1931
	 * @param bool $useParentMount true to return parent mount instead of whatever
1932
	 * is mounted directly on the given path, false otherwise
1933
	 * @return \OC\Files\Mount\MountPoint mount point for which to apply locks
1934
	 */
1935
	private function getMountForLock($absolutePath, $useParentMount = false) {
1936
		$results = [];
1937
		$mount = Filesystem::getMountManager()->find($absolutePath);
1938
		if (!$mount) {
1939
			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...
1940
		}
1941
1942
		if ($useParentMount) {
1943
			// find out if something is mounted directly on the path
1944
			$internalPath = $mount->getInternalPath($absolutePath);
1945
			if ($internalPath === '') {
1946
				// resolve the parent mount instead
1947
				$mount = Filesystem::getMountManager()->find(dirname($absolutePath));
1948
			}
1949
		}
1950
1951
		return $mount;
1952
	}
1953
1954
	/**
1955
	 * Lock the given path
1956
	 *
1957
	 * @param string $path the path of the file to lock, relative to the view
1958
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
1959
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
1960
	 *
1961
	 * @return bool False if the path is excluded from locking, true otherwise
1962
	 * @throws \OCP\Lock\LockedException if the path is already locked
1963
	 */
1964 View Code Duplication
	private function lockPath($path, $type, $lockMountPoint = false) {
1965
		$absolutePath = $this->getAbsolutePath($path);
1966
		$absolutePath = Filesystem::normalizePath($absolutePath);
1967
		if (!$this->shouldLockFile($absolutePath)) {
1968
			return false;
1969
		}
1970
1971
		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
1972
		if ($mount) {
1973
			try {
1974
				$storage = $mount->getStorage();
1975
				if ($storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
1976
					$storage->acquireLock(
1977
						$mount->getInternalPath($absolutePath),
1978
						$type,
1979
						$this->lockingProvider
1980
					);
1981
				}
1982
			} catch (\OCP\Lock\LockedException $e) {
1983
				// rethrow with the a human-readable path
1984
				throw new \OCP\Lock\LockedException(
1985
					$this->getPathRelativeToFiles($absolutePath),
1986
					$e
1987
				);
1988
			}
1989
		}
1990
1991
		return true;
1992
	}
1993
1994
	/**
1995
	 * Change the lock type
1996
	 *
1997
	 * @param string $path the path of the file to lock, relative to the view
1998
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
1999
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2000
	 *
2001
	 * @return bool False if the path is excluded from locking, true otherwise
2002
	 * @throws \OCP\Lock\LockedException if the path is already locked
2003
	 */
2004 View Code Duplication
	public function changeLock($path, $type, $lockMountPoint = false) {
2005
		$path = Filesystem::normalizePath($path);
2006
		$absolutePath = $this->getAbsolutePath($path);
2007
		$absolutePath = Filesystem::normalizePath($absolutePath);
2008
		if (!$this->shouldLockFile($absolutePath)) {
2009
			return false;
2010
		}
2011
2012
		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
2013
		if ($mount) {
2014
			try {
2015
				$storage = $mount->getStorage();
2016
				if ($storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
2017
					$storage->changeLock(
2018
						$mount->getInternalPath($absolutePath),
2019
						$type,
2020
						$this->lockingProvider
2021
					);
2022
				}
2023
			} catch (LockedException $e) {
2024
				// rethrow with the a human-readable path
2025
				throw new LockedException(
2026
					$this->getPathRelativeToFiles($absolutePath),
2027
					$e
2028
				);
2029
			}
2030
		}
2031
2032
		return true;
2033
	}
2034
2035
	/**
2036
	 * Unlock the given path
2037
	 *
2038
	 * @param string $path the path of the file to unlock, relative to the view
2039
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2040
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2041
	 *
2042
	 * @return bool False if the path is excluded from locking, true otherwise
2043
	 */
2044
	private function unlockPath($path, $type, $lockMountPoint = false) {
2045
		$absolutePath = $this->getAbsolutePath($path);
2046
		$absolutePath = Filesystem::normalizePath($absolutePath);
2047
		if (!$this->shouldLockFile($absolutePath)) {
2048
			return false;
2049
		}
2050
2051
		$mount = $this->getMountForLock($absolutePath, $lockMountPoint);
2052
		if ($mount) {
2053
			$storage = $mount->getStorage();
2054
			if ($storage && $storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
2055
				$storage->releaseLock(
2056
					$mount->getInternalPath($absolutePath),
2057
					$type,
2058
					$this->lockingProvider
2059
				);
2060
			}
2061
		}
2062
2063
		return true;
2064
	}
2065
2066
	/**
2067
	 * Lock a path and all its parents up to the root of the view
2068
	 *
2069
	 * @param string $path the path of the file to lock relative to the view
2070
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2071
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2072
	 *
2073
	 * @return bool False if the path is excluded from locking, true otherwise
2074
	 */
2075 View Code Duplication
	public function lockFile($path, $type, $lockMountPoint = false) {
2076
		$absolutePath = $this->getAbsolutePath($path);
2077
		$absolutePath = Filesystem::normalizePath($absolutePath);
2078
		if (!$this->shouldLockFile($absolutePath)) {
2079
			return false;
2080
		}
2081
2082
		$this->lockPath($path, $type, $lockMountPoint);
2083
2084
		$parents = $this->getParents($path);
2085
		foreach ($parents as $parent) {
2086
			$this->lockPath($parent, ILockingProvider::LOCK_SHARED);
2087
		}
2088
2089
		return true;
2090
	}
2091
2092
	/**
2093
	 * Unlock a path and all its parents up to the root of the view
2094
	 *
2095
	 * @param string $path the path of the file to lock relative to the view
2096
	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
2097
	 * @param bool $lockMountPoint true to lock the mount point, false to lock the attached mount/storage
2098
	 *
2099
	 * @return bool False if the path is excluded from locking, true otherwise
2100
	 */
2101 View Code Duplication
	public function unlockFile($path, $type, $lockMountPoint = false) {
2102
		$absolutePath = $this->getAbsolutePath($path);
2103
		$absolutePath = Filesystem::normalizePath($absolutePath);
2104
		if (!$this->shouldLockFile($absolutePath)) {
2105
			return false;
2106
		}
2107
2108
		$this->unlockPath($path, $type, $lockMountPoint);
2109
2110
		$parents = $this->getParents($path);
2111
		foreach ($parents as $parent) {
2112
			$this->unlockPath($parent, ILockingProvider::LOCK_SHARED);
2113
		}
2114
2115
		return true;
2116
	}
2117
2118
	/**
2119
	 * Only lock files in data/user/files/
2120
	 *
2121
	 * @param string $path Absolute path to the file/folder we try to (un)lock
2122
	 * @return bool
2123
	 */
2124
	protected function shouldLockFile($path) {
2125
		$path = Filesystem::normalizePath($path);
2126
2127
		$pathSegments = explode('/', $path);
2128
		if (isset($pathSegments[2])) {
2129
			// E.g.: /username/files/path-to-file
2130
			return ($pathSegments[2] === 'files') && (count($pathSegments) > 3);
2131
		}
2132
2133
		return true;
2134
	}
2135
2136
	/**
2137
	 * Shortens the given absolute path to be relative to
2138
	 * "$user/files".
2139
	 *
2140
	 * @param string $absolutePath absolute path which is under "files"
2141
	 *
2142
	 * @return string path relative to "files" with trimmed slashes or null
2143
	 * if the path was NOT relative to files
2144
	 *
2145
	 * @throws \InvalidArgumentException if the given path was not under "files"
2146
	 * @since 8.1.0
2147
	 */
2148
	public function getPathRelativeToFiles($absolutePath) {
2149
		$path = Filesystem::normalizePath($absolutePath);
2150
		$parts = explode('/', trim($path, '/'), 3);
2151
		// "$user", "files", "path/to/dir"
2152
		if (!isset($parts[1]) || $parts[1] !== 'files') {
2153
			throw new \InvalidArgumentException('"' . $absolutePath . '" must be relative to "files"');
2154
		}
2155
		if (isset($parts[2])) {
2156
			return $parts[2];
2157
		}
2158
		return '';
2159
	}
2160
2161
	/**
2162
	 * @param string $filename
2163
	 * @return array
2164
	 * @throws \OC\User\NoUserException
2165
	 * @throws NotFoundException
2166
	 */
2167
	public function getUidAndFilename($filename) {
2168
		$info = $this->getFileInfo($filename);
2169
		if (!$info instanceof \OCP\Files\FileInfo) {
2170
			throw new NotFoundException($this->getAbsolutePath($filename) . ' not found');
2171
		}
2172
		$uid = $info->getOwner()->getUID();
2173
		if ($uid != \OCP\User::getUser()) {
2174
			Filesystem::initMountPoints($uid);
2175
			$ownerView = new View('/' . $uid . '/files');
2176
			try {
2177
				$filename = $ownerView->getPath($info['fileid']);
2178
			} catch (NotFoundException $e) {
2179
				throw new NotFoundException('File with id ' . $info['fileid'] . ' not found for user ' . $uid);
2180
			}
2181
		}
2182
		return [$uid, $filename];
2183
	}
2184
2185
	/**
2186
	 * Creates parent non-existing folders
2187
	 *
2188
	 * @param string $filePath
2189
	 * @return bool
2190
	 */
2191
	private function createParentDirectories($filePath) {
2192
		$parentDirectory = dirname($filePath);
2193
		while(!$this->file_exists($parentDirectory)) {
2194
			$result = $this->createParentDirectories($parentDirectory);
2195
			if($result === false) {
2196
				return false;
2197
			}
2198
		}
2199
		$this->mkdir($filePath);
2200
		return true;
2201
	}
2202
}
2203