Passed
Push — master ( 999d66...ed09f6 )
by Robin
03:04
created

Share::mkdir()   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 482
	public function __construct(IServer $server, $name, ISystem $system) {
67 482
		parent::__construct();
68 482
		$this->server = $server;
69 482
		$this->name = $name;
70 482
		$this->system = $system;
71 482
		$this->parser = new Parser($server->getTimeZone());
72 482
	}
73
74 482
	private function getAuthFileArgument() {
75 482
		if ($this->server->getAuth()->getUsername()) {
76 482
			return '--authentication-file=' . $this->system->getFD(3);
77
		} else {
78
			return '';
79
		}
80
	}
81
82 482
	protected function getConnection() {
83 482
		$command = sprintf(
84 482
			'%s %s%s -t %s %s %s %s',
85 482
			self::EXEC_CMD,
86 482
			$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 482
			$this->system->getSmbclientPath(),
88 482
			$this->server->getOptions()->getTimeout(),
89 482
			$this->getAuthFileArgument(),
90 482
			$this->server->getAuth()->getExtraCommandLineArguments(),
91 482
			escapeshellarg('//' . $this->server->getHost() . '/' . $this->name)
92
		);
93 482
		$connection = new Connection($command, $this->parser);
94 482
		$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
95 482
		$connection->connect();
96 482
		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 482
		$connection->clearTillPrompt();
101 482
		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 480
	protected function connect() {
110 480
		if ($this->connection and $this->connection->isValid()) {
111 480
			return;
112
		}
113 480
		$this->connection = $this->getConnection();
114 480
	}
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
	public function getName() {
129
		return $this->name;
130
	}
131
132 480
	protected function simpleCommand($command, $path) {
133 480
		$escapedPath = $this->escapePath($path);
134 480
		$cmd = $command . ' ' . $escapedPath;
135 480
		$output = $this->execute($cmd);
136 480
		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 480
	public function dir($path) {
149 480
		$escapedPath = $this->escapePath($path);
150 480
		$output = $this->execute('cd ' . $escapedPath);
151
		//check output for errors
152 480
		$this->parseOutput($output, $path);
153 480
		$output = $this->execute('dir');
154
155 480
		$this->execute('cd /');
156
157
		return $this->parser->parseDir($output, $path, function ($path) {
158
			return $this->getAcls($path);
159 480
		});
160
	}
161
162
	/**
163
	 * @param string $path
164
	 * @return \Icewind\SMB\IFileInfo
165
	 */
166 48
	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 48
		if ($path !== "" && $path !== "/") {
170 46
			$parent = dirname($path);
171 46
			$dir = $this->dir($parent);
172
			$file = array_values(array_filter($dir, function (IFileInfo $info) use ($path) {
173 44
				return $info->getPath() === $path;
174 46
			}));
175 46
			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 26
				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 480
	public function mkdir($path) {
207 480
		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 478
	public function rmdir($path) {
220 478
		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 234
	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 234
			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 204
	public function put($source, $target) {
285 204
		$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
286 204
		$path2 = $this->escapePath($target);
287 186
		$output = $this->execute('put ' . $path1 . ' ' . $path2);
288 186
		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 480
	protected function execute($command) {
418 480
		$this->connect();
419 480
		$this->connection->write($command . PHP_EOL);
420 480
		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 480
	protected function parseOutput($lines, $path = '') {
438 480
		if (count($lines) === 0) {
439 480
			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 482
	protected function escapePath($path) {
459 482
		$this->verifyPath($path);
460 482
		if ($path === '/') {
461 2
			$path = '';
462
		}
463 482
		$path = str_replace('/', '\\', $path);
464 482
		$path = str_replace('"', '^"', $path);
465 482
		$path = ltrim($path, '\\');
466 482
		return '"' . $path . '"';
467
	}
468
469
	/**
470
	 * @param string $path
471
	 * @return string
472
	 */
473 240
	protected function escapeLocalPath($path) {
474 240
		$path = str_replace('"', '\"', $path);
475 240
		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 482
	public function __destruct() {
560 482
		unset($this->connection);
561 482
	}
562
}
563