Completed
Push — prefer-dir ( c7f89d )
by Robin
12:57
created

Share::stat()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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