Completed
Push — master ( 3b1e1b...78e928 )
by Robin
02:21
created

Connection   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 234
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 17.27%

Importance

Changes 34
Bugs 9 Features 2
Metric Value
wmc 34
c 34
b 9
f 2
lcom 1
cbo 7
dl 0
loc 234
ccs 19
cts 110
cp 0.1727
rs 9.2

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 2
A connect() 0 12 2
A disconnect() 0 4 1
D sendCommand() 0 44 10
B sendArticle() 0 31 4
A getResponse() 0 19 4
B getMultiLineResponse() 0 27 5
B getCompressedResponse() 0 36 6
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 Rvdv\Nntp\Socket\Socket;
20
use Rvdv\Nntp\Socket\SocketInterface;
21
22
/**
23
 * @author Robin van der Vleuten <[email protected]>
24
 */
25
class Connection implements ConnectionInterface
26
{
27
    const BUFFER_SIZE = 1024;
28
29
    /**
30
     * @var string
31
     */
32
    private $host;
33
34
    /**
35
     * @var int
36
     */
37
    private $port;
38
39
    /**
40
     * @var bool
41
     */
42
    private $secure;
43
44
    /**
45
     * @var SocketInterface
46
     */
47
    private $socket;
48
49
    /**
50
     * @var int
51
     */
52
    private $timeout;
0 ignored issues
show
Unused Code introduced by
The property $timeout is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
53
54
    /**
55
     * Constructor.
56
     *
57
     * @param string          $host    The host of the NNTP server.
58
     * @param int             $port    The port of the NNTP server.
59
     * @param bool            $secure  A bool indicating if a secure connection should be established.
60
     * @param SocketInterface $socket  An optional socket wrapper instance.
61
     */
62 2
    public function __construct($host, $port, $secure = false, SocketInterface $socket = null)
63
    {
64 2
        $this->host = $host;
65 2
        $this->port = $port;
66 2
        $this->secure = $secure;
67 2
        $this->socket = $socket ?: new Socket();
68 2
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73 2
    public function connect()
74
    {
75 2
        $this->socket
76 2
            ->connect(sprintf('tcp://%s:%d', $this->host, $this->port), 1.0)
77 1
            ->setBlocking(true);
78
79 1
        if ($this->secure) {
80
            $this->socket->enableCrypto(true);
81
        }
82
83 1
        return $this->getResponse();
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function disconnect()
90
    {
91
        $this->socket->disconnect();
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97
    public function sendCommand(CommandInterface $command)
98
    {
99
        $commandString = $command->execute();
100
101
        // NNTP/RFC977 only allows command up to 512 (-2 \r\n) chars.
102
        if (!strlen($commandString) > 510) {
103
            throw new InvalidArgumentException('Failed to write to socket: command exceeded 510 characters');
104
        }
105
106
        if (strlen($commandString."\r\n") !== $this->socket->write($commandString."\r\n")) {
107
            throw new RuntimeException('Failed to write to socket');
108
        }
109
110
        $response = $this->getResponse();
111
112
        if ($command->isMultiLine() && ($response->getStatusCode() >= 200 && $response->getStatusCode() <= 399)) {
113
            $response = $command->isCompressed() ? $this->getCompressedResponse($response) : $this->getMultiLineResponse($response);
114
        }
115
116
        if (in_array($response->getStatusCode(), [Response::COMMAND_UNKNOWN, Response::COMMAND_UNAVAILABLE])) {
117
            throw new RuntimeException('Sent command is either unknown or unavailable on server');
118
        }
119
120
        $expectedResponseCodes = $command->getExpectedResponseCodes();
121
122
        // Check if we received a response expected by the command.
123
        if (!isset($expectedResponseCodes[$response->getStatusCode()])) {
124
            throw new RuntimeException(sprintf(
125
                'Unexpected response received: [%d] %s',
126
                $response->getStatusCode(),
127
                $response->getMessage()
128
            ));
129
        }
130
131
        $expectedResponseHandler = $expectedResponseCodes[$response->getStatusCode()];
132
        if (!is_callable([$command, $expectedResponseHandler])) {
133
            throw new RuntimeException(sprintf('Response handler (%s) is not callable method on given command object', $expectedResponseHandler));
134
        }
135
136
        $command->setResponse($response);
137
        $command->$expectedResponseHandler($response);
138
139
        return $command;
140
    }
141
142
    public function sendArticle(CommandInterface $command)
143
    {
144
        $commandString = $command->execute();
145
146
        if (strlen($commandString."\r\n.\r\n") !== $this->socket->write($commandString."\r\n.\r\n")) {
147
            throw new RuntimeException('Failed to write to socket');
148
        }
149
150
        $response = $this->getResponse();
151
152
        $expectedResponseCodes = $command->getExpectedResponseCodes();
153
154
        // Check if we received a response expected by the command.
155
        if (!isset($expectedResponseCodes[$response->getStatusCode()])) {
156
            throw new RuntimeException(sprintf(
157
                'Unexpected response received: [%d] %s',
158
                $response->getStatusCode(),
159
                $response->getMessage()
160
            ));
161
        }
162
163
        $expectedResponseHandler = $expectedResponseCodes[$response->getStatusCode()];
164
        if (!is_callable([$command, $expectedResponseHandler])) {
165
            throw new RuntimeException(sprintf('Response handler (%s) is not callable method on given command object', $expectedResponseHandler));
166
        }
167
168
        $command->setResponse($response);
169
        $command->$expectedResponseHandler($response);
170
171
        return $command;
172
    }
173
174 1
    protected function getResponse()
175
    {
176 1
        $buffer = '';
177
178 1
        while (!$this->socket->eof()) {
179 1
            $buffer .= $this->socket->gets(self::BUFFER_SIZE);
180
181 1
            if ("\r\n" === substr($buffer, -2)) {
182 1
                break;
183
            }
184
185
            if ($buffer === false) {
186
                $this->disconnect();
187
                throw new RuntimeException('Incorrect data received from buffer');
188
            }
189
        }
190
191 1
        return Response::createFromString($buffer);
192
    }
193
194
    public function getMultiLineResponse(Response $response)
195
    {
196
        $buffer = '';
197
198
        while (!$this->socket->eof()) {
199
            $buffer .= $this->socket->gets(self::BUFFER_SIZE);
200
201
            if ("\n.\r\n" === substr($buffer, -4)) {
202
                break;
203
            }
204
205
            if ($buffer === false) {
206
                $this->disconnect();
207
                throw new RuntimeException('Incorrect data received from buffer');
208
            }
209
        }
210
211
        $lines = explode("\r\n", trim($buffer));
212
        if (end($lines) === '.') {
213
            array_pop($lines);
214
        }
215
216
        $lines = array_filter($lines);
217
        $lines = \SplFixedArray::fromArray($lines);
218
219
        return new MultiLineResponse($response, $lines);
220
    }
221
222
    public function getCompressedResponse(Response $response)
223
    {
224
        // Determine encoding by fetching first line.
225
        $line = $this->socket->gets();
226
227
        $uncompressed = '';
228
229
        while (!$this->socket->eof()) {
230
            $buffer = $this->socket->gets(self::BUFFER_SIZE);
231
232
            if (strlen($buffer) === 0) {
233
                $uncompressed = @gzuncompress($line);
234
235
                if ($uncompressed !== false) {
236
                    break;
237
                }
238
            }
239
240
            if ($buffer === false) {
241
                $this->disconnect();
242
                throw new RuntimeException('Incorrect data received from buffer');
243
            }
244
245
            $line .= $buffer;
246
        }
247
248
        $lines = explode("\r\n", trim($uncompressed));
249
        if (end($lines) === '.') {
250
            array_pop($lines);
251
        }
252
253
        $lines = array_filter($lines);
254
        $lines = \SplFixedArray::fromArray($lines);
255
256
        return new MultiLineResponse($response, $lines);
257
    }
258
}
259