Passed
Push — master ( 7042f9...a688f4 )
by Roeland
21:39 queued 10:37
created

Local::getMetaData()   C

Complexity

Conditions 11
Paths 145

Size

Total Lines 43
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 30
c 1
b 0
f 0
nc 145
nop 1
dl 0
loc 43
rs 6.9416

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 Arthur Schiwon <[email protected]>
6
 * @author Bart Visscher <[email protected]>
7
 * @author Boris Rybalkin <[email protected]>
8
 * @author Brice Maron <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Jakob Sack <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author Jörn Friedrich Dreyer <[email protected]>
13
 * @author Klaas Freitag <[email protected]>
14
 * @author Lukas Reschke <[email protected]>
15
 * @author Martin Mattel <[email protected]>
16
 * @author Michael Gapczynski <[email protected]>
17
 * @author Morris Jobke <[email protected]>
18
 * @author Robin Appelman <[email protected]>
19
 * @author Roeland Jago Douma <[email protected]>
20
 * @author Sjors van der Pluijm <[email protected]>
21
 * @author Stefan Weil <[email protected]>
22
 * @author Thomas Müller <[email protected]>
23
 * @author Tigran Mkrtchyan <[email protected]>
24
 * @author Vincent Petry <[email protected]>
25
 *
26
 * @license AGPL-3.0
27
 *
28
 * This code is free software: you can redistribute it and/or modify
29
 * it under the terms of the GNU Affero General Public License, version 3,
30
 * as published by the Free Software Foundation.
31
 *
32
 * This program is distributed in the hope that it will be useful,
33
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
34
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35
 * GNU Affero General Public License for more details.
36
 *
37
 * You should have received a copy of the GNU Affero General Public License, version 3,
38
 * along with this program. If not, see <http://www.gnu.org/licenses/>
39
 *
40
 */
41
42
namespace OC\Files\Storage;
43
44
use OC\Files\Filesystem;
45
use OC\Files\Storage\Wrapper\Jail;
46
use OCP\Constants;
47
use OCP\Files\ForbiddenException;
48
use OCP\Files\Storage\IStorage;
49
use OCP\ILogger;
50
51
/**
52
 * for local filestore, we only have to map the paths
53
 */
54
class Local extends \OC\Files\Storage\Common {
55
	protected $datadir;
56
57
	protected $dataDirLength;
58
59
	protected $allowSymlinks = false;
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
		return @mkdir($this->getSourcePath($path), 0777, true);
90
	}
91
92
	public function rmdir($path) {
93
		if (!$this->isDeletable($path)) {
94
			return false;
95
		}
96
		try {
97
			$it = new \RecursiveIteratorIterator(
98
				new \RecursiveDirectoryIterator($this->getSourcePath($path)),
99
				\RecursiveIteratorIterator::CHILD_FIRST
100
			);
101
			/**
102
			 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
103
			 * This bug is fixed in PHP 5.5.9 or before
104
			 * See #8376
105
			 */
106
			$it->rewind();
107
			while ($it->valid()) {
108
				/**
109
				 * @var \SplFileInfo $file
110
				 */
111
				$file = $it->current();
112
				if (in_array($file->getBasename(), ['.', '..'])) {
113
					$it->next();
114
					continue;
115
				} else if ($file->isDir()) {
116
					rmdir($file->getPathname());
117
				} else if ($file->isFile() || $file->isLink()) {
118
					unlink($file->getPathname());
119
				}
120
				$it->next();
121
			}
122
			return rmdir($this->getSourcePath($path));
123
		} catch (\UnexpectedValueException $e) {
124
			return false;
125
		}
126
	}
127
128
	public function opendir($path) {
129
		return opendir($this->getSourcePath($path));
130
	}
131
132
	public function is_dir($path) {
133
		if (substr($path, -1) == '/') {
134
			$path = substr($path, 0, -1);
135
		}
136
		return is_dir($this->getSourcePath($path));
137
	}
138
139
	public function is_file($path) {
140
		return is_file($this->getSourcePath($path));
141
	}
142
143
	public function stat($path) {
144
		clearstatcache();
145
		$fullPath = $this->getSourcePath($path);
146
		$statResult = stat($fullPath);
147
		if (PHP_INT_SIZE === 4 && !$this->is_dir($path)) {
148
			$filesize = $this->filesize($path);
149
			$statResult['size'] = $filesize;
150
			$statResult[7] = $filesize;
151
		}
152
		return $statResult;
153
	}
154
155
	/**
156
	 * @inheritdoc
157
	 */
158
	public function getMetaData($path) {
159
		$fullPath = $this->getSourcePath($path);
160
		$stat = @stat($fullPath);
161
		if (!$stat) {
162
			return null;
163
		}
164
165
		$permissions = Constants::PERMISSION_SHARE;
166
		$statPermissions = $stat['mode'];
167
		$isDir = ($statPermissions & 0x4000) === 0x4000;
168
		if ($statPermissions & 0x0100) {
169
			$permissions += Constants::PERMISSION_READ;
170
		}
171
		if ($statPermissions & 0x0080) {
172
			$permissions += Constants::PERMISSION_UPDATE;
173
			if ($isDir) {
174
				$permissions += Constants::PERMISSION_CREATE;
175
			}
176
		}
177
178
		if (!($path === '' || $path === '/')) { // deletable depends on the parents unix permissions
179
			$parent = dirname($fullPath);
180
			if (is_writable($parent)) {
181
				$permissions += Constants::PERMISSION_DELETE;
182
			}
183
		}
184
185
		$data = [];
186
		$data['mimetype'] = $isDir ? 'httpd/unix-directory' : \OC::$server->getMimeTypeDetector()->detectPath($path);
187
		$data['mtime'] = $stat['mtime'];
188
		if ($data['mtime'] === false) {
189
			$data['mtime'] = time();
190
		}
191
		if ($isDir) {
192
			$data['size'] = -1; //unknown
193
		} else {
194
			$data['size'] = $stat['size'];
195
		}
196
		$data['etag'] = $this->calculateEtag($path, $stat);
197
		$data['storage_mtime'] = $data['mtime'];
198
		$data['permissions'] = $permissions;
199
200
		return $data;
201
	}
202
203
	public function filetype($path) {
204
		$filetype = filetype($this->getSourcePath($path));
205
		if ($filetype == 'link') {
206
			$filetype = filetype(realpath($this->getSourcePath($path)));
207
		}
208
		return $filetype;
209
	}
210
211
	public function filesize($path) {
212
		if ($this->is_dir($path)) {
213
			return 0;
214
		}
215
		$fullPath = $this->getSourcePath($path);
216
		if (PHP_INT_SIZE === 4) {
217
			$helper = new \OC\LargeFileHelper;
218
			return $helper->getFileSize($fullPath);
219
		}
220
		return filesize($fullPath);
221
	}
222
223
	public function isReadable($path) {
224
		return is_readable($this->getSourcePath($path));
225
	}
226
227
	public function isUpdatable($path) {
228
		return is_writable($this->getSourcePath($path));
229
	}
230
231
	public function file_exists($path) {
232
		return file_exists($this->getSourcePath($path));
233
	}
234
235
	public function filemtime($path) {
236
		$fullPath = $this->getSourcePath($path);
237
		clearstatcache(true, $fullPath);
238
		if (!$this->file_exists($path)) {
239
			return false;
240
		}
241
		if (PHP_INT_SIZE === 4) {
242
			$helper = new \OC\LargeFileHelper();
243
			return $helper->getFileMtime($fullPath);
244
		}
245
		return filemtime($fullPath);
246
	}
247
248
	public function touch($path, $mtime = null) {
249
		// sets the modification time of the file to the given value.
250
		// If mtime is nil the current time is set.
251
		// note that the access time of the file always changes to the current time.
252
		if ($this->file_exists($path) and !$this->isUpdatable($path)) {
253
			return false;
254
		}
255
		if (!is_null($mtime)) {
256
			$result = @touch($this->getSourcePath($path), $mtime);
257
		} else {
258
			$result = @touch($this->getSourcePath($path));
259
		}
260
		if ($result) {
261
			clearstatcache(true, $this->getSourcePath($path));
262
		}
263
264
		return $result;
265
	}
266
267
	public function file_get_contents($path) {
268
		return file_get_contents($this->getSourcePath($path));
269
	}
270
271
	public function file_put_contents($path, $data) {
272
		return file_put_contents($this->getSourcePath($path), $data);
0 ignored issues
show
Bug Best Practice introduced by
The expression return file_put_contents...urcePath($path), $data) returns the type integer which is incompatible with the return type mandated by OCP\Files\Storage::file_put_contents() of boolean.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
273
	}
274
275
	public function unlink($path) {
276
		if ($this->is_dir($path)) {
277
			return $this->rmdir($path);
278
		} else if ($this->is_file($path)) {
279
			return unlink($this->getSourcePath($path));
280
		} else {
281
			return false;
282
		}
283
284
	}
285
286
	private function treeContainsBlacklistedFile(string $path): bool {
287
		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
288
		foreach ($iterator as $file) {
289
			/** @var \SplFileInfo $file */
290
			if (Filesystem::isFileBlacklisted($file->getBasename())) {
291
				return true;
292
			}
293
		}
294
295
		return false;
296
	}
297
298
	public function rename($path1, $path2) {
299
		$srcParent = dirname($path1);
300
		$dstParent = dirname($path2);
301
302
		if (!$this->isUpdatable($srcParent)) {
303
			\OCP\Util::writeLog('core', 'unable to rename, source directory is not writable : ' . $srcParent, ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

Loading history...
309
			return false;
310
		}
311
312
		if (!$this->file_exists($path1)) {
313
			\OCP\Util::writeLog('core', 'unable to rename, file does not exists : ' . $path1, ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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