Completed
Push — master ( 5a8d9e...0bfb02 )
by Robin
03:26
created

Share::getAuthFileArgument()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

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