Passed
Push — master ( c15172...ad16b1 )
by Morris
14:48 queued 11s
created

Local::checkTreeForForbiddenItems()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 6
rs 10
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author aler9 <[email protected]>
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author Bart Visscher <[email protected]>
8
 * @author Boris Rybalkin <[email protected]>
9
 * @author Brice Maron <[email protected]>
10
 * @author Christoph Wurst <[email protected]>
11
 * @author J0WI <[email protected]>
12
 * @author Jakob Sack <[email protected]>
13
 * @author Joas Schilling <[email protected]>
14
 * @author Jörn Friedrich Dreyer <[email protected]>
15
 * @author Klaas Freitag <[email protected]>
16
 * @author Lukas Reschke <[email protected]>
17
 * @author Michael Gapczynski <[email protected]>
18
 * @author Morris Jobke <[email protected]>
19
 * @author Robin Appelman <[email protected]>
20
 * @author Roeland Jago Douma <[email protected]>
21
 * @author Sjors van der Pluijm <[email protected]>
22
 * @author Stefan Weil <[email protected]>
23
 * @author Thomas Müller <[email protected]>
24
 * @author Tigran Mkrtchyan <[email protected]>
25
 * @author Vincent Petry <[email protected]>
26
 *
27
 * @license AGPL-3.0
28
 *
29
 * This code is free software: you can redistribute it and/or modify
30
 * it under the terms of the GNU Affero General Public License, version 3,
31
 * as published by the Free Software Foundation.
32
 *
33
 * This program is distributed in the hope that it will be useful,
34
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
35
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
36
 * GNU Affero General Public License for more details.
37
 *
38
 * You should have received a copy of the GNU Affero General Public License, version 3,
39
 * along with this program. If not, see <http://www.gnu.org/licenses/>
40
 *
41
 */
42
43
namespace OC\Files\Storage;
44
45
use OC\Files\Filesystem;
46
use OC\Files\Storage\Wrapper\Jail;
47
use OCP\Constants;
48
use OCP\Files\ForbiddenException;
49
use OCP\Files\GenericFileException;
50
use OCP\Files\Storage\IStorage;
51
use OCP\ILogger;
52
53
/**
54
 * for local filestore, we only have to map the paths
55
 */
56
class Local extends \OC\Files\Storage\Common {
57
	protected $datadir;
58
59
	protected $dataDirLength;
60
61
	protected $realDataDir;
62
63
	public function __construct($arguments) {
64
		if (!isset($arguments['datadir']) || !is_string($arguments['datadir'])) {
65
			throw new \InvalidArgumentException('No data directory set for local storage');
66
		}
67
		$this->datadir = str_replace('//', '/', $arguments['datadir']);
68
		// some crazy code uses a local storage on root...
69
		if ($this->datadir === '/') {
70
			$this->realDataDir = $this->datadir;
71
		} else {
72
			$realPath = realpath($this->datadir) ?: $this->datadir;
73
			$this->realDataDir = rtrim($realPath, '/') . '/';
74
		}
75
		if (substr($this->datadir, -1) !== '/') {
76
			$this->datadir .= '/';
77
		}
78
		$this->dataDirLength = strlen($this->realDataDir);
79
	}
80
81
	public function __destruct() {
82
	}
83
84
	public function getId() {
85
		return 'local::' . $this->datadir;
86
	}
87
88
	public function mkdir($path) {
89
		$sourcePath = $this->getSourcePath($path);
90
		$result = @mkdir($sourcePath, 0777, true);
91
		chmod($sourcePath, 0755);
92
		return $result;
93
	}
94
95
	public function rmdir($path) {
96
		if (!$this->isDeletable($path)) {
97
			return false;
98
		}
99
		try {
100
			$it = new \RecursiveIteratorIterator(
101
				new \RecursiveDirectoryIterator($this->getSourcePath($path)),
102
				\RecursiveIteratorIterator::CHILD_FIRST
103
			);
104
			/**
105
			 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
106
			 * This bug is fixed in PHP 5.5.9 or before
107
			 * See #8376
108
			 */
109
			$it->rewind();
110
			while ($it->valid()) {
111
				/**
112
				 * @var \SplFileInfo $file
113
				 */
114
				$file = $it->current();
115
				clearstatcache(true, $this->getSourcePath($file));
116
				if (in_array($file->getBasename(), ['.', '..'])) {
117
					$it->next();
118
					continue;
119
				} elseif ($file->isDir()) {
120
					rmdir($file->getPathname());
121
				} elseif ($file->isFile() || $file->isLink()) {
122
					unlink($file->getPathname());
123
				}
124
				$it->next();
125
			}
126
			clearstatcache(true, $this->getSourcePath($path));
127
			return rmdir($this->getSourcePath($path));
128
		} catch (\UnexpectedValueException $e) {
129
			return false;
130
		}
131
	}
132
133
	public function opendir($path) {
134
		return opendir($this->getSourcePath($path));
135
	}
136
137
	public function is_dir($path) {
138
		if (substr($path, -1) == '/') {
139
			$path = substr($path, 0, -1);
140
		}
141
		return is_dir($this->getSourcePath($path));
142
	}
143
144
	public function is_file($path) {
145
		return is_file($this->getSourcePath($path));
146
	}
147
148
	public function stat($path) {
149
		$fullPath = $this->getSourcePath($path);
150
		clearstatcache(true, $fullPath);
151
		$statResult = @stat($fullPath);
152
		if (PHP_INT_SIZE === 4 && $statResult && !$this->is_dir($path)) {
153
			$filesize = $this->filesize($path);
154
			$statResult['size'] = $filesize;
155
			$statResult[7] = $filesize;
156
		}
157
		return $statResult;
158
	}
159
160
	/**
161
	 * @inheritdoc
162
	 */
163
	public function getMetaData($path) {
164
		$stat = $this->stat($path);
165
		if (!$stat) {
166
			return null;
167
		}
168
169
		$permissions = Constants::PERMISSION_SHARE;
170
		$statPermissions = $stat['mode'];
171
		$isDir = ($statPermissions & 0x4000) === 0x4000;
172
		if ($statPermissions & 0x0100) {
173
			$permissions += Constants::PERMISSION_READ;
174
		}
175
		if ($statPermissions & 0x0080) {
176
			$permissions += Constants::PERMISSION_UPDATE;
177
			if ($isDir) {
178
				$permissions += Constants::PERMISSION_CREATE;
179
			}
180
		}
181
182
		if (!($path === '' || $path === '/')) { // deletable depends on the parents unix permissions
183
			$fullPath = $this->getSourcePath($path);
184
			$parent = dirname($fullPath);
185
			if (is_writable($parent)) {
186
				$permissions += Constants::PERMISSION_DELETE;
187
			}
188
		}
189
190
		$data = [];
191
		$data['mimetype'] = $isDir ? 'httpd/unix-directory' : \OC::$server->getMimeTypeDetector()->detectPath($path);
192
		$data['mtime'] = $stat['mtime'];
193
		if ($data['mtime'] === false) {
194
			$data['mtime'] = time();
195
		}
196
		if ($isDir) {
197
			$data['size'] = -1; //unknown
198
		} else {
199
			$data['size'] = $stat['size'];
200
		}
201
		$data['etag'] = $this->calculateEtag($path, $stat);
202
		$data['storage_mtime'] = $data['mtime'];
203
		$data['permissions'] = $permissions;
204
		$data['name'] = basename($path);
205
206
		return $data;
207
	}
208
209
	public function filetype($path) {
210
		$filetype = filetype($this->getSourcePath($path));
211
		if ($filetype == 'link') {
212
			$filetype = filetype(realpath($this->getSourcePath($path)));
213
		}
214
		return $filetype;
215
	}
216
217
	public function filesize($path) {
218
		if ($this->is_dir($path)) {
219
			return 0;
220
		}
221
		$fullPath = $this->getSourcePath($path);
222
		if (PHP_INT_SIZE === 4) {
223
			$helper = new \OC\LargeFileHelper;
224
			return $helper->getFileSize($fullPath);
225
		}
226
		return filesize($fullPath);
227
	}
228
229
	public function isReadable($path) {
230
		return is_readable($this->getSourcePath($path));
231
	}
232
233
	public function isUpdatable($path) {
234
		return is_writable($this->getSourcePath($path));
235
	}
236
237
	public function file_exists($path) {
238
		return file_exists($this->getSourcePath($path));
239
	}
240
241
	public function filemtime($path) {
242
		$fullPath = $this->getSourcePath($path);
243
		clearstatcache(true, $fullPath);
244
		if (!$this->file_exists($path)) {
245
			return false;
246
		}
247
		if (PHP_INT_SIZE === 4) {
248
			$helper = new \OC\LargeFileHelper();
249
			return $helper->getFileMtime($fullPath);
250
		}
251
		return filemtime($fullPath);
252
	}
253
254
	public function touch($path, $mtime = null) {
255
		// sets the modification time of the file to the given value.
256
		// If mtime is nil the current time is set.
257
		// note that the access time of the file always changes to the current time.
258
		if ($this->file_exists($path) and !$this->isUpdatable($path)) {
259
			return false;
260
		}
261
		if (!is_null($mtime)) {
262
			$result = @touch($this->getSourcePath($path), $mtime);
263
		} else {
264
			$result = @touch($this->getSourcePath($path));
265
		}
266
		if ($result) {
267
			clearstatcache(true, $this->getSourcePath($path));
268
		}
269
270
		return $result;
271
	}
272
273
	public function file_get_contents($path) {
274
		return file_get_contents($this->getSourcePath($path));
275
	}
276
277
	public function file_put_contents($path, $data) {
278
		return file_put_contents($this->getSourcePath($path), $data);
279
	}
280
281
	public function unlink($path) {
282
		if ($this->is_dir($path)) {
283
			return $this->rmdir($path);
284
		} elseif ($this->is_file($path)) {
285
			return unlink($this->getSourcePath($path));
286
		} else {
287
			return false;
288
		}
289
	}
290
291
	private function checkTreeForForbiddenItems(string $path) {
292
		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
293
		foreach ($iterator as $file) {
294
			/** @var \SplFileInfo $file */
295
			if (Filesystem::isFileBlacklisted($file->getBasename())) {
296
				throw new ForbiddenException('Invalid path: ' . $file->getPathname(), false);
297
			}
298
		}
299
	}
300
301
	public function rename($path1, $path2) {
302
		$srcParent = dirname($path1);
303
		$dstParent = dirname($path2);
304
305
		if (!$this->isUpdatable($srcParent)) {
306
			\OCP\Util::writeLog('core', 'unable to rename, source directory is not writable : ' . $srcParent, ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

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

306
			\OCP\Util::writeLog('core', 'unable to rename, source directory is not writable : ' . $srcParent, /** @scrutinizer ignore-deprecated */ ILogger::ERROR);

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

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

Loading history...
307
			return false;
308
		}
309
310
		if (!$this->isUpdatable($dstParent)) {
311
			\OCP\Util::writeLog('core', 'unable to rename, destination directory is not writable : ' . $dstParent, ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

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

311
			\OCP\Util::writeLog('core', 'unable to rename, destination directory is not writable : ' . $dstParent, /** @scrutinizer ignore-deprecated */ ILogger::ERROR);

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

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

Loading history...
312
			return false;
313
		}
314
315
		if (!$this->file_exists($path1)) {
316
			\OCP\Util::writeLog('core', 'unable to rename, file does not exists : ' . $path1, ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

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

316
			\OCP\Util::writeLog('core', 'unable to rename, file does not exists : ' . $path1, /** @scrutinizer ignore-deprecated */ ILogger::ERROR);

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

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

Loading history...
317
			return false;
318
		}
319
320
		if ($this->is_dir($path2)) {
321
			$this->rmdir($path2);
322
		} elseif ($this->is_file($path2)) {
323
			$this->unlink($path2);
324
		}
325
326
		if ($this->is_dir($path1)) {
327
			// we can't move folders across devices, use copy instead
328
			$stat1 = stat(dirname($this->getSourcePath($path1)));
329
			$stat2 = stat(dirname($this->getSourcePath($path2)));
330
			if ($stat1['dev'] !== $stat2['dev']) {
331
				$result = $this->copy($path1, $path2);
332
				if ($result) {
333
					$result &= $this->rmdir($path1);
334
				}
335
				return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type integer which is incompatible with the return type mandated by OCP\Files\Storage::rename() of boolean.
Loading history...
336
			}
337
338
			$this->checkTreeForForbiddenItems($this->getSourcePath($path1));
339
		}
340
341
		return rename($this->getSourcePath($path1), $this->getSourcePath($path2));
342
	}
343
344
	public function copy($path1, $path2) {
345
		if ($this->is_dir($path1)) {
346
			return parent::copy($path1, $path2);
347
		} else {
348
			return copy($this->getSourcePath($path1), $this->getSourcePath($path2));
349
		}
350
	}
351
352
	public function fopen($path, $mode) {
353
		return fopen($this->getSourcePath($path), $mode);
354
	}
355
356
	public function hash($type, $path, $raw = false) {
357
		return hash_file($type, $this->getSourcePath($path), $raw);
358
	}
359
360
	public function free_space($path) {
361
		$sourcePath = $this->getSourcePath($path);
362
		// using !is_dir because $sourcePath might be a part file or
363
		// non-existing file, so we'd still want to use the parent dir
364
		// in such cases
365
		if (!is_dir($sourcePath)) {
366
			// disk_free_space doesn't work on files
367
			$sourcePath = dirname($sourcePath);
368
		}
369
		$space = @disk_free_space($sourcePath);
370
		if ($space === false || is_null($space)) {
371
			return \OCP\Files\FileInfo::SPACE_UNKNOWN;
372
		}
373
		return $space;
374
	}
375
376
	public function search($query) {
377
		return $this->searchInDir($query);
378
	}
379
380
	public function getLocalFile($path) {
381
		return $this->getSourcePath($path);
382
	}
383
384
	public function getLocalFolder($path) {
385
		return $this->getSourcePath($path);
386
	}
387
388
	/**
389
	 * @param string $query
390
	 * @param string $dir
391
	 * @return array
392
	 */
393
	protected function searchInDir($query, $dir = '') {
394
		$files = [];
395
		$physicalDir = $this->getSourcePath($dir);
396
		foreach (scandir($physicalDir) as $item) {
397
			if (\OC\Files\Filesystem::isIgnoredDir($item)) {
398
				continue;
399
			}
400
			$physicalItem = $physicalDir . '/' . $item;
401
402
			if (strstr(strtolower($item), strtolower($query)) !== false) {
403
				$files[] = $dir . '/' . $item;
404
			}
405
			if (is_dir($physicalItem)) {
406
				$files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
407
			}
408
		}
409
		return $files;
410
	}
411
412
	/**
413
	 * check if a file or folder has been updated since $time
414
	 *
415
	 * @param string $path
416
	 * @param int $time
417
	 * @return bool
418
	 */
419
	public function hasUpdated($path, $time) {
420
		if ($this->file_exists($path)) {
421
			return $this->filemtime($path) > $time;
422
		} else {
423
			return true;
424
		}
425
	}
426
427
	/**
428
	 * Get the source path (on disk) of a given path
429
	 *
430
	 * @param string $path
431
	 * @return string
432
	 * @throws ForbiddenException
433
	 */
434
	public function getSourcePath($path) {
435
		if (Filesystem::isFileBlacklisted($path)) {
436
			throw new ForbiddenException('Invalid path: ' . $path, false);
437
		}
438
439
		$fullPath = $this->datadir . $path;
440
		$currentPath = $path;
441
		$allowSymlinks = \OC::$server->getConfig()->getSystemValue('localstorage.allowsymlinks', false);
442
		if ($allowSymlinks || $currentPath === '') {
443
			return $fullPath;
444
		}
445
		$pathToResolve = $fullPath;
446
		$realPath = realpath($pathToResolve);
447
		while ($realPath === false) { // for non existing files check the parent directory
448
			$currentPath = dirname($currentPath);
449
			if ($currentPath === '' || $currentPath === '.') {
450
				return $fullPath;
451
			}
452
			$realPath = realpath($this->datadir . $currentPath);
453
		}
454
		if ($realPath) {
455
			$realPath = $realPath . '/';
456
		}
457
		if (substr($realPath, 0, $this->dataDirLength) === $this->realDataDir) {
458
			return $fullPath;
459
		}
460
461
		\OCP\Util::writeLog('core', "Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

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

461
		\OCP\Util::writeLog('core', "Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", /** @scrutinizer ignore-deprecated */ ILogger::ERROR);

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

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

Loading history...
462
		throw new ForbiddenException('Following symlinks is not allowed', false);
463
	}
464
465
	/**
466
	 * {@inheritdoc}
467
	 */
468
	public function isLocal() {
469
		return true;
470
	}
471
472
	/**
473
	 * get the ETag for a file or folder
474
	 *
475
	 * @param string $path
476
	 * @return string
477
	 */
478
	public function getETag($path) {
479
		return $this->calculateEtag($path, $this->stat($path));
480
	}
481
482
	private function calculateEtag(string $path, array $stat): string {
483
		if ($stat['mode'] & 0x4000) { // is_dir
484
			return parent::getETag($path);
485
		} else {
486
			if ($stat === false) {
0 ignored issues
show
introduced by
The condition $stat === false is always false.
Loading history...
487
				return md5('');
488
			}
489
490
			$toHash = '';
491
			if (isset($stat['mtime'])) {
492
				$toHash .= $stat['mtime'];
493
			}
494
			if (isset($stat['ino'])) {
495
				$toHash .= $stat['ino'];
496
			}
497
			if (isset($stat['dev'])) {
498
				$toHash .= $stat['dev'];
499
			}
500
			if (isset($stat['size'])) {
501
				$toHash .= $stat['size'];
502
			}
503
504
			return md5($toHash);
505
		}
506
	}
507
508
	/**
509
	 * @param IStorage $sourceStorage
510
	 * @param string $sourceInternalPath
511
	 * @param string $targetInternalPath
512
	 * @param bool $preserveMtime
513
	 * @return bool
514
	 */
515
	public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
516
		if ($sourceStorage->instanceOfStorage(Local::class)) {
517
			if ($sourceStorage->instanceOfStorage(Jail::class)) {
518
				/**
519
				 * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
520
				 */
521
				$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
522
			}
523
			/**
524
			 * @var \OC\Files\Storage\Local $sourceStorage
525
			 */
526
			$rootStorage = new Local(['datadir' => '/']);
527
			return $rootStorage->copy($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
528
		} else {
529
			return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
530
		}
531
	}
532
533
	/**
534
	 * @param IStorage $sourceStorage
535
	 * @param string $sourceInternalPath
536
	 * @param string $targetInternalPath
537
	 * @return bool
538
	 */
539
	public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
540
		if ($sourceStorage->instanceOfStorage(Local::class)) {
541
			if ($sourceStorage->instanceOfStorage(Jail::class)) {
542
				/**
543
				 * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
544
				 */
545
				$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
546
			}
547
			/**
548
			 * @var \OC\Files\Storage\Local $sourceStorage
549
			 */
550
			$rootStorage = new Local(['datadir' => '/']);
551
			return $rootStorage->rename($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
552
		} else {
553
			return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
554
		}
555
	}
556
557
	public function writeStream(string $path, $stream, int $size = null): int {
558
		$result = $this->file_put_contents($path, $stream);
559
		if ($result === false) {
560
			throw new GenericFileException("Failed write steam to $path");
561
		} else {
562
			return $result;
563
		}
564
	}
565
}
566