Completed
Push — master ( dffbf0...dbc562 )
by Mr
03:38
created

Client   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 350
Duplicated Lines 1.71 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 47
lcom 1
cbo 11
dl 6
loc 350
ccs 109
cts 109
cp 1
rs 8.64
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 20 4
A config() 0 4 1
A write() 0 23 5
B read() 0 36 7
A readAsIterator() 0 4 1
A rosario() 0 31 5
B parseResponse() 6 36 10
A pregResponse() 0 4 1
B login() 0 40 6
A isLegacy() 0 4 3
A connect() 0 31 4

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\ClientException;
6
use RouterOS\Exceptions\ConfigException;
7
use RouterOS\Exceptions\QueryException;
8
use RouterOS\Helpers\ArrayHelper;
9
10
/**
11
 * Class Client for RouterOS management
12
 *
13
 * @package RouterOS
14
 * @since   0.1
15
 */
16
class Client implements Interfaces\ClientInterface
17
{
18
    use SocketTrait, ShortsTrait;
19
20
    /**
21
     * Configuration of connection
22
     *
23
     * @var \RouterOS\Config
24
     */
25
    private $_config;
26
27
    /**
28
     * API communication object
29
     *
30
     * @var \RouterOS\APIConnector
31
     */
32
33
    private $_connector;
34
35
    /**
36
     * Client constructor.
37
     *
38
     * @param array|\RouterOS\Config $config
39
     * @throws \RouterOS\Exceptions\ClientException
40
     * @throws \RouterOS\Exceptions\ConfigException
41
     * @throws \RouterOS\Exceptions\QueryException
42
     */
43 17
    public function __construct($config)
44
    {
45
        // If array then need create object
46 17
        if (\is_array($config)) {
47 14
            $config = new Config($config);
48
        }
49
50
        // Check for important keys
51 17
        if (true !== $key = ArrayHelper::checkIfKeysNotExist(['host', 'user', 'pass'], $config->getParameters())) {
52 1
            throw new ConfigException("One or few parameters '$key' of Config is not set or empty");
53
        }
54
55
        // Save config if everything is okay
56 16
        $this->_config = $config;
57
58
        // Throw error if cannot to connect
59 16
        if (false === $this->connect()) {
60 1
            throw new ClientException('Unable to connect to ' . $config->get('host') . ':' . $config->get('port'));
61
        }
62 14
    }
63
64
    /**
65
     * Get some parameter from config
66
     *
67
     * @param string $parameter Name of required parameter
68
     * @return mixed
69
     * @throws \RouterOS\Exceptions\ConfigException
70
     */
71 16
    private function config(string $parameter)
72
    {
73 16
        return $this->_config->get($parameter);
74
    }
75
76
    /**
77
     * Send write query to RouterOS (with or without tag)
78
     *
79
     * @param string|array|\RouterOS\Query $query
80
     * @return \RouterOS\Client
81
     * @throws \RouterOS\Exceptions\QueryException
82
     */
83 15
    public function write($query): Client
84
    {
85 15
        if (\is_string($query)) {
86 8
            $query = new Query($query);
87 15
        } elseif (\is_array($query)) {
88 1
            $endpoint = array_shift($query);
89 1
            $query    = new Query($endpoint, $query);
90
        }
91
92 15
        if (!$query instanceof Query) {
93 1
            throw new QueryException('Parameters cannot be processed');
94
        }
95
96
        // Send commands via loop to router
97 15
        foreach ($query->getQuery() as $command) {
98 15
            $this->_connector->writeWord(trim($command));
99
        }
100
101
        // Write zero-terminator (empty string)
102 15
        $this->_connector->writeWord('');
103
104 15
        return $this;
105
    }
106
107
    /**
108
     * Read answer from server after query was executed
109
     *
110
     * A Mikrotik reply is formed of blocks
111
     * Each block starts with a word, one of ('!re', '!trap', '!done', '!fatal')
112
     * Each block end with an zero byte (empty line)
113
     * Reply ends with a complete !done or !fatal block (ended with 'empty line')
114
     * A !fatal block precedes TCP connexion close
115
     *
116
     * @param bool $parse
117
     * @return mixed
118
     */
119 15
    public function read(bool $parse = true)
120
    {
121
        // By default response is empty
122 15
        $response = [];
123
        // We have to wait a !done or !fatal
124 15
        $lastReply = false;
125
126
        // Read answer from socket in loop
127 15
        while (true) {
128 15
            $word = $this->_connector->readWord();
129
130 15
            if ('' === $word) {
131 15
                if ($lastReply) {
132
                    // We received a !done or !fatal message in a precedent loop
133
                    // response is complete
134 15
                    break;
135
                }
136
                // We did not receive the !done or !fatal message
137
                // This 0 length message is the end of a reply !re or !trap
138
                // We have to wait the router to send a !done or !fatal reply followed by optionals values and a 0 length message
139 7
                continue;
140
            }
141
142
            // Save output line to response array
143 15
            $response[] = $word;
144
145
            // If we get a !done or !fatal line in response, we are now ready to finish the read
146
            // but we need to wait a 0 length message, switch the flag
147 15
            if ('!done' === $word || '!fatal' === $word) {
148 15
                $lastReply = true;
149
            }
150
        }
151
152
        // Parse results and return
153 15
        return $parse ? $this->rosario($response) : $response;
154
    }
155
156
    /**
157
     * Read using Iterators to improve performance on large dataset
158
     *
159
     * @return \RouterOS\ResponseIterator
160
     */
161 4
    public function readAsIterator(): ResponseIterator
162
    {
163 4
        return new ResponseIterator($this);
164
    }
165
166
    /**
167
     * This method was created by memory save reasons, it convert response
168
     * from RouterOS to readable array in safe way.
169
     *
170
     * @param array $raw Array RAW response from server
171
     * @return mixed
172
     *
173
     * Based on RouterOSResponseArray solution by @arily
174
     *
175
     * @link    https://github.com/arily/RouterOSResponseArray
176
     * @since   1.0.0
177
     */
178 4
    private function rosario(array $raw): array
179
    {
180
        // This RAW should't be an error
181 4
        $positions = array_keys($raw, '!re');
182 4
        $count     = count($raw);
183 4
        $result    = [];
184
185 4
        if (isset($positions[1])) {
186
187 1
            foreach ($positions as $key => $position) {
188
                // Get length of future block
189 1
                $length = isset($positions[$key + 1])
190 1
                    ? $positions[$key + 1] - $position + 1
191 1
                    : $count - $position;
192
193
                // Convert array to simple items
194 1
                $item = [];
195 1
                for ($i = 1; $i < $length; $i++) {
196 1
                    $item[] = array_shift($raw);
197
                }
198
199
                // Save as result
200 1
                $result[] = $this->parseResponse($item)[0];
201
            }
202
203
        } else {
204 4
            $result = $this->parseResponse($raw);
205
        }
206
207 4
        return $result;
208
    }
209
210
    /**
211
     * Parse response from Router OS
212
     *
213
     * @param array $response Response data
214
     * @return array Array with parsed data
215
     */
216 5
    public function parseResponse(array $response): array
217
    {
218 5
        $result = [];
219 5
        $i      = -1;
220 5
        $lines  = \count($response);
221 5
        foreach ($response as $key => $value) {
222
            switch ($value) {
223 5
                case '!re':
224 2
                    $i++;
225 2
                    break;
226 5
                case '!fatal':
227 1
                    $result = $response;
228 1
                    break 2;
229 4
                case '!trap':
230 4
                case '!done':
231
                    // Check for =ret=, .tag and any other following messages
232 4
                    for ($j = $key + 1; $j <= $lines; $j++) {
233
                        // If we have lines after current one
234 4
                        if (isset($response[$j])) {
235 2
                            $this->pregResponse($response[$j], $matches);
236 2 View Code Duplication
                            if (isset($matches[1][0], $matches[2][0])) {
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...
237 2
                                $result['after'][$matches[1][0]] = $matches[2][0];
238
                            }
239
                        }
240
                    }
241 4
                    break 2;
242
                default:
243 2
                    $this->pregResponse($value, $matches);
244 2 View Code Duplication
                    if (isset($matches[1][0], $matches[2][0])) {
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...
245 2
                        $result[$i][$matches[1][0]] = $matches[2][0];
246
                    }
247 2
                    break;
248
            }
249
        }
250 5
        return $result;
251
    }
252
253
    /**
254
     * Parse result from RouterOS by regular expression
255
     *
256
     * @param string $value
257
     * @param array  $matches
258
     */
259 4
    private function pregResponse(string $value, &$matches)
260
    {
261 4
        preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches);
262 4
    }
263
264
    /**
265
     * Authorization logic
266
     *
267
     * @param bool $legacyRetry Retry login if we detect legacy version of RouterOS
268
     * @return bool
269
     * @throws \RouterOS\Exceptions\ClientException
270
     * @throws \RouterOS\Exceptions\ConfigException
271
     * @throws \RouterOS\Exceptions\QueryException
272
     */
273 15
    private function login(bool $legacyRetry = false): bool
274
    {
275
        // If legacy login scheme is enabled
276 15
        if ($this->config('legacy')) {
277
            // For the first we need get hash with salt
278 2
            $response = $this->write('/login')->read();
279
280
            // Now need use this hash for authorization
281 2
            $query = new Query('/login', [
282 2
                '=name=' . $this->config('user'),
283 2
                '=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response['after']['ret']))
284
            ]);
285
        } else {
286
            // Just login with our credentials
287 14
            $query = new Query('/login', [
288 14
                '=name=' . $this->config('user'),
289 14
                '=password=' . $this->config('pass')
290
            ]);
291
292
            // If we set modern auth scheme but router with legacy firmware then need to retry query,
293
            // but need to prevent endless loop
294 14
            $legacyRetry = true;
295
        }
296
297
        // Execute query and get response
298 15
        $response = $this->write($query)->read(false);
299
300
        // if:
301
        //  - we have more than one response
302
        //  - response is '!done'
303
        // => problem with legacy version, swap it and retry
304
        // Only tested with ROS pre 6.43, will test with post 6.43 => this could make legacy parameter obsolete?
305 15
        if ($legacyRetry && $this->isLegacy($response)) {
306 1
            $this->_config->set('legacy', true);
307 1
            return $this->login();
308
        }
309
310
        // Return true if we have only one line from server and this line is !done
311 15
        return (1 === count($response)) && isset($response[0]) && ($response[0] === '!done');
312
    }
313
314
    /**
315
     * Detect by login request if firmware is legacy
316
     *
317
     * @param array $response
318
     * @return bool
319
     * @throws ConfigException
320
     */
321 14
    private function isLegacy(array &$response): bool
322
    {
323 14
        return \count($response) > 1 && $response[0] === '!done' && !$this->config('legacy');
324
    }
325
326
    /**
327
     * Connect to socket server
328
     *
329
     * @return bool
330
     * @throws \RouterOS\Exceptions\ClientException
331
     * @throws \RouterOS\Exceptions\ConfigException
332
     * @throws \RouterOS\Exceptions\QueryException
333
     */
334 16
    private function connect(): bool
335
    {
336
        // By default we not connected
337 16
        $connected = false;
338
339
        // Few attempts in loop
340 16
        for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) {
341
342
            // Initiate socket session
343 16
            $this->openSocket();
344
345
            // If socket is active
346 15
            if (null !== $this->getSocket()) {
347 15
                $this->_connector = new APIConnector(new Streams\ResourceStream($this->getSocket()));
348
                // If we logged in then exit from loop
349 15
                if (true === $this->login()) {
350 14
                    $connected = true;
351 14
                    break;
352
                }
353
354
                // Else close socket and start from begin
355 1
                $this->closeSocket();
356
            }
357
358
            // Sleep some time between tries
359 1
            sleep($this->config('delay'));
360
        }
361
362
        // Return status of connection
363 15
        return $connected;
364
    }
365
}
366