Passed
Push — develop ( 9a09e8...d94677 )
by Vasil
08:01 queued 04:40
created

Communicator::sendWord()   C

Complexity

Conditions 8
Paths 22

Size

Total Lines 46
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 33
nc 22
nop 1
dl 0
loc 46
rs 5.5555
c 0
b 0
f 0
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. And for the sake of simplicity, if you specify an encryption,
134
     *     don't specify a context and your default context uses the value
135
     *     "DEFAULT" for ciphers, "ADH" will be automatically added to the list
136
     *     of ciphers.
137
     * @param resource|null $context A context for the socket.
138
     *
139
     * @see sendWord()
140
     */
141
    public function __construct(
142
        $host,
143
        $port = 8728,
144
        $persist = false,
145
        $timeout = null,
146
        $key = '',
147
        $crypto = T\NetworkStream::CRYPTO_OFF,
148
        $context = null
149
    ) {
150
        $isUnencrypted = T\NetworkStream::CRYPTO_OFF === $crypto;
151
        if (($context === null) && !$isUnencrypted) {
152
            $context = stream_context_get_default();
153
            $opts = stream_context_get_options($context);
154
            $verifyPeer = isset($opts['ssl']['cafile'])
155
                || isset($opts['ssl']['capath'])
156
                || !!ini_get('openssl.cafile')
157
                || !!ini_get('openssl.capath');
158
            $newOpts = array(
159
                'ssl' => array(
160
                    'verify_peer' => $verifyPeer,
161
                    'verify_peer_name' => $verifyPeer
162
                )
163
            );
164
            if (!$verifyPeer
165
                && (!isset($opts['ssl']['ciphers'])
166
                || 'DEFAULT' === $opts['ssl']['ciphers'])
167
            ) {
168
                $newOpts['ssl']['ciphers'] = 'ADH';
169
            }
170
            stream_context_set_option(
171
                $context,
172
                $newOpts
173
            );
174
        }
175
        // @codeCoverageIgnoreStart
176
        // The $port is customizable in testing.
177
        if (null === $port) {
178
            $port = $isUnencrypted ? 8728 : 8729;
179
        }
180
        // @codeCoverageIgnoreEnd
181
182
        try {
183
            $this->trans = new T\TcpClient(
184
                $host,
185
                $port,
186
                $persist,
187
                $timeout,
188
                $key,
189
                $crypto,
190
                $context
191
            );
192
        } catch (T\Exception $e) {
193
            throw new SocketException(
194
                'Error connecting to RouterOS',
195
                SocketException::CODE_CONNECTION_FAIL,
196
                $e
197
            );
198
        }
199
        $this->setCharset(
200
            self::getDefaultCharset(self::CHARSET_ALL),
201
            self::CHARSET_ALL
202
        );
203
    }
204
205
    /**
206
     * A shorthand gateway.
207
     *
208
     * This is a magic PHP method that allows you to call the object as a
209
     * function. Depending on the argument given, one of the other functions in
210
     * the class is invoked and its returned value is returned by this function.
211
     *
212
     * @param string|null $string A string of the word to send, or NULL to get
213
     *     the next word as a string.
214
     *
215
     * @return int|string If a string is provided, returns the number of bytes
216
     *     sent, otherwise returns the next word as a string.
217
     */
218
    public function __invoke($string = null)
219
    {
220
        return null === $string ? $this->getNextWord()
221
            : $this->sendWord($string);
222
    }
223
224
    /**
225
     * Checks whether a variable is a seekable stream resource.
226
     *
227
     * @param mixed $var The value to check.
228
     *
229
     * @return bool TRUE if $var is a seekable stream, FALSE otherwise.
230
     */
231
    public static function isSeekableStream($var)
232
    {
233
        if (T\Stream::isStream($var)) {
234
            $meta = stream_get_meta_data($var);
235
            return $meta['seekable'];
236
        }
237
        return false;
238
    }
239
240
    /**
241
     * Uses iconv to convert a stream from one charset to another.
242
     *
243
     * @param string   $inCharset  The charset of the stream.
244
     * @param string   $outCharset The desired resulting charset.
245
     * @param resource $stream     The stream to convert. The stream is assumed
246
     *     to be seekable, and is read from its current position to its end,
247
     *     after which, it is seeked back to its starting position.
248
     *
249
     * @return resource A new stream that uses the $out_charset. The stream is a
250
     *     subset from the original stream, from its current position to its
251
     *     end, seeked at its start.
252
     */
253
    public static function iconvStream($inCharset, $outCharset, $stream)
254
    {
255
        $bytes = 0;
256
        $result = fopen('php://temp', 'r+b');
257
        $iconvFilter = stream_filter_append(
258
            $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

258
            /** @scrutinizer ignore-type */ $result,
Loading history...
259
            'convert.iconv.' . $inCharset . '.' . $outCharset,
260
            STREAM_FILTER_WRITE
261
        );
262
263
        flock($stream, LOCK_SH);
264
        $reader = new T\Stream($stream, false);
265
        $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

265
        $writer = new T\Stream(/** @scrutinizer ignore-type */ $result, false);
Loading history...
266
        $chunkSize = $reader->getChunk(T\Stream::DIRECTION_RECEIVE);
267
        while ($reader->isAvailable() && $reader->isDataAwaiting()) {
268
            $bytes += $writer->send(fread($stream, $chunkSize));
269
        }
270
        fseek($stream, -$bytes, SEEK_CUR);
271
        flock($stream, LOCK_UN);
272
273
        stream_filter_remove($iconvFilter);
274
        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

274
        rewind(/** @scrutinizer ignore-type */ $result);
Loading history...
275
        return $result;
276
    }
277
278
    /**
279
     * Sets the default charset(s) for new connections.
280
     *
281
     * @param mixed $charset     The charset to set. If $charsetType is
282
     *     {@link self::CHARSET_ALL}, you can supply either a string to use for
283
     *     all charsets, or an array with the charset types as keys, and the
284
     *     charsets as values.
285
     * @param int   $charsetType Which charset to set. Valid values are the
286
     *     CHARSET_* constants. Any other value is treated as
287
     *     {@link self::CHARSET_ALL}.
288
     *
289
     * @return string|array The old charset. If $charsetType is
290
     *     {@link self::CHARSET_ALL}, the old values will be returned as an
291
     *     array with the types as keys, and charsets as values.
292
     *
293
     * @see setCharset()
294
     */
295
    public static function setDefaultCharset(
296
        $charset,
297
        $charsetType = self::CHARSET_ALL
298
    ) {
299
        if (array_key_exists($charsetType, self::$defaultCharsets)) {
300
             $oldCharset = self::$defaultCharsets[$charsetType];
301
             self::$defaultCharsets[$charsetType] = $charset;
302
             return $oldCharset;
303
        } else {
304
            $oldCharsets = self::$defaultCharsets;
305
            self::$defaultCharsets = is_array($charset) ? $charset : array_fill(
306
                0,
307
                count(self::$defaultCharsets),
308
                $charset
309
            );
310
            return $oldCharsets;
311
        }
312
    }
313
314
    /**
315
     * Gets the default charset(s).
316
     *
317
     * @param int $charsetType Which charset to get. Valid values are the
318
     *     CHARSET_* constants. Any other value is treated as
319
     *     {@link self::CHARSET_ALL}.
320
     *
321
     * @return string|array The current charset. If $charsetType is
322
     *     {@link self::CHARSET_ALL}, the current values will be returned as an
323
     *     array with the types as keys, and charsets as values.
324
     *
325
     * @see setDefaultCharset()
326
     */
327
    public static function getDefaultCharset($charsetType)
328
    {
329
        return array_key_exists($charsetType, self::$defaultCharsets)
330
            ? self::$defaultCharsets[$charsetType] : self::$defaultCharsets;
331
    }
332
333
    /**
334
     * Gets the length of a seekable stream.
335
     *
336
     * Gets the length of a seekable stream.
337
     *
338
     * @param resource $stream The stream to check. The stream is assumed to be
339
     *     seekable.
340
     *
341
     * @return double The number of bytes in the stream between its current
342
     *     position and its end.
343
     */
344
    public static function seekableStreamLength($stream)
345
    {
346
        $streamPosition = (double) sprintf('%u', ftell($stream));
347
        fseek($stream, 0, SEEK_END);
348
        $streamLength = ((double) sprintf('%u', ftell($stream)))
349
            - $streamPosition;
350
        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

350
        fseek($stream, /** @scrutinizer ignore-type */ $streamPosition, SEEK_SET);
Loading history...
351
        return $streamLength;
352
    }
353
354
    /**
355
     * Sets the charset(s) for this connection.
356
     *
357
     * Sets the charset(s) for this connection. The specified charset(s) will be
358
     * used for all future words. When sending, {@link self::CHARSET_LOCAL} is
359
     * converted to {@link self::CHARSET_REMOTE}, and when receiving,
360
     * {@link self::CHARSET_REMOTE} is converted to {@link self::CHARSET_LOCAL}.
361
     * Setting  NULL to either charset will disable charset conversion, and data
362
     * will be both sent and received "as is".
363
     *
364
     * @param mixed $charset     The charset to set. If $charsetType is
365
     *     {@link self::CHARSET_ALL}, you can supply either a string to use for
366
     *     all charsets, or an array with the charset types as keys, and the
367
     *     charsets as values.
368
     * @param int   $charsetType Which charset to set. Valid values are the
369
     *     CHARSET_* constants. Any other value is treated as
370
     *     {@link self::CHARSET_ALL}.
371
     *
372
     * @return string|array The old charset. If $charsetType is
373
     *     {@link self::CHARSET_ALL}, the old values will be returned as an
374
     *     array with the types as keys, and charsets as values.
375
     *
376
     * @see setDefaultCharset()
377
     */
378
    public function setCharset($charset, $charsetType = self::CHARSET_ALL)
379
    {
380
        if (array_key_exists($charsetType, $this->charsets)) {
381
             $oldCharset = $this->charsets[$charsetType];
382
             $this->charsets[$charsetType] = $charset;
383
             return $oldCharset;
384
        } else {
385
            $oldCharsets = $this->charsets;
386
            $this->charsets = is_array($charset) ? $charset : array_fill(
387
                0,
388
                count($this->charsets),
389
                $charset
390
            );
391
            return $oldCharsets;
392
        }
393
    }
394
395
    /**
396
     * Gets the charset(s) for this connection.
397
     *
398
     * @param int $charsetType Which charset to get. Valid values are the
399
     *     CHARSET_* constants. Any other value is treated as
400
     *     {@link self::CHARSET_ALL}.
401
     *
402
     * @return string|array The current charset. If $charsetType is
403
     *     {@link self::CHARSET_ALL}, the current values will be returned as an
404
     *     array with the types as keys, and charsets as values.
405
     *
406
     * @see getDefaultCharset()
407
     * @see setCharset()
408
     */
409
    public function getCharset($charsetType)
410
    {
411
        return array_key_exists($charsetType, $this->charsets)
412
            ? $this->charsets[$charsetType] : $this->charsets;
413
    }
414
415
    /**
416
     * Gets the transmitter for this connection.
417
     *
418
     * @return T\TcpClient The transmitter for this connection.
419
     */
420
    public function getTransmitter()
421
    {
422
        return $this->trans;
423
    }
424
425
    /**
426
     * Sends a word.
427
     *
428
     * Sends a word and automatically encodes its length when doing so.
429
     *
430
     * @param string|resource $word     The word to send, as a string
431
     * or seekable stream.
432
     * @param string|resource $word,... Additional word fragments to be sent
433
     * as a single word.
434
     *
435
     * @return int The number of bytes sent.
436
     *
437
     * @see sendWordFromStream()
438
     * @see getNextWord()
439
     */
440
    public function sendWord($word)
441
    {
442
        $length = 0;
443
        $isCharsetConversionEnabled
444
            = null !== ($rCharset = $this->getCharset(self::CHARSET_REMOTE))
445
            && null !== ($lCharset = $this->getCharset(self::CHARSET_LOCAL));
446
        $wordFragments = array();
447
        foreach (func_get_args() as $word) {
448
            if (is_string($word)) {
449
                if ($isCharsetConversionEnabled) {
450
                    $word = iconv(
451
                        $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...
452
                        $rCharset . '//IGNORE//TRANSLIT',
453
                        $word
454
                    );
455
                }
456
                $length += strlen($word);
457
            } else {
458
                if (!self::isSeekableStream($word)) {
459
                    throw new InvalidArgumentException(
460
                        'Only seekable streams can be sent.',
461
                        InvalidArgumentException::CODE_SEEKABLE_REQUIRED
462
                    );
463
                }
464
                if ($isCharsetConversionEnabled) {
465
                    $word = self::iconvStream(
466
                        $lCharset,
467
                        $rCharset . '//IGNORE//TRANSLIT',
468
                        $word
469
                    );
470
                }
471
                flock($word, LOCK_SH);
472
                $length += self::seekableStreamLength($word);
473
            }
474
            $wordFragments[] = $word;
475
        }
476
        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

476
        static::verifyLengthSupport(/** @scrutinizer ignore-type */ $length);
Loading history...
477
        if ($this->trans->isPersistent()) {
478
            $old = $this->trans->lock(T\Stream::DIRECTION_SEND);
479
            $bytesSent = $this->_sendWord($length, $wordFragments);
480
            $this->trans->lock($old, true);
0 ignored issues
show
Bug introduced by
$old of type false is incompatible with the type integer expected by parameter $direction of PEAR2\Net\Transmitter\TcpClient::lock(). ( Ignorable by Annotation )

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

480
            $this->trans->lock(/** @scrutinizer ignore-type */ $old, true);
Loading history...
481
482
            return $bytesSent;
483
        }
484
485
        return $this->_sendWord($length, $wordFragments);
486
    }
487
488
    /**
489
     * Send the word fragments
490
     *
491
     * @param int|double          $length        The previously computed
492
     *     length of all fragments.
493
     * @param (string|resource)[] $wordFragments The fragments to send.
494
     *
495
     * @return int The number of bytes sent.
496
     */
497
    private function _sendWord($length, $wordFragments)
498
    {
499
        $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

499
        $bytesSent = $this->trans->send(self::encodeLength(/** @scrutinizer ignore-type */ $length));
Loading history...
500
        foreach ($wordFragments as $fragment) {
501
            $bytesSent += $this->trans->send($fragment);
502
            if (!is_string($fragment)) {
503
                flock($fragment, LOCK_UN);
504
            }
505
        }
506
        return $bytesSent;
507
    }
508
509
    /**
510
     * Verifies that the length is supported.
511
     *
512
     * Verifies if the specified length is supported by the API. Throws a
513
     * {@link LengthException} if that's not the case. Currently, RouterOS
514
     * supports words up to 0xFFFFFFFF in length, so that's the only check
515
     * performed.
516
     *
517
     * @param int $length The length to verify.
518
     *
519
     * @return void
520
     */
521
    public static function verifyLengthSupport($length)
522
    {
523
        if ($length > 0xFFFFFFFF) {
524
            throw new LengthException(
525
                'Words with length above 0xFFFFFFFF are not supported.',
526
                LengthException::CODE_UNSUPPORTED,
527
                null,
528
                $length
529
            );
530
        }
531
    }
532
533
    /**
534
     * Encodes the length as required by the RouterOS API.
535
     *
536
     * @param int $length The length to encode.
537
     *
538
     * @return string The encoded length.
539
     */
540
    public static function encodeLength($length)
541
    {
542
        if ($length < 0) {
543
            throw new LengthException(
544
                'Length must not be negative.',
545
                LengthException::CODE_INVALID,
546
                null,
547
                $length
548
            );
549
        } elseif ($length < 0x80) {
550
            return chr($length);
551
        } elseif ($length < 0x4000) {
552
            return pack('n', $length |= 0x8000);
553
        } elseif ($length < 0x200000) {
554
            $length |= 0xC00000;
555
            return pack('n', $length >> 8) . chr($length & 0xFF);
556
        } elseif ($length < 0x10000000) {
557
            return pack('N', $length |= 0xE0000000);
558
        } elseif ($length <= 0xFFFFFFFF) {
559
            return chr(0xF0) . pack('N', $length);
560
        } elseif ($length <= 0x7FFFFFFFF) {
561
            $length = 'f' . base_convert($length, 10, 16);
562
            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

562
            return chr(/** @scrutinizer ignore-type */ hexdec(substr($length, 0, 2))) .
Loading history...
563
                pack('N', hexdec(substr($length, 2)));
564
        }
565
        throw new LengthException(
566
            'Length must not be above 0x7FFFFFFFF.',
567
            LengthException::CODE_BEYOND_SHEME,
568
            null,
569
            $length
570
        );
571
    }
572
573
    /**
574
     * Get the length of the next word in queue.
575
     *
576
     * Get the length of the next word in queue without getting the word.
577
     * For pesisntent connections, note that the underlying transmitter will
578
     * be locked for receiving until either {@link self::getNextWord()} or
579
     * {@link self::getNextWordAsStream()} is called.
580
     *
581
     * @return int|double
582
     */
583
    public function getNextWordLength()
584
    {
585
        if (null === $this->nextWordLength) {
586
            if ($this->trans->isPersistent()) {
587
                $this->nextWordLock = $this->trans->lock(
588
                    T\Stream::DIRECTION_RECEIVE
589
                );
590
            }
591
            $this->nextWordLength = self::decodeLength($this->trans);
592
        }
593
        return $this->nextWordLength;
594
    }
595
596
    /**
597
     * Get the next word in queue as a string.
598
     *
599
     * Get the next word in queue as a string, after automatically decoding its
600
     * length.
601
     *
602
     * @return string The word.
603
     *
604
     * @see close()
605
     */
606
    public function getNextWord()
607
    {
608
        $word = $this->trans->receive(
609
            $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

609
            /** @scrutinizer ignore-type */ $this->getNextWordLength(),
Loading history...
610
            'word'
611
        );
612
        if (false !== $this->nextWordLock) {
613
            $this->trans->lock($this->nextWordLock, true);
614
        }
615
        $this->nextWordLength = null;
616
617
        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...
618
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
619
        ) {
620
            $word = iconv(
621
                $remoteCharset,
622
                $localCharset . '//IGNORE//TRANSLIT',
623
                $word
624
            );
625
        }
626
627
        return $word;
628
    }
629
630
    /**
631
     * Get the next word in queue as a stream.
632
     *
633
     * Get the next word in queue as a stream, after automatically decoding its
634
     * length.
635
     *
636
     * @return resource The word, as a stream.
637
     *
638
     * @see close()
639
     */
640
    public function getNextWordAsStream()
641
    {
642
        $filters = new T\FilterCollection();
643
        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...
644
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
645
        ) {
646
            $filters->append(
647
                'convert.iconv.' .
648
                $remoteCharset . '.' . $localCharset . '//IGNORE//TRANSLIT'
649
            );
650
        }
651
652
        $stream = $this->trans->receiveStream(
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::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

653
            /** @scrutinizer ignore-type */ $this->getNextWordLength(),
Loading history...
654
            $filters,
655
            'stream word'
656
        );
657
        if (false !== $this->nextWordLock) {
658
            $this->trans->lock($this->nextWordLock, true);
659
        }
660
        $this->nextWordLength = null;
661
662
        return $stream;
663
    }
664
665
    /**
666
     * Decodes the length of the incoming message.
667
     *
668
     * Decodes the length of the incoming message, as specified by the RouterOS
669
     * API.
670
     *
671
     * @param T\Stream $trans The transmitter from which to decode the length of
672
     * the incoming message.
673
     *
674
     * @return int|double The decoded length.
675
     *     Is of type "double" only for values above "2 << 31".
676
     */
677
    public static function decodeLength(T\Stream $trans)
678
    {
679
        if ($trans->isPersistent() && $trans instanceof T\TcpClient) {
680
            $old = $trans->lock($trans::DIRECTION_RECEIVE);
681
            $length = self::_decodeLength($trans);
682
            $trans->lock($old, true);
0 ignored issues
show
Bug introduced by
$old of type false is incompatible with the type integer expected by parameter $direction of PEAR2\Net\Transmitter\TcpClient::lock(). ( Ignorable by Annotation )

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

682
            $trans->lock(/** @scrutinizer ignore-type */ $old, true);
Loading history...
683
            return $length;
684
        }
685
        return self::_decodeLength($trans);
686
    }
687
688
    /**
689
     * Decodes the length of the incoming message.
690
     *
691
     * Decodes the length of the incoming message, as specified by the RouterOS
692
     * API.
693
     *
694
     * Difference with the non private function is that this one doesn't perform
695
     * locking if the connection is a persistent one.
696
     *
697
     * @param T\Stream $trans The transmitter from which to decode the length of
698
     *     the incoming message.
699
     *
700
     * @return int|double The decoded length.
701
     *     Is of type "double" only for values above "2 << 31".
702
     */
703
    private static function _decodeLength(T\Stream $trans)
704
    {
705
        $byte = ord($trans->receive(1, 'initial length byte'));
706
        if ($byte & 0x80) {
707
            if (($byte & 0xC0) === 0x80) {
708
                return (($byte & 077) << 8 ) + ord($trans->receive(1));
709
            } elseif (($byte & 0xE0) === 0xC0) {
710
                $rem = unpack('n~', $trans->receive(2));
711
                return (($byte & 037) << 16 ) + $rem['~'];
712
            } elseif (($byte & 0xF0) === 0xE0) {
713
                $rem = unpack('n~/C~~', $trans->receive(3));
714
                return (($byte & 017) << 24 ) + ($rem['~'] << 8) + $rem['~~'];
715
            } elseif (($byte & 0xF8) === 0xF0) {
716
                $rem = unpack('N~', $trans->receive(4));
717
                return (($byte & 007) * 0x100000000/* '<< 32' or '2^32' */)
718
                    + (double) sprintf('%u', $rem['~']);
719
            }
720
            throw new NotSupportedException(
721
                'Unknown control byte encountered.',
722
                NotSupportedException::CODE_CONTROL_BYTE,
723
                null,
724
                $byte
725
            );
726
        } else {
727
            return $byte;
728
        }
729
    }
730
731
    /**
732
     * Closes the opened connection, even if it is a persistent one.
733
     *
734
     * @return bool TRUE on success, FALSE on failure.
735
     */
736
    public function close()
737
    {
738
        return $this->trans->close();
739
    }
740
}
741