PHPMixBill v5.0 - First Upload

This commit is contained in:
Ibnu Maksum
2017-03-11 02:51:06 +07:00
commit 979475b312
767 changed files with 239450 additions and 0 deletions

View File

@ -0,0 +1,819 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Refers to transmitter direction constants.
*/
use PEAR2\Net\Transmitter\Stream as S;
/**
* Refers to the cryptography constants.
*/
use PEAR2\Net\Transmitter\NetworkStream as N;
/**
* Catches arbitrary exceptions at some points.
*/
use Exception as E;
/**
* A RouterOS client.
*
* Provides functionality for easily communicating with a RouterOS host.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
class Client
{
/**
* Used in {@link static::isRequestActive()} to limit search only to
* requests that have a callback.
*/
const FILTER_CALLBACK = 1;
/**
* Used in {@link static::isRequestActive()} to limit search only to
* requests that use the buffer.
*/
const FILTER_BUFFER = 2;
/**
* Used in {@link static::isRequestActive()} to indicate no limit in search.
*/
const FILTER_ALL = 3;
/**
* @var Communicator The communicator for this client.
*/
protected $com;
/**
* @var int The number of currently pending requests.
*/
protected $pendingRequestsCount = 0;
/**
* @var array An array of responses that have not yet been extracted or
* passed to a callback. Key is the tag of the request, and the value
* is an array of associated responses.
*/
protected $responseBuffer = array();
/**
* @var array An array of callbacks to be executed as responses come.
* Key is the tag of the request, and the value is the callback for it.
*/
protected $callbacks = array();
/**
* @var Registry A registry for the operations. Particularly helpful at
* persistent connections.
*/
protected $registry = null;
/**
* @var bool Whether to stream future responses.
*/
private $_streamingResponses = false;
/**
* Creates a new instance of a RouterOS API client.
*
* Creates a new instance of a RouterOS API client with the specified
* settings.
*
* @param string $host Hostname (IP or domain) of the RouterOS server.
* @param string $username The RouterOS username.
* @param string $password The RouterOS password.
* @param int|null $port The port on which the RouterOS server provides
* the API service. You can also specify NULL, in which case the port
* will automatically be chosen between 8728 and 8729, depending on the
* value of $crypto.
* @param bool $persist Whether or not the connection should be a
* persistent one.
* @param float $timeout The timeout for the connection.
* @param string $crypto The encryption for this connection. Must be one
* of the PEAR2\Net\Transmitter\NetworkStream::CRYPTO_* constants. Off
* by default. RouterOS currently supports only TLS, but the setting is
* provided in this fashion for forward compatibility's sake. And for
* the sake of simplicity, if you specify an encryption, don't specify a
* context and your default context uses the value "DEFAULT" for
* ciphers, "ADH" will be automatically added to the list of ciphers.
* @param resource $context A context for the socket.
*
* @see sendSync()
* @see sendAsync()
*/
public function __construct(
$host,
$username,
$password = '',
$port = 8728,
$persist = false,
$timeout = null,
$crypto = N::CRYPTO_OFF,
$context = null
) {
$this->com = new Communicator(
$host,
$port,
$persist,
$timeout,
$username . '/' . $password,
$crypto,
$context
);
$timeout = null == $timeout
? ini_get('default_socket_timeout')
: (int) $timeout;
//Login the user if necessary
if ((!$persist
|| !($old = $this->com->getTransmitter()->lock(S::DIRECTION_ALL)))
&& $this->com->getTransmitter()->isFresh()
) {
if (!static::login($this->com, $username, $password, $timeout)) {
$this->com->close();
throw new DataFlowException(
'Invalid username or password supplied.',
DataFlowException::CODE_INVALID_CREDENTIALS
);
}
}
if (isset($old)) {
$this->com->getTransmitter()->lock($old, true);
}
if ($persist) {
$this->registry = new Registry("{$host}:{$port}/{$username}");
}
}
/**
* A shorthand gateway.
*
* This is a magic PHP method that allows you to call the object as a
* function. Depending on the argument given, one of the other functions in
* the class is invoked and its returned value is returned by this function.
*
* @param mixed $arg Value can be either a {@link Request} to send, which
* would be sent asynchoniously if it has a tag, and synchroniously if
* not, a number to loop with or NULL to complete all pending requests.
* Any other value is converted to string and treated as the tag of a
* request to complete.
*
* @return mixed Whatever the long form function would have returned.
*/
public function __invoke($arg = null)
{
if (is_int($arg) || is_double($arg)) {
return $this->loop($arg);
} elseif ($arg instanceof Request) {
return '' == $arg->getTag() ? $this->sendSync($arg)
: $this->sendAsync($arg);
} elseif (null === $arg) {
return $this->completeRequest();
}
return $this->completeRequest((string) $arg);
}
/**
* Login to a RouterOS connection.
*
* @param Communicator $com The communicator to attempt to login to.
* @param string $username The RouterOS username.
* @param string $password The RouterOS password.
* @param int|null $timeout The time to wait for each response. NULL
* waits indefinetly.
*
* @return bool TRUE on success, FALSE on failure.
*/
public static function login(
Communicator $com,
$username,
$password = '',
$timeout = null
) {
if (null !== ($remoteCharset = $com->getCharset($com::CHARSET_REMOTE))
&& null !== ($localCharset = $com->getCharset($com::CHARSET_LOCAL))
) {
$password = iconv(
$localCharset,
$remoteCharset . '//IGNORE//TRANSLIT',
$password
);
}
$old = null;
try {
if ($com->getTransmitter()->isPersistent()) {
$old = $com->getTransmitter()->lock(S::DIRECTION_ALL);
$result = self::_login($com, $username, $password, $timeout);
$com->getTransmitter()->lock($old, true);
return $result;
}
return self::_login($com, $username, $password, $timeout);
} catch (E $e) {
if ($com->getTransmitter()->isPersistent() && null !== $old) {
$com->getTransmitter()->lock($old, true);
}
throw ($e instanceof NotSupportedException
|| $e instanceof UnexpectedValueException
|| !$com->getTransmitter()->isDataAwaiting()) ? new SocketException(
'This is not a compatible RouterOS service',
SocketException::CODE_SERVICE_INCOMPATIBLE,
$e
) : $e;
}
}
/**
* Login to a RouterOS connection.
*
* This is the actual login procedure, applied regardless of persistence and
* charset settings.
*
* @param Communicator $com The communicator to attempt to login to.
* @param string $username The RouterOS username.
* @param string $password The RouterOS password. Potentially parsed
* already by iconv.
* @param int|null $timeout The time to wait for each response. NULL
* waits indefinetly.
*
* @return bool TRUE on success, FALSE on failure.
*/
private static function _login(
Communicator $com,
$username,
$password = '',
$timeout = null
) {
$request = new Request('/login');
$request->send($com);
$response = new Response($com, false, $timeout);
$request->setArgument('name', $username);
$request->setArgument(
'response',
'00' . md5(
chr(0) . $password
. pack('H*', $response->getProperty('ret'))
)
);
$request->send($com);
$response = new Response($com, false, $timeout);
return $response->getType() === Response::TYPE_FINAL
&& null === $response->getProperty('ret');
}
/**
* Sets the charset(s) for this connection.
*
* Sets the charset(s) for this connection. The specified charset(s) will be
* used for all future requests and responses. When sending,
* {@link Communicator::CHARSET_LOCAL} is converted to
* {@link Communicator::CHARSET_REMOTE}, and when receiving,
* {@link Communicator::CHARSET_REMOTE} is converted to
* {@link Communicator::CHARSET_LOCAL}. Setting NULL to either charset will
* disable charset convertion, and data will be both sent and received "as
* is".
*
* @param mixed $charset The charset to set. If $charsetType is
* {@link Communicator::CHARSET_ALL}, you can supply either a string to
* use for all charsets, or an array with the charset types as keys, and
* the charsets as values.
* @param int $charsetType Which charset to set. Valid values are the
* Communicator::CHARSET_* constants. Any other value is treated as
* {@link Communicator::CHARSET_ALL}.
*
* @return string|array The old charset. If $charsetType is
* {@link Communicator::CHARSET_ALL}, the old values will be returned as
* an array with the types as keys, and charsets as values.
* @see Communicator::setDefaultCharset()
*/
public function setCharset(
$charset,
$charsetType = Communicator::CHARSET_ALL
) {
return $this->com->setCharset($charset, $charsetType);
}
/**
* Gets the charset(s) for this connection.
*
* @param int $charsetType Which charset to get. Valid values are the
* Communicator::CHARSET_* constants. Any other value is treated as
* {@link Communicator::CHARSET_ALL}.
*
* @return string|array The current charset. If $charsetType is
* {@link Communicator::CHARSET_ALL}, the current values will be
* returned as an array with the types as keys, and charsets as values.
* @see setCharset()
*/
public function getCharset($charsetType)
{
return $this->com->getCharset($charsetType);
}
/**
* Sends a request and waits for responses.
*
* @param Request $request The request to send.
* @param callback $callback Optional. A function that is to be executed
* when new responses for this request are available. The callback takes
* two parameters. The {@link Response} object as the first, and the
* {@link Client} object as the second one. If the function returns
* TRUE, the request is canceled. Note that the callback may be executed
* one last time after that with a response that notifies about the
* canceling.
*
* @return $this The client object.
* @see completeRequest()
* @see loop()
* @see cancelRequest()
*/
public function sendAsync(Request $request, $callback = null)
{
//Error checking
$tag = $request->getTag();
if ('' == $tag) {
throw new DataFlowException(
'Asynchonous commands must have a tag.',
DataFlowException::CODE_TAG_REQUIRED
);
}
if ($this->isRequestActive($tag)) {
throw new DataFlowException(
'There must not be multiple active requests sharing a tag.',
DataFlowException::CODE_TAG_UNIQUE
);
}
if (null !== $callback && !is_callable($callback, true)) {
throw new UnexpectedValueException(
'Invalid callback provided.',
UnexpectedValueException::CODE_CALLBACK_INVALID
);
}
$this->send($request);
if (null === $callback) {
//Register the request at the buffer
$this->responseBuffer[$tag] = array();
} else {
//Prepare the callback
$this->callbacks[$tag] = $callback;
}
return $this;
}
/**
* Checks if a request is active.
*
* Checks if a request is active. A request is considered active if it's a
* pending request and/or has responses that are not yet extracted.
*
* @param string $tag The tag of the request to look for.
* @param int $filter One of the FILTER_* consntants. Limits the search
* to the specified places.
*
* @return bool TRUE if the request is active, FALSE otherwise.
* @see getPendingRequestsCount()
* @see completeRequest()
*/
public function isRequestActive($tag, $filter = self::FILTER_ALL)
{
$result = 0;
if ($filter & self::FILTER_CALLBACK) {
$result |= (int) array_key_exists($tag, $this->callbacks);
}
if ($filter & self::FILTER_BUFFER) {
$result |= (int) array_key_exists($tag, $this->responseBuffer);
}
return 0 !== $result;
}
/**
* Sends a request and gets the full response.
*
* @param Request $request The request to send.
*
* @return ResponseCollection The received responses as a collection.
* @see sendAsync()
* @see close()
*/
public function sendSync(Request $request)
{
$tag = $request->getTag();
if ('' == $tag) {
$this->send($request);
} else {
$this->sendAsync($request);
}
return $this->completeRequest($tag);
}
/**
* Completes a specified request.
*
* Starts an event loop for the RouterOS callbacks and finishes when a
* specified request is completed.
*
* @param string $tag The tag of the request to complete. Setting NULL
* completes all requests.
*
* @return ResponseCollection A collection of {@link Response} objects that
* haven't been passed to a callback function or previously extracted
* with {@link static::extractNewResponses()}. Returns an empty
* collection when $tag is set to NULL (responses can still be
* extracted).
*/
public function completeRequest($tag = null)
{
$hasNoTag = '' == $tag;
$result = $hasNoTag ? array()
: $this->extractNewResponses($tag)->toArray();
while ((!$hasNoTag && $this->isRequestActive($tag))
|| ($hasNoTag && 0 !== $this->getPendingRequestsCount())
) {
$newReply = $this->dispatchNextResponse(null);
if ($newReply->getTag() === $tag) {
if ($hasNoTag) {
$result[] = $newReply;
}
if ($newReply->getType() === Response::TYPE_FINAL) {
if (!$hasNoTag) {
$result = array_merge(
$result,
$this->isRequestActive($tag)
? $this->extractNewResponses($tag)->toArray()
: array()
);
}
break;
}
}
}
return new ResponseCollection($result);
}
/**
* Extracts responses for a request.
*
* Gets all new responses for a request that haven't been passed to a
* callback and clears the buffer from them.
*
* @param string $tag The tag of the request to extract new responses for.
* Specifying NULL with extract new responses for all requests.
*
* @return ResponseCollection A collection of {@link Response} objects for
* the specified request.
* @see loop()
*/
public function extractNewResponses($tag = null)
{
if (null === $tag) {
$result = array();
foreach (array_keys($this->responseBuffer) as $tag) {
$result = array_merge(
$result,
$this->extractNewResponses($tag)->toArray()
);
}
return new ResponseCollection($result);
} elseif ($this->isRequestActive($tag, self::FILTER_CALLBACK)) {
return new ResponseCollection(array());
} elseif ($this->isRequestActive($tag, self::FILTER_BUFFER)) {
$result = $this->responseBuffer[$tag];
if (!empty($result)) {
if (end($result)->getType() === Response::TYPE_FINAL) {
unset($this->responseBuffer[$tag]);
} else {
$this->responseBuffer[$tag] = array();
}
}
return new ResponseCollection($result);
} else {
throw new DataFlowException(
'No such request, or the request has already finished.',
DataFlowException::CODE_UNKNOWN_REQUEST
);
}
}
/**
* Starts an event loop for the RouterOS callbacks.
*
* Starts an event loop for the RouterOS callbacks and finishes when there
* are no more pending requests or when a specified timeout has passed
* (whichever comes first).
*
* @param int $sTimeout Timeout for the loop. If NULL, there is no time
* limit.
* @param int $usTimeout Microseconds to add to the time limit.
*
* @return bool TRUE when there are any more pending requests, FALSE
* otherwise.
* @see extractNewResponses()
* @see getPendingRequestsCount()
*/
public function loop($sTimeout = null, $usTimeout = 0)
{
try {
if (null === $sTimeout) {
while ($this->getPendingRequestsCount() !== 0) {
$this->dispatchNextResponse(null);
}
} else {
list($usStart, $sStart) = explode(' ', microtime());
while ($this->getPendingRequestsCount() !== 0
&& ($sTimeout >= 0 || $usTimeout >= 0)
) {
$this->dispatchNextResponse($sTimeout, $usTimeout);
list($usEnd, $sEnd) = explode(' ', microtime());
$sTimeout -= $sEnd - $sStart;
$usTimeout -= $usEnd - $usStart;
if ($usTimeout <= 0) {
if ($sTimeout > 0) {
$usTimeout = 1000000 + $usTimeout;
$sTimeout--;
}
}
$sStart = $sEnd;
$usStart = $usEnd;
}
}
} catch (SocketException $e) {
if ($e->getCode() !== SocketException::CODE_NO_DATA) {
// @codeCoverageIgnoreStart
// It's impossible to reliably cause any other SocketException.
// This line is only here in case the unthinkable happens:
// The connection terminates just after it was supposedly
// about to send back some data.
throw $e;
// @codeCoverageIgnoreEnd
}
}
return $this->getPendingRequestsCount() !== 0;
}
/**
* Gets the number of pending requests.
*
* @return int The number of pending requests.
* @see isRequestActive()
*/
public function getPendingRequestsCount()
{
return $this->pendingRequestsCount;
}
/**
* Cancels a request.
*
* Cancels an active request. Using this function in favor of a plain call
* to the "/cancel" command is highly reccomended, as it also updates the
* counter of pending requests properly. Note that canceling a request also
* removes any responses for it that were not previously extracted with
* {@link static::extractNewResponses()}.
*
* @param string $tag Tag of the request to cancel. Setting NULL will cancel
* all requests.
*
* @return $this The client object.
* @see sendAsync()
* @see close()
*/
public function cancelRequest($tag = null)
{
$cancelRequest = new Request('/cancel');
$hasTag = !('' == $tag);
$hasReg = null !== $this->registry;
if ($hasReg && !$hasTag) {
$tags = array_merge(
array_keys($this->responseBuffer),
array_keys($this->callbacks)
);
$this->registry->setTaglessMode(true);
foreach ($tags as $t) {
$cancelRequest->setArgument(
'tag',
$this->registry->getOwnershipTag() . $t
);
$this->sendSync($cancelRequest);
}
$this->registry->setTaglessMode(false);
} else {
if ($hasTag) {
if ($this->isRequestActive($tag)) {
if ($hasReg) {
$this->registry->setTaglessMode(true);
$cancelRequest->setArgument(
'tag',
$this->registry->getOwnershipTag() . $tag
);
} else {
$cancelRequest->setArgument('tag', $tag);
}
} else {
throw new DataFlowException(
'No such request. Canceling aborted.',
DataFlowException::CODE_CANCEL_FAIL
);
}
}
$this->sendSync($cancelRequest);
if ($hasReg) {
$this->registry->setTaglessMode(false);
}
}
if ($hasTag) {
if ($this->isRequestActive($tag, self::FILTER_BUFFER)) {
$this->responseBuffer[$tag] = $this->completeRequest($tag);
} else {
$this->completeRequest($tag);
}
} else {
$this->loop();
}
return $this;
}
/**
* Sets response streaming setting.
*
* Sets whether future responses are streamed. If responses are streamed,
* the argument values are returned as streams instead of strings. This is
* particularly useful if you expect a response that may contain one or more
* very large words.
*
* @param bool $streamingResponses Whether to stream future responses.
*
* @return bool The previous value of the setting.
* @see isStreamingResponses()
*/
public function setStreamingResponses($streamingResponses)
{
$oldValue = $this->_streamingResponses;
$this->_streamingResponses = (bool) $streamingResponses;
return $oldValue;
}
/**
* Gets response streaming setting.
*
* Gets whether future responses are streamed.
*
* @return bool The value of the setting.
* @see setStreamingResponses()
*/
public function isStreamingResponses()
{
return $this->_streamingResponses;
}
/**
* Closes the opened connection, even if it is a persistent one.
*
* Closes the opened connection, even if it is a persistent one. Note that
* {@link static::extractNewResponses()} can still be used to extract
* responses collected prior to the closing.
*
* @return bool TRUE on success, FALSE on failure.
*/
public function close()
{
$result = true;
/*
* The check below is done because for some unknown reason
* (either a PHP or a RouterOS bug) calling "/quit" on an encrypted
* connection makes one end hang.
*
* Since encrypted connections only appeared in RouterOS 6.1, and
* the "/quit" call is needed for all <6.0 versions, problems due
* to its absence should be limited to some earlier 6.* versions
* on some RouterBOARD devices.
*/
if ($this->com->getTransmitter()->getCrypto() === N::CRYPTO_OFF) {
if (null !== $this->registry) {
$this->registry->setTaglessMode(true);
}
try {
$response = $this->sendSync(new Request('/quit'));
$result = $response[0]->getType() === Response::TYPE_FATAL;
} catch (SocketException $e) {
$result
= $e->getCode() === SocketException::CODE_REQUEST_SEND_FAIL;
} catch (E $e) {
//Ignore unknown errors.
}
if (null !== $this->registry) {
$this->registry->setTaglessMode(false);
}
}
$result = $result && $this->com->close();
$this->callbacks = array();
$this->pendingRequestsCount = 0;
return $result;
}
/**
* Closes the connection, unless it's a persistent one.
*/
public function __destruct()
{
if ($this->com->getTransmitter()->isPersistent()) {
if (0 !== $this->pendingRequestsCount) {
$this->cancelRequest();
}
} else {
$this->close();
}
}
/**
* Sends a request to RouterOS.
*
* @param Request $request The request to send.
*
* @return $this The client object.
* @see sendSync()
* @see sendAsync()
*/
protected function send(Request $request)
{
$request->send($this->com, $this->registry);
$this->pendingRequestsCount++;
return $this;
}
/**
* Dispatches the next response in queue.
*
* Dispatches the next response in queue, i.e. it executes the associated
* callback if there is one, or places the response in the response buffer.
*
* @param int $sTimeout If a response is not immediatly available, wait
* this many seconds. If NULL, wait indefinetly.
* @param int $usTimeout Microseconds to add to the waiting time.
*
* @throws SocketException When there's no response within the time limit.
* @return Response The dispatched response.
*/
protected function dispatchNextResponse($sTimeout = 0, $usTimeout = 0)
{
$response = new Response(
$this->com,
$this->_streamingResponses,
$sTimeout,
$usTimeout,
$this->registry
);
if ($response->getType() === Response::TYPE_FATAL) {
$this->pendingRequestsCount = 0;
$this->com->close();
return $response;
}
$tag = $response->getTag();
$isLastForRequest = $response->getType() === Response::TYPE_FINAL;
if ($isLastForRequest) {
$this->pendingRequestsCount--;
}
if ('' != $tag) {
if ($this->isRequestActive($tag, self::FILTER_CALLBACK)) {
if ($this->callbacks[$tag]($response, $this)) {
$this->cancelRequest($tag);
} elseif ($isLastForRequest) {
unset($this->callbacks[$tag]);
}
} else {
$this->responseBuffer[$tag][] = $response;
}
}
return $response;
}
}

View File

@ -0,0 +1,671 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Using transmitters.
*/
use PEAR2\Net\Transmitter as T;
/**
* A RouterOS communicator.
*
* Implementation of the RouterOS API protocol. Unlike the other classes in this
* package, this class doesn't provide any conviniences beyond the low level
* implementation details (automatic word length encoding/decoding, charset
* translation and data integrity), and because of that, its direct usage is
* strongly discouraged.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
* @see Client
*/
class Communicator
{
/**
* Used when getting/setting all (default) charsets.
*/
const CHARSET_ALL = -1;
/**
* Used when getting/setting the (default) remote charset.
*
* The remote charset is the charset in which RouterOS stores its data.
* If you want to keep compatibility with your Winbox, this charset should
* match the default charset from your Windows' regional settings.
*/
const CHARSET_REMOTE = 0;
/**
* Used when getting/setting the (default) local charset.
*
* The local charset is the charset in which the data from RouterOS will be
* returned as. This charset should match the charset of the place the data
* will eventually be written to.
*/
const CHARSET_LOCAL = 1;
/**
* @var array An array with the default charset types as keys, and the
* default charsets as values.
*/
protected static $defaultCharsets = array(
self::CHARSET_REMOTE => null,
self::CHARSET_LOCAL => null
);
/**
* @var array An array with the current charset types as keys, and the
* current charsets as values.
*/
protected $charsets = array();
/**
* @var T\TcpClient The transmitter for the connection.
*/
protected $trans;
/**
* Creates a new connection with the specified options.
*
* @param string $host Hostname (IP or domain) of the RouterOS server.
* @param int|null $port The port on which the RouterOS server provides
* the API service. You can also specify NULL, in which case the port
* will automatically be chosen between 8728 and 8729, depending on the
* value of $crypto.
* @param bool $persist Whether or not the connection should be a
* persistent one.
* @param float $timeout The timeout for the connection.
* @param string $key A string that uniquely identifies the
* connection.
* @param string $crypto The encryption for this connection. Must be one
* of the PEAR2\Net\Transmitter\NetworkStream::CRYPTO_* constants. Off
* by default. RouterOS currently supports only TLS, but the setting is
* provided in this fashion for forward compatibility's sake. And for
* the sake of simplicity, if you specify an encryption, don't specify a
* context and your default context uses the value "DEFAULT" for
* ciphers, "ADH" will be automatically added to the list of ciphers.
* @param resource $context A context for the socket.
*
* @see sendWord()
*/
public function __construct(
$host,
$port = 8728,
$persist = false,
$timeout = null,
$key = '',
$crypto = T\NetworkStream::CRYPTO_OFF,
$context = null
) {
$isUnencrypted = T\NetworkStream::CRYPTO_OFF === $crypto;
if (($context === null) && !$isUnencrypted) {
$context = stream_context_get_default();
$opts = stream_context_get_options($context);
if (!isset($opts['ssl']['ciphers'])
|| 'DEFAULT' === $opts['ssl']['ciphers']
) {
stream_context_set_option($context, 'ssl', 'ciphers', 'ADH');
}
}
// @codeCoverageIgnoreStart
// The $port is customizable in testing.
if (null === $port) {
$port = $isUnencrypted ? 8728 : 8729;
}
// @codeCoverageIgnoreEnd
try {
$this->trans = new T\TcpClient(
$host,
$port,
$persist,
$timeout,
$key,
$crypto,
$context
);
} catch (T\Exception $e) {
throw new SocketException(
'Error connecting to RouterOS',
SocketException::CODE_CONNECTION_FAIL,
$e
);
}
$this->setCharset(
self::getDefaultCharset(self::CHARSET_ALL),
self::CHARSET_ALL
);
}
/**
* A shorthand gateway.
*
* This is a magic PHP method that allows you to call the object as a
* function. Depending on the argument given, one of the other functions in
* the class is invoked and its returned value is returned by this function.
*
* @param string $string A string of the word to send, or NULL to get the
* next word as a string.
*
* @return int|string If a string is provided, returns the number of bytes
* sent, otherwise retuns the next word as a string.
*/
public function __invoke($string = null)
{
return null === $string ? $this->getNextWord()
: $this->sendWord($string);
}
/**
* Checks whether a variable is a seekable stream resource.
*
* @param mixed $var The value to check.
*
* @return bool TRUE if $var is a seekable stream, FALSE otherwise.
*/
public static function isSeekableStream($var)
{
if (T\Stream::isStream($var)) {
$meta = stream_get_meta_data($var);
return $meta['seekable'];
}
return false;
}
/**
* Uses iconv to convert a stream from one charset to another.
*
* @param string $inCharset The charset of the stream.
* @param string $outCharset The desired resulting charset.
* @param resource $stream The stream to convert. The stream is assumed
* to be seekable, and is read from its current position to its end,
* after which, it is seeked back to its starting position.
*
* @return resource A new stream that uses the $out_charset. The stream is a
* subset from the original stream, from its current position to its
* end, seeked at its start.
*/
public static function iconvStream($inCharset, $outCharset, $stream)
{
$bytes = 0;
$result = fopen('php://temp', 'r+b');
$iconvFilter = stream_filter_append(
$result,
'convert.iconv.' . $inCharset . '.' . $outCharset,
STREAM_FILTER_WRITE
);
flock($stream, LOCK_SH);
while (!feof($stream)) {
$bytes += stream_copy_to_stream($stream, $result, 0xFFFFF);
}
fseek($stream, -$bytes, SEEK_CUR);
flock($stream, LOCK_UN);
stream_filter_remove($iconvFilter);
rewind($result);
return $result;
}
/**
* Sets the default charset(s) for new connections.
*
* @param mixed $charset The charset to set. If $charsetType is
* {@link self::CHARSET_ALL}, you can supply either a string to use for
* all charsets, or an array with the charset types as keys, and the
* charsets as values.
* @param int $charsetType Which charset to set. Valid values are the
* CHARSET_* constants. Any other value is treated as
* {@link self::CHARSET_ALL}.
*
* @return string|array The old charset. If $charsetType is
* {@link self::CHARSET_ALL}, the old values will be returned as an
* array with the types as keys, and charsets as values.
* @see setCharset()
*/
public static function setDefaultCharset(
$charset,
$charsetType = self::CHARSET_ALL
) {
if (array_key_exists($charsetType, self::$defaultCharsets)) {
$oldCharset = self::$defaultCharsets[$charsetType];
self::$defaultCharsets[$charsetType] = $charset;
return $oldCharset;
} else {
$oldCharsets = self::$defaultCharsets;
self::$defaultCharsets = is_array($charset) ? $charset : array_fill(
0,
count(self::$defaultCharsets),
$charset
);
return $oldCharsets;
}
}
/**
* Gets the default charset(s).
*
* @param int $charsetType Which charset to get. Valid values are the
* CHARSET_* constants. Any other value is treated as
* {@link self::CHARSET_ALL}.
*
* @return string|array The current charset. If $charsetType is
* {@link self::CHARSET_ALL}, the current values will be returned as an
* array with the types as keys, and charsets as values.
* @see setDefaultCharset()
*/
public static function getDefaultCharset($charsetType)
{
return array_key_exists($charsetType, self::$defaultCharsets)
? self::$defaultCharsets[$charsetType] : self::$defaultCharsets;
}
/**
* Gets the length of a seekable stream.
*
* Gets the length of a seekable stream.
*
* @param resource $stream The stream to check. The stream is assumed to be
* seekable.
*
* @return double The number of bytes in the stream between its current
* position and its end.
*/
public static function seekableStreamLength($stream)
{
$streamPosition = (double) sprintf('%u', ftell($stream));
fseek($stream, 0, SEEK_END);
$streamLength = ((double) sprintf('%u', ftell($stream)))
- $streamPosition;
fseek($stream, $streamPosition, SEEK_SET);
return $streamLength;
}
/**
* Sets the charset(s) for this connection.
*
* Sets the charset(s) for this connection. The specified charset(s) will be
* used for all future words. When sending, {@link self::CHARSET_LOCAL} is
* converted to {@link self::CHARSET_REMOTE}, and when receiving,
* {@link self::CHARSET_REMOTE} is converted to {@link self::CHARSET_LOCAL}.
* Setting NULL to either charset will disable charset convertion, and data
* will be both sent and received "as is".
*
* @param mixed $charset The charset to set. If $charsetType is
* {@link self::CHARSET_ALL}, you can supply either a string to use for
* all charsets, or an array with the charset types as keys, and the
* charsets as values.
* @param int $charsetType Which charset to set. Valid values are the
* CHARSET_* constants. Any other value is treated as
* {@link self::CHARSET_ALL}.
*
* @return string|array The old charset. If $charsetType is
* {@link self::CHARSET_ALL}, the old values will be returned as an
* array with the types as keys, and charsets as values.
* @see setDefaultCharset()
*/
public function setCharset($charset, $charsetType = self::CHARSET_ALL)
{
if (array_key_exists($charsetType, $this->charsets)) {
$oldCharset = $this->charsets[$charsetType];
$this->charsets[$charsetType] = $charset;
return $oldCharset;
} else {
$oldCharsets = $this->charsets;
$this->charsets = is_array($charset) ? $charset : array_fill(
0,
count($this->charsets),
$charset
);
return $oldCharsets;
}
}
/**
* Gets the charset(s) for this connection.
*
* @param int $charsetType Which charset to get. Valid values are the
* CHARSET_* constants. Any other value is treated as
* {@link self::CHARSET_ALL}.
*
* @return string|array The current charset. If $charsetType is
* {@link self::CHARSET_ALL}, the current values will be returned as an
* array with the types as keys, and charsets as values.
* @see getDefaultCharset()
* @see setCharset()
*/
public function getCharset($charsetType)
{
return array_key_exists($charsetType, $this->charsets)
? $this->charsets[$charsetType] : $this->charsets;
}
/**
* Gets the transmitter for this connection.
*
* @return T\TcpClient The transmitter for this connection.
*/
public function getTransmitter()
{
return $this->trans;
}
/**
* Sends a word.
*
* Sends a word and automatically encodes its length when doing so.
*
* @param string $word The word to send.
*
* @return int The number of bytes sent.
* @see sendWordFromStream()
* @see getNextWord()
*/
public function sendWord($word)
{
if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
&& null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
) {
$word = iconv(
$localCharset,
$remoteCharset . '//IGNORE//TRANSLIT',
$word
);
}
$length = strlen($word);
static::verifyLengthSupport($length);
if ($this->trans->isPersistent()) {
$old = $this->trans->lock(T\Stream::DIRECTION_SEND);
$bytes = $this->trans->send(self::encodeLength($length) . $word);
$this->trans->lock($old, true);
return $bytes;
}
return $this->trans->send(self::encodeLength($length) . $word);
}
/**
* Sends a word based on a stream.
*
* Sends a word based on a stream and automatically encodes its length when
* doing so. The stream is read from its current position to its end, and
* then returned to its current position. Because of those operations, the
* supplied stream must be seekable.
*
* @param string $prefix A string to prepend before the stream contents.
* @param resource $stream The seekable stream to send.
*
* @return int The number of bytes sent.
* @see sendWord()
*/
public function sendWordFromStream($prefix, $stream)
{
if (!self::isSeekableStream($stream)) {
throw new InvalidArgumentException(
'The stream must be seekable.',
InvalidArgumentException::CODE_SEEKABLE_REQUIRED
);
}
if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
&& null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
) {
$prefix = iconv(
$localCharset,
$remoteCharset . '//IGNORE//TRANSLIT',
$prefix
);
$stream = self::iconvStream(
$localCharset,
$remoteCharset . '//IGNORE//TRANSLIT',
$stream
);
}
flock($stream, LOCK_SH);
$totalLength = strlen($prefix) + self::seekableStreamLength($stream);
static::verifyLengthSupport($totalLength);
$bytes = $this->trans->send(self::encodeLength($totalLength) . $prefix);
$bytes += $this->trans->send($stream);
flock($stream, LOCK_UN);
return $bytes;
}
/**
* Verifies that the length is supported.
*
* Verifies if the specified length is supported by the API. Throws a
* {@link LengthException} if that's not the case. Currently, RouterOS
* supports words up to 0xFFFFFFFF in length, so that's the only check
* performed.
*
* @param int $length The length to verify.
*
* @return void
*/
protected static function verifyLengthSupport($length)
{
if ($length > 0xFFFFFFFF) {
throw new LengthException(
'Words with length above 0xFFFFFFFF are not supported.',
LengthException::CODE_UNSUPPORTED,
null,
$length
);
}
}
/**
* Encodes the length as requred by the RouterOS API.
*
* @param int $length The length to encode.
*
* @return string The encoded length.
*/
public static function encodeLength($length)
{
if ($length < 0) {
throw new LengthException(
'Length must not be negative.',
LengthException::CODE_INVALID,
null,
$length
);
} elseif ($length < 0x80) {
return chr($length);
} elseif ($length < 0x4000) {
return pack('n', $length |= 0x8000);
} elseif ($length < 0x200000) {
$length |= 0xC00000;
return pack('n', $length >> 8) . chr($length & 0xFF);
} elseif ($length < 0x10000000) {
return pack('N', $length |= 0xE0000000);
} elseif ($length <= 0xFFFFFFFF) {
return chr(0xF0) . pack('N', $length);
} elseif ($length <= 0x7FFFFFFFF) {
$length = 'f' . base_convert($length, 10, 16);
return chr(hexdec(substr($length, 0, 2))) .
pack('N', hexdec(substr($length, 2)));
}
throw new LengthException(
'Length must not be above 0x7FFFFFFFF.',
LengthException::CODE_BEYOND_SHEME,
null,
$length
);
}
/**
* Get the next word in queue as a string.
*
* Get the next word in queue as a string, after automatically decoding its
* length.
*
* @return string The word.
* @see close()
*/
public function getNextWord()
{
if ($this->trans->isPersistent()) {
$old = $this->trans->lock(T\Stream::DIRECTION_RECEIVE);
$word = $this->trans->receive(
self::decodeLength($this->trans),
'word'
);
$this->trans->lock($old, true);
} else {
$word = $this->trans->receive(
self::decodeLength($this->trans),
'word'
);
}
if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
&& null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
) {
$word = iconv(
$remoteCharset,
$localCharset . '//IGNORE//TRANSLIT',
$word
);
}
return $word;
}
/**
* Get the next word in queue as a stream.
*
* Get the next word in queue as a stream, after automatically decoding its
* length.
*
* @return resource The word, as a stream.
* @see close()
*/
public function getNextWordAsStream()
{
$filters = new T\FilterCollection();
if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
&& null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
) {
$filters->append(
'convert.iconv.' .
$remoteCharset . '.' . $localCharset . '//IGNORE//TRANSLIT'
);
}
if ($this->trans->isPersistent()) {
$old = $this->trans->lock(T\Stream::DIRECTION_RECEIVE);
$stream = $this->trans->receiveStream(
self::decodeLength($this->trans),
$filters,
'stream word'
);
$this->trans->lock($old, true);
} else {
$stream = $this->trans->receiveStream(
self::decodeLength($this->trans),
$filters,
'stream word'
);
}
return $stream;
}
/**
* Decodes the lenght of the incoming message.
*
* Decodes the lenght of the incoming message, as specified by the RouterOS
* API.
*
* @param T\Stream $trans The transmitter from which to decode the length of
* the incoming message.
*
* @return int The decoded length.
*/
public static function decodeLength(T\Stream $trans)
{
if ($trans->isPersistent() && $trans instanceof T\TcpClient) {
$old = $trans->lock($trans::DIRECTION_RECEIVE);
$length = self::_decodeLength($trans);
$trans->lock($old, true);
return $length;
}
return self::_decodeLength($trans);
}
/**
* Decodes the lenght of the incoming message.
*
* Decodes the lenght of the incoming message, as specified by the RouterOS
* API.
*
* Difference with the non private function is that this one doesn't perform
* locking if the connection is a persistent one.
*
* @param T\Stream $trans The transmitter from which to decode the length of
* the incoming message.
*
* @return int The decoded length.
*/
private static function _decodeLength(T\Stream $trans)
{
$byte = ord($trans->receive(1, 'initial length byte'));
if ($byte & 0x80) {
if (($byte & 0xC0) === 0x80) {
return (($byte & 077) << 8 ) + ord($trans->receive(1));
} elseif (($byte & 0xE0) === 0xC0) {
$rem = unpack('n~', $trans->receive(2));
return (($byte & 037) << 16 ) + $rem['~'];
} elseif (($byte & 0xF0) === 0xE0) {
$rem = unpack('n~/C~~', $trans->receive(3));
return (($byte & 017) << 24 ) + ($rem['~'] << 8) + $rem['~~'];
} elseif (($byte & 0xF8) === 0xF0) {
$rem = unpack('N~', $trans->receive(4));
return (($byte & 007) * 0x100000000/* '<< 32' or '2^32' */)
+ (double) sprintf('%u', $rem['~']);
}
throw new NotSupportedException(
'Unknown control byte encountered.',
NotSupportedException::CODE_CONTROL_BYTE,
null,
$byte
);
} else {
return $byte;
}
}
/**
* Closes the opened connection, even if it is a persistent one.
*
* @return bool TRUE on success, FALSE on failure.
*/
public function close()
{
return $this->trans->close();
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Base of this class.
*/
use RuntimeException;
/**
* Exception thrown when the request/response cycle goes an unexpected way.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
class DataFlowException extends RuntimeException implements Exception
{
const CODE_INVALID_CREDENTIALS = 10000;
const CODE_TAG_REQUIRED = 10500;
const CODE_TAG_UNIQUE = 10501;
const CODE_UNKNOWN_REQUEST = 10900;
const CODE_CANCEL_FAIL = 11200;
}

View File

@ -0,0 +1,34 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Generic exception class of this package.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
interface Exception
{
}

View File

@ -0,0 +1,43 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
use InvalidArgumentException as I;
/**
* Exception thrown when there's something wrong with message arguments.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
class InvalidArgumentException extends I implements Exception
{
const CODE_SEEKABLE_REQUIRED = 1100;
const CODE_NAME_INVALID = 20100;
const CODE_ABSOLUTE_REQUIRED = 40200;
const CODE_CMD_UNRESOLVABLE = 40201;
const CODE_CMD_INVALID = 40202;
const CODE_NAME_UNPARSABLE = 41000;
const CODE_VALUE_UNPARSABLE = 41001;
}

View File

@ -0,0 +1,93 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Base of this class.
*/
use LengthException as L;
/**
* Exception thrown when there is a problem with a word's length.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
class LengthException extends L implements Exception
{
const CODE_UNSUPPORTED = 1200;
const CODE_INVALID = 1300;
const CODE_BEYOND_SHEME = 1301;
/**
*
* @var mixed The problematic length.
*/
private $_length;
/**
* Creates a new LengthException.
*
* @param string $message The Exception message to throw.
* @param int $code The Exception code.
* @param \Exception $previous The previous exception used for the exception
* chaining.
* @param number $length The length.
*/
public function __construct(
$message,
$code = 0,
$previous = null,
$length = null
) {
parent::__construct($message, $code, $previous);
$this->_length = $length;
}
/**
* Gets the length.
*
* @return number The length.
*/
public function getLength()
{
return $this->_length;
}
// @codeCoverageIgnoreStart
// String representation is not reliable in testing
/**
* Returns a string representation of the exception.
*
* @return string The exception as a string.
*/
public function __toString()
{
return parent::__toString() . "\nLength:{$this->_length}";
}
// @codeCoverageIgnoreEnd
}

View File

@ -0,0 +1,237 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Implements this interface.
*/
use Countable;
/**
* Implements this interface.
*/
use IteratorAggregate;
/**
* Requred for IteratorAggregate::getIterator() to work properly with foreach.
*/
use ArrayObject;
/**
* Represents a RouterOS message.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
abstract class Message implements IteratorAggregate, Countable
{
/**
* @var array An array with message attributes. Each array key is the the
* name of an attribute, and the correspding array value is the value
* for that attribute.
*/
protected $attributes = array();
/**
* @var string An optional tag to associate the message with.
*/
private $_tag = null;
/**
* A shorthand gateway.
*
* This is a magic PHP method that allows you to call the object as a
* function. Depending on the argument given, one of the other functions in
* the class is invoked and its returned value is returned by this function.
*
* @param string $name The name of an attribute to get the value of, or NULL
* to get the tag.
*
* @return string|resource The value of the specified attribute,
* or the tag if NULL is provided.
*/
public function __invoke($name = null)
{
if (null === $name) {
return $this->getTag();
}
return $this->getAttribute($name);
}
/**
* Sanitizes a name of an attribute (message or query one).
*
* @param mixed $name The name to sanitize.
*
* @return string The sanitized name.
*/
public static function sanitizeAttributeName($name)
{
$name = (string) $name;
if ((empty($name) && $name !== '0')
|| preg_match('/[=\s]/s', $name)
) {
throw new InvalidArgumentException(
'Invalid name of argument supplied.',
InvalidArgumentException::CODE_NAME_INVALID
);
}
return $name;
}
/**
* Sanitizes a value of an attribute (message or query one).
*
* @param mixed $value The value to sanitize.
*
* @return string The sanitized value.
*/
public static function sanitizeAttributeValue($value)
{
if (Communicator::isSeekableStream($value)) {
return $value;
} else {
return (string) $value;
}
}
/**
* Gets the tag that the message is associated with.
*
* @return string The current tag or NULL if there isn't a tag.
* @see setTag()
*/
public function getTag()
{
return $this->_tag;
}
/**
* Sets the tag to associate the request with.
*
* Sets the tag to associate the message with. Setting NULL erases the
* currently set tag.
*
* @param string $tag The tag to set.
*
* @return $this The message object.
* @see getTag()
*/
protected function setTag($tag)
{
$this->_tag = (null === $tag) ? null : (string) $tag;
return $this;
}
/**
* Gets the value of an attribute.
*
* @param string $name The name of the attribute.
*
* @return string|resource|null The value of the specified attribute.
* Returns NULL if such an attribute is not set.
* @see setAttribute()
*/
protected function getAttribute($name)
{
$name = self::sanitizeAttributeName($name);
if (array_key_exists($name, $this->attributes)) {
return $this->attributes[$name];
}
return null;
}
/**
* Gets all arguments in an array.
*
* @return ArrayObject An ArrayObject with the keys being argument names,
* and the array values being argument values.
* @see getArgument()
* @see setArgument()
*/
public function getIterator()
{
return new ArrayObject($this->attributes);
}
/**
* Counts the number of arguments.
*
* @param int $mode The counter mode.
* Either COUNT_NORMAL or COUNT_RECURSIVE.
* When in normal mode, counts the number of arguments.
* When in recursive mode, counts the number of API words
* (including the empty word at the end).
*
* @return int The number of arguments/words.
*/
public function count($mode = COUNT_NORMAL)
{
$result = count($this->attributes);
if ($mode !== COUNT_NORMAL) {
$result += 2/*first+last word*/
+ (int)(null !== $this->getTag());
}
return $result;
}
/**
* Sets an attribute for the message.
*
* @param string $name Name of the attribute.
* @param string|resource|null $value Value of the attribute as a string or
* seekable stream.
* Setting the value to NULL removes an argument of this name.
* If a seekable stream is provided, it is sent from its current
* posistion to its end, and the pointer is seeked back to its current
* position after sending.
* Non seekable streams, as well as all other types, are casted to a
* string.
*
* @return $this The message object.
* @see getArgument()
*/
protected function setAttribute($name, $value = '')
{
if (null === $value) {
unset($this->attributes[self::sanitizeAttributeName($name)]);
} else {
$this->attributes[self::sanitizeAttributeName($name)]
= self::sanitizeAttributeValue($value);
}
return $this;
}
/**
* Removes all attributes from the message.
*
* @return $this The message object.
*/
protected function removeAllAttributes()
{
$this->attributes = array();
return $this;
}
}

View File

@ -0,0 +1,91 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Base of this class.
*/
use Exception as E;
/**
* Exception thrown when encountering something not supported by RouterOS or
* this package.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
class NotSupportedException extends E implements Exception
{
const CODE_CONTROL_BYTE = 1601;
/**
* @var mixed The unsuppported value.
*/
private $_value;
/**
* Creates a new NotSupportedException.
*
* @param string $message The Exception message to throw.
* @param int $code The Exception code.
* @param \Exception $previous The previous exception used for the exception
* chaining.
* @param mixed $value The unsupported value.
*/
public function __construct(
$message,
$code = 0,
$previous = null,
$value = null
) {
parent::__construct($message, $code, $previous);
$this->_value = $value;
}
/**
* Gets the unsupported value.
*
* @return mixed The unsupported value.
*/
public function getValue()
{
return $this->_value;
}
// @codeCoverageIgnoreStart
// String representation is not reliable in testing
/**
* Returns a string representation of the exception.
*
* @return string The exception as a string.
*/
public function __toString()
{
return parent::__toString() . "\nValue:{$this->_value}";
}
// @codeCoverageIgnoreEnd
}

View File

@ -0,0 +1,267 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Refers to transmitter direction constants.
*/
use PEAR2\Net\Transmitter as T;
/**
* Represents a query for RouterOS requests.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
class Query
{
/**
* Checks if the property exists.
*/
const OP_EX = '';
/**
* Checks if the property does not exist.
*/
const OP_NEX = '-';
/**
* Checks if the property equals a certain value.
*/
const OP_EQ = '=';
/**
* Checks if the property is less than a certain value.
*/
const OP_LT = '<';
/**
* Checks if the property is greather than a certain value.
*/
const OP_GT = '>';
/**
* @var array An array of the words forming the query. Each value is an
* array with the first member being the predicate (operator and name),
* and the second member being the value for the predicate.
*/
protected $words = array();
/**
* This class is not to be instantiated normally, but by static methods
* instead. Use {@link static::where()} to create an instance of it.
*/
private function __construct()
{
}
/**
* Sanitizes the operator of a condition.
*
* @param string $operator The operator to sanitize.
*
* @return string The sanitized operator.
*/
protected static function sanitizeOperator($operator)
{
$operator = (string) $operator;
switch ($operator) {
case Query::OP_EX:
case Query::OP_NEX:
case Query::OP_EQ:
case Query::OP_LT:
case Query::OP_GT:
return $operator;
default:
throw new UnexpectedValueException(
'Unknown operator specified',
UnexpectedValueException::CODE_ACTION_UNKNOWN,
null,
$operator
);
}
}
/**
* Creates a new query with an initial condition.
*
* @param string $name The name of the property to test.
* @param string|resource|null $value Value of the property as a string
* or seekable stream. Not required for existence tests.
* If a seekable stream is provided, it is sent from its current
* posistion to its end, and the pointer is seeked back to its current
* position after sending.
* Non seekable streams, as well as all other types, are casted to a
* string.
* @param string $operator One of the OP_* constants.
* Describes the operation to perform.
*
* @return static A new query object.
*/
public static function where(
$name,
$value = null,
$operator = self::OP_EX
) {
$query = new static;
return $query->addWhere($name, $value, $operator);
}
/**
* Negates the query.
*
* @return $this The query object.
*/
public function not()
{
$this->words[] = array('#!', null);
return $this;
}
/**
* Adds a condition as an alternative to the query.
*
* @param string $name The name of the property to test.
* @param string|resource|null $value Value of the property as a string
* or seekable stream. Not required for existence tests.
* If a seekable stream is provided, it is sent from its current
* posistion to its end, and the pointer is seeked back to its current
* position after sending.
* Non seekable streams, as well as all other types, are casted to a
* string.
* @param string $operator One of the OP_* constants.
* Describes the operation to perform.
*
* @return $this The query object.
*/
public function orWhere($name, $value = null, $operator = self::OP_EX)
{
$this->addWhere($name, $value, $operator)->words[] = array('#|', null);
return $this;
}
/**
* Adds a condition in addition to the query.
*
* @param string $name The name of the property to test.
* @param string|resource|null $value Value of the property as a string
* or seekable stream. Not required for existence tests.
* If a seekable stream is provided, it is sent from its current
* posistion to its end, and the pointer is seeked back to its current
* position after sending.
* Non seekable streams, as well as all other types, are casted to a
* string.
* @param string $operator One of the OP_* constants.
* Describes the operation to perform.
*
* @return $this The query object.
*/
public function andWhere($name, $value = null, $operator = self::OP_EX)
{
$this->addWhere($name, $value, $operator)->words[] = array('#&', null);
return $this;
}
/**
* Sends the query over a communicator.
*
* @param Communicator $com The communicator to send the query over.
*
* @return int The number of bytes sent.
*/
public function send(Communicator $com)
{
if ($com->getTransmitter()->isPersistent()) {
$old = $com->getTransmitter()->lock(T\Stream::DIRECTION_SEND);
$bytes = $this->_send($com);
$com->getTransmitter()->lock($old, true);
return $bytes;
}
return $this->_send($com);
}
/**
* Sends the query over a communicator.
*
* The only difference with the non private equivalent is that this one does
* not do locking.
*
* @param Communicator $com The communicator to send the query over.
*
* @return int The number of bytes sent.
*/
private function _send(Communicator $com)
{
if (!$com->getTransmitter()->isAcceptingData()) {
throw new SocketException(
'Transmitter is invalid. Sending aborted.',
SocketException::CODE_QUERY_SEND_FAIL
);
}
$bytes = 0;
foreach ($this->words as $queryWord) {
list($predicate, $value) = $queryWord;
$prefix = '?' . $predicate;
if (null === $value) {
$bytes += $com->sendWord($prefix);
} else {
$prefix .= '=';
if (is_string($value)) {
$bytes += $com->sendWord($prefix . $value);
} else {
$bytes += $com->sendWordFromStream($prefix, $value);
}
}
}
return $bytes;
}
/**
* Adds a condition.
*
* @param string $name The name of the property to test.
* @param string|resource|null $value Value of the property as a string
* or seekable stream. Not required for existence tests.
* If a seekable stream is provided, it is sent from its current
* posistion to its end, and the pointer is seeked back to its current
* position after sending.
* Non seekable streams, as well as all other types, are casted to a
* string.
* @param string $operator One of the ACTION_* constants.
* Describes the operation to perform.
*
* @return $this The query object.
*/
protected function addWhere($name, $value, $operator)
{
$this->words[] = array(
static::sanitizeOperator($operator)
. Message::sanitizeAttributeName($name),
(null === $value ? null : Message::sanitizeAttributeValue($value))
);
return $this;
}
}

View File

@ -0,0 +1,279 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Uses shared memory to keep responses in.
*/
use PEAR2\Cache\SHM;
/**
* A RouterOS registry.
*
* Provides functionality for managing the request/response flow. Particularly
* useful in persistent connections.
*
* Note that this class is not meant to be called directly.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
class Registry
{
/**
* @var SHM The storage.
*/
protected $shm;
/**
* @var int ID of request. Populated at first instance in request.
*/
protected static $requestId = -1;
/**
* @var int ID to be given to next instance, after incrementing it.
*/
protected static $instanceIdSeed = -1;
/**
* @var int ID of instance within the request.
*/
protected $instanceId;
/**
* Creates a registry.
*
* @param string $uri An URI to bind the registry to.
*/
public function __construct($uri)
{
$this->shm = SHM::factory(__CLASS__ . ' ' . $uri);
if (-1 === self::$requestId) {
self::$requestId = $this->shm->add('requestId', 0)
? 0 : $this->shm->inc('requestId');
}
$this->instanceId = ++self::$instanceIdSeed;
$this->shm->add('responseBuffer_' . $this->getOwnershipTag(), array());
}
/**
* Parses a tag.
*
* Parses a tag to reveal the ownership part of it, and the original tag.
*
* @param string $tag The tag (as received) to parse.
*
* @return array An array with the first member being the ownership tag, and
* the second one being the original tag.
*/
public static function parseTag($tag)
{
if (null === $tag) {
return array(null, null);
}
$result = explode('__', $tag, 2);
$result[0] .= '__';
if ('' === $result[1]) {
$result[1] = null;
}
return $result;
}
/**
* Checks if this instance is the tagless mode owner.
*
* @return bool TRUE if this instance is the tagless mode owner, FALSE
* otherwise.
*/
public function isTaglessModeOwner()
{
$this->shm->lock('taglessModeOwner');
$result = $this->shm->exists('taglessModeOwner')
&& $this->getOwnershipTag() === $this->shm->get('taglessModeOwner');
$this->shm->unlock('taglessModeOwner');
return $result;
}
/**
* Sets the "tagless mode" setting.
*
* While in tagless mode, this instance will claim owhership of any
* responses without a tag. While not in this mode, any requests without a
* tag will be given to all instances.
*
* Regardless of mode, if the type of the response is
* {@link Response::TYPE_FATAL}, it will be given to all instances.
*
* @param bool $taglessMode TRUE to claim tagless ownership, FALSE to
* release such ownership, if taken.
*
* @return bool TRUE on success, FALSE on failure.
*/
public function setTaglessMode($taglessMode)
{
return $taglessMode
? ($this->shm->lock('taglessMode')
&& $this->shm->lock('taglessModeOwner')
&& $this->shm->add('taglessModeOwner', $this->getOwnershipTag())
&& $this->shm->unlock('taglessModeOwner'))
: ($this->isTaglessModeOwner()
&& $this->shm->lock('taglessModeOwner')
&& $this->shm->delete('taglessModeOwner')
&& $this->shm->unlock('taglessModeOwner')
&& $this->shm->unlock('taglessMode'));
}
/**
* Get the ownership tag for this instance.
*
* @return string The ownership tag for this registry instance.
*/
public function getOwnershipTag()
{
return self::$requestId . '_' . $this->instanceId . '__';
}
/**
* Add a response to the registry.
*
* @param Response $response The response to add. The caller of this
* function is responsible for ensuring that the ownership tag and the
* original tag are separated, so that only the original one remains in
* the response.
* @param string $ownershipTag The ownership tag that the response had.
*
* @return bool TRUE if the request was added to its buffer, FALSE if
* this instance owns the response, and therefore doesn't need to add
* the response to its buffer.
*/
public function add(Response $response, $ownershipTag)
{
if ($this->getOwnershipTag() === $ownershipTag
|| ($this->isTaglessModeOwner()
&& $response->getType() !== Response::TYPE_FATAL)
) {
return false;
}
if (null === $ownershipTag) {
$this->shm->lock('taglessModeOwner');
if ($this->shm->exists('taglessModeOwner')
&& $response->getType() !== Response::TYPE_FATAL
) {
$ownershipTag = $this->shm->get('taglessModeOwner');
$this->shm->unlock('taglessModeOwner');
} else {
$this->shm->unlock('taglessModeOwner');
foreach ($this->shm->getIterator(
'/^(responseBuffer\_)/',
true
) as $targetBufferName) {
$this->_add($response, $targetBufferName);
}
return true;
}
}
$this->_add($response, 'responseBuffer_' . $ownershipTag);
return true;
}
/**
* Adds a response to a buffer.
*
* @param Response $response The response to add.
* @param string $targetBufferName The name of the buffer to add the
* response to.
*
* @return void
*/
private function _add(Response $response, $targetBufferName)
{
if ($this->shm->lock($targetBufferName)) {
$targetBuffer = $this->shm->get($targetBufferName);
$targetBuffer[] = $response;
$this->shm->set($targetBufferName, $targetBuffer);
$this->shm->unlock($targetBufferName);
}
}
/**
* Gets the next response from this instance's buffer.
*
* @return Response|null The next response, or NULL if there isn't one.
*/
public function getNextResponse()
{
$response = null;
$targetBufferName = 'responseBuffer_' . $this->getOwnershipTag();
if ($this->shm->exists($targetBufferName)
&& $this->shm->lock($targetBufferName)
) {
$targetBuffer = $this->shm->get($targetBufferName);
if (!empty($targetBuffer)) {
$response = array_shift($targetBuffer);
$this->shm->set($targetBufferName, $targetBuffer);
}
$this->shm->unlock($targetBufferName);
}
return $response;
}
/**
* Closes the registry.
*
* Closes the registry, meaning that all buffers are cleared.
*
* @return void
*/
public function close()
{
self::$requestId = -1;
self::$instanceIdSeed = -1;
$this->shm->clear();
}
/**
* Removes a buffer.
*
* @param string $targetBufferName The buffer to remove.
*
* @return void
*/
private function _close($targetBufferName)
{
if ($this->shm->lock($targetBufferName)) {
$this->shm->delete($targetBufferName);
$this->shm->unlock($targetBufferName);
}
}
/**
* Removes this instance's buffer.
*/
public function __destruct()
{
$this->_close('responseBuffer_' . $this->getOwnershipTag());
}
}

View File

@ -0,0 +1,403 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Refers to transmitter direction constants.
*/
use PEAR2\Net\Transmitter as T;
/**
* Represents a RouterOS request.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
class Request extends Message
{
/**
* @var string The command to be executed.
*/
private $_command;
/**
* @var Query A query for the command.
*/
private $_query;
/**
* Creates a request to send to RouterOS.
*
* @param string $command The command to send. Can also contain arguments
* expressed in a shell-like syntax.
* @param Query $query A query to associate with the request.
* @param string $tag The tag for the request.
*
* @see setCommand()
* @see setArgument()
* @see setTag()
* @see setQuery()
*/
public function __construct($command, Query $query = null, $tag = null)
{
if (false !== strpos($command, '=')
&& false !== ($spaceBeforeEquals = strrpos(
strstr($command, '=', true),
' '
))
) {
$this->parseArgumentString(substr($command, $spaceBeforeEquals));
$command = rtrim(substr($command, 0, $spaceBeforeEquals));
}
$this->setCommand($command);
$this->setQuery($query);
$this->setTag($tag);
}
/**
* A shorthand gateway.
*
* This is a magic PHP method that allows you to call the object as a
* function. Depending on the argument given, one of the other functions in
* the class is invoked and its returned value is returned by this function.
*
* @param Query|Communicator|string|null $arg A {@link Query} to associate
* the request with, a {@link Communicator} to send the request over,
* an argument to get the value of, or NULL to get the tag. If a
* second argument is provided, this becomes the name of the argument to
* set the value of, and the second argument is the value to set.
*
* @return string|resource|int|$this Whatever the long form
* function returns.
*/
public function __invoke($arg = null)
{
if (func_num_args() > 1) {
return $this->setArgument(func_get_arg(0), func_get_arg(1));
}
if ($arg instanceof Query) {
return $this->setQuery($arg);
}
if ($arg instanceof Communicator) {
return $this->send($arg);
}
return parent::__invoke($arg);
}
/**
* Sets the command to send to RouterOS.
*
* Sets the command to send to RouterOS. The command can use the API or CLI
* syntax of RouterOS, but either way, it must be absolute (begin with a
* "/") and without arguments.
*
* @param string $command The command to send.
*
* @return $this The request object.
* @see getCommand()
* @see setArgument()
*/
public function setCommand($command)
{
$command = (string) $command;
if (strpos($command, '/') !== 0) {
throw new InvalidArgumentException(
'Commands must be absolute.',
InvalidArgumentException::CODE_ABSOLUTE_REQUIRED
);
}
if (substr_count($command, '/') === 1) {
//Command line syntax convertion
$cmdParts = preg_split('#[\s/]+#sm', $command);
$cmdRes = array($cmdParts[0]);
for ($i = 1, $n = count($cmdParts); $i < $n; $i++) {
if ('..' === $cmdParts[$i]) {
$delIndex = count($cmdRes) - 1;
if ($delIndex < 1) {
throw new InvalidArgumentException(
'Unable to resolve command',
InvalidArgumentException::CODE_CMD_UNRESOLVABLE
);
}
unset($cmdRes[$delIndex]);
$cmdRes = array_values($cmdRes);
} else {
$cmdRes[] = $cmdParts[$i];
}
}
$command = implode('/', $cmdRes);
}
if (!preg_match('#^/\S+$#sm', $command)) {
throw new InvalidArgumentException(
'Invalid command supplied.',
InvalidArgumentException::CODE_CMD_INVALID
);
}
$this->_command = $command;
return $this;
}
/**
* Gets the command that will be send to RouterOS.
*
* Gets the command that will be send to RouterOS in its API syntax.
*
* @return string The command to send.
* @see setCommand()
*/
public function getCommand()
{
return $this->_command;
}
/**
* Sets the query to send with the command.
*
* @param Query $query The query to be set. Setting NULL will remove the
* currently associated query.
*
* @return $this The request object.
* @see getQuery()
*/
public function setQuery(Query $query = null)
{
$this->_query = $query;
return $this;
}
/**
* Gets the currently associated query
*
* @return Query The currently associated query.
* @see setQuery()
*/
public function getQuery()
{
return $this->_query;
}
/**
* Sets the tag to associate the request with.
*
* Sets the tag to associate the request with. Setting NULL erases the
* currently set tag.
*
* @param string $tag The tag to set.
*
* @return $this The request object.
* @see getTag()
*/
public function setTag($tag)
{
return parent::setTag($tag);
}
/**
* Sets an argument for the request.
*
* @param string $name Name of the argument.
* @param string|resource|null $value Value of the argument as a string or
* seekable stream.
* Setting the value to NULL removes an argument of this name.
* If a seekable stream is provided, it is sent from its current
* posistion to its end, and the pointer is seeked back to its current
* position after sending.
* Non seekable streams, as well as all other types, are casted to a
* string.
*
* @return $this The request object.
* @see getArgument()
*/
public function setArgument($name, $value = '')
{
return parent::setAttribute($name, $value);
}
/**
* Gets the value of an argument.
*
* @param string $name The name of the argument.
*
* @return string|resource|null The value of the specified argument.
* Returns NULL if such an argument is not set.
* @see setAttribute()
*/
public function getArgument($name)
{
return parent::getAttribute($name);
}
/**
* Removes all arguments from the request.
*
* @return $this The request object.
*/
public function removeAllArguments()
{
return parent::removeAllAttributes();
}
/**
* Sends a request over a communicator.
*
* @param Communicator $com The communicator to send the request over.
* @param Registry $reg An optional registry to sync the request with.
*
* @return int The number of bytes sent.
* @see Client::sendSync()
* @see Client::sendAsync()
*/
public function send(Communicator $com, Registry $reg = null)
{
if (null !== $reg
&& (null != $this->getTag() || !$reg->isTaglessModeOwner())
) {
$originalTag = $this->getTag();
$this->setTag($reg->getOwnershipTag() . $originalTag);
$bytes = $this->send($com);
$this->setTag($originalTag);
return $bytes;
}
if ($com->getTransmitter()->isPersistent()) {
$old = $com->getTransmitter()->lock(T\Stream::DIRECTION_SEND);
$bytes = $this->_send($com);
$com->getTransmitter()->lock($old, true);
return $bytes;
}
return $this->_send($com);
}
/**
* Sends a request over a communicator.
*
* The only difference with the non private equivalent is that this one does
* not do locking.
*
* @param Communicator $com The communicator to send the request over.
*
* @return int The number of bytes sent.
* @see Client::sendSync()
* @see Client::sendAsync()
*/
private function _send(Communicator $com)
{
if (!$com->getTransmitter()->isAcceptingData()) {
throw new SocketException(
'Transmitter is invalid. Sending aborted.',
SocketException::CODE_REQUEST_SEND_FAIL
);
}
$bytes = 0;
$bytes += $com->sendWord($this->getCommand());
if (null !== ($tag = $this->getTag())) {
$bytes += $com->sendWord('.tag=' . $tag);
}
foreach ($this->attributes as $name => $value) {
$prefix = '=' . $name . '=';
if (is_string($value)) {
$bytes += $com->sendWord($prefix . $value);
} else {
$bytes += $com->sendWordFromStream($prefix, $value);
}
}
$query = $this->getQuery();
if ($query instanceof Query) {
$bytes += $query->send($com);
}
$bytes += $com->sendWord('');
return $bytes;
}
/**
* Parses the arguments of a command.
*
* @param string $string The argument string to parse.
*
* @return void
*/
protected function parseArgumentString($string)
{
/*
* Grammar:
*
* <arguments> := (<<\s+>>, <argument>)*,
* <argument> := <name>, <value>?
* <name> := <<[^\=\s]+>>
* <value> := "=", (<quoted string> | <unquoted string>)
* <quotedString> := <<">>, <<([^"]|\\"|\\\\)*>>, <<">>
* <unquotedString> := <<\S+>>
*/
$token = '';
$name = null;
while ($string = substr($string, strlen($token))) {
if (null === $name) {
if (preg_match('/^\s+([^\s=]+)/sS', $string, $matches)) {
$token = $matches[0];
$name = $matches[1];
} else {
throw new InvalidArgumentException(
"Parsing of argument name failed near '{$string}'",
InvalidArgumentException::CODE_NAME_UNPARSABLE
);
}
} elseif (preg_match('/^\s/s', $string, $matches)) {
//Empty argument
$token = '';
$this->setArgument($name);
$name = null;
} elseif (preg_match(
'/^="(([^\\\"]|\\\"|\\\\)*)"/sS',
$string,
$matches
)) {
$token = $matches[0];
$this->setArgument(
$name,
str_replace(
array('\\"', '\\\\'),
array('"', '\\'),
$matches[1]
)
);
$name = null;
} elseif (preg_match('/^=(\S+)/sS', $string, $matches)) {
$token = $matches[0];
$this->setArgument($name, $matches[1]);
$name = null;
} else {
throw new InvalidArgumentException(
"Parsing of argument value failed near '{$string}'",
InvalidArgumentException::CODE_VALUE_UNPARSABLE
);
}
}
if (null !== $name && ('' !== ($name = trim($name)))) {
$this->setArgument($name, '');
}
}
}

View File

@ -0,0 +1,335 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Refers to transmitter direction constants.
*/
use PEAR2\Net\Transmitter as T;
/**
* Locks are released upon any exception from anywhere.
*/
use Exception as E;
/**
* Represents a RouterOS response.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
class Response extends Message
{
/**
* The last response for a request.
*/
const TYPE_FINAL = '!done';
/**
* A response with data.
*/
const TYPE_DATA = '!re';
/**
* A response signifying error.
*/
const TYPE_ERROR = '!trap';
/**
* A response signifying a fatal error, due to which the connection would be
* terminated.
*/
const TYPE_FATAL = '!fatal';
/**
* @var array An array of unrecognized words in network order.
*/
protected $unrecognizedWords = array();
/**
* @var string The response type.
*/
private $_type;
/**
* Extracts a new response from a communicator.
*
* @param Communicator $com The communicator from which to extract
* the new response.
* @param bool $asStream Whether to populate the argument values
* with streams instead of strings.
* @param int $sTimeout If a response is not immediatly
* available, wait this many seconds. If NULL, wait indefinetly.
* @param int $usTimeout Microseconds to add to the waiting time.
* @param Registry $reg An optional registry to sync the
* response with.
*
* @see getType()
* @see getArgument()
*/
public function __construct(
Communicator $com,
$asStream = false,
$sTimeout = 0,
$usTimeout = null,
Registry $reg = null
) {
if (null === $reg) {
if ($com->getTransmitter()->isPersistent()) {
$old = $com->getTransmitter()
->lock(T\Stream::DIRECTION_RECEIVE);
try {
$this->_receive($com, $asStream, $sTimeout, $usTimeout);
} catch (E $e) {
$com->getTransmitter()->lock($old, true);
throw $e;
}
$com->getTransmitter()->lock($old, true);
} else {
$this->_receive($com, $asStream, $sTimeout, $usTimeout);
}
} else {
while (null === ($response = $reg->getNextResponse())) {
$newResponse = new self($com, true, $sTimeout, $usTimeout);
$tagInfo = $reg::parseTag($newResponse->getTag());
$newResponse->setTag($tagInfo[1]);
if (!$reg->add($newResponse, $tagInfo[0])) {
$response = $newResponse;
break;
}
}
$this->_type = $response->_type;
$this->attributes = $response->attributes;
$this->unrecognizedWords = $response->unrecognizedWords;
$this->setTag($response->getTag());
if (!$asStream) {
foreach ($this->attributes as $name => $value) {
$this->setAttribute(
$name,
stream_get_contents($value)
);
}
foreach ($response->unrecognizedWords as $i => $value) {
$this->unrecognizedWords[$i] = stream_get_contents($value);
}
}
}
}
/**
* Extracts a new response from a communicator.
*
* This is the function that performs the actual receiving, while the
* constructor is also involved in locks and registry sync.
*
* @param Communicator $com The communicator from which to extract
* the new response.
* @param bool $asStream Whether to populate the argument values
* with streams instead of strings.
* @param int $sTimeout If a response is not immediatly
* available, wait this many seconds. If NULL, wait indefinetly.
* Note that if an empty sentence is received, the timeout will be
* reset for another sentence receiving.
* @param int $usTimeout Microseconds to add to the waiting time.
*
* @return void
*/
private function _receive(
Communicator $com,
$asStream = false,
$sTimeout = 0,
$usTimeout = null
) {
do {
if (!$com->getTransmitter()->isDataAwaiting(
$sTimeout,
$usTimeout
)) {
throw new SocketException(
'No data within the time limit',
SocketException::CODE_NO_DATA
);
}
$type = $com->getNextWord();
} while ('' === $type);
$this->setType($type);
if ($asStream) {
for ($word = $com->getNextWordAsStream(), fseek($word, 0, SEEK_END);
ftell($word) !== 0;
$word = $com->getNextWordAsStream(), fseek(
$word,
0,
SEEK_END
)) {
rewind($word);
$ind = fread($word, 1);
if ('=' === $ind || '.' === $ind) {
$prefix = stream_get_line($word, null, '=');
}
if ('=' === $ind) {
$value = fopen('php://temp', 'r+b');
$bytesCopied = ftell($word);
while (!feof($word)) {
$bytesCopied += stream_copy_to_stream(
$word,
$value,
0xFFFFF,
$bytesCopied
);
}
rewind($value);
$this->setAttribute($prefix, $value);
continue;
}
if ('.' === $ind && 'tag' === $prefix) {
$this->setTag(stream_get_contents($word, -1, -1));
continue;
}
rewind($word);
$this->unrecognizedWords[] = $word;
}
} else {
for ($word = $com->getNextWord(); '' !== $word;
$word = $com->getNextWord()) {
if (preg_match('/^=([^=]+)=(.*)$/sS', $word, $matches)) {
$this->setAttribute($matches[1], $matches[2]);
} elseif (preg_match('/^\.tag=(.*)$/sS', $word, $matches)) {
$this->setTag($matches[1]);
} else {
$this->unrecognizedWords[] = $word;
}
}
}
}
/**
* Sets the response type.
*
* Sets the response type. Valid values are the TYPE_* constants.
*
* @param string $type The new response type.
*
* @return $this The response object.
* @see getType()
*/
protected function setType($type)
{
switch ($type) {
case self::TYPE_FINAL:
case self::TYPE_DATA:
case self::TYPE_ERROR:
case self::TYPE_FATAL:
$this->_type = $type;
return $this;
default:
throw new UnexpectedValueException(
'Unrecognized response type.',
UnexpectedValueException::CODE_RESPONSE_TYPE_UNKNOWN,
null,
$type
);
}
}
/**
* Gets the response type.
*
* @return string The response type.
* @see setType()
*/
public function getType()
{
return $this->_type;
}
/**
* Gets the value of an argument.
*
* @param string $name The name of the argument.
*
* @return string|resource|null The value of the specified argument.
* Returns NULL if such an argument is not set.
* @deprecated 1.0.0b5 Use {@link static::getProperty()} instead.
* This method will be removed upon final release, and is currently
* left standing merely because it can't be easily search&replaced in
* existing code, due to the fact the name "getArgument()" is shared
* with {@link Request::getArgument()}, which is still valid.
* @codeCoverageIgnore
*/
public function getArgument($name)
{
trigger_error(
'Response::getArgument() is deprecated in favor of ' .
'Response::getProperty() (but note that Request::getArgument() ' .
'is still valid)',
E_USER_DEPRECATED
);
return $this->getAttribute($name);
}
/**
* Gets the value of a property.
*
* @param string $name The name of the property.
*
* @return string|resource|null The value of the specified property.
* Returns NULL if such a property is not set.
*/
public function getProperty($name)
{
return parent::getAttribute($name);
}
/**
* Gets a list of unrecognized words.
*
* @return array The list of unrecognized words.
*/
public function getUnrecognizedWords()
{
return $this->unrecognizedWords;
}
/**
* Counts the number of arguments or words.
*
* @param int $mode The counter mode.
* Either COUNT_NORMAL or COUNT_RECURSIVE.
* When in normal mode, counts the number of arguments.
* When in recursive mode, counts the number of API words.
*
* @return int The number of arguments/words.
*/
public function count($mode = COUNT_NORMAL)
{
$result = parent::count($mode);
if ($mode !== COUNT_NORMAL) {
$result += count($this->unrecognizedWords);
}
return $result;
}
}

View File

@ -0,0 +1,569 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Implemented by this class.
*/
use ArrayAccess;
/**
* Implemented by this class.
*/
use Countable;
/**
* Implemented by this class.
*/
use SeekableIterator;
/**
* Represents a collection of RouterOS responses.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*
* @method string getType()
* Calls {@link Response::getType()}
* on the response pointed by the pointer.
* @method string[] getUnrecognizedWords()
* Calls {@link Response::getUnrecognizedWords()}
* on the response pointed by the pointer.
* @method string|resource|null getProperty(string $name)
* Calls {@link Response::getProperty()}
* on the response pointed by the pointer.
* @method string getTag()
* Calls {@link Response::getTag()}
* on the response pointed by the pointer.
*/
class ResponseCollection implements ArrayAccess, SeekableIterator, Countable
{
/**
* @var array An array with all {@link Response} objects.
*/
protected $responses = array();
/**
* @var array An array with each {@link Response} object's type.
*/
protected $responseTypes = array();
/**
* @var array An array with each {@link Response} object's tag.
*/
protected $responseTags = array();
/**
* @var array An array with positions of responses, based on an property
* name. The name of each property is the array key, and the array value
* is another array where the key is the value for that property, and
* the value is the posistion of the response. For performance reasons,
* each key is built only when {@link static::setIndex()} is called with
* that property, and remains available for the lifetime of this
* collection.
*/
protected $responsesIndex = array();
/**
* @var array An array with all distinct properties across all
* {@link Response} objects. Created at the first call of
* {@link static::getPropertyMap()}.
*/
protected $propertyMap = null;
/**
* @var int A pointer, as required by SeekableIterator.
*/
protected $position = 0;
/**
* @var string|null Name of property to use as index. NULL when disabled.
*/
protected $index = null;
/**
* @var array Criterias used by {@link compare()} to determine the order
* between two respones. See {@link orderBy()} for a detailed
* description of this array's format.
*/
protected $compareBy = array();
/**
* Creates a new collection.
*
* @param array $responses An array of responses, in network order.
*/
public function __construct(array $responses)
{
$pos = 0;
foreach ($responses as $response) {
if ($response instanceof Response) {
$this->responseTypes[$pos] = $response->getType();
$this->responseTags[$pos] = $response->getTag();
$this->responses[$pos++] = $response;
}
}
}
/**
* A shorthand gateway.
*
* This is a magic PHP method that allows you to call the object as a
* function. Depending on the argument given, one of the other functions in
* the class is invoked and its returned value is returned by this function.
*
* @param int|string|null $offset The offset of the response to seek to.
* If the offset is negative, seek to that relative to the end.
* If the collection is indexed, you can also supply a value to seek to.
* Setting NULL will get the current response's interator.
*
* @return Response|ArrayObject The {@link Response} at the specified
* offset, the current response's iterator (which is an ArrayObject)
* when NULL is given, or FALSE if the offset is invalid
* or the collection is empty.
*/
public function __invoke($offset = null)
{
return null === $offset
? $this->current()->getIterator()
: $this->seek($offset);
}
/**
* Sets a property to be usable as a key in the collection.
*
* @param string|null $name The name of the property to use. Future calls
* that accept a position will then also be able to search values of
* that property for a matching value.
* Specifying NULL will disable such lookups (as is by default).
* Note that in case this value occures multiple times within the
* collection, only the last matching response will be accessible by
* that value.
*
* @return $this The object itself.
*/
public function setIndex($name)
{
if (null !== $name) {
$name = (string)$name;
if (!isset($this->responsesIndex[$name])) {
$this->responsesIndex[$name] = array();
foreach ($this->responses as $pos => $response) {
$val = $response->getProperty($name);
if (null !== $val) {
$this->responsesIndex[$name][$val] = $pos;
}
}
}
}
$this->index = $name;
return $this;
}
/**
* Gets the name of the property used as an index.
*
* @return string|null Name of property used as index. NULL when disabled.
*/
public function getIndex()
{
return $this->index;
}
/**
* Gets the whole collection as an array.
*
* @param bool $useIndex Whether to use the index values as keys for the
* resulting array.
*
* @return array An array with all responses, in network order.
*/
public function toArray($useIndex = false)
{
if ($useIndex) {
$positions = $this->responsesIndex[$this->index];
asort($positions, SORT_NUMERIC);
$positions = array_flip($positions);
return array_combine(
$positions,
array_intersect_key($this->responses, $positions)
);
}
return $this->responses;
}
/**
* Counts the responses/words in the collection.
*
* @param int $mode The counter mode.
* Either COUNT_NORMAL or COUNT_RECURSIVE.
* When in normal mode, counts the number of responses.
* When in recursive mode, counts the total number of API words.
*
* @return int The number of responses in the collection.
*/
public function count($mode = COUNT_NORMAL)
{
if ($mode !== COUNT_NORMAL) {
$result = 0;
foreach ($this->responses as $response) {
$result += $response->count($mode);
}
return $result;
} else {
return count($this->responses);
}
}
/**
* Checks if an offset exists.
*
* @param int|string $offset The offset to check. If the
* collection is indexed, you can also supply a value to check.
* Note that negative numeric offsets are NOT accepted.
*
* @return bool TRUE if the offset exists, FALSE otherwise.
*/
public function offsetExists($offset)
{
return is_int($offset)
? array_key_exists($offset, $this->responses)
: array_key_exists($offset, $this->responsesIndex[$this->index]);
}
/**
* Gets a {@link Response} from a specified offset.
*
* @param int|string $offset The offset of the desired response. If the
* collection is indexed, you can also supply the value to search for.
*
* @return Response The response at the specified offset.
*/
public function offsetGet($offset)
{
return is_int($offset)
? $this->responses[$offset >= 0
? $offset
: count($this->responses) + $offset]
: $this->responses[$this->responsesIndex[$this->index][$offset]];
}
/**
* N/A
*
* This method exists only because it is required for ArrayAccess. The
* collection is read only.
*
* @param int|string $offset N/A
* @param Response $value N/A
*
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function offsetSet($offset, $value)
{
}
/**
* N/A
*
* This method exists only because it is required for ArrayAccess. The
* collection is read only.
*
* @param int|string $offset N/A
*
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function offsetUnset($offset)
{
}
/**
* Resets the pointer to 0, and returns the first response.
*
* @return Response The first response in the collection, or FALSE if the
* collection is empty.
*/
public function rewind()
{
return $this->seek(0);
}
/**
* Moves the position pointer to a specified position.
*
* @param int|string $position The position to move to. If the collection is
* indexed, you can also supply a value to move the pointer to.
* A non-existent index will move the pointer to "-1".
*
* @return Response The {@link Response} at the specified position, or FALSE
* if the specified position is not valid.
*/
public function seek($position)
{
$this->position = is_int($position)
? ($position >= 0
? $position
: count($this->responses) + $position)
: ($this->offsetExists($position)
? $this->responsesIndex[$this->index][$position]
: -1);
return $this->current();
}
/**
* Moves the pointer forward by 1, and gets the next response.
*
* @return Response The next {@link Response} object, or FALSE if the
* position is not valid.
*/
public function next()
{
++$this->position;
return $this->current();
}
/**
* Gets the response at the current pointer position.
*
* @return Response The response at the current pointer position, or FALSE
* if the position is not valid.
*/
public function current()
{
return $this->valid() ? $this->responses[$this->position] : false;
}
/**
* Moves the pointer backwards by 1, and gets the previous response.
*
* @return Response The next {@link Response} object, or FALSE if the
* position is not valid.
*/
public function prev()
{
--$this->position;
return $this->current();
}
/**
* Moves the pointer to the last valid position, and returns the last
* response.
*
* @return Response The last response in the collection, or FALSE if the
* collection is empty.
*/
public function end()
{
$this->position = count($this->responses) - 1;
return $this->current();
}
/**
* Gets the key at the current pointer position.
*
* @return int The key at the current pointer position, i.e. the pointer
* position itself, or FALSE if the position is not valid.
*/
public function key()
{
return $this->valid() ? $this->position : false;
}
/**
* Checks if the pointer is still pointing to an existing offset.
*
* @return bool TRUE if the pointer is valid, FALSE otherwise.
*/
public function valid()
{
return $this->offsetExists($this->position);
}
/**
* Gets all distinct property names.
*
* Gets all distinct property names across all responses.
*
* @return array An array with all distinct property names as keys, and the
* indexes at which they occur as values.
*/
public function getPropertyMap()
{
if (null === $this->propertyMap) {
$properties = array();
foreach ($this->responses as $index => $response) {
$names = array_keys($response->getIterator()->getArrayCopy());
foreach ($names as $name) {
if (!isset($properties[$name])) {
$properties[$name] = array();
}
$properties[$name][] = $index;
}
}
$this->propertyMap = $properties;
}
return $this->propertyMap;
}
/**
* Gets all responses of a specified type.
*
* @param string $type The response type to filter by. Valid values are the
* Response::TYPE_* constants.
*
* @return static A new collection with responses of the
* specified type.
*/
public function getAllOfType($type)
{
$result = array();
foreach (array_keys($this->responseTypes, $type, true) as $index) {
$result[] = $this->responses[$index];
}
return new static($result);
}
/**
* Gets all responses with a specified tag.
*
* @param string $tag The tag to filter by.
*
* @return static A new collection with responses having the
* specified tag.
*/
public function getAllTagged($tag)
{
$result = array();
foreach (array_keys($this->responseTags, $tag, true) as $index) {
$result[] = $this->responses[$index];
}
return new static($result);
}
/**
* Order resones by criteria.
*
* @param mixed[] $criteria The criteria to order respones by. It takes the
* form of an array where each key is the name of the property to use
* as (N+1)th sorting key. The value of each member can be either NULL
* (for that property, sort normally in ascending order), a single sort
* order constant (SORT_ASC or SORT_DESC) to sort normally in the
* specified order, an array where the first member is an order
* constant, and the second one is sorting flags (same as built in PHP
* array functions) or a callback.
* If a callback is provided, it must accept two arguments
* (the two values to be compared), and return -1, 0 or 1 if the first
* value is respectively less than, equal to or greather than the second
* one.
* Each key of $criteria can also be numeric, in which case the
* value is the name of the property, and sorting is done normally in
* ascending order.
*
* @return static A new collection with the responses sorted in the
* specified order.
*/
public function orderBy(array $criteria)
{
$this->compareBy = $criteria;
$sortedResponses = $this->responses;
usort($sortedResponses, array($this, 'compare'));
return new static($sortedResponses);
}
/**
* Calls a method of the response pointed by the pointer.
*
* Calls a method of the response pointed by the pointer. This is a magic
* PHP method, thanks to which any function you call on the collection that
* is not defined will be redirected to the response.
*
* @param string $method The name of the method to call.
* @param array $args The arguments to pass to the method.
*
* @return mixed Whatever the called function returns.
*/
public function __call($method, array $args)
{
return call_user_func_array(
array($this->current(), $method),
$args
);
}
/**
* Compares two respones.
*
* Compares two respones, based on criteria defined in
* {@link static::$compareBy}.
*
* @param Response $itemA The response to compare.
* @param Response $itemB The response to compare $a against.
*
* @return int Returns 0 if the two respones are equal according to every
* criteria specified, -1 if $a should be placed before $b, and 1 if $b
* should be placed before $a.
*/
protected function compare(Response $itemA, Response $itemB)
{
foreach ($this->compareBy as $name => $spec) {
if (!is_string($name)) {
$name = $spec;
$spec = null;
}
$members = array(
0 => $itemA->getProperty($name),
1 => $itemB->getProperty($name)
);
if (is_callable($spec)) {
uasort($members, $spec);
} elseif ($members[0] === $members[1]) {
continue;
} else {
$flags = SORT_REGULAR;
$order = SORT_ASC;
if (is_array($spec)) {
list($order, $flags) = $spec;
} elseif (null !== $spec) {
$order = $spec;
}
if (SORT_ASC === $order) {
asort($members, $flags);
} else {
arsort($members, $flags);
}
}
return (key($members) === 0) ? -1 : 1;
}
return 0;
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
* Base of this class.
*/
use RuntimeException;
/**
* Exception thrown when something goes wrong with the connection.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
class SocketException extends RuntimeException implements Exception
{
const CODE_SERVICE_INCOMPATIBLE = 10200;
const CODE_CONNECTION_FAIL = 100;
const CODE_QUERY_SEND_FAIL = 30600;
const CODE_REQUEST_SEND_FAIL = 40900;
const CODE_NO_DATA = 50000;
}

View File

@ -0,0 +1,88 @@
<?php
/**
* RouterOS API client implementation.
*
* RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
*
* PHP version 5
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @copyright 2011 Vasil Rangelov
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 1.0.0b5
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
use UnexpectedValueException as U;
/**
* Exception thrown when encountering an invalid value in a function argument.
*
* @category Net
* @package PEAR2_Net_RouterOS
* @author Vasil Rangelov <boen.robot@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
class UnexpectedValueException extends U implements Exception
{
const CODE_CALLBACK_INVALID = 10502;
const CODE_ACTION_UNKNOWN = 30100;
const CODE_RESPONSE_TYPE_UNKNOWN = 50100;
/**
* @var mixed The unexpected value.
*/
private $_value;
/**
* Creates a new UnexpectedValueException.
*
* @param string $message The Exception message to throw.
* @param int $code The Exception code.
* @param \Exception $previous The previous exception used for the exception
* chaining.
* @param mixed $value The unexpected value.
*/
public function __construct(
$message,
$code = 0,
$previous = null,
$value = null
) {
parent::__construct($message, $code, $previous);
$this->_value = $value;
}
/**
* Gets the unexpected value.
*
* @return mixed The unexpected value.
*/
public function getValue()
{
return $this->_value;
}
// @codeCoverageIgnoreStart
// String representation is not reliable in testing
/**
* Returns a string representation of the exception.
*
* @return string The exception as a string.
*/
public function __toString()
{
return parent::__toString() . "\nValue:{$this->_value}";
}
// @codeCoverageIgnoreEnd
}

File diff suppressed because it is too large Load Diff