Client::__construct()   B
last analyzed

Complexity

Conditions 8
Paths 18

Size

Total Lines 42
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 8
eloc 23
c 2
b 1
f 0
nc 18
nop 8
dl 0
loc 42
rs 8.4444

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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