Completed
Push — master ( caccf7...5c360a )
by Morris
29:15 queued 11:42
created

Local::isUpdatable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bart Visscher <[email protected]>
6
 * @author Boris Rybalkin <[email protected]>
7
 * @author Brice Maron <[email protected]>
8
 * @author Jakob Sack <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Jörn Friedrich Dreyer <[email protected]>
11
 * @author Klaas Freitag <[email protected]>
12
 * @author Lukas Reschke <[email protected]>
13
 * @author Martin Mattel <[email protected]>
14
 * @author Michael Gapczynski <[email protected]>
15
 * @author Morris Jobke <[email protected]>
16
 * @author Robin Appelman <[email protected]>
17
 * @author Roeland Jago Douma <[email protected]>
18
 * @author Sjors van der Pluijm <[email protected]>
19
 * @author Stefan Weil <[email protected]>
20
 * @author Thomas Müller <[email protected]>
21
 * @author Tigran Mkrtchyan <[email protected]>
22
 * @author Vincent Petry <[email protected]>
23
 *
24
 * @license AGPL-3.0
25
 *
26
 * This code is free software: you can redistribute it and/or modify
27
 * it under the terms of the GNU Affero General Public License, version 3,
28
 * as published by the Free Software Foundation.
29
 *
30
 * This program is distributed in the hope that it will be useful,
31
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
32
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33
 * GNU Affero General Public License for more details.
34
 *
35
 * You should have received a copy of the GNU Affero General Public License, version 3,
36
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
37
 *
38
 */
39
40
namespace OC\Files\Storage;
41
42
use OC\Files\Storage\Wrapper\Jail;
43
use OCP\Files\ForbiddenException;
44
use OCP\Files\Storage\IStorage;
45
46
/**
47
 * for local filestore, we only have to map the paths
48
 */
49
class Local extends \OC\Files\Storage\Common {
50
	protected $datadir;
51
52
	protected $dataDirLength;
53
54
	protected $allowSymlinks = false;
55
56
	protected $realDataDir;
57
58
	public function __construct($arguments) {
59
		if (!isset($arguments['datadir']) || !is_string($arguments['datadir'])) {
60
			throw new \InvalidArgumentException('No data directory set for local storage');
61
		}
62
		$this->datadir = str_replace('//', '/', $arguments['datadir']);
63
		// some crazy code uses a local storage on root...
64
		if ($this->datadir === '/') {
65
			$this->realDataDir = $this->datadir;
66
		} else {
67
			$realPath = realpath($this->datadir) ?: $this->datadir;
68
			$this->realDataDir = rtrim($realPath, '/') . '/';
69
		}
70
		if (substr($this->datadir, -1) !== '/') {
71
			$this->datadir .= '/';
72
		}
73
		$this->dataDirLength = strlen($this->realDataDir);
74
	}
75
76
	public function __destruct() {
77
	}
78
79
	public function getId() {
80
		return 'local::' . $this->datadir;
81
	}
82
83
	public function mkdir($path) {
84
		return @mkdir($this->getSourcePath($path), 0777, true);
85
	}
86
87
	public function rmdir($path) {
88
		if (!$this->isDeletable($path)) {
89
			return false;
90
		}
91
		try {
92
			$it = new \RecursiveIteratorIterator(
93
				new \RecursiveDirectoryIterator($this->getSourcePath($path)),
94
				\RecursiveIteratorIterator::CHILD_FIRST
95
			);
96
			/**
97
			 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
98
			 * This bug is fixed in PHP 5.5.9 or before
99
			 * See #8376
100
			 */
101
			$it->rewind();
102
			while ($it->valid()) {
103
				/**
104
				 * @var \SplFileInfo $file
105
				 */
106
				$file = $it->current();
107
				if (in_array($file->getBasename(), array('.', '..'))) {
108
					$it->next();
109
					continue;
110
				} elseif ($file->isDir()) {
111
					rmdir($file->getPathname());
112
				} elseif ($file->isFile() || $file->isLink()) {
113
					unlink($file->getPathname());
114
				}
115
				$it->next();
116
			}
117
			return rmdir($this->getSourcePath($path));
118
		} catch (\UnexpectedValueException $e) {
119
			return false;
120
		}
121
	}
122
123
	public function opendir($path) {
124
		return opendir($this->getSourcePath($path));
125
	}
126
127
	public function is_dir($path) {
128
		if (substr($path, -1) == '/') {
129
			$path = substr($path, 0, -1);
130
		}
131
		return is_dir($this->getSourcePath($path));
132
	}
133
134
	public function is_file($path) {
135
		return is_file($this->getSourcePath($path));
136
	}
137
138
	public function stat($path) {
139
		clearstatcache();
140
		$fullPath = $this->getSourcePath($path);
141
		$statResult = stat($fullPath);
142
		if (PHP_INT_SIZE === 4 && !$this->is_dir($path)) {
143
			$filesize = $this->filesize($path);
144
			$statResult['size'] = $filesize;
145
			$statResult[7] = $filesize;
146
		}
147
		return $statResult;
148
	}
149
150
	public function filetype($path) {
151
		$filetype = filetype($this->getSourcePath($path));
152
		if ($filetype == 'link') {
153
			$filetype = filetype(realpath($this->getSourcePath($path)));
154
		}
155
		return $filetype;
156
	}
157
158
	public function filesize($path) {
159
		if ($this->is_dir($path)) {
160
			return 0;
161
		}
162
		$fullPath = $this->getSourcePath($path);
163
		if (PHP_INT_SIZE === 4) {
164
			$helper = new \OC\LargeFileHelper;
165
			return $helper->getFileSize($fullPath);
0 ignored issues
show
Bug Compatibility introduced by
The expression $helper->getFileSize($fullPath); of type integer|double adds the type double to the return on line 165 which is incompatible with the return type declared by the interface OCP\Files\Storage::filesize of type integer|false.
Loading history...
166
		}
167
		return filesize($fullPath);
168
	}
169
170
	public function isReadable($path) {
171
		return is_readable($this->getSourcePath($path));
172
	}
173
174
	public function isUpdatable($path) {
175
		return is_writable($this->getSourcePath($path));
176
	}
177
178
	public function file_exists($path) {
179
		return file_exists($this->getSourcePath($path));
180
	}
181
182
	public function filemtime($path) {
183
		$fullPath = $this->getSourcePath($path);
184
		clearstatcache(true, $fullPath);
185
		if (!$this->file_exists($path)) {
186
			return false;
187
		}
188
		if (PHP_INT_SIZE === 4) {
189
			$helper = new \OC\LargeFileHelper();
190
			return $helper->getFileMtime($fullPath);
0 ignored issues
show
Bug Compatibility introduced by
The expression $helper->getFileMtime($fullPath); of type integer|double|null adds the type double to the return on line 190 which is incompatible with the return type declared by the interface OCP\Files\Storage::filemtime of type integer|false.
Loading history...
191
		}
192
		return filemtime($fullPath);
193
	}
194
195
	public function touch($path, $mtime = null) {
196
		// sets the modification time of the file to the given value.
197
		// If mtime is nil the current time is set.
198
		// note that the access time of the file always changes to the current time.
199
		if ($this->file_exists($path) and !$this->isUpdatable($path)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
200
			return false;
201
		}
202
		if (!is_null($mtime)) {
203
			$result = touch($this->getSourcePath($path), $mtime);
204
		} else {
205
			$result = touch($this->getSourcePath($path));
206
		}
207
		if ($result) {
208
			clearstatcache(true, $this->getSourcePath($path));
209
		}
210
211
		return $result;
212
	}
213
214
	public function file_get_contents($path) {
215
		return file_get_contents($this->getSourcePath($path));
216
	}
217
218
	public function file_put_contents($path, $data) {
219
		return file_put_contents($this->getSourcePath($path), $data);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return file_put_contents...rcePath($path), $data); (integer) is incompatible with the return type declared by the interface OCP\Files\Storage::file_put_contents of type boolean.

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...
220
	}
221
222 View Code Duplication
	public function unlink($path) {
223
		if ($this->is_dir($path)) {
224
			return $this->rmdir($path);
225
		} else if ($this->is_file($path)) {
226
			return unlink($this->getSourcePath($path));
227
		} else {
228
			return false;
229
		}
230
231
	}
232
233
	public function rename($path1, $path2) {
234
		$srcParent = dirname($path1);
235
		$dstParent = dirname($path2);
236
237
		if (!$this->isUpdatable($srcParent)) {
238
			\OCP\Util::writeLog('core', 'unable to rename, source directory is not writable : ' . $srcParent, \OCP\Util::ERROR);
239
			return false;
240
		}
241
242
		if (!$this->isUpdatable($dstParent)) {
243
			\OCP\Util::writeLog('core', 'unable to rename, destination directory is not writable : ' . $dstParent, \OCP\Util::ERROR);
244
			return false;
245
		}
246
247
		if (!$this->file_exists($path1)) {
248
			\OCP\Util::writeLog('core', 'unable to rename, file does not exists : ' . $path1, \OCP\Util::ERROR);
249
			return false;
250
		}
251
252
		if ($this->is_dir($path2)) {
253
			$this->rmdir($path2);
254
		} else if ($this->is_file($path2)) {
255
			$this->unlink($path2);
256
		}
257
258
		if ($this->is_dir($path1)) {
259
			// we can't move folders across devices, use copy instead
260
			$stat1 = stat(dirname($this->getSourcePath($path1)));
261
			$stat2 = stat(dirname($this->getSourcePath($path2)));
262
			if ($stat1['dev'] !== $stat2['dev']) {
263
				$result = $this->copy($path1, $path2);
264
				if ($result) {
265
					$result &= $this->rmdir($path1);
266
				}
267
				return $result;
268
			}
269
		}
270
271
		return rename($this->getSourcePath($path1), $this->getSourcePath($path2));
272
	}
273
274
	public function copy($path1, $path2) {
275
		if ($this->is_dir($path1)) {
276
			return parent::copy($path1, $path2);
277
		} else {
278
			return copy($this->getSourcePath($path1), $this->getSourcePath($path2));
279
		}
280
	}
281
282
	public function fopen($path, $mode) {
283
		return fopen($this->getSourcePath($path), $mode);
284
	}
285
286
	public function hash($type, $path, $raw = false) {
287
		return hash_file($type, $this->getSourcePath($path), $raw);
288
	}
289
290
	public function free_space($path) {
291
		$sourcePath = $this->getSourcePath($path);
292
		// using !is_dir because $sourcePath might be a part file or
293
		// non-existing file, so we'd still want to use the parent dir
294
		// in such cases
295
		if (!is_dir($sourcePath)) {
296
			// disk_free_space doesn't work on files
297
			$sourcePath = dirname($sourcePath);
298
		}
299
		$space = @disk_free_space($sourcePath);
300
		if ($space === false || is_null($space)) {
301
			return \OCP\Files\FileInfo::SPACE_UNKNOWN;
302
		}
303
		return $space;
304
	}
305
306
	public function search($query) {
307
		return $this->searchInDir($query);
308
	}
309
310
	public function getLocalFile($path) {
311
		return $this->getSourcePath($path);
312
	}
313
314
	public function getLocalFolder($path) {
315
		return $this->getSourcePath($path);
316
	}
317
318
	/**
319
	 * @param string $query
320
	 * @param string $dir
321
	 * @return array
322
	 */
323
	protected function searchInDir($query, $dir = '') {
324
		$files = array();
325
		$physicalDir = $this->getSourcePath($dir);
326
		foreach (scandir($physicalDir) as $item) {
327
			if (\OC\Files\Filesystem::isIgnoredDir($item))
328
				continue;
329
			$physicalItem = $physicalDir . '/' . $item;
330
331 View Code Duplication
			if (strstr(strtolower($item), strtolower($query)) !== false) {
332
				$files[] = $dir . '/' . $item;
333
			}
334
			if (is_dir($physicalItem)) {
335
				$files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
336
			}
337
		}
338
		return $files;
339
	}
340
341
	/**
342
	 * check if a file or folder has been updated since $time
343
	 *
344
	 * @param string $path
345
	 * @param int $time
346
	 * @return bool
347
	 */
348
	public function hasUpdated($path, $time) {
349
		if ($this->file_exists($path)) {
350
			return $this->filemtime($path) > $time;
351
		} else {
352
			return true;
353
		}
354
	}
355
356
	/**
357
	 * Get the source path (on disk) of a given path
358
	 *
359
	 * @param string $path
360
	 * @return string
361
	 * @throws ForbiddenException
362
	 */
363
	public function getSourcePath($path) {
364
		$fullPath = $this->datadir . $path;
365
		$currentPath = $path;
366
		if ($this->allowSymlinks || $currentPath === '') {
367
			return $fullPath;
368
		}
369
		$pathToResolve = $fullPath;
370
		$realPath = realpath($pathToResolve);
371
		while ($realPath === false) { // for non existing files check the parent directory
372
			$currentPath = dirname($currentPath);
373
			if ($currentPath === '' || $currentPath === '.') {
374
				return $fullPath;
375
			}
376
			$realPath = realpath($this->datadir . $currentPath);
377
		}
378
		if ($realPath) {
379
			$realPath = $realPath . '/';
380
		}
381
		if (substr($realPath, 0, $this->dataDirLength) === $this->realDataDir) {
382
			return $fullPath;
383
		}
384
385
		\OCP\Util::writeLog('core', "Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", \OCP\Util::ERROR);
386
		throw new ForbiddenException('Following symlinks is not allowed', false);
387
	}
388
389
	/**
390
	 * {@inheritdoc}
391
	 */
392
	public function isLocal() {
393
		return true;
394
	}
395
396
	/**
397
	 * get the ETag for a file or folder
398
	 *
399
	 * @param string $path
400
	 * @return string
401
	 */
402
	public function getETag($path) {
403
		if ($this->is_file($path)) {
404
			$stat = $this->stat($path);
405
			return md5(
406
				$stat['mtime'] .
407
				$stat['ino'] .
408
				$stat['dev'] .
409
				$stat['size']
410
			);
411
		} else {
412
			return parent::getETag($path);
413
		}
414
	}
415
416
	/**
417
	 * @param IStorage $sourceStorage
418
	 * @param string $sourceInternalPath
419
	 * @param string $targetInternalPath
420
	 * @param bool $preserveMtime
421
	 * @return bool
422
	 */
423 View Code Duplication
	public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
424
		if ($sourceStorage->instanceOfStorage(Local::class)) {
425
			if ($sourceStorage->instanceOfStorage(Jail::class)) {
426
				/**
427
				 * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
428
				 */
429
				$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
430
			}
431
			/**
432
			 * @var \OC\Files\Storage\Local $sourceStorage
433
			 */
434
			$rootStorage = new Local(['datadir' => '/']);
435
			return $rootStorage->copy($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
436
		} else {
437
			return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
438
		}
439
	}
440
441
	/**
442
	 * @param IStorage $sourceStorage
443
	 * @param string $sourceInternalPath
444
	 * @param string $targetInternalPath
445
	 * @return bool
446
	 */
447 View Code Duplication
	public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
448
		if ($sourceStorage->instanceOfStorage(Local::class)) {
449
			if ($sourceStorage->instanceOfStorage(Jail::class)) {
450
				/**
451
				 * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
452
				 */
453
				$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
454
			}
455
			/**
456
			 * @var \OC\Files\Storage\Local $sourceStorage
457
			 */
458
			$rootStorage = new Local(['datadir' => '/']);
459
			return $rootStorage->rename($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
460
		} else {
461
			return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
462
		}
463
	}
464
}
465