Passed
Push — develop ( 274774...6bd711 )
by Manuele
02:16
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 23
    public function setProtocolV2(bool $isProtocolV2): self
60
    {
61 23
        $this->protocolV2 = $isProtocolV2;
62
63 23
        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 23
    public function setToken(?string $token): self
84
    {
85 23
        $this->token = $token;
86
87 23
        return $this;
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     *
93
     * @throws NuVotifierException                 General NuVotifier Exception (an unknown exception)
94
     * @throws NuVotifierChallengeInvalidException NuVotifier says the challenge was invalid
95
     * @throws NuVotifierSignatureInvalidException NuVotifier says the signature was invalid
96
     * @throws NuVotifierUnknownServiceException   NuVotifier says that the service is unknown (the token doesn't belong
97
     *                                             to the service name)
98
     * @throws NuVotifierUsernameTooLongException  NuVotifier says the username is too long
99
     */
100 20
    public function sendVote(VoteInterface ...$votes): void
101
    {
102 20
        if (!$this->isProtocolV2()) {
103 1
            call_user_func_array([$this, 'parent::sendVote'], func_get_args());
104
105 1
            return;
106
        }
107
108
        // Check if all variables have been set, to create a connection
109 19
        $this->checkRequiredVariablesForSocket();
110
111 19
        foreach ($votes as $vote) {
112
            // Connect to the server
113 19
            $socket = $this->getSocket();
114 19
            $socket->open($this->getHost(), $this->getPort());
115
116
            // Check whether the connection really belongs to a NuVotifier plugin
117 19
            if (!$this->verifyConnection($header = $socket->read(64))) {
118 3
                throw new NotVotifierException();
119
            }
120
121
            // Extract the challenge
122 16
            $headerParts = explode(' ', $header);
123 16
            $challenge = mb_substr($headerParts[2], 0, -1);
124
125
            // Update the timestamp of the vote being sent
126 16
            $vote->setTimestamp(new DateTime());
127
128
            // Check if all variables have been set, to create a package
129 16
            $this->checkRequiredVariablesForPackage($vote);
130
131
            // Send the vote
132 8
            $socket->write($this->preparePackageV2($vote, $challenge));
133
134
            // Check if the vote was successful
135
            /*
136
             * https://github.com/NuVotifier/NuVotifier/blob/master/common/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2Decoder.java
137
             * Examples:
138
             * {"status":"ok"}
139
             * {"status":"error","cause":"CorruptedFrameException","error":"Challenge is not valid"}
140
             * {"status":"error","cause":"CorruptedFrameException","error":"Unknown service 'xxx'"}
141
             * {"status":"error","cause":"CorruptedFrameException","error":"Signature is not valid (invalid token?)"}
142
             * {"status":"error","cause":"CorruptedFrameException","error":"Username too long"} (over 16 characters)
143
             */
144 7
            $result = json_decode($socket->read(256));
145 6
            if ('ok' !== $result->status) {
146 5
                if ('Challenge is not valid' === $result->error) {
147 1
                    throw new NuVotifierChallengeInvalidException();
148 4
                } elseif (preg_match('/Unknown service \'(.*)\'/', $result->error, $matches)) {
149 1
                    throw new NuVotifierUnknownServiceException();
150 3
                } elseif ('Signature is not valid (invalid token?)' === $result->error) {
151 1
                    throw new NuVotifierSignatureInvalidException();
152 2
                } elseif ('Username too long' === $result->error) {
153 1
                    throw new NuVotifierUsernameTooLongException();
154
                }
155 1
                throw new NuVotifierException('Unknown NuVotifier Exception');
156
            }
157
158
            // Make sure to close the connection after package was sent
159 1
            $socket->__destruct();
160
        }
161 1
    }
162
163
    /**
164
     * Check that service name, username, address, timestamp and token have been set.
165
     *
166
     * @param VoteInterface $vote The vote to check
167
     *
168
     * @throws InvalidArgumentException If one required parameter wasn't set
169
     */
170 17
    protected function checkRequiredVariablesForPackage(VoteInterface $vote)
171
    {
172 17
        if (!$this->isProtocolV2()) {
173 1
            parent::checkRequiredVariablesForSocket();
174
175 1
            return;
176
        }
177
178
        if (
179 16
            null === $vote->getServiceName()
180 11
            || null === $vote->getUsername()
181 10
            || null === $vote->getAddress()
182 9
            || null === $vote->getTimestamp()
183 16
            || !isset($this->token)
184
        ) {
185 8
            $countError = 0;
186 8
            $errorMessage = '';
187
188 8
            if (null === $vote->getServiceName()) {
189 5
                $errorMessage .= 'The host variable wasn\'t set with "->setServiceName(...)".';
190 5
                ++$countError;
191
            }
192 8
            if (null === $vote->getUsername()) {
193 5
                $errorMessage .= $countError > 0 ? ' ' : '';
194 5
                $errorMessage .= 'The host variable wasn\'t set with "->setUsername(...)".';
195 5
                ++$countError;
196
            }
197 8
            if (null === $vote->getAddress()) {
198 6
                $errorMessage .= $countError > 0 ? ' ' : '';
199 6
                $errorMessage .= 'The host variable wasn\'t set with "->setAddress(...)".';
200 6
                ++$countError;
201
            }
202 8
            if (null === $vote->getTimestamp()) {
203 7
                $errorMessage .= $countError > 0 ? ' ' : '';
204 7
                $errorMessage .= 'The host variable wasn\'t set with "->setTimestamp(...)".';
205
            }
206 8
            if (!isset($this->token)) {
207 7
                $errorMessage .= $countError > 0 ? ' ' : '';
208 7
                $errorMessage .= 'The token variable wasn\'t set with "->setToken(...)".';
209
            }
210
211 8
            throw new InvalidArgumentException($errorMessage);
212
        }
213 8
    }
214
215
    /**
216
     * Verifies that the connection is correct.
217
     *
218
     * @param string|null $header The header that the plugin usually sends
219
     *
220
     * @return bool returns true if connections is available, otherwise false
221
     */
222 20
    protected function verifyConnection(?string $header): bool
223
    {
224 20
        $header_parts = explode(' ', $header);
225
        if (
226 20
            null === $header
227 20
            || false === mb_strpos($header, 'VOTIFIER')
228 20
            || 3 !== count($header_parts)
229
        ) {
230 3
            return false;
231
        }
232
233 17
        return true;
234
    }
235
236
    /**
237
     * Prepares the vote package to be sent as version 2 protocol package.
238
     *
239
     * @param VoteInterface $vote      The vote package with information
240
     * @param string        $challenge The challenge sent by the server
241
     *
242
     * @return string returns the string to be sent to the server
243
     */
244 8
    protected function preparePackageV2(VoteInterface $vote, string $challenge): string
245
    {
246 8
        $payloadJson = json_encode(
247
            [
248 8
                'username' => $vote->getUsername(),
249 8
                'serviceName' => $vote->getServiceName(),
250 8
                'timestamp' => $vote->getTimestamp(),
251 8
                'address' => $vote->getAddress(),
252 8
                'challenge' => $challenge,
253
            ]
254
        );
255 8
        $signature = base64_encode(hash_hmac('sha256', $payloadJson, $this->token, true));
256 8
        $messageJson = json_encode(['signature' => $signature, 'payload' => $payloadJson]);
257
258
        // 0x733a = s:
259 8
        return pack('nn', 0x733a, mb_strlen($messageJson)) . $messageJson;
260
    }
261
}
262