Completed
Push — master ( 67a981...19aadf )
by Robin
04:39 queued 02:52
created

Connection::callCommandHandlerForResponse()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 23
ccs 0
cts 13
cp 0
rs 8.7972
cc 4
eloc 12
nc 4
nop 2
crap 20
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 2
    public function __construct($host, $port, $secure = false, SocketInterface $socket = null)
60
    {
61 2
        $this->host = $host;
62 2
        $this->port = $port;
63 2
        $this->secure = $secure;
64 2
        $this->socket = $socket ?: new Socket();
65 2
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70 2
    public function connect()
71
    {
72 2
        $this->socket->connect(sprintf('tcp://%s:%d', $this->host, $this->port));
73
74 1
        if ($this->secure) {
75
            $this->socket->enableCrypto(true);
76
        }
77
78 1
        return $this->getResponse();
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84
    public function disconnect()
85
    {
86
        $this->socket->disconnect();
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92
    public function sendCommand(CommandInterface $command)
93
    {
94
        $commandString = $command();
95
96
        // NNTP/RFC977 only allows command up to 512 (-2 \r\n) chars.
97
        if (!strlen($commandString) > 510) {
98
            throw new InvalidArgumentException('Failed to write to socket: command exceeded 510 characters');
99
        }
100
101
        if (strlen($commandString."\r\n") !== $this->socket->write($commandString."\r\n")) {
102
            throw new RuntimeException('Failed to write to socket');
103
        }
104
105
        $response = $this->getResponse();
106
107
        if ($command->isMultiLine() && ($response->getStatusCode() >= 200 && $response->getStatusCode() <= 399)) {
108
            $response = $command->isCompressed() ? $this->getCompressedResponse($response) : $this->getMultiLineResponse($response);
109
        }
110
111
        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->execute();
0 ignored issues
show
Bug introduced by
The method execute() does not seem to exist on object<Rvdv\Nntp\Command\CommandInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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 1
    private function getResponse()
128
    {
129 1
        $buffer = '';
130
131 1
        while (!$this->socket->eof()) {
132 1
            $buffer .= $this->socket->gets(self::BUFFER_SIZE);
133
134 1
            if ("\r\n" === substr($buffer, -2)) {
135 1
                break;
136
            }
137
138
            if ($buffer === false) {
139
                $this->disconnect();
140
                throw new RuntimeException('Incorrect data received from buffer');
141
            }
142
        }
143
144 1
        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
        $lines = array_filter($lines);
212
        $lines = \SplFixedArray::fromArray($lines);
213
214
        return new MultiLineResponse($response, $lines);
0 ignored issues
show
Documentation introduced by
$lines is of type object<SplFixedArray>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
215
    }
216
217
    private function callCommandHandlerForResponse(CommandInterface $command, ResponseInterface $response)
218
    {
219
        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
        if (($responseName = array_search($response->getStatusCode(), Response::$codes, true)) === false) {
225
            throw new RuntimeException(sprintf(
226
                'Unexpected response received: [%d] %s',
227
                $response->getStatusCode(),
228
                $response->getMessage()
229
            ));
230
        }
231
232
        $responseHandlerMethod = 'on'.$responseName;
233
234
        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
        return call_user_func([$command, $responseHandlerMethod], $response);
239
    }
240
}
241