Completed
Push — master ( ec564d...deb061 )
by Robin
05:48
created

Share::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 7
cts 7
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
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\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->hasStdBuf() ? 'stdbuf -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 255
		);
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 4
			return;
104
		}
105 762
		$this->connection = $this->getConnection();
106 762
	}
107
108
	protected function reconnect() {
109
		$this->connection->reconnect();
110
		if (!$this->connection->isValid()) {
111
			throw new ConnectionException();
112
		}
113
	}
114
115
	/**
116
	 * Get the name of the share
117
	 *
118
	 * @return string
119
	 */
120 3
	public function getName() {
121 3
		return $this->name;
122
	}
123
124 753
	protected function simpleCommand($command, $path) {
125 753
		$escapedPath = $this->escapePath($path);
126 753
		$cmd = $command . ' ' . $escapedPath;
127 753
		$output = $this->execute($cmd);
128
		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
		$this->parseOutput($output, $path);
145
		$output = $this->execute('dir');
146
147
		$this->execute('cd /');
148
149
		return $this->parser->parseDir($output, $path);
150
	}
151
152
	/**
153
	 * @param string $path
154
	 * @return \Icewind\SMB\IFileInfo
155
	 */
156
	public function stat($path) {
157
		$escapedPath = $this->escapePath($path);
158
		$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
		if ($escapedPath == '""' && count($output) < 2) {
163
			$output = $this->execute('allinfo ' . '"."');
164
		}
165
		if (count($output) < 3) {
166
			$this->parseOutput($output, $path);
167
		}
168
		$stat = $this->parser->parseStat($output);
169
		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
	public function rmdir($path) {
195
		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
	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
			return $this->simpleCommand('del', $path);
213
		} 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
				$this->simpleCommand('ls', $path);
217
			} catch (NotFoundException $e2) {
218
				throw $e;
219
			} catch (\Exception $e2) {
220
				throw new InvalidTypeException($path);
221
			}
222
			throw $e;
223
		} catch (FileInUseException $e) {
224
			if ($secondTry) {
225
				throw $e;
226
			}
227
			$this->reconnect();
228
			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 View Code Duplication
	public function rename($from, $to) {
243
		$path1 = $this->escapePath($from);
244
		$path2 = $this->escapePath($to);
245
		$output = $this->execute('rename ' . $path1 . ' ' . $path2);
246
		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 9 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 9
		$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
261 9
		$path2 = $this->escapePath($target);
262 9
		$output = $this->execute('put ' . $path1 . ' ' . $path2);
263
		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 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
		$path1 = $this->escapePath($source);
278
		$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping
279
		$output = $this->execute('get ' . $path1 . ' ' . $path2);
280
		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
	public function read($source) {
293
		$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
		$connection = $this->getConnection();
297
298
		$connection->write('get ' . $source . ' ' . $this->system->getFD(5));
299
		$connection->write('exit');
300
		$fh = $connection->getFileOutputStream();
301
		stream_context_set_option($fh, 'file', 'connection', $connection);
302
		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
	public function write($target) {
315
		$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
		$connection = $this->getConnection();
319
320
		$fh = $connection->getFileInputStream();
321
		$connection->write('put ' . $this->system->getFD(4) . ' ' . $target);
322
		$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
		return CallbackWrapper::wrap($fh, null, null, function () use ($connection, $target) {
327
			$connection->close(false); // dont terminate, give the upload some time
328
		});
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
	public function setMode($path, $mode) {
337
		$modeString = '';
338
		foreach (self::MODE_MAP as $modeByte => $string) {
339
			if ($mode & $modeByte) {
340
				$modeString .= $string;
341
			}
342
		}
343
		$path = $this->escapePath($path);
344
345
		// first reset the mode to normal
346
		$cmd = 'setmode ' . $path . ' -rsha';
347
		$output = $this->execute($cmd);
348
		$this->parseOutput($output, $path);
349
350
		if ($mode !== FileInfo::MODE_NORMAL) {
351
			// then set the modes we want
352
			$cmd = 'setmode ' . $path . ' ' . $modeString;
353
			$output = $this->execute($cmd);
354
			return $this->parseOutput($output, $path);
355
		} else {
356
			return true;
357
		}
358
	}
359
360
	/**
361
	 * @param string $path
362
	 * @return INotifyHandler
363
	 * @throws ConnectionException
364
	 * @throws DependencyException
365
	 */
366 12
	public function notify($path) {
367 12
		if (!$this->system->hasStdBuf()) { //stdbuf is required to disable smbclient's output buffering
368
			throw new DependencyException('stdbuf is required for usage of the notify command');
369
		}
370 12
		$connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process
371 12
		$command = 'notify ' . $this->escapePath($path);
372 12
		$connection->write($command . PHP_EOL);
373 12
		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
	protected function parseOutput($lines, $path = '') {
401
		if (count($lines) === 0) {
402
			return true;
403
		} else {
404
			$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
			$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 9
	protected function escapeLocalPath($path) {
437 9
		$path = str_replace('"', '\"', $path);
438 9
		return '"' . $path . '"';
439
	}
440
441 768
	public function __destruct() {
442 768
		unset($this->connection);
443 768
	}
444
}
445