Completed
Push — master ( db78ce...3a572f )
by Mr
01:28
created

Client   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 319
Duplicated Lines 4.39 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 41
lcom 1
cbo 5
dl 14
loc 319
rs 9.1199
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A config() 0 4 1
B encodeLength() 14 27 6
A write() 0 13 2
B read() 0 29 6
B parseResponse() 0 28 9
A login() 0 25 3
A connect() 0 27 4
A setSocket() 0 8 2
A getSocket() 0 6 2
A openSocket() 0 39 4
A closeSocket() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Client often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Client, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace RouterOS;
4
5
use RouterOS\Exceptions\Exception;
6
use RouterOS\Interfaces\ClientInterface;
7
use RouterOS\Interfaces\ConfigInterface;
8
use RouterOS\Interfaces\QueryInterface;
9
10
/**
11
 * Class Client
12
 * @package RouterOS
13
 * @since 0.1
14
 */
15
class Client implements Interfaces\ClientInterface
16
{
17
    /**
18
     * Socket resource
19
     * @var resource|null
20
     */
21
    private $_socket;
22
23
    /**
24
     * Code of error
25
     * @var int
26
     */
27
    private $_socket_err_num;
28
29
    /**
30
     * Description of socket error
31
     * @var string
32
     */
33
    private $_socket_err_str;
34
35
    /**
36
     * Configuration of connection
37
     * @var Config
38
     */
39
    private $_config;
40
41
    /**
42
     * Client constructor.
43
     * @param   ConfigInterface $config
44
     */
45
    public function __construct(ConfigInterface $config)
46
    {
47
        $this->_config = $config;
0 ignored issues
show
Documentation Bug introduced by
$config is of type object<RouterOS\Interfaces\ConfigInterface>, but the property $_config was declared to be of type object<RouterOS\Config>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
48
        $this->connect();
49
    }
50
51
    /**
52
     * Get some parameter from config
53
     *
54
     * @param   string $parameter
55
     * @return  mixed
56
     */
57
    private function config(string $parameter)
58
    {
59
        return $this->_config->get($parameter);
60
    }
61
62
    /**
63
     * Convert ordinary string to hex string
64
     *
65
     * @param   string $string
66
     * @return  string
67
     */
68
    private function encodeLength(string $string): string
69
    {
70
        // Yeah, that's insane, but was more ugly, you need read this post if you interesting a details:
71
        // https://wiki.mikrotik.com/wiki/Manual:API#API_words
72
        switch (true) {
73
            case ($string < 0x80):
74
                $string = \chr($string);
75
                break;
76
            case ($string < 0x4000):
77
                $string |= 0x8000;
78
                $string = \chr(($string >> 8) & 0xFF) . \chr($string & 0xFF);
79
                break;
80
            case ($string < 0x200000):
81
                $string |= 0xC00000;
82
                $string = \chr(($string >> 16) & 0xFF) . \chr(($string >> 8) & 0xFF) . \chr($string & 0xFF);
83
                break;
84 View Code Duplication
            case ($string < 0x10000000):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
85
                $string |= 0xE0000000;
86
                $string = \chr(($string >> 24) & 0xFF) . \chr(($string >> 16) & 0xFF) . \chr(($string >> 8) & 0xFF) . \chr($string & 0xFF);
87
                break;
88 View Code Duplication
            case ($string >= 0x10000000):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
89
                $string = \chr(0xF0) . \chr(($string >> 24) & 0xFF) . \chr(($string >> 16) & 0xFF) . \chr(($string >> 8) & 0xFF) . \chr($string & 0xFF);
90
                break;
91
        }
92
93
        return $string;
94
    }
95
96
    /**
97
     * Send write query to RouterOS (with or without tag)
98
     *
99
     * @param   QueryInterface $query
100
     * @return  ClientInterface
101
     */
102
    public function write(QueryInterface $query): ClientInterface
103
    {
104
        // Send commands via loop to router
105
        foreach ($query->getQuery() as $command) {
106
            $command = trim($command);
107
            fwrite($this->_socket, $this->encodeLength(\strlen($command)) . $command);
108
        }
109
110
        // Write zero-terminator
111
        fwrite($this->_socket, \chr(0));
112
113
        return $this;
114
    }
115
116
    /**
117
     * Read answer from server after query was executed
118
     *
119
     * @param   bool $parse
120
     * @return  array
121
     */
122
    public function read(bool $parse = true): array
123
    {
124
        // By default response is empty
125
        $response = [];
126
127
        // Read answer from socket in loop
128
        while (true) {
129
            // Read the first byte of input which gives us some or all of the length
130
            // of the remaining reply.
131
            $byte = \ord(fread($this->_socket, 1));
132
133
            // Save output line to response array
134
            $response[] = stream_get_contents($this->_socket, $byte);
135
136
            // If we get a !done line in response, change state of $isDone variable
137
            $isDone = ('!done' === end($response));
138
139
            // Get status about latest operation
140
            $status = stream_get_meta_data($this->_socket);
141
142
            // If we do not have unread bytes from socket or <-same and if done, then exit from loop
143
            if ((!$status['unread_bytes']) || (!$status['unread_bytes'] && $isDone)) {
144
                break;
145
            }
146
        }
147
148
        // Parse results and return
149
        return $parse ? $this->parseResponse($response) : $response;
150
    }
151
152
    /**
153
     * Parse response from Router OS
154
     *
155
     * @param   array $response Response data
156
     * @return  array Array with parsed data
157
     */
158
    private function parseResponse(array $response): array
159
    {
160
        $parsed = [];
161
        $single = null;
162
        foreach ($response as $item) {
163
            if (\in_array($item, ['!fatal', '!re', '!trap'])) {
164
                if ($item === '!re') {
165
                    $current =& $parsed[];
166
                } else {
167
                    $current =& $parsed[$item][];
168
                }
169
            } elseif ($item !== '!done') {
170
                $matches = [];
171
                if (preg_match_all('/^=(.*)=(.*)/', $item, $matches)) {
172
                    if ($matches[1][0] === 'ret') {
173
                        $single = $matches[2][0];
174
                    }
175
                    $current[$matches[1][0]] = $matches[2][0] ?? '';
0 ignored issues
show
Bug introduced by
The variable $current does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
176
                }
177
            }
178
        }
179
180
        if (empty($parsed) && null !== $single) {
181
            $parsed[] = $single;
182
        }
183
184
        return $parsed;
185
    }
186
187
    /**
188
     * Authorization logic
189
     *
190
     * @return  bool
191
     */
192
    private function login(): bool
193
    {
194
        // If legacy login scheme is enabled
195
        if ($this->config('legacy')) {
196
            // For the first we need get hash with salt
197
            $query = new Query('/login');
198
            $response = $this->write($query)->read();
199
200
            // Now need use this hash for authorization
201
            $query = (new Query('/login'))
202
                ->add('=name=' . $this->config('user'))
203
                ->add('=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response[0])));
204
        } else {
205
            // Just login with our credentials
206
            $query = (new Query('/login'))
207
                ->add('=name=' . $this->config('user'))
208
                ->add('=password=' . $this->config('pass'));
209
        }
210
211
        // Execute query and get response
212
        $response = $this->write($query)->read(false);
213
214
        // Return true if we have only one line from server and this line is !done
215
        return isset($response[0]) && $response[0] === '!done';
216
    }
217
218
    /**
219
     * Connect to socket server
220
     *
221
     * @return  bool
222
     */
223
    public function connect(): bool
224
    {
225
        // Few attempts in loop
226
        for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) {
227
228
            // Initiate socket session
229
            $this->openSocket();
230
231
            // If socket is active
232
            if ($this->getSocket()) {
233
234
                // If we logged in then exit from loop
235
                if (true === $this->login()) {
236
                    break;
237
                }
238
239
                // Else close socket and start from begin
240
                $this->closeSocket();
241
            }
242
243
            // Sleep some time between tries
244
            sleep($this->config('delay'));
245
        }
246
247
        // Return status of connection
248
        return true;
249
    }
250
251
    /**
252
     * Save socket resource to static variable
253
     *
254
     * @param   resource|null $socket
255
     * @return  bool
256
     */
257
    private function setSocket($socket): bool
258
    {
259
        if (\is_resource($socket)) {
260
            $this->_socket = $socket;
261
            return true;
262
        }
263
        return false;
264
    }
265
266
    /**
267
     * Return socket resource if is exist
268
     *
269
     * @return  bool|resource
270
     */
271
    public function getSocket()
272
    {
273
        return \is_resource($this->_socket)
274
            ? $this->_socket
275
            : false;
276
    }
277
278
    /**
279
     * Initiate socket session
280
     *
281
     * @return  bool
282
     */
283
    private function openSocket(): bool
284
    {
285
        // Connect to server
286
        $socket = false;
287
288
        // Default: Context for ssl
289
        $context = stream_context_create([
290
            'ssl' => [
291
                'ciphers' => 'ADH:ALL',
292
                'verify_peer' => false,
293
                'verify_peer_name' => false
294
            ]
295
        ]);
296
297
        // Default: Proto tcp:// but for ssl we need ssl://
298
        $proto = $this->config('ssl') ? 'ssl://' : '';
299
300
        try {
301
            // Initiate socket client
302
            $socket = stream_socket_client(
303
                $proto . $this->config('host') . ':' . $this->config('port'),
304
                $this->_socket_err_num,
305
                $this->_socket_err_str,
306
                $this->config('timeout'),
307
                STREAM_CLIENT_CONNECT,
308
                $context
309
            );
310
            // Throw error is socket is not initiated
311
            if (false === $socket) {
312
                throw new Exception('stream_socket_client() failed: code: ' . $this->_socket_err_num . ' reason:' . $this->_socket_err_str);
313
            }
314
315
        } catch (Exception $e) {
316
            // __construct
317
        }
318
319
        // Save socket to static variable
320
        return $this->setSocket($socket);
321
    }
322
323
    /**
324
     * Close socket session
325
     *
326
     * @return bool
327
     */
328
    private function closeSocket(): bool
329
    {
330
        fclose($this->_socket);
331
        return true;
332
    }
333
}
334