Completed
Pull Request — master (#70)
by Raffael
13:44
created

Share::append()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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