Passed
Push — master ( b24dcf...4d841d )
by Gabor
03:31
created

ServiceAdapter::setOption()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 0
cts 5
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 2
crap 2
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
use WebHemi\Ftp\AbstractServiceAdapter;
20
21
/**
22
 * Class ServiceAdapter.
23
 */
24
class ServiceAdapter extends AbstractServiceAdapter
25
{
26
    /** @var resource */
27
    private $connectionId = null;
28
29
    /**
30
     * Disconnected by garbage collection.
31
     */
32
    public function __destruct()
33
    {
34
        $this->disconnect();
35
    }
36
37
    /**
38
     * Connect and login to remote host.
39
     *
40
     * @return ServiceInterface
41
     */
42
    public function connect() : ServiceInterface
43
    {
44
        if ($this->getOption('secure', true)) {
45
            $this->connectionId = @ftp_ssl_connect($this->getOption('host'));
46
        } else {
47
            $this->connectionId = @ftp_connect($this->getOption('host'));
48
        }
49
50
        if (!$this->connectionId) {
51
            throw new RuntimeException(
52
                sprintf('Cannot establish connection to server: %s', $this->getOption('host')),
53
                1000
54
            );
55
        }
56
57
        $loginResult = @ftp_login($this->connectionId, $this->getOption('username'), $this->getOption('password'));
58
59
        if (!$loginResult) {
60
            throw new RuntimeException('Cannot connect to remote host: invalid credentials', 1001);
61
        }
62
63
        return $this;
64
    }
65
66
    /**
67
     * Disconnect from remote host.
68
     *
69
     * @return ServiceInterface
70
     */
71
    public function disconnect() : ServiceInterface
72
    {
73
        if (!empty($this->connectionId)) {
74
            ftp_close($this->connectionId);
75
            $this->connectionId = null;
76
        }
77
    }
78
79
    /**
80
     * Toggles connection security level.
81
     *
82
     * @param bool $state
83
     * @return ServiceInterface
84
     */
85
    public function setSecureConnection(bool $state) : ServiceInterface
86
    {
87
        $this->setOption('secure', (bool) $state);
88
89
        if (!empty($this->connectionId)) {
90
            ftp_close($this->connectionId);
91
            $this->connectionId = null;
92
            $this->connect();
93
        }
94
95
        return $this;
96
    }
97
98
    /**
99
     * Toggles connection passive mode.
100
     *
101
     * @param bool $state
102
     * @return ServiceInterface
103
     */
104
    public function setPassiveMode(bool $state) : ServiceInterface
105
    {
106
        ftp_pasv($this->connectionId, (bool) $state);
107
        return $this;
108
    }
109
110
    /**
111
     * Sets remote path.
112
     *
113
     * @param string $path
114
     * @return ServiceInterface
115
     */
116
    public function setRemotePath(string $path) : ServiceInterface
117
    {
118
        if (trim($this->getRemotePath(), '/') == trim($path, '/')) {
119
            return $this;
120
        }
121
122
        if (strpos($path, '/') !== 0) {
123
            $path = $this->getRemotePath().$path;
124
        }
125
126
        $chdirResult = @ftp_chdir($this->connectionId, $path);
127
128
        if (!$chdirResult) {
129
            throw new RuntimeException(sprintf('No such directory on remote host: %s', $path), 1002);
130
        }
131
132
        return $this;
133
    }
134
135
    /**
136
     * Gets remote path.
137
     *
138
     * @return string
139
     */
140
    public function getRemotePath() : string
141
    {
142
        return ftp_pwd($this->connectionId).'/';
143
    }
144
145
    /**
146
     * Lists remote path.
147
     *
148
     * @param null|string $path
149
     * @param bool|null $changeToDirectory
150
     * @return array
151
     */
152
    public function getRemoteFileList(? string $path, ? bool $changeToDirectory) : array
153
    {
154
        $fileList = [];
155
156
        if (!empty($path) && $changeToDirectory) {
157
            $this->setRemotePath($path);
158
            $path = '.';
159
        }
160
161
        $result = @ftp_rawlist($this->connectionId, $path);
162
163
        if (!is_array($result)) {
164
            throw new RuntimeException('Cannot retrieve file list', 1006);
165
        }
166
167
        foreach ($result as $fileRawData) {
168
            $fileData = [];
169
170
            preg_match(
171
                '/^(?P<rights>(?P<type>(-|d))[^\s]+)\s+(?P<symlinks>\d+)\s+(?P<user>[^\s]+)\s+(?P<group>[^\s]+)\s+'
172
                    .'(?P<size>\d+)\s+(?P<date>(?P<month>[^\s]+)\s+(?P<day>[^\s]+)\s+'
173
                    .'(?P<time>[^\s]+))\s+(?P<filename>.+)$/',
174
                $fileRawData,
175
                $fileData
176
            );
177
            $fileInfo = pathinfo($fileData['filename']);
178
179
            $fileList[] = [
180
                'type' => $this->getFileType($fileData['type']),
181
                'chmod' => $this->getOctalChmod($fileData['rights']),
182
                'symlinks' => $fileData['symlinks'],
183
                'user' => $fileData['user'],
184
                'group' => $fileData['group'],
185
                'size' => $fileData['size'],
186
                'date' => $this->getFileDate($fileData),
187
                'basename' => $fileInfo['basename'],
188
                'filename' => $fileInfo['filename'],
189
                'extension' => $fileInfo['extension'] ?? '',
190
            ];
191
        }
192
193
        return $fileList;
194
    }
195
196
    /**
197
     * @param string $fileData
198
     * @return string
199
     */
200
    private function getFileType(string $fileData) : string
201
    {
202
        switch ($fileData) {
203
            case 'd':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
204
                $fileType = 'directory';
205
                break;
206
207
            case 'l':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
208
                $fileType = 'symlink';
209
                break;
210
211
            default:
212
                $fileType = 'file';
213
        }
214
215
        return $fileType;
216
    }
217
218
    /**
219
     * @param array $fileData
220
     * @return string
221
     */
222
    private function getFileDate(array $fileData) : string
223
    {
224
        if (strpos($fileData['time'], ':') !== false) {
225
            $date = $fileData['month'].' '.$fileData['day'].' '.date('Y').' '.$fileData['time'];
226
        } else {
227
            $date = $fileData['date'].' 12:00:00';
228
        }
229
230
        $time = strtotime($date);
231
232
        return date('Y-m-d H:i:s', $time);
233
    }
234
235
    /**
236
     * Converts file rights string into octal value.
237
     *
238
     * @param string $permissions The UNIX-style permission string, e.g.: 'drwxr-xr-x'
239
     * @return string
240
     */
241
    private function getOctalChmod(string $permissions) : string
242
    {
243
        $mode = 0;
244
        $mapper = [
245
            0 => [], // type like d as directory, l as link etc.
246
            // Owner
247
            1 => ['r' => 0400],
248
            2 => ['w' => 0200],
249
            3 => [
250
                'x' => 0100,
251
                's' => 04100,
252
                'S' => 04000
253
            ],
254
            // Group
255
            4 => ['r' => 040],
256
            5 => ['w' => 020],
257
            6 => [
258
                'x' => 010,
259
                's' => 02010,
260
                'S' => 02000
261
            ],
262
            // World
263
            7 => ['r' => 04],
264
            8 => ['w' => 02],
265
            9 => [
266
                'x' => 01,
267
                't' => 01001,
268
                'T' => 01000
269
            ],
270
        ];
271
272
        for ($i = 1; $i <= 9; $i++) {
273
            $mode += $mapper[$i][$permissions[$i]] ?? 0;
274
        }
275
276
        return (string) $mode;
277
    }
278
279
    /**
280
     * Uploads file to remote host.
281
     *
282
     * @see self::setRemotePath
283
     * @see self::setLocalPath
284
     *
285
     * @param string $sourceFileName
286
     * @param string $destinationFileName
287
     * @param int $fileMode
288
     * @return mixed
289
     */
290
    public function upload(
291
        string $sourceFileName,
292
        string $destinationFileName,
293
        int $fileMode = self::FILE_MODE_BINARY
294
    ) : ServiceInterface {
295
        $this->checkLocalFile($sourceFileName);
296
        $this->checkRemoteFile($destinationFileName);
297
298
        if (!file_exists($this->localPath.'/'.$sourceFileName)) {
299
            throw new RuntimeException(sprintf('File not found: %s', $this->localPath.'/'.$sourceFileName), 1007);
300
        }
301
302
        $uploadResult = @ftp_put(
303
            $this->connectionId,
304
            $destinationFileName,
305
            $this->localPath.'/'.$sourceFileName,
306
            $fileMode
307
        );
308
309
        if (!$uploadResult) {
310
            throw new RuntimeException(sprintf('There was a problem while uploading file: %s', $sourceFileName), 1008);
311
        }
312
313
        return $this;
314
    }
315
316
    /**
317
     * Downloads file from remote host.
318
     *
319
     * @see self::setRemotePath
320
     * @see self::setLocalPath
321
     *
322
     * @param string $remoteFileName
323
     * @param string $localFileName
324
     * @param int $fileMode
325
     * @return mixed
326
     */
327
    public function download(
328
        string $remoteFileName,
329
        string&$localFileName,
330
        int $fileMode = self::FILE_MODE_BINARY
331
    ) : ServiceInterface {
332
        $this->checkRemoteFile($remoteFileName);
333
        $this->checkLocalFile($localFileName, true);
334
335
        $downloadResult = @ftp_get(
336
            $this->connectionId,
337
            $this->localPath.'/'.$localFileName,
338
            $remoteFileName,
339
            $fileMode
340
        );
341
342
        if (!$downloadResult) {
343
            throw new RuntimeException(
344
                sprintf('There was a problem while downloading file: %s', $remoteFileName),
345
                1010
346
            );
347
        }
348
349
        return $this;
350
    }
351
352
    /**
353
     * Checks local file, and generates new unique name if necessary.
354
     *
355
     * @param string $localFileName
356
     * @param bool $forceUnique
357
     * @throws RuntimeException
358
     */
359
    private function checkLocalFile(string&$localFileName, bool $forceUnique = false) : void
360
    {
361
        $pathInfo = pathinfo($localFileName);
362
363
        if ($pathInfo['dirname'] != '.') {
364
            $this->setLocalPath($pathInfo['dirname']);
365
            $localFileName = $pathInfo['basename'];
366
        }
367
368
        if (!$forceUnique) {
369
            return;
370
        }
371
372
        $variant = 0;
373
374
        while (file_exists($this->localPath.'/'.$localFileName) && $variant++ < 20) {
375
            $fileNameParts = [
376
                $pathInfo['filename'],
377
                '('.$variant.')',
378
                $pathInfo['extension']
379
            ];
380
381
            // remove empty parts (e.g.: when there's no extension)
382
            $fileNameParts = array_filter($fileNameParts);
383
384
            $localFileName = implode('.', $fileNameParts);
385
        }
386
387
        if ($variant >= 20) {
388
            throw new RuntimeException(
389
                sprintf('Too many similar files in folder %s, please cleanup first.', $this->localPath),
390
                1009
391
            );
392
        }
393
    }
394
395
    /**
396
     * Check remote file name.
397
     *
398
     * @param string $remoteFileName
399
     */
400
    private function checkRemoteFile(string&$remoteFileName) : void
401
    {
402
        $pathInfo = pathinfo($remoteFileName);
403
404
        if ($pathInfo['dirname'] != '.') {
405
            $this->setRemotePath($pathInfo['dirname']);
406
            $remoteFileName = $pathInfo['basename'];
407
        }
408
    }
409
410
    /**
411
     * Moves file on remote host.
412
     *
413
     * @param string $currentPath
414
     * @param string $newPath
415
     * @return ServiceInterface
416
     */
417
    public function moveRemoteFile(string $currentPath, string $newPath) : ServiceInterface
418
    {
419
        $result = @ftp_rename($this->connectionId, $currentPath, $newPath);
420
421
        if (!$result) {
422
            throw new RuntimeException(
423
                sprintf('Unable to move/rename file from %s to %s', $currentPath, $newPath),
424
                1011
425
            );
426
        }
427
428
        return $this;
429
    }
430
431
    /**
432
     * Deletes file on remote host.
433
     *
434
     * @param string $path
435
     * @return ServiceInterface
436
     */
437
    public function deleteRemoteFile(string $path) : ServiceInterface
438
    {
439
        $result = @ftp_delete($this->connectionId, $path);
440
441
        if (!$result) {
442
            throw new RuntimeException(sprintf('Unable to delete file on remote host: %s', $path), 1012);
443
        }
444
445
        return $this;
446
    }
447
}
448