Completed
Push — develop ( 9cdb61...7482a6 )
by Vasil
03:28
created

Client::dispatchNextResponse()   D

Complexity

Conditions 9
Paths 15

Size

Total Lines 40
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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