Communicator::_decodeLength()   B
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 28
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 6
eloc 21
c 1
b 1
f 0
nc 6
nop 1
dl 0
loc 28
rs 8.9617
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 = (bool)$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
                    ) ? (bool)$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,
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);
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);
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));
0 ignored issues
show
introduced by
The condition null !== $lCharset = $th...et(self::CHARSET_LOCAL) is always true.
Loading history...
482
        $wordFragments = array();
483
        foreach (func_get_args() as $word) {
0 ignored issues
show
introduced by
$word is overwriting one of the parameters of this function.
Loading history...
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...
Bug introduced by
It seems like $lCharset can also be of type array<string,null|string>; however, parameter $from_encoding of iconv() does only seem to accept string, 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

487
                        /** @scrutinizer ignore-type */ $lCharset,
Loading history...
488
                        $rCharset . '//IGNORE//TRANSLIT',
0 ignored issues
show
Bug introduced by
Are you sure $rCharset of type array<string,null|string>|string can be used in concatenation? ( Ignorable by Annotation )

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

488
                        /** @scrutinizer ignore-type */ $rCharset . '//IGNORE//TRANSLIT',
Loading history...
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,
0 ignored issues
show
Bug introduced by
It seems like $lCharset can also be of type array<string,null|string>; however, parameter $inCharset of PEAR2\Net\RouterOS\Communicator::iconvStream() does only seem to accept string, 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

502
                        /** @scrutinizer ignore-type */ $lCharset,
Loading history...
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);
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|double $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
        }
586
        if ($length < 0x80) {
587
            return chr($length);
588
        }
589
        if ($length < 0x4000) {
590
            return pack('n', $length |= 0x8000);
591
        }
592
        if ($length < 0x200000) {
593
            $length |= 0xC00000;
594
            return pack('n', $length >> 8) . chr($length & 0xFF);
595
        }
596
        if ($length < 0x10000000) {
597
            return pack('N', $length |= 0xE0000000);
598
        }
599
        if ($length <= 0xFFFFFFFF) {
600
            return chr(0xF0) . pack('N', $length);
601
        }
602
        if ($length <= 0x7FFFFFFFF) {
603
            $lengthHex = 'f' . base_convert($length, 10, 16);
604
            return chr(/** @scrutinizer ignore-type */ hexdec(
605
                    substr($lengthHex, 0, 2)
606
                )) .
607
                pack('N', hexdec(substr($lengthHex, 2)));
608
        }
609
        throw new LengthException(
610
            'Length must not be above 0x7FFFFFFFF.',
611
            LengthException::CODE_BEYOND_SHEME,
612
            null,
613
            $length
614
        );
615
    }
616
617
    /**
618
     * Get the length of the next word in queue.
619
     *
620
     * Get the length of the next word in queue without getting the word.
621
     * For pesisntent connections, note that the underlying transmitter will
622
     * be locked for receiving until either {@link self::getNextWord()} or
623
     * {@link self::getNextWordAsStream()} is called.
624
     *
625
     * @return int|double
626
     */
627
    public function getNextWordLength()
628
    {
629
        if (null === $this->nextWordLength) {
630
            if ($this->trans->isPersistent()) {
631
                $this->nextWordLock = $this->trans->lock(
632
                    T\Stream::DIRECTION_RECEIVE
633
                );
634
            }
635
            $this->nextWordLength = self::decodeLength($this->trans);
636
        }
637
        return $this->nextWordLength;
638
    }
639
640
    /**
641
     * Get the next word in queue as a string.
642
     *
643
     * Get the next word in queue as a string, after automatically decoding its
644
     * length.
645
     *
646
     * @return string The word.
647
     *
648
     * @see close()
649
     */
650
    public function getNextWord()
651
    {
652
        $word = $this->trans->receive(
653
            $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

653
            /** @scrutinizer ignore-type */ $this->getNextWordLength(),
Loading history...
654
            'word'
655
        );
656
        if (false !== $this->nextWordLock) {
657
            $this->trans->lock($this->nextWordLock, true);
0 ignored issues
show
Bug introduced by
It seems like $this->nextWordLock can also be of type true; however, parameter $direction of PEAR2\Net\Transmitter\TcpClient::lock() 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

657
            $this->trans->lock(/** @scrutinizer ignore-type */ $this->nextWordLock, true);
Loading history...
658
        }
659
        $this->nextWordLength = null;
660
661
        if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
662
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
0 ignored issues
show
introduced by
The condition null !== $localCharset =...et(self::CHARSET_LOCAL) is always true.
Loading history...
663
        ) {
664
            $word = iconv(
665
                $remoteCharset,
0 ignored issues
show
Bug introduced by
It seems like $remoteCharset can also be of type array<string,null|string>; however, parameter $from_encoding of iconv() does only seem to accept string, 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

665
                /** @scrutinizer ignore-type */ $remoteCharset,
Loading history...
666
                $localCharset . '//IGNORE//TRANSLIT',
0 ignored issues
show
Bug introduced by
Are you sure $localCharset of type array<string,null|string>|string can be used in concatenation? ( Ignorable by Annotation )

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

666
                /** @scrutinizer ignore-type */ $localCharset . '//IGNORE//TRANSLIT',
Loading history...
667
                $word
668
            );
669
        }
670
671
        return $word;
672
    }
673
674
    /**
675
     * Get the next word in queue as a stream.
676
     *
677
     * Get the next word in queue as a stream, after automatically decoding its
678
     * length.
679
     *
680
     * @return resource The word, as a stream.
681
     *
682
     * @see close()
683
     */
684
    public function getNextWordAsStream()
685
    {
686
        $filters = new T\FilterCollection();
687
        if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
688
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
0 ignored issues
show
introduced by
The condition null !== $localCharset =...et(self::CHARSET_LOCAL) is always true.
Loading history...
689
        ) {
690
            $filters->append(
691
                'convert.iconv.' .
692
                $remoteCharset . '.' . $localCharset . '//IGNORE//TRANSLIT'
0 ignored issues
show
Bug introduced by
Are you sure $remoteCharset of type array<string,null|string>|string can be used in concatenation? ( Ignorable by Annotation )

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

692
                /** @scrutinizer ignore-type */ $remoteCharset . '.' . $localCharset . '//IGNORE//TRANSLIT'
Loading history...
Bug introduced by
Are you sure $localCharset of type array<string,null|string>|string can be used in concatenation? ( Ignorable by Annotation )

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

692
                $remoteCharset . '.' . /** @scrutinizer ignore-type */ $localCharset . '//IGNORE//TRANSLIT'
Loading history...
693
            );
694
        }
695
696
        $stream = $this->trans->receiveStream(
697
            $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

697
            /** @scrutinizer ignore-type */ $this->getNextWordLength(),
Loading history...
698
            $filters,
699
            'stream word'
700
        );
701
        if (false !== $this->nextWordLock) {
702
            $this->trans->lock($this->nextWordLock, true);
0 ignored issues
show
Bug introduced by
It seems like $this->nextWordLock can also be of type true; however, parameter $direction of PEAR2\Net\Transmitter\TcpClient::lock() 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

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