Completed
Pull Request — master (#85)
by Raffael
02:45
created

Share::write()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2.003

Importance

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