├── .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+(?