Completed
Push — notifyhandler ( 1e7809...ebc1c0 )
by Robin
03:34
created

Share   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 415
Duplicated Lines 4.34 %

Coupling/Cohesion

Components 1
Dependencies 14

Test Coverage

Coverage 93.29%

Importance

Changes 5
Bugs 1 Features 0
Metric Value
wmc 46
c 5
b 1
f 0
lcom 1
cbo 14
dl 18
loc 415
ccs 153
cts 164
cp 0.9329
rs 8.3999

24 Methods

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