Passed
Push — master ( 61f7fa...459725 )
by Manuele
05:14 queued 01:03
created

NuVotifier::sendVote()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 60
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 9

Importance

Changes 0
Metric Value
eloc 26
dl 0
loc 60
c 0
b 0
f 0
rs 8.0555
ccs 27
cts 27
cp 1
cc 9
nc 9
nop 1
crap 9

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Votifier PHP Client
5
 *
6
 * @package   VotifierClient
7
 * @author    Manuele Vaccari <[email protected]>
8
 * @copyright Copyright (c) 2017-2020 Manuele Vaccari <[email protected]>
9
 * @license   https://github.com/D3strukt0r/votifier-client-php/blob/master/LICENSE.txt GNU General Public License v3.0
10
 * @link      https://github.com/D3strukt0r/votifier-client-php
11
 */
12
13
namespace D3strukt0r\Votifier\Client\Server;
14
15
use D3strukt0r\Votifier\Client\Exception\NotVotifierException;
16
use D3strukt0r\Votifier\Client\Exception\NuVotifierChallengeInvalidException;
17
use D3strukt0r\Votifier\Client\Exception\NuVotifierException;
18
use D3strukt0r\Votifier\Client\Exception\NuVotifierSignatureInvalidException;
19
use D3strukt0r\Votifier\Client\Exception\NuVotifierUnknownServiceException;
20
use D3strukt0r\Votifier\Client\Exception\NuVotifierUsernameTooLongException;
21
use D3strukt0r\Votifier\Client\Vote\VoteInterface;
22
use DateTime;
23
use InvalidArgumentException;
24
25
use function count;
26
27
/**
28
 * The Class to access a server which uses the plugin "NuVotifier".
29
 */
30
class NuVotifier extends Votifier
31
{
32
    /**
33
     * @var bool use version 2 of the protocol
34
     */
35
    protected $protocolV2 = false;
36
37
    /**
38
     * @var string|null The token from the config.yml
39
     */
40
    protected $token;
41
42
    /**
43
     * Checks whether the connection uses the version 2 protocol.
44
     *
45
     * @return bool returns true, if using the new version of NuVotifier or false otherwise
46
     */
47 21
    public function isProtocolV2(): bool
48
    {
49 21
        return $this->protocolV2;
50
    }
51
52
    /**
53
     * Sets whether to use version 2 of the protocol.
54
     *
55
     * @param bool $isProtocolV2 Whether to use version 2 of the protocol
56
     *
57
     * @return $this returns the class itself, for doing multiple things at once
58
     */
59 27
    public function setProtocolV2(bool $isProtocolV2): self
60
    {
61 27
        $this->protocolV2 = $isProtocolV2;
62
63 27
        return $this;
64
    }
65
66
    /**
67
     * Gets the token from the config.yml.
68
     *
69
     * @return string|null returns The token from the config.yml
70
     */
71 1
    public function getToken(): ?string
72
    {
73 1
        return $this->token;
74
    }
75
76
    /**
77
     * Sets the token from the config.yml.
78
     *
79
     * @param string|null $token The token from the config.yml
80
     *
81
     * @return $this returns the class itself, for doing multiple things at once
82
     */
83 27
    public function setToken(?string $token): self
84
    {
85 27
        $this->token = $token;
86
87 27
        return $this;
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93 4
    public function verifyConnection(): void
94
    {
95
        // Check if all variables have been set, to create a connection
96 4
        $this->checkVariablesForSocket();
97
98
        // Connect to the server
99 4
        $socket = $this->getSocket();
100 4
        $socket->open($this->getHost(), $this->getPort());
101
102
        // Check whether the connection really belongs to a NuVotifier plugin
103 4
        if (!$this->verifyConnectionHeader($socket->read(64))) {
104 3
            throw new NotVotifierException();
105
        }
106 1
    }
107
108
    /**
109
     * {@inheritdoc}
110
     *
111
     * @throws NuVotifierException                 General NuVotifier Exception (an unknown exception)
112
     * @throws NuVotifierChallengeInvalidException NuVotifier says the challenge was invalid
113
     * @throws NuVotifierSignatureInvalidException NuVotifier says the signature was invalid
114
     * @throws NuVotifierUnknownServiceException   NuVotifier says that the service is unknown (the token doesn't belong
115
     *                                             to the service name)
116
     * @throws NuVotifierUsernameTooLongException  NuVotifier says the username is too long
117
     */
118 20
    public function sendVote(VoteInterface ...$votes): void
119
    {
120 20
        if (!$this->isProtocolV2()) {
121 1
            call_user_func_array([$this, 'parent::sendVote'], func_get_args());
122
123 1
            return;
124
        }
125
126
        // Check if all variables have been set, to create a connection
127 19
        $this->checkVariablesForSocket();
128
129 19
        foreach ($votes as $vote) {
130
            // Connect to the server
131 19
            $socket = $this->getSocket();
132 19
            $socket->open($this->getHost(), $this->getPort());
133
134
            // Check whether the connection really belongs to a NuVotifier plugin
135 19
            if (!$this->verifyConnectionHeader($header = $socket->read(64))) {
136 3
                throw new NotVotifierException();
137
            }
138
139
            // Extract the challenge
140 16
            $headerParts = explode(' ', $header);
141 16
            $challenge = mb_substr($headerParts[2], 0, -1);
142
143
            // Update the timestamp of the vote being sent
144 16
            $vote->setTimestamp(new DateTime());
145
146
            // Check if all variables have been set, to create a package
147 16
            $this->checkVariablesForPackage($vote);
148
149
            // Send the vote
150 8
            $socket->write($this->preparePackageV2($vote, $challenge));
151
152
            // Check if the vote was successful
153
            /*
154
             * https://github.com/NuVotifier/NuVotifier/blob/master/common/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2Decoder.java
155
             * Examples:
156
             * {"status":"ok"}
157
             * {"status":"error","cause":"CorruptedFrameException","error":"Challenge is not valid"}
158
             * {"status":"error","cause":"CorruptedFrameException","error":"Unknown service 'xxx'"}
159
             * {"status":"error","cause":"CorruptedFrameException","error":"Signature is not valid (invalid token?)"}
160
             * {"status":"error","cause":"CorruptedFrameException","error":"Username too long"} (over 16 characters)
161
             */
162 7
            $result = json_decode($socket->read(256));
163 6
            if ('ok' !== $result->status) {
164 5
                if ('Challenge is not valid' === $result->error) {
165 1
                    throw new NuVotifierChallengeInvalidException();
166 4
                } elseif (preg_match('/Unknown service \'(.*)\'/', $result->error)) {
167 1
                    throw new NuVotifierUnknownServiceException();
168 3
                } elseif ('Signature is not valid (invalid token?)' === $result->error) {
169 1
                    throw new NuVotifierSignatureInvalidException();
170 2
                } elseif ('Username too long' === $result->error) {
171 1
                    throw new NuVotifierUsernameTooLongException();
172
                }
173 1
                throw new NuVotifierException('Unknown NuVotifier Exception');
174
            }
175
176
            // Make sure to close the connection after package was sent
177 1
            $socket->__destruct();
178
        }
179 1
    }
180
181
    /**
182
     * Check that service name, username, address, timestamp and token have been set.
183
     *
184
     * @param VoteInterface $vote The vote to check
185
     *
186
     * @throws InvalidArgumentException If one required parameter wasn't set
187
     */
188 17
    protected function checkVariablesForPackage(VoteInterface $vote)
189
    {
190 17
        if (!$this->isProtocolV2()) {
191 1
            parent::checkVariablesForSocket();
192
193 1
            return;
194
        }
195
196
        if (
197 16
            null === $vote->getServiceName()
198 11
            || null === $vote->getUsername()
199 10
            || null === $vote->getAddress()
200 9
            || null === $vote->getTimestamp()
201 16
            || !isset($this->token)
202
        ) {
203 8
            $countError = 0;
204 8
            $errorMessage = '';
205
206 8
            if (null === $vote->getServiceName()) {
207 5
                $errorMessage .= 'The host variable wasn\'t set with "->setServiceName(...)".';
208 5
                ++$countError;
209
            }
210 8
            if (null === $vote->getUsername()) {
211 5
                $errorMessage .= $countError > 0 ? ' ' : '';
212 5
                $errorMessage .= 'The host variable wasn\'t set with "->setUsername(...)".';
213 5
                ++$countError;
214
            }
215 8
            if (null === $vote->getAddress()) {
216 6
                $errorMessage .= $countError > 0 ? ' ' : '';
217 6
                $errorMessage .= 'The host variable wasn\'t set with "->setAddress(...)".';
218 6
                ++$countError;
219
            }
220 8
            if (null === $vote->getTimestamp()) {
221 7
                $errorMessage .= $countError > 0 ? ' ' : '';
222 7
                $errorMessage .= 'The host variable wasn\'t set with "->setTimestamp(...)".';
223
            }
224 8
            if (!isset($this->token)) {
225 7
                $errorMessage .= $countError > 0 ? ' ' : '';
226 7
                $errorMessage .= 'The token variable wasn\'t set with "->setToken(...)".';
227
            }
228
229 8
            throw new InvalidArgumentException($errorMessage);
230
        }
231 8
    }
232
233
    /**
234
     * Verifies that the connection is correct.
235
     *
236
     * @param string|null $header The header that the plugin usually sends
237
     *
238
     * @return bool returns true if connections is available, otherwise false
239
     */
240 24
    protected function verifyConnectionHeader(?string $header): bool
241
    {
242 24
        $header_parts = explode(' ', $header);
243
        if (
244 24
            null === $header
245 24
            || false === mb_strpos($header, 'VOTIFIER')
246 24
            || 3 !== count($header_parts)
247
        ) {
248 6
            return false;
249
        }
250
251 18
        return true;
252
    }
253
254
    /**
255
     * Prepares the vote package to be sent as version 2 protocol package.
256
     *
257
     * @param VoteInterface $vote      The vote package with information
258
     * @param string        $challenge The challenge sent by the server
259
     *
260
     * @return string returns the string to be sent to the server
261
     */
262 8
    protected function preparePackageV2(VoteInterface $vote, string $challenge): string
263
    {
264 8
        $payloadJson = json_encode(
265
            [
266 8
                'username' => $vote->getUsername(),
267 8
                'serviceName' => $vote->getServiceName(),
268 8
                'timestamp' => $vote->getTimestamp(),
269 8
                'address' => $vote->getAddress(),
270 8
                'challenge' => $challenge,
271
            ]
272
        );
273 8
        $signature = base64_encode(hash_hmac('sha256', $payloadJson, $this->token, true));
274 8
        $messageJson = json_encode(['signature' => $signature, 'payload' => $payloadJson]);
275
276
        // 0x733a = s:
277 8
        return pack('nn', 0x733a, mb_strlen($messageJson)) . $messageJson;
278
    }
279
}
280