Completed
Push — master ( 8aa334...f95c84 )
by Robin
02:03
created

Connection::callCommandHandlerForResponse()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 6.4984

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 23
ccs 6
cts 13
cp 0.4615
rs 8.7972
cc 4
eloc 12
nc 4
nop 2
crap 6.4984
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 ($buffer === false) {
139
                $this->disconnect();
140
                throw new RuntimeException('Incorrect data received from buffer');
141
            }
142
        }
143
144 3
        return Response::createFromString($buffer);
145
    }
146
147
    private function getMultiLineResponse(Response $response)
148
    {
149
        $lines = [];
150
151
        while (!$this->socket->eof()) {
152
            $line = $this->socket->gets(self::BUFFER_SIZE);
153
            if (substr($line, -2) !== "\r\n" || strlen($line) < 2) {
154
                continue;
155
            }
156
157
            // Remove CR LF from the end of the line.
158
            $line = substr($line, 0, -2);
159
160
            // Check if the line terminates the text response.
161
            if ($line === '.') {
162
                return new MultiLineResponse($response, array_filter($lines));
163
            }
164
165
            // If 1st char is '.' it's doubled (NNTP/RFC977 2.4.1).
166
            if (substr($line, 0, 2) === '..') {
167
                $line = substr($line, 1);
168
            }
169
170
            // Add the line to the array of lines.
171
            $lines[] = $line;
172
        }
173
    }
174
175
    private function getCompressedResponse(Response $response)
176
    {
177
        // Determine encoding by fetching first line.
178
        $line = $this->socket->gets(self::BUFFER_SIZE);
179
180
        if (substr($line, 0, 7) == '=ybegin') {
181
            $this->disconnect();
182
            throw new RuntimeException('yEnc encoded overviews are not currently supported.');
183
        }
184
185
        $uncompressed = '';
186
187
        while (!$this->socket->eof()) {
188
            $buffer = $this->socket->gets(self::BUFFER_SIZE);
189
190
            if (strlen($buffer) === 0) {
191
                $uncompressed = @gzuncompress($line);
192
193
                if ($uncompressed !== false) {
194
                    break;
195
                }
196
            }
197
198
            if ($buffer === false) {
199
                $this->disconnect();
200
                throw new RuntimeException('Incorrect data received from buffer');
201
            }
202
203
            $line .= $buffer;
204
        }
205
206
        $lines = explode("\r\n", trim($uncompressed));
207
        if (end($lines) === '.') {
208
            array_pop($lines);
209
        }
210
211
        return new MultiLineResponse($response, array_filter($lines));
212
    }
213
214 1
    private function callCommandHandlerForResponse(CommandInterface $command, ResponseInterface $response)
215
    {
216 1
        if (in_array($response->getStatusCode(), [Response::$codes['CommandUnknown'], Response::$codes['CommandUnavailable']])) {
217
            throw new RuntimeException('Sent command is either unknown or unavailable on server');
218
        }
219
220
        // Check if we received a response code that we're aware of.
221 1
        if (($responseName = array_search($response->getStatusCode(), Response::$codes, true)) === false) {
222
            throw new RuntimeException(sprintf(
223
                'Unexpected response received: [%d] %s',
224
                $response->getStatusCode(),
225
                $response->getMessage()
226
            ));
227
        }
228
229 1
        $responseHandlerMethod = 'on'.$responseName;
230
231 1
        if (!is_callable([$command, $responseHandlerMethod])) {
232
            throw new UnknownHandlerException(sprintf('Response handler (%s) is not a callable method on given command object', $responseHandlerMethod));
233
        }
234
235 1
        return call_user_func([$command, $responseHandlerMethod], $response);
236
    }
237
}
238