Completed
Push — master ( afc829...7797d4 )
by Robin
03:44
created

Share   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 421
Duplicated Lines 4.28 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 92.73%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 46
c 1
b 0
f 0
lcom 1
cbo 15
dl 18
loc 421
ccs 153
cts 165
cp 0.9273
rs 7.4918

25 Methods

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