Completed
Push — master ( de268f...fc9f0b )
by Robin
02:48
created

Share::escapePath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2.0054

Importance

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