Completed
Push — master ( bebed6...b6e66d )
by Robin
02:58
created

Share::getConnection()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 3.0017

Importance

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