Passed
Push — develop ( 441879...db80b6 )
by Vasil
02:54
created

Communicator::__construct()   F

Complexity

Conditions 20
Paths 534

Size

Total Lines 93
Code Lines 61

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 61
nc 534
nop 7
dl 0
loc 93
rs 2.7422
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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
 * ~~summary~~
5
 *
6
 * ~~description~~
7
 *
8
 * PHP version 5
9
 *
10
 * @category  Net
11
 * @package   PEAR2_Net_RouterOS
12
 * @author    Vasil Rangelov <[email protected]>
13
 * @copyright 2011 Vasil Rangelov
14
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
15
 * @version   GIT: $Id$
16
 * @link      http://pear2.php.net/PEAR2_Net_RouterOS
17
 */
18
/**
19
 * The namespace declaration.
20
 */
21
namespace PEAR2\Net\RouterOS;
22
23
/**
24
 * Using transmitters.
25
 */
26
use PEAR2\Net\Transmitter as T;
27
28
/**
29
 * A RouterOS communicator.
30
 *
31
 * Implementation of the RouterOS API protocol. Unlike the other classes in this
32
 * package, this class doesn't provide any conveniences beyond the low level
33
 * implementation details (automatic word length encoding/decoding, charset
34
 * translation and data integrity), and because of that, its direct usage is
35
 * strongly discouraged.
36
 *
37
 * @category Net
38
 * @package  PEAR2_Net_RouterOS
39
 * @author   Vasil Rangelov <[email protected]>
40
 * @license  http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
41
 * @link     http://pear2.php.net/PEAR2_Net_RouterOS
42
 * @see      Client
43
 */
44
class Communicator
45
{
46
    /**
47
     * Used when getting/setting all (default) charsets.
48
     */
49
    const CHARSET_ALL = -1;
50
51
    /**
52
     * Used when getting/setting the (default) remote charset.
53
     *
54
     * The remote charset is the charset in which RouterOS stores its data.
55
     * If you want to keep compatibility with your Winbox, this charset should
56
     * match the default charset from your Windows' regional settings.
57
     */
58
    const CHARSET_REMOTE = 0;
59
60
    /**
61
     * Used when getting/setting the (default) local charset.
62
     *
63
     * The local charset is the charset in which the data from RouterOS will be
64
     * returned as. This charset should match the charset of the place the data
65
     * will eventually be written to.
66
     */
67
    const CHARSET_LOCAL = 1;
68
69
    /**
70
     * An array with the default charset.
71
     *
72
     * Charset types as keys, and the default charsets as values.
73
     *
74
     * @var array<string,string|null>
75
     */
76
    protected static $defaultCharsets = array(
77
        self::CHARSET_REMOTE => null,
78
        self::CHARSET_LOCAL  => null
79
    );
80
81
    /**
82
     * An array with the current charset.
83
     *
84
     * Charset types as keys, and the current charsets as values.
85
     *
86
     * @var array<string,string|null>
87
     */
88
    protected $charsets = array();
89
90
    /**
91
     * Length of next word.
92
     *
93
     * Length of next word. NULL if no peeking was done.
94
     *
95
     * @var int|double|null
96
     */
97
    protected $nextWordLength = null;
98
99
    /**
100
     * Last state of the word lock.
101
     *
102
     * Last state of the word lock when using persisntent connections.
103
     * Unused by non-persistent connections.
104
     *
105
     * @var int|false
106
     */
107
    protected $nextWordLock = false;
108
109
    /**
110
     * The transmitter for the connection.
111
     *
112
     * @var T\TcpClient
113
     */
114
    protected $trans;
115
116
    /**
117
     * Creates a new connection with the specified options.
118
     *
119
     * @param string        $host    Hostname (IP or domain) of RouterOS.
120
     * @param int|null      $port    The port on which the RouterOS host
121
     *     provides the API service. You can also specify NULL, in which case
122
     *     the port will automatically be chosen between 8728 and 8729,
123
     *     depending on the value of $crypto.
124
     * @param bool          $persist Whether or not the connection should be a
125
     *     persistent one.
126
     * @param double|null   $timeout The timeout for the connection.
127
     * @param string        $key     A string that uniquely identifies the
128
     *     connection.
129
     * @param string        $crypto  The encryption for this connection.
130
     *     Must be one of the PEAR2\Net\Transmitter\NetworkStream::CRYPTO_*
131
     *     constants. Off by default. RouterOS currently supports only TLS, but
132
     *     the setting is provided in this fashion for forward compatibility's
133
     *     sake.
134
     * @param resource|null $context A context for the socket.
135
     *     If not set, the default context will be used, and adjusted to use
136
     *     "ADH" as the "ciphers" SSL context option if there are no CAs on
137
     *     any level and that option has not been explicitly set.
138
     *     If that option has been set to "ADH" (explicitly or not),
139
     *     the "verify_peer" option will be set to FALSE, unless explicitly
140
     *     set otherwise. The "verify_peer_name" option is set to match
141
     *     "verify_peer", unless explicitly set otherwise.
142
     *
143
     * @see sendWord()
144
     */
145
    public function __construct(
146
        $host,
147
        $port = 8728,
148
        $persist = false,
149
        $timeout = null,
150
        $key = '',
151
        $crypto = T\NetworkStream::CRYPTO_OFF,
152
        $context = null
153
    ) {
154
        $isUnencrypted = T\NetworkStream::CRYPTO_OFF === $crypto;
155
        if (($context === null) && !$isUnencrypted) {
156
            $context = stream_context_get_default();
157
            $opts = stream_context_get_options($context);
158
            if (isset($opts['ssl']['verify_peer'])) {
159
                $verifyPeer = !!$opts['ssl']['verify_peer'];
160
            } elseif (isset($opts['ssl']['ciphers'])
161
                && 'ADH' === $opts['ssl']['ciphers']
162
            ) {
163
                $verifyPeer = false;
164
            } else {
165
                $verifyPeer = isset($opts['ssl']['cafile'])
166
                    || isset($opts['ssl']['capath']);
167
                if (!$verifyPeer
168
                    && function_exists('openssl_get_cert_locations')
169
                ) {
170
                    $fileCt = array(
171
                        'default_cert_file',
172
                        'default_cert_file_env',
173
                        'ini_cafile'
174
                    );
175
                    $getenvCt = array(
176
                        'default_cert_file_env',
177
                        'default_cert_dir_env'
178
                    );
179
                    foreach (openssl_get_cert_locations() as $ct => $cl) {
180
                        if (in_array($ct, $getenvCt, true)) {
181
                            $cl = (string) getenv($cl);
182
                        }
183
                        if ('' === $cl) {
184
                            continue;
185
                        }
186
                        $verifyPeer = in_array($ct, $fileCt)
187
                            ? is_file($cl)
188
                            : is_dir($cl);
189
                        if ($verifyPeer) {
190
                            break;
191
                        }
192
                    }
193
                }
194
            }
195
            $newOpts = array(
196
                'ssl' => array(
197
                    'verify_peer' => $verifyPeer,
198
                    'verify_peer_name' => isset(
199
                        $opts['ssl']['verify_peer_name']
200
                    ) ? !!$opts['ssl']['verify_peer_name'] : $verifyPeer
201
                )
202
            );
203
            if (!$verifyPeer && !isset($opts['ssl']['ciphers'])) {
204
                $newOpts['ssl']['ciphers'] = 'ADH';
205
            }
206
            stream_context_set_option(
207
                $context,
208
                $newOpts
209
            );
210
        }
211
        // @codeCoverageIgnoreStart
212
        // The $port is customizable in testing.
213
        if (null === $port) {
214
            $port = $isUnencrypted ? 8728 : 8729;
215
        }
216
        // @codeCoverageIgnoreEnd
217
218
        try {
219
            $this->trans = new T\TcpClient(
220
                $host,
221
                $port,
222
                $persist,
223
                $timeout,
224
                $key,
225
                $crypto,
226
                $context
227
            );
228
        } catch (T\Exception $e) {
229
            throw new SocketException(
230
                'Error connecting to RouterOS',
231
                SocketException::CODE_CONNECTION_FAIL,
232
                $e
233
            );
234
        }
235
        $this->setCharset(
236
            self::getDefaultCharset(self::CHARSET_ALL),
237
            self::CHARSET_ALL
238
        );
239
    }
240
241
    /**
242
     * A shorthand gateway.
243
     *
244
     * This is a magic PHP method that allows you to call the object as a
245
     * function. Depending on the argument given, one of the other functions in
246
     * the class is invoked and its returned value is returned by this function.
247
     *
248
     * If one or more arguments are provided, it sends a word with the supplied
249
     * word fragments. Otherwise, gets the next word as a string.
250
     *
251
     * @return int|string If arguments are given, returns the number of bytes
252
     *     sent, otherwise returns the next word as a string.
253
     */
254
    public function __invoke()
255
    {
256
        return 0 === func_num_args() ? $this->getNextWord()
257
            : call_user_func_array(array($this, 'sendWord'), func_get_args());
258
    }
259
260
    /**
261
     * Checks whether a variable is a seekable stream resource.
262
     *
263
     * @param mixed $var The value to check.
264
     *
265
     * @return bool TRUE if $var is a seekable stream, FALSE otherwise.
266
     */
267
    public static function isSeekableStream($var)
268
    {
269
        if (T\Stream::isStream($var)) {
270
            $meta = stream_get_meta_data($var);
271
            return $meta['seekable'];
272
        }
273
        return false;
274
    }
275
276
    /**
277
     * Uses iconv to convert a stream from one charset to another.
278
     *
279
     * @param string   $inCharset  The charset of the stream.
280
     * @param string   $outCharset The desired resulting charset.
281
     * @param resource $stream     The stream to convert. The stream is assumed
282
     *     to be seekable, and is read from its current position to its end,
283
     *     after which, it is seeked back to its starting position.
284
     *
285
     * @return resource A new stream that uses the $out_charset. The stream is a
286
     *     subset from the original stream, from its current position to its
287
     *     end, seeked at its start.
288
     */
289
    public static function iconvStream($inCharset, $outCharset, $stream)
290
    {
291
        $bytes = 0;
292
        $result = fopen('php://temp', 'r+b');
293
        $iconvFilter = stream_filter_append(
294
            $result,
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type false; however, parameter $stream of stream_filter_append() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

294
            /** @scrutinizer ignore-type */ $result,
Loading history...
295
            'convert.iconv.' . $inCharset . '.' . $outCharset,
296
            STREAM_FILTER_WRITE
297
        );
298
299
        flock($stream, LOCK_SH);
300
        $reader = new T\Stream($stream, false);
301
        $writer = new T\Stream($result, false);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type false; however, parameter $stream of PEAR2\Net\Transmitter\Stream::__construct() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

301
        $writer = new T\Stream(/** @scrutinizer ignore-type */ $result, false);
Loading history...
302
        $chunkSize = $reader->getChunk(T\Stream::DIRECTION_RECEIVE);
303
        while ($reader->isAvailable() && $reader->isDataAwaiting()) {
304
            $bytes += $writer->send(fread($stream, $chunkSize));
305
        }
306
        fseek($stream, -$bytes, SEEK_CUR);
307
        flock($stream, LOCK_UN);
308
309
        stream_filter_remove($iconvFilter);
310
        rewind($result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type false; however, parameter $handle of rewind() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

310
        rewind(/** @scrutinizer ignore-type */ $result);
Loading history...
311
        return $result;
312
    }
313
314
    /**
315
     * Sets the default charset(s) for new connections.
316
     *
317
     * @param mixed $charset     The charset to set. If $charsetType is
318
     *     {@link self::CHARSET_ALL}, you can supply either a string to use for
319
     *     all charsets, or an array with the charset types as keys, and the
320
     *     charsets as values.
321
     * @param int   $charsetType Which charset to set. Valid values are the
322
     *     CHARSET_* constants. Any other value is treated as
323
     *     {@link self::CHARSET_ALL}.
324
     *
325
     * @return string|array The old charset. If $charsetType is
326
     *     {@link self::CHARSET_ALL}, the old values will be returned as an
327
     *     array with the types as keys, and charsets as values.
328
     *
329
     * @see setCharset()
330
     */
331
    public static function setDefaultCharset(
332
        $charset,
333
        $charsetType = self::CHARSET_ALL
334
    ) {
335
        if (array_key_exists($charsetType, self::$defaultCharsets)) {
336
             $oldCharset = self::$defaultCharsets[$charsetType];
337
             self::$defaultCharsets[$charsetType] = $charset;
338
             return $oldCharset;
339
        } else {
340
            $oldCharsets = self::$defaultCharsets;
341
            self::$defaultCharsets = is_array($charset) ? $charset : array_fill(
342
                0,
343
                count(self::$defaultCharsets),
344
                $charset
345
            );
346
            return $oldCharsets;
347
        }
348
    }
349
350
    /**
351
     * Gets the default charset(s).
352
     *
353
     * @param int $charsetType Which charset to get. Valid values are the
354
     *     CHARSET_* constants. Any other value is treated as
355
     *     {@link self::CHARSET_ALL}.
356
     *
357
     * @return string|array The current charset. If $charsetType is
358
     *     {@link self::CHARSET_ALL}, the current values will be returned as an
359
     *     array with the types as keys, and charsets as values.
360
     *
361
     * @see setDefaultCharset()
362
     */
363
    public static function getDefaultCharset($charsetType)
364
    {
365
        return array_key_exists($charsetType, self::$defaultCharsets)
366
            ? self::$defaultCharsets[$charsetType] : self::$defaultCharsets;
367
    }
368
369
    /**
370
     * Gets the length of a seekable stream.
371
     *
372
     * Gets the length of a seekable stream.
373
     *
374
     * @param resource $stream The stream to check. The stream is assumed to be
375
     *     seekable.
376
     *
377
     * @return double The number of bytes in the stream between its current
378
     *     position and its end.
379
     */
380
    public static function seekableStreamLength($stream)
381
    {
382
        $streamPosition = (double) sprintf('%u', ftell($stream));
383
        fseek($stream, 0, SEEK_END);
384
        $streamLength = ((double) sprintf('%u', ftell($stream)))
385
            - $streamPosition;
386
        fseek($stream, $streamPosition, SEEK_SET);
0 ignored issues
show
Bug introduced by
$streamPosition of type double is incompatible with the type integer expected by parameter $offset of fseek(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

386
        fseek($stream, /** @scrutinizer ignore-type */ $streamPosition, SEEK_SET);
Loading history...
387
        return $streamLength;
388
    }
389
390
    /**
391
     * Sets the charset(s) for this connection.
392
     *
393
     * Sets the charset(s) for this connection. The specified charset(s) will be
394
     * used for all future words. When sending, {@link self::CHARSET_LOCAL} is
395
     * converted to {@link self::CHARSET_REMOTE}, and when receiving,
396
     * {@link self::CHARSET_REMOTE} is converted to {@link self::CHARSET_LOCAL}.
397
     * Setting  NULL to either charset will disable charset conversion, and data
398
     * will be both sent and received "as is".
399
     *
400
     * @param mixed $charset     The charset to set. If $charsetType is
401
     *     {@link self::CHARSET_ALL}, you can supply either a string to use for
402
     *     all charsets, or an array with the charset types as keys, and the
403
     *     charsets as values.
404
     * @param int   $charsetType Which charset to set. Valid values are the
405
     *     CHARSET_* constants. Any other value is treated as
406
     *     {@link self::CHARSET_ALL}.
407
     *
408
     * @return string|array The old charset. If $charsetType is
409
     *     {@link self::CHARSET_ALL}, the old values will be returned as an
410
     *     array with the types as keys, and charsets as values.
411
     *
412
     * @see setDefaultCharset()
413
     */
414
    public function setCharset($charset, $charsetType = self::CHARSET_ALL)
415
    {
416
        if (array_key_exists($charsetType, $this->charsets)) {
417
             $oldCharset = $this->charsets[$charsetType];
418
             $this->charsets[$charsetType] = $charset;
419
             return $oldCharset;
420
        } else {
421
            $oldCharsets = $this->charsets;
422
            $this->charsets = is_array($charset) ? $charset : array_fill(
423
                0,
424
                count($this->charsets),
425
                $charset
426
            );
427
            return $oldCharsets;
428
        }
429
    }
430
431
    /**
432
     * Gets the charset(s) for this connection.
433
     *
434
     * @param int $charsetType Which charset to get. Valid values are the
435
     *     CHARSET_* constants. Any other value is treated as
436
     *     {@link self::CHARSET_ALL}.
437
     *
438
     * @return string|array The current charset. If $charsetType is
439
     *     {@link self::CHARSET_ALL}, the current values will be returned as an
440
     *     array with the types as keys, and charsets as values.
441
     *
442
     * @see getDefaultCharset()
443
     * @see setCharset()
444
     */
445
    public function getCharset($charsetType)
446
    {
447
        return array_key_exists($charsetType, $this->charsets)
448
            ? $this->charsets[$charsetType] : $this->charsets;
449
    }
450
451
    /**
452
     * Gets the transmitter for this connection.
453
     *
454
     * @return T\TcpClient The transmitter for this connection.
455
     */
456
    public function getTransmitter()
457
    {
458
        return $this->trans;
459
    }
460
461
    /**
462
     * Sends a word.
463
     *
464
     * Sends a word and automatically encodes its length when doing so.
465
     *
466
     * @param string|resource $word     The word to send, as a string
467
     * or seekable stream.
468
     * @param string|resource $word,... Additional word fragments to be sent
469
     * as a single word.
470
     *
471
     * @return int The number of bytes sent.
472
     *
473
     * @see sendWordFromStream()
474
     * @see getNextWord()
475
     */
476
    public function sendWord($word)
477
    {
478
        $length = 0;
479
        $isCharsetConversionEnabled
480
            = null !== ($rCharset = $this->getCharset(self::CHARSET_REMOTE))
481
            && null !== ($lCharset = $this->getCharset(self::CHARSET_LOCAL));
482
        $wordFragments = array();
483
        foreach (func_get_args() as $word) {
484
            if (is_string($word)) {
485
                if ($isCharsetConversionEnabled) {
486
                    $word = iconv(
487
                        $lCharset,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $lCharset does not seem to be defined for all execution paths leading up to this point.
Loading history...
488
                        $rCharset . '//IGNORE//TRANSLIT',
489
                        $word
490
                    );
491
                }
492
                $length += strlen($word);
493
            } else {
494
                if (!self::isSeekableStream($word)) {
495
                    throw new InvalidArgumentException(
496
                        'Only seekable streams can be sent.',
497
                        InvalidArgumentException::CODE_SEEKABLE_REQUIRED
498
                    );
499
                }
500
                if ($isCharsetConversionEnabled) {
501
                    $word = self::iconvStream(
502
                        $lCharset,
503
                        $rCharset . '//IGNORE//TRANSLIT',
504
                        $word
505
                    );
506
                }
507
                flock($word, LOCK_SH);
508
                $length += self::seekableStreamLength($word);
509
            }
510
            $wordFragments[] = $word;
511
        }
512
        static::verifyLengthSupport($length);
0 ignored issues
show
Bug introduced by
It seems like $length can also be of type double; however, parameter $length of PEAR2\Net\RouterOS\Commu...::verifyLengthSupport() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

512
        static::verifyLengthSupport(/** @scrutinizer ignore-type */ $length);
Loading history...
513
        if ($this->trans->isPersistent()) {
514
            $old = $this->trans->lock(T\Stream::DIRECTION_SEND);
515
            $bytesSent = $this->_sendWord($length, $wordFragments);
516
            $this->trans->lock($old, true);
517
518
            return $bytesSent;
519
        }
520
521
        return $this->_sendWord($length, $wordFragments);
522
    }
523
524
    /**
525
     * Send the word fragments
526
     *
527
     * @param int|double          $length        The previously computed
528
     *     length of all fragments.
529
     * @param (string|resource)[] $wordFragments The fragments to send.
530
     *
531
     * @return int The number of bytes sent.
532
     */
533
    private function _sendWord($length, $wordFragments)
534
    {
535
        $bytesSent = $this->trans->send(self::encodeLength($length));
0 ignored issues
show
Bug introduced by
It seems like $length can also be of type double; however, parameter $length of PEAR2\Net\RouterOS\Communicator::encodeLength() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

535
        $bytesSent = $this->trans->send(self::encodeLength(/** @scrutinizer ignore-type */ $length));
Loading history...
536
        foreach ($wordFragments as $fragment) {
537
            $bytesSent += $this->trans->send($fragment);
538
            if (!is_string($fragment)) {
539
                flock($fragment, LOCK_UN);
540
            }
541
        }
542
        return $bytesSent;
543
    }
544
545
    /**
546
     * Verifies that the length is supported.
547
     *
548
     * Verifies if the specified length is supported by the API. Throws a
549
     * {@link LengthException} if that's not the case. Currently, RouterOS
550
     * supports words up to 0xFFFFFFFF in length, so that's the only check
551
     * performed.
552
     *
553
     * @param int $length The length to verify.
554
     *
555
     * @return void
556
     */
557
    public static function verifyLengthSupport($length)
558
    {
559
        if ($length > 0xFFFFFFFF) {
560
            throw new LengthException(
561
                'Words with length above 0xFFFFFFFF are not supported.',
562
                LengthException::CODE_UNSUPPORTED,
563
                null,
564
                $length
565
            );
566
        }
567
    }
568
569
    /**
570
     * Encodes the length as required by the RouterOS API.
571
     *
572
     * @param int $length The length to encode.
573
     *
574
     * @return string The encoded length.
575
     */
576
    public static function encodeLength($length)
577
    {
578
        if ($length < 0) {
579
            throw new LengthException(
580
                'Length must not be negative.',
581
                LengthException::CODE_INVALID,
582
                null,
583
                $length
584
            );
585
        } elseif ($length < 0x80) {
586
            return chr($length);
587
        } elseif ($length < 0x4000) {
588
            return pack('n', $length |= 0x8000);
589
        } elseif ($length < 0x200000) {
590
            $length |= 0xC00000;
591
            return pack('n', $length >> 8) . chr($length & 0xFF);
592
        } elseif ($length < 0x10000000) {
593
            return pack('N', $length |= 0xE0000000);
594
        } elseif ($length <= 0xFFFFFFFF) {
595
            return chr(0xF0) . pack('N', $length);
596
        } elseif ($length <= 0x7FFFFFFFF) {
597
            $length = 'f' . base_convert($length, 10, 16);
598
            return chr(hexdec(substr($length, 0, 2))) .
0 ignored issues
show
Bug introduced by
It seems like hexdec(substr($length, 0, 2)) can also be of type double; however, parameter $ascii of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

598
            return chr(/** @scrutinizer ignore-type */ hexdec(substr($length, 0, 2))) .
Loading history...
599
                pack('N', hexdec(substr($length, 2)));
600
        }
601
        throw new LengthException(
602
            'Length must not be above 0x7FFFFFFFF.',
603
            LengthException::CODE_BEYOND_SHEME,
604
            null,
605
            $length
606
        );
607
    }
608
609
    /**
610
     * Get the length of the next word in queue.
611
     *
612
     * Get the length of the next word in queue without getting the word.
613
     * For pesisntent connections, note that the underlying transmitter will
614
     * be locked for receiving until either {@link self::getNextWord()} or
615
     * {@link self::getNextWordAsStream()} is called.
616
     *
617
     * @return int|double
618
     */
619
    public function getNextWordLength()
620
    {
621
        if (null === $this->nextWordLength) {
622
            if ($this->trans->isPersistent()) {
623
                $this->nextWordLock = $this->trans->lock(
624
                    T\Stream::DIRECTION_RECEIVE
625
                );
626
            }
627
            $this->nextWordLength = self::decodeLength($this->trans);
628
        }
629
        return $this->nextWordLength;
630
    }
631
632
    /**
633
     * Get the next word in queue as a string.
634
     *
635
     * Get the next word in queue as a string, after automatically decoding its
636
     * length.
637
     *
638
     * @return string The word.
639
     *
640
     * @see close()
641
     */
642
    public function getNextWord()
643
    {
644
        $word = $this->trans->receive(
645
            $this->getNextWordLength(),
0 ignored issues
show
Bug introduced by
It seems like $this->getNextWordLength() can also be of type double; however, parameter $length of PEAR2\Net\Transmitter\TcpClient::receive() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

645
            /** @scrutinizer ignore-type */ $this->getNextWordLength(),
Loading history...
646
            'word'
647
        );
648
        if (false !== $this->nextWordLock) {
649
            $this->trans->lock($this->nextWordLock, true);
650
        }
651
        $this->nextWordLength = null;
652
653
        if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
0 ignored issues
show
introduced by
The condition null !== $remoteCharset ...et(self::CHARSET_LOCAL) can never be false.
Loading history...
654
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
655
        ) {
656
            $word = iconv(
657
                $remoteCharset,
658
                $localCharset . '//IGNORE//TRANSLIT',
659
                $word
660
            );
661
        }
662
663
        return $word;
664
    }
665
666
    /**
667
     * Get the next word in queue as a stream.
668
     *
669
     * Get the next word in queue as a stream, after automatically decoding its
670
     * length.
671
     *
672
     * @return resource The word, as a stream.
673
     *
674
     * @see close()
675
     */
676
    public function getNextWordAsStream()
677
    {
678
        $filters = new T\FilterCollection();
679
        if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
0 ignored issues
show
introduced by
The condition null !== $remoteCharset ...et(self::CHARSET_LOCAL) can never be false.
Loading history...
680
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
681
        ) {
682
            $filters->append(
683
                'convert.iconv.' .
684
                $remoteCharset . '.' . $localCharset . '//IGNORE//TRANSLIT'
685
            );
686
        }
687
688
        $stream = $this->trans->receiveStream(
689
            $this->getNextWordLength(),
0 ignored issues
show
Bug introduced by
It seems like $this->getNextWordLength() can also be of type double; however, parameter $length of PEAR2\Net\Transmitter\TcpClient::receiveStream() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

689
            /** @scrutinizer ignore-type */ $this->getNextWordLength(),
Loading history...
690
            $filters,
691
            'stream word'
692
        );
693
        if (false !== $this->nextWordLock) {
694
            $this->trans->lock($this->nextWordLock, true);
695
        }
696
        $this->nextWordLength = null;
697
698
        return $stream;
699
    }
700
701
    /**
702
     * Decodes the length of the incoming message.
703
     *
704
     * Decodes the length of the incoming message, as specified by the RouterOS
705
     * API.
706
     *
707
     * @param T\Stream $trans The transmitter from which to decode the length of
708
     * the incoming message.
709
     *
710
     * @return int|double The decoded length.
711
     *     Is of type "double" only for values above "2 << 31".
712
     */
713
    public static function decodeLength(T\Stream $trans)
714
    {
715
        if ($trans->isPersistent() && $trans instanceof T\TcpClient) {
716
            $old = $trans->lock($trans::DIRECTION_RECEIVE);
717
            $length = self::_decodeLength($trans);
718
            $trans->lock($old, true);
719
            return $length;
720
        }
721
        return self::_decodeLength($trans);
722
    }
723
724
    /**
725
     * Decodes the length of the incoming message.
726
     *
727
     * Decodes the length of the incoming message, as specified by the RouterOS
728
     * API.
729
     *
730
     * Difference with the non private function is that this one doesn't perform
731
     * locking if the connection is a persistent one.
732
     *
733
     * @param T\Stream $trans The transmitter from which to decode the length of
734
     *     the incoming message.
735
     *
736
     * @return int|double The decoded length.
737
     *     Is of type "double" only for values above "2 << 31".
738
     */
739
    private static function _decodeLength(T\Stream $trans)
740
    {
741
        $byte = ord($trans->receive(1, 'initial length byte'));
742
        if ($byte & 0x80) {
743
            if (($byte & 0xC0) === 0x80) {
744
                return (($byte & 077) << 8 ) + ord($trans->receive(1));
745
            } elseif (($byte & 0xE0) === 0xC0) {
746
                $rem = unpack('n~', $trans->receive(2));
747
                return (($byte & 037) << 16 ) + $rem['~'];
748
            } elseif (($byte & 0xF0) === 0xE0) {
749
                $rem = unpack('n~/C~~', $trans->receive(3));
750
                return (($byte & 017) << 24 ) + ($rem['~'] << 8) + $rem['~~'];
751
            } elseif (($byte & 0xF8) === 0xF0) {
752
                $rem = unpack('N~', $trans->receive(4));
753
                return (($byte & 007) * 0x100000000/* '<< 32' or '2^32' */)
754
                    + (double) sprintf('%u', $rem['~']);
755
            }
756
            throw new NotSupportedException(
757
                'Unknown control byte encountered.',
758
                NotSupportedException::CODE_CONTROL_BYTE,
759
                null,
760
                $byte
761
            );
762
        } else {
763
            return $byte;
764
        }
765
    }
766
767
    /**
768
     * Closes the opened connection, even if it is a persistent one.
769
     *
770
     * @return bool TRUE on success, FALSE on failure.
771
     */
772
    public function close()
773
    {
774
        return $this->trans->close();
775
    }
776
}
777