Completed
Push — master ( 237a74...6854cf )
by Mr
10:28 queued 09:12
created

Client   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 350
Duplicated Lines 1.71 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 90.83%

Importance

Changes 0
Metric Value
wmc 47
lcom 1
cbo 12
dl 6
loc 350
ccs 99
cts 109
cp 0.9083
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
use RouterOS\Interfaces\ClientInterface;
10
11
/**
12
 * Class Client for RouterOS management
13
 *
14
 * @package RouterOS
15
 * @since   0.1
16
 */
17
class Client implements Interfaces\ClientInterface
18
{
19
    use SocketTrait, ShortsTrait;
20
21
    /**
22
     * Configuration of connection
23
     *
24
     * @var \RouterOS\Config
25
     */
26
    private $_config;
27
28
    /**
29
     * API communication object
30
     *
31
     * @var \RouterOS\APIConnector
32
     */
33
34
    private $_connector;
35
36
    /**
37
     * Client constructor.
38
     *
39
     * @param array|\RouterOS\Config $config
40
     * @throws \RouterOS\Exceptions\ClientException
41
     * @throws \RouterOS\Exceptions\ConfigException
42
     * @throws \RouterOS\Exceptions\QueryException
43
     */
44 13
    public function __construct($config)
45
    {
46
        // If array then need create object
47 13
        if (\is_array($config)) {
48 2
            $config = new Config($config);
49
        }
50
51
        // Check for important keys
52 13
        if (true !== $key = ArrayHelper::checkIfKeysNotExist(['host', 'user', 'pass'], $config->getParameters())) {
53 1
            throw new ConfigException("One or few parameters '$key' of Config is not set or empty");
54
        }
55
56
        // Save config if everything is okay
57 12
        $this->_config = $config;
58
59
        // Throw error if cannot to connect
60 12
        if (false === $this->connect()) {
61 1
            throw new ClientException('Unable to connect to ' . $config->get('host') . ':' . $config->get('port'));
62
        }
63 10
    }
64
65
    /**
66
     * Get some parameter from config
67
     *
68
     * @param string $parameter Name of required parameter
69
     * @return mixed
70
     * @throws \RouterOS\Exceptions\ConfigException
71
     */
72 12
    private function config(string $parameter)
73
    {
74 12
        return $this->_config->get($parameter);
75
    }
76
77
    /**
78
     * Send write query to RouterOS (with or without tag)
79
     *
80
     * @param string|array|\RouterOS\Query $query
81
     * @return \RouterOS\Interfaces\ClientInterface
82
     * @throws \RouterOS\Exceptions\QueryException
83
     */
84 11
    public function write($query): self
85
    {
86 11
        if (\is_string($query)) {
87 4
            $query = new Query($query);
88 11
        } elseif (\is_array($query)) {
89 1
            $endpoint = array_shift($query);
90 1
            $query    = new Query($endpoint, $query);
91
        }
92
93 11
        if (!$query instanceof Query) {
94 1
            throw new QueryException('Parameters cannot be processed');
95
        }
96
97
        // Send commands via loop to router
98 11
        foreach ($query->getQuery() as $command) {
99 11
            $this->_connector->writeWord(trim($command));
100
        }
101
102
        // Write zero-terminator (empty string)
103 11
        $this->_connector->writeWord('');
104
105 11
        return $this;
106
    }
107
108
    /**
109
     * Read answer from server after query was executed
110
     *
111
     * A Mikrotik reply is formed of blocks
112
     * Each block starts with a word, one of ('!re', '!trap', '!done', '!fatal')
113
     * Each block end with an zero byte (empty line)
114
     * Reply ends with a complete !done or !fatal block (ended with 'empty line')
115
     * A !fatal block precedes TCP connexion close
116
     *
117
     * @param bool $parse
118
     * @return mixed
119
     */
120 11
    public function read(bool $parse = true)
121
    {
122
        // By default response is empty
123 11
        $response = [];
124
        // We have to wait a !done or !fatal
125 11
        $lastReply = false;
126
127
        // Read answer from socket in loop
128 11
        while (true) {
129 11
            $word = $this->_connector->readWord();
130
131 11
            if ('' === $word) {
132 11
                if ($lastReply) {
133
                    // We received a !done or !fatal message in a precedent loop
134
                    // response is complete
135 11
                    break;
136
                }
137
                // We did not receive the !done or !fatal message
138
                // This 0 length message is the end of a reply !re or !trap
139
                // We have to wait the router to send a !done or !fatal reply followed by optionals values and a 0 length message
140 4
                continue;
141
            }
142
143
            // Save output line to response array
144 11
            $response[] = $word;
145
146
            // If we get a !done or !fatal line in response, we are now ready to finish the read
147
            // but we need to wait a 0 length message, switch the flag
148 11
            if ('!done' === $word || '!fatal' === $word) {
149 11
                $lastReply = true;
150
            }
151
        }
152
153
        // Parse results and return
154 11
        return $parse ? $this->rosario($response) : $response;
155
    }
156
157
    /**
158
     * Read using Iterators to improve performance on large dataset
159
     *
160
     * @return \RouterOS\ResponseIterator
161
     */
162
    public function readAsIterator(): ResponseIterator
163
    {
164
        return new ResponseIterator($this);
165
    }
166
167
    /**
168
     * This method was created by memory save reasons, it convert response
169
     * from RouterOS to readable array in safe way.
170
     *
171
     * @param array $raw Array RAW response from server
172
     * @return mixed
173
     *
174
     * Based on RouterOSResponseArray solution by @arily
175
     *
176
     * @link    https://github.com/arily/RouterOSResponseArray
177
     * @since   1.0.0
178
     */
179 4
    private function rosario(array $raw): array
180
    {
181
        // This RAW should't be an error
182 4
        $positions = array_keys($raw, '!re');
183 4
        $count     = count($raw);
184 4
        $result    = [];
185
186 4
        if (isset($positions[1])) {
187
188
            foreach ($positions as $key => $position) {
189
                // Get length of future block
190
                $length = isset($positions[$key + 1])
191
                    ? $positions[$key + 1] - $position + 1
192
                    : $count - $position;
193
194
                // Convert array to simple items
195
                $item = [];
196
                for ($i = 1; $i < $length; $i++) {
197
                    $item[] = array_shift($raw);
198
                }
199
200
                // Save as result
201
                $result[] = $this->parseResponse($item)[0];
202
            }
203
204
        } else {
205 4
            $result = $this->parseResponse($raw);
206
        }
207
208 4
        return $result;
209
    }
210
211
    /**
212
     * Parse response from Router OS
213
     *
214
     * @param array $response Response data
215
     * @return array Array with parsed data
216
     */
217 4
    public function parseResponse(array $response): array
218
    {
219 4
        $result = [];
220 4
        $i      = -1;
221 4
        $lines  = \count($response);
222 4
        foreach ($response as $key => $value) {
223
            switch ($value) {
224 4
                case '!re':
225 1
                    $i++;
226 1
                    break;
227 4
                case '!fatal':
228 1
                    $result = $response;
229 1
                    break 2;
230 3
                case '!trap':
231 3
                case '!done':
232
                    // Check for =ret=, .tag and any other following messages
233 3
                    for ($j = $key + 1; $j <= $lines; $j++) {
234
                        // If we have lines after current one
235 3
                        if (isset($response[$j])) {
236 2
                            $this->pregResponse($response[$j], $matches);
237 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...
238 2
                                $result['after'][$matches[1][0]] = $matches[2][0];
239
                            }
240
                        }
241
                    }
242 3
                    break 2;
243
                default:
244 1
                    $this->pregResponse($value, $matches);
245 1 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...
246 1
                        $result[$i][$matches[1][0]] = $matches[2][0];
247
                    }
248 1
                    break;
249
            }
250
        }
251 4
        return $result;
252
    }
253
254
    /**
255
     * Parse result from RouterOS by regular expression
256
     *
257
     * @param string $value
258
     * @param array  $matches
259
     */
260 3
    private function pregResponse(string $value, &$matches)
261
    {
262 3
        preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches);
263 3
    }
264
265
    /**
266
     * Authorization logic
267
     *
268
     * @param bool $legacyRetry Retry login if we detect legacy version of RouterOS
269
     * @return bool
270
     * @throws \RouterOS\Exceptions\ClientException
271
     * @throws \RouterOS\Exceptions\ConfigException
272
     * @throws \RouterOS\Exceptions\QueryException
273
     */
274 11
    private function login(bool $legacyRetry = false): bool
275
    {
276
        // If legacy login scheme is enabled
277 11
        if ($this->config('legacy')) {
278
            // For the first we need get hash with salt
279 2
            $response = $this->write('/login')->read();
0 ignored issues
show
Bug introduced by
The call to read() misses a required argument $parse.

This check looks for function calls that miss required arguments.

Loading history...
280
281
            // Now need use this hash for authorization
282 2
            $query = new Query('/login', [
283 2
                '=name=' . $this->config('user'),
284 2
                '=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response['after']['ret']))
285
            ]);
286
        } else {
287
            // Just login with our credentials
288 10
            $query = new Query('/login', [
289 10
                '=name=' . $this->config('user'),
290 10
                '=password=' . $this->config('pass')
291
            ]);
292
293
            // If we set modern auth scheme but router with legacy firmware then need to retry query,
294
            // but need to prevent endless loop
295 10
            $legacyRetry = true;
296
        }
297
298
        // Execute query and get response
299 11
        $response = $this->write($query)->read(false);
300
301
        // if:
302
        //  - we have more than one response
303
        //  - response is '!done'
304
        // => problem with legacy version, swap it and retry
305
        // Only tested with ROS pre 6.43, will test with post 6.43 => this could make legacy parameter obsolete?
306 11
        if ($legacyRetry && $this->isLegacy($response)) {
307 1
            $this->_config->set('legacy', true);
308 1
            return $this->login();
309
        }
310
311
        // Return true if we have only one line from server and this line is !done
312 11
        return (1 === count($response)) && isset($response[0]) && ($response[0] === '!done');
313
    }
314
315
    /**
316
     * Detect by login request if firmware is legacy
317
     *
318
     * @param array $response
319
     * @return bool
320
     * @throws ConfigException
321
     */
322 10
    private function isLegacy(array &$response): bool
323
    {
324 10
        return \count($response) > 1 && $response[0] === '!done' && !$this->config('legacy');
325
    }
326
327
    /**
328
     * Connect to socket server
329
     *
330
     * @return bool
331
     * @throws \RouterOS\Exceptions\ClientException
332
     * @throws \RouterOS\Exceptions\ConfigException
333
     * @throws \RouterOS\Exceptions\QueryException
334
     */
335 12
    private function connect(): bool
336
    {
337
        // By default we not connected
338 12
        $connected = false;
339
340
        // Few attempts in loop
341 12
        for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) {
342
343
            // Initiate socket session
344 12
            $this->openSocket();
345
346
            // If socket is active
347 11
            if (null !== $this->getSocket()) {
348 11
                $this->_connector = new APIConnector(new Streams\ResourceStream($this->getSocket()));
349
                // If we logged in then exit from loop
350 11
                if (true === $this->login()) {
351 10
                    $connected = true;
352 10
                    break;
353
                }
354
355
                // Else close socket and start from begin
356 1
                $this->closeSocket();
357
            }
358
359
            // Sleep some time between tries
360 1
            sleep($this->config('delay'));
361
        }
362
363
        // Return status of connection
364 11
        return $connected;
365
    }
366
}
367