Completed
Push — master ( 669135...206d6a )
by Robin
03:18
created

Share   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 416
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Test Coverage

Coverage 93.33%

Importance

Changes 6
Bugs 1 Features 0
Metric Value
wmc 46
lcom 1
cbo 14
dl 0
loc 416
c 6
b 1
f 0
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 dir() 0 10 1
A stat() 0 15 4
A mkdir() 0 3 1
A rmdir() 0 3 1
B del() 0 23 6
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 read() 0 12 1
A write() 0 16 1
A rename() 0 6 1
A put() 0 6 1
A get() 0 6 1
B getConnection() 0 21 5
A connect() 0 6 3
A reconnect() 0 6 2
A escapePath() 0 10 2
A escapeLocalPath() 0 4 1
A __destruct() 0 3 1

How to fix   Complexity   

Complex Class

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 512
	public function __construct($server, $name, System $system = null) {
49 512
		parent::__construct();
50 512
		$this->server = $server;
51 512
		$this->name = $name;
52 512
		$this->system = (!is_null($system)) ? $system : new System();
53 512
		$this->parser = new Parser(new TimeZoneProvider($this->server->getHost(), $this->system));
54 512
	}
55
56 510
	protected function getConnection() {
57 510
		$workgroupArgument = ($this->server->getWorkgroup()) ? ' -W ' . escapeshellarg($this->server->getWorkgroup()) : '';
58 510
		$smbClientPath = $this->system->getSmbclientPath();
59 510
		if (!$smbClientPath) {
60 2
			throw new DependencyException('Can\'t find smbclient binary in path');
61
		}
62 510
		$command = sprintf('%s%s %s --authentication-file=%s %s',
63 510
			$this->system->hasStdBuf() ? 'stdbuf -o0 ' : '',
64 510
			$this->system->getSmbclientPath(),
65 510
			$workgroupArgument,
66 510
			System::getFD(3),
67 510
			escapeshellarg('//' . $this->server->getHost() . '/' . $this->name)
68 510
		);
69 510
		$connection = new Connection($command, $this->parser);
70 510
		$connection->writeAuthentication($this->server->getUser(), $this->server->getPassword());
71 510
		$connection->connect();
72 510
		if (!$connection->isValid()) {
73
			throw new ConnectionException($connection->readLine());
74
		}
75 510
		return $connection;
76
	}
77
78
	/**
79
	 * @throws \Icewind\SMB\Exception\ConnectionException
80
	 * @throws \Icewind\SMB\Exception\AuthenticationException
81
	 * @throws \Icewind\SMB\Exception\InvalidHostException
82
	 */
83 508
	protected function connect() {
84 508
		if ($this->connection and $this->connection->isValid()) {
85 508
			return;
86
		}
87 508
		$this->connection = $this->getConnection();
88 508
	}
89
90 2
	protected function reconnect() {
91 2
		$this->connection->reconnect();
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 508
	protected function simpleCommand($command, $path) {
107 508
		$escapedPath = $this->escapePath($path);
108 508
		$cmd = $command . ' ' . $escapedPath;
109 508
		$output = $this->execute($cmd);
110 508
		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 262
	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 262
			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 58
	public function rename($from, $to) {
224 58
		$path1 = $this->escapePath($from);
225 40
		$path2 = $this->escapePath($to);
226 40
		$output = $this->execute('rename ' . $path1 . ' ' . $path2);
227 40
		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 230
	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...
241 230
		$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
242 230
		$path2 = $this->escapePath($target);
243 212
		$output = $this->execute('put ' . $path1 . ' ' . $path2);
244 212
		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
	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...
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 10
	public function notify($path) {
354 10
		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 10
		$connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process
358 10
		$command = 'notify ' . $this->escapePath($path);
359 10
		$connection->write($command . PHP_EOL);
360 10
		return new NotifyHandler($connection, $path);
361
	}
362
363
	/**
364
	 * @param string $command
365
	 * @return array
366
	 */
367 508
	protected function execute($command) {
368 508
		$this->connect();
369 508
		$this->connection->write($command . PHP_EOL);
370 508
		$output = $this->connection->read();
371 508
		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 508
	protected function parseOutput($lines, $path = '') {
389 508
		if (count($lines) === 0) {
390 508
			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 510
	protected function escapePath($path) {
410 510
		$this->verifyPath($path);
411 510
		if ($path === '/') {
412 2
			$path = '';
413 2
		}
414 510
		$path = str_replace('/', '\\', $path);
415 510
		$path = str_replace('"', '^"', $path);
416 510
		$path = ltrim($path, '\\');
417 510
		return '"' . $path . '"';
418
	}
419
420
	/**
421
	 * @param string $path
422
	 * @return string
423
	 */
424 266
	protected function escapeLocalPath($path) {
425 266
		$path = str_replace('"', '\"', $path);
426 266
		return '"' . $path . '"';
427
	}
428
429 512
	public function __destruct() {
430 512
		unset($this->connection);
431 512
	}
432
}
433