Completed
Push — master ( 0d9341...91d173 )
by Robin
01:19
created

Share::connect()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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