Completed
Push — master ( 5679e8...cbad80 )
by Robin
35s queued 14s
created

Connection   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 235
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 15.32%

Importance

Changes 10
Bugs 0 Features 0
Metric Value
wmc 36
c 10
b 0
f 0
lcom 1
cbo 7
dl 0
loc 235
ccs 17
cts 111
cp 0.1532
rs 8.8

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 2
A connect() 0 10 2
A disconnect() 0 4 1
D sendCommand() 0 44 10
B sendArticle() 0 31 4
A getResponse() 0 19 4
B getMultiLineResponse() 0 30 6
C getCompressedResponse() 0 41 7
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
     * Constructor.
51
     *
52
     * @param string          $host   The host of the NNTP server.
53
     * @param int             $port   The port of the NNTP server.
54
     * @param bool            $secure A bool indicating if a secure connection should be established.
55
     * @param SocketInterface $socket An optional socket wrapper instance.
56
     */
57 2
    public function __construct($host, $port, $secure = false, SocketInterface $socket = null)
58
    {
59 2
        $this->host = $host;
60 2
        $this->port = $port;
61 2
        $this->secure = $secure;
62 2
        $this->socket = $socket ?: new Socket();
63 2
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68 2
    public function connect()
69
    {
70 2
        $this->socket->connect(sprintf('tcp://%s:%d', $this->host, $this->port));
71
72 1
        if ($this->secure) {
73
            $this->socket->enableCrypto(true);
74
        }
75
76 1
        return $this->getResponse();
77
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82
    public function disconnect()
83
    {
84
        $this->socket->disconnect();
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function sendCommand(CommandInterface $command)
91
    {
92
        $commandString = $command->execute();
93
94
        // NNTP/RFC977 only allows command up to 512 (-2 \r\n) chars.
95
        if (!strlen($commandString) > 510) {
96
            throw new InvalidArgumentException('Failed to write to socket: command exceeded 510 characters');
97
        }
98
99
        if (strlen($commandString."\r\n") !== $this->socket->write($commandString."\r\n")) {
100
            throw new RuntimeException('Failed to write to socket');
101
        }
102
103
        $response = $this->getResponse();
104
105
        if ($command->isMultiLine() && ($response->getStatusCode() >= 200 && $response->getStatusCode() <= 399)) {
106
            $response = $command->isCompressed() ? $this->getCompressedResponse($response) : $this->getMultiLineResponse($response);
107
        }
108
109
        if (in_array($response->getStatusCode(), [Response::COMMAND_UNKNOWN, Response::COMMAND_UNAVAILABLE])) {
110
            throw new RuntimeException('Sent command is either unknown or unavailable on server');
111
        }
112
113
        $expectedResponseCodes = $command->getExpectedResponseCodes();
114
115
        // Check if we received a response expected by the command.
116
        if (!isset($expectedResponseCodes[$response->getStatusCode()])) {
117
            throw new RuntimeException(sprintf(
118
                'Unexpected response received: [%d] %s',
119
                $response->getStatusCode(),
120
                $response->getMessage()
121
            ));
122
        }
123
124
        $expectedResponseHandler = $expectedResponseCodes[$response->getStatusCode()];
125
        if (!is_callable([$command, $expectedResponseHandler])) {
126
            throw new RuntimeException(sprintf('Response handler (%s) is not callable method on given command object', $expectedResponseHandler));
127
        }
128
129
        $command->setResponse($response);
0 ignored issues
show
Bug introduced by
It seems like $response defined by $command->isCompressed()...LineResponse($response) on line 106 can also be of type null; however, Rvdv\Nntp\Command\CommandInterface::setResponse() does only seem to accept object<Rvdv\Nntp\Response\ResponseInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
130
        $command->$expectedResponseHandler($response);
131
132
        return $command;
133
    }
134
135
    public function sendArticle(CommandInterface $command)
136
    {
137
        $commandString = $command->execute();
138
139
        if (strlen($commandString."\r\n.\r\n") !== $this->socket->write($commandString."\r\n.\r\n")) {
140
            throw new RuntimeException('Failed to write to socket');
141
        }
142
143
        $response = $this->getResponse();
144
145
        $expectedResponseCodes = $command->getExpectedResponseCodes();
146
147
        // Check if we received a response expected by the command.
148
        if (!isset($expectedResponseCodes[$response->getStatusCode()])) {
149
            throw new RuntimeException(sprintf(
150
                'Unexpected response received: [%d] %s',
151
                $response->getStatusCode(),
152
                $response->getMessage()
153
            ));
154
        }
155
156
        $expectedResponseHandler = $expectedResponseCodes[$response->getStatusCode()];
157
        if (!is_callable([$command, $expectedResponseHandler])) {
158
            throw new RuntimeException(sprintf('Response handler (%s) is not callable method on given command object', $expectedResponseHandler));
159
        }
160
161
        $command->setResponse($response);
162
        $command->$expectedResponseHandler($response);
163
164
        return $command;
165
    }
166
167 1
    protected function getResponse()
168
    {
169 1
        $buffer = '';
170
171 1
        while (!$this->socket->eof()) {
172 1
            $buffer .= $this->socket->gets(self::BUFFER_SIZE);
173
174 1
            if ("\r\n" === substr($buffer, -2)) {
175 1
                break;
176
            }
177
178
            if ($buffer === false) {
179
                $this->disconnect();
180
                throw new RuntimeException('Incorrect data received from buffer');
181
            }
182
        }
183
184 1
        return Response::createFromString($buffer);
185
    }
186
187
    public function getMultiLineResponse(Response $response)
188
    {
189
        $lines = [];
190
191
        while (!$this->socket->eof()) {
192
            $line = $this->socket->gets(self::BUFFER_SIZE);
193
            if (substr($line, -2) !== "\r\n" || strlen($line) < 2) {
194
                continue;
195
            }
196
197
            // Remove CR LF from the end of the line.
198
            $line = substr($line, 0, -2);
199
200
            // Check if the line terminates the text response.
201
            if ($line === '.') {
202
                // Return all previous lines.
203
                $lines = array_filter($lines);
204
                $lines = \SplFixedArray::fromArray($lines);
205
                return new MultiLineResponse($response, $lines);
206
            }
207
208
            // If 1st char is '.' it's doubled (NNTP/RFC977 2.4.1).
209
            if (substr($line, 0, 2) === '..') {
210
                $line = substr($line, 1);
211
            }
212
213
            // Add the line to the array of lines.
214
            $lines[] = $line;
215
        }
216
    }
217
218
    public function getCompressedResponse(Response $response)
219
    {
220
        // Determine encoding by fetching first line.
221
        $line = $this->socket->gets(self::BUFFER_SIZE);
222
223
        if (substr($line, 0, 7) == '=ybegin') {
224
            $this->disconnect();
225
            throw new RuntimeException('yEnc encoded overviews are not currently supported.');
226
        }
227
228
        $uncompressed = '';
229
230
        while (!$this->socket->eof()) {
231
            $buffer = $this->socket->gets(self::BUFFER_SIZE);
232
233
            if (strlen($buffer) === 0) {
234
                $uncompressed = @gzuncompress($line);
235
236
                if ($uncompressed !== false) {
237
                    break;
238
                }
239
            }
240
241
            if ($buffer === false) {
242
                $this->disconnect();
243
                throw new RuntimeException('Incorrect data received from buffer');
244
            }
245
246
            $line .= $buffer;
247
        }
248
249
        $lines = explode("\r\n", trim($uncompressed));
250
        if (end($lines) === '.') {
251
            array_pop($lines);
252
        }
253
254
        $lines = array_filter($lines);
255
        $lines = \SplFixedArray::fromArray($lines);
256
257
        return new MultiLineResponse($response, $lines);
258
    }
259
}
260