Completed
Push — stable2 ( f25894...4ad10a )
by Robin
08:18 queued 06:18
created

Share   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 418
Duplicated Lines 4.31 %

Coupling/Cohesion

Components 1
Dependencies 14

Test Coverage

Coverage 93.33%

Importance

Changes 7
Bugs 1 Features 0
Metric Value
wmc 46
c 7
b 1
f 0
lcom 1
cbo 14
dl 18
loc 418
ccs 154
cts 165
cp 0.9333
rs 8.3999

24 Methods

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