Skip to content

Commit f82e39f

Browse files
committed
Add isConnected() method to FtpConnection
1 parent 32faa07 commit f82e39f

File tree

4 files changed

+198
-55
lines changed

4 files changed

+198
-55
lines changed

ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ FTP protocol support for the XP Framework ChangeLog
33

44
## ?.?.? / ????-??-??
55

6+
* Added `isConnected()` method to FtpConnection class in order to detect
7+
and gracefully handle disconnects.
8+
* Added `timeout()`, `passive()`, `user` and `remoteEndpoint` accessors
9+
to FtpConnection class
10+
(@thekid)
11+
612
## 7.1.0 / 2016-08-29
713

814
* Added forward compatibility with XP 8.0.0 - @thekid

src/main/php/peer/ftp/FtpConnection.class.php

Lines changed: 76 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,24 @@
77
use peer\SSLSocket;
88
use peer\SocketException;
99
use util\log\Traceable;
10-
10+
use lang\IllegalArgumentException;
1111

1212
/**
1313
* FTP client
1414
*
1515
* Usage example:
16-
* <code>
17-
* $c= create(new FtpConnection('ftp://user:pass@example.com/'))->connect();
18-
*
19-
* // Retrieve root directory's listing
20-
* Console::writeLine($c->rootDir()->entries());
16+
* ```
17+
* $c= create(new FtpConnection('ftp://user:pass@example.com/'))->connect();
18+
*
19+
* // Retrieve root directory's listing
20+
* Console::writeLine($c->rootDir()->entries());
2121
*
22-
* $c->close();
23-
* </code>
22+
* $c->close();
23+
* ```
2424
*
25-
* @test xp://net.xp_framework.unittest.peer.ftp.IntegrationTest
26-
* @see rfc://959
27-
* @purpose FTP protocol implementation
25+
* @test xp://net.xp_framework.unittest.peer.ftp.FtpConnectionTest
26+
* @test xp://net.xp_framework.unittest.peer.ftp.IntegrationTest
27+
* @see rfc://959
2828
*/
2929
class FtpConnection extends \lang\Object implements Traceable {
3030
protected
@@ -39,31 +39,64 @@ class FtpConnection extends \lang\Object implements Traceable {
3939

4040
/**
4141
* Constructor. Accepts a DSN of the following form:
42-
* <pre>
43-
* {scheme}://[{user}:{password}@]{host}[:{port}]/[?{options}]
44-
* </pre>
42+
* `{scheme}://[{user}:{password}@]{host}[:{port}]/[?{options}]`
4543
*
4644
* Scheme is one of the following:
47-
* <ul>
48-
* <li>ftp (default)</li>
49-
* <li>ftps (with SSL)</li>
50-
* </ul>
45+
* - ftp (default)
46+
* - ftps (with SSL)
5147
*
5248
* Note: SSL connect is only available if OpenSSL support is enabled
5349
* into your version of PHP.
5450
*
5551
* Options include:
56-
* <ul>
57-
* <li>timeout - integer value indicating connection timeout in seconds, default: 4</li>
58-
* <li>passive - boolean value controlling whether to use passive mode or not</li>
59-
* </ul>
52+
* - timeout - integer value indicating connection timeout in seconds, default: 4
53+
* - passive - boolean value controlling whether to use passive mode or not, default: true
6054
*
61-
* @param string dsn
55+
* @param string|peer.URL $dsn
56+
* @throws lang.IllegalArgumentException if scheme is unsupported
6257
*/
6358
public function __construct($dsn) {
64-
$this->url= new URL($dsn);
59+
$this->url= $dsn instanceof URL ? $dsn : new URL($dsn);
60+
61+
switch ($this->url->getScheme()) {
62+
case 'ftp':
63+
$this->socket= new Socket($this->url->getHost(), $this->url->getPort(21));
64+
break;
65+
66+
case 'ftps':
67+
$this->socket= new SSLSocket($this->url->getHost(), $this->url->getPort(21));
68+
break;
69+
70+
default:
71+
throw new IllegalArgumentException('Unsupported scheme "'.$this->url->getScheme().'"');
72+
}
73+
74+
switch (strtolower($this->url->getParam('passive', 'false'))) {
75+
case 'true': case 'yes': case 'on': case '1':
76+
$this->setPassive(true);
77+
break;
78+
79+
case 'false': case 'no': case 'off': case '0':
80+
$this->setPassive(false);
81+
break;
82+
83+
default:
84+
throw new IllegalArgumentException('Unexpected value "'.$this->url->getParam('passive').'" for passive');
85+
}
6586
}
66-
87+
88+
/** @return string */
89+
public function user() { return $this->url->getUser(); }
90+
91+
/** @return peer.SocketEndpoint */
92+
public function remoteEndpoint() { return $this->socket->remoteEndpoint(); }
93+
94+
/** @return bool */
95+
public function passive() { return $this->passive; }
96+
97+
/** @return double */
98+
public function timeout() { return (double)$this->url->getParam('timeout', 4); }
99+
67100
/**
68101
* Connect (and log in, if necessary)
69102
*
@@ -73,47 +106,29 @@ public function __construct($dsn) {
73106
* @throws peer.SocketException for general I/O failures
74107
*/
75108
public function connect() {
76-
$host= $this->url->getHost();
77-
$port= $this->url->getPort(21);
78-
$timeout= $this->url->getParam('timeout', 4);
79-
80-
switch ($this->url->getScheme()) {
81-
case 'ftp':
82-
$this->socket= new Socket($host, $port);
83-
break;
84-
85-
case 'ftps':
86-
$this->socket= new SSLSocket($host, $port);
87-
break;
88-
}
89-
90-
$this->socket->connect($timeout);
109+
$this->socket->connect($this->timeout());
91110

92111
// Read banner message
93112
$this->expect($this->getResponse(), [220]);
94113

95114
// User & password
96-
if ($this->url->getUser()) {
115+
if (null !== ($user= $this->url->getUser())) {
97116
try {
98-
$this->expect($this->sendCommand('USER %s', $this->url->getUser()), [331]);
117+
$this->expect($this->sendCommand('USER %s', $user), [331]);
99118
$this->expect($this->sendCommand('PASS %s', $this->url->getPassword()), [230]);
100119
} catch (\peer\ProtocolException $e) {
101120
$this->socket->close();
102121
throw new AuthenticationException(sprintf(
103-
'Authentication failed for %s@%s (using password: %s): %s',
104-
$this->url->getUser(),
105-
$host,
122+
'Authentication failed for %s@%s:%d (using password: %s): %s',
123+
$this->url->getUser(),
124+
$this->url->getPort(21),
125+
$this->url->getHost(),
106126
$this->url->getPassword() ? 'yes' : 'no',
107127
$e->getMessage()
108128
), $this->url->getUser(), $this->url->getPassword());
109129
}
110130
}
111131

112-
// Set passive mode
113-
if (null !== ($pasv= $this->url->getParam('passive'))) {
114-
$this->setPassive((bool)$pasv);
115-
}
116-
117132
// Setup list parser
118133
$this->setupListParser();
119134

@@ -154,7 +169,16 @@ public function close() {
154169
}
155170
return true;
156171
}
157-
172+
173+
/**
174+
* Returns true if connection is established
175+
*
176+
* @return bool
177+
*/
178+
public function isConnected() {
179+
return $this->socket !== null && $this->socket->isConnected();
180+
}
181+
158182
/**
159183
* Retrieve transfer socket
160184
*
@@ -176,7 +200,6 @@ public function transferSocket() {
176200
*
177201
* @param bool enable enable or disable passive mode
178202
* @return bool success
179-
* @throws peer.SocketException
180203
*/
181204
public function setPassive($enable) {
182205
$this->passive= $enable;
@@ -187,9 +210,7 @@ public function setPassive($enable) {
187210
*
188211
* @return peer.ftp.FtpDir
189212
*/
190-
public function rootDir() {
191-
return $this->root;
192-
}
213+
public function rootDir() { return $this->root; }
193214

194215
/**
195216
* Read response
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php namespace peer\ftp\unittest;
2+
3+
use peer\ftp\FtpConnection;
4+
use peer\URL;
5+
use lang\IllegalArgumentException;
6+
use lang\FormatException;
7+
8+
class FtpConnectionTest extends \unittest\TestCase {
9+
10+
/** @return var[][] */
11+
private function dsns() {
12+
return [
13+
['ftp://localhost'],
14+
['ftp://localhost:21'],
15+
['ftp://test:test@localhost'],
16+
['ftp://test:test@localhost:21'],
17+
['ftp://localhost?passive=true'],
18+
['ftp://localhost?passive=false'],
19+
['ftp://localhost?timeout=1.0'],
20+
['ftp://localhost?passive=false&timeout=2.0'],
21+
['ftps://localhost']
22+
];
23+
}
24+
25+
#[@test, @values('dsns')]
26+
public function can_create($dsn) {
27+
new FtpConnection($dsn);
28+
}
29+
30+
#[@test, @values('dsns')]
31+
public function can_create_with_url($dsn) {
32+
new FtpConnection(new URL($dsn));
33+
}
34+
35+
#[@test, @expect(FormatException::class)]
36+
public function raises_error_for_malformed() {
37+
new FtpConnection('');
38+
}
39+
40+
#[@test, @expect(IllegalArgumentException::class)]
41+
public function raises_error_for_unsupported_scheme() {
42+
new FtpConnection('http://localhost');
43+
}
44+
45+
#[@test, @expect(IllegalArgumentException::class)]
46+
public function raises_error_for_unsupported_passive_mode() {
47+
new FtpConnection('ftp://localhost?passive=@INVALID@');
48+
}
49+
50+
#[@test]
51+
public function timeout_defaults_to_four_seconds() {
52+
$this->assertEquals(4.0, (new FtpConnection('ftp://localhost'))->timeout());
53+
}
54+
55+
#[@test]
56+
public function timeout_can_be_set_via_dsn() {
57+
$this->assertEquals(1.0, (new FtpConnection('ftp://localhost?timeout=1.0'))->timeout());
58+
}
59+
60+
#[@test]
61+
public function passive_mode_defaults_to_false() {
62+
$this->assertEquals(false, (new FtpConnection('ftp://localhost'))->passive());
63+
}
64+
65+
#[@test, @values([
66+
# ['false', false],
67+
# ['off', false],
68+
# ['no', false],
69+
# ['0', false],
70+
# ['true', true],
71+
# ['on', true],
72+
# ['yes', true],
73+
# ['1', true]
74+
#])]
75+
public function passive_mode_can_be_set_via_dsn($value, $result) {
76+
$this->assertEquals($result, (new FtpConnection('ftp://localhost?passive='.$value))->passive());
77+
}
78+
79+
#[@test]
80+
public function remote_endpoint() {
81+
$this->assertEquals('localhost:21', (new FtpConnection('ftp://localhost'))->remoteEndpoint()->getAddress());
82+
}
83+
84+
#[@test]
85+
public function remote_endpoint_with_non_default_port() {
86+
$this->assertEquals('localhost:2121', (new FtpConnection('ftp://localhost:2121'))->remoteEndpoint()->getAddress());
87+
}
88+
89+
#[@test]
90+
public function anonymous_user() {
91+
$this->assertNull((new FtpConnection('ftp://localhost'))->user());
92+
}
93+
94+
#[@test]
95+
public function authenticated_user() {
96+
$this->assertEquals('test', (new FtpConnection('ftp://test:pass@localhost'))->user());
97+
}
98+
}

src/test/php/peer/ftp/unittest/IntegrationTest.class.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,29 @@ public function tearDown() {
5757
$this->conn->close();
5858
}
5959

60+
#[@test]
61+
public function initially_not_connected() {
62+
$this->assertFalse($this->conn->isConnected());
63+
}
64+
6065
#[@test]
6166
public function connect() {
6267
$this->conn->connect();
6368
}
6469

70+
#[@test]
71+
public function is_connected_after_connect() {
72+
$this->conn->connect();
73+
$this->assertTrue($this->conn->isConnected());
74+
}
75+
76+
#[@test]
77+
public function is_no_longer_connected_after_close() {
78+
$this->conn->connect();
79+
$this->conn->close();
80+
$this->assertFalse($this->conn->isConnected());
81+
}
82+
6583
#[@test, @expect(AuthenticationException::class)]
6684
public function incorrect_credentials() {
6785
(new FtpConnection('ftp://test:INCORRECT@'.self::$bindAddress.'?timeout=1'))->connect();

0 commit comments

Comments
 (0)