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