Passed
Push — master ( 8481d6...507b18 )
by Robin
03:14
created

Share::read()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 1
dl 0
loc 11
ccs 8
cts 8
cp 1
crap 1
rs 10
c 0
b 0
f 0
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()) {
76 486
			return '--authentication-file=' . $this->system->getFD(3);
77
		} else {
78
			return '';
79
		}
80
	}
81
82 486
	protected function getConnection() {
83 486
		$command = sprintf(
84 486
			'%s %s%s -t %s %s %s %s',
85 486
			self::EXEC_CMD,
86 486
			$this->system->getStdBufPath() ? $this->system->getStdBufPath() . ' -o0 ' : '',
0 ignored issues
show
Bug introduced by
Are you sure $this->system->getStdBufPath() of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

86
			$this->system->getStdBufPath() ? /** @scrutinizer ignore-type */ $this->system->getStdBufPath() . ' -o0 ' : '',
Loading history...
87 486
			$this->system->getSmbclientPath(),
88 486
			$this->server->getOptions()->getTimeout(),
89 486
			$this->getAuthFileArgument(),
90 486
			$this->server->getAuth()->getExtraCommandLineArguments(),
91 486
			escapeshellarg('//' . $this->server->getHost() . '/' . $this->name)
92
		);
93 486
		$connection = new Connection($command, $this->parser);
94 486
		$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
95 486
		$connection->connect();
96 486
		if (!$connection->isValid()) {
97
			throw new ConnectionException($connection->readLine());
98
		}
99
		// some versions of smbclient add a help message in first of the first prompt
100 486
		$connection->clearTillPrompt();
101 486
		return $connection;
102
	}
103
104
	/**
105
	 * @throws \Icewind\SMB\Exception\ConnectionException
106
	 * @throws \Icewind\SMB\Exception\AuthenticationException
107
	 * @throws \Icewind\SMB\Exception\InvalidHostException
108
	 */
109 484
	protected function connect() {
110 484
		if ($this->connection and $this->connection->isValid()) {
111 484
			return;
112
		}
113 484
		$this->connection = $this->getConnection();
114 484
	}
115
116 2
	protected function reconnect() {
117 2
		$this->connection->reconnect();
118 2
		if (!$this->connection->isValid()) {
119
			throw new ConnectionException();
120
		}
121 2
	}
122
123
	/**
124
	 * Get the name of the share
125
	 *
126
	 * @return string
127
	 */
128 4
	public function getName() {
129 4
		return $this->name;
130
	}
131
132 484
	protected function simpleCommand($command, $path) {
133 484
		$escapedPath = $this->escapePath($path);
134 484
		$cmd = $command . ' ' . $escapedPath;
135 484
		$output = $this->execute($cmd);
136 484
		return $this->parseOutput($output, $path);
137
	}
138
139
	/**
140
	 * List the content of a remote folder
141
	 *
142
	 * @param $path
143
	 * @return \Icewind\SMB\IFileInfo[]
144
	 *
145
	 * @throws \Icewind\SMB\Exception\NotFoundException
146
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
147
	 */
148 484
	public function dir($path) {
149 484
		$escapedPath = $this->escapePath($path);
150 484
		$output = $this->execute('cd ' . $escapedPath);
151
		//check output for errors
152 484
		$this->parseOutput($output, $path);
153 484
		$output = $this->execute('dir');
154
155 484
		$this->execute('cd /');
156
157
		return $this->parser->parseDir($output, $path, function ($path) {
158
			return $this->getAcls($path);
159 484
		});
160
	}
161
162
	/**
163
	 * @param string $path
164
	 * @return \Icewind\SMB\IFileInfo
165
	 */
166 50
	public function stat($path) {
167
		// some windows server setups don't seem to like the allinfo command
168
		// use the dir command instead to get the file info where possible
169 50
		if ($path !== "" && $path !== "/") {
170 48
			$parent = dirname($path);
171 48
			$dir = $this->dir($parent);
172
			$file = array_values(array_filter($dir, function (IFileInfo $info) use ($path) {
173 46
				return $info->getPath() === $path;
174 48
			}));
175 48
			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...
176 28
				return $file[0];
177
			}
178
		}
179
180 22
		$escapedPath = $this->escapePath($path);
181 4
		$output = $this->execute('allinfo ' . $escapedPath);
182
		// Windows and non Windows Fileserver may respond different
183
		// to the allinfo command for directories. If the result is a single
184
		// line = error line, redo it with a different allinfo parameter
185 4
		if ($escapedPath == '""' && count($output) < 2) {
186
			$output = $this->execute('allinfo ' . '"."');
187
		}
188 4
		if (count($output) < 3) {
189 2
			$this->parseOutput($output, $path);
190
		}
191 2
		$stat = $this->parser->parseStat($output);
192
		return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], $stat['mode'], function () use ($path) {
0 ignored issues
show
Bug introduced by
It seems like $stat['mode'] can also be of type double; however, parameter $mode of Icewind\SMB\Wrapped\FileInfo::__construct() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

192
		return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], /** @scrutinizer ignore-type */ $stat['mode'], function () use ($path) {
Loading history...
193
			return $this->getAcls($path);
194 2
		});
195
	}
196
197
	/**
198
	 * Create a folder on the share
199
	 *
200
	 * @param string $path
201
	 * @return bool
202
	 *
203
	 * @throws \Icewind\SMB\Exception\NotFoundException
204
	 * @throws \Icewind\SMB\Exception\AlreadyExistsException
205
	 */
206 484
	public function mkdir($path) {
207 484
		return $this->simpleCommand('mkdir', $path);
208
	}
209
210
	/**
211
	 * Remove a folder on the share
212
	 *
213
	 * @param string $path
214
	 * @return bool
215
	 *
216
	 * @throws \Icewind\SMB\Exception\NotFoundException
217
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
218
	 */
219 482
	public function rmdir($path) {
220 482
		return $this->simpleCommand('rmdir', $path);
221
	}
222
223
	/**
224
	 * Delete a file on the share
225
	 *
226
	 * @param string $path
227
	 * @param bool $secondTry
228
	 * @return bool
229
	 * @throws InvalidTypeException
230
	 * @throws NotFoundException
231
	 * @throws \Exception
232
	 */
233 236
	public function del($path, $secondTry = false) {
234
		//del return a file not found error when trying to delete a folder
235
		//we catch it so we can check if $path doesn't exist or is of invalid type
236
		try {
237 236
			return $this->simpleCommand('del', $path);
238 22
		} catch (NotFoundException $e) {
239
			//no need to do anything with the result, we just check if this throws the not found error
240
			try {
241 2
				$this->simpleCommand('ls', $path);
242 2
			} catch (NotFoundException $e2) {
243 2
				throw $e;
244
			} catch (\Exception $e2) {
245
				throw new InvalidTypeException($path);
246
			}
247
			throw $e;
248 20
		} catch (FileInUseException $e) {
249 2
			if ($secondTry) {
250
				throw $e;
251
			}
252 2
			$this->reconnect();
253 2
			return $this->del($path, true);
254
		}
255
	}
256
257
	/**
258
	 * Rename a remote file
259
	 *
260
	 * @param string $from
261
	 * @param string $to
262
	 * @return bool
263
	 *
264
	 * @throws \Icewind\SMB\Exception\NotFoundException
265
	 * @throws \Icewind\SMB\Exception\AlreadyExistsException
266
	 */
267 56
	public function rename($from, $to) {
268 56
		$path1 = $this->escapePath($from);
269 38
		$path2 = $this->escapePath($to);
270 38
		$output = $this->execute('rename ' . $path1 . ' ' . $path2);
271 38
		return $this->parseOutput($output, $to);
272
	}
273
274
	/**
275
	 * Upload a local file
276
	 *
277
	 * @param string $source local file
278
	 * @param string $target remove file
279
	 * @return bool
280
	 *
281
	 * @throws \Icewind\SMB\Exception\NotFoundException
282
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
283
	 */
284 206
	public function put($source, $target) {
285 206
		$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
286 206
		$path2 = $this->escapePath($target);
287 188
		$output = $this->execute('put ' . $path1 . ' ' . $path2);
288 188
		return $this->parseOutput($output, $target);
289
	}
290
291
	/**
292
	 * Download a remote file
293
	 *
294
	 * @param string $source remove file
295
	 * @param string $target local file
296
	 * @return bool
297
	 *
298
	 * @throws \Icewind\SMB\Exception\NotFoundException
299
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
300
	 */
301 88
	public function get($source, $target) {
302 88
		$path1 = $this->escapePath($source);
303 70
		$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping
304 70
		$output = $this->execute('get ' . $path1 . ' ' . $path2);
305 70
		return $this->parseOutput($output, $source);
306
	}
307
308
	/**
309
	 * Open a readable stream to a remote file
310
	 *
311
	 * @param string $source
312
	 * @return resource a read only stream with the contents of the remote file
313
	 *
314
	 * @throws \Icewind\SMB\Exception\NotFoundException
315
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
316
	 */
317 50
	public function read($source) {
318 50
		$source = $this->escapePath($source);
319
		// since returned stream is closed by the caller we need to create a new instance
320
		// since we can't re-use the same file descriptor over multiple calls
321 32
		$connection = $this->getConnection();
322
323 32
		$connection->write('get ' . $source . ' ' . $this->system->getFD(5));
324 32
		$connection->write('exit');
325 32
		$fh = $connection->getFileOutputStream();
326 32
		stream_context_set_option($fh, 'file', 'connection', $connection);
327 32
		return $fh;
328
	}
329
330
	/**
331
	 * Open a writable stream to a remote file
332
	 *
333
	 * @param string $target
334
	 * @return resource a write only stream to upload a remote file
335
	 *
336
	 * @throws \Icewind\SMB\Exception\NotFoundException
337
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
338
	 */
339 50
	public function write($target) {
340 50
		$target = $this->escapePath($target);
341
		// since returned stream is closed by the caller we need to create a new instance
342
		// since we can't re-use the same file descriptor over multiple calls
343 32
		$connection = $this->getConnection();
344
345 32
		$fh = $connection->getFileInputStream();
346 32
		$connection->write('put ' . $this->system->getFD(4) . ' ' . $target);
347 32
		$connection->write('exit');
348
349
		// use a close callback to ensure the upload is finished before continuing
350
		// this also serves as a way to keep the connection in scope
351
		return CallbackWrapper::wrap($fh, null, null, function () use ($connection, $target) {
0 ignored issues
show
Unused Code introduced by
The import $target is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
Bug Best Practice introduced by
The expression return Icewind\Streams\C...ion(...) { /* ... */ }) could also return false which is incompatible with the documented return type resource. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
352 32
			$connection->close(false); // dont terminate, give the upload some time
353 32
		});
354
	}
355
356
	/**
357
	 * Append to stream
358
	 * Note: smbclient does not support this (Use php-libsmbclient)
359
	 *
360
	 * @param string $target
361
	 *
362
	 * @throws \Icewind\SMB\Exception\DependencyException
363
	 */
364 2
	public function append($target) {
365 2
		throw new DependencyException('php-libsmbclient is required for append');
366
	}
367
368
	/**
369
	 * @param string $path
370
	 * @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
371
	 * @return mixed
372
	 */
373
	public function setMode($path, $mode) {
374
		$modeString = '';
375
		foreach (self::MODE_MAP as $modeByte => $string) {
376
			if ($mode & $modeByte) {
377
				$modeString .= $string;
378
			}
379
		}
380
		$path = $this->escapePath($path);
381
382
		// first reset the mode to normal
383
		$cmd = 'setmode ' . $path . ' -rsha';
384
		$output = $this->execute($cmd);
385
		$this->parseOutput($output, $path);
386
387
		if ($mode !== FileInfo::MODE_NORMAL) {
388
			// then set the modes we want
389
			$cmd = 'setmode ' . $path . ' ' . $modeString;
390
			$output = $this->execute($cmd);
391
			return $this->parseOutput($output, $path);
392
		} else {
393
			return true;
394
		}
395
	}
396
397
	/**
398
	 * @param string $path
399
	 * @return INotifyHandler
400
	 * @throws ConnectionException
401
	 * @throws DependencyException
402
	 */
403 2
	public function notify($path) {
404 2
		if (!$this->system->getStdBufPath()) { //stdbuf is required to disable smbclient's output buffering
405
			throw new DependencyException('stdbuf is required for usage of the notify command');
406
		}
407 2
		$connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process
408 2
		$command = 'notify ' . $this->escapePath($path);
409 2
		$connection->write($command . PHP_EOL);
410 2
		return new NotifyHandler($connection, $path);
411
	}
412
413
	/**
414
	 * @param string $command
415
	 * @return array
416
	 */
417 484
	protected function execute($command) {
418 484
		$this->connect();
419 484
		$this->connection->write($command . PHP_EOL);
420 484
		return $this->connection->read();
421
	}
422
423
	/**
424
	 * check output for errors
425
	 *
426
	 * @param string[] $lines
427
	 * @param string $path
428
	 *
429
	 * @return bool
430
	 * @throws \Icewind\SMB\Exception\AlreadyExistsException
431
	 * @throws \Icewind\SMB\Exception\AccessDeniedException
432
	 * @throws \Icewind\SMB\Exception\NotEmptyException
433
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
434
	 * @throws \Icewind\SMB\Exception\Exception
435
	 * @throws NotFoundException
436
	 */
437 484
	protected function parseOutput($lines, $path = '') {
438 484
		if (count($lines) === 0) {
439 484
			return true;
440
		} else {
441 38
			$this->parser->checkForError($lines, $path);
442
			return false;
443
		}
444
	}
445
446
	/**
447
	 * @param string $string
448
	 * @return string
449
	 */
450
	protected function escape($string) {
451
		return escapeshellarg($string);
452
	}
453
454
	/**
455
	 * @param string $path
456
	 * @return string
457
	 */
458 486
	protected function escapePath($path) {
459 486
		$this->verifyPath($path);
460 486
		if ($path === '/') {
461 2
			$path = '';
462
		}
463 486
		$path = str_replace('/', '\\', $path);
464 486
		$path = str_replace('"', '^"', $path);
465 486
		$path = ltrim($path, '\\');
466 486
		return '"' . $path . '"';
467
	}
468
469
	/**
470
	 * @param string $path
471
	 * @return string
472
	 */
473 242
	protected function escapeLocalPath($path) {
474 242
		$path = str_replace('"', '\"', $path);
475 242
		return '"' . $path . '"';
476
	}
477
478
	protected function getAcls($path) {
479
		$commandPath = $this->system->getSmbcAclsPath();
480
		if (!$commandPath) {
481
			return [];
482
		}
483
484
		$command = sprintf(
485
			'%s %s %s %s/%s %s',
486
			$commandPath,
0 ignored issues
show
Bug introduced by
It seems like $commandPath can also be of type true; however, parameter $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

486
			/** @scrutinizer ignore-type */ $commandPath,
Loading history...
487
			$this->getAuthFileArgument(),
488
			$this->server->getAuth()->getExtraCommandLineArguments(),
489
			escapeshellarg('//' . $this->server->getHost()),
490
			escapeshellarg($this->name),
491
			escapeshellarg($path)
492
		);
493
		$connection = new RawConnection($command);
494
		$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
495
		$connection->connect();
496
		if (!$connection->isValid()) {
497
			throw new ConnectionException($connection->readLine());
498
		}
499
500
		$rawAcls = $connection->readAll();
501
502
		$acls = [];
503
		foreach ($rawAcls as $acl) {
504
			[$type, $acl] = explode(':', $acl, 2);
505
			if ($type !== 'ACL') {
506
				continue;
507
			}
508
			[$user, $permissions] = explode(':', $acl, 2);
509
			[$type, $flags, $mask] = explode('/', $permissions);
510
511
			$type = $type === 'ALLOWED' ? ACL::TYPE_ALLOW : ACL::TYPE_DENY;
512
513
			$flagsInt = 0;
514
			foreach (explode('|', $flags) as $flagString) {
515
				if ($flagString === 'OI') {
516
					$flagsInt += ACL::FLAG_OBJECT_INHERIT;
517
				} elseif ($flagString === 'CI') {
518
					$flagsInt += ACL::FLAG_CONTAINER_INHERIT;
519
				}
520
			}
521
522
			if (substr($mask, 0, 2) === '0x') {
523
				$maskInt = hexdec($mask);
524
			} else {
525
				$maskInt = 0;
526
				foreach (explode('|', $mask) as $maskString) {
527
					if ($maskString === 'R') {
528
						$maskInt += ACL::MASK_READ;
529
					} elseif ($maskString === 'W') {
530
						$maskInt += ACL::MASK_WRITE;
531
					} elseif ($maskString === 'X') {
532
						$maskInt += ACL::MASK_EXECUTE;
533
					} elseif ($maskString === 'D') {
534
						$maskInt += ACL::MASK_DELETE;
535
					} elseif ($maskString === 'READ') {
536
						$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE;
537
					} elseif ($maskString === 'CHANGE') {
538
						$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE;
539
					} elseif ($maskString === 'FULL') {
540
						$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE;
541
					}
542
				}
543
			}
544
545
			if (isset($acls[$user])) {
546
				$existing = $acls[$user];
547
				$maskInt += $existing->getMask();
548
			}
549
			$acls[$user] = new ACL($type, $flagsInt, $maskInt);
550
		}
551
552
		return $acls;
553
	}
554
555
	public function getServer(): IServer {
556
		return $this->server;
557
	}
558
559 488
	public function __destruct() {
560 488
		unset($this->connection);
561 488
	}
562
}
563