Completed
Push — master ( 0cff70...dba08f )
by Morris
09:32
created

SFTP   D

Complexity

Total Complexity 90

Size/Duplication

Total Lines 426
Duplicated Lines 0.94 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 90
lcom 1
cbo 6
dl 4
loc 426
rs 4.8717
c 1
b 1
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
B splitHost() 0 16 5
C __construct() 0 33 7
B getConnection() 0 24 5
A test() 0 9 3
A getId() 0 11 2
A getHost() 0 3 1
A getRoot() 0 3 1
A getUser() 0 3 1
A absPath() 0 3 1
A hostKeysPath() 0 12 3
B writeHostKeys() 0 15 5
B readHostKeys() 0 22 6
A mkdir() 0 7 2
A rmdir() 0 11 2
B opendir() 0 19 6
A filetype() 0 15 4
A file_exists() 0 7 2
A unlink() 0 7 2
D fopen() 4 29 17
A touch() 0 15 4
A getFile() 0 3 1
A uploadFile() 0 3 1
A rename() 0 13 4
A stat() 0 12 4
A constructUrl() 0 7 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SFTP often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SFTP, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Andreas Fischer <[email protected]>
4
 * @author Bart Visscher <[email protected]>
5
 * @author hkjolhede <[email protected]>
6
 * @author Jörn Friedrich Dreyer <[email protected]>
7
 * @author Lennart Rosam <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Robin McCorkell <[email protected]>
12
 * @author Ross Nicoll <[email protected]>
13
 * @author SA <[email protected]>
14
 * @author Vincent Petry <[email protected]>
15
 *
16
 * @copyright Copyright (c) 2016, ownCloud, Inc.
17
 * @license AGPL-3.0
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
namespace OCA\Files_External\Lib\Storage;
33
use Icewind\Streams\IteratorDirectory;
34
35
use Icewind\Streams\RetryWrapper;
36
use phpseclib\Net\SFTP\Stream;
37
38
/**
39
* Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to
40
* provide access to SFTP servers.
41
*/
42
class SFTP extends \OC\Files\Storage\Common {
43
	private $host;
44
	private $user;
45
	private $root;
46
	private $port = 22;
47
48
	private $auth;
49
50
	/**
51
	* @var SFTP
52
	*/
53
	protected $client;
54
55
	/**
56
	 * @param string $host protocol://server:port
57
	 * @return array [$server, $port]
58
	 */
59
	private function splitHost($host) {
60
		$input = $host;
61
		if (strpos($host, '://') === false) {
62
			// add a protocol to fix parse_url behavior with ipv6
63
			$host = 'http://' . $host;
64
		}
65
66
		$parsed = parse_url($host);
67
		if(is_array($parsed) && isset($parsed['port'])) {
68
			return [$parsed['host'], $parsed['port']];
69
		} else if (is_array($parsed)) {
70
			return [$parsed['host'], 22];
71
		} else {
72
			return [$input, 22];
73
		}
74
	}
75
76
	/**
77
	 * {@inheritdoc}
78
	 */
79
	public function __construct($params) {
80
		// Register sftp://
81
		Stream::register();
82
83
		$parsedHost =  $this->splitHost($params['host']);
84
85
		$this->host = $parsedHost[0];
86
		$this->port = $parsedHost[1];
87
88
		if (!isset($params['user'])) {
89
			throw new \UnexpectedValueException('no authentication parameters specified');
90
		}
91
		$this->user = $params['user'];
92
93
		if (isset($params['public_key_auth'])) {
94
			$this->auth = $params['public_key_auth'];
95
		} elseif (isset($params['password'])) {
96
			$this->auth = $params['password'];
97
		} else {
98
			throw new \UnexpectedValueException('no authentication parameters specified');
99
		}
100
101
		$this->root
102
			= isset($params['root']) ? $this->cleanPath($params['root']) : '/';
103
104
		if ($this->root[0] != '/') {
105
			 $this->root = '/' . $this->root;
106
		}
107
108
		if (substr($this->root, -1, 1) != '/') {
109
			$this->root .= '/';
110
		}
111
	}
112
113
	/**
114
	 * Returns the connection.
115
	 *
116
	 * @return \phpseclib\Net\SFTP connected client instance
117
	 * @throws \Exception when the connection failed
118
	 */
119
	public function getConnection() {
120
		if (!is_null($this->client)) {
121
			return $this->client;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->client; (OCA\Files_External\Lib\Storage\SFTP) is incompatible with the return type documented by OCA\Files_External\Lib\Storage\SFTP::getConnection of type phpseclib\Net\SFTP.

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...
122
		}
123
124
		$hostKeys = $this->readHostKeys();
125
		$this->client = new \phpseclib\Net\SFTP($this->host, $this->port);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \phpseclib\Net\SFTP($this->host, $this->port) of type object<phpseclib\Net\SFTP> is incompatible with the declared type object<OCA\Files_External\Lib\Storage\SFTP> of property $client.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
126
127
		// The SSH Host Key MUST be verified before login().
128
		$currentHostKey = $this->client->getServerPublicHostKey();
129
		if (array_key_exists($this->host, $hostKeys)) {
130
			if ($hostKeys[$this->host] != $currentHostKey) {
131
				throw new \Exception('Host public key does not match known key');
132
			}
133
		} else {
134
			$hostKeys[$this->host] = $currentHostKey;
135
			$this->writeHostKeys($hostKeys);
136
		}
137
138
		if (!$this->client->login($this->user, $this->auth)) {
139
			throw new \Exception('Login failed');
140
		}
141
		return $this->client;
142
	}
143
144
	/**
145
	 * {@inheritdoc}
146
	 */
147
	public function test() {
148
		if (
149
			!isset($this->host)
150
			|| !isset($this->user)
151
		) {
152
			return false;
153
		}
154
		return $this->getConnection()->nlist() !== false;
155
	}
156
157
	/**
158
	 * {@inheritdoc}
159
	 */
160
	public function getId(){
161
		$id = 'sftp::' . $this->user . '@' . $this->host;
162
		if ($this->port !== 22) {
163
			$id .= ':' . $this->port;
164
		}
165
		// note: this will double the root slash,
166
		// we should not change it to keep compatible with
167
		// old storage ids
168
		$id .= '/' . $this->root;
169
		return $id;
170
	}
171
172
	/**
173
	 * @return string
174
	 */
175
	public function getHost() {
176
		return $this->host;
177
	}
178
179
	/**
180
	 * @return string
181
	 */
182
	public function getRoot() {
183
		return $this->root;
184
	}
185
186
	/**
187
	 * @return mixed
188
	 */
189
	public function getUser() {
190
		return $this->user;
191
	}
192
193
	/**
194
	 * @param string $path
195
	 * @return string
196
	 */
197
	private function absPath($path) {
198
		return $this->root . $this->cleanPath($path);
199
	}
200
201
	/**
202
	 * @return string|false
203
	 */
204
	private function hostKeysPath() {
205
		try {
206
			$storage_view = \OCP\Files::getStorage('files_external');
207
			if ($storage_view) {
208
				return \OC::$server->getConfig()->getSystemValue('datadirectory') .
209
					$storage_view->getAbsolutePath('') .
210
					'ssh_hostKeys';
211
			}
212
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
213
		}
214
		return false;
215
	}
216
217
	/**
218
	 * @param $keys
219
	 * @return bool
220
	 */
221
	protected function writeHostKeys($keys) {
222
		try {
223
			$keyPath = $this->hostKeysPath();
224
			if ($keyPath && file_exists($keyPath)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $keyPath of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
225
				$fp = fopen($keyPath, 'w');
226
				foreach ($keys as $host => $key) {
227
					fwrite($fp, $host . '::' . $key . "\n");
228
				}
229
				fclose($fp);
230
				return true;
231
			}
232
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
233
		}
234
		return false;
235
	}
236
237
	/**
238
	 * @return array
239
	 */
240
	protected function readHostKeys() {
241
		try {
242
			$keyPath = $this->hostKeysPath();
243
			if (file_exists($keyPath)) {
244
				$hosts = array();
245
				$keys = array();
246
				$lines = file($keyPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
247
				if ($lines) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lines of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
248
					foreach ($lines as $line) {
249
						$hostKeyArray = explode("::", $line, 2);
250
						if (count($hostKeyArray) == 2) {
251
							$hosts[] = $hostKeyArray[0];
252
							$keys[] = $hostKeyArray[1];
253
						}
254
					}
255
					return array_combine($hosts, $keys);
256
				}
257
			}
258
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
259
		}
260
		return array();
261
	}
262
263
	/**
264
	 * {@inheritdoc}
265
	 */
266
	public function mkdir($path) {
267
		try {
268
			return $this->getConnection()->mkdir($this->absPath($path));
269
		} catch (\Exception $e) {
270
			return false;
271
		}
272
	}
273
274
	/**
275
	 * {@inheritdoc}
276
	 */
277
	public function rmdir($path) {
278
		try {
279
			$result = $this->getConnection()->delete($this->absPath($path), true);
280
			// workaround: stray stat cache entry when deleting empty folders
281
			// see https://github.com/phpseclib/phpseclib/issues/706
282
			$this->getConnection()->clearStatCache();
283
			return $result;
284
		} catch (\Exception $e) {
285
			return false;
286
		}
287
	}
288
289
	/**
290
	 * {@inheritdoc}
291
	 */
292
	public function opendir($path) {
293
		try {
294
			$list = $this->getConnection()->nlist($this->absPath($path));
295
			if ($list === false) {
296
				return false;
297
			}
298
299
			$id = md5('sftp:' . $path);
0 ignored issues
show
Unused Code introduced by
$id is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
300
			$dirStream = array();
301
			foreach($list as $file) {
302
				if ($file != '.' && $file != '..') {
303
					$dirStream[] = $file;
304
				}
305
			}
306
			return IteratorDirectory::wrap($dirStream);
307
		} catch(\Exception $e) {
308
			return false;
309
		}
310
	}
311
312
	/**
313
	 * {@inheritdoc}
314
	 */
315
	public function filetype($path) {
316
		try {
317
			$stat = $this->getConnection()->stat($this->absPath($path));
318
			if ($stat['type'] == NET_SFTP_TYPE_REGULAR) {
319
				return 'file';
320
			}
321
322
			if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) {
323
				return 'dir';
324
			}
325
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
326
327
		}
328
		return false;
329
	}
330
331
	/**
332
	 * {@inheritdoc}
333
	 */
334
	public function file_exists($path) {
335
		try {
336
			return $this->getConnection()->stat($this->absPath($path)) !== false;
337
		} catch (\Exception $e) {
338
			return false;
339
		}
340
	}
341
342
	/**
343
	 * {@inheritdoc}
344
	 */
345
	public function unlink($path) {
346
		try {
347
			return $this->getConnection()->delete($this->absPath($path), true);
348
		} catch (\Exception $e) {
349
			return false;
350
		}
351
	}
352
353
	/**
354
	 * {@inheritdoc}
355
	 */
356
	public function fopen($path, $mode) {
357
		try {
358
			$absPath = $this->absPath($path);
0 ignored issues
show
Unused Code introduced by
$absPath is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
359
			switch($mode) {
360
				case 'r':
361
				case 'rb':
362
					if ( !$this->file_exists($path)) {
363
						return false;
364
					}
365
				case 'w':
366
				case 'wb':
367
				case 'a':
368
				case 'ab':
369
				case 'r+':
370
				case 'w+':
371
				case 'wb+':
372
				case 'a+':
373
				case 'x':
374
				case 'x+':
375
				case 'c':
376 View Code Duplication
				case 'c+':
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...
377
					$context = stream_context_create(array('sftp' => array('session' => $this->getConnection())));
378
					$handle = fopen($this->constructUrl($path), $mode, false, $context);
379
					return RetryWrapper::wrap($handle);
380
			}
381
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
382
		}
383
		return false;
384
	}
385
386
	/**
387
	 * {@inheritdoc}
388
	 */
389
	public function touch($path, $mtime=null) {
390
		try {
391
			if (!is_null($mtime)) {
392
				return false;
393
			}
394
			if (!$this->file_exists($path)) {
395
				$this->getConnection()->put($this->absPath($path), '');
396
			} else {
397
				return false;
398
			}
399
		} catch (\Exception $e) {
400
			return false;
401
		}
402
		return true;
403
	}
404
405
	/**
406
	 * @param string $path
407
	 * @param string $target
408
	 * @throws \Exception
409
	 */
410
	public function getFile($path, $target) {
411
		$this->getConnection()->get($path, $target);
412
	}
413
414
	/**
415
	 * @param string $path
416
	 * @param string $target
417
	 * @throws \Exception
418
	 */
419
	public function uploadFile($path, $target) {
420
		$this->getConnection()->put($target, $path, NET_SFTP_LOCAL_FILE);
421
	}
422
423
	/**
424
	 * {@inheritdoc}
425
	 */
426
	public function rename($source, $target) {
427
		try {
428
			if (!$this->is_dir($target) && $this->file_exists($target)) {
429
				$this->unlink($target);
430
			}
431
			return $this->getConnection()->rename(
432
				$this->absPath($source),
433
				$this->absPath($target)
434
			);
435
		} catch (\Exception $e) {
436
			return false;
437
		}
438
	}
439
440
	/**
441
	 * {@inheritdoc}
442
	 */
443
	public function stat($path) {
444
		try {
445
			$stat = $this->getConnection()->stat($this->absPath($path));
446
447
			$mtime = $stat ? $stat['mtime'] : -1;
448
			$size = $stat ? $stat['size'] : 0;
449
450
			return array('mtime' => $mtime, 'size' => $size, 'ctime' => -1);
451
		} catch (\Exception $e) {
452
			return false;
453
		}
454
	}
455
456
	/**
457
	 * @param string $path
458
	 * @return string
459
	 */
460
	public function constructUrl($path) {
461
		// Do not pass the password here. We want to use the Net_SFTP object
462
		// supplied via stream context or fail. We only supply username and
463
		// hostname because this might show up in logs (they are not used).
464
		$url = 'sftp://' . urlencode($this->user) . '@' . $this->host . ':' . $this->port . $this->root . $path;
465
		return $url;
466
	}
467
}
468