Test Failed
Branch master (3ad9ef)
by Gabor
05:05
created

ServiceAdapter::download()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 12
nc 2
nop 3
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
1
<?php
2
/**
3
 * WebHemi.
4
 *
5
 * PHP version 7.1
6
 *
7
 * @copyright 2012 - 2018 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\Ftp\ServiceInterface;
18
use WebHemi\Ftp\ServiceAdapter\AbstractServiceAdapter;
19
20
/**
21
 * Class ServiceAdapter.
22
 *
23
 * @codeCoverageIgnore - don't test third party library.
24
 */
25
class ServiceAdapter extends AbstractServiceAdapter
26
{
27
    /**
28
     * @var resource
29
     */
30
    private $connectionId = null;
31
32
    /**
33
     * Disconnected by garbage collection.
34
     */
35
    public function __destruct()
36
    {
37
        $this->disconnect();
38
    }
39
40
    /**
41
     * Connect and login to remote host.
42
     *
43
     * @return ServiceInterface
44
     */
45
    public function connect() : ServiceInterface
46
    {
47
        if ($this->getOption('secure', true)) {
48
            $this->connectionId = @ftp_ssl_connect($this->getOption('host'));
49
        } else {
50
            $this->connectionId = @ftp_connect($this->getOption('host'));
51
        }
52
53
        if (!$this->connectionId) {
54
            throw new RuntimeException(
55
                sprintf('Cannot establish connection to server: %s', $this->getOption('host')),
56
                1000
57
            );
58
        }
59
60
        $loginResult = @ftp_login($this->connectionId, $this->getOption('username'), $this->getOption('password'));
61
62
        if (!$loginResult) {
63
            throw new RuntimeException('Cannot connect to remote host: invalid credentials', 1001);
64
        }
65
66
        return $this;
67
    }
68
69
    /**
70
     * Disconnect from remote host.
71
     *
72
     * @return ServiceInterface
73
     */
74
    public function disconnect() : ServiceInterface
75
    {
76
        if (!empty($this->connectionId)) {
77
            ftp_close($this->connectionId);
78
            $this->connectionId = null;
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return WebHemi\Ftp\ServiceInterface. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
79
        }
80
    }
81
82
    /**
83
     * Toggles connection security level.
84
     *
85
     * @param  bool $state
86
     * @return ServiceInterface
87
     */
88
    public function setSecureConnection(bool $state) : ServiceInterface
89
    {
90
        $this->setOption('secure', (bool) $state);
91
92
        if (!empty($this->connectionId)) {
93
            ftp_close($this->connectionId);
94
            $this->connectionId = null;
95
            $this->connect();
96
        }
97
98
        return $this;
99
    }
100
101
    /**
102
     * Toggles connection passive mode.
103
     *
104
     * @param  bool $state
105
     * @return ServiceInterface
106
     */
107
    public function setPassiveMode(bool $state) : ServiceInterface
108
    {
109
        ftp_pasv($this->connectionId, (bool) $state);
110
        return $this;
111
    }
112
113
    /**
114
     * Sets remote path.
115
     *
116
     * @param  string $path
117
     * @return ServiceInterface
118
     */
119
    public function setRemotePath(string $path) : ServiceInterface
120
    {
121
        if (trim($this->getRemotePath(), '/') == trim($path, '/')) {
122
            return $this;
123
        }
124
125
        if (strpos($path, '/') !== 0) {
126
            $path = $this->getRemotePath().$path;
127
        }
128
129
        $chdirResult = @ftp_chdir($this->connectionId, $path);
130
131
        if (!$chdirResult) {
132
            throw new RuntimeException(sprintf('No such directory on remote host: %s', $path), 1002);
133
        }
134
135
        return $this;
136
    }
137
138
    /**
139
     * Gets remote path.
140
     *
141
     * @return string
142
     */
143
    public function getRemotePath() : string
144
    {
145
        return ftp_pwd($this->connectionId).'/';
146
    }
147
148
    /**
149
     * Lists remote path.
150
     *
151
     * @param  null|string $path
152
     * @param  bool|null   $changeToDirectory
153
     * @return array
154
     */
155
    public function getRemoteFileList(? string $path, ? bool $changeToDirectory) : array
156
    {
157
        $fileList = [];
158
159
        if (!empty($path) && $changeToDirectory) {
160
            $this->setRemotePath($path);
161
            $path = '.';
162
        }
163
164
        $result = @ftp_rawlist($this->connectionId, $path);
165
166
        if (!is_array($result)) {
0 ignored issues
show
introduced by
The condition ! is_array($result) can never be true.
Loading history...
167
            throw new RuntimeException('Cannot retrieve file list', 1006);
168
        }
169
170
        foreach ($result as $fileRawData) {
171
            $fileData = [];
172
173
            preg_match(
174
                '/^(?P<rights>(?P<type>(-|d))[^\s]+)\s+(?P<symlinks>\d+)\s+(?P<user>[^\s]+)\s+(?P<group>[^\s]+)\s+'
175
                    .'(?P<size>\d+)\s+(?P<date>(?P<month>[^\s]+)\s+(?P<day>[^\s]+)\s+'
176
                    .'(?P<time>[^\s]+))\s+(?P<filename>.+)$/',
177
                $fileRawData,
178
                $fileData
179
            );
180
            $fileInfo = pathinfo($fileData['filename']);
181
182
            $fileList[] = [
183
                'type' => $this->getFileType($fileData['type']),
184
                'chmod' => $this->getOctalChmod($fileData['rights']),
185
                'symlinks' => $fileData['symlinks'],
186
                'user' => $fileData['user'],
187
                'group' => $fileData['group'],
188
                'size' => $fileData['size'],
189
                'date' => $this->getFileDate($fileData),
190
                'basename' => $fileInfo['basename'],
191
                'filename' => $fileInfo['filename'],
192
                'extension' => $fileInfo['extension'] ?? '',
193
            ];
194
        }
195
196
        return $fileList;
197
    }
198
199
    /**
200
     * @param string $fileData
201
     * @return string
202
     */
203
    private function getFileType(string $fileData) : string
204
    {
205
        switch ($fileData) {
206
            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...
207
                $fileType = 'directory';
208
                break;
209
210
            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...
211
                $fileType = 'symlink';
212
                break;
213
214
            default:
215
                $fileType = 'file';
216
        }
217
218
        return $fileType;
219
    }
220
221
    /**
222
     * @param array $fileData
223
     * @return string
224
     */
225
    private function getFileDate(array $fileData) : string
226
    {
227
        if (strpos($fileData['time'], ':') !== false) {
228
            $date = $fileData['month'].' '.$fileData['day'].' '.date('Y').' '.$fileData['time'];
229
        } else {
230
            $date = $fileData['date'].' 12:00:00';
231
        }
232
233
        $time = strtotime($date);
234
235
        return date('Y-m-d H:i:s', $time);
236
    }
237
238
    /**
239
     * Uploads file to remote host.
240
     *
241
     * @see self::setRemotePath
242
     * @see self::setLocalPath
243
     *
244
     * @param  string $sourceFileName
245
     * @param  string $destinationFileName
246
     * @param  int    $fileMode
247
     * @return ServiceInterface
248
     */
249
    public function upload(
250
        string $sourceFileName,
251
        string $destinationFileName,
252
        int $fileMode = self::FILE_MODE_BINARY
253
    ) : ServiceInterface {
254
        $this->checkLocalFile($sourceFileName);
255
        $this->checkRemoteFile($destinationFileName);
256
257
        if (!file_exists($this->localPath.'/'.$sourceFileName)) {
258
            throw new RuntimeException(sprintf('File not found: %s', $this->localPath.'/'.$sourceFileName), 1007);
259
        }
260
261
        $uploadResult = @ftp_put(
262
            $this->connectionId,
263
            $destinationFileName,
264
            $this->localPath.'/'.$sourceFileName,
265
            $fileMode
266
        );
267
268
        if (!$uploadResult) {
269
            throw new RuntimeException(sprintf('There was a problem while uploading file: %s', $sourceFileName), 1008);
270
        }
271
272
        return $this;
273
    }
274
275
    /**
276
     * Downloads file from remote host.
277
     *
278
     * @see self::setRemotePath
279
     * @see self::setLocalPath
280
     *
281
     * @param  string $remoteFileName
282
     * @param  string $localFileName
283
     * @param  int    $fileMode
284
     * @return ServiceInterface
285
     */
286
    public function download(
287
        string $remoteFileName,
288
        string&$localFileName,
289
        int $fileMode = self::FILE_MODE_BINARY
290
    ) : ServiceInterface {
291
        $this->checkRemoteFile($remoteFileName);
292
        $this->checkLocalFile($localFileName, true);
293
294
        $downloadResult = @ftp_get(
295
            $this->connectionId,
296
            $this->localPath.'/'.$localFileName,
297
            $remoteFileName,
298
            $fileMode
299
        );
300
301
        if (!$downloadResult) {
302
            throw new RuntimeException(
303
                sprintf('There was a problem while downloading file: %s', $remoteFileName),
304
                1010
305
            );
306
        }
307
308
        return $this;
309
    }
310
311
    /**
312
     * Check remote file name.
313
     *
314
     * @param string $remoteFileName
315
     */
316
    protected function checkRemoteFile(string&$remoteFileName) : void
317
    {
318
        $pathInfo = pathinfo($remoteFileName);
319
320
        if ($pathInfo['dirname'] != '.') {
321
            $this->setRemotePath($pathInfo['dirname']);
322
            $remoteFileName = $pathInfo['basename'];
323
        }
324
    }
325
326
    /**
327
     * Moves file on remote host.
328
     *
329
     * @param  string $currentPath
330
     * @param  string $newPath
331
     * @return ServiceInterface
332
     */
333
    public function moveRemoteFile(string $currentPath, string $newPath) : ServiceInterface
334
    {
335
        $result = @ftp_rename($this->connectionId, $currentPath, $newPath);
336
337
        if (!$result) {
338
            throw new RuntimeException(
339
                sprintf('Unable to move/rename file from %s to %s', $currentPath, $newPath),
340
                1011
341
            );
342
        }
343
344
        return $this;
345
    }
346
347
    /**
348
     * Deletes file on remote host.
349
     *
350
     * @param  string $path
351
     * @return ServiceInterface
352
     */
353
    public function deleteRemoteFile(string $path) : ServiceInterface
354
    {
355
        $result = @ftp_delete($this->connectionId, $path);
356
357
        if (!$result) {
358
            throw new RuntimeException(sprintf('Unable to delete file on remote host: %s', $path), 1012);
359
        }
360
361
        return $this;
362
    }
363
}
364