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

Share::setMode()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4.0312

Importance

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