FtpTransport::searchFile()   B
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 0
cts 20
cp 0
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 15
nc 4
nop 1
crap 20
1
<?php
2
3
namespace TreeHouse\Feeder\Transport;
4
5
use TreeHouse\Feeder\Event\FetchProgressEvent;
6
use TreeHouse\Feeder\Exception\TransportException;
7
use TreeHouse\Feeder\FeedEvents;
8
use TreeHouse\Feeder\Transport\Matcher\FileMatcher;
9
use TreeHouse\Feeder\Transport\Matcher\GlobMatcher;
10
use TreeHouse\Feeder\Transport\Matcher\MatcherInterface;
11
use TreeHouse\Feeder\Transport\Matcher\PatternMatcher;
12
13
class FtpTransport extends AbstractTransport implements ProgressAwareInterface
14
{
15
    /**
16
     * @var resource
17
     */
18
    protected $ftpConnection;
19
20
    /**
21
     * @var string
22
     */
23
    protected $fileName;
24
25
    /**
26
     * @var MatcherInterface
27
     */
28
    protected $fileMatcher;
29
30
    /**
31
     * @param string $host
32
     * @param string $user
33
     * @param string $pass
34
     * @param string $file
35
     * @param array  $options
36
     *
37
     * @return FtpTransport
38
     */
39
    public static function create($host, $user = null, $pass = null, $file, array $options = [])
40
    {
41
        $conn = new Connection(array_merge(
42
            [
43
                'host' => $host,
44
                'user' => $user,
45
                'pass' => $pass,
46
                'file' => $file,
47
                'timeout' => 10,
48
            ],
49
            $options
50
        ));
51
        $transport = new self($conn);
52
53
        return $transport;
54
    }
55
56
    public function __clone()
57
    {
58
        parent::__clone();
59
60
        $this->closeFtpConnection();
61
        $this->fileName = null;
62
        $this->fileMatcher = null;
63
    }
64
65
    public function __destruct()
66
    {
67
        $this->closeFtpConnection();
68
    }
69
70
    public function __toString()
71
    {
72
        if ($this->fileName) {
73
            $file = $this->fileName;
74
        } else {
75
            $file = $this->connection['file'];
76
        }
77
78
        return $this->connection['host'] . ':/' . $file;
79
    }
80
81
    /**
82
     * @return string
83
     */
84
    public function getHost()
85
    {
86
        return $this->connection['host'];
87
    }
88
89
    /**
90
     * @return string|null
91
     */
92
    public function getUser()
93
    {
94
        return isset($this->connection['user']) ? $this->connection['user'] : null;
95
    }
96
97
    /**
98
     * @return string|null
99
     */
100
    public function getPass()
101
    {
102
        return isset($this->connection['pass']) ? $this->connection['pass'] : null;
103
    }
104
105
    /**
106
     * @return string|null
107
     */
108
    public function getMode()
109
    {
110
        return isset($this->connection['mode']) ? $this->connection['mode'] : null;
111
    }
112
113
    /**
114
     * @param string $mode
115
     */
116
    public function setMode($mode)
117
    {
118
        $this->connection['mode'] = $mode;
119
    }
120
121
    /**
122
     * @return bool|null
123
     */
124
    public function getPasv()
125
    {
126
        return isset($this->connection['pasv']) ? (boolean) $this->connection['pasv'] : null;
127
    }
128
129
    /**
130
     * @param bool $pasv
131
     */
132
    public function setPasv($pasv)
133
    {
134
        $this->connection['pasv'] = (boolean) $pasv;
135
    }
136
137
    /**
138
     * @return bool|null
139
     */
140
    public function getPattern()
141
    {
142
        return isset($this->connection['pattern']) ? (boolean) $this->connection['pattern'] : null;
143
    }
144
145
    /**
146
     * @param bool $pattern
147
     */
148
    public function setPattern($pattern)
149
    {
150
        $this->connection['pattern'] = (boolean) $pattern;
151
    }
152
153
    /**
154
     * @param string $file
155
     */
156
    public function setFilename($file)
157
    {
158
        $this->connection['file'] = $file;
159
        $this->fileName = null;
160
    }
161
162
    /**
163
     * Returns the file to download from the ftp. Handles globbing rules and
164
     * checks if the file is listed in the remote dir.
165
     *
166
     * @throws TransportException When remote file could not be found
167
     * @return string
168
     *
169
     */
170
    public function getFilename()
171
    {
172
        if (!$this->fileName) {
173
            $matcher = $this->getFileMatcher();
174
            $this->fileName = $this->searchFile($matcher);
175
        }
176
177
        return $this->fileName;
178
    }
179
180
    /**
181
     * @return \DateTime|null
182
     */
183
    public function getLastModifiedDate()
184
    {
185
        // see if uploaded feed is newer
186
        if ($time = ftp_mdtm($this->getFtpConnection(), $this->getFilename())) {
187
            return new \DateTime('@' . $time);
188
        }
189
    }
190
191
    /**
192
     * @return int
193
     */
194
    public function getSize()
195
    {
196
        return ftp_size($this->getFtpConnection(), $this->getFilename());
197
    }
198
199
    /**
200
     * @param MatcherInterface $matcher
201
     */
202
    public function setFileMatcher(MatcherInterface $matcher)
203
    {
204
        $this->fileMatcher = $matcher;
205
    }
206
207
    /**
208
     * @return MatcherInterface
209
     */
210
    public function getFileMatcher()
211
    {
212
        if (null === $this->fileMatcher) {
213
            $this->fileMatcher = $this->createFileMatcher();
214
        }
215
216
        return $this->fileMatcher;
217
    }
218
219
    /**
220
     * @param MatcherInterface $matcher
221
     *
222
     * @throws TransportException
223
     *
224
     * @return string
225
     */
226
    protected function searchFile(MatcherInterface $matcher)
227
    {
228
        $conn = $this->getFtpConnection();
229
        $cwd = ftp_pwd($conn);
230
        $files = ftp_nlist($conn, $cwd);
231
232
        if (false === $files) {
233
            $msg = sprintf('Error listing files from directory "%s"', $cwd);
234
            if (!$this->getPasv()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getPasv() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
235
                $msg .= '. You might want to try passive mode using "pasv: true" in your transport configuration.';
236
            }
237
238
            throw new TransportException($msg);
239
        }
240
241
        // strip cwd off the files
242
        $files = array_map(function ($file) use ($cwd) {
243
            return preg_replace(sprintf('/^%s/', preg_quote($cwd, '/')), '', $file);
244
        }, $files);
245
246
        if (null !== $file = $matcher->match($files)) {
247
            return $file;
248
        }
249
250
        throw new TransportException(sprintf('File "%s" was not found on FTP', (string) $matcher));
251
    }
252
253
    /**
254
     * @param string $destination
255
     */
256
    protected function doFetch($destination)
257
    {
258
        $tmpFile = $this->downloadToTmpFile();
259
260
        // download complete, move to actual destination
261
        rename($tmpFile, $destination);
262
    }
263
264
    /**
265
     * @throws TransportException
266
     * @return string
267
     *
268
     */
269
    protected function downloadToTmpFile()
270
    {
271
        $conn = $this->getFtpConnection();
272
        $file = $this->getFilename();
273
        $tmpFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . basename($file);
274
275
        $fileSize = $this->getSize();
276
277
        $mode = $this->getMode() ? constant('FTP_' . strtoupper($this->getMode())) : FTP_ASCII;
278
279
        $ret = ftp_nb_get($conn, $tmpFile, $file, $mode);
280
        $currentBytes = 0;
281
        while ($ret === FTP_MOREDATA) {
282
            $ret = ftp_nb_continue($conn);
283
            clearstatcache();
284
            $bytes = filesize($tmpFile);
285
            $diff = $bytes - $currentBytes;
286
            $currentBytes = $bytes;
287
288
            $this->eventDispatcher->dispatch(
289
                FeedEvents::FETCH_PROGRESS,
290
                new FetchProgressEvent($currentBytes, $diff, $fileSize)
0 ignored issues
show
Unused Code introduced by
The call to FetchProgressEvent::__construct() has too many arguments starting with $fileSize.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
291
            );
292
        }
293
294
        if ($ret !== FTP_FINISHED) {
295
            throw new TransportException(sprintf('Error downloading feed to %s', $tmpFile));
296
        }
297
298
        return $tmpFile;
299
    }
300
301
    /**
302
     * Returns shared ftp connection.
303
     *
304
     * @return resource
305
     */
306
    protected function getFtpConnection()
307
    {
308
        if (is_null($this->ftpConnection)) {
309
            $host = $this->connection['host'];
310
            $user = $this->connection['user'];
311
            $pass = $this->connection['pass'];
312
313
            $this->ftpConnection = $this->connect($host, $user, $pass);
314
315
            // set timeout
316
            ftp_set_option($this->ftpConnection, FTP_TIMEOUT_SEC, $this->connection['timeout']);
317
318
            // set passive mode if it's defined
319
            if (null !== $pasv = $this->getPasv()) {
320
                ftp_pasv($this->ftpConnection, $pasv);
321
            }
322
        }
323
324
        return $this->ftpConnection;
325
    }
326
327
    /**
328
     * Connects to ftp.
329
     *
330
     * @param string $host
331
     * @param string $user
332
     * @param string $pass
333
     *
334
     * @throws TransportException
335
     * @return resource
336
     *
337
     */
338
    protected function connect($host, $user, $pass)
339
    {
340
        $conn = ftp_connect($host);
341
        if (($conn === false) || (@ftp_login($conn, $user, $pass) === false)) {
342
            throw new TransportException(
343
                is_resource($conn) ? 'Could not login to FTP' : 'Could not make FTP connection'
344
            );
345
        }
346
347
        return $conn;
348
    }
349
350
    /**
351
     * Closes shared ftp connection.
352
     */
353
    protected function closeFtpConnection()
354
    {
355
        if (is_resource($this->ftpConnection)) {
356
            ftp_close($this->ftpConnection);
357
        }
358
359
        $this->ftpConnection = null;
360
    }
361
362
    /**
363
     * @return MatcherInterface
364
     */
365
    protected function createFileMatcher()
366
    {
367
        $file = $this->connection['file'];
368
369
        // see if a pattern is used
370
        if ($pattern = $this->getPattern()) {
0 ignored issues
show
Unused Code introduced by
$pattern is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
371
            return new PatternMatcher($file);
372
        }
373
374
        // see if globbing is used
375
        if (false !== strpos($file, '*')) {
376
            return new GlobMatcher($file);
377
        }
378
379
        // just a regular file matcher
380
        return new FileMatcher($file);
381
    }
382
}
383