1203 lines
47 KiB
PHP
Raw Normal View History

2017-03-11 02:51:06 +07:00
<?php
/**
* RouterOS API client implementation.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* 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.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* PHP version 5
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @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
2023-10-05 16:55:44 +07:00
* @version 1.0.0b6
2017-03-11 02:51:06 +07:00
* @link http://pear2.php.net/PEAR2_Net_RouterOS
*/
/**
* The namespace declaration.
*/
namespace PEAR2\Net\RouterOS;
/**
2023-10-05 16:55:44 +07:00
* Returned from {@link Util::getCurrentTime()}.
2017-03-11 02:51:06 +07:00
*/
use DateTime;
/**
2023-10-05 16:55:44 +07:00
* Used at {@link Util::getCurrentTime()} to get the proper time.
2017-03-11 02:51:06 +07:00
*/
2023-10-05 16:55:44 +07:00
use DateTimeZone;
2017-03-11 02:51:06 +07:00
/**
* Implemented by this class.
*/
use Countable;
/**
2023-10-05 16:55:44 +07:00
* Used to detect streams in various methods of this class.
2017-03-11 02:51:06 +07:00
*/
use PEAR2\Net\Transmitter\Stream;
2023-10-05 16:55:44 +07:00
/**
* Used to catch a DateTime exception at {@link Util::getCurrentTime()}.
*/
use Exception as E;
2017-03-11 02:51:06 +07:00
/**
* Utility class.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Abstracts away frequently used functionality (particularly CRUD operations)
2023-10-05 16:55:44 +07:00
* in convenient to use methods by wrapping around a connection.
*
2017-03-11 02:51:06 +07:00
* @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 Util implements Countable
{
/**
2023-10-05 16:55:44 +07:00
* The connection to wrap around.
*
* @var Client
2017-03-11 02:51:06 +07:00
*/
protected $client;
/**
2023-10-05 16:55:44 +07:00
* The current menu.
*
* Note that the root menu (only) uses an empty string.
* This is done to enable commands executed at it without special casing it
* at all commands.
* Instead, only {@link static::setMenu()} is special cased.
*
* @var string
2017-03-11 02:51:06 +07:00
*/
2023-10-05 16:55:44 +07:00
protected $menu = '';
2017-03-11 02:51:06 +07:00
/**
2023-10-05 16:55:44 +07:00
* An array with the numbers of items in the current menu.
*
* Numbers as keys, and the corresponding IDs as values.
* NULL when the cache needs regenerating.
*
* @var array<int,string>|null
2017-03-11 02:51:06 +07:00
*/
protected $idCache = null;
/**
* Creates a new Util instance.
2023-10-05 16:55:44 +07:00
*
* Wraps around a connection to provide convenience methods.
*
2017-03-11 02:51:06 +07:00
* @param Client $client The connection to wrap around.
*/
public function __construct(Client $client)
{
$this->client = $client;
}
/**
* Gets the current menu.
2023-10-05 16:55:44 +07:00
*
* @return string The absolute path to current menu, using API syntax.
2017-03-11 02:51:06 +07:00
*/
public function getMenu()
{
2023-10-05 16:55:44 +07:00
return '' === $this->menu ? '/' : $this->menu;
2017-03-11 02:51:06 +07:00
}
2023-10-05 16:55:44 +07:00
2017-03-11 02:51:06 +07:00
/**
* Sets the current menu.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Sets the current menu.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @param string $newMenu The menu to change to. Can be specified with API
* or CLI syntax and can be either absolute or relative. If relative,
* it's relative to the current menu, which by default is the root.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @return $this The object itself. If an empty string is given for
* a new menu, no change is performed,
* but the ID cache is cleared anyway.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @see static::clearIdCache()
*/
public function setMenu($newMenu)
{
$newMenu = (string)$newMenu;
if ('' !== $newMenu) {
$menuRequest = new Request('/menu');
2023-10-05 16:55:44 +07:00
if ('/' === $newMenu) {
$this->menu = '';
} elseif ('/' === $newMenu[0]) {
2017-03-11 02:51:06 +07:00
$this->menu = $menuRequest->setCommand($newMenu)->getCommand();
} else {
2023-10-05 16:55:44 +07:00
$newMenu = (string)substr(
$menuRequest->setCommand(
'/' .
str_replace('/', ' ', (string)substr($this->menu, 1)) .
' ' .
str_replace('/', ' ', $newMenu)
. ' ?'
)->getCommand(),
1,
-2/*strlen('/?')*/
);
if ('' !== $newMenu) {
$this->menu = '/' . $newMenu;
} else {
$this->menu = '';
}
2017-03-11 02:51:06 +07:00
}
}
$this->clearIdCache();
return $this;
}
2023-10-05 16:55:44 +07:00
/**
* Creates a Request object.
*
* Creates a {@link Request} object, with a command that's at the
* current menu. The request can then be sent using {@link Client}.
*
* @param string $command The command of the request, not including
* the menu. The request will have that command at the current menu.
* @param array $args Arguments of the request.
* Each array key is the name of the argument, and each array value is
* the value of the argument to be passed.
* Arguments without a value (i.e. empty arguments) can also be
* specified using a numeric key, and the name of the argument as the
* array value.
* @param Query|null $query The {@link Query} of the request.
* @param string|null $tag The tag of the request.
*
* @return Request The {@link Request} object.
*
* @throws NotSupportedException On an attempt to call a command in a
* different menu using API syntax.
* @throws InvalidArgumentException On an attempt to call a command in a
* different menu using CLI syntax.
*/
public function newRequest(
$command,
array $args = array(),
Query $query = null,
$tag = null
) {
if (false !== strpos($command, '/')) {
throw new NotSupportedException(
'Command tried to go to a different menu',
NotSupportedException::CODE_MENU_MISMATCH,
null,
$command
);
}
$request = new Request('/menu', $query, $tag);
$request->setCommand("{$this->menu}/{$command}");
foreach ($args as $name => $value) {
if (is_int($name)) {
$request->setArgument($value);
} else {
$request->setArgument($name, $value);
}
}
return $request;
}
2017-03-11 02:51:06 +07:00
/**
* Executes a RouterOS script.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Executes a RouterOS script, written as a string or a stream.
* Note that in cases of errors, the line numbers will be off, because the
* script is executed at the current menu as context, with the specified
* variables pre declared. This is achieved by prepending 1+count($params)
* lines before your actual script.
2023-10-05 16:55:44 +07:00
*
* @param string|resource $source The source of the script,
* as a string or stream. If a stream is provided, reading starts from
* the current position to the end of the stream, and the pointer stays
* at the end after reading is done.
* @param array<string|int,mixed> $params An array of parameters to make
* available in the script as local variables.
2017-03-11 02:51:06 +07:00
* Variable names are array keys, and variable values are array values.
* Array values are automatically processed with
* {@link static::escapeValue()}. Streams are also supported, and are
2023-10-05 16:55:44 +07:00
* processed in chunks, each with
* {@link static::escapeString()} with all bytes being escaped.
* Processing starts from the current position to the end of the stream,
* and the stream's pointer is left untouched after the reading is done.
* Variables with a value of type "nothing" can be declared with a
* numeric array key and the variable name as the array value
* (that is casted to a string).
2017-03-11 02:51:06 +07:00
* Note that the script's (generated) name is always added as the
* variable "_", which will be inadvertently lost if you overwrite it
* from here.
2023-10-05 16:55:44 +07:00
* @param string|null $policy Allows you to specify a policy the
* script must follow. Has the same format as in terminal.
2017-03-11 02:51:06 +07:00
* If left NULL, the script has no restrictions beyond those imposed by
* the username.
2023-10-05 16:55:44 +07:00
* @param string|null $name The script is executed after being
* saved in "/system script" and is removed after execution.
* If this argument is left NULL, a random string,
* prefixed with the computer's name, is generated and used
* as the script's name.
* To eliminate any possibility of name clashes,
* you can specify your own name instead.
*
* @return ResponseCollection The responses of all requests involved, i.e.
* the add, the run and the remove.
*
* @throws RouterErrorException When there is an error in any step of the
* way. The reponses include all successful commands prior to the error
* as well. If the error occurs during the run, there will also be a
* remove attempt, and the results will include its results as well.
2017-03-11 02:51:06 +07:00
*/
public function exec(
$source,
array $params = array(),
$policy = null,
$name = null
) {
2023-10-05 16:55:44 +07:00
if (null === $name) {
$name = uniqid(gethostname(), true);
}
$request = new Request('/system/script/add');
$request->setArgument('name', $name);
$request->setArgument('policy', $policy);
$params += array('_' => $name);
$finalSource = fopen('php://temp', 'r+b');
fwrite(
$finalSource,
'/' . str_replace('/', ' ', substr($this->menu, 1)). "\n"
);
Script::append($finalSource, $source, $params);
fwrite($finalSource, "\n");
rewind($finalSource);
$request->setArgument('source', $finalSource);
$addResult = $this->client->sendSync($request);
if (count($addResult->getAllOfType(Response::TYPE_ERROR)) > 0) {
throw new RouterErrorException(
'Error when trying to add script',
RouterErrorException::CODE_SCRIPT_ADD_ERROR,
null,
$addResult
);
}
$request = new Request('/system/script/run');
$request->setArgument('number', $name);
$runResult = $this->client->sendSync($request);
$request = new Request('/system/script/remove');
$request->setArgument('numbers', $name);
$removeResult = $this->client->sendSync($request);
$results = new ResponseCollection(
array_merge(
$addResult->toArray(),
$runResult->toArray(),
$removeResult->toArray()
)
);
if (count($runResult->getAllOfType(Response::TYPE_ERROR)) > 0) {
throw new RouterErrorException(
'Error when running script',
RouterErrorException::CODE_SCRIPT_RUN_ERROR,
null,
$results
);
}
if (count($removeResult->getAllOfType(Response::TYPE_ERROR)) > 0) {
throw new RouterErrorException(
'Error when removing script',
RouterErrorException::CODE_SCRIPT_REMOVE_ERROR,
null,
$results
);
}
return $results;
2017-03-11 02:51:06 +07:00
}
2023-10-05 16:55:44 +07:00
2017-03-11 02:51:06 +07:00
/**
* Clears the ID cache.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Normally, the ID cache improves performance when targeting items by a
* number. If you're using both Util's methods and other means (e.g.
* {@link Client} or {@link Util::exec()}) to add/move/remove items, the
* cache may end up being out of date. By calling this method right before
* targeting an item with a number, you can ensure number accuracy.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Note that Util's {@link static::move()} and {@link static::remove()}
* methods automatically clear the cache before returning, while
* {@link static::add()} adds the new item's ID to the cache as the next
* number. A change in the menu also clears the cache.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Note also that the cache is being rebuilt unconditionally every time you
* use {@link static::find()} with a callback.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @return $this The Util object itself.
*/
public function clearIdCache()
{
$this->idCache = null;
return $this;
}
2023-10-05 16:55:44 +07:00
/**
* Gets the current time on the router.
*
* Gets the current time on the router, regardless of the current menu.
*
* If the timezone is one known to both RouterOS and PHP, it will be used
* as the timezone identifier. Otherwise (e.g. "manual"), the current GMT
* offset will be used as a timezone, without any DST awareness.
*
* @return DateTime The current time of the router, as a DateTime object.
*/
public function getCurrentTime()
{
$clock = $this->client->sendSync(
new Request(
'/system/clock/print
.proplist=date,time,time-zone-name,gmt-offset'
)
)->current();
$clockParts = array();
foreach (array(
'date',
'time',
'time-zone-name',
'gmt-offset'
) as $clockPart) {
$clockParts[$clockPart] = $clock->getProperty($clockPart);
if (Stream::isStream($clockParts[$clockPart])) {
$clockParts[$clockPart] = stream_get_contents(
$clockParts[$clockPart]
);
}
}
$datetime = ucfirst(strtolower($clockParts['date'])) . ' ' .
$clockParts['time'];
try {
$result = DateTime::createFromFormat(
'M/j/Y H:i:s',
$datetime,
new DateTimeZone($clockParts['time-zone-name'])
);
} catch (E $e) {
$result = DateTime::createFromFormat(
'M/j/Y H:i:s P',
$datetime . ' ' . $clockParts['gmt-offset'],
new DateTimeZone('UTC')
);
}
return $result;
}
2017-03-11 02:51:06 +07:00
/**
* Finds the IDs of items at the current menu.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Finds the IDs of items based on specified criteria, and returns them as
* a comma separated string, ready for insertion at a "numbers" argument.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Accepts zero or more criteria as arguments. If zero arguments are
* specified, returns all items' IDs. The value of each criteria can be a
* number (just as in Winbox), a literal ID to be included, a {@link Query}
* object, or a callback. If a callback is specified, it is called for each
* item, with the item as an argument. If it returns a true value, the
* item's ID is included in the result. Every other value is casted to a
* string. A string is treated as a comma separated values of IDs, numbers
* or callback names. Non-existent callback names are instead placed in the
* result, which may be useful in menus that accept identifiers other than
* IDs, but note that it can cause errors on other menus.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @return string A comma separated list of all items matching the
* specified criteria.
*/
public function find()
{
if (func_num_args() === 0) {
if (null === $this->idCache) {
2023-10-05 16:55:44 +07:00
$ret = $this->client->sendSync(
new Request($this->menu . '/find')
)->getProperty('ret');
if (null === $ret) {
$this->idCache = array();
return '';
} elseif (!is_string($ret)) {
$ret = stream_get_contents($ret);
}
2017-03-11 02:51:06 +07:00
$idCache = str_replace(
';',
',',
2023-10-05 16:55:44 +07:00
strtolower($ret)
2017-03-11 02:51:06 +07:00
);
$this->idCache = explode(',', $idCache);
return $idCache;
}
return implode(',', $this->idCache);
}
$idList = '';
foreach (func_get_args() as $criteria) {
if ($criteria instanceof Query) {
foreach ($this->client->sendSync(
new Request($this->menu . '/print .proplist=.id', $criteria)
2023-10-05 16:55:44 +07:00
)->getAllOfType(Response::TYPE_DATA) as $response) {
$newId = $response->getProperty('.id');
$idList .= strtolower(
is_string($newId)
? $newId
: stream_get_contents($newId) . ','
);
2017-03-11 02:51:06 +07:00
}
} elseif (is_callable($criteria)) {
$idCache = array();
foreach ($this->client->sendSync(
new Request($this->menu . '/print')
2023-10-05 16:55:44 +07:00
)->getAllOfType(Response::TYPE_DATA) as $response) {
$newId = $response->getProperty('.id');
$newId = strtolower(
is_string($newId)
? $newId
: stream_get_contents($newId)
);
2017-03-11 02:51:06 +07:00
if ($criteria($response)) {
2023-10-05 16:55:44 +07:00
$idList .= $newId . ',';
2017-03-11 02:51:06 +07:00
}
2023-10-05 16:55:44 +07:00
$idCache[] = $newId;
2017-03-11 02:51:06 +07:00
}
$this->idCache = $idCache;
} else {
$this->find();
if (is_int($criteria)) {
if (isset($this->idCache[$criteria])) {
$idList = $this->idCache[$criteria] . ',';
}
} else {
$criteria = (string)$criteria;
if ($criteria === (string)(int)$criteria) {
if (isset($this->idCache[(int)$criteria])) {
$idList .= $this->idCache[(int)$criteria] . ',';
}
} elseif (false === strpos($criteria, ',')) {
$idList .= $criteria . ',';
} else {
$criteriaArr = explode(',', $criteria);
for ($i = count($criteriaArr) - 1; $i >= 0; --$i) {
if ('' === $criteriaArr[$i]) {
unset($criteriaArr[$i]);
} elseif ('*' === $criteriaArr[$i][0]) {
$idList .= $criteriaArr[$i] . ',';
unset($criteriaArr[$i]);
}
}
if (!empty($criteriaArr)) {
$idList .= call_user_func_array(
array($this, 'find'),
$criteriaArr
) . ',';
}
}
}
}
}
return rtrim($idList, ',');
}
/**
* Gets a value of a specified item at the current menu.
2023-10-05 16:55:44 +07:00
*
* @param int|string|null|Query $number A number identifying the target
* item. Can also be an ID or (in some menus) name. For menus where
2017-03-11 02:51:06 +07:00
* there are no items (e.g. "/system identity"), you can specify NULL.
2023-10-05 16:55:44 +07:00
* You can also specify a {@link Query}, in which case the first match
* will be considered the target item.
* @param string|resource|null $valueName The name of the value to get.
* If omitted, or set to NULL, gets all properties of the target item.
*
* @return string|resource|null|array The value of the specified
* property as a string or as new PHP temp stream if the underlying
* {@link Client::isStreamingResponses()} is set to TRUE.
* If the property is not set, NULL will be returned.
* If $valueName is NULL, returns all properties as an array, where
* the result is parsed with {@link Script::parseValueToArray()}.
*
* @throws RouterErrorException When the router returns an error response
2017-03-11 02:51:06 +07:00
* (e.g. no such item, invalid property, etc.).
*/
2023-10-05 16:55:44 +07:00
public function get($number, $valueName = null)
2017-03-11 02:51:06 +07:00
{
2023-10-05 16:55:44 +07:00
if ($number instanceof Query) {
$number = explode(',', $this->find($number));
$number = $number[0];
} elseif (is_int($number) || ((string)$number === (string)(int)$number)) {
2017-03-11 02:51:06 +07:00
$this->find();
if (isset($this->idCache[(int)$number])) {
$number = $this->idCache[(int)$number];
} else {
2023-10-05 16:55:44 +07:00
throw new RouterErrorException(
'Unable to resolve number from ID cache (no such item maybe)',
RouterErrorException::CODE_CACHE_ERROR
);
2017-03-11 02:51:06 +07:00
}
}
$request = new Request($this->menu . '/get');
$request->setArgument('number', $number);
$request->setArgument('value-name', $valueName);
$responses = $this->client->sendSync($request);
if (Response::TYPE_ERROR === $responses->getType()) {
2023-10-05 16:55:44 +07:00
throw new RouterErrorException(
'Error getting property',
RouterErrorException::CODE_GET_ERROR,
null,
$responses
);
2017-03-11 02:51:06 +07:00
}
2023-10-05 16:55:44 +07:00
$result = $responses->getProperty('ret');
if (Stream::isStream($result)) {
$result = stream_get_contents($result);
2017-03-11 02:51:06 +07:00
}
2023-10-05 16:55:44 +07:00
if (null === $valueName) {
2017-03-11 02:51:06 +07:00
// @codeCoverageIgnoreStart
2023-10-05 16:55:44 +07:00
//Some earlier RouterOS versions use "," instead of ";" as separator
//Newer versions can't possibly enter this condition
if (false === strpos($result, ';')
&& preg_match('/^([^=,]+\=[^=,]*)(?:\,(?1))+$/', $result)
) {
$result = str_replace(',', ';', $result);
}
2017-03-11 02:51:06 +07:00
// @codeCoverageIgnoreEnd
2023-10-05 16:55:44 +07:00
return Script::parseValueToArray('{' . $result . '}');
2017-03-11 02:51:06 +07:00
}
2023-10-05 16:55:44 +07:00
return $result;
2017-03-11 02:51:06 +07:00
}
/**
* Enables all items at the current menu matching certain criteria.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Zero or more arguments can be specified, each being a criteria.
* If zero arguments are specified, enables all items.
* See {@link static::find()} for a description of what criteria are
* accepted.
2023-10-05 16:55:44 +07:00
*
* @return ResponseCollection Returns the response collection, allowing you
* to inspect the output. Current RouterOS versions don't return
* anything useful, but if future ones do, you can read it right away.
*
* @throws RouterErrorException When the router returns one or more errors.
2017-03-11 02:51:06 +07:00
*/
public function enable()
{
2023-10-05 16:55:44 +07:00
$responses = $this->doBulk('enable', func_get_args());
if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
throw new RouterErrorException(
'Error when enabling items',
RouterErrorException::CODE_ENABLE_ERROR,
null,
$responses
);
}
return $responses;
2017-03-11 02:51:06 +07:00
}
/**
* Disables all items at the current menu matching certain criteria.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Zero or more arguments can be specified, each being a criteria.
* If zero arguments are specified, disables all items.
* See {@link static::find()} for a description of what criteria are
* accepted.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @return ResponseCollection Returns the response collection, allowing you
2023-10-05 16:55:44 +07:00
* to inspect the output. Current RouterOS versions don't return
* anything useful, but if future ones do, you can read it right away.
*
* @throws RouterErrorException When the router returns one or more errors.
2017-03-11 02:51:06 +07:00
*/
public function disable()
{
2023-10-05 16:55:44 +07:00
$responses = $this->doBulk('disable', func_get_args());
if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
throw new RouterErrorException(
'Error when disabling items',
RouterErrorException::CODE_DISABLE_ERROR,
null,
$responses
);
}
return $responses;
2017-03-11 02:51:06 +07:00
}
/**
* Removes all items at the current menu matching certain criteria.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Zero or more arguments can be specified, each being a criteria.
* If zero arguments are specified, removes all items.
* See {@link static::find()} for a description of what criteria are
* accepted.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @return ResponseCollection Returns the response collection, allowing you
2023-10-05 16:55:44 +07:00
* to inspect the output. Current RouterOS versions don't return
* anything useful, but if future ones do, you can read it right away.
*
* @throws RouterErrorException When the router returns one or more errors.
2017-03-11 02:51:06 +07:00
*/
public function remove()
{
2023-10-05 16:55:44 +07:00
$responses = $this->doBulk('remove', func_get_args());
2017-03-11 02:51:06 +07:00
$this->clearIdCache();
2023-10-05 16:55:44 +07:00
if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
throw new RouterErrorException(
'Error when removing items',
RouterErrorException::CODE_REMOVE_ERROR,
null,
$responses
);
}
return $responses;
}
/**
* Comments items.
*
* Sets new comments on all items at the current menu
* which match certain criteria, using the "comment" command.
*
* Note that not all menus have a "comment" command. Most notably, those are
* menus without items in them (e.g. "/system identity"), and menus with
* fixed items (e.g. "/ip service").
*
* @param mixed $numbers Targeted items. Can be any criteria
* accepted by {@link static::find()}.
* @param string|resource $comment The new comment to set on the item as a
* string or a seekable stream.
* If a seekable stream is provided, it is sent from its current
* position 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 ResponseCollection Returns the response collection, allowing you
* to inspect the output. Current RouterOS versions don't return
* anything useful, but if future ones do, you can read it right away.
*
* @throws RouterErrorException When the router returns one or more errors.
*/
public function comment($numbers, $comment)
{
$commentRequest = new Request($this->menu . '/comment');
$commentRequest->setArgument('comment', $comment);
$commentRequest->setArgument('numbers', $this->find($numbers));
$responses = $this->client->sendSync($commentRequest);
if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
throw new RouterErrorException(
'Error when commenting items',
RouterErrorException::CODE_COMMENT_ERROR,
null,
$responses
);
}
return $responses;
2017-03-11 02:51:06 +07:00
}
/**
* Sets new values.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Sets new values on certain properties on all items at the current menu
* which match certain criteria.
2023-10-05 16:55:44 +07:00
*
* @param mixed $numbers Items
* to be modified.
* Can be any criteria accepted by {@link static::find()} or NULL
* in case the menu is one without items (e.g. "/system identity").
* @param array<string,string|resource>|array<int,string> $newValues An
* array with the names of each property to set as an array key, and the
* new value as an array value.
2017-03-11 02:51:06 +07:00
* Flags (properties with a value "true" that is interpreted as
* equivalent of "yes" from CLI) can also be specified with a numeric
* index as the array key, and the name of the flag as the array value.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @return ResponseCollection Returns the response collection, allowing you
2023-10-05 16:55:44 +07:00
* to inspect the output. Current RouterOS versions don't return
* anything useful, but if future ones do, you can read it right away.
*
* @throws RouterErrorException When the router returns one or more errors.
2017-03-11 02:51:06 +07:00
*/
public function set($numbers, array $newValues)
{
$setRequest = new Request($this->menu . '/set');
foreach ($newValues as $name => $value) {
if (is_int($name)) {
$setRequest->setArgument($value, 'true');
} else {
$setRequest->setArgument($name, $value);
}
}
if (null !== $numbers) {
$setRequest->setArgument('numbers', $this->find($numbers));
}
2023-10-05 16:55:44 +07:00
$responses = $this->client->sendSync($setRequest);
if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
throw new RouterErrorException(
'Error when setting items',
RouterErrorException::CODE_SET_ERROR,
null,
$responses
);
}
return $responses;
2017-03-11 02:51:06 +07:00
}
/**
* Alias of {@link static::set()}
2023-10-05 16:55:44 +07:00
*
* @param mixed $numbers Items to be modified.
* Can be any criteria accepted by {@link static::find()} or NULL
* in case the menu is one without items (e.g. "/system identity").
* @param string $valueName Name of property to be modified.
* @param string|resource|null $newValue The new value to set.
* If set to NULL, the property is unset.
*
2017-03-11 02:51:06 +07:00
* @return ResponseCollection Returns the response collection, allowing you
2023-10-05 16:55:44 +07:00
* to inspect the output. Current RouterOS versions don't return
* anything useful, but if future ones do, you can read it right away.
*
* @throws RouterErrorException When the router returns one or more errors.
2017-03-11 02:51:06 +07:00
*/
2023-10-05 16:55:44 +07:00
public function edit($numbers, $valueName, $newValue)
2017-03-11 02:51:06 +07:00
{
2023-10-05 16:55:44 +07:00
return null === $newValue
? $this->unsetValue($numbers, $valueName)
: $this->set($numbers, array($valueName => $newValue));
2017-03-11 02:51:06 +07:00
}
/**
* Unsets a value of a specified item at the current menu.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Equivalent of scripting's "unset" command. The "Value" part in the method
* name is added because "unset" is a language construct, and thus a
* reserved word.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @param mixed $numbers Targeted items. Can be any criteria accepted
* by {@link static::find()}.
* @param string $valueName The name of the value you want to unset.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @return ResponseCollection Returns the response collection, allowing you
2023-10-05 16:55:44 +07:00
* to inspect the output. Current RouterOS versions don't return
* anything useful, but if future ones do, you can read it right away.
*
* @throws RouterErrorException When the router returns one or more errors.
2017-03-11 02:51:06 +07:00
*/
public function unsetValue($numbers, $valueName)
{
$unsetRequest = new Request($this->menu . '/unset');
2023-10-05 16:55:44 +07:00
$responses = $this->client->sendSync(
2017-03-11 02:51:06 +07:00
$unsetRequest->setArgument('numbers', $this->find($numbers))
->setArgument('value-name', $valueName)
);
2023-10-05 16:55:44 +07:00
if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
throw new RouterErrorException(
'Error when unsetting value of items',
RouterErrorException::CODE_UNSET_ERROR,
null,
$responses
);
}
return $responses;
2017-03-11 02:51:06 +07:00
}
/**
* Adds a new item at the current menu.
2023-10-05 16:55:44 +07:00
*
* @param array<string,string|resource>|array<int,string> $values Accepts
* one or more items to add to the current menu.
* The data about each item is specified as an array with the names of
* each property as an array key, and the value as an array value.
2017-03-11 02:51:06 +07:00
* Flags (properties with a value "true" that is interpreted as
* equivalent of "yes" from CLI) can also be specified with a numeric
* index as the array key, and the name of the flag as the array value.
2023-10-05 16:55:44 +07:00
* @param array<string,string|resource>|array<int,string> $... Additional
* items.
*
* @return string A comma separated list of the new items' IDs.
*
* @throws RouterErrorException When one or more items were not succesfully
* added. Note that the response collection will include all replies of
* all add commands, including the successful ones, in order.
2017-03-11 02:51:06 +07:00
*/
public function add(array $values)
{
$addRequest = new Request($this->menu . '/add');
2023-10-05 16:55:44 +07:00
$hasErrors = false;
$results = array();
2017-03-11 02:51:06 +07:00
foreach (func_get_args() as $values) {
if (!is_array($values)) {
continue;
}
foreach ($values as $name => $value) {
if (is_int($name)) {
$addRequest->setArgument($value, 'true');
} else {
$addRequest->setArgument($name, $value);
}
}
2023-10-05 16:55:44 +07:00
$result = $this->client->sendSync($addRequest);
if (count($result->getAllOfType(Response::TYPE_ERROR)) > 0) {
$hasErrors = true;
2017-03-11 02:51:06 +07:00
}
2023-10-05 16:55:44 +07:00
$results = array_merge($results, $result->toArray());
2017-03-11 02:51:06 +07:00
$addRequest->removeAllArguments();
}
2023-10-05 16:55:44 +07:00
$this->clearIdCache();
if ($hasErrors) {
throw new RouterErrorException(
'Router returned error when adding items',
RouterErrorException::CODE_ADD_ERROR,
null,
new ResponseCollection($results)
);
}
$results = new ResponseCollection($results);
$idList = '';
foreach ($results->getAllOfType(Response::TYPE_FINAL) as $final) {
$idList .= ',' . strtolower($final->getProperty('ret'));
}
2017-03-11 02:51:06 +07:00
return substr($idList, 1);
}
/**
* Moves items at the current menu before a certain other item.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Moves items before a certain other item. Note that the "move"
* command is not available on all menus. As a rule of thumb, if the order
* of items in a menu is irrelevant to their interpretation, there won't
* be a move command on that menu. If in doubt, check from a terminal.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @param mixed $numbers Targeted items. Can be any criteria accepted
* by {@link static::find()}.
2023-10-05 16:55:44 +07:00
* @param mixed $destination Item before which the targeted items will be
2017-03-11 02:51:06 +07:00
* moved to. Can be any criteria accepted by {@link static::find()}.
* If multiple items match the criteria, the targeted items will move
* above the first match.
2023-10-05 16:55:44 +07:00
* If NULL is given (or this argument is omitted), the targeted items
* will be moved to the bottom of the menu.
*
2017-03-11 02:51:06 +07:00
* @return ResponseCollection Returns the response collection, allowing you
2023-10-05 16:55:44 +07:00
* to inspect the output. Current RouterOS versions don't return
* anything useful, but if future ones do, you can read it right away.
*
* @throws RouterErrorException When the router returns one or more errors.
2017-03-11 02:51:06 +07:00
*/
2023-10-05 16:55:44 +07:00
public function move($numbers, $destination = null)
2017-03-11 02:51:06 +07:00
{
$moveRequest = new Request($this->menu . '/move');
$moveRequest->setArgument('numbers', $this->find($numbers));
2023-10-05 16:55:44 +07:00
if (null !== $destination) {
$destination = $this->find($destination);
if (false !== strpos($destination, ',')) {
$destination = strstr($destination, ',', true);
}
$moveRequest->setArgument('destination', $destination);
2017-03-11 02:51:06 +07:00
}
$this->clearIdCache();
2023-10-05 16:55:44 +07:00
$responses = $this->client->sendSync($moveRequest);
if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
throw new RouterErrorException(
'Error when moving items',
RouterErrorException::CODE_MOVE_ERROR,
null,
$responses
);
}
return $responses;
2017-03-11 02:51:06 +07:00
}
/**
* Counts items at the current menu.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Counts items at the current menu. This executes a dedicated command
* ("print" with a "count-only" argument) on RouterOS, which is why only
* queries are allowed as a criteria, in contrast with
* {@link static::find()}, where numbers and callbacks are allowed also.
2023-10-05 16:55:44 +07:00
*
* @param Query|null $query A query to filter items by.
* Without it, all items are included in the count.
* @param string|resource|null $from A comma separated list of item IDs.
* Any items in the set that still exist at the time of couting
* are included in the final tally. Note that the $query filters this
* set further (i.e. the item must be in the list AND match the $query).
* Leaving the value to NULL means all matching items at the current
* menu are included in the count.
*
2017-03-11 02:51:06 +07:00
* @return int The number of items, or -1 on failure (e.g. if the
* current menu does not have a "print" command or items to be counted).
*/
2023-10-05 16:55:44 +07:00
public function count(Query $query = null, $from = null)
2017-03-11 02:51:06 +07:00
{
2023-10-05 16:55:44 +07:00
$countRequest = new Request(
$this->menu . '/print count-only=""',
$query
);
$countRequest->setArgument('from', $from);
$result = $this->client->sendSync($countRequest)->end()
->getProperty('ret');
2017-03-11 02:51:06 +07:00
if (null === $result) {
return -1;
}
if (Stream::isStream($result)) {
$result = stream_get_contents($result);
}
return (int)$result;
}
/**
* Gets all items in the current menu.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Gets all items in the current menu, using a print request.
2023-10-05 16:55:44 +07:00
*
* @param array<string,string|resource>|array<int,string> $args Additional
* arguments to pass to the request.
2017-03-11 02:51:06 +07:00
* Each array key is the name of the argument, and each array value is
* the value of the argument to be passed.
* Arguments without a value (i.e. empty arguments) can also be
* specified using a numeric key, and the name of the argument as the
* array value.
2023-10-05 16:55:44 +07:00
* The "follow" and "follow-only" arguments are prohibited,
* as they would cause a synchronous request to run forever, without
* allowing the results to be observed.
* If you need to use those arguments, use {@link static::newRequest()},
* and pass the resulting {@link Request} to {@link Client::sendAsync()}.
* The "count-only" argument is also prohibited, as results from it
* would not be consumable. Use {@link static::count()} for that.
* @param Query|null $query A query to
* filter items by.
2017-03-11 02:51:06 +07:00
* NULL to get all items.
2023-10-05 16:55:44 +07:00
*
* @return ResponseCollection A response collection with all
2017-03-11 02:51:06 +07:00
* {@link Response::TYPE_DATA} responses. The collection will be empty
2023-10-05 16:55:44 +07:00
* when there are no matching items.
*
* @throws NotSupportedException If $args contains prohibited arguments
* ("follow", "follow-only" or "count-only").
*
* @throws RouterErrorException When there's an error upon attempting to
* call the "print" command on the specified menu (e.g. if there's no
* "print" command at the menu to begin with).
2017-03-11 02:51:06 +07:00
*/
public function getAll(array $args = array(), Query $query = null)
{
$printRequest = new Request($this->menu . '/print', $query);
foreach ($args as $name => $value) {
if (is_int($name)) {
$printRequest->setArgument($value);
} else {
$printRequest->setArgument($name, $value);
}
}
2023-10-05 16:55:44 +07:00
foreach (array('follow', 'follow-only', 'count-only') as $arg) {
if ($printRequest->getArgument($arg) !== null) {
throw new NotSupportedException(
"The argument '{$arg}' was specified, but is prohibited",
NotSupportedException::CODE_ARG_PROHIBITED,
null,
$arg
);
}
}
2017-03-11 02:51:06 +07:00
$responses = $this->client->sendSync($printRequest);
if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
2023-10-05 16:55:44 +07:00
throw new RouterErrorException(
'Error when reading items',
RouterErrorException::CODE_GETALL_ERROR,
null,
$responses
);
2017-03-11 02:51:06 +07:00
}
return $responses->getAllOfType(Response::TYPE_DATA);
}
/**
* Puts a file on RouterOS's file system.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* Puts a file on RouterOS's file system, regardless of the current menu.
* Note that this is a **VERY VERY VERY** time consuming method - it takes a
* minimum of a little over 4 seconds, most of which are in sleep. It waits
* 2 seconds after a file is first created (required to actually start
* writing to the file), and another 2 seconds after its contents is written
* (performed in order to verify success afterwards).
* Similarly for removal (when $data is NULL) - there are two seconds in
* sleep, used to verify the file was really deleted.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* If you want an efficient way of transferring files, use (T)FTP.
* If you want an efficient way of removing files, use
* {@link static::setMenu()} to move to the "/file" menu, and call
* {@link static::remove()} without performing verification afterwards.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @param string $filename The filename to write data in.
* @param string|resource|null $data The data the file is going to have
* as a string or a seekable stream.
* Setting the value to NULL removes a file of this name.
* If a seekable stream is provided, it is sent from its current
2023-10-05 16:55:44 +07:00
* position to its end, and the pointer is seeked back to its current
2017-03-11 02:51:06 +07:00
* position after sending.
* Non seekable streams, as well as all other types, are casted to a
* string.
* @param bool $overwrite Whether to overwrite the file if
* it exists.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @return bool TRUE on success, FALSE on failure.
*/
public function filePutContents($filename, $data, $overwrite = false)
{
$printRequest = new Request(
'/file/print .proplist=""',
Query::where('name', $filename)
);
$fileExists = count($this->client->sendSync($printRequest)) > 1;
if (null === $data) {
if (!$fileExists) {
return false;
}
$removeRequest = new Request('/file/remove');
$this->client->sendSync(
$removeRequest->setArgument('numbers', $filename)
);
//Required for RouterOS to REALLY remove the file.
sleep(2);
return !(count($this->client->sendSync($printRequest)) > 1);
}
if (!$overwrite && $fileExists) {
return false;
}
$result = $this->client->sendSync(
$printRequest->setArgument('file', $filename)
);
if (count($result->getAllOfType(Response::TYPE_ERROR)) > 0) {
return false;
}
//Required for RouterOS to write the initial file.
sleep(2);
$setRequest = new Request('/file/set contents=""');
$setRequest->setArgument('numbers', $filename);
$this->client->sendSync($setRequest);
$this->client->sendSync($setRequest->setArgument('contents', $data));
//Required for RouterOS to write the file's new contents.
sleep(2);
$fileSize = $this->client->sendSync(
$printRequest->setArgument('file', null)
->setArgument('.proplist', 'size')
)->getProperty('size');
if (Stream::isStream($fileSize)) {
$fileSize = stream_get_contents($fileSize);
}
if (Communicator::isSeekableStream($data)) {
return Communicator::seekableStreamLength($data) == $fileSize;
} else {
return sprintf('%u', strlen((string)$data)) === $fileSize;
2023-10-05 16:55:44 +07:00
}
2017-03-11 02:51:06 +07:00
}
/**
* Gets the contents of a specified file.
2023-10-05 16:55:44 +07:00
*
* @param string $filename The name of the file to get
* the contents of.
* @param string|null $tmpScriptName In order to get the file's contents, a
* script is created at "/system script", the source of which is then
* overwritten with the file's contents, then retrieved from there,
* after which the script is removed.
* If this argument is left NULL, a random string,
* prefixed with the computer's name, is generated and used
* as the script's name.
* To eliminate any possibility of name clashes,
* you can specify your own name instead.
*
* @return string|resource The contents of the file as a string or as
* new PHP temp stream if the underlying
2017-03-11 02:51:06 +07:00
* {@link Client::isStreamingResponses()} is set to TRUE.
2023-10-05 16:55:44 +07:00
*
* @throws RouterErrorException When there's an error with the temporary
* script used to get the file, or if the file doesn't exist.
2017-03-11 02:51:06 +07:00
*/
public function fileGetContents($filename, $tmpScriptName = null)
{
2023-10-05 16:55:44 +07:00
try {
$responses = $this->exec(
':error ("&" . [/file get $filename contents]);',
array('filename' => $filename),
null,
$tmpScriptName
);
throw new RouterErrorException(
'Unable to read file through script (no error returned)',
RouterErrorException::CODE_SCRIPT_FILE_ERROR,
null,
$responses
);
} catch (RouterErrorException $e) {
if ($e->getCode() !== RouterErrorException::CODE_SCRIPT_RUN_ERROR) {
throw $e;
}
$message = $e->getResponses()->getAllOfType(Response::TYPE_ERROR)
->getProperty('message');
if (Stream::isStream($message)) {
$successToken = fread($message, 1/*strlen('&')*/);
if ('&' === $successToken) {
$messageCopy = fopen('php://temp', 'r+b');
stream_copy_to_stream($message, $messageCopy);
rewind($messageCopy);
fclose($message);
return $messageCopy;
}
rewind($message);
} elseif (strpos($message, '&') === 0) {
return substr($message, 1/*strlen('&')*/);
}
throw $e;
2017-03-11 02:51:06 +07:00
}
}
/**
* Performs an action on a bulk of items at the current menu.
2023-10-05 16:55:44 +07:00
*
* @param string $command What command to perform.
* @param array $args Zero or more arguments can be specified,
* each being a criteria.
* If zero arguments are specified, matches all items.
2017-03-11 02:51:06 +07:00
* See {@link static::find()} for a description of what criteria are
* accepted.
2023-10-05 16:55:44 +07:00
*
2017-03-11 02:51:06 +07:00
* @return ResponseCollection Returns the response collection, allowing you
* to inspect errors, if any.
*/
2023-10-05 16:55:44 +07:00
protected function doBulk($command, array $args = array())
2017-03-11 02:51:06 +07:00
{
2023-10-05 16:55:44 +07:00
$bulkRequest = new Request("{$this->menu}/{$command}");
2017-03-11 02:51:06 +07:00
$bulkRequest->setArgument(
'numbers',
call_user_func_array(array($this, 'find'), $args)
);
return $this->client->sendSync($bulkRequest);
}
}