Completed
Push — develop ( b42f4c...d20a7c )
by Vasil
03:07
created

Communicator::sendWord()   C

Complexity

Conditions 12
Paths 62

Size

Total Lines 62
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 62
rs 6.2072
c 0
b 0
f 0
cc 12
eloc 43
nc 62
nop 1

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. 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
            if (!isset($opts['ssl']['ciphers'])
155
                || 'DEFAULT' === $opts['ssl']['ciphers']
156
            ) {
157
                stream_context_set_option(
158
                    $context,
159
                    array(
160
                        'ssl' => array(
161
                            'ciphers' => 'ADH',
162
                            'verify_peer' => false,
163
                            'verify_peer_name' => false
164
                        )
165
                    )
166
                );
167
            }
168
        }
169
        // @codeCoverageIgnoreStart
170
        // The $port is customizable in testing.
171
        if (null === $port) {
172
            $port = $isUnencrypted ? 8728 : 8729;
173
        }
174
        // @codeCoverageIgnoreEnd
175
176
        try {
177
            $this->trans = new T\TcpClient(
178
                $host,
179
                $port,
180
                $persist,
181
                $timeout,
182
                $key,
183
                $crypto,
184
                $context
185
            );
186
        } catch (T\Exception $e) {
187
            throw new SocketException(
188
                'Error connecting to RouterOS',
189
                SocketException::CODE_CONNECTION_FAIL,
190
                $e
191
            );
192
        }
193
        $this->setCharset(
194
            self::getDefaultCharset(self::CHARSET_ALL),
195
            self::CHARSET_ALL
196
        );
197
    }
198
199
    /**
200
     * A shorthand gateway.
201
     *
202
     * This is a magic PHP method that allows you to call the object as a
203
     * function. Depending on the argument given, one of the other functions in
204
     * the class is invoked and its returned value is returned by this function.
205
     *
206
     * @param string|null $string A string of the word to send, or NULL to get
207
     *     the next word as a string.
208
     *
209
     * @return int|string If a string is provided, returns the number of bytes
210
     *     sent, otherwise returns the next word as a string.
211
     */
212
    public function __invoke($string = null)
213
    {
214
        return null === $string ? $this->getNextWord()
215
            : $this->sendWord($string);
216
    }
217
218
    /**
219
     * Checks whether a variable is a seekable stream resource.
220
     *
221
     * @param mixed $var The value to check.
222
     *
223
     * @return bool TRUE if $var is a seekable stream, FALSE otherwise.
224
     */
225
    public static function isSeekableStream($var)
226
    {
227
        if (T\Stream::isStream($var)) {
228
            $meta = stream_get_meta_data($var);
229
            return $meta['seekable'];
230
        }
231
        return false;
232
    }
233
234
    /**
235
     * Uses iconv to convert a stream from one charset to another.
236
     *
237
     * @param string   $inCharset  The charset of the stream.
238
     * @param string   $outCharset The desired resulting charset.
239
     * @param resource $stream     The stream to convert. The stream is assumed
240
     *     to be seekable, and is read from its current position to its end,
241
     *     after which, it is seeked back to its starting position.
242
     *
243
     * @return resource A new stream that uses the $out_charset. The stream is a
244
     *     subset from the original stream, from its current position to its
245
     *     end, seeked at its start.
246
     */
247
    public static function iconvStream($inCharset, $outCharset, $stream)
248
    {
249
        $bytes = 0;
250
        $result = fopen('php://temp', 'r+b');
251
        $iconvFilter = stream_filter_append(
252
            $result,
253
            'convert.iconv.' . $inCharset . '.' . $outCharset,
254
            STREAM_FILTER_WRITE
255
        );
256
257
        flock($stream, LOCK_SH);
258
        $reader = new T\Stream($stream, false);
259
        $writer = new T\Stream($result, false);
260
        $chunkSize = $reader->getChunk(T\Stream::DIRECTION_RECEIVE);
261
        while ($reader->isAvailable() && $reader->isDataAwaiting()) {
262
            $bytes += $writer->send(fread($stream, $chunkSize));
263
        }
264
        fseek($stream, -$bytes, SEEK_CUR);
265
        flock($stream, LOCK_UN);
266
267
        stream_filter_remove($iconvFilter);
268
        rewind($result);
269
        return $result;
270
    }
271
272
    /**
273
     * Sets the default charset(s) for new connections.
274
     *
275
     * @param mixed $charset     The charset to set. If $charsetType is
276
     *     {@link self::CHARSET_ALL}, you can supply either a string to use for
277
     *     all charsets, or an array with the charset types as keys, and the
278
     *     charsets as values.
279
     * @param int   $charsetType Which charset to set. Valid values are the
280
     *     CHARSET_* constants. Any other value is treated as
281
     *     {@link self::CHARSET_ALL}.
282
     *
283
     * @return string|array The old charset. If $charsetType is
284
     *     {@link self::CHARSET_ALL}, the old values will be returned as an
285
     *     array with the types as keys, and charsets as values.
286
     *
287
     * @see setCharset()
288
     */
289
    public static function setDefaultCharset(
290
        $charset,
291
        $charsetType = self::CHARSET_ALL
292
    ) {
293
        if (array_key_exists($charsetType, self::$defaultCharsets)) {
294
             $oldCharset = self::$defaultCharsets[$charsetType];
295
             self::$defaultCharsets[$charsetType] = $charset;
296
             return $oldCharset;
297
        } else {
298
            $oldCharsets = self::$defaultCharsets;
299
            self::$defaultCharsets = is_array($charset) ? $charset : array_fill(
300
                0,
301
                count(self::$defaultCharsets),
302
                $charset
303
            );
304
            return $oldCharsets;
305
        }
306
    }
307
308
    /**
309
     * Gets the default charset(s).
310
     *
311
     * @param int $charsetType Which charset to get. Valid values are the
312
     *     CHARSET_* constants. Any other value is treated as
313
     *     {@link self::CHARSET_ALL}.
314
     *
315
     * @return string|array The current charset. If $charsetType is
316
     *     {@link self::CHARSET_ALL}, the current values will be returned as an
317
     *     array with the types as keys, and charsets as values.
318
     *
319
     * @see setDefaultCharset()
320
     */
321
    public static function getDefaultCharset($charsetType)
322
    {
323
        return array_key_exists($charsetType, self::$defaultCharsets)
324
            ? self::$defaultCharsets[$charsetType] : self::$defaultCharsets;
325
    }
326
327
    /**
328
     * Gets the length of a seekable stream.
329
     *
330
     * Gets the length of a seekable stream.
331
     *
332
     * @param resource $stream The stream to check. The stream is assumed to be
333
     *     seekable.
334
     *
335
     * @return double The number of bytes in the stream between its current
336
     *     position and its end.
337
     */
338
    public static function seekableStreamLength($stream)
339
    {
340
        $streamPosition = (double) sprintf('%u', ftell($stream));
341
        fseek($stream, 0, SEEK_END);
342
        $streamLength = ((double) sprintf('%u', ftell($stream)))
343
            - $streamPosition;
344
        fseek($stream, $streamPosition, SEEK_SET);
345
        return $streamLength;
346
    }
347
348
    /**
349
     * Sets the charset(s) for this connection.
350
     *
351
     * Sets the charset(s) for this connection. The specified charset(s) will be
352
     * used for all future words. When sending, {@link self::CHARSET_LOCAL} is
353
     * converted to {@link self::CHARSET_REMOTE}, and when receiving,
354
     * {@link self::CHARSET_REMOTE} is converted to {@link self::CHARSET_LOCAL}.
355
     * Setting  NULL to either charset will disable charset conversion, and data
356
     * will be both sent and received "as is".
357
     *
358
     * @param mixed $charset     The charset to set. If $charsetType is
359
     *     {@link self::CHARSET_ALL}, you can supply either a string to use for
360
     *     all charsets, or an array with the charset types as keys, and the
361
     *     charsets as values.
362
     * @param int   $charsetType Which charset to set. Valid values are the
363
     *     CHARSET_* constants. Any other value is treated as
364
     *     {@link self::CHARSET_ALL}.
365
     *
366
     * @return string|array The old charset. If $charsetType is
367
     *     {@link self::CHARSET_ALL}, the old values will be returned as an
368
     *     array with the types as keys, and charsets as values.
369
     *
370
     * @see setDefaultCharset()
371
     */
372
    public function setCharset($charset, $charsetType = self::CHARSET_ALL)
373
    {
374
        if (array_key_exists($charsetType, $this->charsets)) {
375
             $oldCharset = $this->charsets[$charsetType];
376
             $this->charsets[$charsetType] = $charset;
377
             return $oldCharset;
378
        } else {
379
            $oldCharsets = $this->charsets;
380
            $this->charsets = is_array($charset) ? $charset : array_fill(
381
                0,
382
                count($this->charsets),
383
                $charset
384
            );
385
            return $oldCharsets;
386
        }
387
    }
388
389
    /**
390
     * Gets the charset(s) for this connection.
391
     *
392
     * @param int $charsetType Which charset to get. Valid values are the
393
     *     CHARSET_* constants. Any other value is treated as
394
     *     {@link self::CHARSET_ALL}.
395
     *
396
     * @return string|array The current charset. If $charsetType is
397
     *     {@link self::CHARSET_ALL}, the current values will be returned as an
398
     *     array with the types as keys, and charsets as values.
399
     *
400
     * @see getDefaultCharset()
401
     * @see setCharset()
402
     */
403
    public function getCharset($charsetType)
404
    {
405
        return array_key_exists($charsetType, $this->charsets)
406
            ? $this->charsets[$charsetType] : $this->charsets;
407
    }
408
409
    /**
410
     * Gets the transmitter for this connection.
411
     *
412
     * @return T\TcpClient The transmitter for this connection.
413
     */
414
    public function getTransmitter()
415
    {
416
        return $this->trans;
417
    }
418
419
    /**
420
     * Sends a word.
421
     *
422
     * Sends a word and automatically encodes its length when doing so.
423
     *
424
     * @param string|resource $word     The word to send, as a string
425
     * or seekable stream.
426
     * @param string|resource $word,... Additional word fragments to be sent
1 ignored issue
show
Bug introduced by
There is no parameter named $word,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
427
     * as a single word.
428
     *
429
     * @return int The number of bytes sent.
430
     *
431
     * @see sendWordFromStream()
432
     * @see getNextWord()
433
     */
434
    public function sendWord($word)
1 ignored issue
show
Unused Code introduced by
The parameter $word is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
435
    {
436
        $length = 0;
437
        $isCharsetConversionEnabled =
0 ignored issues
show
Coding Style introduced by
Multi-line assignments must have the equal sign on the second line
Loading history...
438
            null !== ($rCharset = $this->getCharset(self::CHARSET_REMOTE))
439
            && null !== ($lCharset = $this->getCharset(self::CHARSET_LOCAL));
440
        $wordFragments = array();
441
        foreach (func_get_args() as $word) {
442
            if (is_string($word)) {
443
                if ($isCharsetConversionEnabled) {
444
                    $word = iconv(
445
                        $lCharset,
0 ignored issues
show
Bug introduced by
The variable $lCharset does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
446
                        $rCharset . '//IGNORE//TRANSLIT',
447
                        $word
448
                    );
449
                }
450
                $length += strlen($word);
451
            } else {
452
                if (!self::isSeekableStream($word)) {
453
                    throw new InvalidArgumentException(
454
                        'Only seekable streams can be sent.',
455
                        InvalidArgumentException::CODE_SEEKABLE_REQUIRED
456
                    );
457
                }
458
                if ($isCharsetConversionEnabled) {
459
                    $word = self::iconvStream(
460
                        $lCharset,
0 ignored issues
show
Bug introduced by
It seems like $lCharset defined by $this->getCharset(self::CHARSET_LOCAL) on line 439 can also be of type array<string,string|null>; however, PEAR2\Net\RouterOS\Communicator::iconvStream() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
461
                        $rCharset . '//IGNORE//TRANSLIT',
462
                        $word
463
                    );
464
                }
465
                flock($word, LOCK_SH);
466
                $length += self::seekableStreamLength($word);
467
            }
468
            $wordFragments[] = $word;
469
        }
470
        static::verifyLengthSupport($length);
471
        if ($this->trans->isPersistent()) {
472
            $old = $this->trans->lock(T\Stream::DIRECTION_SEND);
473
474
            $bytesSent = $this->trans->send(self::encodeLength($length));
475
            foreach ($wordFragments as $fragment) {
476
                $bytesSent += $this->trans->send($fragment);
477
                if (!is_string($fragment)) {
478
                    flock($fragment, LOCK_UN);
479
                }
480
            }
481
482
            $this->trans->lock($old, true);
483
            return $bytesSent;
484
        }
485
486
        $bytesSent = $this->trans->send(self::encodeLength($length));
487
        foreach ($wordFragments as $word) {
488
            $bytesSent += $this->trans->send($word);
489
            if (!is_string($word)) {
490
                flock($word, LOCK_UN);
491
            }
492
        }
493
494
        return $bytesSent;
495
    }
496
497
    /**
498
     * Verifies that the length is supported.
499
     *
500
     * Verifies if the specified length is supported by the API. Throws a
501
     * {@link LengthException} if that's not the case. Currently, RouterOS
502
     * supports words up to 0xFFFFFFFF in length, so that's the only check
503
     * performed.
504
     *
505
     * @param int $length The length to verify.
506
     *
507
     * @return void
508
     */
509
    public static function verifyLengthSupport($length)
510
    {
511
        if ($length > 0xFFFFFFFF) {
512
            throw new LengthException(
513
                'Words with length above 0xFFFFFFFF are not supported.',
514
                LengthException::CODE_UNSUPPORTED,
515
                null,
516
                $length
517
            );
518
        }
519
    }
520
521
    /**
522
     * Encodes the length as required by the RouterOS API.
523
     *
524
     * @param int $length The length to encode.
525
     *
526
     * @return string The encoded length.
527
     */
528
    public static function encodeLength($length)
529
    {
530
        if ($length < 0) {
531
            throw new LengthException(
532
                'Length must not be negative.',
533
                LengthException::CODE_INVALID,
534
                null,
535
                $length
536
            );
537
        } elseif ($length < 0x80) {
538
            return chr($length);
539
        } elseif ($length < 0x4000) {
540
            return pack('n', $length |= 0x8000);
541
        } elseif ($length < 0x200000) {
542
            $length |= 0xC00000;
543
            return pack('n', $length >> 8) . chr($length & 0xFF);
544
        } elseif ($length < 0x10000000) {
545
            return pack('N', $length |= 0xE0000000);
546
        } elseif ($length <= 0xFFFFFFFF) {
547
            return chr(0xF0) . pack('N', $length);
548
        } elseif ($length <= 0x7FFFFFFFF) {
549
            $length = 'f' . base_convert($length, 10, 16);
550
            return chr(hexdec(substr($length, 0, 2))) .
551
                pack('N', hexdec(substr($length, 2)));
552
        }
553
        throw new LengthException(
554
            'Length must not be above 0x7FFFFFFFF.',
555
            LengthException::CODE_BEYOND_SHEME,
556
            null,
557
            $length
558
        );
559
    }
560
561
    /**
562
     * Get the length of the next word in queue.
563
     *
564
     * Get the length of the next word in queue without getting the word.
565
     * For pesisntent connections, note that the underlying transmitter will
566
     * be locked for receiving until either {@link self::getNextWord()} or
567
     * {@link self::getNextWordAsStream()} is called.
568
     *
569
     * @return int|double
570
     */
571
    public function getNextWordLength()
572
    {
573
        if (null === $this->nextWordLength) {
574
            if ($this->trans->isPersistent()) {
575
                $this->nextWordLock = $this->trans->lock(
576
                    T\Stream::DIRECTION_RECEIVE
577
                );
578
            }
579
            $this->nextWordLength = self::decodeLength($this->trans);
580
        }
581
        return $this->nextWordLength;
582
    }
583
584
    /**
585
     * Get the next word in queue as a string.
586
     *
587
     * Get the next word in queue as a string, after automatically decoding its
588
     * length.
589
     *
590
     * @return string The word.
591
     *
592
     * @see close()
593
     */
594
    public function getNextWord()
595
    {
596
        $word = $this->trans->receive(
597
            $this->getNextWordLength(),
598
            'word'
599
        );
600
        if (false !== $this->nextWordLock) {
601
            $this->trans->lock($this->nextWordLock, true);
602
        }
603
        $this->nextWordLength = null;
604
605
        if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
606
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
607
        ) {
608
            $word = iconv(
609
                $remoteCharset,
610
                $localCharset . '//IGNORE//TRANSLIT',
611
                $word
612
            );
613
        }
614
615
        return $word;
616
    }
617
618
    /**
619
     * Get the next word in queue as a stream.
620
     *
621
     * Get the next word in queue as a stream, after automatically decoding its
622
     * length.
623
     *
624
     * @return resource The word, as a stream.
625
     *
626
     * @see close()
627
     */
628
    public function getNextWordAsStream()
629
    {
630
        $filters = new T\FilterCollection();
631
        if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
632
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
633
        ) {
634
            $filters->append(
635
                'convert.iconv.' .
636
                $remoteCharset . '.' . $localCharset . '//IGNORE//TRANSLIT'
637
            );
638
        }
639
640
        $stream = $this->trans->receiveStream(
641
            $this->getNextWordLength(),
642
            $filters,
643
            'stream word'
644
        );
645
        if (false !== $this->nextWordLock) {
646
            $this->trans->lock($this->nextWordLock, true);
647
        }
648
        $this->nextWordLength = null;
649
650
        return $stream;
651
    }
652
653
    /**
654
     * Decodes the length of the incoming message.
655
     *
656
     * Decodes the length of the incoming message, as specified by the RouterOS
657
     * API.
658
     *
659
     * @param T\Stream $trans The transmitter from which to decode the length of
660
     * the incoming message.
661
     *
662
     * @return int|double The decoded length.
663
     *     Is of type "double" only for values above "2 << 31".
664
     */
665
    public static function decodeLength(T\Stream $trans)
666
    {
667
        if ($trans->isPersistent() && $trans instanceof T\TcpClient) {
668
            $old = $trans->lock($trans::DIRECTION_RECEIVE);
669
            $length = self::_decodeLength($trans);
670
            $trans->lock($old, true);
671
            return $length;
672
        }
673
        return self::_decodeLength($trans);
674
    }
675
676
    /**
677
     * Decodes the length of the incoming message.
678
     *
679
     * Decodes the length of the incoming message, as specified by the RouterOS
680
     * API.
681
     *
682
     * Difference with the non private function is that this one doesn't perform
683
     * locking if the connection is a persistent one.
684
     *
685
     * @param T\Stream $trans The transmitter from which to decode the length of
686
     *     the incoming message.
687
     *
688
     * @return int|double The decoded length.
689
     *     Is of type "double" only for values above "2 << 31".
690
     */
691
    private static function _decodeLength(T\Stream $trans)
692
    {
693
        $byte = ord($trans->receive(1, 'initial length byte'));
694
        if ($byte & 0x80) {
695
            if (($byte & 0xC0) === 0x80) {
696
                return (($byte & 077) << 8 ) + ord($trans->receive(1));
697
            } elseif (($byte & 0xE0) === 0xC0) {
698
                $rem = unpack('n~', $trans->receive(2));
699
                return (($byte & 037) << 16 ) + $rem['~'];
700
            } elseif (($byte & 0xF0) === 0xE0) {
701
                $rem = unpack('n~/C~~', $trans->receive(3));
702
                return (($byte & 017) << 24 ) + ($rem['~'] << 8) + $rem['~~'];
703
            } elseif (($byte & 0xF8) === 0xF0) {
704
                $rem = unpack('N~', $trans->receive(4));
705
                return (($byte & 007) * 0x100000000/* '<< 32' or '2^32' */)
706
                    + (double) sprintf('%u', $rem['~']);
707
            }
708
            throw new NotSupportedException(
709
                'Unknown control byte encountered.',
710
                NotSupportedException::CODE_CONTROL_BYTE,
711
                null,
712
                $byte
713
            );
714
        } else {
715
            return $byte;
716
        }
717
    }
718
719
    /**
720
     * Closes the opened connection, even if it is a persistent one.
721
     *
722
     * @return bool TRUE on success, FALSE on failure.
723
     */
724
    public function close()
725
    {
726
        return $this->trans->close();
727
    }
728
}
729