Completed
Push — master ( 78b032...4251cd )
by Robin
01:40
created

Share   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 516
Duplicated Lines 3.49 %

Coupling/Cohesion

Components 1
Dependencies 19

Test Coverage

Coverage 63.93%

Importance

Changes 0
Metric Value
wmc 58
lcom 1
cbo 19
dl 18
loc 516
ccs 140
cts 219
cp 0.6393
rs 4.5599
c 0
b 0
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A getAuthFileArgument() 0 7 2
B getConnection() 0 30 6
A connect() 0 7 3
A reconnect() 0 10 3
A getName() 0 3 1
A simpleCommand() 0 6 1
A dir() 0 13 1
B stat() 0 30 7
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 21 2
A append() 0 3 1
A setMode() 0 23 4
A notify() 0 9 2
A execute() 0 4 1
A parseOutput() 0 7 2
A escape() 0 3 1
A escapePath() 0 10 2
A escapeLocalPath() 0 4 1
A getAcls() 0 25 3
A getServer() 0 3 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\ACL;
12
use Icewind\SMB\Exception\AlreadyExistsException;
13
use Icewind\SMB\Exception\AuthenticationException;
14
use Icewind\SMB\Exception\ConnectException;
15
use Icewind\SMB\Exception\ConnectionException;
16
use Icewind\SMB\Exception\DependencyException;
17
use Icewind\SMB\Exception\Exception;
18
use Icewind\SMB\Exception\FileInUseException;
19
use Icewind\SMB\Exception\InvalidHostException;
20
use Icewind\SMB\Exception\InvalidTypeException;
21
use Icewind\SMB\Exception\NotFoundException;
22
use Icewind\SMB\Exception\InvalidRequestException;
23
use Icewind\SMB\IFileInfo;
24
use Icewind\SMB\INotifyHandler;
25
use Icewind\SMB\IServer;
26
use Icewind\SMB\ISystem;
27
use Icewind\Streams\CallbackWrapper;
28
use Icewind\SMB\Native\NativeShare;
29
use Icewind\SMB\Native\NativeServer;
30
31
class Share extends AbstractShare {
32
	/**
33
	 * @var IServer $server
34
	 */
35
	private $server;
36
37
	/**
38
	 * @var string $name
39
	 */
40
	private $name;
41
42
	/**
43
	 * @var Connection|null $connection
44
	 */
45
	public $connection = null;
46
47
	/**
48
	 * @var Parser
49
	 */
50
	protected $parser;
51
52
	/**
53
	 * @var ISystem
54
	 */
55
	private $system;
56
57
	const MODE_MAP = [
58
		FileInfo::MODE_READONLY => 'r',
59
		FileInfo::MODE_HIDDEN   => 'h',
60
		FileInfo::MODE_ARCHIVE  => 'a',
61
		FileInfo::MODE_SYSTEM   => 's'
62
	];
63
64
	const EXEC_CMD = 'exec';
65
66 488
	/**
67 488
	 * @param IServer $server
68 488
	 * @param string $name
69 488
	 * @param ISystem $system
70 488
	 */
71 488
	public function __construct(IServer $server, string $name, ISystem $system) {
72 488
		parent::__construct();
73
		$this->server = $server;
74 486
		$this->name = $name;
75 486
		$this->system = $system;
76 486
		$this->parser = new Parser($server->getTimeZone());
77
	}
78
79
	private function getAuthFileArgument(): string {
80
		if ($this->server->getAuth()->getUsername()) {
81
			return '--authentication-file=' . $this->system->getFD(3);
82 486
		} else {
83 486
			return '';
84 486
		}
85 486
	}
86 486
87 486
	protected function getConnection(): Connection {
88 486
		$maxProtocol = $this->server->getOptions()->getMaxProtocol();
89 486
		$minProtocol = $this->server->getOptions()->getMinProtocol();
90 486
		$smbClient = $this->system->getSmbclientPath();
91 486
		$stdBuf = $this->system->getStdBufPath();
92
		if ($smbClient === null) {
93 486
			throw new Exception("Backend not available");
94 486
		}
95 486
		$command = sprintf(
96 486
			'%s %s%s -t %s %s %s %s %s %s',
97
			self::EXEC_CMD,
98
			$stdBuf ? $stdBuf . ' -o0 ' : '',
99
			$smbClient,
100 486
			$this->server->getOptions()->getTimeout(),
101 486
			$this->getAuthFileArgument(),
102
			$this->server->getAuth()->getExtraCommandLineArguments(),
103
			$maxProtocol ? "--option='client max protocol=" . $maxProtocol . "'" : "",
104
			$minProtocol ? "--option='client min protocol=" . $minProtocol . "'" : "",
105
			escapeshellarg('//' . $this->server->getHost() . '/' . $this->name)
106
		);
107
		$connection = new Connection($command, $this->parser);
108
		$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
109 484
		$connection->connect();
110 484
		if (!$connection->isValid()) {
111 484
			throw new ConnectionException((string)$connection->readLine());
112
		}
113 484
		// some versions of smbclient add a help message in first of the first prompt
114 484
		$connection->clearTillPrompt();
115
		return $connection;
116 2
	}
117 2
118 2
	/**
119
	 * @throws ConnectionException
120
	 * @throws AuthenticationException
121 2
	 * @throws InvalidHostException
122
	 * @psalm-assert Connection $this->connection
123
	 */
124
	protected function connect(): Connection {
125
		if ($this->connection and $this->connect()->isValid()) {
126
			return $this->connection;
127
		}
128 4
		$this->connection = $this->getConnection();
129 4
		return $this->connection;
130
	}
131
132 484
	/**
133 484
	 * @throws ConnectionException
134 484
	 * @throws AuthenticationException
135 484
	 * @throws InvalidHostException
136 484
	 * @psalm-assert Connection $this->connection
137
	 */
138
	protected function reconnect(): void {
139
		if ($this->connection === null) {
140
			$this->connect();
141
		} else {
142
			$this->connection->reconnect();
143
			if (!$this->connection->isValid()) {
144
				throw new ConnectionException();
145
			}
146
		}
147
	}
148 484
149 484
	/**
150 484
	 * Get the name of the share
151
	 *
152 484
	 * @return string
153 484
	 */
154
	public function getName(): string {
155 484
		return $this->name;
156
	}
157 484
158
	protected function simpleCommand(string $command, string $path): bool {
159 484
		$escapedPath = $this->escapePath($path);
160
		$cmd = $command . ' ' . $escapedPath;
161
		$output = $this->execute($cmd);
162
		return $this->parseOutput($output, $path);
163
	}
164
165
	/**
166 50
	 * List the content of a remote folder
167
	 *
168
	 * @param string $path
169 50
	 * @return IFileInfo[]
170 48
	 *
171 48
	 * @throws NotFoundException
172 48
	 * @throws InvalidTypeException
173 46
	 */
174 48
	public function dir(string $path): array {
175 48
		$escapedPath = $this->escapePath($path);
176 28
		$output = $this->execute('cd ' . $escapedPath);
177
		//check output for errors
178
		$this->parseOutput($output, $path);
179
		$output = $this->execute('dir');
180 22
181 4
		$this->execute('cd /');
182
183
		return $this->parser->parseDir($output, $path, function (string $path) {
184
			return $this->getAcls($path);
185 4
		});
186
	}
187
188 4
	/**
189 2
	 * @param string $path
190
	 * @return IFileInfo
191 2
	 */
192 2
	public function stat(string $path): IFileInfo {
193
		// some windows server setups don't seem to like the allinfo command
194 2
		// use the dir command instead to get the file info where possible
195
		if ($path !== "" && $path !== "/") {
196
			$parent = dirname($path);
197
			$dir = $this->dir($parent);
198
			$file = array_values(array_filter($dir, function (IFileInfo $info) use ($path) {
199
				return $info->getPath() === $path;
200
			}));
201
			if ($file) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $file of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
202
				return $file[0];
203
			}
204
		}
205
206 484
		$escapedPath = $this->escapePath($path);
207 484
		$output = $this->execute('allinfo ' . $escapedPath);
208
		// Windows and non Windows Fileserver may respond different
209
		// to the allinfo command for directories. If the result is a single
210
		// line = error line, redo it with a different allinfo parameter
211
		if ($escapedPath == '""' && count($output) < 2) {
212
			$output = $this->execute('allinfo ' . '"."');
213
		}
214
		if (count($output) < 3) {
215
			$this->parseOutput($output, $path);
216
		}
217
		$stat = $this->parser->parseStat($output);
218
		return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], $stat['mode'], function () use ($path) {
219 482
			return $this->getAcls($path);
220 482
		});
221
	}
222
223
	/**
224
	 * Create a folder on the share
225
	 *
226
	 * @param string $path
227
	 * @return bool
228
	 *
229
	 * @throws NotFoundException
230
	 * @throws AlreadyExistsException
231
	 */
232
	public function mkdir(string $path): bool {
233 236
		return $this->simpleCommand('mkdir', $path);
234
	}
235
236
	/**
237 236
	 * Remove a folder on the share
238 22
	 *
239
	 * @param string $path
240
	 * @return bool
241 2
	 *
242 2
	 * @throws NotFoundException
243 2
	 * @throws InvalidTypeException
244
	 */
245
	public function rmdir(string $path): bool {
246
		return $this->simpleCommand('rmdir', $path);
247
	}
248 20
249 2
	/**
250
	 * Delete a file on the share
251
	 *
252 2
	 * @param string $path
253 2
	 * @param bool $secondTry
254
	 * @return bool
255
	 * @throws InvalidTypeException
256
	 * @throws NotFoundException
257
	 * @throws \Exception
258
	 */
259
	public function del(string $path, bool $secondTry = false): bool {
260
		//del return a file not found error when trying to delete a folder
261
		//we catch it so we can check if $path doesn't exist or is of invalid type
262
		try {
263
			return $this->simpleCommand('del', $path);
264
		} catch (NotFoundException $e) {
265
			//no need to do anything with the result, we just check if this throws the not found error
266
			try {
267 56
				$this->simpleCommand('ls', $path);
268 56
			} catch (NotFoundException $e2) {
269 38
				throw $e;
270 38
			} catch (\Exception $e2) {
271 38
				throw new InvalidTypeException($path);
272
			}
273
			throw $e;
274
		} catch (FileInUseException $e) {
275
			if ($secondTry) {
276
				throw $e;
277
			}
278
			$this->reconnect();
279
			return $this->del($path, true);
280
		}
281
	}
282
283
	/**
284 206
	 * Rename a remote file
285 206
	 *
286 206
	 * @param string $from
287 188
	 * @param string $to
288 188
	 * @return bool
289
	 *
290
	 * @throws NotFoundException
291
	 * @throws AlreadyExistsException
292
	 */
293 View Code Duplication
	public function rename(string $from, string $to): bool {
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...
294
		$path1 = $this->escapePath($from);
295
		$path2 = $this->escapePath($to);
296
		$output = $this->execute('rename ' . $path1 . ' ' . $path2);
297
		return $this->parseOutput($output, $to);
298
	}
299
300
	/**
301 88
	 * Upload a local file
302 88
	 *
303 70
	 * @param string $source local file
304 70
	 * @param string $target remove file
305 70
	 * @return bool
306
	 *
307
	 * @throws NotFoundException
308
	 * @throws InvalidTypeException
309
	 */
310 View Code Duplication
	public function put(string $source, string $target): bool {
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...
311
		$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
312
		$path2 = $this->escapePath($target);
313
		$output = $this->execute('put ' . $path1 . ' ' . $path2);
314
		return $this->parseOutput($output, $target);
315
	}
316
317 50
	/**
318 50
	 * Download a remote file
319
	 *
320
	 * @param string $source remove file
321 32
	 * @param string $target local file
322
	 * @return bool
323 32
	 *
324 32
	 * @throws NotFoundException
325 32
	 * @throws InvalidTypeException
326 32
	 */
327 32 View Code Duplication
	public function get(string $source, string $target): bool {
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...
328
		$path1 = $this->escapePath($source);
329
		$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping
330
		$output = $this->execute('get ' . $path1 . ' ' . $path2);
331
		return $this->parseOutput($output, $source);
332
	}
333
334
	/**
335
	 * Open a readable stream to a remote file
336
	 *
337
	 * @param string $source
338
	 * @return resource a read only stream with the contents of the remote file
339 50
	 *
340 50
	 * @throws NotFoundException
341
	 * @throws InvalidTypeException
342
	 */
343 32
	public function read(string $source) {
344
		$source = $this->escapePath($source);
345 32
		// since returned stream is closed by the caller we need to create a new instance
346 32
		// since we can't re-use the same file descriptor over multiple calls
347 32
		$connection = $this->getConnection();
348
349
		$connection->write('get ' . $source . ' ' . $this->system->getFD(5));
350
		$connection->write('exit');
351 32
		$fh = $connection->getFileOutputStream();
352 32
		stream_context_set_option($fh, 'file', 'connection', $connection);
353 32
		return $fh;
354
	}
355
356
	/**
357
	 * Open a writable stream to a remote file
358
	 *
359
	 * @param string $target
360
	 * @return resource a write only stream to upload a remote file
361
	 *
362
	 * @throws NotFoundException
363
	 * @throws InvalidTypeException
364 2
	 */
365 2
	public function write(string $target) {
366
		$target = $this->escapePath($target);
367
		// since returned stream is closed by the caller we need to create a new instance
368
		// since we can't re-use the same file descriptor over multiple calls
369
		$connection = $this->getConnection();
370
371
		$fh = $connection->getFileInputStream();
372
		$connection->write('put ' . $this->system->getFD(4) . ' ' . $target);
373
		$connection->write('exit');
374
375
		// use a close callback to ensure the upload is finished before continuing
376
		// this also serves as a way to keep the connection in scope
377
		$stream = CallbackWrapper::wrap($fh, null, null, function () use ($connection) {
378
			$connection->close(false); // dont terminate, give the upload some time
379
		});
380
		if (is_resource($stream)) {
381
			return $stream;
382
		} else {
383
			throw new InvalidRequestException($target);
384
		}
385
	}
386
387
	/**
388
	 * Append to stream
389
	 * Note: smbclient does not support this (Use php-libsmbclient)
390
	 *
391
	 * @param string $target
392
	 *
393
	 * @throws DependencyException
394
	 */
395
	public function append(string $target) {
396
		throw new DependencyException('php-libsmbclient is required for append');
397
	}
398
399
	/**
400
	 * @param string $path
401
	 * @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
402
	 * @return mixed
403 2
	 */
404 2
	public function setMode(string $path, int $mode) {
405
		$modeString = '';
406
		foreach (self::MODE_MAP as $modeByte => $string) {
407 2
			if ($mode & $modeByte) {
408 2
				$modeString .= $string;
409 2
			}
410 2
		}
411
		$path = $this->escapePath($path);
412
413
		// first reset the mode to normal
414
		$cmd = 'setmode ' . $path . ' -rsha';
415
		$output = $this->execute($cmd);
416
		$this->parseOutput($output, $path);
417 484
418 484
		if ($mode !== FileInfo::MODE_NORMAL) {
419 484
			// then set the modes we want
420 484
			$cmd = 'setmode ' . $path . ' ' . $modeString;
421
			$output = $this->execute($cmd);
422
			return $this->parseOutput($output, $path);
423
		} else {
424
			return true;
425
		}
426
	}
427
428
	/**
429
	 * @param string $path
430
	 * @return INotifyHandler
431
	 * @throws ConnectionException
432
	 * @throws DependencyException
433
	 */
434
	public function notify(string $path): INotifyHandler {
435
		if (!$this->system->getStdBufPath()) { //stdbuf is required to disable smbclient's output buffering
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->system->getStdBufPath() of type string|null is loosely compared to false; 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...
436
			throw new DependencyException('stdbuf is required for usage of the notify command');
437 484
		}
438 484
		$connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process
439 484
		$command = 'notify ' . $this->escapePath($path);
440
		$connection->write($command . PHP_EOL);
441 38
		return new NotifyHandler($connection, $path);
442
	}
443
444
	/**
445
	 * @param string $command
446
	 * @return string[]
447
	 */
448
	protected function execute(string $command): array {
449
		$this->connect()->write($command . PHP_EOL);
450
		return $this->connect()->read();
451
	}
452
453
	/**
454
	 * check output for errors
455
	 *
456
	 * @param string[] $lines
457
	 * @param string $path
458 486
	 *
459 486
	 * @return bool
460 486
	 * @throws AlreadyExistsException
461 2
	 * @throws \Icewind\SMB\Exception\AccessDeniedException
462
	 * @throws \Icewind\SMB\Exception\NotEmptyException
463 486
	 * @throws InvalidTypeException
464 486
	 * @throws \Icewind\SMB\Exception\Exception
465 486
	 * @throws NotFoundException
466 486
	 */
467
	protected function parseOutput(array $lines, string $path = ''): bool {
468
		if (count($lines) === 0) {
469
			return true;
470
		} else {
471
			$this->parser->checkForError($lines, $path);
472
		}
473 242
	}
474 242
475 242
	/**
476
	 * @param string $string
477
	 * @return string
478
	 */
479
	protected function escape(string $string): string {
480
		return escapeshellarg($string);
481
	}
482
483
	/**
484
	 * @param string $path
485
	 * @return string
486
	 */
487
	protected function escapePath(string $path): string {
488
		$this->verifyPath($path);
489
		if ($path === '/') {
490
			$path = '';
491
		}
492
		$path = str_replace('/', '\\', $path);
493
		$path = str_replace('"', '^"', $path);
494
		$path = ltrim($path, '\\');
495
		return '"' . $path . '"';
496
	}
497
498
	/**
499
	 * @param string $path
500
	 * @return string
501
	 */
502
	protected function escapeLocalPath(string $path): string {
503
		$path = str_replace('"', '\"', $path);
504
		return '"' . $path . '"';
505
	}
506
507
	/**
508
	 * @param string $path
509
	 * @return ACL[]
510
	 * @throws ConnectionException
511
	 * @throws ConnectException
512
	 */
513
	protected function getAcls(string $path): array {
514
		$commandPath = $this->system->getSmbcAclsPath();
515
		if (!$commandPath) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $commandPath of type string|null is loosely compared to false; 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...
516
			return [];
517
		}
518
519
		$command = sprintf(
520
			'%s %s %s %s/%s %s',
521
			$commandPath,
522
			$this->getAuthFileArgument(),
523
			$this->server->getAuth()->getExtraCommandLineArguments(),
524
			escapeshellarg('//' . $this->server->getHost()),
525
			escapeshellarg($this->name),
526
			escapeshellarg($path)
527
		);
528
		$connection = new RawConnection($command);
529
		$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
530
		$connection->connect();
531
		if (!$connection->isValid()) {
532
			throw new ConnectionException((string)$connection->readLine());
533
		}
534
535
		$rawAcls = $connection->readAll();
536
		return $this->parser->parseACLs($rawAcls);
537
	}
538
539
	public function getServer(): IServer {
540
		return $this->server;
541
	}
542
543
	public function __destruct() {
544
		unset($this->connection);
545
	}
546
}
547