Completed
Branch develop (b42f4c)
by Vasil
13:57
created

Client::getStreamingResponses()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 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
 * Refers to transmitter direction constants.
25
 */
26
use PEAR2\Net\Transmitter\Stream as S;
27
28
/**
29
 * Refers to the cryptography constants.
30
 */
31
use PEAR2\Net\Transmitter\NetworkStream as N;
32
33
/**
34
 * Catches arbitrary exceptions at some points.
35
 */
36
use Exception as E;
37
38
/**
39
 * A RouterOS client.
40
 *
41
 * Provides functionality for easily communicating with a RouterOS host.
42
 *
43
 * @category Net
44
 * @package  PEAR2_Net_RouterOS
45
 * @author   Vasil Rangelov <[email protected]>
46
 * @license  http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
47
 * @link     http://pear2.php.net/PEAR2_Net_RouterOS
48
 */
49
class Client
50
{
51
    /**
52
     * Used in {@link static::isRequestActive()} to limit search only to
53
     * requests that have a callback.
54
     */
55
    const FILTER_CALLBACK = 1;
56
    /**
57
     * Used in {@link static::isRequestActive()} to limit search only to
58
     * requests that use the buffer.
59
     */
60
    const FILTER_BUFFER = 2;
61
    /**
62
     * Used in {@link static::isRequestActive()} to indicate no limit in search.
63
     */
64
    const FILTER_ALL = 3;
65
66
    /**
67
     * The communicator for this client.
68
     *
69
     * @var Communicator
70
     */
71
    protected $com;
72
73
    /**
74
     * The number of currently pending requests.
75
     *
76
     * @var int
77
     */
78
    protected $pendingRequestsCount = 0;
79
80
    /**
81
     * An array of responses that have not yet been extracted
82
     * or passed to a callback.
83
     *
84
     * Key is the tag of the request, and the value is an array of
85
     * associated responses.
86
     *
87
     * @var array<string,Response[]>
88
     */
89
    protected $responseBuffer = array();
90
91
    /**
92
     * An array of callbacks to be executed as responses come.
93
     *
94
     * Key is the tag of the request, and the value is the callback for it.
95
     *
96
     * @var array<string,callback>
97
     */
98
    protected $callbacks = array();
99
100
    /**
101
     * A registry for the operations.
102
     *
103
     * Particularly helpful at persistent connections.
104
     *
105
     * @var Registry
106
     */
107
    protected $registry = null;
108
109
    /**
110
     * Stream response words that are above this many bytes.
111
     * NULL to disable streaming completely.
112
     *
113
     * @var int|null
114
     */
115
    private $_streamingResponses = null;
116
117
    /**
118
     * Creates a new instance of a RouterOS API client.
119
     *
120
     * Creates a new instance of a RouterOS API client with the specified
121
     * settings.
122
     *
123
     * @param string        $host     Hostname (IP or domain) of RouterOS.
124
     * @param string        $username The RouterOS username.
125
     * @param string        $password The RouterOS password.
126
     * @param int|null      $port     The port on which the RouterOS host
127
     *     provides the API service. You can also specify NULL, in which case
128
     *     the port will automatically be chosen between 8728 and 8729,
129
     *     depending on the value of $crypto.
130
     * @param bool          $persist  Whether or not the connection should be a
131
     *     persistent one.
132
     * @param double|null   $timeout  The timeout for the connection.
133
     * @param string        $crypto   The encryption for this connection.
134
     *     Must be one of the PEAR2\Net\Transmitter\NetworkStream::CRYPTO_*
135
     *     constants. Off by default. RouterOS currently supports only TLS, but
136
     *     the setting is provided in this fashion for forward compatibility's
137
     *     sake. And for the sake of simplicity, if you specify an encryption,
138
     *     don't specify a context and your default context uses the value
139
     *     "DEFAULT" for ciphers, "ADH" will be automatically added to the list
140
     *     of ciphers.
141
     * @param resource|null $context  A context for the socket.
142
     *
143
     * @see sendSync()
144
     * @see sendAsync()
145
     */
146
    public function __construct(
147
        $host,
148
        $username,
149
        $password = '',
150
        $port = 8728,
151
        $persist = false,
152
        $timeout = null,
153
        $crypto = N::CRYPTO_OFF,
154
        $context = null
155
    ) {
156
        $this->com = new Communicator(
157
            $host,
158
            $port,
159
            $persist,
160
            $timeout,
161
            $username . '/' . $password,
162
            $crypto,
163
            $context
164
        );
165
        $timeout = null == $timeout
166
            ? ini_get('default_socket_timeout')
167
            : (int) $timeout;
168
        //Login the user if necessary
169
        if ((!$persist
170
            || !($old = $this->com->getTransmitter()->lock(S::DIRECTION_ALL)))
171
            && $this->com->getTransmitter()->isFresh()
172
        ) {
173
            if (!static::login($this->com, $username, $password, $timeout)) {
174
                $this->com->close();
175
                throw new DataFlowException(
176
                    'Invalid username or password supplied.',
177
                    DataFlowException::CODE_INVALID_CREDENTIALS
178
                );
179
            }
180
        }
181
182
        if (isset($old)) {
183
            $this->com->getTransmitter()->lock($old, true);
184
        }
185
186
        if ($persist) {
187
            $this->registry = new Registry("{$host}:{$port}/{$username}");
188
        }
189
    }
190
191
    /**
192
     * A shorthand gateway.
193
     *
194
     * This is a magic PHP method that allows you to call the object as a
195
     * function. Depending on the argument given, one of the other functions in
196
     * the class is invoked and its returned value is returned by this function.
197
     *
198
     * @param mixed $arg Value can be either a {@link Request} to send, which
199
     *     would be sent asynchronously if it has a tag, and synchronously if
200
     *     not, a number to loop with or NULL to complete all pending requests.
201
     *     Any other value is converted to string and treated as the tag of a
202
     *     request to complete.
203
     *
204
     * @return mixed Whatever the long form function would have returned.
205
     */
206
    public function __invoke($arg = null)
207
    {
208
        if (is_int($arg) || is_double($arg)) {
209
            return $this->loop($arg);
210
        } elseif ($arg instanceof Request) {
211
            return '' == $arg->getTag() ? $this->sendSync($arg)
212
                : $this->sendAsync($arg);
213
        } elseif (null === $arg) {
214
            return $this->completeRequest();
215
        }
216
        return $this->completeRequest((string) $arg);
217
    }
218
219
    /**
220
     * Login to a RouterOS connection.
221
     *
222
     * @param Communicator $com      The communicator to attempt to login to.
223
     * @param string       $username The RouterOS username.
224
     * @param string       $password The RouterOS password.
225
     * @param int|null     $timeout  The time to wait for each response. NULL
226
     *     waits indefinitely.
227
     *
228
     * @return bool TRUE on success, FALSE on failure.
229
     */
230
    public static function login(
231
        Communicator $com,
232
        $username,
233
        $password = '',
234
        $timeout = null
235
    ) {
236
        if (null !== ($remoteCharset = $com->getCharset($com::CHARSET_REMOTE))
237
            && null !== ($localCharset = $com->getCharset($com::CHARSET_LOCAL))
238
        ) {
239
            $password = iconv(
240
                $localCharset,
241
                $remoteCharset . '//IGNORE//TRANSLIT',
242
                $password
243
            );
244
        }
245
        $old = null;
246
        try {
247
            if ($com->getTransmitter()->isPersistent()) {
248
                $old = $com->getTransmitter()->lock(S::DIRECTION_ALL);
249
                $result = self::_login($com, $username, $password, $timeout);
250
                $com->getTransmitter()->lock($old, true);
251
                return $result;
252
            }
253
            return self::_login($com, $username, $password, $timeout);
254
        } catch (E $e) {
255
            if ($com->getTransmitter()->isPersistent() && null !== $old) {
256
                $com->getTransmitter()->lock($old, true);
257
            }
258
            throw ($e instanceof NotSupportedException
259
            || $e instanceof UnexpectedValueException
260
            || !$com->getTransmitter()->isDataAwaiting()) ? new SocketException(
261
                'This is not a compatible RouterOS service',
262
                SocketException::CODE_SERVICE_INCOMPATIBLE,
263
                $e
264
            ) : $e;
265
        }
266
    }
267
268
    /**
269
     * Login to a RouterOS connection.
270
     *
271
     * This is the actual login procedure, applied regardless of persistence and
272
     * charset settings.
273
     *
274
     * @param Communicator $com      The communicator to attempt to login to.
275
     * @param string       $username The RouterOS username.
276
     * @param string       $password The RouterOS password. Potentially parsed
277
     *     already by iconv.
278
     * @param int|null     $timeout  The time to wait for each response. NULL
279
     *     waits indefinitely.
280
     *
281
     * @return bool TRUE on success, FALSE on failure.
282
     */
283
    private static function _login(
284
        Communicator $com,
285
        $username,
286
        $password = '',
287
        $timeout = null
288
    ) {
289
        $request = new Request('/login');
290
        $request->send($com);
291
        $response = new Response($com, null, $timeout);
292
        $request->setArgument('name', $username);
293
        $request->setArgument(
294
            'response',
295
            '00' . md5(
296
                chr(0) . $password
297
                . pack('H*', $response->getProperty('ret'))
298
            )
299
        );
300
        $request->verify($com)->send($com);
301
302
        $response = new Response($com, null, $timeout);
303
        if ($response->getType() === Response::TYPE_FINAL) {
304
            return null === $response->getProperty('ret');
305
        } else {
306
            while ($response->getType() !== Response::TYPE_FINAL
307
                && $response->getType() !== Response::TYPE_FATAL
308
            ) {
309
                $response = new Response($com, null, $timeout);
310
            }
311
            return false;
312
        }
313
    }
314
315
    /**
316
     * Sets the charset(s) for this connection.
317
     *
318
     * Sets the charset(s) for this connection. The specified charset(s) will be
319
     * used for all future requests and responses. When sending,
320
     * {@link Communicator::CHARSET_LOCAL} is converted to
321
     * {@link Communicator::CHARSET_REMOTE}, and when receiving,
322
     * {@link Communicator::CHARSET_REMOTE} is converted to
323
     * {@link Communicator::CHARSET_LOCAL}. Setting NULL to either charset will
324
     * disable charset convertion, and data will be both sent and received "as
325
     * is".
326
     *
327
     * @param mixed $charset     The charset to set. If $charsetType is
328
     *     {@link Communicator::CHARSET_ALL}, you can supply either a string to
329
     *     use for all charsets, or an array with the charset types as keys, and
330
     *     the charsets as values.
331
     * @param int   $charsetType Which charset to set. Valid values are the
332
     *     Communicator::CHARSET_* constants. Any other value is treated as
333
     *     {@link Communicator::CHARSET_ALL}.
334
     *
335
     * @return string|array The old charset. If $charsetType is
336
     *     {@link Communicator::CHARSET_ALL}, the old values will be returned as
337
     *     an array with the types as keys, and charsets as values.
338
     *
339
     * @see Communicator::setDefaultCharset()
340
     */
341
    public function setCharset(
342
        $charset,
343
        $charsetType = Communicator::CHARSET_ALL
344
    ) {
345
        return $this->com->setCharset($charset, $charsetType);
346
    }
347
348
    /**
349
     * Gets the charset(s) for this connection.
350
     *
351
     * @param int $charsetType Which charset to get. Valid values are the
352
     *     Communicator::CHARSET_* constants. Any other value is treated as
353
     *     {@link Communicator::CHARSET_ALL}.
354
     *
355
     * @return string|array The current charset. If $charsetType is
356
     *     {@link Communicator::CHARSET_ALL}, the current values will be
357
     *     returned as an array with the types as keys, and charsets as values.
358
     *
359
     * @see setCharset()
360
     */
361
    public function getCharset($charsetType)
362
    {
363
        return $this->com->getCharset($charsetType);
364
    }
365
366
    /**
367
     * Sends a request and waits for responses.
368
     *
369
     * @param Request       $request  The request to send.
370
     * @param callback|null $callback Optional. A function that is to be
371
     *     executed when new responses for this request are available.
372
     *     The callback takes two parameters. The {@link Response} object as
373
     *     the first, and the {@link Client} object as the second one. If the
374
     *     callback returns TRUE, the request is canceled. Note that the
375
     *     callback may be executed at least two times after that. Once with a
376
     *     {@link Response::TYPE_ERROR} response that notifies about the
377
     *     canceling, plus the {@link Response::TYPE_FINAL} response.
378
     *
379
     * @return $this The client object.
380
     *
381
     * @see completeRequest()
382
     * @see loop()
383
     * @see cancelRequest()
384
     */
385
    public function sendAsync(Request $request, $callback = null)
386
    {
387
        //Error checking
388
        $tag = $request->getTag();
389
        if ('' == $tag) {
390
            throw new DataFlowException(
391
                'Asynchonous commands must have a tag.',
392
                DataFlowException::CODE_TAG_REQUIRED
393
            );
394
        }
395
        if ($this->isRequestActive($tag)) {
396
            throw new DataFlowException(
397
                'There must not be multiple active requests sharing a tag.',
398
                DataFlowException::CODE_TAG_UNIQUE
399
            );
400
        }
401
        if (null !== $callback && !is_callable($callback, true)) {
402
            throw new UnexpectedValueException(
403
                'Invalid callback provided.',
404
                UnexpectedValueException::CODE_CALLBACK_INVALID
405
            );
406
        }
407
408
        $this->send($request);
409
410
        if (null === $callback) {
411
            //Register the request at the buffer
412
            $this->responseBuffer[$tag] = array();
413
        } else {
414
            //Prepare the callback
415
            $this->callbacks[$tag] = $callback;
416
        }
417
        return $this;
418
    }
419
420
    /**
421
     * Checks if a request is active.
422
     *
423
     * Checks if a request is active. A request is considered active if it's a
424
     * pending request and/or has responses that are not yet extracted.
425
     *
426
     * @param string $tag    The tag of the request to look for.
427
     * @param int    $filter One of the FILTER_* constants. Limits the search
428
     *     to the specified places.
429
     *
430
     * @return bool TRUE if the request is active, FALSE otherwise.
431
     *
432
     * @see getPendingRequestsCount()
433
     * @see completeRequest()
434
     */
435
    public function isRequestActive($tag, $filter = self::FILTER_ALL)
436
    {
437
        $result = 0;
438
        if ($filter & self::FILTER_CALLBACK) {
439
            $result |= (int) array_key_exists($tag, $this->callbacks);
440
        }
441
        if ($filter & self::FILTER_BUFFER) {
442
            $result |= (int) array_key_exists($tag, $this->responseBuffer);
443
        }
444
        return 0 !== $result;
445
    }
446
447
    /**
448
     * Sends a request and gets the full response.
449
     *
450
     * @param Request $request The request to send.
451
     *
452
     * @return ResponseCollection The received responses as a collection.
453
     *
454
     * @see sendAsync()
455
     * @see close()
456
     */
457
    public function sendSync(Request $request)
458
    {
459
        $tag = $request->getTag();
460
        if ('' == $tag) {
461
            $this->send($request);
462
        } else {
463
            $this->sendAsync($request);
464
        }
465
        return $this->completeRequest($tag);
466
    }
467
468
    /**
469
     * Completes a specified request.
470
     *
471
     * Starts an event loop for the RouterOS callbacks and finishes when a
472
     * specified request is completed.
473
     *
474
     * @param string|null $tag The tag of the request to complete.
475
     *     Setting NULL completes all requests.
476
     *
477
     * @return ResponseCollection A collection of {@link Response} objects that
478
     *     haven't been passed to a callback function or previously extracted
479
     *     with {@link static::extractNewResponses()}. Returns an empty
480
     *     collection when $tag is set to NULL (responses can still be
481
     *     extracted).
482
     */
483
    public function completeRequest($tag = null)
484
    {
485
        $hasNoTag = '' == $tag;
486
        $result = $hasNoTag ? array()
487
            : $this->extractNewResponses($tag)->toArray();
488
        while ((!$hasNoTag && $this->isRequestActive($tag))
489
        || ($hasNoTag && 0 !== $this->getPendingRequestsCount())
490
        ) {
491
            $newReply = $this->dispatchNextResponse(null);
492
            if ($newReply->getTag() === $tag) {
493
                if ($hasNoTag) {
494
                    $result[] = $newReply;
495
                }
496
                if ($newReply->getType() === Response::TYPE_FINAL) {
497
                    if (!$hasNoTag) {
498
                        $result = array_merge(
499
                            $result,
500
                            $this->isRequestActive($tag)
501
                            ? $this->extractNewResponses($tag)->toArray()
502
                            : array()
503
                        );
504
                    }
505
                    break;
506
                }
507
            }
508
        }
509
        return new ResponseCollection($result);
510
    }
511
512
    /**
513
     * Extracts responses for a request.
514
     *
515
     * Gets all new responses for a request that haven't been passed to a
516
     * callback and clears the buffer from them.
517
     *
518
     * @param string|null $tag The tag of the request to extract
519
     *     new responses for.
520
     *     Specifying NULL with extract new responses for all requests.
521
     *
522
     * @return ResponseCollection A collection of {@link Response} objects for
523
     *     the specified request.
524
     *
525
     * @see loop()
526
     */
527
    public function extractNewResponses($tag = null)
528
    {
529
        if (null === $tag) {
530
            $result = array();
531
            foreach (array_keys($this->responseBuffer) as $tag) {
532
                $result = array_merge(
533
                    $result,
534
                    $this->extractNewResponses($tag)->toArray()
535
                );
536
            }
537
            return new ResponseCollection($result);
538
        } elseif ($this->isRequestActive($tag, self::FILTER_CALLBACK)) {
539
            return new ResponseCollection(array());
540
        } elseif ($this->isRequestActive($tag, self::FILTER_BUFFER)) {
541
            $result = $this->responseBuffer[$tag];
542
            if (!empty($result)) {
543
                if (end($result)->getType() === Response::TYPE_FINAL) {
544
                    unset($this->responseBuffer[$tag]);
545
                } else {
546
                    $this->responseBuffer[$tag] = array();
547
                }
548
            }
549
            return new ResponseCollection($result);
550
        } else {
551
            throw new DataFlowException(
552
                'No such request, or the request has already finished.',
553
                DataFlowException::CODE_UNKNOWN_REQUEST
554
            );
555
        }
556
    }
557
558
    /**
559
     * Starts an event loop for the RouterOS callbacks.
560
     *
561
     * Starts an event loop for the RouterOS callbacks and finishes when there
562
     * are no more pending requests or when a specified timeout has passed
563
     * (whichever comes first).
564
     *
565
     * @param int|null $sTimeout  Timeout for the loop.
566
     *     If NULL, there is no time limit.
567
     * @param int      $usTimeout Microseconds to add to the time limit.
568
     *
569
     * @return bool TRUE when there are any more pending requests, FALSE
570
     *     otherwise.
571
     *
572
     * @see extractNewResponses()
573
     * @see getPendingRequestsCount()
574
     */
575
    public function loop($sTimeout = null, $usTimeout = 0)
576
    {
577
        try {
578
            if (null === $sTimeout) {
579
                while ($this->getPendingRequestsCount() !== 0) {
580
                    $this->dispatchNextResponse(null);
581
                }
582
            } else {
583
                list($usStart, $sStart) = explode(' ', microtime());
584
                while ($this->getPendingRequestsCount() !== 0
585
                    && ($sTimeout >= 0 || $usTimeout >= 0)
586
                ) {
587
                    $this->dispatchNextResponse($sTimeout, $usTimeout);
588
                    list($usEnd, $sEnd) = explode(' ', microtime());
589
590
                    $sTimeout -= $sEnd - $sStart;
591
                    $usTimeout -= $usEnd - $usStart;
592
                    if ($usTimeout <= 0) {
593
                        if ($sTimeout > 0) {
594
                            $usTimeout = 1000000 + $usTimeout;
595
                            $sTimeout--;
596
                        }
597
                    }
598
599
                    $sStart = $sEnd;
600
                    $usStart = $usEnd;
601
                }
602
            }
603
        } catch (SocketException $e) {
604
            if ($e->getCode() !== SocketException::CODE_NO_DATA) {
605
                // @codeCoverageIgnoreStart
606
                // It's impossible to reliably cause any other SocketException.
607
                // This line is only here in case the unthinkable happens:
608
                // The connection terminates just after it was supposedly
609
                // about to send back some data.
610
                throw $e;
611
                // @codeCoverageIgnoreEnd
612
            }
613
        }
614
        return $this->getPendingRequestsCount() !== 0;
615
    }
616
617
    /**
618
     * Gets the number of pending requests.
619
     *
620
     * @return int The number of pending requests.
621
     *
622
     * @see isRequestActive()
623
     */
624
    public function getPendingRequestsCount()
625
    {
626
        return $this->pendingRequestsCount;
627
    }
628
629
    /**
630
     * Cancels a request.
631
     *
632
     * Cancels an active request. Using this function in favor of a plain call
633
     * to the "/cancel" command is highly recommended, as it also updates the
634
     * counter of pending requests properly. Note that canceling a request also
635
     * removes any responses for it that were not previously extracted with
636
     * {@link static::extractNewResponses()}.
637
     *
638
     * @param string|null $tag Tag of the request to cancel.
639
     *     Setting NULL will cancel all requests.
640
     *
641
     * @return $this The client object.
642
     *
643
     * @see sendAsync()
644
     * @see close()
645
     */
646
    public function cancelRequest($tag = null)
647
    {
648
        $cancelRequest = new Request('/cancel');
649
        $hasTag = !('' == $tag);
650
        $hasReg = null !== $this->registry;
651
        if ($hasReg && !$hasTag) {
652
            $tags = array_merge(
653
                array_keys($this->responseBuffer),
654
                array_keys($this->callbacks)
655
            );
656
            $this->registry->setTaglessMode(true);
657
            foreach ($tags as $t) {
658
                $cancelRequest->setArgument(
659
                    'tag',
660
                    $this->registry->getOwnershipTag() . $t
661
                );
662
                $this->sendSync($cancelRequest);
663
            }
664
            $this->registry->setTaglessMode(false);
665
        } else {
666
            if ($hasTag) {
667
                if ($this->isRequestActive($tag)) {
668
                    if ($hasReg) {
669
                        $this->registry->setTaglessMode(true);
670
                        $cancelRequest->setArgument(
671
                            'tag',
672
                            $this->registry->getOwnershipTag() . $tag
673
                        );
674
                    } else {
675
                        $cancelRequest->setArgument('tag', $tag);
676
                    }
677
                } else {
678
                    throw new DataFlowException(
679
                        'No such request. Canceling aborted.',
680
                        DataFlowException::CODE_CANCEL_FAIL
681
                    );
682
                }
683
            }
684
            $this->sendSync($cancelRequest);
685
            if ($hasReg) {
686
                $this->registry->setTaglessMode(false);
687
            }
688
        }
689
690
        if ($hasTag) {
691
            if ($this->isRequestActive($tag, self::FILTER_BUFFER)) {
692
                $this->responseBuffer[$tag] = $this->completeRequest($tag);
693
            } else {
694
                $this->completeRequest($tag);
695
            }
696
        } else {
697
            $this->loop();
698
        }
699
        return $this;
700
    }
701
702
    /**
703
     * Sets response streaming setting.
704
     *
705
     * Sets when future response words are streamed. If a word is streamed,
706
     * the property value is returned a stream instead of a string, and
707
     * unrecognized words are returned entirely as streams instead of strings.
708
     * This is particularly useful if you expect a response that may contain
709
     * one or more very large words.
710
     *
711
     * @param int|null $threshold Threshold after which to stream
712
     *     a word. That is, a word less than this length will not be streamed.
713
     *     If set to 0, effectively all words are streamed.
714
     *     NULL to disable streaming altogether.
715
     *
716
     * @return $this The client object.
717
     *
718
     * @see getStreamingResponses()
719
     */
720
    public function setStreamingResponses($threshold)
721
    {
722
        $this->_streamingResponses = $threshold === null
723
            ? null
724
            : (int) $threshold;
725
        return $this;
726
    }
727
728
    /**
729
     * Gets response streaming setting.
730
     *
731
     * Gets when future response words are streamed.
732
     *
733
     * @return int|null The value of the setting.
734
     *
735
     * @see setStreamingResponses()
736
     */
737
    public function getStreamingResponses()
738
    {
739
        return $this->_streamingResponses;
740
    }
741
742
    /**
743
     * Closes the opened connection, even if it is a persistent one.
744
     *
745
     * Closes the opened connection, even if it is a persistent one. Note that
746
     * {@link static::extractNewResponses()} can still be used to extract
747
     * responses collected prior to the closing.
748
     *
749
     * @return bool TRUE on success, FALSE on failure.
750
     */
751
    public function close()
752
    {
753
        $result = true;
754
        /*
755
         * The check below is done because for some unknown reason
756
         * (either a PHP or a RouterOS bug) calling "/quit" on an encrypted
757
         * connection makes one end hang.
758
         *
759
         * Since encrypted connections only appeared in RouterOS 6.1, and
760
         * the "/quit" call is needed for all <6.0 versions, problems due
761
         * to its absence should be limited to some earlier 6.* versions
762
         * on some RouterBOARD devices.
763
         */
764
        if ($this->com->getTransmitter()->getCrypto() === N::CRYPTO_OFF) {
765
            if (null !== $this->registry) {
766
                $this->registry->setTaglessMode(true);
767
            }
768
            try {
769
                $response = $this->sendSync(new Request('/quit'));
770
                $result = $response[0]->getType() === Response::TYPE_FATAL;
771
            } catch (SocketException $e) {
772
                $result
773
                    = $e->getCode() === SocketException::CODE_REQUEST_SEND_FAIL;
774
            } catch (E $e) {
775
                //Ignore unknown errors.
776
            }
777
            if (null !== $this->registry) {
778
                $this->registry->setTaglessMode(false);
779
            }
780
        }
781
        $result = $result && $this->com->close();
782
        $this->callbacks = array();
783
        $this->pendingRequestsCount = 0;
784
        return $result;
785
    }
786
787
    /**
788
     * Closes the connection, unless it's a persistent one.
789
     */
790
    public function __destruct()
791
    {
792
        if ($this->com->getTransmitter()->isPersistent()) {
793
            if (0 !== $this->pendingRequestsCount) {
794
                $this->cancelRequest();
795
            }
796
        } else {
797
            $this->close();
798
        }
799
    }
800
801
    /**
802
     * Sends a request to RouterOS.
803
     *
804
     * @param Request $request The request to send.
805
     *
806
     * @return $this The client object.
807
     *
808
     * @see sendSync()
809
     * @see sendAsync()
810
     */
811
    protected function send(Request $request)
812
    {
813
        $request->verify($this->com)->send($this->com, $this->registry);
814
        $this->pendingRequestsCount++;
815
        return $this;
816
    }
817
818
    /**
819
     * Dispatches the next response in queue.
820
     *
821
     * Dispatches the next response in queue, i.e. it executes the associated
822
     * callback if there is one, or places the response in the response buffer.
823
     *
824
     * @param int|null $sTimeout  If a response is not immediately available,
825
     *     wait this many seconds.
826
     *     If NULL, wait indefinitely.
827
     * @param int      $usTimeout Microseconds to add to the waiting time.
828
     *
829
     * @throws SocketException When there's no response within the time limit.
830
     * @return Response The dispatched response.
831
     */
832
    protected function dispatchNextResponse($sTimeout = 0, $usTimeout = 0)
833
    {
834
        $response = new Response(
835
            $this->com,
836
            $this->_streamingResponses,
837
            $sTimeout,
838
            $usTimeout,
839
            $this->registry
840
        );
841
        if ($response->getType() === Response::TYPE_FATAL) {
842
            $this->pendingRequestsCount = 0;
843
            $this->com->close();
844
            return $response;
845
        }
846
847
        $tag = $response->getTag();
848
        $isLastForRequest = $response->getType() === Response::TYPE_FINAL;
849
        if ($isLastForRequest) {
850
            $this->pendingRequestsCount--;
851
        }
852
853
        if ('' != $tag) {
854
            if ($this->isRequestActive($tag, self::FILTER_CALLBACK)) {
855
                if ($this->callbacks[$tag]($response, $this)) {
856
                    try {
857
                        $this->cancelRequest($tag);
858
                    } catch (DataFlowException $e) {
859
                        if ($e->getCode() !== DataFlowException::CODE_UNKNOWN_REQUEST
860
                        ) {
861
                            throw $e;
862
                        }
863
                    }
864
                } elseif ($isLastForRequest) {
865
                    unset($this->callbacks[$tag]);
866
                }
867
            } else {
868
                $this->responseBuffer[$tag][] = $response;
869
            }
870
        }
871
        return $response;
872
    }
873
}
874