Skip to content

Commit f4f24e9

Browse files
committed
Added Util::parseValue();
Modified Util::get() and Util::set() to accept NULL for menus that don't have entries; Reorganized many Unit tests at what was previously RequestHandlingTest.php to use data providers; Added a check about the OpenSSL extension at stub.php; Response::_receive() releases the lock on any exception, not just SocketException; Added a shorter default_socket_timeout(), to avoid needlessly long delays in tests. CS fixes.
1 parent 68e575e commit f4f24e9

12 files changed

+1480
-1023
lines changed

RELEASE-1.0.0b4

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ Brand new way of manipulating data, and listen...
44
- CRUD operations
55
- Support of targeting and finding entries by numbers, just like from terminal
66
- Executing scripts (with the ability to pass typed parameters ala SQL prepared statements)
7-
- Putting and getting files out of RouterOS.
7+
- Putting and getting files out of RouterOS
8+
- Helper methods for converting back and forth between PHP and RouterOS values.
89
* Client::loop() and Client::completeRequest() no longer fail if there's no reply within "default_socket_timeout" seconds. This means you can now use the "listen" command without also setting up something else to keep the connection busy.
910
* Client::loop() now accepts timeouts modeled after stream_select()'s, as opposed to a single float value. As before, the default is "no time limit", but is now specified with NULL instead of 0. Analogous arguments have been added to Response's constructor.
11+
* When receiving, the release lock is released when ANY exception is thrown. Previously, this would be so only in case of SocketException.
1012
* Chnaged the PHAR stub to not fail when reading the hash fails.
11-
* Doc and CS fixes.
13+
* Exceptions now use constants to hold each code.
14+
* Doc and CS fixes, and unit test reorganization.

src/PEAR2/Net/RouterOS/Client.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ public function loop($timeout_s = null, $timeout_us = 0)
559559
}
560560
}
561561
} catch (SocketException $e) {
562-
if ($e->getCode() !== 50000) {
562+
if ($e->getCode() !== SocketException::CODE_NO_DATA) {
563563
// @codeCoverageIgnoreStart
564564
// It's impossible to reliably cause any other SocketException.
565565
// This line is only here in case the unthinkable happens:
@@ -699,16 +699,18 @@ public function isStreamingResponses()
699699
*/
700700
public function close()
701701
{
702-
$result = false;
702+
$result = true;
703703
try {
704-
if (null !== $this->registry) {
705-
$this->registry->setTaglessMode(true);
706-
}
707-
$response = $this->sendSync(new Request('/quit'));
708-
if (null !== $this->registry) {
709-
$this->registry->setTaglessMode(false);
704+
if ($this->com->getTransmitter()->getCrypto() === N::CRYPTO_OFF) {
705+
if (null !== $this->registry) {
706+
$this->registry->setTaglessMode(true);
707+
}
708+
$response = $this->sendSync(new Request('/quit'));
709+
if (null !== $this->registry) {
710+
$this->registry->setTaglessMode(false);
711+
}
712+
$result = $response->getType() === Response::TYPE_FATAL;
710713
}
711-
$result = $response->getType() === Response::TYPE_FATAL;
712714
$result = $result && $this->com->close();
713715
} catch (SocketException $e) {
714716
$result

src/PEAR2/Net/RouterOS/Communicator.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,12 @@ public function __construct(
129129
stream_context_set_option($context, 'ssl', 'ciphers', 'ADH');
130130
}
131131
}
132+
// @codeCoverageIgnoreStart
133+
// The $port is customizable in testing.
132134
if (null === $port) {
133135
$port = $isUnencrypted ? 8728 : 8729;
134136
}
137+
// @codeCoverageIgnoreEnd
135138

136139
try {
137140
$this->trans = new T\TcpClient(

src/PEAR2/Net/RouterOS/Request.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,9 +347,11 @@ protected function parseArgumentString($string)
347347
$token = '';
348348
$this->setArgument($name);
349349
$name = null;
350-
} elseif (
351-
preg_match('/^="(([^\\\"]|\\\"|\\\\)*)"/sS', $string, $matches)
352-
) {
350+
} elseif (preg_match(
351+
'/^="(([^\\\"]|\\\"|\\\\)*)"/sS',
352+
$string,
353+
$matches
354+
)) {
353355
$token = $matches[0];
354356
$this->setArgument(
355357
$name,

src/PEAR2/Net/RouterOS/Response.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
*/
2626
use PEAR2\Net\Transmitter as T;
2727

28+
/**
29+
* Locks are released upon any exception from anywhere.
30+
*/
31+
use Exception as E;
32+
2833
/**
2934
* Represents a RouterOS response.
3035
*
@@ -97,11 +102,11 @@ public function __construct(
97102
->lock(T\Stream::DIRECTION_RECEIVE);
98103
try {
99104
$this->_receive($com, $asStream, $timeout_s, $timeout_us);
100-
$com->getTransmitter()->lock($old, true);
101-
} catch (SocketException $e) {
105+
} catch (E $e) {
102106
$com->getTransmitter()->lock($old, true);
103107
throw $e;
104108
}
109+
$com->getTransmitter()->lock($old, true);
105110
} else {
106111
$this->_receive($com, $asStream, $timeout_s, $timeout_us);
107112
}
@@ -160,8 +165,7 @@ private function _receive(
160165
if (!$com->getTransmitter()->isDataAwaiting(
161166
$timeout_s,
162167
$timeout_us
163-
)
164-
) {
168+
)) {
165169
throw new SocketException(
166170
'No data within the time limit',
167171
SocketException::CODE_NO_DATA

src/PEAR2/Net/RouterOS/Util.php

Lines changed: 130 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,114 @@ class Util
5959
* keys, and the corresponding IDs as values.
6060
*/
6161
protected $idCache = null;
62+
63+
/**
64+
* Parses a value from a RouterOS scripting context.
65+
*
66+
* Turns a value from RouterOS into an equivalent PHP value, based on
67+
* determining the type in the same way RouterOS would determine it for a
68+
* literal.
69+
*
70+
* This method is intended to be the very opposite of {@link escapeValue()}.
71+
* That is, results from that method, if given to this method, should
72+
* produce equivalent results.
73+
*
74+
* @param string $value The value to be parsed. Must be a literal of a
75+
* value, e.g. what {@link escapeValue()} will give you.
76+
*
77+
* @return mixed Depending on RouterOS type detected:
78+
* - "nil" or "nothing" - NULL.
79+
* - "number" - int or double for large values.
80+
* - "bool" - a boolean.
81+
* - "time" - a {@link DateInterval} object.
82+
* - "array" - an array, with the values processed recursively.
83+
* - "str" - a string.
84+
* - Unrecognized type - treated as an unquoted string.
85+
*/
86+
public static function parseValue($value)
87+
{
88+
$value = (string)$value;
89+
90+
if ('' === $value) {
91+
return null;
92+
} elseif (in_array($value, array('true', 'false', 'yes', 'no'), true)) {
93+
return $value === 'true' || $value === 'yes';
94+
} elseif ($value === (string)($num = (int)$value)
95+
|| $value === (string)($num = (double)$value)
96+
) {
97+
return $num;
98+
} elseif (preg_match(
99+
'/^
100+
(?:(\d+)w)?
101+
(?:(\d+)d)?
102+
(?:(\d\d)\:)?
103+
(\d\d)\:
104+
(\d\d(:\.\d{1,6})?)
105+
$/x',
106+
$value,
107+
$time
108+
)) {
109+
$days = isset($time[2]) ? (int)$time[2] : 0;
110+
if (isset($time[1])) {
111+
$days += 7 * (int)$time[1];
112+
}
113+
if ('' === $time[3]) {
114+
$time[3] = 0;
115+
}
116+
return new DateInterval(
117+
"P{$days}DT{$time[3]}H{$time[4]}M{$time[5]}S"
118+
);
119+
} elseif (('"' === $value[0]) && substr(strrev($value), 0, 1) === '"') {
120+
return str_replace(
121+
array('\"', '\\\\', "\\\n", "\\\r\n", "\\\r"),
122+
array('"', '\\'),
123+
substr($value, 1, -1)
124+
);
125+
} elseif ('{' === $value[0]) {
126+
$len = strlen($value);
127+
if ($value[$len - 1] === '}') {
128+
$value = substr($value, 1, -1);
129+
if ('' === $value) {
130+
return array();
131+
}
132+
$parsedValue = preg_split(
133+
'/
134+
(\"[^"]*\")
135+
|
136+
(\{[^{}]*(?2)?\})
137+
|
138+
([^;]+)
139+
/sx',
140+
$value,
141+
null,
142+
PREG_SPLIT_DELIM_CAPTURE
143+
);
144+
$result = array();
145+
foreach ($parsedValue as $token) {
146+
if ('' === $token || ';' === $token) {
147+
continue;
148+
}
149+
$result[] = static::parseValue($token);
150+
}
151+
return $result;
152+
}
153+
}
154+
return $value;
155+
}
62156

63157
/**
64158
* Escapes a value for a RouterOS scripting context.
65159
*
66-
* Turns any PHP value into an equivalent whole value that can be inserted
67-
* as part of a RouterOS script.
160+
* Turns any native PHP value into an equivalent whole value that can be
161+
* inserted as part of a RouterOS script.
162+
*
163+
* DateTime and DateInterval objects will be casted to RouterOS' "time"
164+
* type. A DateTime object will be converted to a time relative to the UNIX
165+
* epoch time. Note that if a DateInterval does not have the "days" property
166+
* ("a" in formatting), then its months and years will be ignored, because
167+
* they can't be unambigiously converted to a "time" value.
168+
*
169+
* Unrecognized types are casted to strings.
68170
*
69171
* @param mixed $value The value to be escaped.
70172
*
@@ -85,7 +187,7 @@ public static function escapeValue($value)
85187
break;
86188
case 'array':
87189
if (0 === count($value)) {
88-
$value = '{}';
190+
$value = '({})';
89191
break;
90192
}
91193
$result = '';
@@ -105,13 +207,11 @@ public static function escapeValue($value)
105207
}
106208
if ($value instanceof DateInterval) {
107209
if (false === $value->days || $value->days < 0) {
108-
$value = $value->format('%r')
109-
. ($value->y * 365 + $value->m * 12 + $value->d)
110-
. $value->format('d%H:%I:%S');
210+
$value = $value->format('%r%dd%H:%I:%S');
111211
} else {
112212
$value = $value->format('%r%ad%H:%I:%S');
113213
}
114-
if (isset($usec)) {
214+
if (strpos('.', $value) === false && isset($usec)) {
115215
$value .= '.' . $usec;
116216
}
117217
break;
@@ -226,11 +326,10 @@ public function changeMenu($newMenu = '')
226326
* @param string $source A script to execute.
227327
* @param array $params An array of local variables to make available in
228328
* the script. Variable names are array keys, and variable values are
229-
* array values. Note that the script's (generated) name is always added
230-
* as the variable "_", which you can overwrite from here.
231-
* Native PHP types will be converted to their RouterOS equivalents.
232-
* DateTime and DateInterval objects will be casted to RouterOS' "time"
233-
* type. Other types are casted to strings.
329+
* array values. Array values are automatically processed with
330+
* {@link escapeValue()}.
331+
* Note that the script's (generated) name is always added as the
332+
* variable "_", which you can overwrite from here.
234333
* @param string $policy Allows you to specify a policy the script must
235334
* follow. Has the same format as in terminal. If left NULL, the script
236335
* has no restrictions.
@@ -369,9 +468,10 @@ public function find()
369468
/**
370469
* Gets a value of a specified entry at the current menu.
371470
*
372-
* @param int $number A number identifying the entry you're
373-
* targeting. Can also be an ID or (in some menus) name.
374-
* @param string $value_name The name of the value you want to get.
471+
* @param int|string|null $number A number identifying the entry you're
472+
* targeting. Can also be an ID or (in some menus) name. For menus where
473+
* there are no entries (e.g. "/system identity"), you can specify NULL.
474+
* @param string $value_name The name of the value you want to get.
375475
*
376476
* @return string|null|bool The value of the specified property. If the
377477
* property is not set, NULL will be returned. If no such entry exists,
@@ -388,11 +488,13 @@ public function get($number, $value_name)
388488
}
389489
}
390490

391-
$number = (string)$number;
392-
$request = new Request(
393-
$this->menu . '/print',
394-
Query::where('.id', $number)->orWhere('name', $number)
395-
);
491+
$request = new Request($this->menu . '/print');
492+
if (null !== $number) {
493+
$number = (string)$number;
494+
$request->setQuery(
495+
Query::where('.id', $number)->orWhere('name', $number)
496+
);
497+
}
396498
$request->setArgument('.proplist', $value_name);
397499
$responses = $this->client->sendSync($request)
398500
->getAllOfType(Response::TYPE_DATA);
@@ -457,7 +559,8 @@ public function remove()
457559
* which match certain criteria.
458560
*
459561
* @param mixed $numbers Targeted entries. Can be any criteria accepted by
460-
* {@link find()}.
562+
* {@link find()} or NULL in case the menu is one without entries
563+
* (e.g. "/system identity").
461564
* @param array $newValues An array with the names of each property to set
462565
* as an array key, and the new value as an array value.
463566
*
@@ -470,9 +573,10 @@ public function set($numbers, array $newValues)
470573
foreach ($newValues as $name => $value) {
471574
$setRequest->setArgument($name, $value);
472575
}
473-
return $this->client->sendSync(
474-
$setRequest->setArgument('numbers', $this->find($numbers))
475-
);
576+
if (null !== $numbers) {
577+
$setRequest->setArgument('numbers', $this->find($numbers));
578+
}
579+
return $this->client->sendSync($setRequest);
476580
}
477581

478582
/**
@@ -516,11 +620,11 @@ public function unsetValue($numbers, $value_name)
516620
/**
517621
* Adds a new entry at the current menu.
518622
*
519-
* @param array $values Accepts one or more entries to add to the
623+
* @param array $values Accepts one or more entries to add to the
520624
* current menu. The data about each entry is specified as an array with
521625
* the names of each property as an array key, and the value as an array
522626
* value.
523-
* @param array $values,... Additional entries.
627+
* @param array $... Additional entries.
524628
*
525629
* @return string A comma separated list of the new entries' IDs.
526630
*/

0 commit comments

Comments
 (0)