Passed
Push — master ( 2b4cfa...2d29d5 )
by Gabor
03:08
created

ServiceAdapter::getOctalChmod()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 37
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 37
ccs 0
cts 32
cp 0
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 25
nc 2
nop 1
crap 6
1
<?php
2
/**
3
 * WebHemi.
4
 *
5
 * PHP version 7.1
6
 *
7
 * @copyright 2012 - 2017 Gixx-web (http://www.gixx-web.com)
8
 * @license   https://opensource.org/licenses/MIT The MIT License (MIT)
9
 *
10
 * @link      http://www.gixx-web.com
11
 */
12
declare(strict_types = 1);
13
14
namespace WebHemi\Ftp\ServiceAdapter\Base;
15
16
use RuntimeException;
17
use WebHemi\Configuration\ServiceInterface as ConfigurationInterface;
18
use WebHemi\Ftp\ServiceInterface;
19
20
/**
21
 * Class ServiceAdapter.
22
 */
23
class ServiceAdapter implements ServiceInterface
24
{
25
    /** @var resource */
26
    private $connectionId = null;
27
    /** @var string */
28
    private $localPath = __DIR__;
29
    /** @var array */
30
    protected $options = [];
31
32
    /**
33
     * ServiceAdapter constructor.
34
     *
35
     * @param ConfigurationInterface $configuration
36
     */
37
    public function __construct(ConfigurationInterface $configuration)
38
    {
39
        $this->setOptions($configuration->getData('ftp'));
40
    }
41
42
    /**
43
     * Disconnected by garbage collection.
44
     */
45
    public function __destruct()
46
    {
47
        $this->disconnect();
48
    }
49
50
    /**
51
     * Connect and login to remote host.
52
     *
53
     * @return ServiceInterface
54
     */
55
    public function connect() : ServiceInterface
56
    {
57
        if ($this->getOption('secure', true)) {
58
            $this->connectionId = @ftp_ssl_connect($this->getOption('host'));
59
        } else {
60
            $this->connectionId = @ftp_connect($this->getOption('host'));
61
        }
62
63
        if (!$this->connectionId) {
64
            throw new RuntimeException(
65
                sprintf('Cannot establish connection to server: %s', $this->getOption('host')),
66
                1000
67
            );
68
        }
69
70
        $loginResult = @ftp_login($this->connectionId, $this->getOption('username'), $this->getOption('password'));
71
72
        if (!$loginResult) {
73
            throw new RuntimeException('Cannot connect to remote host: invalid credentials', 1001);
74
        }
75
76
        return $this;
77
    }
78
79
    /**
80
     * Disconnect from remote host.
81
     *
82
     * @return ServiceInterface
83
     */
84
    public function disconnect() : ServiceInterface
85
    {
86
        if (!empty($this->connectionId)) {
87
            ftp_close($this->connectionId);
88
            $this->connectionId = null;
89
        }
90
    }
91
92
    /**
93
     * Sets an option data.
94
     *
95
     * @param string $key
96
     * @param mixed $value
97
     * @return ServiceInterface
98
     */
99
    public function setOption(string $key, $value) : ServiceInterface
100
    {
101
        $this->options[$key] = $value;
102
        return $this;
103
    }
104
105
    /**
106
     * Sets a group of options.
107
     *
108
     * @param array $options
109
     * @return ServiceInterface
110
     */
111
    public function setOptions(array $options) : ServiceInterface
112
    {
113
        $this->options = array_merge($this->options, $options);
114
        return $this;
115
    }
116
117
    /**
118
     * Gets a specific option data.
119
     *
120
     * @param string $key
121
     * @param mixed $default
122
     * @return mixed
123
     */
124
    public function getOption(string $key, $default = null)
125
    {
126
        return $this->options[$key] ?? $default;
127
    }
128
129
    /**
130
     * Toggles connection security level.
131
     *
132
     * @param bool $state
133
     * @return ServiceInterface
134
     */
135
    public function setSecureConnection(bool $state) : ServiceInterface
136
    {
137
        $this->setOption('secure', (bool)$state);
138
139
        if (!empty($this->connectionId)) {
140
            ftp_close($this->connectionId);
141
            $this->connectionId = null;
142
            $this->connect();
143
        }
144
145
        return $this;
146
    }
147
148
    /**
149
     * Toggles connection passive mode.
150
     *
151
     * @param bool $state
152
     * @return ServiceInterface
153
     */
154
    public function setPassiveMode(bool $state) : ServiceInterface
155
    {
156
        ftp_pasv($this->connectionId, (bool)$state);
157
        return $this;
158
    }
159
160
    /**
161
     * Sets remote path.
162
     *
163
     * @param string $path
164
     * @return ServiceInterface
165
     */
166
    public function setRemotePath(string $path) : ServiceInterface
167
    {
168
        if (trim($this->getRemotePath(), '/') == trim($path, '/')) {
169
            return $this;
170
        }
171
172
        if (strpos($path, '/') !== 0) {
173
            $path = $this->getRemotePath() . $path;
174
        }
175
176
        $chdirResult = @ftp_chdir($this->connectionId, $path);
177
178
        if (!$chdirResult) {
179
            throw new RuntimeException(sprintf('No such directory on remote host: %s', $path), 1002);
180
        }
181
182
        return $this;
183
    }
184
185
    /**
186
     * Gets remote path.
187
     *
188
     * @return string
189
     */
190
    public function getRemotePath() : string
191
    {
192
        return ftp_pwd($this->connectionId) . '/';
193
    }
194
195
    /**
196
     * Sets local path.
197
     *
198
     * @param string $path
199
     * @return ServiceInterface
200
     */
201
    public function setLocalPath(string $path) : ServiceInterface
202
    {
203
        // if it's not an absolute path, we take it relative to the current folder
204
        if (strpos($path, '/') !== 0) {
205
            $path = __DIR__ . '/' . $path;
206
        }
207
208
        if (!realpath($path) || !is_dir($path)) {
209
            throw new RuntimeException(sprintf('No such directory: %s', $path), 1003);
210
        }
211
212
        if (!is_readable($path)) {
213
            throw new RuntimeException(sprintf('Cannot read directory: %s; Permission denied.', $path), 1004);
214
        }
215
216
        if (!is_writable($path)) {
217
            throw new RuntimeException(
218
                sprintf('Cannot write data into directory: %s; Permission denied.', $path),
219
                1005
220
            );
221
        }
222
223
        $this->localPath = $path;
224
225
        return $this;
226
    }
227
228
    /**
229
     * Gets local path.
230
     *
231
     * @return string
232
     */
233
    public function getLocalPath() : string
234
    {
235
        return $this->localPath;
236
    }
237
238
    /**
239
     * Lists remote path.
240
     *
241
     * @param null|string $path
242
     * @param bool|null $changeToDirectory
243
     * @return array
244
     */
245
    public function getRemoteFileList(? string $path, ? bool $changeToDirectory) : array
246
    {
247
        $fileList = [];
248
249
        if (!empty($path) && $changeToDirectory) {
250
            $this->setRemotePath($path);
251
            $path = '.';
252
        }
253
254
        $result = @ftp_rawlist($this->connectionId, $path);
255
256
        if (!is_array($result)) {
257
            throw new RuntimeException('Cannot retrieve file list', 1006);
258
        }
259
260
        foreach ($result as $fileRawData) {
261
            $fileData = [];
262
263
            preg_match(
264
                '/^(?P<rights>(?P<type>(-|d))[^\s]+)\s+(?P<symlinks>\d+)\s+(?P<user>[^\s]+)\s+(?P<group>[^\s]+)\s+'
265
                    .'(?P<size>\d+)\s+(?P<date>(?P<month>[^\s]+)\s+(?P<day>[^\s]+)\s+'
266
                    .'(?P<time>[^\s]+))\s+(?P<filename>.+)$/',
267
                $fileRawData,
268
                $fileData
269
            );
270
            $fileInfo = pathinfo($fileData['filename']);
271
272
            $fileList[] = [
273
                'type' => $fileData['type'] == 'd' ? 'directory' : ($fileData['type'] == 'l' ? 'symlink' : 'file'),
274
                'chmod' => $this->getOctalChmod($fileData['rights']),
275
                'symlinks' => $fileData['symlinks'],
276
                'user' => $fileData['user'],
277
                'group' => $fileData['group'],
278
                'size' => $fileData['size'],
279
                'date' => date(
280
                    'Y-m-d H:i:s',
281
                    strtotime(
282
                        strpos($fileData['time'], ':') !== false
283
                            ? $fileData['month'] . ' ' . $fileData['day'] . ' ' . date('Y') . ' ' . $fileData['time']
284
                            : $fileData['date'] . ' 12:00:00'
285
                    )
286
                ),
287
                'basename' => $fileInfo['basename'],
288
                'filename' => $fileInfo['filename'],
289
                'extension' => isset($fileInfo['extension']) ? $fileInfo['extension'] : '',
290
            ];
291
        }
292
293
        return $fileList;
294
    }
295
296
    /**
297
     * Converts file rights string into octal value.
298
     *
299
     * @param string $permissions The UNIX-style permission string, e.g.: 'drwxr-xr-x'
300
     * @return string
301
     */
302
    private function getOctalChmod(string $permissions) : string
303
    {
304
        $mode = 0;
305
        $mapper = [
306
            0 => [], // type like d as directory, l as link etc.
307
            // Owner
308
            1 => ['r' => 0400],
309
            2 => ['w' => 0200],
310
            3 => [
311
                'x' => 0100,
312
                's' => 04100,
313
                'S' => 04000
314
            ],
315
            // Group
316
            4 => ['r' => 040],
317
            5 => ['w' => 020],
318
            6 => [
319
                'x' => 010,
320
                's' => 02010,
321
                'S' => 02000
322
            ],
323
            // World
324
            7 => ['r' => 04],
325
            8 => ['w' => 02],
326
            9 => [
327
                'x' => 01,
328
                't' => 01001,
329
                'T' => 01000
330
            ],
331
        ];
332
333
        for ($i = 1; $i <= 9; $i++) {
334
            $mode += $mapper[$i][$permissions[$i]] ?? 0;
335
        }
336
337
        return (string)$mode;
338
    }
339
340
    /**
341
     * Uploads file to remote host.
342
     *
343
     * @see self::setRemotePath
344
     * @see self::setLocalPath
345
     *
346
     * @param string $sourceFileName
347
     * @param string $destinationFileName
348
     * @param int $fileMode
349
     * @return mixed
350
     */
351
    public function upload(
352
        string $sourceFileName,
353
        string $destinationFileName,
354
        int $fileMode = self::FILE_MODE_BINARY
355
    ) : ServiceInterface {
356
        $this->checkLocalFile($sourceFileName);
357
        $this->checkRemoteFile($destinationFileName);
358
359
        if (!file_exists($this->localPath . '/' . $sourceFileName)) {
360
            throw new RuntimeException(sprintf('File not found: %s', $this->localPath . '/' . $sourceFileName), 1007);
361
        }
362
363
        $uploadResult = @ftp_put(
364
            $this->connectionId,
365
            $destinationFileName,
366
            $this->localPath . '/' . $sourceFileName,
367
            $fileMode
368
        );
369
370
        if (!$uploadResult) {
371
            throw new RuntimeException(sprintf('There was a problem while uploading file: %s', $sourceFileName), 1008);
372
        }
373
374
        return $this;
375
    }
376
377
    /**
378
     * Downloads file from remote host.
379
     *
380
     * @see self::setRemotePath
381
     * @see self::setLocalPath
382
     *
383
     * @param string $remoteFileName
384
     * @param string $localFileName
385
     * @param int $fileMode
386
     * @return mixed
387
     */
388
    public function download(
389
        string $remoteFileName,
390
        string&$localFileName,
391
        int $fileMode = self::FILE_MODE_BINARY
392
    ) : ServiceInterface {
393
        $this->checkRemoteFile($remoteFileName);
394
        $this->checkLocalFile($localFileName, true);
395
396
        $downloadResult = @ftp_get(
397
            $this->connectionId,
398
            $this->localPath . '/' . $localFileName,
399
            $remoteFileName,
400
            $fileMode
401
        );
402
403
        if (!$downloadResult) {
404
            throw new RuntimeException(
405
                sprintf('There was a problem while downloading file: %s', $remoteFileName),
406
                1010
407
            );
408
        }
409
410
        return $this;
411
    }
412
413
    /**
414
     * Checks local file, and generates new unique name if necessary.
415
     *
416
     * @param string $localFileName
417
     * @param bool $forceUnique
418
     * @throws RuntimeException
419
     */
420
    private function checkLocalFile(string&$localFileName, bool $forceUnique = false) : void
421
    {
422
        $pathInfo = pathinfo($localFileName);
423
424
        if ($pathInfo['dirname'] != '.') {
425
            $this->setLocalPath($pathInfo['dirname']);
426
            $localFileName = $pathInfo['basename'];
427
        }
428
429
        if (!$forceUnique) {
430
            return;
431
        }
432
433
        $variant = 0;
434
435
        while (file_exists($this->localPath.'/'.$localFileName) && $variant++ < 20) {
436
            $fileNameParts = [
437
                $pathInfo['filename'],
438
                '('.$variant.')',
439
                $pathInfo['extension']
440
            ];
441
442
            // remove empty parts (e.g.: when there's no extension)
443
            $fileNameParts = array_filter($fileNameParts);
444
445
            $localFileName = implode('.', $fileNameParts);
446
        }
447
448
        if ($variant >= 20) {
449
            throw new RuntimeException(
450
                sprintf('Too many similar files in folder %s, please cleanup first.', $this->localPath),
451
                1009
452
            );
453
        }
454
    }
455
456
    /**
457
     * Check remote file name.
458
     *
459
     * @param string $remoteFileName
460
     */
461
    private function checkRemoteFile(string&$remoteFileName) : void
462
    {
463
        $pathInfo = pathinfo($remoteFileName);
464
465
        if ($pathInfo['dirname'] != '.') {
466
            $this->setRemotePath($pathInfo['dirname']);
467
            $remoteFileName = $pathInfo['basename'];
468
        }
469
    }
470
471
    /**
472
     * Moves file on remote host.
473
     *
474
     * @param string $currentPath
475
     * @param string $newPath
476
     * @return ServiceInterface
477
     */
478
    public function moveRemoteFile(string $currentPath, string $newPath) : ServiceInterface
479
    {
480
        $result = @ftp_rename($this->connectionId, $currentPath, $newPath);
481
482
        if (!$result) {
483
            throw new RuntimeException(
484
                sprintf('Unable to move/rename file from %s to %s', $currentPath, $newPath),
485
                1011
486
            );
487
        }
488
489
        return $this;
490
    }
491
492
    /**
493
     * Deletes file on remote host.
494
     *
495
     * @param string $path
496
     * @return ServiceInterface
497
     */
498
    public function deleteRemoteFile(string $path) : ServiceInterface
499
    {
500
        $result = @ftp_delete($this->connectionId, $path);
501
502
        if (!$result) {
503
            throw new RuntimeException(sprintf('Unable to delete file on remote host: %s', $path), 1012);
504
        }
505
506
        return $this;
507
    }
508
}
509