Connection   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 214
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 39.36%

Importance

Changes 11
Bugs 0 Features 0
Metric Value
wmc 35
c 11
b 0
f 0
lcom 1
cbo 9
dl 0
loc 214
ccs 37
cts 94
cp 0.3936
rs 9

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 2
A sendArticle() 0 12 2
A connect() 0 10 2
A disconnect() 0 4 1
B sendCommand() 0 21 7
A getResponse() 0 20 4
B getMultiLineResponse() 0 27 6
C getCompressedResponse() 0 40 7
B callCommandHandlerForResponse() 0 23 4
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\Exception\UnknownHandlerException;
18
use Rvdv\Nntp\Response\MultiLineResponse;
19
use Rvdv\Nntp\Response\Response;
20
use Rvdv\Nntp\Response\ResponseInterface;
21
use Rvdv\Nntp\Socket\Socket;
22
use Rvdv\Nntp\Socket\SocketInterface;
23
24
/**
25
 * @author Robin van der Vleuten <[email protected]>
26
 */
27
class Connection implements ConnectionInterface
28
{
29
    const BUFFER_SIZE = 1024;
30
31
    /**
32
     * @var string
33
     */
34
    private $host;
35
36
    /**
37
     * @var int
38
     */
39
    private $port;
40
41
    /**
42
     * @var bool
43
     */
44
    private $secure;
45
46
    /**
47
     * @var SocketInterface
48
     */
49
    private $socket;
50
51
    /**
52
     * Constructor.
53
     *
54
     * @param string          $host   the host of the NNTP server
55
     * @param int             $port   the port of the NNTP server
56
     * @param bool            $secure a bool indicating if a secure connection should be established
57
     * @param SocketInterface $socket an optional socket wrapper instance
58
     */
59 7
    public function __construct($host, $port, $secure = false, SocketInterface $socket = null)
60
    {
61 7
        $this->host = $host;
62 7
        $this->port = $port;
63 7
        $this->secure = $secure;
64 7
        $this->socket = $socket ?: new Socket();
65 7
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70 3
    public function connect()
71
    {
72 3
        $this->socket->connect(sprintf('tcp://%s:%d', $this->host, $this->port));
73
74 2
        if ($this->secure) {
75 1
            $this->socket->enableCrypto(true);
76 1
        }
77
78 2
        return $this->getResponse();
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84 1
    public function disconnect()
85
    {
86 1
        $this->socket->disconnect();
87 1
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92 3
    public function sendCommand(CommandInterface $command)
93
    {
94 3
        $commandString = $command();
95
96
        // NNTP/RFC977 only allows command up to 512 (-2 \r\n) chars.
97 3
        if (strlen($commandString) > 510) {
98 1
            throw new InvalidArgumentException('Failed to write to socket: command exceeded 510 characters');
99
        }
100
101 2
        if (strlen($commandString."\r\n") !== $this->socket->write($commandString."\r\n")) {
102 1
            throw new RuntimeException('Failed to write to socket');
103
        }
104
105 1
        $response = $this->getResponse();
106
107 1
        if ($command->isMultiLine() && ($response->getStatusCode() >= 200 && $response->getStatusCode() <= 399)) {
108
            $response = $command->isCompressed() ? $this->getCompressedResponse($response) : $this->getMultiLineResponse($response);
109
        }
110
111 1
        return $this->callCommandHandlerForResponse($command, $response);
0 ignored issues
show
Bug introduced by
It seems like $response defined by $command->isCompressed()...LineResponse($response) on line 108 can also be of type null; however, Rvdv\Nntp\Connection\Con...andHandlerForResponse() 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...
112
    }
113
114
    public function sendArticle(CommandInterface $command)
115
    {
116
        $commandString = $command();
117
118
        if (strlen($commandString."\r\n.\r\n") !== $this->socket->write($commandString."\r\n.\r\n")) {
119
            throw new RuntimeException('Failed to write to socket');
120
        }
121
122
        $response = $this->getResponse();
123
124
        return $this->callCommandHandlerForResponse($command, $response);
125
    }
126
127 3
    private function getResponse()
128
    {
129 3
        $buffer = '';
130
131 3
        while (!$this->socket->eof()) {
132 3
            $buffer .= $this->socket->gets(self::BUFFER_SIZE);
133
134 3
            if ("\r\n" === substr($buffer, -2)) {
135 3
                break;
136
            }
137
138
            if (false === $buffer) {
139
                $this->disconnect();
140
141
                throw new RuntimeException('Incorrect data received from buffer');
142
            }
143
        }
144
145 3
        return Response::createFromString($buffer);
146
    }
147
148
    private function getMultiLineResponse(Response $response)
149
    {
150
        $lines = [];
151
152
        while (!$this->socket->eof()) {
153
            $line = $this->socket->gets(self::BUFFER_SIZE);
154
            if ("\r\n" !== substr($line, -2) || strlen($line) < 2) {
155
                continue;
156
            }
157
158
            // Remove CR LF from the end of the line.
159
            $line = substr($line, 0, -2);
160
161
            // Check if the line terminates the text response.
162
            if ('.' === $line) {
163
                return new MultiLineResponse($response, $lines);
164
            }
165
166
            // If 1st char is '.' it's doubled (NNTP/RFC977 2.4.1).
167
            if ('..' === substr($line, 0, 2)) {
168
                $line = substr($line, 1);
169
            }
170
171
            // Add the line to the array of lines.
172
            $lines[] = $line;
173
        }
174
    }
175
176
    private function getCompressedResponse(Response $response)
177
    {
178
        // Determine encoding by fetching first line.
179
        $line = $this->socket->gets(self::BUFFER_SIZE);
180
181
        if ('=ybegin' == substr($line, 0, 7)) {
182
            $this->disconnect();
183
184
            throw new RuntimeException('yEnc encoded overviews are not currently supported.');
185
        }
186
187
        $uncompressed = '';
188
189
        while (!$this->socket->eof()) {
190
            $buffer = $this->socket->gets(self::BUFFER_SIZE);
191
192
            if (0 === strlen($buffer)) {
193
                $uncompressed = @gzuncompress($line);
194
195
                if (false !== $uncompressed) {
196
                    break;
197
                }
198
            }
199
200
            if (false === $buffer) {
201
                $this->disconnect();
202
203
                throw new RuntimeException('Incorrect data received from buffer');
204
            }
205
206
            $line .= $buffer;
207
        }
208
209
        $lines = explode("\r\n", trim($uncompressed));
210
        if ('.' === end($lines)) {
211
            array_pop($lines);
212
        }
213
214
        return new MultiLineResponse($response, array_filter($lines));
215
    }
216
217 1
    private function callCommandHandlerForResponse(CommandInterface $command, ResponseInterface $response)
218
    {
219 1
        if (in_array($response->getStatusCode(), [Response::$codes['CommandUnknown'], Response::$codes['CommandUnavailable']])) {
220
            throw new RuntimeException('Sent command is either unknown or unavailable on server');
221
        }
222
223
        // Check if we received a response code that we're aware of.
224 1
        if (false === ($responseName = array_search($response->getStatusCode(), Response::$codes, true))) {
225
            throw new RuntimeException(sprintf(
226
                'Unexpected response received: [%d] %s',
227
                $response->getStatusCode(),
228
                $response->getMessage()
229
            ));
230
        }
231
232 1
        $responseHandlerMethod = 'on'.$responseName;
233
234 1
        if (!is_callable([$command, $responseHandlerMethod])) {
235
            throw new UnknownHandlerException(sprintf('Response handler (%s) is not a callable method on given command object', $responseHandlerMethod));
236
        }
237
238 1
        return call_user_func([$command, $responseHandlerMethod], $response);
239
    }
240
}
241