Passed
Push — master ( 80a4ed...26ec76 )
by Robin
73:34 queued 71:57
created

Share::append()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
ccs 2
cts 2
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 484
		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 48
			$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 2
		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 32
		return CallbackWrapper::wrap($fh, null, null, function () use ($connection) {
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 $values of sprintf() does only seem to accept double|integer|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