Completed
Pull Request — master (#18)
by
unknown
02:07
created

Connection::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 7
ccs 6
cts 6
cp 1
rs 9.4285
cc 1
eloc 5
nc 1
nop 4
crap 1
1
<?php
2
3
/*
4
 * This file is part of the NNTP library.
5
 *
6
 * (c) Robin van der Vleuten <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Rvdv\Nntp\Connection;
13
14
use Rvdv\Nntp\Command\CommandInterface;
15
use Rvdv\Nntp\Exception\InvalidArgumentException;
16
use Rvdv\Nntp\Exception\RuntimeException;
17
use Rvdv\Nntp\Response\MultiLineResponse;
18
use Rvdv\Nntp\Response\Response;
19
20
/**
21
 * @author Robin van der Vleuten <[email protected]>
22
 */
23
class Connection implements ConnectionInterface
24
{
25
    /**
26
     * @var string
27
     */
28
    private $host;
29
30
    /**
31
     * @var int
32
     */
33
    private $port;
34
35
    /**
36
     * @var bool
37
     */
38
    private $secure;
39
40
    /**
41
     * @var resource
42
     */
43
    private $socket;
44
45
    /**
46
     * @var int
47
     */
48
    private $timeout;
49
50
    /**
51
     * Constructor.
52
     *
53
     * @param string $host    The hostname of the NNTP server.
54
     * @param int    $port    The port of the NNTP server.
55
     * @param bool   $secure  A bool indicating if a secure connection should be established.
56
     * @param int    $timeout The socket timeout in seconds.
57
     */
58 2
    public function __construct($host, $port, $secure = false, $timeout = 15)
59
    {
60 2
        $this->host = $host;
61 2
        $this->port = $port;
62 2
        $this->secure = $secure;
63 2
        $this->timeout = $timeout;
64 2
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69 2
    public function connect()
70
    {
71 2
        $address = gethostbyname($this->host);
72 2
        $url = $this->getSocketUrl($address);
73
74 2
        if (!$this->socket = @stream_socket_client($url, $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT)) {
75 1
            throw new RuntimeException(sprintf('Connection to %s:%d failed: %s', $address, $this->port, $errstr), $errno);
76
        }
77
78 1
        if ($this->secure) {
79
            stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
80
        }
81
82 1
        stream_set_blocking($this->socket, 0);
83
84 1
        return $this->getResponse();
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function disconnect()
91
    {
92
        if (is_resource($this->socket)) {
93
            stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
94
95
            return fclose($this->socket);
96
        }
97
98
        return false;
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104
    public function sendCommand(CommandInterface $command)
105
    {
106
        $commandString = $command->execute();
107
108
        // NNTP/RFC977 only allows command up to 512 (-2 \r\n) chars.
109
        if (!strlen($commandString) > 510) {
110
            throw new InvalidArgumentException('Failed to write to socket: command exceeded 510 characters');
111
        }
112
113
        if (!@fwrite($this->socket, $commandString."\r\n")) {
114
            throw new RuntimeException('Failed to write to socket');
115
        }
116
117
        $response = $this->getResponse();
118
119
        if ($command->isMultiLine()) {
120
            $response = $command->isCompressed() ? $this->getCompressedResponse($response) : $this->getMultiLineResponse($response);
121
        }
122
123
        if (in_array($response->getStatusCode(), array(Response::COMMAND_UNKNOWN, Response::COMMAND_UNAVAILABLE))) {
124
            throw new RuntimeException('Sent command is either unknown or unavailable on server');
125
        }
126
127
        $expectedResponseCodes = $command->getExpectedResponseCodes();
128
129
        // Check if we received a response expected by the command.
130
        if (!isset($expectedResponseCodes[$response->getStatusCode()])) {
131
            throw new RuntimeException(sprintf(
132
                'Unexpected response received: [%d] %s',
133
                $response->getStatusCode(),
134
                $response->getMessage()
135
            ));
136
        }
137
138
        $expectedResponseHandler = $expectedResponseCodes[$response->getStatusCode()];
139
        if (!is_callable(array($command, $expectedResponseHandler))) {
140
            throw new RuntimeException(sprintf('Response handler (%s) is not callable method on given command object', $expectedResponseHandler));
141
        }
142
143
        $command->setResponse($response);
144
        $command->$expectedResponseHandler($response);
145
146
        return $command;
147
    }
148
    
149
    public function sendArticle(CommandInterface $command)
150
    {
151
        $commandString = $command->execute();
152
153
        if (!@fwrite($this->socket, $commandString."\r\n.\r\n")) {
154
            throw new RuntimeException('Failed to write to socket');
155
        }
156
157
        $response = $this->getResponse();
158
159
        $expectedResponseCodes = $command->getExpectedResponseCodes();
160
161
        // Check if we received a response expected by the command.
162
        if (!isset($expectedResponseCodes[$response->getStatusCode()])) {
163
            throw new RuntimeException(sprintf(
164
                'Unexpected response received: [%d] %s',
165
                $response->getStatusCode(),
166
                $response->getMessage()
167
            ));
168
        }
169
170
        $expectedResponseHandler = $expectedResponseCodes[$response->getStatusCode()];
171
        if (!is_callable(array($command, $expectedResponseHandler))) {
172
            throw new RuntimeException(sprintf('Response handler (%s) is not callable method on given command object', $expectedResponseHandler));
173
        }
174
175
        $command->setResponse($response);
176
        $command->$expectedResponseHandler($response);
177
178
        return $command;
179
    }
180
181 1
    protected function getResponse()
182
    {
183 1
        $buffer = '';
184
185 1
        while (!feof($this->socket)) {
186 1
            $buffer .= @fgets($this->socket, 1024);
187
188 1
            if ("\r\n" === substr($buffer, -2)) {
189 1
                break;
190
            }
191
192 1
            if ($buffer === false) {
193
                $this->disconnect();
194
                throw new RuntimeException('Incorrect data received from buffer');
195
            }
196 1
        }
197
198 1
        return Response::createFromString($buffer);
199
    }
200
201
    public function getMultiLineResponse(Response $response)
202
    {
203
        $buffer = '';
204
205
        while (!feof($this->socket)) {
206
            $buffer .= @fgets($this->socket, 1024);
207
208
            if ("\n.\r\n" === substr($buffer, -4)) {
209
                break;
210
            }
211
212
            if ($buffer === false) {
213
                $this->disconnect();
214
                throw new RuntimeException('Incorrect data received from buffer');
215
            }
216
        }
217
218
        $lines = explode("\r\n", trim($buffer));
219
        if (end($lines) === '.') {
220
            array_pop($lines);
221
        }
222
223
        $lines = array_filter($lines);
224
        $lines = \SplFixedArray::fromArray($lines);
225
226
        return new MultiLineResponse($response, $lines);
227
    }
228
229
    public function getCompressedResponse(Response $response)
230
    {
231
        // Determine encoding by fetching first line.
232
        $line = @fread($this->socket, 1024);
233
234
        $uncompressed = '';
235
236
        while (!feof($this->socket)) {
237
            $buffer = @fread($this->socket, 32768);
238
239
            if (strlen($buffer) === 0) {
240
                $uncompressed = @gzuncompress($line);
241
242
                if ($uncompressed !== false) {
243
                    break;
244
                }
245
            }
246
247
            if ($buffer === false) {
248
                $this->disconnect();
249
                throw new RuntimeException('Incorrect data received from buffer');
250
            }
251
252
            $line .= $buffer;
253
        }
254
255
        $lines = explode("\r\n", trim($uncompressed));
256
        if (end($lines) === '.') {
257
            array_pop($lines);
258
        }
259
260
        $lines = array_filter($lines);
261
        $lines = \SplFixedArray::fromArray($lines);
262
263
        return new MultiLineResponse($response, $lines);
264
    }
265
266
    /**
267
     * @param string $address
268
     */
269 2
    protected function getSocketUrl($address)
270
    {
271 2
        if (strpos($address, ':') !== false) {
272
            // enclose IPv6 addresses in square brackets before appending port
273
            $address = '['.$address.']';
274
        }
275
276 2
        return sprintf('tcp://%s:%s', $address, $this->port);
277
    }
278
}
279