Completed
Push — master ( 782271...5855e7 )
by Robin
01:45
created

Connection::getSocketUrl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

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