Client::close()   B
last analyzed

Complexity

Conditions 7
Paths 42

Size

Total Lines 34
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 17
c 1
b 0
f 0
nc 42
nop 0
dl 0
loc 34
rs 8.8333
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)) {
0 ignored issues
show
Bug introduced by
It seems like $timeout can also be of type string; however, parameter $timeout of PEAR2\Net\RouterOS\Client::login() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

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

173
            if (!static::login($this->com, $username, $password, /** @scrutinizer ignore-type */ $timeout)) {
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $arg can also be of type double; however, parameter $sTimeout of PEAR2\Net\RouterOS\Client::loop() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

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

209
            return $this->loop(/** @scrutinizer ignore-type */ $arg);
Loading history...
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))
0 ignored issues
show
introduced by
The condition null !== $localCharset =...et($com::CHARSET_LOCAL) is always true.
Loading history...
238
        ) {
239
            $password = iconv(
240
                $localCharset,
0 ignored issues
show
Bug introduced by
It seems like $localCharset can also be of type array<string,null|string>; however, parameter $from_encoding of iconv() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

241
                /** @scrutinizer ignore-type */ $remoteCharset . '//IGNORE//TRANSLIT',
Loading history...
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->setArgument('name', $username);
291
        $request->setArgument('password', $password);
292
        $oldCharset = $com->getCharset($com::CHARSET_ALL);
293
        $com->setCharset(null, $com::CHARSET_ALL);
294
        $request->verify($com)->send($com);
295
        $com->setCharset($oldCharset, $com::CHARSET_ALL);
296
        $response = new Response($com, false, $timeout);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type integer|null expected by parameter $streamOn of PEAR2\Net\RouterOS\Response::__construct(). ( Ignorable by Annotation )

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

296
        $response = new Response($com, /** @scrutinizer ignore-type */ false, $timeout);
Loading history...
297
        if ($response->getType() === Response::TYPE_FINAL
298
            && null === $response->getProperty('ret')
299
        ) {
300
            // version >= 6.43
301
            return null === $response->getProperty('message');
302
        } elseif ($response->getType() === Response::TYPE_FINAL) {
303
            // version < 6.43
304
            $request->setArgument('password', '');
305
            $request->setArgument(
306
                'response',
307
                '00' . md5(
308
                    chr(0) . $password
309
                    . pack(
310
                        'H*',
311
                        is_string($response->getProperty('ret'))
312
                        ? $response->getProperty('ret')
313
                        : stream_get_contents($response->getProperty('ret'))
0 ignored issues
show
Bug introduced by
It seems like $response->getProperty('ret') can also be of type string; however, parameter $stream of stream_get_contents() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

313
                        : stream_get_contents(/** @scrutinizer ignore-type */ $response->getProperty('ret'))
Loading history...
314
                    )
315
                )
316
            );
317
            $request->verify($com)->send($com);
318
            $response = new Response($com, false, $timeout);
319
            if ($response->getType() === Response::TYPE_FINAL) {
320
                return null === $response->getProperty('ret');
321
            }
322
        }
323
        while ($response->getType() !== Response::TYPE_FINAL
324
            && $response->getType() !== Response::TYPE_FATAL
325
        ) {
326
            $response = new Response($com, false, $timeout);
327
        }
328
        return false;
329
    }
330
331
    /**
332
     * Sets the charset(s) for this connection.
333
     *
334
     * Sets the charset(s) for this connection. The specified charset(s) will be
335
     * used for all future requests and responses. When sending,
336
     * {@link Communicator::CHARSET_LOCAL} is converted to
337
     * {@link Communicator::CHARSET_REMOTE}, and when receiving,
338
     * {@link Communicator::CHARSET_REMOTE} is converted to
339
     * {@link Communicator::CHARSET_LOCAL}. Setting NULL to either charset will
340
     * disable charset conversion, and data will be both sent and received "as
341
     * is".
342
     *
343
     * @param mixed $charset     The charset to set. If $charsetType is
344
     *     {@link Communicator::CHARSET_ALL}, you can supply either a string to
345
     *     use for all charsets, or an array with the charset types as keys, and
346
     *     the charsets as values.
347
     * @param int   $charsetType Which charset to set. Valid values are the
348
     *     Communicator::CHARSET_* constants. Any other value is treated as
349
     *     {@link Communicator::CHARSET_ALL}.
350
     *
351
     * @return string|array The old charset. If $charsetType is
352
     *     {@link Communicator::CHARSET_ALL}, the old values will be returned as
353
     *     an array with the types as keys, and charsets as values.
354
     *
355
     * @see Communicator::setDefaultCharset()
356
     */
357
    public function setCharset(
358
        $charset,
359
        $charsetType = Communicator::CHARSET_ALL
360
    ) {
361
        return $this->com->setCharset($charset, $charsetType);
362
    }
363
364
    /**
365
     * Gets the charset(s) for this connection.
366
     *
367
     * @param int $charsetType Which charset to get. Valid values are the
368
     *     Communicator::CHARSET_* constants. Any other value is treated as
369
     *     {@link Communicator::CHARSET_ALL}.
370
     *
371
     * @return string|array The current charset. If $charsetType is
372
     *     {@link Communicator::CHARSET_ALL}, the current values will be
373
     *     returned as an array with the types as keys, and charsets as values.
374
     *
375
     * @see setCharset()
376
     */
377
    public function getCharset($charsetType)
378
    {
379
        return $this->com->getCharset($charsetType);
380
    }
381
382
    /**
383
     * Sends a request and waits for responses.
384
     *
385
     * @param Request       $request  The request to send.
386
     * @param callback|null $callback Optional. A function that is to be
387
     *     executed when new responses for this request are available.
388
     *     The callback takes two parameters. The {@link Response} object as
389
     *     the first, and the {@link Client} object as the second one. If the
390
     *     callback returns TRUE, the request is canceled. Note that the
391
     *     callback may be executed at least two times after that. Once with a
392
     *     {@link Response::TYPE_ERROR} response that notifies about the
393
     *     canceling, plus the {@link Response::TYPE_FINAL} response.
394
     *
395
     * @return $this The client object.
396
     *
397
     * @see completeRequest()
398
     * @see loop()
399
     * @see cancelRequest()
400
     */
401
    public function sendAsync(Request $request, $callback = null)
402
    {
403
        //Error checking
404
        $tag = $request->getTag();
405
        if ('' === (string)$tag) {
406
            throw new DataFlowException(
407
                'Asynchonous commands must have a tag.',
408
                DataFlowException::CODE_TAG_REQUIRED
409
            );
410
        }
411
        if ($this->isRequestActive($tag)) {
412
            throw new DataFlowException(
413
                'There must not be multiple active requests sharing a tag.',
414
                DataFlowException::CODE_TAG_UNIQUE
415
            );
416
        }
417
        if (null !== $callback && !is_callable($callback, true)) {
418
            throw new UnexpectedValueException(
419
                'Invalid callback provided.',
420
                UnexpectedValueException::CODE_CALLBACK_INVALID
421
            );
422
        }
423
424
        $this->send($request);
425
426
        if (null === $callback) {
427
            //Register the request at the buffer
428
            $this->responseBuffer[$tag] = array();
429
        } else {
430
            //Prepare the callback
431
            $this->callbacks[$tag] = $callback;
432
        }
433
        return $this;
434
    }
435
436
    /**
437
     * Checks if a request is active.
438
     *
439
     * Checks if a request is active. A request is considered active if it's a
440
     * pending request and/or has responses that are not yet extracted.
441
     *
442
     * @param string $tag    The tag of the request to look for.
443
     * @param int    $filter One of the FILTER_* constants. Limits the search
444
     *     to the specified places.
445
     *
446
     * @return bool TRUE if the request is active, FALSE otherwise.
447
     *
448
     * @see getPendingRequestsCount()
449
     * @see completeRequest()
450
     */
451
    public function isRequestActive($tag, $filter = self::FILTER_ALL)
452
    {
453
        $result = 0;
454
        if ($filter & self::FILTER_CALLBACK) {
455
            $result |= (int) array_key_exists($tag, $this->callbacks);
456
        }
457
        if ($filter & self::FILTER_BUFFER) {
458
            $result |= (int) array_key_exists($tag, $this->responseBuffer);
459
        }
460
        return 0 !== $result;
461
    }
462
463
    /**
464
     * Sends a request and gets the full response.
465
     *
466
     * @param Request $request The request to send.
467
     *
468
     * @return ResponseCollection The received responses as a collection.
469
     *
470
     * @see sendAsync()
471
     * @see close()
472
     */
473
    public function sendSync(Request $request)
474
    {
475
        $tag = $request->getTag();
476
        if ('' == $tag) {
477
            $this->send($request);
478
        } else {
479
            $this->sendAsync($request);
480
        }
481
        return $this->completeRequest($tag);
482
    }
483
484
    /**
485
     * Completes a specified request.
486
     *
487
     * Starts an event loop for the RouterOS callbacks and finishes when a
488
     * specified request is completed.
489
     *
490
     * @param string|null $tag The tag of the request to complete.
491
     *     Setting NULL completes all requests.
492
     *
493
     * @return ResponseCollection A collection of {@link Response} objects that
494
     *     haven't been passed to a callback function or previously extracted
495
     *     with {@link static::extractNewResponses()}. Returns an empty
496
     *     collection when $tag is set to NULL (responses can still be
497
     *     extracted).
498
     */
499
    public function completeRequest($tag = null)
500
    {
501
        $hasNoTag = '' == $tag;
502
        $result = $hasNoTag ? array()
503
            : $this->extractNewResponses($tag)->toArray();
504
        while ((!$hasNoTag && $this->isRequestActive($tag))
505
        || ($hasNoTag && 0 !== $this->getPendingRequestsCount())
506
        ) {
507
            $newReply = $this->dispatchNextResponse(null);
508
            if ($newReply->getTag() === $tag) {
509
                if ($hasNoTag) {
510
                    $result[] = $newReply;
511
                }
512
                if ($newReply->getType() === Response::TYPE_FINAL) {
513
                    if (!$hasNoTag) {
514
                        $result = array_merge(
515
                            $result,
516
                            $this->isRequestActive($tag)
517
                            ? $this->extractNewResponses($tag)->toArray()
518
                            : array()
519
                        );
520
                    }
521
                    break;
522
                }
523
            }
524
        }
525
        return new ResponseCollection($result);
526
    }
527
528
    /**
529
     * Extracts responses for a request.
530
     *
531
     * Gets all new responses for a request that haven't been passed to a
532
     * callback and clears the buffer from them.
533
     *
534
     * @param string|null $tag The tag of the request to extract
535
     *     new responses for.
536
     *     Specifying NULL with extract new responses for all requests.
537
     *
538
     * @return ResponseCollection A collection of {@link Response} objects for
539
     *     the specified request.
540
     *
541
     * @see loop()
542
     */
543
    public function extractNewResponses($tag = null)
544
    {
545
        if (null === $tag) {
546
            $result = array();
547
            foreach (array_keys($this->responseBuffer) as $tag) {
548
                $result = array_merge(
549
                    $result,
550
                    $this->extractNewResponses($tag)->toArray()
551
                );
552
            }
553
            return new ResponseCollection($result);
554
        } elseif ($this->isRequestActive($tag, self::FILTER_CALLBACK)) {
555
            return new ResponseCollection(array());
556
        } elseif ($this->isRequestActive($tag, self::FILTER_BUFFER)) {
557
            $result = $this->responseBuffer[$tag];
558
            if (!empty($result)) {
559
                if (end($result)->getType() === Response::TYPE_FINAL) {
560
                    unset($this->responseBuffer[$tag]);
561
                } else {
562
                    $this->responseBuffer[$tag] = array();
563
                }
564
            }
565
            return new ResponseCollection($result);
566
        } else {
567
            throw new DataFlowException(
568
                'No such request, or the request has already finished.',
569
                DataFlowException::CODE_UNKNOWN_REQUEST
570
            );
571
        }
572
    }
573
574
    /**
575
     * Starts an event loop for the RouterOS callbacks.
576
     *
577
     * Starts an event loop for the RouterOS callbacks and finishes when there
578
     * are no more pending requests or when a specified timeout has passed
579
     * (whichever comes first).
580
     *
581
     * @param int|null $sTimeout  Timeout for the loop.
582
     *     If NULL, there is no time limit.
583
     * @param int      $usTimeout Microseconds to add to the time limit.
584
     *
585
     * @return bool TRUE when there are any more pending requests, FALSE
586
     *     otherwise.
587
     *
588
     * @see extractNewResponses()
589
     * @see getPendingRequestsCount()
590
     */
591
    public function loop($sTimeout = null, $usTimeout = 0)
592
    {
593
        try {
594
            if (null === $sTimeout) {
595
                while ($this->getPendingRequestsCount() !== 0) {
596
                    $this->dispatchNextResponse(null);
597
                }
598
            } else {
599
                list($usStart, $sStart) = explode(' ', microtime());
600
                while ($this->getPendingRequestsCount() !== 0
601
                    && ($sTimeout >= 0 || $usTimeout >= 0)
602
                ) {
603
                    $this->dispatchNextResponse($sTimeout, $usTimeout);
604
                    list($usEnd, $sEnd) = explode(' ', microtime());
605
606
                    $sTimeout -= $sEnd - $sStart;
607
                    $usTimeout -= $usEnd - $usStart;
608
                    if ($usTimeout <= 0) {
609
                        if ($sTimeout > 0) {
610
                            $usTimeout = 1000000 + $usTimeout;
611
                            $sTimeout--;
612
                        }
613
                    }
614
615
                    $sStart = $sEnd;
616
                    $usStart = $usEnd;
617
                }
618
            }
619
        } catch (SocketException $e) {
620
            if ($e->getCode() !== SocketException::CODE_NO_DATA) {
621
                // @codeCoverageIgnoreStart
622
                // It's impossible to reliably cause any other SocketException.
623
                // This line is only here in case the unthinkable happens:
624
                // The connection terminates just after it was supposedly
625
                // about to send back some data.
626
                throw $e;
627
                // @codeCoverageIgnoreEnd
628
            }
629
        }
630
        return $this->getPendingRequestsCount() !== 0;
631
    }
632
633
    /**
634
     * Gets the number of pending requests.
635
     *
636
     * @return int The number of pending requests.
637
     *
638
     * @see isRequestActive()
639
     */
640
    public function getPendingRequestsCount()
641
    {
642
        return $this->pendingRequestsCount;
643
    }
644
645
    /**
646
     * Cancels a request.
647
     *
648
     * Cancels an active request. Using this function in favor of a plain call
649
     * to the "/cancel" command is highly recommended, as it also updates the
650
     * counter of pending requests properly. Note that canceling a request also
651
     * removes any responses for it that were not previously extracted with
652
     * {@link static::extractNewResponses()}.
653
     *
654
     * @param string|null $tag Tag of the request to cancel.
655
     *     Setting NULL will cancel all requests.
656
     *
657
     * @return $this The client object.
658
     *
659
     * @see sendAsync()
660
     * @see close()
661
     */
662
    public function cancelRequest($tag = null)
663
    {
664
        $cancelRequest = new Request('/cancel');
665
        $hasTag = !('' === (string)$tag);
666
        $hasReg = null !== $this->registry;
667
        if ($hasReg && !$hasTag) {
668
            $tags = array_merge(
669
                array_keys($this->responseBuffer),
670
                array_keys($this->callbacks)
671
            );
672
            $this->registry->setTaglessMode(true);
673
            foreach ($tags as $t) {
674
                $cancelRequest->setArgument(
675
                    'tag',
676
                    $this->registry->getOwnershipTag() . $t
677
                );
678
                $this->sendSync($cancelRequest);
679
            }
680
            $this->registry->setTaglessMode(false);
681
        } else {
682
            if ($hasTag) {
683
                if ($this->isRequestActive($tag)) {
684
                    if ($hasReg) {
685
                        $this->registry->setTaglessMode(true);
686
                        $cancelRequest->setArgument(
687
                            'tag',
688
                            $this->registry->getOwnershipTag() . $tag
689
                        );
690
                    } else {
691
                        $cancelRequest->setArgument('tag', $tag);
692
                    }
693
                } else {
694
                    throw new DataFlowException(
695
                        'No such request. Canceling aborted.',
696
                        DataFlowException::CODE_CANCEL_FAIL
697
                    );
698
                }
699
            }
700
            $this->sendSync($cancelRequest);
701
            if ($hasReg) {
702
                $this->registry->setTaglessMode(false);
703
            }
704
        }
705
706
        if ($hasTag) {
707
            if ($this->isRequestActive($tag, self::FILTER_BUFFER)) {
708
                $this->responseBuffer[$tag] = $this->completeRequest($tag);
709
            } else {
710
                $this->completeRequest($tag);
711
            }
712
        } else {
713
            $this->loop();
714
        }
715
        return $this;
716
    }
717
718
    /**
719
     * Sets response streaming setting.
720
     *
721
     * Sets when future response words are streamed. If a word is streamed,
722
     * the property value is returned a stream instead of a string, and
723
     * unrecognized words are returned entirely as streams instead of strings.
724
     * This is particularly useful if you expect a response that may contain
725
     * one or more very large words.
726
     *
727
     * @param int|null $threshold Threshold after which to stream
728
     *     a word. That is, a word less than this length will not be streamed.
729
     *     If set to 0, effectively all words are streamed.
730
     *     NULL to disable streaming altogether.
731
     *
732
     * @return $this The client object.
733
     *
734
     * @see getStreamingResponses()
735
     */
736
    public function setStreamingResponses($threshold)
737
    {
738
        $this->_streamingResponses = $threshold === null
739
            ? null
740
            : (int) $threshold;
741
        return $this;
742
    }
743
744
    /**
745
     * Gets response streaming setting.
746
     *
747
     * Gets when future response words are streamed.
748
     *
749
     * @return int|null The value of the setting.
750
     *
751
     * @see setStreamingResponses()
752
     */
753
    public function getStreamingResponses()
754
    {
755
        return $this->_streamingResponses;
756
    }
757
758
    /**
759
     * Closes the opened connection, even if it is a persistent one.
760
     *
761
     * Closes the opened connection, even if it is a persistent one. Note that
762
     * {@link static::extractNewResponses()} can still be used to extract
763
     * responses collected prior to the closing.
764
     *
765
     * @return bool TRUE on success, FALSE on failure.
766
     */
767
    public function close()
768
    {
769
        $result = true;
770
        /*
771
         * The check below is done because for some unknown reason
772
         * (either a PHP or a RouterOS bug) calling "/quit" on an encrypted
773
         * connection makes one end hang.
774
         *
775
         * Since encrypted connections only appeared in RouterOS 6.1, and
776
         * the "/quit" call is needed for all <6.0 versions, problems due
777
         * to its absence should be limited to some earlier 6.* versions
778
         * on some RouterBOARD devices.
779
         */
780
        if ($this->com->getTransmitter()->getCrypto() === N::CRYPTO_OFF) {
781
            if (null !== $this->registry) {
782
                $this->registry->setTaglessMode(true);
783
            }
784
            try {
785
                $response = $this->sendSync(new Request('/quit'));
786
                $result = $response[0]->getType() === Response::TYPE_FATAL;
787
            } catch (SocketException $e) {
788
                $result
789
                    = $e->getCode() === SocketException::CODE_REQUEST_SEND_FAIL;
790
            } catch (E $e) {
791
                //Ignore unknown errors.
792
            }
793
            if (null !== $this->registry) {
794
                $this->registry->setTaglessMode(false);
795
            }
796
        }
797
        $result = $result && $this->com->close();
798
        $this->callbacks = array();
799
        $this->pendingRequestsCount = 0;
800
        return $result;
801
    }
802
803
    /**
804
     * Closes the connection, unless it's a persistent one.
805
     */
806
    public function __destruct()
807
    {
808
        if ($this->com->getTransmitter()->isPersistent()) {
809
            if (0 !== $this->pendingRequestsCount) {
810
                $this->cancelRequest();
811
            }
812
        } else {
813
            $this->close();
814
        }
815
    }
816
817
    /**
818
     * Sends a request to RouterOS.
819
     *
820
     * @param Request $request The request to send.
821
     *
822
     * @return $this The client object.
823
     *
824
     * @see sendSync()
825
     * @see sendAsync()
826
     */
827
    protected function send(Request $request)
828
    {
829
        $request->verify($this->com)->send($this->com, $this->registry);
830
        $this->pendingRequestsCount++;
831
        return $this;
832
    }
833
834
    /**
835
     * Dispatches the next response in queue.
836
     *
837
     * Dispatches the next response in queue, i.e. it executes the associated
838
     * callback if there is one, or places the response in the response buffer.
839
     *
840
     * @param int|null $sTimeout  If a response is not immediately available,
841
     *     wait this many seconds.
842
     *     If NULL, wait indefinitely.
843
     * @param int      $usTimeout Microseconds to add to the waiting time.
844
     *
845
     * @return Response  The dispatched response.
846
     * @throws SocketException When there's no response within the time limit.
847
     */
848
    protected function dispatchNextResponse($sTimeout = 0, $usTimeout = 0)
849
    {
850
        $response = new Response(
851
            $this->com,
852
            $this->_streamingResponses,
853
            $sTimeout,
854
            $usTimeout,
855
            $this->registry
856
        );
857
        if ($response->getType() === Response::TYPE_FATAL) {
858
            $this->pendingRequestsCount = 0;
859
            $this->com->close();
860
            return $response;
861
        }
862
863
        $tag = $response->getTag();
864
        $isLastForRequest = $response->getType() === Response::TYPE_FINAL;
865
        if ($isLastForRequest) {
866
            $this->pendingRequestsCount--;
867
        }
868
869
        if ('' !== (string)$tag) {
870
            if ($this->isRequestActive($tag, self::FILTER_CALLBACK)) {
871
                if ($this->callbacks[$tag]($response, $this)) {
872
                    try {
873
                        $this->cancelRequest($tag);
874
                    } catch (DataFlowException $e) {
875
                        if ($e->getCode() !== $e::CODE_UNKNOWN_REQUEST
876
                        ) {
877
                            throw $e;
878
                        }
879
                    }
880
                } elseif ($isLastForRequest) {
881
                    unset($this->callbacks[$tag]);
882
                }
883
            } else {
884
                $this->responseBuffer[$tag][] = $response;
885
            }
886
        }
887
        return $response;
888
    }
889
}
890