Completed
Pull Request — master (#81)
by
unknown
03:09
created

Share   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 452
Duplicated Lines 3.98 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 93.1%

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 16
dl 18
loc 452
ccs 162
cts 174
cp 0.931
rs 8.48
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A getName() 0 3 1
A __construct() 0 7 1
A getAuthFileArgument() 0 7 2
A getConnection() 0 21 3
A connect() 0 6 3
A reconnect() 0 6 2
A simpleCommand() 0 6 1
A dir() 0 11 1
B stat() 0 28 7
A mkdir() 0 3 1
A rmdir() 0 3 1
B del() 0 23 6
A rename() 6 6 1
A put() 6 6 1
A get() 6 6 1
A read() 0 12 1
A write() 0 16 1
A append() 0 3 1
A setMode() 0 23 4
A notify() 0 9 2
A execute() 0 5 1
A parseOutput() 0 8 2
A escape() 0 3 1
A escapePath() 0 10 2
A escapeLocalPath() 0 4 1
A __destruct() 0 3 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 Share 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 Share, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Copyright (c) 2014 Robin Appelman <[email protected]>
4
 * This file is licensed under the Licensed under the MIT license:
5
 * http://opensource.org/licenses/MIT
6
 */
7
8
namespace Icewind\SMB\Wrapped;
9
10
use Icewind\SMB\AbstractShare;
11
use Icewind\SMB\Exception\ConnectionException;
12
use Icewind\SMB\Exception\DependencyException;
13
use Icewind\SMB\Exception\FileInUseException;
14
use Icewind\SMB\Exception\InvalidTypeException;
15
use Icewind\SMB\Exception\NotFoundException;
16
use Icewind\SMB\Exception\InvalidRequestException;
17
use Icewind\SMB\IFileInfo;
18
use Icewind\SMB\INotifyHandler;
19
use Icewind\SMB\IServer;
20
use Icewind\SMB\ISystem;
21
use Icewind\Streams\CallbackWrapper;
22
use Icewind\SMB\Native\NativeShare;
23
use Icewind\SMB\Native\NativeServer;
24
25
class Share extends AbstractShare {
26
	/**
27
	 * @var IServer $server
28
	 */
29
	private $server;
30
31
	/**
32
	 * @var string $name
33
	 */
34
	private $name;
35
36
	/**
37
	 * @var Connection $connection
38
	 */
39
	public $connection;
40
41
	/**
42
	 * @var Parser
43
	 */
44
	protected $parser;
45
46
	/**
47
	 * @var ISystem
48
	 */
49
	private $system;
50
51
	const MODE_MAP = [
52
		FileInfo::MODE_READONLY => 'r',
53
		FileInfo::MODE_HIDDEN   => 'h',
54
		FileInfo::MODE_ARCHIVE  => 'a',
55
		FileInfo::MODE_SYSTEM   => 's'
56
	];
57
58
	const EXEC_CMD = 'exec';
59
60
	/**
61
	 * @param IServer $server
62
	 * @param string $name
63
	 * @param ISystem $system
64
	 */
65 1028
	public function __construct(IServer $server, $name, ISystem $system) {
66 1028
		parent::__construct();
67 1028
		$this->server = $server;
68 1028
		$this->name = $name;
69 1028
		$this->system = $system;
70 1028
		$this->parser = new Parser($server->getTimeZone());
71 1028
	}
72
73 1024
	private function getAuthFileArgument() {
74 1024
		if ($this->server->getAuth()->getUsername()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->server->getAuth()->getUsername() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
75 1024
			return '--authentication-file=' . $this->system->getFD(3);
76
		} else {
77
			return '';
78
		}
79
	}
80
81 1024
	protected function getConnection() {
82 1024
		$command = sprintf(
83 1024
			'%s %s%s -t %s %s %s %s',
84 1024
			self::EXEC_CMD,
85 1024
			$this->system->getStdBufPath() ? $this->system->getStdBufPath() . ' -o0 ' : '',
86 1024
			$this->system->getSmbclientPath(),
87 1024
			$this->server->getOptions()->getTimeout(),
88 1024
			$this->getAuthFileArgument(),
89 1024
			$this->server->getAuth()->getExtraCommandLineArguments(),
90 1024
			escapeshellarg('//' . $this->server->getHost() . '/' . $this->name)
91 256
		);
92 1024
		$connection = new Connection($command, $this->parser);
93 1024
		$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
94 1024
		$connection->connect();
95 1024
		if (!$connection->isValid()) {
96
			throw new ConnectionException($connection->readLine());
97
		}
98
		// some versions of smbclient add a help message in first of the first prompt
99 1024
		$connection->clearTillPrompt();
100 1024
		return $connection;
101
	}
102
103
	/**
104
	 * @throws \Icewind\SMB\Exception\ConnectionException
105
	 * @throws \Icewind\SMB\Exception\AuthenticationException
106
	 * @throws \Icewind\SMB\Exception\InvalidHostException
107
	 */
108 1020
	protected function connect() {
109 1020
		if ($this->connection and $this->connection->isValid()) {
110 1020
			return;
111
		}
112 1020
		$this->connection = $this->getConnection();
113 1020
	}
114
115 4
	protected function reconnect() {
116 4
		$this->connection->reconnect();
117 4
		if (!$this->connection->isValid()) {
118
			throw new ConnectionException();
119
		}
120 4
	}
121
122
	/**
123
	 * Get the name of the share
124
	 *
125
	 * @return string
126
	 */
127 8
	public function getName() {
128 8
		return $this->name;
129
	}
130
131 1020
	protected function simpleCommand($command, $path) {
132 1020
		$escapedPath = $this->escapePath($path);
133 1020
		$cmd = $command . ' ' . $escapedPath;
134 1020
		$output = $this->execute($cmd);
135 1020
		return $this->parseOutput($output, $path);
136
	}
137
138
	/**
139
	 * List the content of a remote folder
140
	 *
141
	 * @param $path
142
	 * @return \Icewind\SMB\IFileInfo[]
143
	 *
144
	 * @throws \Icewind\SMB\Exception\NotFoundException
145
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
146
	 */
147 1004
	public function dir($path) {
148 1004
		$escapedPath = $this->escapePath($path);
149 1004
		$output = $this->execute('cd ' . $escapedPath);
150
		//check output for errors
151 1004
		$this->parseOutput($output, $path);
152 1004
		$output = $this->execute('dir');
153
154 1004
		$this->execute('cd /');
155
156 1004
		return $this->parser->parseDir($output, $path);
157
	}
158
159
	/**
160
	 * @param string $path
161
	 * @return \Icewind\SMB\IFileInfo
162
	 */
163 132
	public function stat($path) {
164
		// some windows server setups don't seem to like the allinfo command
165
		// use the dir command instead to get the file info where possible
166 132
		if ($path !== "" && $path !== "/") {
167 128
			$parent = dirname($path);
168 128
			$dir = $this->dir($parent);
169 96
			$file = array_values(array_filter($dir, function (IFileInfo $info) use ($path) {
170 124
				return $info->getPath() === $path;
171 128
			}));
172 128
			if ($file) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $file 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...
173 88
				return $file[0];
174
			}
175 10
		}
176
177 44
		$escapedPath = $this->escapePath($path);
178 8
		$output = $this->execute('allinfo ' . $escapedPath);
179
		// Windows and non Windows Fileserver may respond different
180
		// to the allinfo command for directories. If the result is a single
181
		// line = error line, redo it with a different allinfo parameter
182 8
		if ($escapedPath == '""' && count($output) < 2) {
183
			$output = $this->execute('allinfo ' . '"."');
184
		}
185 8
		if (count($output) < 3) {
186 4
			$this->parseOutput($output, $path);
187
		}
188 4
		$stat = $this->parser->parseStat($output);
189 4
		return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], $stat['mode']);
190
	}
191
192
	/**
193
	 * Create a folder on the share
194
	 *
195
	 * @param string $path
196
	 * @return bool
197
	 *
198
	 * @throws \Icewind\SMB\Exception\NotFoundException
199
	 * @throws \Icewind\SMB\Exception\AlreadyExistsException
200
	 */
201 1008
	public function mkdir($path) {
202 1008
		return $this->simpleCommand('mkdir', $path);
203
	}
204
205
	/**
206
	 * Remove a folder on the share
207
	 *
208
	 * @param string $path
209
	 * @return bool
210
	 *
211
	 * @throws \Icewind\SMB\Exception\NotFoundException
212
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
213
	 */
214 1008
	public function rmdir($path) {
215 1008
		return $this->simpleCommand('rmdir', $path);
216
	}
217
218
	/**
219
	 * Delete a file on the share
220
	 *
221
	 * @param string $path
222
	 * @param bool $secondTry
223
	 * @return bool
224
	 * @throws InvalidTypeException
225
	 * @throws NotFoundException
226
	 * @throws \Exception
227
	 */
228 524
	public function del($path, $secondTry = false) {
229
		//del return a file not found error when trying to delete a folder
230
		//we catch it so we can check if $path doesn't exist or is of invalid type
231
		try {
232 524
			return $this->simpleCommand('del', $path);
233 48
		} catch (NotFoundException $e) {
234
			//no need to do anything with the result, we just check if this throws the not found error
235
			try {
236 8
				$this->simpleCommand('ls', $path);
237 8
			} catch (NotFoundException $e2) {
238 4
				throw $e;
239 4
			} catch (\Exception $e2) {
240 4
				throw new InvalidTypeException($path);
241
			}
242
			throw $e;
243 40
		} catch (FileInUseException $e) {
244 4
			if ($secondTry) {
245
				throw $e;
246
			}
247 4
			$this->reconnect();
248 4
			return $this->del($path, true);
249
		}
250
	}
251
252
	/**
253
	 * Rename a remote file
254
	 *
255
	 * @param string $from
256
	 * @param string $to
257
	 * @return bool
258
	 *
259
	 * @throws \Icewind\SMB\Exception\NotFoundException
260
	 * @throws \Icewind\SMB\Exception\AlreadyExistsException
261
	 */
262 120 View Code Duplication
	public function rename($from, $to) {
263 120
		$path1 = $this->escapePath($from);
264 84
		$path2 = $this->escapePath($to);
265 84
		$output = $this->execute('rename ' . $path1 . ' ' . $path2);
266 84
		return $this->parseOutput($output, $to);
267
	}
268
269
	/**
270
	 * Upload a local file
271
	 *
272
	 * @param string $source local file
273
	 * @param string $target remove file
274
	 * @return bool
275
	 *
276
	 * @throws \Icewind\SMB\Exception\NotFoundException
277
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
278
	 */
279 460 View Code Duplication
	public function put($source, $target) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
280 460
		$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
281 460
		$path2 = $this->escapePath($target);
282 424
		$output = $this->execute('put ' . $path1 . ' ' . $path2);
283 424
		return $this->parseOutput($output, $target);
284
	}
285
286
	/**
287
	 * Download a remote file
288
	 *
289
	 * @param string $source remove file
290
	 * @param string $target local file
291
	 * @return bool
292
	 *
293
	 * @throws \Icewind\SMB\Exception\NotFoundException
294
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
295
	 */
296 176 View Code Duplication
	public function get($source, $target) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
297 176
		$path1 = $this->escapePath($source);
298 140
		$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping
299 140
		$output = $this->execute('get ' . $path1 . ' ' . $path2);
300 140
		return $this->parseOutput($output, $source);
301
	}
302
303
	/**
304
	 * Open a readable stream to a remote file
305
	 *
306
	 * @param string $source
307
	 * @return resource a read only stream with the contents of the remote file
308
	 *
309
	 * @throws \Icewind\SMB\Exception\NotFoundException
310
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
311
	 */
312 100
	public function read($source) {
313 100
		$source = $this->escapePath($source);
314
		// since returned stream is closed by the caller we need to create a new instance
315
		// since we can't re-use the same file descriptor over multiple calls
316 64
		$connection = $this->getConnection();
317
318 64
		$connection->write('get ' . $source . ' ' . $this->system->getFD(5));
319 64
		$connection->write('exit');
320 64
		$fh = $connection->getFileOutputStream();
321 64
		stream_context_set_option($fh, 'file', 'connection', $connection);
322 64
		return $fh;
323
	}
324
325
	/**
326
	 * Open a writable stream to a remote file
327
	 *
328
	 * @param string $target
329
	 * @return resource a write only stream to upload a remote file
330
	 *
331
	 * @throws \Icewind\SMB\Exception\NotFoundException
332
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
333
	 */
334 100
	public function write($target) {
335 100
		$target = $this->escapePath($target);
336
		// since returned stream is closed by the caller we need to create a new instance
337
		// since we can't re-use the same file descriptor over multiple calls
338 64
		$connection = $this->getConnection();
339
340 64
		$fh = $connection->getFileInputStream();
341 64
		$connection->write('put ' . $this->system->getFD(4) . ' ' . $target);
342 64
		$connection->write('exit');
343
344
		// use a close callback to ensure the upload is finished before continuing
345
		// this also serves as a way to keep the connection in scope
346 64
		return CallbackWrapper::wrap($fh, null, null, function () use ($connection, $target) {
347 64
			$connection->close(false); // dont terminate, give the upload some time
348 64
		});
349
	}
350
351
	/**
352
	 * Append to stream
353
	 * Note: smbclient does not support this (Use php-libsmbclient)
354
	 *
355
	 * @param string $target
356
	 *
357
	 * @throws \Icewind\SMB\Exception\DependencyException
358
	 */
359 4
	public function append($target) {
360 4
		throw new DependencyException('php-libsmbclient is required for append');
361
	}
362
363
	/**
364
	 * @param string $path
365
	 * @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
366
	 * @return mixed
367
	 */
368 32
	public function setMode($path, $mode) {
369 32
		$modeString = '';
370 32
		foreach (self::MODE_MAP as $modeByte => $string) {
371 32
			if ($mode & $modeByte) {
372 32
				$modeString .= $string;
373 8
			}
374 8
		}
375 32
		$path = $this->escapePath($path);
376
377
		// first reset the mode to normal
378 32
		$cmd = 'setmode ' . $path . ' -rsha';
379 32
		$output = $this->execute($cmd);
380 32
		$this->parseOutput($output, $path);
381
382 32
		if ($mode !== FileInfo::MODE_NORMAL) {
383
			// then set the modes we want
384 32
			$cmd = 'setmode ' . $path . ' ' . $modeString;
385 32
			$output = $this->execute($cmd);
386 32
			return $this->parseOutput($output, $path);
387
		} else {
388 32
			return true;
389
		}
390
	}
391
392
	/**
393
	 * @param string $path
394
	 * @return INotifyHandler
395
	 * @throws ConnectionException
396
	 * @throws DependencyException
397
	 */
398 20
	public function notify($path) {
399 20
		if (!$this->system->getStdBufPath()) { //stdbuf is required to disable smbclient's output buffering
400
			throw new DependencyException('stdbuf is required for usage of the notify command');
401
		}
402 20
		$connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process
403 20
		$command = 'notify ' . $this->escapePath($path);
404 20
		$connection->write($command . PHP_EOL);
405 20
		return new NotifyHandler($connection, $path);
406
	}
407
408
	/**
409
	 * @param string $command
410
	 * @return array
411
	 */
412 1020
	protected function execute($command) {
413 1020
		$this->connect();
414 1020
		$this->connection->write($command . PHP_EOL);
415 1020
		return $this->connection->read();
416
	}
417
418
	/**
419
	 * check output for errors
420
	 *
421
	 * @param string[] $lines
422
	 * @param string $path
423
	 *
424
	 * @throws NotFoundException
425
	 * @throws \Icewind\SMB\Exception\AlreadyExistsException
426
	 * @throws \Icewind\SMB\Exception\AccessDeniedException
427
	 * @throws \Icewind\SMB\Exception\NotEmptyException
428
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
429
	 * @throws \Icewind\SMB\Exception\Exception
430
	 * @return bool
431
	 */
432 1020
	protected function parseOutput($lines, $path = '') {
433 1020
		if (count($lines) === 0) {
434 1020
			return true;
435
		} else {
436 80
			$this->parser->checkForError($lines, $path);
437
			return false;
438
		}
439
	}
440
441
	/**
442
	 * @param string $string
443
	 * @return string
444
	 */
445
	protected function escape($string) {
446
		return escapeshellarg($string);
447
	}
448
449
	/**
450
	 * @param string $path
451
	 * @return string
452
	 */
453 1024
	protected function escapePath($path) {
454 1024
		$this->verifyPath($path);
455 1024
		if ($path === '/') {
456 4
			$path = '';
457 1
		}
458 1024
		$path = str_replace('/', '\\', $path);
459 1024
		$path = str_replace('"', '^"', $path);
460 1024
		$path = ltrim($path, '\\');
461 1024
		return '"' . $path . '"';
462
	}
463
464
	/**
465
	 * @param string $path
466
	 * @return string
467
	 */
468 532
	protected function escapeLocalPath($path) {
469 532
		$path = str_replace('"', '\"', $path);
470 532
		return '"' . $path . '"';
471
	}
472
473 1028
	public function __destruct() {
474 1028
		unset($this->connection);
475 1028
	}
476
}
477