├── .gitmodules
├── composer.json
├── data
└── roscon.xml
├── scripts
├── roscon
├── roscon.bat
└── roscon.php
├── src
└── PEAR2
│ └── Net
│ └── RouterOS
│ ├── Client.php
│ ├── Communicator.php
│ ├── DataFlowException.php
│ ├── Exception.php
│ ├── InvalidArgumentException.php
│ ├── LengthException.php
│ ├── Message.php
│ ├── NotSupportedException.php
│ ├── ParserException.php
│ ├── Query.php
│ ├── Registry.php
│ ├── Request.php
│ ├── Response.php
│ ├── ResponseCollection.php
│ ├── RouterErrorException.php
│ ├── Script.php
│ ├── SocketException.php
│ ├── UnexpectedValueException.php
│ └── Util.php
└── stub.php
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "docs/wiki"]
2 | path = docs/wiki
3 | url = https://github.com/pear2/Net_RouterOS.wiki.git
4 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pear2/net_routeros",
3 | "description": "This package allows you to read and write information from a RouterOS host using MikroTik's RouterOS API.",
4 | "keywords": ["routeros", "package", "api", "mikrotik", "pear2", "router"],
5 | "homepage": "http://pear2.github.com/Net_RouterOS/",
6 | "license": "LGPL-2.1",
7 | "authors": [
8 | {
9 | "name": "Vasil Rangelov",
10 | "email": "boen.robot@gmail.com",
11 | "role": "lead"
12 | }
13 | ],
14 | "support": {
15 | "issues": "https://github.com/pear2/Net_RouterOS/issues",
16 | "wiki": "https://github.com/pear2/Net_RouterOS/wiki"
17 | },
18 | "require": {
19 | "php": ">=5.3.0",
20 | "pear2/net_transmitter": ">=1.0.0b1"
21 | },
22 | "require-dev": {
23 | "phpunit/phpunit": "@stable",
24 | "squizlabs/php_codesniffer": "@stable",
25 | "pear2/cache_shm": "dev-develop",
26 | "pear2/console_commandline": "dev-master",
27 | "pear2/console_color": "dev-develop"
28 | },
29 | "suggest": {
30 | "pear2/cache_shm": "Enables persistent connections.",
31 | "pear2/console_commandline": "Enables the console",
32 | "pear2/console_color": "Enables colors in the console",
33 | "ext-apc": "This, APCu or Wincache is required for persistent connections.",
34 | "ext-apcu": "This, APC or Wincache is required for persistent connections.",
35 | "ext-wincache": "This, APC or APCu is required for persistent connections. Reccomended for Windows.",
36 | "ext-openssl": "Enables encrypted connections."
37 | },
38 | "autoload": {
39 | "psr-0": {
40 | "PEAR2\\Net\\RouterOS\\": "src/",
41 | "PEAR2\\Net\\Transmitter\\": "vendor/pear2/net_transmitter/src/",
42 | "PEAR2\\Cache\\SHM": "vendor/pear2/cache_shm/src/",
43 | "PEAR2\\Console\\Color": "vendor/pear2/console_color/src/"
44 | }
45 | },
46 | "bin": ["scripts/roscon.php"],
47 | "minimum-stability": "dev"
48 | }
49 |
--------------------------------------------------------------------------------
/data/roscon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | RouterOS API console.
4 | GIT: $Id$
5 |
6 | Hostname of the RouterOS to connect to.
7 |
8 |
9 | Username to log in with. If left empty, no login will be performed.
10 | true
11 |
12 |
13 | Password to log in with.
14 | true
15 |
16 |
22 |
28 |
34 |
39 |
46 |
52 |
67 |
75 |
90 |
105 |
111 |
112 |
--------------------------------------------------------------------------------
/scripts/roscon:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 |
14 | * @copyright 2011 Vasil Rangelov
15 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
16 | * @version GIT: $Id$
17 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
18 | */
19 |
20 | /**
21 | * Run the console.
22 | */
23 | require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'roscon.php';
24 |
--------------------------------------------------------------------------------
/scripts/roscon.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | :: Prefer PHP binary in the following order:
3 | :: 1. Whatever %PHPBIN% points to.
4 | :: 2. "php" from %cd% with one of %pathext% extensions.
5 | :: 3. "php" from a %path% path with one of %pathext% extensions.
6 | :: 4. Whatever %PHP_PEAR_PHP_BIN% points to.
7 | ::
8 | :: Once a binary is found, a file is looked for that has the same name
9 | :: (including folder) as this batch file.
10 | :: Preferred extensions are ".php" and then no extension.
11 | ::
12 | :: On failure to find PHP binary or a PHP file, this batch file returns 255.
13 | goto SET_BIN
14 | :PHP_ERR
15 | echo PHP interpreter not found. Please set the %%PHPBIN%% or %%PHP_PEAR_PHP_BIN%% environment variable to one, or add one to your %%PATH%%.
16 | setlocal
17 | goto :END
18 | :FILE_ERR
19 | echo The file to be ran was not found. It should be at either "%~d0%~p0%~n0.php" or "%~d0%~p0%~n0".
20 | goto :END
21 | :SET_BIN
22 | if "%PHPBIN%" == "" set PHPBIN=php
23 | where /q %PHPBIN%
24 | if %ERRORLEVEL% == 0 goto SET_FILE
25 | if "%PHP_PEAR_PHP_BIN%" == "" goto PHP_ERR
26 | where /q "%PHP_PEAR_PHP_BIN%" 2>nul
27 | if %ERRORLEVEL% neq 0 goto PHP_ERR
28 | set PHPBIN=%PHP_PEAR_PHP_BIN%
29 | :SET_FILE
30 | setlocal
31 | set PHPFILE=%~d0%~p0%~n0.php
32 | if exist "%PHPFILE%" goto RUN
33 | set PHPFILE=%~d0%~p0%~n0
34 | if exist "%PHPFILE%" goto RUN
35 | goto FILE_ERR
36 | :RUN
37 | "%PHPBIN%" "%PHPFILE%" %*
38 | set PHPBIN_ERRORLEVEL="%ERRORLEVEL%"
39 | :END
40 | if "%PHPBIN_ERRORLEVEL%" == "" set PHPBIN_ERRORLEVEL=255
41 | exit /B %PHPBIN_ERRORLEVEL%
42 |
--------------------------------------------------------------------------------
/scripts/roscon.php:
--------------------------------------------------------------------------------
1 |
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 | /**
20 | * Used as a "catch all" for errors when connecting.
21 | */
22 | use Exception as E;
23 |
24 | /**
25 | * Used to register dependency paths, if needed.
26 | */
27 | use PEAR2\Autoload;
28 |
29 | /**
30 | * Used for coloring the output, if the "--colors" argument is specified.
31 | */
32 | use PEAR2\Console\Color;
33 |
34 | /**
35 | * Used for parsing the command line arguments.
36 | */
37 | use PEAR2\Console\CommandLine;
38 |
39 | /**
40 | * The whole application is around that.
41 | */
42 | use PEAR2\Net\RouterOS;
43 |
44 | /**
45 | * Used for error handling when connecting or receiving.
46 | */
47 | use PEAR2\Net\Transmitter\SocketException as SE;
48 |
49 | //Detect disallowed direct runs of either this file or "roscon".
50 | if (PHP_SAPI !== 'cli') {
51 | $includedFiles = get_included_files();
52 | $rosconPos = array_search(
53 | dirname(__FILE__) . DIRECTORY_SEPARATOR . 'roscon',
54 | $includedFiles,
55 | true
56 | );
57 | if (false !== $rosconPos) {
58 | unset($includedFiles[$rosconPos]);
59 | }
60 |
61 | if (count($includedFiles) === 1) {
62 | header('Content-Type: text/plain;charset=UTF-8');
63 | echo <<parse();
234 | } catch (CommandLine\Exception $e) {
235 | fwrite(
236 | STDERR,
237 | "Error when parsing command line: {$e->getMessage()}\n"
238 | );
239 | $cmdParser->displayUsage(13);
240 | }
241 |
242 | $comTimeout = null === $cmd->options['conTime']
243 | ? (null === $cmd->options['time']
244 | ? (int)ini_get('default_socket_timeout')
245 | : $cmd->options['time'])
246 | : $cmd->options['conTime'];
247 | $cmd->options['time'] = $cmd->options['time'] ?: 3;
248 | $comContext = null === $cmd->options['caPath']
249 | ? null
250 | : stream_context_create(
251 | is_file($cmd->options['caPath'])
252 | ? array(
253 | 'ssl' => array(
254 | 'verify_peer' => true,
255 | 'cafile' => $cmd->options['caPath'])
256 | )
257 | : array(
258 | 'ssl' => array(
259 | 'verify_peer' => true,
260 | 'capath' => $cmd->options['caPath'])
261 | )
262 | );
263 |
264 | $cColors = array(
265 | 'SEND' => '',
266 | 'SENT' => '',
267 | 'RECV' => '',
268 | 'ERR' => '',
269 | 'NOTE' => '',
270 | '' => ''
271 | );
272 | if ('auto' === $cmd->options['isColored']) {
273 | $cmd->options['isColored'] = ((strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN'
274 | || getenv('ANSICON_VER') != false)
275 | && class_exists('PEAR2\Console\Color', true)) ? 'yes' : 'no';
276 | }
277 | if ('yes' === $cmd->options['isColored']) {
278 | if (class_exists('PEAR2\Console\Color', true)) {
279 | $cColors['SEND'] = new Color(
280 | Color\Fonts::PURPLE
281 | );
282 | $cColors['SENT'] = clone $cColors['SEND'];
283 | $cColors['SENT']->setStyles(Color\Styles::UNDERLINE, true);
284 | $cColors['RECV'] = new Color(
285 | Color\Fonts::GREEN
286 | );
287 | $cColors['ERR'] = new Color(
288 | Color\Fonts::WHITE,
289 | Color\Backgrounds::RED
290 | );
291 | $cColors['NOTE'] = new Color(
292 | Color\Fonts::BLUE,
293 | Color\Backgrounds::YELLOW
294 | );
295 | $cColors[''] = new Color();
296 |
297 | foreach ($cColors as $mode => $color) {
298 | $cColors[$mode] = ((string)$color) . "\033[K";
299 | }
300 | } else {
301 | fwrite(
302 | STDERR,
303 | <<args['hostname'],
315 | $cmd->options['portNum'],
316 | false,
317 | $comTimeout,
318 | '',
319 | (string)$cmd->options['crypto'],
320 | $comContext
321 | );
322 | } catch (E $e) {
323 | fwrite(STDERR, "Error upon connecting: {$e->getMessage()}\n");
324 | $previous = $e->getPrevious();
325 | if ($previous instanceof SE) {
326 | fwrite(
327 | STDERR,
328 | "Details: ({$previous->getSocketErrorNumber()}) "
329 | . $previous->getSocketErrorMessage() . "\n\n"
330 | );
331 | }
332 | if ($e instanceof RouterOS\SocketException
333 | && $e->getCode() === RouterOS\SocketException::CODE_CONNECTION_FAIL
334 | ) {
335 | $phpBin = defined('PHP_BINARY')
336 | ? PHP_BINARY
337 | : (PHP_BINDIR . DIRECTORY_SEPARATOR .
338 | (PHP_SAPI === 'cli' ? 'php' : 'php-cgi') .
339 | (stristr(PHP_OS, 'win') === false ? '' : '.exe'));
340 | fwrite(
341 | STDERR,
342 | <<args['username']) {
408 | try {
409 | if (!RouterOS\Client::login(
410 | $com,
411 | $cmd->args['username'],
412 | (string)$cmd->args['password'],
413 | $comTimeout
414 | )) {
415 | fwrite(
416 | STDERR,
417 | <<getMessage());
446 | return;
447 | }
448 | }
449 |
450 | if ($cmd->options['verbose']) {
451 | $cSep = ' | ';
452 | $cColumns = array(
453 | 'mode' => 4,
454 | 'length' => 11,
455 | 'encodedLength' => 12
456 | );
457 | $cColumns['contents'] = $cmd->options['size'] - 1//row length
458 | - array_sum($cColumns)
459 | - (3/*strlen($c_sep)*/ * count($cColumns));
460 | fwrite(
461 | STDOUT,
462 | implode(
463 | "\n",
464 | array(
465 | implode(
466 | $cSep,
467 | array(
468 | str_pad(
469 | 'MODE',
470 | $cColumns['mode'],
471 | ' ',
472 | STR_PAD_RIGHT
473 | ),
474 | str_pad(
475 | 'LENGTH',
476 | $cColumns['length'],
477 | ' ',
478 | STR_PAD_BOTH
479 | ),
480 | str_pad(
481 | 'LENGTH',
482 | $cColumns['encodedLength'],
483 | ' ',
484 | STR_PAD_BOTH
485 | ),
486 | ' CONTENTS'
487 | )
488 | ),
489 | implode(
490 | $cSep,
491 | array(
492 | str_repeat(' ', $cColumns['mode']),
493 | str_pad(
494 | '(decoded)',
495 | $cColumns['length'],
496 | ' ',
497 | STR_PAD_BOTH
498 | ),
499 | str_pad(
500 | '(encoded)',
501 | $cColumns['encodedLength'],
502 | ' ',
503 | STR_PAD_BOTH
504 | ),
505 | ''
506 | )
507 | ),
508 | implode(
509 | '-|-',
510 | array(
511 | str_repeat('-', $cColumns['mode']),
512 | str_repeat('-', $cColumns['length']),
513 | str_repeat('-', $cColumns['encodedLength']),
514 | str_repeat('-', $cColumns['contents'])
515 | )
516 | )
517 | )
518 | ) . "\n"
519 | );
520 |
521 | $cRegexWrap = '/([^\n]{1,' . ($cColumns['contents']) . '})/sS';
522 |
523 | $printWord = function (
524 | $mode,
525 | $word,
526 | $msg = ''
527 | ) use (
528 | $cSep,
529 | $cColumns,
530 | $cRegexWrap,
531 | $cColors
532 | ) {
533 | $wordFragments = preg_split(
534 | $cRegexWrap,
535 | $word,
536 | null,
537 | PREG_SPLIT_DELIM_CAPTURE
538 | );
539 | for ($i = 0, $l = count($wordFragments); $i < $l; $i += 2) {
540 | unset($wordFragments[$i]);
541 | }
542 | if ('' !== $cColors['']) {
543 | $wordFragments = str_replace("\033", "\033[27@", $wordFragments);
544 | }
545 |
546 | $isAbnormal = 'ERR' === $mode || 'NOTE' === $mode;
547 | if ($isAbnormal) {
548 | $details = str_pad(
549 | $msg,
550 | $cColumns['length'] + $cColumns['encodedLength'] + 3,
551 | ' ',
552 | STR_PAD_BOTH
553 | );
554 | } else {
555 | $length = strlen($word);
556 | $lengthBytes = RouterOS\Communicator::encodeLength($length);
557 | $encodedLength = '';
558 | for ($i = 0, $l = strlen($lengthBytes); $i < $l; ++$i) {
559 | $encodedLength .= str_pad(
560 | dechex(ord($lengthBytes[$i])),
561 | 2,
562 | '0',
563 | STR_PAD_LEFT
564 | );
565 | }
566 |
567 | $details = str_pad(
568 | $length,
569 | $cColumns['length'],
570 | ' ',
571 | STR_PAD_LEFT
572 | ) .
573 | $cSep .
574 | str_pad(
575 | '0x' . strtoupper($encodedLength),
576 | $cColumns['encodedLength'],
577 | ' ',
578 | STR_PAD_LEFT
579 | );
580 | }
581 | fwrite(
582 | STDOUT,
583 | $cColors[$mode] .
584 | str_pad($mode, $cColumns['mode'], ' ', STR_PAD_RIGHT) .
585 | $cColors[''] .
586 | "{$cSep}{$details}{$cSep}{$cColors[$mode]}" .
587 | implode(
588 | "\n{$cColors['']}" .
589 | str_repeat(' ', $cColumns['mode']) .
590 | $cSep .
591 | implode(
592 | ($isAbnormal ? ' ' : $cSep),
593 | array(
594 | str_repeat(' ', $cColumns['length']),
595 | str_repeat(' ', $cColumns['encodedLength'])
596 | )
597 | ) . $cSep . $cColors[$mode],
598 | $wordFragments
599 | ) . "\n{$cColors['']}"
600 | );
601 | };
602 | } else {
603 | $printWord = function ($mode, $word, $msg = '') use ($cColors) {
604 | if ('' !== $cColors['']) {
605 | $word = str_replace("\033", "\033[27@", $word);
606 | $msg = str_replace("\033", "\033[27@", $msg);
607 | }
608 |
609 | if ('ERR' === $mode || 'NOTE' === $mode) {
610 | fwrite(STDERR, "{$cColors[$mode]}-- {$msg}");
611 | if ('' !== $word) {
612 | fwrite(STDERR, ": {$word}");
613 | }
614 | fwrite(STDERR, "{$cColors['']}\n");
615 | } elseif ('SENT' !== $mode) {
616 | fwrite(STDOUT, "{$cColors[$mode]}{$word}{$cColors['']}\n");
617 | }
618 | };
619 | }
620 |
621 | //Input/Output cycle
622 | while (true) {
623 | $prevWord = null;
624 | $word = '';
625 | $words = array();
626 |
627 |
628 | if (!$com->getTransmitter()->isAvailable()) {
629 | $printWord('NOTE', '', 'Connection terminated');
630 | break;
631 | }
632 |
633 | //Input cycle
634 | while (true) {
635 | if ($cmd->options['verbose']) {
636 | fwrite(
637 | STDOUT,
638 | implode(
639 | $cSep,
640 | array(
641 | $cColors['SEND'] .
642 | str_pad('SEND', $cColumns['mode'], ' ', STR_PAD_RIGHT)
643 | . $cColors[''],
644 | str_pad(
645 | '',
646 | $cColumns['length'],
647 | ' ',
648 | STR_PAD_LEFT
649 | ),
650 | str_pad(
651 | '',
652 | $cColumns['encodedLength'],
653 | ' ',
654 | STR_PAD_LEFT
655 | ),
656 | ''
657 | )
658 | )
659 | );
660 | }
661 |
662 | fwrite(STDOUT, (string)$cColors['SEND']);
663 |
664 | if ($cmd->options['multiline']) {
665 | while (true) {
666 | $line = stream_get_line(STDIN, PHP_INT_MAX, PHP_EOL);
667 | if (chr(3) === $line) {
668 | break;
669 | }
670 | if ((chr(3) . chr(3)) === $line) {
671 | $word .= chr(3);
672 | } else {
673 | $word .= $line . PHP_EOL;
674 | }
675 | if ($cmd->options['verbose']) {
676 | fwrite(
677 | STDOUT,
678 | "\n{$cColors['']}" .
679 | implode(
680 | $cSep,
681 | array(
682 | str_repeat(' ', $cColumns['mode']),
683 | str_repeat(' ', $cColumns['length']),
684 | str_repeat(' ', $cColumns['encodedLength']),
685 | ''
686 | )
687 | )
688 | . $cColors['SEND']
689 | );
690 | }
691 | }
692 | if ('' !== $word) {
693 | $word = substr($word, 0, -strlen(PHP_EOL));
694 | }
695 | } else {
696 | $word = stream_get_line(STDIN, PHP_INT_MAX, PHP_EOL);
697 | }
698 |
699 | if ($cmd->options['verbose']) {
700 | fwrite(STDOUT, "\n");
701 | }
702 | fwrite(STDOUT, (string)$cColors['']);
703 |
704 | $words[] = $word;
705 | if ('w' === $cmd->options['commandMode']) {
706 | break;
707 | }
708 | if ('' === $word) {
709 | if ('s' === $cmd->options['commandMode']) {
710 | break;
711 | } elseif ('' === $prevWord) {//'e' === $cmd->options['commandMode']
712 | array_pop($words);
713 | break;
714 | }
715 | }
716 | $prevWord = $word;
717 | $word = '';
718 | }
719 |
720 | //Input flush
721 | foreach ($words as $word) {
722 | try {
723 | $com->sendWord($word);
724 | $printWord('SENT', $word);
725 | } catch (SE $e) {
726 | if (0 === $e->getFragment()) {
727 | $printWord('ERR', '', 'Failed to send word');
728 | } else {
729 | $printWord(
730 | 'ERR',
731 | substr($word, 0, $e->getFragment()),
732 | 'Partial word sent'
733 | );
734 | }
735 | }
736 | }
737 |
738 | //Output cycle
739 | while (true) {
740 | if (!$com->getTransmitter()->isAvailable()) {
741 | break;
742 | }
743 |
744 | if (!$com->getTransmitter()->isDataAwaiting($cmd->options['time'])) {
745 | $printWord('NOTE', '', 'Receiving timed out');
746 | break;
747 | }
748 |
749 | try {
750 | $word = $com->getNextWord();
751 | $printWord('RECV', $word);
752 |
753 | if ('w' === $cmd->options['replyMode']
754 | || ('s' === $cmd->options['replyMode'] && '' === $word)
755 | ) {
756 | break;
757 | }
758 | } catch (SE $e) {
759 | if ('' === $e->getFragment()) {
760 | $printWord('ERR', '', 'Failed to receive word');
761 | } else {
762 | $printWord('ERR', $e->getFragment(), 'Partial word received');
763 | }
764 | break;
765 | } catch (RouterOS\NotSupportedException $e) {
766 | $printWord('ERR', $e->getValue(), 'Unsupported control byte');
767 | break;
768 | } catch (E $e) {
769 | $printWord('ERR', (string)$e, 'Unknown error');
770 | break;
771 | }
772 | }
773 | }
774 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/Client.php:
--------------------------------------------------------------------------------
1 |
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
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
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
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 | * Whether to stream future responses.
111 | *
112 | * @var bool
113 | */
114 | private $_streamingResponses = false;
115 |
116 | /**
117 | * Creates a new instance of a RouterOS API client.
118 | *
119 | * Creates a new instance of a RouterOS API client with the specified
120 | * settings.
121 | *
122 | * @param string $host Hostname (IP or domain) of RouterOS.
123 | * @param string $username The RouterOS username.
124 | * @param string $password The RouterOS password.
125 | * @param int|null $port The port on which the RouterOS host
126 | * provides the API service. You can also specify NULL, in which case
127 | * the port will automatically be chosen between 8728 and 8729,
128 | * depending on the value of $crypto.
129 | * @param bool $persist Whether or not the connection should be a
130 | * persistent one.
131 | * @param double|null $timeout The timeout for the connection.
132 | * @param string $crypto The encryption for this connection.
133 | * Must be one of the PEAR2\Net\Transmitter\NetworkStream::CRYPTO_*
134 | * constants. Off by default. RouterOS currently supports only TLS, but
135 | * the setting is provided in this fashion for forward compatibility's
136 | * sake. And for the sake of simplicity, if you specify an encryption,
137 | * don't specify a context and your default context uses the value
138 | * "DEFAULT" for ciphers, "ADH" will be automatically added to the list
139 | * of ciphers.
140 | * @param resource|null $context A context for the socket.
141 | *
142 | * @see sendSync()
143 | * @see sendAsync()
144 | */
145 | public function __construct(
146 | $host,
147 | $username,
148 | $password = '',
149 | $port = 8728,
150 | $persist = false,
151 | $timeout = null,
152 | $crypto = N::CRYPTO_OFF,
153 | $context = null
154 | ) {
155 | $this->com = new Communicator(
156 | $host,
157 | $port,
158 | $persist,
159 | $timeout,
160 | $username . '/' . $password,
161 | $crypto,
162 | $context
163 | );
164 | $timeout = null == $timeout
165 | ? ini_get('default_socket_timeout')
166 | : (int) $timeout;
167 | //Login the user if necessary
168 | if ((!$persist
169 | || !($old = $this->com->getTransmitter()->lock(S::DIRECTION_ALL)))
170 | && $this->com->getTransmitter()->isFresh()
171 | ) {
172 | if (!static::login($this->com, $username, $password, $timeout)) {
173 | $this->com->close();
174 | throw new DataFlowException(
175 | 'Invalid username or password supplied.',
176 | DataFlowException::CODE_INVALID_CREDENTIALS
177 | );
178 | }
179 | }
180 |
181 | if (isset($old)) {
182 | $this->com->getTransmitter()->lock($old, true);
183 | }
184 |
185 | if ($persist) {
186 | $this->registry = new Registry("{$host}:{$port}/{$username}");
187 | }
188 | }
189 |
190 | /**
191 | * A shorthand gateway.
192 | *
193 | * This is a magic PHP method that allows you to call the object as a
194 | * function. Depending on the argument given, one of the other functions in
195 | * the class is invoked and its returned value is returned by this function.
196 | *
197 | * @param mixed $arg Value can be either a {@link Request} to send, which
198 | * would be sent asynchronously if it has a tag, and synchronously if
199 | * not, a number to loop with or NULL to complete all pending requests.
200 | * Any other value is converted to string and treated as the tag of a
201 | * request to complete.
202 | *
203 | * @return mixed Whatever the long form function would have returned.
204 | */
205 | public function __invoke($arg = null)
206 | {
207 | if (is_int($arg) || is_double($arg)) {
208 | return $this->loop($arg);
209 | } elseif ($arg instanceof Request) {
210 | return '' == $arg->getTag() ? $this->sendSync($arg)
211 | : $this->sendAsync($arg);
212 | } elseif (null === $arg) {
213 | return $this->completeRequest();
214 | }
215 | return $this->completeRequest((string) $arg);
216 | }
217 |
218 | /**
219 | * Login to a RouterOS connection.
220 | *
221 | * @param Communicator $com The communicator to attempt to login to.
222 | * @param string $username The RouterOS username.
223 | * @param string $password The RouterOS password.
224 | * @param int|null $timeout The time to wait for each response. NULL
225 | * waits indefinitely.
226 | *
227 | * @return bool TRUE on success, FALSE on failure.
228 | */
229 | public static function login(
230 | Communicator $com,
231 | $username,
232 | $password = '',
233 | $timeout = null
234 | ) {
235 | if (null !== ($remoteCharset = $com->getCharset($com::CHARSET_REMOTE))
236 | && null !== ($localCharset = $com->getCharset($com::CHARSET_LOCAL))
237 | ) {
238 | $password = iconv(
239 | $localCharset,
240 | $remoteCharset . '//IGNORE//TRANSLIT',
241 | $password
242 | );
243 | }
244 | $old = null;
245 | try {
246 | if ($com->getTransmitter()->isPersistent()) {
247 | $old = $com->getTransmitter()->lock(S::DIRECTION_ALL);
248 | $result = self::_login($com, $username, $password, $timeout);
249 | $com->getTransmitter()->lock($old, true);
250 | return $result;
251 | }
252 | return self::_login($com, $username, $password, $timeout);
253 | } catch (E $e) {
254 | if ($com->getTransmitter()->isPersistent() && null !== $old) {
255 | $com->getTransmitter()->lock($old, true);
256 | }
257 | throw ($e instanceof NotSupportedException
258 | || $e instanceof UnexpectedValueException
259 | || !$com->getTransmitter()->isDataAwaiting()) ? new SocketException(
260 | 'This is not a compatible RouterOS service',
261 | SocketException::CODE_SERVICE_INCOMPATIBLE,
262 | $e
263 | ) : $e;
264 | }
265 | }
266 |
267 | /**
268 | * Login to a RouterOS connection.
269 | *
270 | * This is the actual login procedure, applied regardless of persistence and
271 | * charset settings.
272 | *
273 | * @param Communicator $com The communicator to attempt to login to.
274 | * @param string $username The RouterOS username.
275 | * @param string $password The RouterOS password. Potentially parsed
276 | * already by iconv.
277 | * @param int|null $timeout The time to wait for each response. NULL
278 | * waits indefinitely.
279 | *
280 | * @return bool TRUE on success, FALSE on failure.
281 | */
282 | private static function _login(
283 | Communicator $com,
284 | $username,
285 | $password = '',
286 | $timeout = null
287 | ) {
288 | $request = new Request('/login');
289 | $request->send($com);
290 | $response = new Response($com, false, $timeout);
291 | $request->setArgument('name', $username);
292 | $request->setArgument(
293 | 'response',
294 | '00' . md5(
295 | chr(0) . $password
296 | . pack('H*', $response->getProperty('ret'))
297 | )
298 | );
299 | $request->verify($com)->send($com);
300 |
301 | $response = new Response($com, false, $timeout);
302 | if ($response->getType() === Response::TYPE_FINAL) {
303 | return null === $response->getProperty('ret');
304 | } else {
305 | while ($response->getType() !== Response::TYPE_FINAL
306 | && $response->getType() !== Response::TYPE_FATAL
307 | ) {
308 | $response = new Response($com, false, $timeout);
309 | }
310 | return false;
311 | }
312 | }
313 |
314 | /**
315 | * Sets the charset(s) for this connection.
316 | *
317 | * Sets the charset(s) for this connection. The specified charset(s) will be
318 | * used for all future requests and responses. When sending,
319 | * {@link Communicator::CHARSET_LOCAL} is converted to
320 | * {@link Communicator::CHARSET_REMOTE}, and when receiving,
321 | * {@link Communicator::CHARSET_REMOTE} is converted to
322 | * {@link Communicator::CHARSET_LOCAL}. Setting NULL to either charset will
323 | * disable charset convertion, and data will be both sent and received "as
324 | * is".
325 | *
326 | * @param mixed $charset The charset to set. If $charsetType is
327 | * {@link Communicator::CHARSET_ALL}, you can supply either a string to
328 | * use for all charsets, or an array with the charset types as keys, and
329 | * the charsets as values.
330 | * @param int $charsetType Which charset to set. Valid values are the
331 | * Communicator::CHARSET_* constants. Any other value is treated as
332 | * {@link Communicator::CHARSET_ALL}.
333 | *
334 | * @return string|array The old charset. If $charsetType is
335 | * {@link Communicator::CHARSET_ALL}, the old values will be returned as
336 | * an array with the types as keys, and charsets as values.
337 | *
338 | * @see Communicator::setDefaultCharset()
339 | */
340 | public function setCharset(
341 | $charset,
342 | $charsetType = Communicator::CHARSET_ALL
343 | ) {
344 | return $this->com->setCharset($charset, $charsetType);
345 | }
346 |
347 | /**
348 | * Gets the charset(s) for this connection.
349 | *
350 | * @param int $charsetType Which charset to get. Valid values are the
351 | * Communicator::CHARSET_* constants. Any other value is treated as
352 | * {@link Communicator::CHARSET_ALL}.
353 | *
354 | * @return string|array The current charset. If $charsetType is
355 | * {@link Communicator::CHARSET_ALL}, the current values will be
356 | * returned as an array with the types as keys, and charsets as values.
357 | *
358 | * @see setCharset()
359 | */
360 | public function getCharset($charsetType)
361 | {
362 | return $this->com->getCharset($charsetType);
363 | }
364 |
365 | /**
366 | * Sends a request and waits for responses.
367 | *
368 | * @param Request $request The request to send.
369 | * @param callback|null $callback Optional. A function that is to be
370 | * executed when new responses for this request are available.
371 | * The callback takes two parameters. The {@link Response} object as
372 | * the first, and the {@link Client} object as the second one. If the
373 | * callback returns TRUE, the request is canceled. Note that the
374 | * callback may be executed at least two times after that. Once with a
375 | * {@link Response::TYPE_ERROR} response that notifies about the
376 | * canceling, plus the {@link Response::TYPE_FINAL} response.
377 | *
378 | * @return $this The client object.
379 | *
380 | * @see completeRequest()
381 | * @see loop()
382 | * @see cancelRequest()
383 | */
384 | public function sendAsync(Request $request, $callback = null)
385 | {
386 | //Error checking
387 | $tag = $request->getTag();
388 | if ('' == $tag) {
389 | throw new DataFlowException(
390 | 'Asynchonous commands must have a tag.',
391 | DataFlowException::CODE_TAG_REQUIRED
392 | );
393 | }
394 | if ($this->isRequestActive($tag)) {
395 | throw new DataFlowException(
396 | 'There must not be multiple active requests sharing a tag.',
397 | DataFlowException::CODE_TAG_UNIQUE
398 | );
399 | }
400 | if (null !== $callback && !is_callable($callback, true)) {
401 | throw new UnexpectedValueException(
402 | 'Invalid callback provided.',
403 | UnexpectedValueException::CODE_CALLBACK_INVALID
404 | );
405 | }
406 |
407 | $this->send($request);
408 |
409 | if (null === $callback) {
410 | //Register the request at the buffer
411 | $this->responseBuffer[$tag] = array();
412 | } else {
413 | //Prepare the callback
414 | $this->callbacks[$tag] = $callback;
415 | }
416 | return $this;
417 | }
418 |
419 | /**
420 | * Checks if a request is active.
421 | *
422 | * Checks if a request is active. A request is considered active if it's a
423 | * pending request and/or has responses that are not yet extracted.
424 | *
425 | * @param string $tag The tag of the request to look for.
426 | * @param int $filter One of the FILTER_* constants. Limits the search
427 | * to the specified places.
428 | *
429 | * @return bool TRUE if the request is active, FALSE otherwise.
430 | *
431 | * @see getPendingRequestsCount()
432 | * @see completeRequest()
433 | */
434 | public function isRequestActive($tag, $filter = self::FILTER_ALL)
435 | {
436 | $result = 0;
437 | if ($filter & self::FILTER_CALLBACK) {
438 | $result |= (int) array_key_exists($tag, $this->callbacks);
439 | }
440 | if ($filter & self::FILTER_BUFFER) {
441 | $result |= (int) array_key_exists($tag, $this->responseBuffer);
442 | }
443 | return 0 !== $result;
444 | }
445 |
446 | /**
447 | * Sends a request and gets the full response.
448 | *
449 | * @param Request $request The request to send.
450 | *
451 | * @return ResponseCollection The received responses as a collection.
452 | *
453 | * @see sendAsync()
454 | * @see close()
455 | */
456 | public function sendSync(Request $request)
457 | {
458 | $tag = $request->getTag();
459 | if ('' == $tag) {
460 | $this->send($request);
461 | } else {
462 | $this->sendAsync($request);
463 | }
464 | return $this->completeRequest($tag);
465 | }
466 |
467 | /**
468 | * Completes a specified request.
469 | *
470 | * Starts an event loop for the RouterOS callbacks and finishes when a
471 | * specified request is completed.
472 | *
473 | * @param string|null $tag The tag of the request to complete.
474 | * Setting NULL completes all requests.
475 | *
476 | * @return ResponseCollection A collection of {@link Response} objects that
477 | * haven't been passed to a callback function or previously extracted
478 | * with {@link static::extractNewResponses()}. Returns an empty
479 | * collection when $tag is set to NULL (responses can still be
480 | * extracted).
481 | */
482 | public function completeRequest($tag = null)
483 | {
484 | $hasNoTag = '' == $tag;
485 | $result = $hasNoTag ? array()
486 | : $this->extractNewResponses($tag)->toArray();
487 | while ((!$hasNoTag && $this->isRequestActive($tag))
488 | || ($hasNoTag && 0 !== $this->getPendingRequestsCount())
489 | ) {
490 | $newReply = $this->dispatchNextResponse(null);
491 | if ($newReply->getTag() === $tag) {
492 | if ($hasNoTag) {
493 | $result[] = $newReply;
494 | }
495 | if ($newReply->getType() === Response::TYPE_FINAL) {
496 | if (!$hasNoTag) {
497 | $result = array_merge(
498 | $result,
499 | $this->isRequestActive($tag)
500 | ? $this->extractNewResponses($tag)->toArray()
501 | : array()
502 | );
503 | }
504 | break;
505 | }
506 | }
507 | }
508 | return new ResponseCollection($result);
509 | }
510 |
511 | /**
512 | * Extracts responses for a request.
513 | *
514 | * Gets all new responses for a request that haven't been passed to a
515 | * callback and clears the buffer from them.
516 | *
517 | * @param string|null $tag The tag of the request to extract
518 | * new responses for.
519 | * Specifying NULL with extract new responses for all requests.
520 | *
521 | * @return ResponseCollection A collection of {@link Response} objects for
522 | * the specified request.
523 | *
524 | * @see loop()
525 | */
526 | public function extractNewResponses($tag = null)
527 | {
528 | if (null === $tag) {
529 | $result = array();
530 | foreach (array_keys($this->responseBuffer) as $tag) {
531 | $result = array_merge(
532 | $result,
533 | $this->extractNewResponses($tag)->toArray()
534 | );
535 | }
536 | return new ResponseCollection($result);
537 | } elseif ($this->isRequestActive($tag, self::FILTER_CALLBACK)) {
538 | return new ResponseCollection(array());
539 | } elseif ($this->isRequestActive($tag, self::FILTER_BUFFER)) {
540 | $result = $this->responseBuffer[$tag];
541 | if (!empty($result)) {
542 | if (end($result)->getType() === Response::TYPE_FINAL) {
543 | unset($this->responseBuffer[$tag]);
544 | } else {
545 | $this->responseBuffer[$tag] = array();
546 | }
547 | }
548 | return new ResponseCollection($result);
549 | } else {
550 | throw new DataFlowException(
551 | 'No such request, or the request has already finished.',
552 | DataFlowException::CODE_UNKNOWN_REQUEST
553 | );
554 | }
555 | }
556 |
557 | /**
558 | * Starts an event loop for the RouterOS callbacks.
559 | *
560 | * Starts an event loop for the RouterOS callbacks and finishes when there
561 | * are no more pending requests or when a specified timeout has passed
562 | * (whichever comes first).
563 | *
564 | * @param int|null $sTimeout Timeout for the loop.
565 | * If NULL, there is no time limit.
566 | * @param int $usTimeout Microseconds to add to the time limit.
567 | *
568 | * @return bool TRUE when there are any more pending requests, FALSE
569 | * otherwise.
570 | *
571 | * @see extractNewResponses()
572 | * @see getPendingRequestsCount()
573 | */
574 | public function loop($sTimeout = null, $usTimeout = 0)
575 | {
576 | try {
577 | if (null === $sTimeout) {
578 | while ($this->getPendingRequestsCount() !== 0) {
579 | $this->dispatchNextResponse(null);
580 | }
581 | } else {
582 | list($usStart, $sStart) = explode(' ', microtime());
583 | while ($this->getPendingRequestsCount() !== 0
584 | && ($sTimeout >= 0 || $usTimeout >= 0)
585 | ) {
586 | $this->dispatchNextResponse($sTimeout, $usTimeout);
587 | list($usEnd, $sEnd) = explode(' ', microtime());
588 |
589 | $sTimeout -= $sEnd - $sStart;
590 | $usTimeout -= $usEnd - $usStart;
591 | if ($usTimeout <= 0) {
592 | if ($sTimeout > 0) {
593 | $usTimeout = 1000000 + $usTimeout;
594 | $sTimeout--;
595 | }
596 | }
597 |
598 | $sStart = $sEnd;
599 | $usStart = $usEnd;
600 | }
601 | }
602 | } catch (SocketException $e) {
603 | if ($e->getCode() !== SocketException::CODE_NO_DATA) {
604 | // @codeCoverageIgnoreStart
605 | // It's impossible to reliably cause any other SocketException.
606 | // This line is only here in case the unthinkable happens:
607 | // The connection terminates just after it was supposedly
608 | // about to send back some data.
609 | throw $e;
610 | // @codeCoverageIgnoreEnd
611 | }
612 | }
613 | return $this->getPendingRequestsCount() !== 0;
614 | }
615 |
616 | /**
617 | * Gets the number of pending requests.
618 | *
619 | * @return int The number of pending requests.
620 | *
621 | * @see isRequestActive()
622 | */
623 | public function getPendingRequestsCount()
624 | {
625 | return $this->pendingRequestsCount;
626 | }
627 |
628 | /**
629 | * Cancels a request.
630 | *
631 | * Cancels an active request. Using this function in favor of a plain call
632 | * to the "/cancel" command is highly recommended, as it also updates the
633 | * counter of pending requests properly. Note that canceling a request also
634 | * removes any responses for it that were not previously extracted with
635 | * {@link static::extractNewResponses()}.
636 | *
637 | * @param string|null $tag Tag of the request to cancel.
638 | * Setting NULL will cancel all requests.
639 | *
640 | * @return $this The client object.
641 | *
642 | * @see sendAsync()
643 | * @see close()
644 | */
645 | public function cancelRequest($tag = null)
646 | {
647 | $cancelRequest = new Request('/cancel');
648 | $hasTag = !('' == $tag);
649 | $hasReg = null !== $this->registry;
650 | if ($hasReg && !$hasTag) {
651 | $tags = array_merge(
652 | array_keys($this->responseBuffer),
653 | array_keys($this->callbacks)
654 | );
655 | $this->registry->setTaglessMode(true);
656 | foreach ($tags as $t) {
657 | $cancelRequest->setArgument(
658 | 'tag',
659 | $this->registry->getOwnershipTag() . $t
660 | );
661 | $this->sendSync($cancelRequest);
662 | }
663 | $this->registry->setTaglessMode(false);
664 | } else {
665 | if ($hasTag) {
666 | if ($this->isRequestActive($tag)) {
667 | if ($hasReg) {
668 | $this->registry->setTaglessMode(true);
669 | $cancelRequest->setArgument(
670 | 'tag',
671 | $this->registry->getOwnershipTag() . $tag
672 | );
673 | } else {
674 | $cancelRequest->setArgument('tag', $tag);
675 | }
676 | } else {
677 | throw new DataFlowException(
678 | 'No such request. Canceling aborted.',
679 | DataFlowException::CODE_CANCEL_FAIL
680 | );
681 | }
682 | }
683 | $this->sendSync($cancelRequest);
684 | if ($hasReg) {
685 | $this->registry->setTaglessMode(false);
686 | }
687 | }
688 |
689 | if ($hasTag) {
690 | if ($this->isRequestActive($tag, self::FILTER_BUFFER)) {
691 | $this->responseBuffer[$tag] = $this->completeRequest($tag);
692 | } else {
693 | $this->completeRequest($tag);
694 | }
695 | } else {
696 | $this->loop();
697 | }
698 | return $this;
699 | }
700 |
701 | /**
702 | * Sets response streaming setting.
703 | *
704 | * Sets whether future responses are streamed. If responses are streamed,
705 | * the argument values are returned as streams instead of strings. This is
706 | * particularly useful if you expect a response that may contain one or more
707 | * very large words.
708 | *
709 | * @param bool $streamingResponses Whether to stream future responses.
710 | *
711 | * @return bool The previous value of the setting.
712 | *
713 | * @see isStreamingResponses()
714 | */
715 | public function setStreamingResponses($streamingResponses)
716 | {
717 | $oldValue = $this->_streamingResponses;
718 | $this->_streamingResponses = (bool) $streamingResponses;
719 | return $oldValue;
720 | }
721 |
722 | /**
723 | * Gets response streaming setting.
724 | *
725 | * Gets whether future responses are streamed.
726 | *
727 | * @return bool The value of the setting.
728 | *
729 | * @see setStreamingResponses()
730 | */
731 | public function isStreamingResponses()
732 | {
733 | return $this->_streamingResponses;
734 | }
735 |
736 | /**
737 | * Closes the opened connection, even if it is a persistent one.
738 | *
739 | * Closes the opened connection, even if it is a persistent one. Note that
740 | * {@link static::extractNewResponses()} can still be used to extract
741 | * responses collected prior to the closing.
742 | *
743 | * @return bool TRUE on success, FALSE on failure.
744 | */
745 | public function close()
746 | {
747 | $result = true;
748 | /*
749 | * The check below is done because for some unknown reason
750 | * (either a PHP or a RouterOS bug) calling "/quit" on an encrypted
751 | * connection makes one end hang.
752 | *
753 | * Since encrypted connections only appeared in RouterOS 6.1, and
754 | * the "/quit" call is needed for all <6.0 versions, problems due
755 | * to its absence should be limited to some earlier 6.* versions
756 | * on some RouterBOARD devices.
757 | */
758 | if ($this->com->getTransmitter()->getCrypto() === N::CRYPTO_OFF) {
759 | if (null !== $this->registry) {
760 | $this->registry->setTaglessMode(true);
761 | }
762 | try {
763 | $response = $this->sendSync(new Request('/quit'));
764 | $result = $response[0]->getType() === Response::TYPE_FATAL;
765 | } catch (SocketException $e) {
766 | $result
767 | = $e->getCode() === SocketException::CODE_REQUEST_SEND_FAIL;
768 | } catch (E $e) {
769 | //Ignore unknown errors.
770 | }
771 | if (null !== $this->registry) {
772 | $this->registry->setTaglessMode(false);
773 | }
774 | }
775 | $result = $result && $this->com->close();
776 | $this->callbacks = array();
777 | $this->pendingRequestsCount = 0;
778 | return $result;
779 | }
780 |
781 | /**
782 | * Closes the connection, unless it's a persistent one.
783 | */
784 | public function __destruct()
785 | {
786 | if ($this->com->getTransmitter()->isPersistent()) {
787 | if (0 !== $this->pendingRequestsCount) {
788 | $this->cancelRequest();
789 | }
790 | } else {
791 | $this->close();
792 | }
793 | }
794 |
795 | /**
796 | * Sends a request to RouterOS.
797 | *
798 | * @param Request $request The request to send.
799 | *
800 | * @return $this The client object.
801 | *
802 | * @see sendSync()
803 | * @see sendAsync()
804 | */
805 | protected function send(Request $request)
806 | {
807 | $request->verify($this->com)->send($this->com, $this->registry);
808 | $this->pendingRequestsCount++;
809 | return $this;
810 | }
811 |
812 | /**
813 | * Dispatches the next response in queue.
814 | *
815 | * Dispatches the next response in queue, i.e. it executes the associated
816 | * callback if there is one, or places the response in the response buffer.
817 | *
818 | * @param int|null $sTimeout If a response is not immediately available,
819 | * wait this many seconds.
820 | * If NULL, wait indefinitely.
821 | * @param int $usTimeout Microseconds to add to the waiting time.
822 | *
823 | * @throws SocketException When there's no response within the time limit.
824 | * @return Response The dispatched response.
825 | */
826 | protected function dispatchNextResponse($sTimeout = 0, $usTimeout = 0)
827 | {
828 | $response = new Response(
829 | $this->com,
830 | $this->_streamingResponses,
831 | $sTimeout,
832 | $usTimeout,
833 | $this->registry
834 | );
835 | if ($response->getType() === Response::TYPE_FATAL) {
836 | $this->pendingRequestsCount = 0;
837 | $this->com->close();
838 | return $response;
839 | }
840 |
841 | $tag = $response->getTag();
842 | $isLastForRequest = $response->getType() === Response::TYPE_FINAL;
843 | if ($isLastForRequest) {
844 | $this->pendingRequestsCount--;
845 | }
846 |
847 | if ('' != $tag) {
848 | if ($this->isRequestActive($tag, self::FILTER_CALLBACK)) {
849 | if ($this->callbacks[$tag]($response, $this)) {
850 | try {
851 | $this->cancelRequest($tag);
852 | } catch (DataFlowException $e) {
853 | if ($e->getCode() !== DataFlowException::CODE_UNKNOWN_REQUEST
854 | ) {
855 | throw $e;
856 | }
857 | }
858 | } elseif ($isLastForRequest) {
859 | unset($this->callbacks[$tag]);
860 | }
861 | } else {
862 | $this->responseBuffer[$tag][] = $response;
863 | }
864 | }
865 | return $response;
866 | }
867 | }
868 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/Communicator.php:
--------------------------------------------------------------------------------
1 |
13 | * @copyright 2011 Vasil Rangelov
14 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
15 | * @version GIT: $Id$
16 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
17 | */
18 | /**
19 | * The namespace declaration.
20 | */
21 | namespace PEAR2\Net\RouterOS;
22 |
23 | /**
24 | * Using transmitters.
25 | */
26 | use PEAR2\Net\Transmitter as T;
27 |
28 | /**
29 | * A RouterOS communicator.
30 | *
31 | * Implementation of the RouterOS API protocol. Unlike the other classes in this
32 | * package, this class doesn't provide any conveniences beyond the low level
33 | * implementation details (automatic word length encoding/decoding, charset
34 | * translation and data integrity), and because of that, its direct usage is
35 | * strongly discouraged.
36 | *
37 | * @category Net
38 | * @package PEAR2_Net_RouterOS
39 | * @author Vasil Rangelov
40 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
41 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
42 | * @see Client
43 | */
44 | class Communicator
45 | {
46 | /**
47 | * Used when getting/setting all (default) charsets.
48 | */
49 | const CHARSET_ALL = -1;
50 |
51 | /**
52 | * Used when getting/setting the (default) remote charset.
53 | *
54 | * The remote charset is the charset in which RouterOS stores its data.
55 | * If you want to keep compatibility with your Winbox, this charset should
56 | * match the default charset from your Windows' regional settings.
57 | */
58 | const CHARSET_REMOTE = 0;
59 |
60 | /**
61 | * Used when getting/setting the (default) local charset.
62 | *
63 | * The local charset is the charset in which the data from RouterOS will be
64 | * returned as. This charset should match the charset of the place the data
65 | * will eventually be written to.
66 | */
67 | const CHARSET_LOCAL = 1;
68 |
69 | /**
70 | * An array with the default charset.
71 | *
72 | * Charset types as keys, and the default charsets as values.
73 | *
74 | * @var array
75 | */
76 | protected static $defaultCharsets = array(
77 | self::CHARSET_REMOTE => null,
78 | self::CHARSET_LOCAL => null
79 | );
80 |
81 | /**
82 | * An array with the current charset.
83 | *
84 | * Charset types as keys, and the current charsets as values.
85 | *
86 | * @var array
87 | */
88 | protected $charsets = array();
89 |
90 | /**
91 | * The transmitter for the connection.
92 | *
93 | * @var T\TcpClient
94 | */
95 | protected $trans;
96 |
97 | /**
98 | * Creates a new connection with the specified options.
99 | *
100 | * @param string $host Hostname (IP or domain) of RouterOS.
101 | * @param int|null $port The port on which the RouterOS host
102 | * provides the API service. You can also specify NULL, in which case
103 | * the port will automatically be chosen between 8728 and 8729,
104 | * depending on the value of $crypto.
105 | * @param bool $persist Whether or not the connection should be a
106 | * persistent one.
107 | * @param double|null $timeout The timeout for the connection.
108 | * @param string $key A string that uniquely identifies the
109 | * connection.
110 | * @param string $crypto The encryption for this connection.
111 | * Must be one of the PEAR2\Net\Transmitter\NetworkStream::CRYPTO_*
112 | * constants. Off by default. RouterOS currently supports only TLS, but
113 | * the setting is provided in this fashion for forward compatibility's
114 | * sake. And for the sake of simplicity, if you specify an encryption,
115 | * don't specify a context and your default context uses the value
116 | * "DEFAULT" for ciphers, "ADH" will be automatically added to the list
117 | * of ciphers.
118 | * @param resource|null $context A context for the socket.
119 | *
120 | * @see sendWord()
121 | */
122 | public function __construct(
123 | $host,
124 | $port = 8728,
125 | $persist = false,
126 | $timeout = null,
127 | $key = '',
128 | $crypto = T\NetworkStream::CRYPTO_OFF,
129 | $context = null
130 | ) {
131 | $isUnencrypted = T\NetworkStream::CRYPTO_OFF === $crypto;
132 | if (($context === null) && !$isUnencrypted) {
133 | $context = stream_context_get_default();
134 | $opts = stream_context_get_options($context);
135 | if (!isset($opts['ssl']['ciphers'])
136 | || 'DEFAULT' === $opts['ssl']['ciphers']
137 | ) {
138 | stream_context_set_option(
139 | $context,
140 | array(
141 | 'ssl' => array(
142 | 'ciphers' => 'ADH',
143 | 'verify_peer' => false,
144 | 'verify_peer_name' => false
145 | )
146 | )
147 | );
148 | }
149 | }
150 | // @codeCoverageIgnoreStart
151 | // The $port is customizable in testing.
152 | if (null === $port) {
153 | $port = $isUnencrypted ? 8728 : 8729;
154 | }
155 | // @codeCoverageIgnoreEnd
156 |
157 | try {
158 | $this->trans = new T\TcpClient(
159 | $host,
160 | $port,
161 | $persist,
162 | $timeout,
163 | $key,
164 | $crypto,
165 | $context
166 | );
167 | } catch (T\Exception $e) {
168 | throw new SocketException(
169 | 'Error connecting to RouterOS',
170 | SocketException::CODE_CONNECTION_FAIL,
171 | $e
172 | );
173 | }
174 | $this->setCharset(
175 | self::getDefaultCharset(self::CHARSET_ALL),
176 | self::CHARSET_ALL
177 | );
178 | }
179 |
180 | /**
181 | * A shorthand gateway.
182 | *
183 | * This is a magic PHP method that allows you to call the object as a
184 | * function. Depending on the argument given, one of the other functions in
185 | * the class is invoked and its returned value is returned by this function.
186 | *
187 | * @param string|null $string A string of the word to send, or NULL to get
188 | * the next word as a string.
189 | *
190 | * @return int|string If a string is provided, returns the number of bytes
191 | * sent, otherwise returns the next word as a string.
192 | */
193 | public function __invoke($string = null)
194 | {
195 | return null === $string ? $this->getNextWord()
196 | : $this->sendWord($string);
197 | }
198 |
199 | /**
200 | * Checks whether a variable is a seekable stream resource.
201 | *
202 | * @param mixed $var The value to check.
203 | *
204 | * @return bool TRUE if $var is a seekable stream, FALSE otherwise.
205 | */
206 | public static function isSeekableStream($var)
207 | {
208 | if (T\Stream::isStream($var)) {
209 | $meta = stream_get_meta_data($var);
210 | return $meta['seekable'];
211 | }
212 | return false;
213 | }
214 |
215 | /**
216 | * Uses iconv to convert a stream from one charset to another.
217 | *
218 | * @param string $inCharset The charset of the stream.
219 | * @param string $outCharset The desired resulting charset.
220 | * @param resource $stream The stream to convert. The stream is assumed
221 | * to be seekable, and is read from its current position to its end,
222 | * after which, it is seeked back to its starting position.
223 | *
224 | * @return resource A new stream that uses the $out_charset. The stream is a
225 | * subset from the original stream, from its current position to its
226 | * end, seeked at its start.
227 | */
228 | public static function iconvStream($inCharset, $outCharset, $stream)
229 | {
230 | $bytes = 0;
231 | $result = fopen('php://temp', 'r+b');
232 | $iconvFilter = stream_filter_append(
233 | $result,
234 | 'convert.iconv.' . $inCharset . '.' . $outCharset,
235 | STREAM_FILTER_WRITE
236 | );
237 |
238 | flock($stream, LOCK_SH);
239 | $reader = new T\Stream($stream, false);
240 | $writer = new T\Stream($result, false);
241 | $chunkSize = $reader->getChunk(T\Stream::DIRECTION_RECEIVE);
242 | while ($reader->isAvailable() && $reader->isDataAwaiting()) {
243 | $bytes += $writer->send(fread($stream, $chunkSize));
244 | }
245 | fseek($stream, -$bytes, SEEK_CUR);
246 | flock($stream, LOCK_UN);
247 |
248 | stream_filter_remove($iconvFilter);
249 | rewind($result);
250 | return $result;
251 | }
252 |
253 | /**
254 | * Sets the default charset(s) for new connections.
255 | *
256 | * @param mixed $charset The charset to set. If $charsetType is
257 | * {@link self::CHARSET_ALL}, you can supply either a string to use for
258 | * all charsets, or an array with the charset types as keys, and the
259 | * charsets as values.
260 | * @param int $charsetType Which charset to set. Valid values are the
261 | * CHARSET_* constants. Any other value is treated as
262 | * {@link self::CHARSET_ALL}.
263 | *
264 | * @return string|array The old charset. If $charsetType is
265 | * {@link self::CHARSET_ALL}, the old values will be returned as an
266 | * array with the types as keys, and charsets as values.
267 | *
268 | * @see setCharset()
269 | */
270 | public static function setDefaultCharset(
271 | $charset,
272 | $charsetType = self::CHARSET_ALL
273 | ) {
274 | if (array_key_exists($charsetType, self::$defaultCharsets)) {
275 | $oldCharset = self::$defaultCharsets[$charsetType];
276 | self::$defaultCharsets[$charsetType] = $charset;
277 | return $oldCharset;
278 | } else {
279 | $oldCharsets = self::$defaultCharsets;
280 | self::$defaultCharsets = is_array($charset) ? $charset : array_fill(
281 | 0,
282 | count(self::$defaultCharsets),
283 | $charset
284 | );
285 | return $oldCharsets;
286 | }
287 | }
288 |
289 | /**
290 | * Gets the default charset(s).
291 | *
292 | * @param int $charsetType Which charset to get. Valid values are the
293 | * CHARSET_* constants. Any other value is treated as
294 | * {@link self::CHARSET_ALL}.
295 | *
296 | * @return string|array The current charset. If $charsetType is
297 | * {@link self::CHARSET_ALL}, the current values will be returned as an
298 | * array with the types as keys, and charsets as values.
299 | *
300 | * @see setDefaultCharset()
301 | */
302 | public static function getDefaultCharset($charsetType)
303 | {
304 | return array_key_exists($charsetType, self::$defaultCharsets)
305 | ? self::$defaultCharsets[$charsetType] : self::$defaultCharsets;
306 | }
307 |
308 | /**
309 | * Gets the length of a seekable stream.
310 | *
311 | * Gets the length of a seekable stream.
312 | *
313 | * @param resource $stream The stream to check. The stream is assumed to be
314 | * seekable.
315 | *
316 | * @return double The number of bytes in the stream between its current
317 | * position and its end.
318 | */
319 | public static function seekableStreamLength($stream)
320 | {
321 | $streamPosition = (double) sprintf('%u', ftell($stream));
322 | fseek($stream, 0, SEEK_END);
323 | $streamLength = ((double) sprintf('%u', ftell($stream)))
324 | - $streamPosition;
325 | fseek($stream, $streamPosition, SEEK_SET);
326 | return $streamLength;
327 | }
328 |
329 | /**
330 | * Sets the charset(s) for this connection.
331 | *
332 | * Sets the charset(s) for this connection. The specified charset(s) will be
333 | * used for all future words. When sending, {@link self::CHARSET_LOCAL} is
334 | * converted to {@link self::CHARSET_REMOTE}, and when receiving,
335 | * {@link self::CHARSET_REMOTE} is converted to {@link self::CHARSET_LOCAL}.
336 | * Setting NULL to either charset will disable charset conversion, and data
337 | * will be both sent and received "as is".
338 | *
339 | * @param mixed $charset The charset to set. If $charsetType is
340 | * {@link self::CHARSET_ALL}, you can supply either a string to use for
341 | * all charsets, or an array with the charset types as keys, and the
342 | * charsets as values.
343 | * @param int $charsetType Which charset to set. Valid values are the
344 | * CHARSET_* constants. Any other value is treated as
345 | * {@link self::CHARSET_ALL}.
346 | *
347 | * @return string|array The old charset. If $charsetType is
348 | * {@link self::CHARSET_ALL}, the old values will be returned as an
349 | * array with the types as keys, and charsets as values.
350 | *
351 | * @see setDefaultCharset()
352 | */
353 | public function setCharset($charset, $charsetType = self::CHARSET_ALL)
354 | {
355 | if (array_key_exists($charsetType, $this->charsets)) {
356 | $oldCharset = $this->charsets[$charsetType];
357 | $this->charsets[$charsetType] = $charset;
358 | return $oldCharset;
359 | } else {
360 | $oldCharsets = $this->charsets;
361 | $this->charsets = is_array($charset) ? $charset : array_fill(
362 | 0,
363 | count($this->charsets),
364 | $charset
365 | );
366 | return $oldCharsets;
367 | }
368 | }
369 |
370 | /**
371 | * Gets the charset(s) for this connection.
372 | *
373 | * @param int $charsetType Which charset to get. Valid values are the
374 | * CHARSET_* constants. Any other value is treated as
375 | * {@link self::CHARSET_ALL}.
376 | *
377 | * @return string|array The current charset. If $charsetType is
378 | * {@link self::CHARSET_ALL}, the current values will be returned as an
379 | * array with the types as keys, and charsets as values.
380 | *
381 | * @see getDefaultCharset()
382 | * @see setCharset()
383 | */
384 | public function getCharset($charsetType)
385 | {
386 | return array_key_exists($charsetType, $this->charsets)
387 | ? $this->charsets[$charsetType] : $this->charsets;
388 | }
389 |
390 | /**
391 | * Gets the transmitter for this connection.
392 | *
393 | * @return T\TcpClient The transmitter for this connection.
394 | */
395 | public function getTransmitter()
396 | {
397 | return $this->trans;
398 | }
399 |
400 | /**
401 | * Sends a word.
402 | *
403 | * Sends a word and automatically encodes its length when doing so.
404 | *
405 | * @param string $word The word to send.
406 | *
407 | * @return int The number of bytes sent.
408 | *
409 | * @see sendWordFromStream()
410 | * @see getNextWord()
411 | */
412 | public function sendWord($word)
413 | {
414 | if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
415 | && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
416 | ) {
417 | $word = iconv(
418 | $localCharset,
419 | $remoteCharset . '//IGNORE//TRANSLIT',
420 | $word
421 | );
422 | }
423 | $length = strlen($word);
424 | static::verifyLengthSupport($length);
425 | if ($this->trans->isPersistent()) {
426 | $old = $this->trans->lock(T\Stream::DIRECTION_SEND);
427 | $bytes = $this->trans->send(self::encodeLength($length) . $word);
428 | $this->trans->lock($old, true);
429 | return $bytes;
430 | }
431 | return $this->trans->send(self::encodeLength($length) . $word);
432 | }
433 |
434 | /**
435 | * Sends a word based on a stream.
436 | *
437 | * Sends a word based on a stream and automatically encodes its length when
438 | * doing so. The stream is read from its current position to its end, and
439 | * then returned to its current position. Because of those operations, the
440 | * supplied stream must be seekable.
441 | *
442 | * @param string $prefix A string to prepend before the stream contents.
443 | * @param resource $stream The seekable stream to send.
444 | *
445 | * @return int The number of bytes sent.
446 | *
447 | * @see sendWord()
448 | */
449 | public function sendWordFromStream($prefix, $stream)
450 | {
451 | if (!self::isSeekableStream($stream)) {
452 | throw new InvalidArgumentException(
453 | 'The stream must be seekable.',
454 | InvalidArgumentException::CODE_SEEKABLE_REQUIRED
455 | );
456 | }
457 | if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
458 | && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
459 | ) {
460 | $prefix = iconv(
461 | $localCharset,
462 | $remoteCharset . '//IGNORE//TRANSLIT',
463 | $prefix
464 | );
465 | $stream = self::iconvStream(
466 | $localCharset,
467 | $remoteCharset . '//IGNORE//TRANSLIT',
468 | $stream
469 | );
470 | }
471 |
472 | flock($stream, LOCK_SH);
473 | $totalLength = strlen($prefix) + self::seekableStreamLength($stream);
474 | static::verifyLengthSupport($totalLength);
475 |
476 | $bytes = $this->trans->send(self::encodeLength($totalLength) . $prefix);
477 | $bytes += $this->trans->send($stream);
478 |
479 | flock($stream, LOCK_UN);
480 | return $bytes;
481 | }
482 |
483 | /**
484 | * Verifies that the length is supported.
485 | *
486 | * Verifies if the specified length is supported by the API. Throws a
487 | * {@link LengthException} if that's not the case. Currently, RouterOS
488 | * supports words up to 0xFFFFFFFF in length, so that's the only check
489 | * performed.
490 | *
491 | * @param int $length The length to verify.
492 | *
493 | * @return void
494 | */
495 | public static function verifyLengthSupport($length)
496 | {
497 | if ($length > 0xFFFFFFFF) {
498 | throw new LengthException(
499 | 'Words with length above 0xFFFFFFFF are not supported.',
500 | LengthException::CODE_UNSUPPORTED,
501 | null,
502 | $length
503 | );
504 | }
505 | }
506 |
507 | /**
508 | * Encodes the length as required by the RouterOS API.
509 | *
510 | * @param int $length The length to encode.
511 | *
512 | * @return string The encoded length.
513 | */
514 | public static function encodeLength($length)
515 | {
516 | if ($length < 0) {
517 | throw new LengthException(
518 | 'Length must not be negative.',
519 | LengthException::CODE_INVALID,
520 | null,
521 | $length
522 | );
523 | } elseif ($length < 0x80) {
524 | return chr($length);
525 | } elseif ($length < 0x4000) {
526 | return pack('n', $length |= 0x8000);
527 | } elseif ($length < 0x200000) {
528 | $length |= 0xC00000;
529 | return pack('n', $length >> 8) . chr($length & 0xFF);
530 | } elseif ($length < 0x10000000) {
531 | return pack('N', $length |= 0xE0000000);
532 | } elseif ($length <= 0xFFFFFFFF) {
533 | return chr(0xF0) . pack('N', $length);
534 | } elseif ($length <= 0x7FFFFFFFF) {
535 | $length = 'f' . base_convert($length, 10, 16);
536 | return chr(hexdec(substr($length, 0, 2))) .
537 | pack('N', hexdec(substr($length, 2)));
538 | }
539 | throw new LengthException(
540 | 'Length must not be above 0x7FFFFFFFF.',
541 | LengthException::CODE_BEYOND_SHEME,
542 | null,
543 | $length
544 | );
545 | }
546 |
547 | /**
548 | * Get the next word in queue as a string.
549 | *
550 | * Get the next word in queue as a string, after automatically decoding its
551 | * length.
552 | *
553 | * @return string The word.
554 | *
555 | * @see close()
556 | */
557 | public function getNextWord()
558 | {
559 | if ($this->trans->isPersistent()) {
560 | $old = $this->trans->lock(T\Stream::DIRECTION_RECEIVE);
561 | $word = $this->trans->receive(
562 | self::decodeLength($this->trans),
563 | 'word'
564 | );
565 | $this->trans->lock($old, true);
566 | } else {
567 | $word = $this->trans->receive(
568 | self::decodeLength($this->trans),
569 | 'word'
570 | );
571 | }
572 |
573 | if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
574 | && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
575 | ) {
576 | $word = iconv(
577 | $remoteCharset,
578 | $localCharset . '//IGNORE//TRANSLIT',
579 | $word
580 | );
581 | }
582 |
583 | return $word;
584 | }
585 |
586 | /**
587 | * Get the next word in queue as a stream.
588 | *
589 | * Get the next word in queue as a stream, after automatically decoding its
590 | * length.
591 | *
592 | * @return resource The word, as a stream.
593 | *
594 | * @see close()
595 | */
596 | public function getNextWordAsStream()
597 | {
598 | $filters = new T\FilterCollection();
599 | if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
600 | && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
601 | ) {
602 | $filters->append(
603 | 'convert.iconv.' .
604 | $remoteCharset . '.' . $localCharset . '//IGNORE//TRANSLIT'
605 | );
606 | }
607 |
608 | if ($this->trans->isPersistent()) {
609 | $old = $this->trans->lock(T\Stream::DIRECTION_RECEIVE);
610 | $stream = $this->trans->receiveStream(
611 | self::decodeLength($this->trans),
612 | $filters,
613 | 'stream word'
614 | );
615 | $this->trans->lock($old, true);
616 | } else {
617 | $stream = $this->trans->receiveStream(
618 | self::decodeLength($this->trans),
619 | $filters,
620 | 'stream word'
621 | );
622 | }
623 |
624 | return $stream;
625 | }
626 |
627 | /**
628 | * Decodes the length of the incoming message.
629 | *
630 | * Decodes the length of the incoming message, as specified by the RouterOS
631 | * API.
632 | *
633 | * @param T\Stream $trans The transmitter from which to decode the length of
634 | * the incoming message.
635 | *
636 | * @return int|double The decoded length.
637 | * Is of type "double" only for values above "2 << 31".
638 | */
639 | public static function decodeLength(T\Stream $trans)
640 | {
641 | if ($trans->isPersistent() && $trans instanceof T\TcpClient) {
642 | $old = $trans->lock($trans::DIRECTION_RECEIVE);
643 | $length = self::_decodeLength($trans);
644 | $trans->lock($old, true);
645 | return $length;
646 | }
647 | return self::_decodeLength($trans);
648 | }
649 |
650 | /**
651 | * Decodes the length of the incoming message.
652 | *
653 | * Decodes the length of the incoming message, as specified by the RouterOS
654 | * API.
655 | *
656 | * Difference with the non private function is that this one doesn't perform
657 | * locking if the connection is a persistent one.
658 | *
659 | * @param T\Stream $trans The transmitter from which to decode the length of
660 | * the incoming message.
661 | *
662 | * @return int|double The decoded length.
663 | * Is of type "double" only for values above "2 << 31".
664 | */
665 | private static function _decodeLength(T\Stream $trans)
666 | {
667 | $byte = ord($trans->receive(1, 'initial length byte'));
668 | if ($byte & 0x80) {
669 | if (($byte & 0xC0) === 0x80) {
670 | return (($byte & 077) << 8 ) + ord($trans->receive(1));
671 | } elseif (($byte & 0xE0) === 0xC0) {
672 | $rem = unpack('n~', $trans->receive(2));
673 | return (($byte & 037) << 16 ) + $rem['~'];
674 | } elseif (($byte & 0xF0) === 0xE0) {
675 | $rem = unpack('n~/C~~', $trans->receive(3));
676 | return (($byte & 017) << 24 ) + ($rem['~'] << 8) + $rem['~~'];
677 | } elseif (($byte & 0xF8) === 0xF0) {
678 | $rem = unpack('N~', $trans->receive(4));
679 | return (($byte & 007) * 0x100000000/* '<< 32' or '2^32' */)
680 | + (double) sprintf('%u', $rem['~']);
681 | }
682 | throw new NotSupportedException(
683 | 'Unknown control byte encountered.',
684 | NotSupportedException::CODE_CONTROL_BYTE,
685 | null,
686 | $byte
687 | );
688 | } else {
689 | return $byte;
690 | }
691 | }
692 |
693 | /**
694 | * Closes the opened connection, even if it is a persistent one.
695 | *
696 | * @return bool TRUE on success, FALSE on failure.
697 | */
698 | public function close()
699 | {
700 | return $this->trans->close();
701 | }
702 | }
703 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/DataFlowException.php:
--------------------------------------------------------------------------------
1 |
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 | * Base of this class.
25 | */
26 | use RuntimeException;
27 |
28 | /**
29 | * Exception thrown when the request/response cycle goes an unexpected way.
30 | *
31 | * @category Net
32 | * @package PEAR2_Net_RouterOS
33 | * @author Vasil Rangelov
34 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
35 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
36 | */
37 | class DataFlowException extends RuntimeException implements Exception
38 | {
39 | const CODE_INVALID_CREDENTIALS = 10000;
40 | const CODE_TAG_REQUIRED = 10500;
41 | const CODE_TAG_UNIQUE = 10501;
42 | const CODE_UNKNOWN_REQUEST = 10900;
43 | const CODE_CANCEL_FAIL = 11200;
44 | }
45 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/Exception.php:
--------------------------------------------------------------------------------
1 |
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 | * Generic exception class of this package.
25 | *
26 | * @category Net
27 | * @package PEAR2_Net_RouterOS
28 | * @author Vasil Rangelov
29 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
30 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
31 | */
32 | interface Exception
33 | {
34 | }
35 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/InvalidArgumentException.php:
--------------------------------------------------------------------------------
1 |
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 | use InvalidArgumentException as I;
24 |
25 | /**
26 | * Exception thrown when there's something wrong with message arguments.
27 | *
28 | * @category Net
29 | * @package PEAR2_Net_RouterOS
30 | * @author Vasil Rangelov
31 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
32 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
33 | */
34 | class InvalidArgumentException extends I implements Exception
35 | {
36 | const CODE_SEEKABLE_REQUIRED = 1100;
37 | const CODE_NAME_INVALID = 20100;
38 | const CODE_ABSOLUTE_REQUIRED = 40200;
39 | const CODE_CMD_UNRESOLVABLE = 40201;
40 | const CODE_CMD_INVALID = 40202;
41 | const CODE_NAME_UNPARSABLE = 41000;
42 | const CODE_VALUE_UNPARSABLE = 41001;
43 | }
44 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/LengthException.php:
--------------------------------------------------------------------------------
1 |
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 | * Base of this class.
25 | */
26 | use LengthException as L;
27 |
28 | /**
29 | * Used in $previous
30 | */
31 | use Exception as E;
32 |
33 | /**
34 | * Exception thrown when there is a problem with a word's length.
35 | *
36 | * @category Net
37 | * @package PEAR2_Net_RouterOS
38 | * @author Vasil Rangelov
39 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
40 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
41 | */
42 | class LengthException extends L implements Exception
43 | {
44 |
45 | const CODE_UNSUPPORTED = 1200;
46 | const CODE_INVALID = 1300;
47 | const CODE_BEYOND_SHEME = 1301;
48 |
49 | /**
50 | * The problematic length.
51 | *
52 | * @var int|double|null
53 | */
54 | private $_length;
55 |
56 | /**
57 | * Creates a new LengthException.
58 | *
59 | * @param string $message The Exception message to throw.
60 | * @param int $code The Exception code.
61 | * @param E|null $previous The previous exception used for the
62 | * exception chaining.
63 | * @param int|double|null $length The length.
64 | */
65 | public function __construct(
66 | $message,
67 | $code = 0,
68 | E $previous = null,
69 | $length = null
70 | ) {
71 | parent::__construct($message, $code, $previous);
72 | $this->_length = $length;
73 | }
74 |
75 | /**
76 | * Gets the length.
77 | *
78 | * @return int|double|null The length.
79 | */
80 | public function getLength()
81 | {
82 | return $this->_length;
83 | }
84 |
85 | // @codeCoverageIgnoreStart
86 | // String representation is not reliable in testing
87 |
88 | /**
89 | * Returns a string representation of the exception.
90 | *
91 | * @return string The exception as a string.
92 | */
93 | public function __toString()
94 | {
95 | return parent::__toString() . "\nLength:{$this->_length}";
96 | }
97 |
98 | // @codeCoverageIgnoreEnd
99 | }
100 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/Message.php:
--------------------------------------------------------------------------------
1 |
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 | * Implements this interface.
25 | */
26 | use Countable;
27 |
28 | /**
29 | * Implements this interface.
30 | */
31 | use IteratorAggregate;
32 |
33 | /**
34 | * Required for IteratorAggregate::getIterator() to work properly with foreach.
35 | */
36 | use ArrayObject;
37 |
38 | /**
39 | * Represents a RouterOS message.
40 | *
41 | * @category Net
42 | * @package PEAR2_Net_RouterOS
43 | * @author Vasil Rangelov
44 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
45 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
46 | */
47 | abstract class Message implements IteratorAggregate, Countable
48 | {
49 |
50 | /**
51 | * An array with message attributes.
52 | *
53 | * Each array key is the the name of an attribute,
54 | * and the corresponding array value is the value for that attribute.
55 | *
56 | * @var array
57 | */
58 | protected $attributes = array();
59 |
60 | /**
61 | * An optional tag to associate the message with.
62 | *
63 | * @var string
64 | */
65 | private $_tag = null;
66 |
67 | /**
68 | * A shorthand gateway.
69 | *
70 | * This is a magic PHP method that allows you to call the object as a
71 | * function. Depending on the argument given, one of the other functions in
72 | * the class is invoked and its returned value is returned by this function.
73 | *
74 | * @param string|null $name The name of an attribute to get the value of,
75 | * or NULL to get the tag.
76 | *
77 | * @return string|resource The value of the specified attribute,
78 | * or the tag if NULL is provided.
79 | */
80 | public function __invoke($name = null)
81 | {
82 | if (null === $name) {
83 | return $this->getTag();
84 | }
85 | return $this->getAttribute($name);
86 | }
87 |
88 | /**
89 | * Sanitizes a name of an attribute (message or query one).
90 | *
91 | * @param mixed $name The name to sanitize.
92 | *
93 | * @return string The sanitized name.
94 | */
95 | public static function sanitizeAttributeName($name)
96 | {
97 | $name = (string) $name;
98 | if ((empty($name) && $name !== '0')
99 | || preg_match('/[=\s]/s', $name)
100 | ) {
101 | throw new InvalidArgumentException(
102 | 'Invalid name of argument supplied.',
103 | InvalidArgumentException::CODE_NAME_INVALID
104 | );
105 | }
106 | return $name;
107 | }
108 |
109 | /**
110 | * Sanitizes a value of an attribute (message or query one).
111 | *
112 | * @param mixed $value The value to sanitize.
113 | *
114 | * @return string|resource The sanitized value.
115 | */
116 | public static function sanitizeAttributeValue($value)
117 | {
118 | if (Communicator::isSeekableStream($value)) {
119 | return $value;
120 | } else {
121 | return (string) $value;
122 | }
123 | }
124 |
125 | /**
126 | * Gets the tag that the message is associated with.
127 | *
128 | * @return string The current tag or NULL if there isn't a tag.
129 | *
130 | * @see setTag()
131 | */
132 | public function getTag()
133 | {
134 | return $this->_tag;
135 | }
136 |
137 | /**
138 | * Sets the tag to associate the request with.
139 | *
140 | * Sets the tag to associate the message with. Setting NULL erases the
141 | * currently set tag.
142 | *
143 | * @param string $tag The tag to set.
144 | *
145 | * @return $this The message object.
146 | *
147 | * @see getTag()
148 | */
149 | protected function setTag($tag)
150 | {
151 | $this->_tag = (null === $tag) ? null : (string) $tag;
152 | return $this;
153 | }
154 |
155 | /**
156 | * Gets the value of an attribute.
157 | *
158 | * @param string $name The name of the attribute.
159 | *
160 | * @return string|resource|null The value of the specified attribute.
161 | * Returns NULL if such an attribute is not set.
162 | *
163 | * @see setAttribute()
164 | */
165 | protected function getAttribute($name)
166 | {
167 | $name = self::sanitizeAttributeName($name);
168 | if (array_key_exists($name, $this->attributes)) {
169 | return $this->attributes[$name];
170 | }
171 | return null;
172 | }
173 |
174 | /**
175 | * Gets all arguments in an array.
176 | *
177 | * @return ArrayObject An ArrayObject with the keys being argument names,
178 | * and the array values being argument values.
179 | *
180 | * @see getArgument()
181 | * @see setArgument()
182 | */
183 | public function getIterator()
184 | {
185 | return new ArrayObject($this->attributes);
186 | }
187 |
188 | /**
189 | * Counts the number of attributes.
190 | *
191 | * @return int The number of attributes.
192 | */
193 | public function count()
194 | {
195 | return count($this->attributes);
196 | }
197 |
198 | /**
199 | * Sets an attribute for the message.
200 | *
201 | * @param string $name Name of the attribute.
202 | * @param string|resource|null $value Value of the attribute as a string or
203 | * seekable stream.
204 | * Setting the value to NULL removes an argument of this name.
205 | * If a seekable stream is provided, it is sent from its current
206 | * position to its end, and the pointer is seeked back to its current
207 | * position after sending.
208 | * Non seekable streams, as well as all other types, are casted to a
209 | * string.
210 | *
211 | * @return $this The message object.
212 | *
213 | * @see getArgument()
214 | */
215 | protected function setAttribute($name, $value = '')
216 | {
217 | if (null === $value) {
218 | unset($this->attributes[self::sanitizeAttributeName($name)]);
219 | } else {
220 | $this->attributes[self::sanitizeAttributeName($name)]
221 | = self::sanitizeAttributeValue($value);
222 | }
223 | return $this;
224 | }
225 |
226 | /**
227 | * Removes all attributes from the message.
228 | *
229 | * @return $this The message object.
230 | */
231 | protected function removeAllAttributes()
232 | {
233 | $this->attributes = array();
234 | return $this;
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/NotSupportedException.php:
--------------------------------------------------------------------------------
1 |
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 | * Base of this class.
25 | */
26 | use Exception as E;
27 |
28 | /**
29 | * Exception thrown when encountering something not supported by RouterOS or
30 | * this package.
31 | *
32 | * @category Net
33 | * @package PEAR2_Net_RouterOS
34 | * @author Vasil Rangelov
35 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
36 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
37 | */
38 | class NotSupportedException extends E implements Exception
39 | {
40 |
41 | const CODE_CONTROL_BYTE = 1601;
42 |
43 | const CODE_MENU_MISMATCH = 60000;
44 |
45 | const CODE_ARG_PROHIBITED = 60001;
46 |
47 | /**
48 | * The unsupported value.
49 | *
50 | * @var mixed
51 | */
52 | private $_value;
53 |
54 | /**
55 | * Creates a new NotSupportedException.
56 | *
57 | * @param string $message The Exception message to throw.
58 | * @param int $code The Exception code.
59 | * @param E|null $previous The previous exception used for the exception
60 | * chaining.
61 | * @param mixed $value The unsupported value.
62 | */
63 | public function __construct(
64 | $message,
65 | $code = 0,
66 | E $previous = null,
67 | $value = null
68 | ) {
69 | parent::__construct($message, $code, $previous);
70 | $this->_value = $value;
71 | }
72 |
73 | /**
74 | * Gets the unsupported value.
75 | *
76 | * @return mixed The unsupported value.
77 | */
78 | public function getValue()
79 | {
80 | return $this->_value;
81 | }
82 |
83 | // @codeCoverageIgnoreStart
84 | // String representation is not reliable in testing
85 |
86 | /**
87 | * Returns a string representation of the exception.
88 | *
89 | * @return string The exception as a string.
90 | */
91 | public function __toString()
92 | {
93 | return parent::__toString() . "\nValue:{$this->_value}";
94 | }
95 |
96 | // @codeCoverageIgnoreEnd
97 | }
98 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/ParserException.php:
--------------------------------------------------------------------------------
1 |
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 | * Base of this class.
25 | */
26 | use DomainException;
27 |
28 | /**
29 | * Exception thrown when a value can't be parsed properly.
30 | *
31 | * @category Net
32 | * @package PEAR2_Net_RouterOS
33 | * @author Vasil Rangelov
34 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
35 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
36 | */
37 | class ParserException extends DomainException implements Exception
38 | {
39 | const CODE_DATETIME = 1;
40 | const CODE_DATEINTERVAL = 2;
41 | const CODE_ARRAY = 3;
42 | }
43 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/Query.php:
--------------------------------------------------------------------------------
1 |
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 as T;
27 |
28 | /**
29 | * Represents a query for RouterOS requests.
30 | *
31 | * @category Net
32 | * @package PEAR2_Net_RouterOS
33 | * @author Vasil Rangelov
34 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
35 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
36 | */
37 | class Query
38 | {
39 |
40 | /**
41 | * Checks if the property exists.
42 | */
43 | const OP_EX = '';
44 |
45 | /**
46 | * Checks if the property does not exist.
47 | */
48 | const OP_NEX = '-';
49 |
50 | /**
51 | * Checks if the property equals a certain value.
52 | */
53 | const OP_EQ = '=';
54 |
55 | /**
56 | * Checks if the property is less than a certain value.
57 | */
58 | const OP_LT = '<';
59 |
60 | /**
61 | * Checks if the property is greater than a certain value.
62 | */
63 | const OP_GT = '>';
64 |
65 | /**
66 | * An array of the words forming the query.
67 | *
68 | * Each value is an array with the first member being the predicate
69 | * (operator and name), and the second member being the value
70 | * for the predicate.
71 | *
72 | * @var array[]
73 | */
74 | protected $words = array();
75 |
76 | /**
77 | * This class is not to be instantiated normally, but by static methods
78 | * instead. Use {@link static::where()} to create an instance of it.
79 | */
80 | protected function __construct()
81 | {
82 |
83 | }
84 |
85 | /**
86 | * Sanitizes the operator of a condition.
87 | *
88 | * @param string $operator The operator to sanitize.
89 | *
90 | * @return string The sanitized operator.
91 | */
92 | protected static function sanitizeOperator($operator)
93 | {
94 | $operator = (string) $operator;
95 | switch ($operator) {
96 | case Query::OP_EX:
97 | case Query::OP_NEX:
98 | case Query::OP_EQ:
99 | case Query::OP_LT:
100 | case Query::OP_GT:
101 | return $operator;
102 | default:
103 | throw new UnexpectedValueException(
104 | 'Unknown operator specified',
105 | UnexpectedValueException::CODE_ACTION_UNKNOWN,
106 | null,
107 | $operator
108 | );
109 | }
110 | }
111 |
112 | /**
113 | * Creates a new query with an initial condition.
114 | *
115 | * @param string $name The name of the property to test.
116 | * @param string|resource|null $value Value of the property as a string
117 | * or seekable stream. Not required for existence tests.
118 | * If a seekable stream is provided, it is sent from its current
119 | * position to its end, and the pointer is seeked back to its current
120 | * position after sending.
121 | * Non seekable streams, as well as all other types, are casted to a
122 | * string.
123 | * @param string $operator One of the OP_* constants.
124 | * Describes the operation to perform.
125 | *
126 | * @return static A new query object.
127 | */
128 | public static function where(
129 | $name,
130 | $value = null,
131 | $operator = self::OP_EX
132 | ) {
133 | $query = new static;
134 | return $query->addWhere($name, $value, $operator);
135 | }
136 |
137 | /**
138 | * Negates the query.
139 | *
140 | * @return $this The query object.
141 | */
142 | public function not()
143 | {
144 | $this->words[] = array('#!', null);
145 | return $this;
146 | }
147 |
148 | /**
149 | * Adds a condition as an alternative to the query.
150 | *
151 | * @param string $name The name of the property to test.
152 | * @param string|resource|null $value Value of the property as a string
153 | * or seekable stream. Not required for existence tests.
154 | * If a seekable stream is provided, it is sent from its current
155 | * position to its end, and the pointer is seeked back to its current
156 | * position after sending.
157 | * Non seekable streams, as well as all other types, are casted to a
158 | * string.
159 | * @param string $operator One of the OP_* constants.
160 | * Describes the operation to perform.
161 | *
162 | * @return $this The query object.
163 | */
164 | public function orWhere($name, $value = null, $operator = self::OP_EX)
165 | {
166 | $this->addWhere($name, $value, $operator)->words[] = array('#|', null);
167 | return $this;
168 | }
169 |
170 | /**
171 | * Adds a condition in addition to the query.
172 | *
173 | * @param string $name The name of the property to test.
174 | * @param string|resource|null $value Value of the property as a string
175 | * or seekable stream. Not required for existence tests.
176 | * If a seekable stream is provided, it is sent from its current
177 | * position to its end, and the pointer is seeked back to its current
178 | * position after sending.
179 | * Non seekable streams, as well as all other types, are casted to a
180 | * string.
181 | * @param string $operator One of the OP_* constants.
182 | * Describes the operation to perform.
183 | *
184 | * @return $this The query object.
185 | */
186 | public function andWhere($name, $value = null, $operator = self::OP_EX)
187 | {
188 | $this->addWhere($name, $value, $operator)->words[] = array('#&', null);
189 | return $this;
190 | }
191 |
192 | /**
193 | * Sends the query over a communicator.
194 | *
195 | * @param Communicator $com The communicator to send the query over.
196 | *
197 | * @return int The number of bytes sent.
198 | */
199 | public function send(Communicator $com)
200 | {
201 | if ($com->getTransmitter()->isPersistent()) {
202 | $old = $com->getTransmitter()->lock(T\Stream::DIRECTION_SEND);
203 | $bytes = $this->_send($com);
204 | $com->getTransmitter()->lock($old, true);
205 | return $bytes;
206 | }
207 | return $this->_send($com);
208 | }
209 |
210 | /**
211 | * Sends the query over a communicator.
212 | *
213 | * The only difference with the non private equivalent is that this one does
214 | * not do locking.
215 | *
216 | * @param Communicator $com The communicator to send the query over.
217 | *
218 | * @return int The number of bytes sent.
219 | */
220 | private function _send(Communicator $com)
221 | {
222 | if (!$com->getTransmitter()->isAcceptingData()) {
223 | throw new SocketException(
224 | 'Transmitter is invalid. Sending aborted.',
225 | SocketException::CODE_QUERY_SEND_FAIL
226 | );
227 | }
228 | $bytes = 0;
229 | foreach ($this->words as $queryWord) {
230 | list($predicate, $value) = $queryWord;
231 | $prefix = '?' . $predicate;
232 | if (null === $value) {
233 | $bytes += $com->sendWord($prefix);
234 | } else {
235 | $prefix .= '=';
236 | if (is_string($value)) {
237 | $bytes += $com->sendWord($prefix . $value);
238 | } else {
239 | $bytes += $com->sendWordFromStream($prefix, $value);
240 | }
241 | }
242 | }
243 | return $bytes;
244 | }
245 |
246 | /**
247 | * Verifies the query.
248 | *
249 | * Verifies the query against a communicator, i.e. whether the query
250 | * could successfully be sent (assuming the connection is still opened).
251 | *
252 | * @param Communicator $com The Communicator to check against.
253 | *
254 | * @return $this The query object itself.
255 | *
256 | * @throws LengthException If the resulting length of an API word is not
257 | * supported.
258 | */
259 | public function verify(Communicator $com)
260 | {
261 | foreach ($this->words as $queryWord) {
262 | list($predicate, $value) = $queryWord;
263 | if (null === $value) {
264 | $com::verifyLengthSupport(strlen('?' . $predicate));
265 | } elseif (is_string($value)) {
266 | $com::verifyLengthSupport(
267 | strlen('?' . $predicate . '=' . $value)
268 | );
269 | } else {
270 | $com::verifyLengthSupport(
271 | strlen('?' . $predicate . '=') +
272 | $com::seekableStreamLength($value)
273 | );
274 | }
275 | }
276 | return $this;
277 | }
278 |
279 | /**
280 | * Adds a condition.
281 | *
282 | * @param string $name The name of the property to test.
283 | * @param string|resource|null $value Value of the property as a string
284 | * or seekable stream. Not required for existence tests.
285 | * If a seekable stream is provided, it is sent from its current
286 | * position to its end, and the pointer is seeked back to its current
287 | * position after sending.
288 | * Non seekable streams, as well as all other types, are casted to a
289 | * string.
290 | * @param string $operator One of the ACTION_* constants.
291 | * Describes the operation to perform.
292 | *
293 | * @return $this The query object.
294 | */
295 | protected function addWhere($name, $value, $operator)
296 | {
297 | $this->words[] = array(
298 | static::sanitizeOperator($operator)
299 | . Message::sanitizeAttributeName($name),
300 | (null === $value ? null : Message::sanitizeAttributeValue($value))
301 | );
302 | return $this;
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/Registry.php:
--------------------------------------------------------------------------------
1 |
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 | * Uses shared memory to keep responses in.
25 | */
26 | use PEAR2\Cache\SHM;
27 |
28 | /**
29 | * A RouterOS registry.
30 | *
31 | * Provides functionality for managing the request/response flow. Particularly
32 | * useful in persistent connections.
33 | *
34 | * Note that this class is not meant to be called directly.
35 | *
36 | * @category Net
37 | * @package PEAR2_Net_RouterOS
38 | * @author Vasil Rangelov
39 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
40 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
41 | */
42 | class Registry
43 | {
44 | /**
45 | * The storage.
46 | *
47 | * @var SHM
48 | */
49 | protected $shm;
50 |
51 | /**
52 | * ID of request. Populated at first instance in request.
53 | *
54 | * @var int
55 | */
56 | protected static $requestId = -1;
57 |
58 | /**
59 | * ID to be given to next instance, after incrementing it.
60 | *
61 | * @var int
62 | */
63 | protected static $instanceIdSeed = -1;
64 |
65 | /**
66 | * ID of instance within the request.
67 | *
68 | * @var int
69 | */
70 | protected $instanceId;
71 |
72 | /**
73 | * Creates a registry.
74 | *
75 | * @param string $uri An URI to bind the registry to.
76 | */
77 | public function __construct($uri)
78 | {
79 | $this->shm = SHM::factory(__CLASS__ . ' ' . $uri);
80 | if (-1 === self::$requestId) {
81 | self::$requestId = $this->shm->add('requestId', 0)
82 | ? 0 : $this->shm->inc('requestId');
83 | }
84 | $this->instanceId = ++self::$instanceIdSeed;
85 | $this->shm->add('responseBuffer_' . $this->getOwnershipTag(), array());
86 | }
87 |
88 | /**
89 | * Parses a tag.
90 | *
91 | * Parses a tag to reveal the ownership part of it, and the original tag.
92 | *
93 | * @param string $tag The tag (as received) to parse.
94 | *
95 | * @return array An array with
96 | * the first member being the ownership tag, and
97 | * the second one being the original tag.
98 | */
99 | public static function parseTag($tag)
100 | {
101 | if (null === $tag) {
102 | return array(null, null);
103 | }
104 | $result = explode('__', $tag, 2);
105 | $result[0] .= '__';
106 | if ('' === $result[1]) {
107 | $result[1] = null;
108 | }
109 | return $result;
110 | }
111 |
112 | /**
113 | * Checks if this instance is the tagless mode owner.
114 | *
115 | * @return bool TRUE if this instance is the tagless mode owner, FALSE
116 | * otherwise.
117 | */
118 | public function isTaglessModeOwner()
119 | {
120 | $this->shm->lock('taglessModeOwner');
121 | $result = $this->shm->exists('taglessModeOwner')
122 | && $this->getOwnershipTag() === $this->shm->get('taglessModeOwner');
123 | $this->shm->unlock('taglessModeOwner');
124 | return $result;
125 | }
126 |
127 | /**
128 | * Sets the "tagless mode" setting.
129 | *
130 | * While in tagless mode, this instance will claim ownership of any
131 | * responses without a tag. While not in this mode, any requests without a
132 | * tag will be given to all instances.
133 | *
134 | * Regardless of mode, if the type of the response is
135 | * {@link Response::TYPE_FATAL}, it will be given to all instances.
136 | *
137 | * @param bool $taglessMode TRUE to claim tagless ownership, FALSE to
138 | * release such ownership, if taken.
139 | *
140 | * @return bool TRUE on success, FALSE on failure.
141 | */
142 | public function setTaglessMode($taglessMode)
143 | {
144 | return $taglessMode
145 | ? ($this->shm->lock('taglessMode')
146 | && $this->shm->lock('taglessModeOwner')
147 | && $this->shm->add('taglessModeOwner', $this->getOwnershipTag())
148 | && $this->shm->unlock('taglessModeOwner'))
149 | : ($this->isTaglessModeOwner()
150 | && $this->shm->lock('taglessModeOwner')
151 | && $this->shm->delete('taglessModeOwner')
152 | && $this->shm->unlock('taglessModeOwner')
153 | && $this->shm->unlock('taglessMode'));
154 | }
155 |
156 | /**
157 | * Get the ownership tag for this instance.
158 | *
159 | * @return string The ownership tag for this registry instance.
160 | */
161 | public function getOwnershipTag()
162 | {
163 | return self::$requestId . '_' . $this->instanceId . '__';
164 | }
165 |
166 | /**
167 | * Add a response to the registry.
168 | *
169 | * @param Response $response The response to add. The caller of this
170 | * function is responsible for ensuring that the ownership tag and the
171 | * original tag are separated, so that only the original one remains in
172 | * the response.
173 | * @param string $ownershipTag The ownership tag that the response had.
174 | *
175 | * @return bool TRUE if the request was added to its buffer, FALSE if
176 | * this instance owns the response, and therefore doesn't need to add
177 | * the response to its buffer.
178 | */
179 | public function add(Response $response, $ownershipTag)
180 | {
181 | if ($this->getOwnershipTag() === $ownershipTag
182 | || ($this->isTaglessModeOwner()
183 | && $response->getType() !== Response::TYPE_FATAL)
184 | ) {
185 | return false;
186 | }
187 |
188 | if (null === $ownershipTag) {
189 | $this->shm->lock('taglessModeOwner');
190 | if ($this->shm->exists('taglessModeOwner')
191 | && $response->getType() !== Response::TYPE_FATAL
192 | ) {
193 | $ownershipTag = $this->shm->get('taglessModeOwner');
194 | $this->shm->unlock('taglessModeOwner');
195 | } else {
196 | $this->shm->unlock('taglessModeOwner');
197 | foreach ($this->shm->getIterator(
198 | '/^(responseBuffer\_)/',
199 | true
200 | ) as $targetBufferName) {
201 | $this->_add($response, $targetBufferName);
202 | }
203 | return true;
204 | }
205 | }
206 |
207 | $this->_add($response, 'responseBuffer_' . $ownershipTag);
208 | return true;
209 | }
210 |
211 | /**
212 | * Adds a response to a buffer.
213 | *
214 | * @param Response $response The response to add.
215 | * @param string $targetBufferName The name of the buffer to add the
216 | * response to.
217 | *
218 | * @return void
219 | */
220 | private function _add(Response $response, $targetBufferName)
221 | {
222 | if ($this->shm->lock($targetBufferName)) {
223 | $targetBuffer = $this->shm->get($targetBufferName);
224 | $targetBuffer[] = $response;
225 | $this->shm->set($targetBufferName, $targetBuffer);
226 | $this->shm->unlock($targetBufferName);
227 | }
228 | }
229 |
230 | /**
231 | * Gets the next response from this instance's buffer.
232 | *
233 | * @return Response|null The next response, or NULL if there isn't one.
234 | */
235 | public function getNextResponse()
236 | {
237 | $response = null;
238 | $targetBufferName = 'responseBuffer_' . $this->getOwnershipTag();
239 | if ($this->shm->exists($targetBufferName)
240 | && $this->shm->lock($targetBufferName)
241 | ) {
242 | $targetBuffer = $this->shm->get($targetBufferName);
243 | if (!empty($targetBuffer)) {
244 | $response = array_shift($targetBuffer);
245 | $this->shm->set($targetBufferName, $targetBuffer);
246 | }
247 | $this->shm->unlock($targetBufferName);
248 | }
249 | return $response;
250 | }
251 |
252 | /**
253 | * Closes the registry.
254 | *
255 | * Closes the registry, meaning that all buffers are cleared.
256 | *
257 | * @return void
258 | */
259 | public function close()
260 | {
261 | self::$requestId = -1;
262 | self::$instanceIdSeed = -1;
263 | $this->shm->clear();
264 | }
265 |
266 | /**
267 | * Removes a buffer.
268 | *
269 | * @param string $targetBufferName The buffer to remove.
270 | *
271 | * @return void
272 | */
273 | private function _close($targetBufferName)
274 | {
275 | if ($this->shm->lock($targetBufferName)) {
276 | $this->shm->delete($targetBufferName);
277 | $this->shm->unlock($targetBufferName);
278 | }
279 | }
280 |
281 | /**
282 | * Removes this instance's buffer.
283 | */
284 | public function __destruct()
285 | {
286 | $this->_close('responseBuffer_' . $this->getOwnershipTag());
287 | }
288 | }
289 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/Request.php:
--------------------------------------------------------------------------------
1 |
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 as T;
27 |
28 | /**
29 | * Represents a RouterOS request.
30 | *
31 | * @category Net
32 | * @package PEAR2_Net_RouterOS
33 | * @author Vasil Rangelov
34 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
35 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
36 | */
37 | class Request extends Message
38 | {
39 |
40 | /**
41 | * The command to be executed.
42 | *
43 | * @var string
44 | */
45 | private $_command;
46 |
47 | /**
48 | * A query for the command.
49 | *
50 | * @var Query
51 | */
52 | private $_query;
53 |
54 | /**
55 | * Creates a request to send to RouterOS.
56 | *
57 | * @param string $command The command to send.
58 | * Can also contain arguments expressed in a shell-like syntax.
59 | * @param Query|null $query A query to associate with the request.
60 | * @param string|null $tag The tag for the request.
61 | *
62 | * @see setCommand()
63 | * @see setArgument()
64 | * @see setTag()
65 | * @see setQuery()
66 | */
67 | public function __construct($command, Query $query = null, $tag = null)
68 | {
69 | if (false !== strpos($command, '=')
70 | && false !== ($spaceBeforeEquals = strrpos(
71 | strstr($command, '=', true),
72 | ' '
73 | ))
74 | ) {
75 | $this->parseArgumentString(substr($command, $spaceBeforeEquals));
76 | $command = rtrim(substr($command, 0, $spaceBeforeEquals));
77 | }
78 | $this->setCommand($command);
79 | $this->setQuery($query);
80 | $this->setTag($tag);
81 | }
82 |
83 | /**
84 | * A shorthand gateway.
85 | *
86 | * This is a magic PHP method that allows you to call the object as a
87 | * function. Depending on the argument given, one of the other functions in
88 | * the class is invoked and its returned value is returned by this function.
89 | *
90 | * @param Query|Communicator|string|null $arg A {@link Query} to associate
91 | * the request with, a {@link Communicator} to send the request over,
92 | * an argument to get the value of, or NULL to get the tag. If a
93 | * second argument is provided, this becomes the name of the argument to
94 | * set the value of, and the second argument is the value to set.
95 | *
96 | * @return string|resource|int|$this Whatever the long form
97 | * function returns.
98 | */
99 | public function __invoke($arg = null)
100 | {
101 | if (func_num_args() > 1) {
102 | return $this->setArgument(func_get_arg(0), func_get_arg(1));
103 | }
104 | if ($arg instanceof Query) {
105 | return $this->setQuery($arg);
106 | }
107 | if ($arg instanceof Communicator) {
108 | return $this->send($arg);
109 | }
110 | return parent::__invoke($arg);
111 | }
112 |
113 | /**
114 | * Sets the command to send to RouterOS.
115 | *
116 | * Sets the command to send to RouterOS. The command can use the API or CLI
117 | * syntax of RouterOS, but either way, it must be absolute (begin with a
118 | * "/") and without arguments.
119 | *
120 | * @param string $command The command to send.
121 | *
122 | * @return $this The request object.
123 | *
124 | * @see getCommand()
125 | * @see setArgument()
126 | */
127 | public function setCommand($command)
128 | {
129 | $command = (string) $command;
130 | if (strpos($command, '/') !== 0) {
131 | throw new InvalidArgumentException(
132 | 'Commands must be absolute.',
133 | InvalidArgumentException::CODE_ABSOLUTE_REQUIRED
134 | );
135 | }
136 | if (substr_count($command, '/') === 1) {
137 | //Command line syntax convertion
138 | $cmdParts = preg_split('#[\s/]+#sm', $command);
139 | $cmdRes = array($cmdParts[0]);
140 | for ($i = 1, $n = count($cmdParts); $i < $n; $i++) {
141 | if ('..' === $cmdParts[$i]) {
142 | $delIndex = count($cmdRes) - 1;
143 | if ($delIndex < 1) {
144 | throw new InvalidArgumentException(
145 | 'Unable to resolve command',
146 | InvalidArgumentException::CODE_CMD_UNRESOLVABLE
147 | );
148 | }
149 | unset($cmdRes[$delIndex]);
150 | $cmdRes = array_values($cmdRes);
151 | } else {
152 | $cmdRes[] = $cmdParts[$i];
153 | }
154 | }
155 | $command = implode('/', $cmdRes);
156 | }
157 | if (!preg_match('#^/\S+$#sm', $command)) {
158 | throw new InvalidArgumentException(
159 | 'Invalid command supplied.',
160 | InvalidArgumentException::CODE_CMD_INVALID
161 | );
162 | }
163 | $this->_command = $command;
164 | return $this;
165 | }
166 |
167 | /**
168 | * Gets the command that will be send to RouterOS.
169 | *
170 | * Gets the command that will be send to RouterOS in its API syntax.
171 | *
172 | * @return string The command to send.
173 | *
174 | * @see setCommand()
175 | */
176 | public function getCommand()
177 | {
178 | return $this->_command;
179 | }
180 |
181 | /**
182 | * Sets the query to send with the command.
183 | *
184 | * @param Query|null $query The query to be set.
185 | * Setting NULL will remove the currently associated query.
186 | *
187 | * @return $this The request object.
188 | *
189 | * @see getQuery()
190 | */
191 | public function setQuery(Query $query = null)
192 | {
193 | $this->_query = $query;
194 | return $this;
195 | }
196 |
197 | /**
198 | * Gets the currently associated query
199 | *
200 | * @return Query|null The currently associated query.
201 | *
202 | * @see setQuery()
203 | */
204 | public function getQuery()
205 | {
206 | return $this->_query;
207 | }
208 |
209 | /**
210 | * Sets the tag to associate the request with.
211 | *
212 | * Sets the tag to associate the request with. Setting NULL erases the
213 | * currently set tag.
214 | *
215 | * @param string|null $tag The tag to set.
216 | *
217 | * @return $this The request object.
218 | *
219 | * @see getTag()
220 | */
221 | public function setTag($tag)
222 | {
223 | return parent::setTag($tag);
224 | }
225 |
226 | /**
227 | * Sets an argument for the request.
228 | *
229 | * @param string $name Name of the argument.
230 | * @param string|resource|null $value Value of the argument as a string or
231 | * seekable stream.
232 | * Setting the value to NULL removes an argument of this name.
233 | * If a seekable stream is provided, it is sent from its current
234 | * position to its end, and the pointer is seeked back to its current
235 | * position after sending.
236 | * Non seekable streams, as well as all other types, are casted to a
237 | * string.
238 | *
239 | * @return $this The request object.
240 | *
241 | * @see getArgument()
242 | */
243 | public function setArgument($name, $value = '')
244 | {
245 | return parent::setAttribute($name, $value);
246 | }
247 |
248 | /**
249 | * Gets the value of an argument.
250 | *
251 | * @param string $name The name of the argument.
252 | *
253 | * @return string|resource|null The value of the specified argument.
254 | * Returns NULL if such an argument is not set.
255 | *
256 | * @see setAttribute()
257 | */
258 | public function getArgument($name)
259 | {
260 | return parent::getAttribute($name);
261 | }
262 |
263 | /**
264 | * Removes all arguments from the request.
265 | *
266 | * @return $this The request object.
267 | */
268 | public function removeAllArguments()
269 | {
270 | return parent::removeAllAttributes();
271 | }
272 |
273 | /**
274 | * Sends a request over a communicator.
275 | *
276 | * @param Communicator $com The communicator to send the request over.
277 | * @param Registry|null $reg An optional registry to sync the request with.
278 | *
279 | * @return int The number of bytes sent.
280 | *
281 | * @see Client::sendSync()
282 | * @see Client::sendAsync()
283 | */
284 | public function send(Communicator $com, Registry $reg = null)
285 | {
286 | if (null !== $reg
287 | && (null != $this->getTag() || !$reg->isTaglessModeOwner())
288 | ) {
289 | $originalTag = $this->getTag();
290 | $this->setTag($reg->getOwnershipTag() . $originalTag);
291 | $bytes = $this->send($com);
292 | $this->setTag($originalTag);
293 | return $bytes;
294 | }
295 | if ($com->getTransmitter()->isPersistent()) {
296 | $old = $com->getTransmitter()->lock(T\Stream::DIRECTION_SEND);
297 | $bytes = $this->_send($com);
298 | $com->getTransmitter()->lock($old, true);
299 | return $bytes;
300 | }
301 | return $this->_send($com);
302 | }
303 |
304 | /**
305 | * Sends a request over a communicator.
306 | *
307 | * The only difference with the non private equivalent is that this one does
308 | * not do locking.
309 | *
310 | * @param Communicator $com The communicator to send the request over.
311 | *
312 | * @return int The number of bytes sent.
313 | *
314 | * @see Client::sendSync()
315 | * @see Client::sendAsync()
316 | */
317 | private function _send(Communicator $com)
318 | {
319 | if (!$com->getTransmitter()->isAcceptingData()) {
320 | throw new SocketException(
321 | 'Transmitter is invalid. Sending aborted.',
322 | SocketException::CODE_REQUEST_SEND_FAIL
323 | );
324 | }
325 | $bytes = 0;
326 | $bytes += $com->sendWord($this->getCommand());
327 | if (null !== ($tag = $this->getTag())) {
328 | $bytes += $com->sendWord('.tag=' . $tag);
329 | }
330 | foreach ($this->attributes as $name => $value) {
331 | $prefix = '=' . $name . '=';
332 | if (is_string($value)) {
333 | $bytes += $com->sendWord($prefix . $value);
334 | } else {
335 | $bytes += $com->sendWordFromStream($prefix, $value);
336 | }
337 | }
338 | $query = $this->getQuery();
339 | if ($query instanceof Query) {
340 | $bytes += $query->send($com);
341 | }
342 | $bytes += $com->sendWord('');
343 | return $bytes;
344 | }
345 |
346 | /**
347 | * Verifies the request.
348 | *
349 | * Verifies the request against a communicator, i.e. whether the request
350 | * could successfully be sent (assuming the connection is still opened).
351 | *
352 | * @param Communicator $com The Communicator to check against.
353 | *
354 | * @return $this The request object itself.
355 | *
356 | * @throws LengthException If the resulting length of an API word is not
357 | * supported.
358 | */
359 | public function verify(Communicator $com)
360 | {
361 | $com::verifyLengthSupport(strlen($this->getCommand()));
362 | $com::verifyLengthSupport(strlen('.tag=' . (string)$this->getTag()));
363 | foreach ($this->attributes as $name => $value) {
364 | if (is_string($value)) {
365 | $com::verifyLengthSupport(strlen('=' . $name . '=' . $value));
366 | } else {
367 | $com::verifyLengthSupport(
368 | strlen('=' . $name . '=') +
369 | $com::seekableStreamLength($value)
370 | );
371 | }
372 | }
373 | $query = $this->getQuery();
374 | if ($query instanceof Query) {
375 | $query->verify($com);
376 | }
377 | return $this;
378 | }
379 |
380 | /**
381 | * Parses the arguments of a command.
382 | *
383 | * @param string $string The argument string to parse.
384 | *
385 | * @return void
386 | */
387 | protected function parseArgumentString($string)
388 | {
389 | /*
390 | * Grammar:
391 | *
392 | * := (<<\s+>>, )*,
393 | * := , ?
394 | * := <<[^\=\s]+>>
395 | * := "=", ( | )
396 | * := <<">>, <<([^"]|\\"|\\\\)*>>, <<">>
397 | * := <<\S+>>
398 | */
399 |
400 | $token = '';
401 | $name = null;
402 | while ($string = substr($string, strlen($token))) {
403 | if (null === $name) {
404 | if (preg_match('/^\s+([^\s=]+)/sS', $string, $matches)) {
405 | $token = $matches[0];
406 | $name = $matches[1];
407 | } else {
408 | throw new InvalidArgumentException(
409 | "Parsing of argument name failed near '{$string}'",
410 | InvalidArgumentException::CODE_NAME_UNPARSABLE
411 | );
412 | }
413 | } elseif (preg_match('/^\s/s', $string, $matches)) {
414 | //Empty argument
415 | $token = '';
416 | $this->setArgument($name);
417 | $name = null;
418 | } elseif (preg_match(
419 | '/^="(([^\\\"]|\\\"|\\\\)*)"/sS',
420 | $string,
421 | $matches
422 | )) {
423 | $token = $matches[0];
424 | $this->setArgument(
425 | $name,
426 | str_replace(
427 | array('\\"', '\\\\'),
428 | array('"', '\\'),
429 | $matches[1]
430 | )
431 | );
432 | $name = null;
433 | } elseif (preg_match('/^=(\S+)/sS', $string, $matches)) {
434 | $token = $matches[0];
435 | $this->setArgument($name, $matches[1]);
436 | $name = null;
437 | } else {
438 | throw new InvalidArgumentException(
439 | "Parsing of argument value failed near '{$string}'",
440 | InvalidArgumentException::CODE_VALUE_UNPARSABLE
441 | );
442 | }
443 | }
444 |
445 | if (null !== $name && ('' !== ($name = trim($name)))) {
446 | $this->setArgument($name, '');
447 | }
448 |
449 | }
450 | }
451 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/Response.php:
--------------------------------------------------------------------------------
1 |
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 as T;
27 |
28 | /**
29 | * Locks are released upon any exception from anywhere.
30 | */
31 | use Exception as E;
32 |
33 | /**
34 | * Represents a RouterOS response.
35 | *
36 | * @category Net
37 | * @package PEAR2_Net_RouterOS
38 | * @author Vasil Rangelov
39 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
40 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
41 | */
42 | class Response extends Message
43 | {
44 |
45 | /**
46 | * The last response for a request.
47 | */
48 | const TYPE_FINAL = '!done';
49 |
50 | /**
51 | * A response with data.
52 | */
53 | const TYPE_DATA = '!re';
54 |
55 | /**
56 | * A response signifying error.
57 | */
58 | const TYPE_ERROR = '!trap';
59 |
60 | /**
61 | * A response signifying a fatal error, due to which the connection would be
62 | * terminated.
63 | */
64 | const TYPE_FATAL = '!fatal';
65 |
66 | /**
67 | * An array of unrecognized words in network order.
68 | *
69 | * @var string[]
70 | */
71 | protected $unrecognizedWords = array();
72 |
73 | /**
74 | * The response type.
75 | *
76 | * @var string
77 | */
78 | private $_type;
79 |
80 | /**
81 | * Extracts a new response from a communicator.
82 | *
83 | * @param Communicator $com The communicator from which to extract
84 | * the new response.
85 | * @param bool $asStream Whether to populate the argument values
86 | * with streams instead of strings.
87 | * @param int $sTimeout If a response is not immediately
88 | * available, wait this many seconds. If NULL, wait indefinitely.
89 | * @param int|null $usTimeout Microseconds to add to the waiting time.
90 | * @param Registry|null $reg An optional registry to sync the
91 | * response with.
92 | *
93 | * @see getType()
94 | * @see getArgument()
95 | */
96 | public function __construct(
97 | Communicator $com,
98 | $asStream = false,
99 | $sTimeout = 0,
100 | $usTimeout = null,
101 | Registry $reg = null
102 | ) {
103 | if (null === $reg) {
104 | if ($com->getTransmitter()->isPersistent()) {
105 | $old = $com->getTransmitter()
106 | ->lock(T\Stream::DIRECTION_RECEIVE);
107 | try {
108 | $this->_receive($com, $asStream, $sTimeout, $usTimeout);
109 | } catch (E $e) {
110 | $com->getTransmitter()->lock($old, true);
111 | throw $e;
112 | }
113 | $com->getTransmitter()->lock($old, true);
114 | } else {
115 | $this->_receive($com, $asStream, $sTimeout, $usTimeout);
116 | }
117 | } else {
118 | while (null === ($response = $reg->getNextResponse())) {
119 | $newResponse = new self($com, true, $sTimeout, $usTimeout);
120 | $tagInfo = $reg::parseTag($newResponse->getTag());
121 | $newResponse->setTag($tagInfo[1]);
122 | if (!$reg->add($newResponse, $tagInfo[0])) {
123 | $response = $newResponse;
124 | break;
125 | }
126 | }
127 |
128 | $this->_type = $response->_type;
129 | $this->attributes = $response->attributes;
130 | $this->unrecognizedWords = $response->unrecognizedWords;
131 | $this->setTag($response->getTag());
132 |
133 | if (!$asStream) {
134 | foreach ($this->attributes as $name => $value) {
135 | $this->setAttribute(
136 | $name,
137 | stream_get_contents($value)
138 | );
139 | }
140 | foreach ($response->unrecognizedWords as $i => $value) {
141 | $this->unrecognizedWords[$i] = stream_get_contents($value);
142 | }
143 | }
144 | }
145 | }
146 |
147 | /**
148 | * Extracts a new response from a communicator.
149 | *
150 | * This is the function that performs the actual receiving, while the
151 | * constructor is also involved in locks and registry sync.
152 | *
153 | * @param Communicator $com The communicator from which to extract
154 | * the new response.
155 | * @param bool $asStream Whether to populate the argument values
156 | * with streams instead of strings.
157 | * @param int $sTimeout If a response is not immediately
158 | * available, wait this many seconds. If NULL, wait indefinitely.
159 | * Note that if an empty sentence is received, the timeout will be
160 | * reset for another sentence receiving.
161 | * @param int|null $usTimeout Microseconds to add to the waiting time.
162 | *
163 | * @return void
164 | */
165 | private function _receive(
166 | Communicator $com,
167 | $asStream = false,
168 | $sTimeout = 0,
169 | $usTimeout = null
170 | ) {
171 | do {
172 | if (!$com->getTransmitter()->isDataAwaiting(
173 | $sTimeout,
174 | $usTimeout
175 | )) {
176 | throw new SocketException(
177 | 'No data within the time limit',
178 | SocketException::CODE_NO_DATA
179 | );
180 | }
181 | $type = $com->getNextWord();
182 | } while ('' === $type);
183 | $this->setType($type);
184 | if ($asStream) {
185 | for ($word = $com->getNextWordAsStream(), fseek($word, 0, SEEK_END);
186 | ftell($word) !== 0;
187 | $word = $com->getNextWordAsStream(), fseek(
188 | $word,
189 | 0,
190 | SEEK_END
191 | )) {
192 | rewind($word);
193 | $ind = fread($word, 1);
194 | if ('=' === $ind || '.' === $ind) {
195 | $prefix = stream_get_line($word, null, '=');
196 | }
197 | if ('=' === $ind) {
198 | $value = fopen('php://temp', 'r+b');
199 | $bytesCopied = ftell($word);
200 | while (!feof($word)) {
201 | $bytesCopied += stream_copy_to_stream(
202 | $word,
203 | $value,
204 | 0xFFFFF,
205 | $bytesCopied
206 | );
207 | }
208 | rewind($value);
209 | $this->setAttribute($prefix, $value);
210 | continue;
211 | }
212 | if ('.' === $ind && 'tag' === $prefix) {
213 | $this->setTag(stream_get_contents($word, -1, -1));
214 | continue;
215 | }
216 | rewind($word);
217 | $this->unrecognizedWords[] = $word;
218 | }
219 | } else {
220 | for ($word = $com->getNextWord(); '' !== $word;
221 | $word = $com->getNextWord()) {
222 | if (preg_match('/^=([^=]+)=(.*)$/sS', $word, $matches)) {
223 | $this->setAttribute($matches[1], $matches[2]);
224 | } elseif (preg_match('/^\.tag=(.*)$/sS', $word, $matches)) {
225 | $this->setTag($matches[1]);
226 | } else {
227 | $this->unrecognizedWords[] = $word;
228 | }
229 | }
230 | }
231 | }
232 |
233 | /**
234 | * Sets the response type.
235 | *
236 | * Sets the response type. Valid values are the TYPE_* constants.
237 | *
238 | * @param string $type The new response type.
239 | *
240 | * @return $this The response object.
241 | *
242 | * @see getType()
243 | */
244 | protected function setType($type)
245 | {
246 | switch ($type) {
247 | case self::TYPE_FINAL:
248 | case self::TYPE_DATA:
249 | case self::TYPE_ERROR:
250 | case self::TYPE_FATAL:
251 | $this->_type = $type;
252 | return $this;
253 | default:
254 | throw new UnexpectedValueException(
255 | 'Unrecognized response type.',
256 | UnexpectedValueException::CODE_RESPONSE_TYPE_UNKNOWN,
257 | null,
258 | $type
259 | );
260 | }
261 | }
262 |
263 | /**
264 | * Gets the response type.
265 | *
266 | * @return string The response type.
267 | *
268 | * @see setType()
269 | */
270 | public function getType()
271 | {
272 | return $this->_type;
273 | }
274 |
275 | /**
276 | * Gets the value of an argument.
277 | *
278 | * @param string $name The name of the argument.
279 | *
280 | * @return string|resource|null The value of the specified argument.
281 | * Returns NULL if such an argument is not set.
282 | *
283 | * @deprecated 1.0.0b5 Use {@link static::getProperty()} instead.
284 | * This method will be removed upon final release, and is currently
285 | * left standing merely because it can't be easily search&replaced in
286 | * existing code, due to the fact the name "getArgument()" is shared
287 | * with {@link Request::getArgument()}, which is still valid.
288 | * @codeCoverageIgnore
289 | */
290 | public function getArgument($name)
291 | {
292 | trigger_error(
293 | 'Response::getArgument() is deprecated in favor of ' .
294 | 'Response::getProperty() (but note that Request::getArgument() ' .
295 | 'is still valid)',
296 | E_USER_DEPRECATED
297 | );
298 | return $this->getAttribute($name);
299 | }
300 |
301 | /**
302 | * Gets the value of a property.
303 | *
304 | * @param string $name The name of the property.
305 | *
306 | * @return string|resource|null The value of the specified property.
307 | * Returns NULL if such a property is not set.
308 | */
309 | public function getProperty($name)
310 | {
311 | return parent::getAttribute($name);
312 | }
313 |
314 | /**
315 | * Gets a list of unrecognized words.
316 | *
317 | * @return string[] The list of unrecognized words.
318 | */
319 | public function getUnrecognizedWords()
320 | {
321 | return $this->unrecognizedWords;
322 | }
323 | }
324 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/ResponseCollection.php:
--------------------------------------------------------------------------------
1 |
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 | * Implemented by this class.
25 | */
26 | use ArrayAccess;
27 |
28 | /**
29 | * Implemented by this class.
30 | */
31 | use Countable;
32 |
33 | /**
34 | * Implemented by this class.
35 | */
36 | use SeekableIterator;
37 |
38 | /**
39 | * Represents a collection of RouterOS responses.
40 | *
41 | * @category Net
42 | * @package PEAR2_Net_RouterOS
43 | * @author Vasil Rangelov
44 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
45 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
46 | *
47 | * @method string getType()
48 | * Calls {@link Response::getType()}
49 | * on the response pointed by the pointer.
50 | * @method string[] getUnrecognizedWords()
51 | * Calls {@link Response::getUnrecognizedWords()}
52 | * on the response pointed by the pointer.
53 | * @method string|resource|null getProperty(string $name)
54 | * Calls {@link Response::getProperty()}
55 | * on the response pointed by the pointer.
56 | * @method string getTag()
57 | * Calls {@link Response::getTag()}
58 | * on the response pointed by the pointer.
59 | */
60 | class ResponseCollection implements ArrayAccess, SeekableIterator, Countable
61 | {
62 |
63 | /**
64 | * An array with all {@link Response} objects.
65 | *
66 | * An array with all Response objects.
67 | *
68 | * @var Response[]
69 | */
70 | protected $responses = array();
71 |
72 | /**
73 | * An array with each Response object's type.
74 | *
75 | * An array with each {@link Response} object's type.
76 | *
77 | * @var string[]
78 | */
79 | protected $responseTypes = array();
80 |
81 | /**
82 | * An array with each Response object's tag.
83 | *
84 | * An array with each {@link Response} object's tag.
85 | *
86 | * @var string[]
87 | */
88 | protected $responseTags = array();
89 |
90 | /**
91 | * An array with positions of responses, based on an property name.
92 | *
93 | * The name of each property is the array key, and the array value
94 | * is another array where the key is the value for that property, and
95 | * the value is the position of the response. For performance reasons,
96 | * each key is built only when {@link static::setIndex()} is called with
97 | * that property, and remains available for the lifetime of this collection.
98 | *
99 | * @var array>
100 | */
101 | protected $responsesIndex = array();
102 |
103 | /**
104 | * An array with all distinct properties.
105 | *
106 | * An array with all distinct properties across all {@link Response}
107 | * objects. Created at the first call of {@link static::getPropertyMap()}.
108 | *
109 | * @var array
110 | */
111 | protected $propertyMap = null;
112 |
113 | /**
114 | * A pointer, as required by SeekableIterator.
115 | *
116 | * @var int
117 | */
118 | protected $position = 0;
119 |
120 | /**
121 | * Name of property to use as index
122 | *
123 | * NULL when disabled.
124 | *
125 | * @var string|null
126 | */
127 | protected $index = null;
128 |
129 | /**
130 | * Compare criteria.
131 | *
132 | * Used by {@link static::compare()} to determine the order between
133 | * two responses. See {@link static::orderBy()} for a detailed description
134 | * of this array's format.
135 | *
136 | * @var string[]|array>
137 | */
138 | protected $compareBy = array();
139 |
140 | /**
141 | * Creates a new collection.
142 | *
143 | * @param Response[] $responses An array of responses, in network order.
144 | */
145 | public function __construct(array $responses)
146 | {
147 | $pos = 0;
148 | foreach ($responses as $response) {
149 | if ($response instanceof Response) {
150 | $this->responseTypes[$pos] = $response->getType();
151 | $this->responseTags[$pos] = $response->getTag();
152 | $this->responses[$pos++] = $response;
153 | }
154 | }
155 | }
156 |
157 | /**
158 | * A shorthand gateway.
159 | *
160 | * This is a magic PHP method that allows you to call the object as a
161 | * function. Depending on the argument given, one of the other functions in
162 | * the class is invoked and its returned value is returned by this function.
163 | *
164 | * @param int|string|null $offset The offset of the response to seek to.
165 | * If the offset is negative, seek to that relative to the end.
166 | * If the collection is indexed, you can also supply a value to seek to.
167 | * Setting NULL will get the current response's iterator.
168 | *
169 | * @return Response|ArrayObject The {@link Response} at the specified
170 | * offset, the current response's iterator (which is an ArrayObject)
171 | * when NULL is given, or FALSE if the offset is invalid
172 | * or the collection is empty.
173 | */
174 | public function __invoke($offset = null)
175 | {
176 | return null === $offset
177 | ? $this->current()->getIterator()
178 | : $this->seek($offset);
179 | }
180 |
181 | /**
182 | * Sets a property to be usable as a key in the collection.
183 | *
184 | * @param string|null $name The name of the property to use. Future calls
185 | * that accept a position will then also be able to search values of
186 | * that property for a matching value.
187 | * Specifying NULL will disable such lookups (as is by default).
188 | * Note that in case this value occurs multiple times within the
189 | * collection, only the last matching response will be accessible by
190 | * that value.
191 | *
192 | * @return $this The object itself.
193 | */
194 | public function setIndex($name)
195 | {
196 | if (null !== $name) {
197 | $name = (string)$name;
198 | if (!isset($this->responsesIndex[$name])) {
199 | $this->responsesIndex[$name] = array();
200 | foreach ($this->responses as $pos => $response) {
201 | $val = $response->getProperty($name);
202 | if (null !== $val) {
203 | $this->responsesIndex[$name][$val] = $pos;
204 | }
205 | }
206 | }
207 | }
208 | $this->index = $name;
209 | return $this;
210 | }
211 |
212 | /**
213 | * Gets the name of the property used as an index.
214 | *
215 | * @return string|null Name of property used as index. NULL when disabled.
216 | */
217 | public function getIndex()
218 | {
219 | return $this->index;
220 | }
221 |
222 | /**
223 | * Gets the whole collection as an array.
224 | *
225 | * @param bool $useIndex Whether to use the index values as keys for the
226 | * resulting array.
227 | *
228 | * @return Response[] An array with all responses, in network order.
229 | */
230 | public function toArray($useIndex = false)
231 | {
232 | if ($useIndex) {
233 | $positions = $this->responsesIndex[$this->index];
234 | asort($positions, SORT_NUMERIC);
235 | $positions = array_flip($positions);
236 | return array_combine(
237 | $positions,
238 | array_intersect_key($this->responses, $positions)
239 | );
240 | }
241 | return $this->responses;
242 | }
243 |
244 | /**
245 | * Counts the responses in the collection.
246 | *
247 | * @return int The number of responses in the collection.
248 | */
249 | public function count()
250 | {
251 | return count($this->responses);
252 | }
253 |
254 | /**
255 | * Checks if an offset exists.
256 | *
257 | * @param int|string $offset The offset to check. If the
258 | * collection is indexed, you can also supply a value to check.
259 | * Note that negative numeric offsets are NOT accepted.
260 | *
261 | * @return bool TRUE if the offset exists, FALSE otherwise.
262 | */
263 | public function offsetExists($offset)
264 | {
265 | return is_int($offset)
266 | ? array_key_exists($offset, $this->responses)
267 | : array_key_exists($offset, $this->responsesIndex[$this->index]);
268 | }
269 |
270 | /**
271 | * Gets a {@link Response} from a specified offset.
272 | *
273 | * @param int|string $offset The offset of the desired response. If the
274 | * collection is indexed, you can also supply the value to search for.
275 | *
276 | * @return Response The response at the specified offset.
277 | */
278 | public function offsetGet($offset)
279 | {
280 | return is_int($offset)
281 | ? $this->responses[$offset >= 0
282 | ? $offset
283 | : count($this->responses) + $offset]
284 | : $this->responses[$this->responsesIndex[$this->index][$offset]];
285 | }
286 |
287 | /**
288 | * N/A
289 | *
290 | * This method exists only because it is required for ArrayAccess. The
291 | * collection is read only.
292 | *
293 | * @param int|string $offset N/A
294 | * @param Response $value N/A
295 | *
296 | * @return void
297 | *
298 | * @SuppressWarnings(PHPMD.UnusedFormalParameter)
299 | */
300 | public function offsetSet($offset, $value)
301 | {
302 |
303 | }
304 |
305 | /**
306 | * N/A
307 | *
308 | * This method exists only because it is required for ArrayAccess. The
309 | * collection is read only.
310 | *
311 | * @param int|string $offset N/A
312 | *
313 | * @return void
314 | *
315 | * @SuppressWarnings(PHPMD.UnusedFormalParameter)
316 | */
317 | public function offsetUnset($offset)
318 | {
319 |
320 | }
321 |
322 | /**
323 | * Resets the pointer to 0, and returns the first response.
324 | *
325 | * @return Response|false The first response in the collection,
326 | * or FALSE if the collection is empty.
327 | */
328 | public function rewind()
329 | {
330 | return $this->seek(0);
331 | }
332 |
333 | /**
334 | * Moves the position pointer to a specified position.
335 | *
336 | * @param int|string $position The position to move to. If the collection is
337 | * indexed, you can also supply a value to move the pointer to.
338 | * A non-existent index will move the pointer to "-1".
339 | *
340 | * @return Response|false The {@link Response} at the specified position,
341 | * or FALSE if the specified position is not valid.
342 | */
343 | public function seek($position)
344 | {
345 | $this->position = is_int($position)
346 | ? ($position >= 0
347 | ? $position
348 | : count($this->responses) + $position)
349 | : ($this->offsetExists($position)
350 | ? $this->responsesIndex[$this->index][$position]
351 | : -1);
352 | return $this->current();
353 | }
354 |
355 | /**
356 | * Moves the pointer forward by 1, and gets the next response.
357 | *
358 | * @return Response|false The next {@link Response} object,
359 | * or FALSE if the position is not valid.
360 | */
361 | public function next()
362 | {
363 | ++$this->position;
364 | return $this->current();
365 | }
366 |
367 | /**
368 | * Gets the response at the current pointer position.
369 | *
370 | * @return Response|false The response at the current pointer position,
371 | * or FALSE if the position is not valid.
372 | */
373 | public function current()
374 | {
375 | return $this->valid() ? $this->responses[$this->position] : false;
376 | }
377 |
378 | /**
379 | * Moves the pointer backwards by 1, and gets the previous response.
380 | *
381 | * @return Response|false The next {@link Response} object,
382 | * or FALSE if the position is not valid.
383 | */
384 | public function prev()
385 | {
386 | --$this->position;
387 | return $this->current();
388 | }
389 |
390 | /**
391 | * Moves the pointer to the last valid position, and returns the last
392 | * response.
393 | *
394 | * @return Response|false The last response in the collection,
395 | * or FALSE if the collection is empty.
396 | */
397 | public function end()
398 | {
399 | $this->position = count($this->responses) - 1;
400 | return $this->current();
401 | }
402 |
403 | /**
404 | * Gets the key at the current pointer position.
405 | *
406 | * @return int|false The key at the current pointer position,
407 | * i.e. the pointer position itself, or FALSE if the position
408 | * is not valid.
409 | */
410 | public function key()
411 | {
412 | return $this->valid() ? $this->position : false;
413 | }
414 |
415 | /**
416 | * Checks if the pointer is still pointing to an existing offset.
417 | *
418 | * @return bool TRUE if the pointer is valid, FALSE otherwise.
419 | */
420 | public function valid()
421 | {
422 | return $this->offsetExists($this->position);
423 | }
424 |
425 | /**
426 | * Gets all distinct property names.
427 | *
428 | * Gets all distinct property names across all responses.
429 | *
430 | * @return array An array with
431 | * all distinct property names as keys, and
432 | * the indexes at which they occur as values.
433 | */
434 | public function getPropertyMap()
435 | {
436 | if (null === $this->propertyMap) {
437 | $properties = array();
438 | foreach ($this->responses as $index => $response) {
439 | $names = array_keys($response->getIterator()->getArrayCopy());
440 | foreach ($names as $name) {
441 | if (!isset($properties[$name])) {
442 | $properties[$name] = array();
443 | }
444 | $properties[$name][] = $index;
445 | }
446 | }
447 | $this->propertyMap = $properties;
448 | }
449 | return $this->propertyMap;
450 | }
451 |
452 | /**
453 | * Gets all responses of a specified type.
454 | *
455 | * @param string $type The response type to filter by. Valid values are the
456 | * Response::TYPE_* constants.
457 | *
458 | * @return static A new collection with responses of the
459 | * specified type.
460 | */
461 | public function getAllOfType($type)
462 | {
463 | $result = array();
464 | foreach (array_keys($this->responseTypes, $type, true) as $index) {
465 | $result[] = $this->responses[$index];
466 | }
467 | return new static($result);
468 | }
469 |
470 | /**
471 | * Gets all responses with a specified tag.
472 | *
473 | * @param string $tag The tag to filter by.
474 | *
475 | * @return static A new collection with responses having the
476 | * specified tag.
477 | */
478 | public function getAllTagged($tag)
479 | {
480 | $result = array();
481 | foreach (array_keys($this->responseTags, $tag, true) as $index) {
482 | $result[] = $this->responses[$index];
483 | }
484 | return new static($result);
485 | }
486 |
487 | /**
488 | * Order resones by criteria.
489 | *
490 | * @param string[]|array> $criteria The
491 | * criteria to order responses by. It takes the
492 | * form of an array where each key is the name of the property to use
493 | * as (N+1)th sorting key. The value of each member can be either NULL
494 | * (for that property, sort normally in ascending order), a single sort
495 | * order constant (SORT_ASC or SORT_DESC) to sort normally in the
496 | * specified order, an array where the first member is an order
497 | * constant, and the second one is sorting flags (same as built in PHP
498 | * array functions) or a callback.
499 | * If a callback is provided, it must accept two arguments
500 | * (the two values to be compared), and return -1, 0 or 1 if the first
501 | * value is respectively less than, equal to or greater than the second
502 | * one.
503 | * Each key of $criteria can also be numeric, in which case the
504 | * value is the name of the property, and sorting is done normally in
505 | * ascending order.
506 | *
507 | * @return static A new collection with the responses sorted in the
508 | * specified order.
509 | */
510 | public function orderBy(array $criteria)
511 | {
512 | $this->compareBy = $criteria;
513 | $sortedResponses = $this->responses;
514 | usort($sortedResponses, array($this, 'compare'));
515 | return new static($sortedResponses);
516 | }
517 |
518 | /**
519 | * Calls a method of the response pointed by the pointer.
520 | *
521 | * Calls a method of the response pointed by the pointer. This is a magic
522 | * PHP method, thanks to which any function you call on the collection that
523 | * is not defined will be redirected to the response.
524 | *
525 | * @param string $method The name of the method to call.
526 | * @param array $args The arguments to pass to the method.
527 | *
528 | * @return mixed Whatever the called function returns.
529 | */
530 | public function __call($method, array $args)
531 | {
532 | return call_user_func_array(
533 | array($this->current(), $method),
534 | $args
535 | );
536 | }
537 |
538 | /**
539 | * Compares two responses.
540 | *
541 | * Compares two responses, based on criteria defined in
542 | * {@link static::$compareBy}.
543 | *
544 | * @param Response $itemA The response to compare.
545 | * @param Response $itemB The response to compare $a against.
546 | *
547 | * @return int Returns 0 if the two responses are equal according to every
548 | * criteria specified, -1 if $a should be placed before $b, and 1 if $b
549 | * should be placed before $a.
550 | */
551 | protected function compare(Response $itemA, Response $itemB)
552 | {
553 | foreach ($this->compareBy as $name => $spec) {
554 | if (!is_string($name)) {
555 | $name = $spec;
556 | $spec = null;
557 | }
558 |
559 | $members = array(
560 | 0 => $itemA->getProperty($name),
561 | 1 => $itemB->getProperty($name)
562 | );
563 |
564 | if (is_callable($spec)) {
565 | uasort($members, $spec);
566 | } elseif ($members[0] === $members[1]) {
567 | continue;
568 | } else {
569 | $flags = SORT_REGULAR;
570 | $order = SORT_ASC;
571 | if (is_array($spec)) {
572 | list($order, $flags) = $spec;
573 | } elseif (null !== $spec) {
574 | $order = $spec;
575 | }
576 |
577 | if (SORT_ASC === $order) {
578 | asort($members, $flags);
579 | } else {
580 | arsort($members, $flags);
581 | }
582 | }
583 | return (key($members) === 0) ? -1 : 1;
584 | }
585 |
586 | return 0;
587 | }
588 | }
589 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/RouterErrorException.php:
--------------------------------------------------------------------------------
1 |
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 | * Base of this class.
25 | */
26 | use RuntimeException;
27 |
28 | /**
29 | * Refered to in the constructor.
30 | */
31 | use Exception as E;
32 |
33 | /**
34 | * Exception thrown by higher level classes (Util, etc.) when the router
35 | * returns an error.
36 | *
37 | * @category Net
38 | * @package PEAR2_Net_RouterOS
39 | * @author Vasil Rangelov
40 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
41 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
42 | */
43 | class RouterErrorException extends RuntimeException implements Exception
44 | {
45 | const CODE_ITEM_ERROR = 0x100000;
46 | const CODE_SCRIPT_ERROR = 0x200000;
47 | const CODE_READ_ERROR = 0x010000;
48 | const CODE_WRITE_ERROR = 0x020000;
49 | const CODE_EXEC_ERROR = 0x040000;
50 |
51 | const CODE_CACHE_ERROR = 0x100001;
52 | const CODE_GET_ERROR = 0x110001;
53 | const CODE_GETALL_ERROR = 0x110002;
54 | const CODE_ADD_ERROR = 0x120001;
55 | const CODE_SET_ERROR = 0x120002;
56 | const CODE_REMOVE_ERROR = 0x120004;
57 | const CODE_ENABLE_ERROR = 0x120012;
58 | const CODE_DISABLE_ERROR = 0x120022;
59 | const CODE_COMMENT_ERROR = 0x120042;
60 | const CODE_UNSET_ERROR = 0x120082;
61 | const CODE_MOVE_ERROR = 0x120107;
62 | const CODE_SCRIPT_ADD_ERROR = 0x220001;
63 | const CODE_SCRIPT_REMOVE_ERROR = 0x220004;
64 | const CODE_SCRIPT_RUN_ERROR = 0x240001;
65 | const CODE_SCRIPT_FILE_ERROR = 0x240003;
66 |
67 | /**
68 | * The complete response returned by the router.
69 | *
70 | * NULL when the router was not contacted at all.
71 | *
72 | * @var ResponseCollection|null
73 | */
74 | private $_responses = null;
75 |
76 | /**
77 | * Creates a new RouterErrorException.
78 | *
79 | * @param string $message The Exception message to throw.
80 | * @param int $code The Exception code.
81 | * @param E|null $previous The previous exception used for
82 | * the exception chaining.
83 | * @param ResponseCollection|null $responses The complete set responses
84 | * returned by the router.
85 | */
86 | public function __construct(
87 | $message,
88 | $code = 0,
89 | E $previous = null,
90 | ResponseCollection $responses = null
91 | ) {
92 | parent::__construct($message, $code, $previous);
93 | $this->_responses = $responses;
94 | }
95 |
96 | /**
97 | * Gets the complete set responses returned by the router.
98 | *
99 | * @return ResponseCollection|null The complete set responses
100 | * returned by the router.
101 | */
102 | public function getResponses()
103 | {
104 | return $this->_responses;
105 | }
106 |
107 | // @codeCoverageIgnoreStart
108 | // String representation is not reliable in testing
109 |
110 | /**
111 | * Returns a string representation of the exception.
112 | *
113 | * @return string The exception as a string.
114 | */
115 | public function __toString()
116 | {
117 | $result = parent::__toString();
118 | if ($this->_responses instanceof ResponseCollection) {
119 | $result .= "\nResponse collection:\n" .
120 | print_r($this->_responses->toArray(), true);
121 | }
122 | return $result;
123 | }
124 |
125 | // @codeCoverageIgnoreEnd
126 | }
127 |
--------------------------------------------------------------------------------
/src/PEAR2/Net/RouterOS/Script.php:
--------------------------------------------------------------------------------
1 |
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 | * Values at {@link Script::escapeValue()} can be casted from this type.
25 | */
26 | use DateTime;
27 |
28 | /**
29 | * Values at {@link Script::escapeValue()} can be casted from this type.
30 | */
31 | use DateInterval;
32 |
33 | /**
34 | * Used at {@link Script::escapeValue()} to get the proper time.
35 | */
36 | use DateTimeZone;
37 |
38 | /**
39 | * Used to reliably write to streams at {@link Script::prepare()}.
40 | */
41 | use PEAR2\Net\Transmitter\Stream;
42 |
43 | /**
44 | * Used to catch DateTime and DateInterval exceptions at
45 | * {@link Script::parseValue()}.
46 | */
47 | use Exception as E;
48 |
49 | /**
50 | * Scripting class.
51 | *
52 | * Provides functionality related to parsing and composing RouterOS scripts and
53 | * values.
54 | *
55 | * @category Net
56 | * @package PEAR2_Net_RouterOS
57 | * @author Vasil Rangelov
58 | * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
59 | * @link http://pear2.php.net/PEAR2_Net_RouterOS
60 | */
61 | class Script
62 | {
63 | /**
64 | * Parses a value from a RouterOS scripting context.
65 | *
66 | * Turns a value from RouterOS into an equivalent PHP value, based on
67 | * determining the type in the same way RouterOS would determine it for a
68 | * literal.
69 | *
70 | * This method is intended to be the very opposite of
71 | * {@link static::escapeValue()}. That is, results from that method, if
72 | * given to this method, should produce equivalent results.
73 | *
74 | * @param string $value The value to be parsed.
75 | * Must be a literal of a value,
76 | * e.g. what {@link static::escapeValue()} will give you.
77 | * @param DateTimeZone|null $timezone The timezone which any resulting
78 | * DateTime object (either the main value, or values within an array)
79 | * will use. Defaults to UTC.
80 | *
81 | * @return mixed Depending on RouterOS type detected:
82 | * - "nil" (the string "[]") or "nothing" (empty string) - NULL.
83 | * - "num" - int or double for large values.
84 | * - "bool" - a boolean.
85 | * - "array" - an array, with the keys and values processed recursively.
86 | * - "time" - a {@link DateInterval} object.
87 | * - "date" (pseudo type; string in the form "M/j/Y") - a DateTime
88 | * object with the specified date, at midnight.
89 | * - "datetime" (pseudo type; string in the form "M/j/Y H:i:s") - a
90 | * DateTime object with the specified date and time.
91 | * - "str" (a quoted string) - a string, with the contents escaped.
92 | * - Unrecognized type - casted to a string, unmodified.
93 | */
94 | public static function parseValue($value, DateTimeZone $timezone = null)
95 | {
96 | $value = static::parseValueToSimple($value);
97 | if (!is_string($value)) {
98 | return $value;
99 | }
100 |
101 | try {
102 | return static::parseValueToArray($value, $timezone);
103 | } catch (ParserException $e) {
104 | try {
105 | return static::parseValueToDateInterval($value);
106 | } catch (ParserException $e) {
107 | try {
108 | return static::parseValueToDateTime($value, $timezone);
109 | } catch (ParserException $e) {
110 | return static::parseValueToString($value);
111 | }
112 | }
113 | }
114 | }
115 |
116 | /**
117 | * Parses a RouterOS value into a PHP string.
118 | *
119 | * @param string $value The value to be parsed.
120 | * Must be a literal of a value,
121 | * e.g. what {@link static::escapeValue()} will give you.
122 | *
123 | * @return string If a quoted string is provided, it would be parsed.
124 | * Otherwise, the value is casted to a string, and returned unmodified.
125 | */
126 | public static function parseValueToString($value)
127 | {
128 | $value = (string)$value;
129 | if ('"' === $value[0] && '"' === $value[strlen($value) - 1]) {
130 | return str_replace(
131 | array('\"', '\\\\', "\\\n", "\\\r\n", "\\\r"),
132 | array('"', '\\'),
133 | substr($value, 1, -1)
134 | );
135 | }
136 | return $value;
137 | }
138 |
139 | /**
140 | * Parses a RouterOS value into a PHP simple type.
141 | *
142 | * Parses a RouterOS value into a PHP simple type. "Simple" types being
143 | * scalar types, plus NULL.
144 | *
145 | * @param string $value The value to be parsed. Must be a literal of a
146 | * value, e.g. what {@link static::escapeValue()} will give you.
147 | *
148 | * @return string|bool|int|double|null Depending on RouterOS type detected:
149 | * - "nil" (the string "[]") or "nothing" (empty string) - NULL.
150 | * - "num" - int or double for large values.
151 | * - "bool" - a boolean.
152 | * - Unrecognized type - casted to a string, unmodified.
153 | */
154 | public static function parseValueToSimple($value)
155 | {
156 | $value = (string)$value;
157 |
158 | if (in_array($value, array('', '[]'), true)) {
159 | return null;
160 | } elseif (in_array($value, array('true', 'false', 'yes', 'no'), true)) {
161 | return $value === 'true' || $value === 'yes';
162 | } elseif ($value === (string)($num = (int)$value)
163 | || $value === (string)($num = (double)$value)
164 | ) {
165 | return $num;
166 | }
167 | return $value;
168 | }
169 |
170 | /**
171 | * Parses a RouterOS value into a PHP DateTime object
172 | *
173 | * Parses a RouterOS value into a PHP DateTime object.
174 | *
175 | * @param string $value The value to be parsed.
176 | * Must be a literal of a value,
177 | * e.g. what {@link static::escapeValue()} will give you.
178 | * @param DateTimeZone|null $timezone The timezone which the resulting
179 | * DateTime object will use. Defaults to UTC.
180 | *
181 | * @return DateTime Depending on RouterOS type detected:
182 | * - "date" (pseudo type; string in the form "M/j/Y") - a DateTime
183 | * object with the specified date, at midnight UTC time (regardless
184 | * of timezone provided).
185 | * - "datetime" (pseudo type; string in the form "M/j/Y H:i:s") - a
186 | * DateTime object with the specified date and time,
187 | * with the specified timezone.
188 | *
189 | * @throws ParserException When the value is not of a recognized type.
190 | */
191 | public static function parseValueToDateTime(
192 | $value,
193 | DateTimeZone $timezone = null
194 | ) {
195 | $previous = null;
196 | $value = (string)$value;
197 | if ('' !== $value && preg_match(
198 | '#^
199 | (?jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)
200 | /
201 | (?\d\d?)
202 | /
203 | (?\d{4})
204 | (?:
205 | \s+(?