Completed
Push — master ( fdaf09...72216c )
by Robin
04:18
created

Share::execute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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