Completed
Push — stable3.0 ( 04db6f...bd43cd )
by Robin
03:57
created

Share::stat()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7.1653

Importance

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