Passed
Push — master ( f33305...caebdc )
by Robin
15:26 queued 12s
created

Local::getMetaData()   D

Complexity

Conditions 13
Paths 290

Size

Total Lines 47
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 13
eloc 33
c 2
b 0
f 0
nc 290
nop 1
dl 0
loc 47
rs 4.6583

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

326
			\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...
327
			return false;
328
		}
329
330
		if (!$this->isUpdatable($dstParent)) {
331
			\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

331
			\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...
332
			return false;
333
		}
334
335
		if (!$this->file_exists($path1)) {
336
			\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

336
			\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...
337
			return false;
338
		}
339
340
		if ($this->is_dir($path2)) {
341
			$this->rmdir($path2);
342
		} elseif ($this->is_file($path2)) {
343
			$this->unlink($path2);
344
		}
345
346
		if ($this->is_dir($path1)) {
347
			// we can't move folders across devices, use copy instead
348
			$stat1 = stat(dirname($this->getSourcePath($path1)));
349
			$stat2 = stat(dirname($this->getSourcePath($path2)));
350
			if ($stat1['dev'] !== $stat2['dev']) {
351
				$result = $this->copy($path1, $path2);
352
				if ($result) {
353
					$result &= $this->rmdir($path1);
354
				}
355
				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...
356
			}
357
358
			$this->checkTreeForForbiddenItems($this->getSourcePath($path1));
359
		}
360
361
		return rename($this->getSourcePath($path1), $this->getSourcePath($path2));
362
	}
363
364
	public function copy($path1, $path2) {
365
		if ($this->is_dir($path1)) {
366
			return parent::copy($path1, $path2);
367
		} else {
368
			$oldMask = umask(022);
369
			$result = copy($this->getSourcePath($path1), $this->getSourcePath($path2));
370
			umask($oldMask);
371
			return $result;
372
		}
373
	}
374
375
	public function fopen($path, $mode) {
376
		$oldMask = umask(022);
377
		$result = fopen($this->getSourcePath($path), $mode);
378
		umask($oldMask);
379
		return $result;
380
	}
381
382
	public function hash($type, $path, $raw = false) {
383
		return hash_file($type, $this->getSourcePath($path), $raw);
384
	}
385
386
	public function free_space($path) {
387
		$sourcePath = $this->getSourcePath($path);
388
		// using !is_dir because $sourcePath might be a part file or
389
		// non-existing file, so we'd still want to use the parent dir
390
		// in such cases
391
		if (!is_dir($sourcePath)) {
392
			// disk_free_space doesn't work on files
393
			$sourcePath = dirname($sourcePath);
394
		}
395
		$space = function_exists('disk_free_space') ? disk_free_space($sourcePath) : false;
396
		if ($space === false || is_null($space)) {
397
			return \OCP\Files\FileInfo::SPACE_UNKNOWN;
398
		}
399
		return $space;
400
	}
401
402
	public function search($query) {
403
		return $this->searchInDir($query);
404
	}
405
406
	public function getLocalFile($path) {
407
		return $this->getSourcePath($path);
408
	}
409
410
	public function getLocalFolder($path) {
411
		return $this->getSourcePath($path);
412
	}
413
414
	/**
415
	 * @param string $query
416
	 * @param string $dir
417
	 * @return array
418
	 */
419
	protected function searchInDir($query, $dir = '') {
420
		$files = [];
421
		$physicalDir = $this->getSourcePath($dir);
422
		foreach (scandir($physicalDir) as $item) {
423
			if (\OC\Files\Filesystem::isIgnoredDir($item)) {
424
				continue;
425
			}
426
			$physicalItem = $physicalDir . '/' . $item;
427
428
			if (strstr(strtolower($item), strtolower($query)) !== false) {
429
				$files[] = $dir . '/' . $item;
430
			}
431
			if (is_dir($physicalItem)) {
432
				$files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
433
			}
434
		}
435
		return $files;
436
	}
437
438
	/**
439
	 * check if a file or folder has been updated since $time
440
	 *
441
	 * @param string $path
442
	 * @param int $time
443
	 * @return bool
444
	 */
445
	public function hasUpdated($path, $time) {
446
		if ($this->file_exists($path)) {
447
			return $this->filemtime($path) > $time;
448
		} else {
449
			return true;
450
		}
451
	}
452
453
	/**
454
	 * Get the source path (on disk) of a given path
455
	 *
456
	 * @param string $path
457
	 * @return string
458
	 * @throws ForbiddenException
459
	 */
460
	public function getSourcePath($path) {
461
		if (Filesystem::isFileBlacklisted($path)) {
462
			throw new ForbiddenException('Invalid path: ' . $path, false);
463
		}
464
465
		$fullPath = $this->datadir . $path;
466
		$currentPath = $path;
467
		$allowSymlinks = $this->config->getSystemValue('localstorage.allowsymlinks', false);
468
		if ($allowSymlinks || $currentPath === '') {
469
			return $fullPath;
470
		}
471
		$pathToResolve = $fullPath;
472
		$realPath = realpath($pathToResolve);
473
		while ($realPath === false) { // for non existing files check the parent directory
474
			$currentPath = dirname($currentPath);
475
			if ($currentPath === '' || $currentPath === '.') {
476
				return $fullPath;
477
			}
478
			$realPath = realpath($this->datadir . $currentPath);
479
		}
480
		if ($realPath) {
481
			$realPath = $realPath . '/';
482
		}
483
		if (substr($realPath, 0, $this->dataDirLength) === $this->realDataDir) {
484
			return $fullPath;
485
		}
486
487
		\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

487
		\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...
488
		throw new ForbiddenException('Following symlinks is not allowed', false);
489
	}
490
491
	/**
492
	 * {@inheritdoc}
493
	 */
494
	public function isLocal() {
495
		return true;
496
	}
497
498
	/**
499
	 * get the ETag for a file or folder
500
	 *
501
	 * @param string $path
502
	 * @return string
503
	 */
504
	public function getETag($path) {
505
		return $this->calculateEtag($path, $this->stat($path));
506
	}
507
508
	private function calculateEtag(string $path, array $stat): string {
509
		if ($stat['mode'] & 0x4000 && !($stat['mode'] & 0x8000)) { // is_dir & not socket
510
			return parent::getETag($path);
511
		} else {
512
			if ($stat === false) {
0 ignored issues
show
introduced by
The condition $stat === false is always false.
Loading history...
513
				return md5('');
514
			}
515
516
			$toHash = '';
517
			if (isset($stat['mtime'])) {
518
				$toHash .= $stat['mtime'];
519
			}
520
			if (isset($stat['ino'])) {
521
				$toHash .= $stat['ino'];
522
			}
523
			if (isset($stat['dev'])) {
524
				$toHash .= $stat['dev'];
525
			}
526
			if (isset($stat['size'])) {
527
				$toHash .= $stat['size'];
528
			}
529
530
			return md5($toHash);
531
		}
532
	}
533
534
	/**
535
	 * @param IStorage $sourceStorage
536
	 * @param string $sourceInternalPath
537
	 * @param string $targetInternalPath
538
	 * @param bool $preserveMtime
539
	 * @return bool
540
	 */
541
	public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
542
		// Don't treat ACLStorageWrapper like local storage where copy can be done directly.
543
		// Instead use the slower recursive copying in php from Common::copyFromStorage with
544
		// more permissions checks.
545
		if ($sourceStorage->instanceOfStorage(Local::class) && !$sourceStorage->instanceOfStorage('OCA\GroupFolders\ACL\ACLStorageWrapper')) {
546
			if ($sourceStorage->instanceOfStorage(Jail::class)) {
547
				/**
548
				 * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
549
				 */
550
				$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
551
			}
552
			/**
553
			 * @var \OC\Files\Storage\Local $sourceStorage
554
			 */
555
			$rootStorage = new Local(['datadir' => '/']);
556
			return $rootStorage->copy($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
557
		} else {
558
			return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
559
		}
560
	}
561
562
	/**
563
	 * @param IStorage $sourceStorage
564
	 * @param string $sourceInternalPath
565
	 * @param string $targetInternalPath
566
	 * @return bool
567
	 */
568
	public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
569
		if ($sourceStorage->instanceOfStorage(Local::class)) {
570
			if ($sourceStorage->instanceOfStorage(Jail::class)) {
571
				/**
572
				 * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
573
				 */
574
				$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
575
			}
576
			/**
577
			 * @var \OC\Files\Storage\Local $sourceStorage
578
			 */
579
			$rootStorage = new Local(['datadir' => '/']);
580
			return $rootStorage->rename($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
581
		} else {
582
			return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
583
		}
584
	}
585
586
	public function writeStream(string $path, $stream, int $size = null): int {
587
		$result = $this->file_put_contents($path, $stream);
588
		if (is_resource($stream)) {
589
			fclose($stream);
590
		}
591
		if ($result === false) {
592
			throw new GenericFileException("Failed write stream to $path");
593
		} else {
594
			return $result;
595
		}
596
	}
597
}
598