Completed
Push — stable2 ( 929455...10fadc )
by Robin
13:53 queued 12:28
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 0%

Importance

Changes 0
Metric Value
wmc 46
lcom 1
cbo 14
dl 18
loc 418
ccs 0
cts 165
cp 0
rs 8.72
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A escape() 0 3 1
A __construct() 0 7 2
A getConnection() 0 23 5
A connect() 0 6 3
A reconnect() 0 6 2
A getName() 0 3 1
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
A setMode() 0 29 4
A notify() 0 9 2
A execute() 0 5 1
A parseOutput() 0 8 2
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
	public function __construct($server, $name, System $system = null) {
49
		parent::__construct();
50
		$this->server = $server;
51
		$this->name = $name;
52
		$this->system = (!is_null($system)) ? $system : new System();
53
		$this->parser = new Parser(new TimeZoneProvider($this->server->getHost(), $this->system));
54
	}
55
56
	protected function getConnection() {
57
		$workgroupArgument = ($this->server->getWorkgroup()) ? ' -W ' . escapeshellarg($this->server->getWorkgroup()) : '';
58
		$smbClientPath = $this->system->getSmbclientPath();
59
		if (!$smbClientPath) {
60
			throw new DependencyException('Can\'t find smbclient binary in path');
61
		}
62
		$command = sprintf('%s%s %s --authentication-file=%s %s',
63
			$this->system->hasStdBuf() ? 'stdbuf -o0 ' : '',
64
			$this->system->getSmbclientPath(),
65
			$workgroupArgument,
66
			System::getFD(3),
67
			escapeshellarg('//' . $this->server->getHost() . '/' . $this->name)
68
		);
69
		$connection = new Connection($command, $this->parser);
70
		$connection->writeAuthentication($this->server->getUser(), $this->server->getPassword());
71
		$connection->connect();
72
		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
		$connection->clearTillPrompt();
77
		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
	protected function connect() {
86
		if ($this->connection and $this->connection->isValid()) {
87
			return;
88
		}
89
		$this->connection = $this->getConnection();
90
	}
91
92
	protected function reconnect() {
93
		$this->connection->reconnect();
94
		if (!$this->connection->isValid()) {
95
			throw new ConnectionException();
96
		}
97
	}
98
99
	/**
100
	 * Get the name of the share
101
	 *
102
	 * @return string
103
	 */
104
	public function getName() {
105
		return $this->name;
106
	}
107
108
	protected function simpleCommand($command, $path) {
109
		$escapedPath = $this->escapePath($path);
110
		$cmd = $command . ' ' . $escapedPath;
111
		$output = $this->execute($cmd);
112
		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
	public function dir($path) {
125
		$escapedPath = $this->escapePath($path);
126
		$output = $this->execute('cd ' . $escapedPath);
127
		//check output for errors
128
		$this->parseOutput($output, $path);
129
		$output = $this->execute('dir');
130
131
		$this->execute('cd /');
132
133
		return $this->parser->parseDir($output, $path);
134
	}
135
136
	/**
137
	 * @param string $path
138
	 * @return \Icewind\SMB\IFileInfo
139
	 */
140
	public function stat($path) {
141
		$escapedPath = $this->escapePath($path);
142
		$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
		if ($escapedPath == '""' && count($output) < 2) {
147
			$output = $this->execute('allinfo ' . '"."');
148
		}
149
		if (count($output) < 3) {
150
			$this->parseOutput($output, $path);
151
		}
152
		$stat = $this->parser->parseStat($output);
153
		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
	public function mkdir($path) {
166
		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
	public function rmdir($path) {
179
		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
	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
			return $this->simpleCommand('del', $path);
197
		} 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
				$this->simpleCommand('ls', $path);
201
			} catch (NotFoundException $e2) {
202
				throw $e;
203
			} catch (\Exception $e2) {
204
				throw new InvalidTypeException($path);
205
			}
206
			throw $e;
207
		} catch (FileInUseException $e) {
208
			if ($secondTry) {
209
				throw $e;
210
			}
211
			$this->reconnect();
212
			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 View Code Duplication
	public function rename($from, $to) {
227
		$path1 = $this->escapePath($from);
228
		$path2 = $this->escapePath($to);
229
		$output = $this->execute('rename ' . $path1 . ' ' . $path2);
230
		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 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
		$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
245
		$path2 = $this->escapePath($target);
246
		$output = $this->execute('put ' . $path1 . ' ' . $path2);
247
		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 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
		$path1 = $this->escapePath($source);
262
		$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping
263
		$output = $this->execute('get ' . $path1 . ' ' . $path2);
264
		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
	public function read($source) {
277
		$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
		$connection = $this->getConnection();
281
282
		$connection->write('get ' . $source . ' ' . System::getFD(5));
283
		$connection->write('exit');
284
		$fh = $connection->getFileOutputStream();
285
		stream_context_set_option($fh, 'file', 'connection', $connection);
286
		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
	public function write($target) {
299
		$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
		$connection = $this->getConnection();
303
304
		$fh = $connection->getFileInputStream();
305
		$connection->write('put ' . System::getFD(4) . ' ' . $target);
306
		$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
		return CallbackWrapper::wrap($fh, null, null, function () use ($connection, $target) {
311
			$connection->close(false); // dont terminate, give the upload some time
312
		});
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
	public function setMode($path, $mode) {
321
		$modeString = '';
322
		$modeMap = array(
323
			FileInfo::MODE_READONLY => 'r',
324
			FileInfo::MODE_HIDDEN   => 'h',
325
			FileInfo::MODE_ARCHIVE  => 'a',
326
			FileInfo::MODE_SYSTEM   => 's'
327
		);
328
		foreach ($modeMap as $modeByte => $string) {
329
			if ($mode & $modeByte) {
330
				$modeString .= $string;
331
			}
332
		}
333
		$path = $this->escapePath($path);
334
335
		// first reset the mode to normal
336
		$cmd = 'setmode ' . $path . ' -rsha';
337
		$output = $this->execute($cmd);
338
		$this->parseOutput($output, $path);
339
340
		if ($mode !== FileInfo::MODE_NORMAL) {
341
			// then set the modes we want
342
			$cmd = 'setmode ' . $path . ' ' . $modeString;
343
			$output = $this->execute($cmd);
344
			return $this->parseOutput($output, $path);
345
		} else {
346
			return true;
347
		}
348
	}
349
350
	/**
351
	 * @param string $path
352
	 * @return INotifyHandler
353
	 * @throws ConnectionException
354
	 * @throws DependencyException
355
	 */
356
	public function notify($path) {
357
		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
		$connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process
361
		$command = 'notify ' . $this->escapePath($path);
362
		$connection->write($command . PHP_EOL);
363
		return new NotifyHandler($connection, $path);
364
	}
365
366
	/**
367
	 * @param string $command
368
	 * @return array
369
	 */
370
	protected function execute($command) {
371
		$this->connect();
372
		$this->connection->write($command . PHP_EOL);
373
		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
	protected function parseOutput($lines, $path = '') {
391
		if (count($lines) === 0) {
392
			return true;
393
		} else {
394
			$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
	protected function escapePath($path) {
412
		$this->verifyPath($path);
413
		if ($path === '/') {
414
			$path = '';
415
		}
416
		$path = str_replace('/', '\\', $path);
417
		$path = str_replace('"', '^"', $path);
418
		$path = ltrim($path, '\\');
419
		return '"' . $path . '"';
420
	}
421
422
	/**
423
	 * @param string $path
424
	 * @return string
425
	 */
426
	protected function escapeLocalPath($path) {
427
		$path = str_replace('"', '\"', $path);
428
		return '"' . $path . '"';
429
	}
430
431
	public function __destruct() {
432
		unset($this->connection);
433
	}
434
}
435