Total Complexity | 100 |
Total Lines | 1130 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Util often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Util, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
60 | class Util implements Countable |
||
61 | { |
||
62 | /** |
||
63 | * The connection to wrap around. |
||
64 | * |
||
65 | * @var Client |
||
66 | */ |
||
67 | protected $client; |
||
68 | |||
69 | /** |
||
70 | * The current menu. |
||
71 | * |
||
72 | * Note that the root menu (only) uses an empty string. |
||
73 | * This is done to enable commands executed at it without special casing it |
||
74 | * at all commands. |
||
75 | * Instead, only {@link static::setMenu()} is special cased. |
||
76 | * |
||
77 | * @var string |
||
78 | */ |
||
79 | protected $menu = ''; |
||
80 | |||
81 | /** |
||
82 | * An array with the numbers of items in the current menu. |
||
83 | * |
||
84 | * Numbers as keys, and the corresponding IDs as values. |
||
85 | * NULL when the cache needs regenerating. |
||
86 | * |
||
87 | * @var array<int,string>|null |
||
88 | */ |
||
89 | protected $idCache = null; |
||
90 | |||
91 | /** |
||
92 | * Creates a new Util instance. |
||
93 | * |
||
94 | * Wraps around a connection to provide convenience methods. |
||
95 | * |
||
96 | * @param Client $client The connection to wrap around. |
||
97 | */ |
||
98 | public function __construct(Client $client) |
||
99 | { |
||
100 | $this->client = $client; |
||
101 | } |
||
102 | |||
103 | /** |
||
104 | * Gets the current menu. |
||
105 | * |
||
106 | * @return string The absolute path to current menu, using API syntax. |
||
107 | */ |
||
108 | public function getMenu() |
||
109 | { |
||
110 | return '' === $this->menu ? '/' : $this->menu; |
||
111 | } |
||
112 | |||
113 | /** |
||
114 | * Sets the current menu. |
||
115 | * |
||
116 | * Sets the current menu. |
||
117 | * |
||
118 | * @param string $newMenu The menu to change to. Can be specified with API |
||
119 | * or CLI syntax and can be either absolute or relative. If relative, |
||
120 | * it's relative to the current menu, which by default is the root. |
||
121 | * |
||
122 | * @return $this The object itself. If an empty string is given for |
||
123 | * a new menu, no change is performed, |
||
124 | * but the ID cache is cleared anyway. |
||
125 | * |
||
126 | * @see static::clearIdCache() |
||
127 | */ |
||
128 | public function setMenu($newMenu) |
||
129 | { |
||
130 | $newMenu = (string)$newMenu; |
||
131 | if ('' !== $newMenu) { |
||
132 | $menuRequest = new Request('/menu'); |
||
133 | if ('/' === $newMenu) { |
||
134 | $this->menu = ''; |
||
135 | } elseif ('/' === $newMenu[0]) { |
||
136 | $this->menu = $menuRequest->setCommand($newMenu)->getCommand(); |
||
137 | } else { |
||
138 | $newMenu = (string)substr( |
||
139 | $menuRequest->setCommand( |
||
140 | '/' . |
||
141 | str_replace('/', ' ', (string)substr($this->menu, 1)) . |
||
142 | ' ' . |
||
143 | str_replace('/', ' ', $newMenu) |
||
144 | . ' ?' |
||
145 | )->getCommand(), |
||
146 | 1, |
||
147 | -2/*strlen('/?')*/ |
||
148 | ); |
||
149 | if ('' !== $newMenu) { |
||
150 | $this->menu = '/' . $newMenu; |
||
151 | } else { |
||
152 | $this->menu = ''; |
||
153 | } |
||
154 | } |
||
155 | } |
||
156 | $this->clearIdCache(); |
||
157 | return $this; |
||
158 | } |
||
159 | |||
160 | /** |
||
161 | * Creates a Request object. |
||
162 | * |
||
163 | * Creates a {@link Request} object, with a command that's at the |
||
164 | * current menu. The request can then be sent using {@link Client}. |
||
165 | * |
||
166 | * @param string $command The command of the request, not including |
||
167 | * the menu. The request will have that command at the current menu. |
||
168 | * @param array $args Arguments of the request. |
||
169 | * Each array key is the name of the argument, and each array value is |
||
170 | * the value of the argument to be passed. |
||
171 | * Arguments without a value (i.e. empty arguments) can also be |
||
172 | * specified using a numeric key, and the name of the argument as the |
||
173 | * array value. |
||
174 | * @param Query|null $query The {@link Query} of the request. |
||
175 | * @param string|null $tag The tag of the request. |
||
176 | * |
||
177 | * @return Request The {@link Request} object. |
||
178 | * |
||
179 | * @throws NotSupportedException On an attempt to call a command in a |
||
180 | * different menu using API syntax. |
||
181 | * @throws InvalidArgumentException On an attempt to call a command in a |
||
182 | * different menu using CLI syntax. |
||
183 | */ |
||
184 | public function newRequest( |
||
185 | $command, |
||
186 | array $args = array(), |
||
187 | Query $query = null, |
||
188 | $tag = null |
||
189 | ) { |
||
190 | if (false !== strpos($command, '/')) { |
||
191 | throw new NotSupportedException( |
||
192 | 'Command tried to go to a different menu', |
||
193 | NotSupportedException::CODE_MENU_MISMATCH, |
||
194 | null, |
||
195 | $command |
||
196 | ); |
||
197 | } |
||
198 | $request = new Request('/menu', $query, $tag); |
||
199 | $request->setCommand("{$this->menu}/{$command}"); |
||
200 | foreach ($args as $name => $value) { |
||
201 | if (is_int($name)) { |
||
202 | $request->setArgument($value); |
||
203 | } else { |
||
204 | $request->setArgument($name, $value); |
||
205 | } |
||
206 | } |
||
207 | return $request; |
||
208 | } |
||
209 | |||
210 | /** |
||
211 | * Executes a RouterOS script. |
||
212 | * |
||
213 | * Executes a RouterOS script, written as a string or a stream. |
||
214 | * Note that in cases of errors, the line numbers will be off, because the |
||
215 | * script is executed at the current menu as context, with the specified |
||
216 | * variables pre declared. This is achieved by prepending 1+count($params) |
||
217 | * lines before your actual script. |
||
218 | * |
||
219 | * @param string|resource $source The source of the script, |
||
220 | * as a string or stream. If a stream is provided, reading starts from |
||
221 | * the current position to the end of the stream, and the pointer stays |
||
222 | * at the end after reading is done. |
||
223 | * @param array<string|int,mixed> $params An array of parameters to make |
||
224 | * available in the script as local variables. |
||
225 | * Variable names are array keys, and variable values are array values. |
||
226 | * Array values are automatically processed with |
||
227 | * {@link static::escapeValue()}. Streams are also supported, and are |
||
228 | * processed in chunks, each with |
||
229 | * {@link static::escapeString()} with all bytes being escaped. |
||
230 | * Processing starts from the current position to the end of the stream, |
||
231 | * and the stream's pointer is left untouched after the reading is done. |
||
232 | * Variables with a value of type "nothing" can be declared with a |
||
233 | * numeric array key and the variable name as the array value |
||
234 | * (that is casted to a string). |
||
235 | * Note that the script's (generated) name is always added as the |
||
236 | * variable "_", which will be inadvertently lost if you overwrite it |
||
237 | * from here. |
||
238 | * @param string|null $policy Allows you to specify a policy the |
||
239 | * script must follow. Has the same format as in terminal. |
||
240 | * If left NULL, the script has no restrictions beyond those imposed by |
||
241 | * the username. |
||
242 | * @param string|null $name The script is executed after being |
||
243 | * saved in "/system script" and is removed after execution. |
||
244 | * If this argument is left NULL, a random string, |
||
245 | * prefixed with the computer's name, is generated and used |
||
246 | * as the script's name. |
||
247 | * To eliminate any possibility of name clashes, |
||
248 | * you can specify your own name instead. |
||
249 | * |
||
250 | * @return ResponseCollection The responses of all requests involved, i.e. |
||
251 | * the add, the run and the remove. |
||
252 | * |
||
253 | * @throws RouterErrorException When there is an error in any step of the |
||
254 | * way. The reponses include all successful commands prior to the error |
||
255 | * as well. If the error occurs during the run, there will also be a |
||
256 | * remove attempt, and the results will include its results as well. |
||
257 | */ |
||
258 | public function exec( |
||
259 | $source, |
||
260 | array $params = array(), |
||
261 | $policy = null, |
||
262 | $name = null |
||
263 | ) { |
||
264 | if (null === $name) { |
||
265 | $name = uniqid(gethostname(), true); |
||
266 | } |
||
267 | |||
268 | $request = new Request('/system/script/add'); |
||
269 | $request->setArgument('name', $name); |
||
270 | $request->setArgument('policy', $policy); |
||
271 | |||
272 | $params += array('_' => $name); |
||
273 | |||
274 | $finalSource = fopen('php://temp', 'r+b'); |
||
275 | fwrite( |
||
276 | $finalSource, |
||
277 | '/' . str_replace('/', ' ', substr($this->menu, 1)). "\n" |
||
278 | ); |
||
279 | Script::append($finalSource, $source, $params); |
||
280 | fwrite($finalSource, "\n"); |
||
281 | rewind($finalSource); |
||
282 | |||
283 | $request->setArgument('source', $finalSource); |
||
284 | $addResult = $this->client->sendSync($request); |
||
285 | |||
286 | if (count($addResult->getAllOfType(Response::TYPE_ERROR)) > 0) { |
||
287 | throw new RouterErrorException( |
||
288 | 'Error when trying to add script', |
||
289 | RouterErrorException::CODE_SCRIPT_ADD_ERROR, |
||
290 | null, |
||
291 | $addResult |
||
292 | ); |
||
293 | } |
||
294 | |||
295 | $request = new Request('/system/script/run'); |
||
296 | $request->setArgument('number', $name); |
||
297 | $runResult = $this->client->sendSync($request); |
||
298 | $request = new Request('/system/script/remove'); |
||
299 | $request->setArgument('numbers', $name); |
||
300 | $removeResult = $this->client->sendSync($request); |
||
301 | |||
302 | $results = new ResponseCollection( |
||
303 | array_merge( |
||
304 | $addResult->toArray(), |
||
305 | $runResult->toArray(), |
||
306 | $removeResult->toArray() |
||
307 | ) |
||
308 | ); |
||
309 | |||
310 | if (count($runResult->getAllOfType(Response::TYPE_ERROR)) > 0) { |
||
311 | throw new RouterErrorException( |
||
312 | 'Error when running script', |
||
313 | RouterErrorException::CODE_SCRIPT_RUN_ERROR, |
||
314 | null, |
||
315 | $results |
||
316 | ); |
||
317 | } |
||
318 | if (count($removeResult->getAllOfType(Response::TYPE_ERROR)) > 0) { |
||
319 | throw new RouterErrorException( |
||
320 | 'Error when removing script', |
||
321 | RouterErrorException::CODE_SCRIPT_REMOVE_ERROR, |
||
322 | null, |
||
323 | $results |
||
324 | ); |
||
325 | } |
||
326 | |||
327 | return $results; |
||
328 | } |
||
329 | |||
330 | /** |
||
331 | * Clears the ID cache. |
||
332 | * |
||
333 | * Normally, the ID cache improves performance when targeting items by a |
||
334 | * number. If you're using both Util's methods and other means (e.g. |
||
335 | * {@link Client} or {@link Util::exec()}) to add/move/remove items, the |
||
336 | * cache may end up being out of date. By calling this method right before |
||
337 | * targeting an item with a number, you can ensure number accuracy. |
||
338 | * |
||
339 | * Note that Util's {@link static::move()} and {@link static::remove()} |
||
340 | * methods automatically clear the cache before returning, while |
||
341 | * {@link static::add()} adds the new item's ID to the cache as the next |
||
342 | * number. A change in the menu also clears the cache. |
||
343 | * |
||
344 | * Note also that the cache is being rebuilt unconditionally every time you |
||
345 | * use {@link static::find()} with a callback. |
||
346 | * |
||
347 | * @return $this The Util object itself. |
||
348 | */ |
||
349 | public function clearIdCache() |
||
350 | { |
||
351 | $this->idCache = null; |
||
352 | return $this; |
||
353 | } |
||
354 | |||
355 | /** |
||
356 | * Gets the current time on the router. |
||
357 | * |
||
358 | * Gets the current time on the router, regardless of the current menu. |
||
359 | * |
||
360 | * If the timezone is one known to both RouterOS and PHP, it will be used |
||
361 | * as the timezone identifier. Otherwise (e.g. "manual"), the current GMT |
||
362 | * offset will be used as a timezone, without any DST awareness. |
||
363 | * |
||
364 | * @return DateTime The current time of the router, as a DateTime object. |
||
365 | */ |
||
366 | public function getCurrentTime() |
||
367 | { |
||
368 | $clock = $this->client->sendSync( |
||
369 | new Request( |
||
370 | '/system/clock/print |
||
371 | .proplist=date,time,time-zone-name,gmt-offset' |
||
372 | ) |
||
373 | )->current(); |
||
374 | $clockParts = array(); |
||
375 | foreach (array( |
||
376 | 'date', |
||
377 | 'time', |
||
378 | 'time-zone-name', |
||
379 | 'gmt-offset' |
||
380 | ) as $clockPart) { |
||
381 | $clockParts[$clockPart] = $clock->getProperty($clockPart); |
||
382 | if (Stream::isStream($clockParts[$clockPart])) { |
||
383 | $clockParts[$clockPart] = stream_get_contents( |
||
384 | $clockParts[$clockPart] |
||
385 | ); |
||
386 | } |
||
387 | } |
||
388 | $datetime = ucfirst(strtolower($clockParts['date'])) . ' ' . |
||
389 | $clockParts['time']; |
||
390 | try { |
||
391 | $result = DateTime::createFromFormat( |
||
392 | 'M/j/Y H:i:s', |
||
393 | $datetime, |
||
394 | new DateTimeZone($clockParts['time-zone-name']) |
||
395 | ); |
||
396 | } catch (E $e) { |
||
397 | $result = DateTime::createFromFormat( |
||
398 | 'M/j/Y H:i:s P', |
||
399 | $datetime . ' ' . $clockParts['gmt-offset'], |
||
400 | new DateTimeZone('UTC') |
||
401 | ); |
||
402 | } |
||
403 | return $result; |
||
404 | } |
||
405 | |||
406 | /** |
||
407 | * Finds the IDs of items at the current menu. |
||
408 | * |
||
409 | * Finds the IDs of items based on specified criteria, and returns them as |
||
410 | * a comma separated list, ready for insertion at a "numbers" argument, |
||
411 | * a print's "from" argument, or an equivalent items targeting argument. |
||
412 | * |
||
413 | * Accepts zero or more criteria as arguments. If zero arguments are |
||
414 | * specified, returns all items' IDs. The value of each criteria can be an |
||
415 | * integer (a number as in Winbox), a {@link Query} object, or a callback. |
||
416 | * Other values (including non existent callback names) are casted to a |
||
417 | * string. A string is treated as a comma separated list of IDs or |
||
418 | * whatever else the menu can accept as a unique identifier. |
||
419 | * Those lists are then normalized (meaning excessive commas are stripped |
||
420 | * and trimmed), and appended to the result. |
||
421 | * |
||
422 | * Callbacks are called for each item, with the item |
||
423 | * as an argument. If the callback returns a true value, the item's ID is |
||
424 | * included in the result. |
||
425 | * |
||
426 | * @return string A comma separated list of all items matching the |
||
427 | * specified criteria. |
||
428 | */ |
||
429 | public function find() |
||
430 | { |
||
431 | if (func_num_args() === 0) { |
||
432 | if (null === $this->idCache) { |
||
433 | $ret = $this->client->sendSync( |
||
434 | new Request($this->menu . '/find') |
||
435 | )->getProperty('ret'); |
||
436 | if (null === $ret) { |
||
437 | $this->idCache = array(); |
||
438 | return ''; |
||
439 | } elseif (!is_string($ret)) { |
||
440 | $ret = stream_get_contents($ret); |
||
441 | } |
||
442 | |||
443 | $idCache = str_replace( |
||
444 | ';', |
||
445 | ',', |
||
446 | strtolower($ret) |
||
447 | ); |
||
448 | $this->idCache = explode(',', $idCache); |
||
449 | return $idCache; |
||
450 | } |
||
451 | return implode(',', $this->idCache); |
||
452 | } |
||
453 | $idList = ''; |
||
454 | foreach (func_get_args() as $criteria) { |
||
455 | if ($criteria instanceof Query) { |
||
456 | foreach ($this->client->sendSync( |
||
457 | new Request($this->menu . '/print .proplist=.id', $criteria) |
||
458 | )->getAllOfType(Response::TYPE_DATA) as $response) { |
||
459 | $newId = $response->getProperty('.id'); |
||
460 | $newId = strtolower( |
||
461 | is_string($newId) |
||
462 | ? $newId |
||
463 | : stream_get_contents($newId) |
||
464 | ); |
||
465 | $idList .= $newId . ','; |
||
466 | } |
||
467 | } elseif (is_callable($criteria)) { |
||
468 | $idCache = array(); |
||
469 | foreach ($this->client->sendSync( |
||
470 | new Request($this->menu . '/print') |
||
471 | )->getAllOfType(Response::TYPE_DATA) as $response) { |
||
472 | $newId = $response->getProperty('.id'); |
||
473 | $newId = strtolower( |
||
474 | is_string($newId) |
||
475 | ? $newId |
||
476 | : stream_get_contents($newId) |
||
477 | ); |
||
478 | if ($criteria($response)) { |
||
479 | $idList .= $newId . ','; |
||
480 | } |
||
481 | $idCache[] = $newId; |
||
482 | } |
||
483 | $this->idCache = $idCache; |
||
484 | } elseif (is_int($criteria)) { |
||
485 | $this->find(); |
||
486 | if (isset($this->idCache[$criteria])) { |
||
487 | $idList .= $this->idCache[$criteria] . ','; |
||
488 | } |
||
489 | } else { |
||
490 | $criteria = (string)$criteria; |
||
491 | if (false === strpos($criteria, ',')) { |
||
492 | $idList .= $criteria . ','; |
||
493 | } else { |
||
494 | $idList .= trim( |
||
495 | preg_replace('/,+/S', ',', $criteria), |
||
496 | ',' |
||
497 | ) . ','; |
||
498 | } |
||
499 | } |
||
500 | } |
||
501 | return rtrim($idList, ','); |
||
502 | } |
||
503 | |||
504 | /** |
||
505 | * Gets a value of a specified item at the current menu. |
||
506 | * |
||
507 | * @param int|string|null|Query $number A number identifying the target |
||
508 | * item. Can also be an ID or (in some menus) name. For menus where |
||
509 | * there are no items (e.g. "/system identity"), you can specify NULL. |
||
510 | * You can also specify a {@link Query}, in which case the first match |
||
511 | * will be considered the target item. |
||
512 | * @param string|resource|null $valueName The name of the value to get. |
||
513 | * If omitted, or set to NULL, gets all properties of the target item. |
||
514 | * |
||
515 | * @return string|resource|null|array The value of the specified |
||
516 | * property as a string or as new PHP temp stream if the underlying |
||
517 | * {@link Client::isStreamingResponses()} is set to TRUE. |
||
518 | * If the property is not set, NULL will be returned. |
||
519 | * If $valueName is NULL, returns all properties as an array, where |
||
520 | * the result is parsed with {@link Script::parseValueToArray()}. |
||
521 | * |
||
522 | * @throws RouterErrorException When the router returns an error response |
||
523 | * (e.g. no such item, invalid property, etc.). |
||
524 | */ |
||
525 | public function get($number, $valueName = null) |
||
526 | { |
||
527 | if ($number instanceof Query) { |
||
528 | $number = explode(',', $this->find($number)); |
||
529 | $number = $number[0]; |
||
530 | } elseif (is_int($number)) { |
||
531 | $this->find(); |
||
532 | if (isset($this->idCache[$number])) { |
||
533 | $number = $this->idCache[$number]; |
||
534 | } else { |
||
535 | throw new RouterErrorException( |
||
536 | 'Unable to resolve number from ID cache (no such item maybe)', |
||
537 | RouterErrorException::CODE_CACHE_ERROR |
||
538 | ); |
||
539 | } |
||
540 | } |
||
541 | |||
542 | $request = new Request($this->menu . '/get'); |
||
543 | $request->setArgument('number', $number); |
||
544 | $request->setArgument('value-name', $valueName); |
||
545 | $responses = $this->client->sendSync($request); |
||
546 | if (Response::TYPE_ERROR === $responses->getType()) { |
||
547 | throw new RouterErrorException( |
||
548 | 'Error getting property', |
||
549 | RouterErrorException::CODE_GET_ERROR, |
||
550 | null, |
||
551 | $responses |
||
552 | ); |
||
553 | } |
||
554 | |||
555 | $result = $responses->getProperty('ret'); |
||
556 | if (Stream::isStream($result)) { |
||
557 | $result = stream_get_contents($result); |
||
558 | } |
||
559 | if (null === $valueName) { |
||
560 | // @codeCoverageIgnoreStart |
||
561 | //Some earlier RouterOS versions use "," instead of ";" as separator |
||
562 | //Newer versions can't possibly enter this condition |
||
563 | if (false === strpos($result, ';') |
||
564 | && preg_match('/^([^=,]+\=[^=,]*)(?:\,(?1))+$/', $result) |
||
565 | ) { |
||
566 | $result = str_replace(',', ';', $result); |
||
567 | } |
||
568 | // @codeCoverageIgnoreEnd |
||
569 | return Script::parseValueToArray('{' . $result . '}'); |
||
570 | } |
||
571 | return $result; |
||
572 | } |
||
573 | |||
574 | /** |
||
575 | * Enables all items at the current menu matching certain criteria. |
||
576 | * |
||
577 | * Zero or more arguments can be specified, each being a criteria. |
||
578 | * If zero arguments are specified, enables all items. |
||
579 | * See {@link static::find()} for a description of what criteria are |
||
580 | * accepted. |
||
581 | * |
||
582 | * @return ResponseCollection Returns the response collection, allowing you |
||
583 | * to inspect the output. Current RouterOS versions don't return |
||
584 | * anything useful, but if future ones do, you can read it right away. |
||
585 | * |
||
586 | * @throws RouterErrorException When the router returns one or more errors. |
||
587 | */ |
||
588 | public function enable() |
||
589 | { |
||
590 | $responses = $this->doBulk('enable', func_get_args()); |
||
591 | if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) { |
||
592 | throw new RouterErrorException( |
||
593 | 'Error when enabling items', |
||
594 | RouterErrorException::CODE_ENABLE_ERROR, |
||
595 | null, |
||
596 | $responses |
||
597 | ); |
||
598 | } |
||
599 | return $responses; |
||
600 | } |
||
601 | |||
602 | /** |
||
603 | * Disables all items at the current menu matching certain criteria. |
||
604 | * |
||
605 | * Zero or more arguments can be specified, each being a criteria. |
||
606 | * If zero arguments are specified, disables all items. |
||
607 | * See {@link static::find()} for a description of what criteria are |
||
608 | * accepted. |
||
609 | * |
||
610 | * @return ResponseCollection Returns the response collection, allowing you |
||
611 | * to inspect the output. Current RouterOS versions don't return |
||
612 | * anything useful, but if future ones do, you can read it right away. |
||
613 | * |
||
614 | * @throws RouterErrorException When the router returns one or more errors. |
||
615 | */ |
||
616 | public function disable() |
||
617 | { |
||
618 | $responses = $this->doBulk('disable', func_get_args()); |
||
619 | if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) { |
||
620 | throw new RouterErrorException( |
||
621 | 'Error when disabling items', |
||
622 | RouterErrorException::CODE_DISABLE_ERROR, |
||
623 | null, |
||
624 | $responses |
||
625 | ); |
||
626 | } |
||
627 | return $responses; |
||
628 | } |
||
629 | |||
630 | /** |
||
631 | * Removes all items at the current menu matching certain criteria. |
||
632 | * |
||
633 | * Zero or more arguments can be specified, each being a criteria. |
||
634 | * If zero arguments are specified, removes all items. |
||
635 | * See {@link static::find()} for a description of what criteria are |
||
636 | * accepted. |
||
637 | * |
||
638 | * @return ResponseCollection Returns the response collection, allowing you |
||
639 | * to inspect the output. Current RouterOS versions don't return |
||
640 | * anything useful, but if future ones do, you can read it right away. |
||
641 | * |
||
642 | * @throws RouterErrorException When the router returns one or more errors. |
||
643 | */ |
||
644 | public function remove() |
||
645 | { |
||
646 | $responses = $this->doBulk('remove', func_get_args()); |
||
647 | $this->clearIdCache(); |
||
648 | if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) { |
||
649 | throw new RouterErrorException( |
||
650 | 'Error when removing items', |
||
651 | RouterErrorException::CODE_REMOVE_ERROR, |
||
652 | null, |
||
653 | $responses |
||
654 | ); |
||
655 | } |
||
656 | return $responses; |
||
657 | } |
||
658 | |||
659 | /** |
||
660 | * Comments items. |
||
661 | * |
||
662 | * Sets new comments on all items at the current menu |
||
663 | * which match certain criteria, using the "comment" command. |
||
664 | * |
||
665 | * Note that not all menus have a "comment" command. Most notably, those are |
||
666 | * menus without items in them (e.g. "/system identity"), and menus with |
||
667 | * fixed items (e.g. "/ip service"). |
||
668 | * |
||
669 | * @param mixed $numbers Targeted items. Can be any criteria |
||
670 | * accepted by {@link static::find()}. |
||
671 | * @param string|resource $comment The new comment to set on the item as a |
||
672 | * string or a seekable stream. |
||
673 | * If a seekable stream is provided, it is sent from its current |
||
674 | * position to its end, and the pointer is seeked back to its current |
||
675 | * position after sending. |
||
676 | * Non seekable streams, as well as all other types, are casted to a |
||
677 | * string. |
||
678 | * |
||
679 | * @return ResponseCollection Returns the response collection, allowing you |
||
680 | * to inspect the output. Current RouterOS versions don't return |
||
681 | * anything useful, but if future ones do, you can read it right away. |
||
682 | * |
||
683 | * @throws RouterErrorException When the router returns one or more errors. |
||
684 | */ |
||
685 | public function comment($numbers, $comment) |
||
686 | { |
||
687 | $commentRequest = new Request($this->menu . '/comment'); |
||
688 | $commentRequest->setArgument('comment', $comment); |
||
689 | $commentRequest->setArgument('numbers', $this->find($numbers)); |
||
690 | $responses = $this->client->sendSync($commentRequest); |
||
691 | if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) { |
||
692 | throw new RouterErrorException( |
||
693 | 'Error when commenting items', |
||
694 | RouterErrorException::CODE_COMMENT_ERROR, |
||
695 | null, |
||
696 | $responses |
||
697 | ); |
||
698 | } |
||
699 | return $responses; |
||
700 | } |
||
701 | |||
702 | /** |
||
703 | * Sets new values. |
||
704 | * |
||
705 | * Sets new values on certain properties on all items at the current menu |
||
706 | * which match certain criteria. |
||
707 | * |
||
708 | * @param mixed $numbers Items |
||
709 | * to be modified. |
||
710 | * Can be any criteria accepted by {@link static::find()} or NULL |
||
711 | * in case the menu is one without items (e.g. "/system identity"). |
||
712 | * @param array<string,string|resource>|array<int,string> $newValues An |
||
713 | * array with the names of each property to set as an array key, and the |
||
714 | * new value as an array value. |
||
715 | * Flags (properties with a value "true" that is interpreted as |
||
716 | * equivalent of "yes" from CLI) can also be specified with a numeric |
||
717 | * index as the array key, and the name of the flag as the array value. |
||
718 | * |
||
719 | * @return ResponseCollection Returns the response collection, allowing you |
||
720 | * to inspect the output. Current RouterOS versions don't return |
||
721 | * anything useful, but if future ones do, you can read it right away. |
||
722 | * |
||
723 | * @throws RouterErrorException When the router returns one or more errors. |
||
724 | */ |
||
725 | public function set($numbers, array $newValues) |
||
726 | { |
||
727 | $setRequest = new Request($this->menu . '/set'); |
||
728 | foreach ($newValues as $name => $value) { |
||
729 | if (is_int($name)) { |
||
730 | $setRequest->setArgument($value, 'true'); |
||
731 | } else { |
||
732 | $setRequest->setArgument($name, $value); |
||
733 | } |
||
734 | } |
||
735 | if (null !== $numbers) { |
||
736 | $setRequest->setArgument('numbers', $this->find($numbers)); |
||
737 | } |
||
738 | $responses = $this->client->sendSync($setRequest); |
||
739 | if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) { |
||
740 | throw new RouterErrorException( |
||
741 | 'Error when setting items', |
||
742 | RouterErrorException::CODE_SET_ERROR, |
||
743 | null, |
||
744 | $responses |
||
745 | ); |
||
746 | } |
||
747 | return $responses; |
||
748 | } |
||
749 | |||
750 | /** |
||
751 | * Sets or unsets a value. |
||
752 | * |
||
753 | * Sets or unsets a value of a single property on all items at the current |
||
754 | * menu which match certain criteria. |
||
755 | * |
||
756 | * @param mixed $numbers Items to be modified. |
||
757 | * Can be any criteria accepted by {@link static::find()} or NULL |
||
758 | * in case the menu is one without items (e.g. "/system identity"). |
||
759 | * @param string $valueName Name of property to be modified. |
||
760 | * @param string|resource|null $newValue The new value to set. |
||
761 | * If set to NULL, the property is unset. |
||
762 | * |
||
763 | * @return ResponseCollection Returns the response collection, allowing you |
||
764 | * to inspect the output. Current RouterOS versions don't return |
||
765 | * anything useful, but if future ones do, you can read it right away. |
||
766 | * |
||
767 | * @throws RouterErrorException When the router returns one or more errors. |
||
768 | */ |
||
769 | public function edit($numbers, $valueName, $newValue) |
||
770 | { |
||
771 | return null === $newValue |
||
772 | ? $this->unsetValue($numbers, $valueName) |
||
773 | : $this->set($numbers, array($valueName => $newValue)); |
||
774 | } |
||
775 | |||
776 | /** |
||
777 | * Unsets a value of a specified item at the current menu. |
||
778 | * |
||
779 | * Equivalent of scripting's "unset" command. The "Value" part in the method |
||
780 | * name is added because "unset" is a language construct, and thus a |
||
781 | * reserved word. |
||
782 | * |
||
783 | * @param mixed $numbers Targeted items. Can be any criteria accepted |
||
784 | * by {@link static::find()}. |
||
785 | * @param string $valueName The name of the value you want to unset. |
||
786 | * |
||
787 | * @return ResponseCollection Returns the response collection, allowing you |
||
788 | * to inspect the output. Current RouterOS versions don't return |
||
789 | * anything useful, but if future ones do, you can read it right away. |
||
790 | * |
||
791 | * @throws RouterErrorException When the router returns one or more errors. |
||
792 | */ |
||
793 | public function unsetValue($numbers, $valueName) |
||
794 | { |
||
795 | $unsetRequest = new Request($this->menu . '/unset'); |
||
796 | $responses = $this->client->sendSync( |
||
797 | $unsetRequest->setArgument('numbers', $this->find($numbers)) |
||
798 | ->setArgument('value-name', $valueName) |
||
799 | ); |
||
800 | if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) { |
||
801 | throw new RouterErrorException( |
||
802 | 'Error when unsetting value of items', |
||
803 | RouterErrorException::CODE_UNSET_ERROR, |
||
804 | null, |
||
805 | $responses |
||
806 | ); |
||
807 | } |
||
808 | return $responses; |
||
809 | } |
||
810 | |||
811 | /** |
||
812 | * Adds a new item at the current menu. |
||
813 | * |
||
814 | * @param array<string,string|resource>|array<int,string> $values Accepts |
||
815 | * one or more items to add to the current menu. |
||
816 | * The data about each item is specified as an array with the names of |
||
817 | * each property as an array key, and the value as an array value. |
||
818 | * Flags (properties with a value "true" that is interpreted as |
||
819 | * equivalent of "yes" from CLI) can also be specified with a numeric |
||
820 | * index as the array key, and the name of the flag as the array value. |
||
821 | * @param array<string,string|resource>|array<int,string> $values,... Additional |
||
822 | * items. |
||
823 | * |
||
824 | * @return string A comma separated list of the new items' IDs. |
||
825 | * |
||
826 | * @throws RouterErrorException When one or more items were not succesfully |
||
827 | * added. Note that the response collection will include all replies of |
||
828 | * all add commands, including the successful ones, in order. |
||
829 | */ |
||
830 | public function add(array $values) |
||
831 | { |
||
832 | $addRequest = new Request($this->menu . '/add'); |
||
833 | $hasErrors = false; |
||
834 | $results = array(); |
||
835 | foreach (func_get_args() as $values) { |
||
836 | if (!is_array($values)) { |
||
837 | continue; |
||
838 | } |
||
839 | foreach ($values as $name => $value) { |
||
840 | if (is_int($name)) { |
||
841 | $addRequest->setArgument($value, 'true'); |
||
842 | } else { |
||
843 | $addRequest->setArgument($name, $value); |
||
844 | } |
||
845 | } |
||
846 | $result = $this->client->sendSync($addRequest); |
||
847 | if (count($result->getAllOfType(Response::TYPE_ERROR)) > 0) { |
||
848 | $hasErrors = true; |
||
849 | } |
||
850 | $results = array_merge($results, $result->toArray()); |
||
851 | $addRequest->removeAllArguments(); |
||
852 | } |
||
853 | |||
854 | $this->clearIdCache(); |
||
855 | if ($hasErrors) { |
||
856 | throw new RouterErrorException( |
||
857 | 'Router returned error when adding items', |
||
858 | RouterErrorException::CODE_ADD_ERROR, |
||
859 | null, |
||
860 | new ResponseCollection($results) |
||
861 | ); |
||
862 | } |
||
863 | $results = new ResponseCollection($results); |
||
864 | $idList = ''; |
||
865 | foreach ($results->getAllOfType(Response::TYPE_FINAL) as $final) { |
||
866 | $idList .= ',' . strtolower($final->getProperty('ret')); |
||
867 | } |
||
868 | return substr($idList, 1); |
||
869 | } |
||
870 | |||
871 | /** |
||
872 | * Moves items at the current menu before a certain other item. |
||
873 | * |
||
874 | * Moves items before a certain other item. Note that the "move" |
||
875 | * command is not available on all menus. As a rule of thumb, if the order |
||
876 | * of items in a menu is irrelevant to their interpretation, there won't |
||
877 | * be a move command on that menu. If in doubt, check from a terminal. |
||
878 | * |
||
879 | * @param mixed $numbers Targeted items. Can be any criteria accepted |
||
880 | * by {@link static::find()}. |
||
881 | * @param mixed $destination Item before which the targeted items will be |
||
882 | * moved to. Can be any criteria accepted by {@link static::find()}. |
||
883 | * If multiple items match the criteria, the targeted items will move |
||
884 | * above the first match. |
||
885 | * If NULL is given (or this argument is omitted), the targeted items |
||
886 | * will be moved to the bottom of the menu. |
||
887 | * |
||
888 | * @return ResponseCollection Returns the response collection, allowing you |
||
889 | * to inspect the output. Current RouterOS versions don't return |
||
890 | * anything useful, but if future ones do, you can read it right away. |
||
891 | * |
||
892 | * @throws RouterErrorException When the router returns one or more errors. |
||
893 | */ |
||
894 | public function move($numbers, $destination = null) |
||
916 | } |
||
917 | |||
918 | /** |
||
919 | * Counts items at the current menu. |
||
920 | * |
||
921 | * Counts items at the current menu. This executes a dedicated command |
||
922 | * ("print" with a "count-only" argument) on RouterOS, which is why only |
||
923 | * queries are allowed as a criteria, in contrast with |
||
924 | * {@link static::find()}, where numbers and callbacks are allowed also. |
||
925 | * |
||
926 | * @param Query|null $query A query to filter items by. |
||
927 | * Without it, all items are included in the count. |
||
928 | * @param string|resource|null $from A comma separated list of item IDs. |
||
929 | * Any items in the set that still exist at the time of couting |
||
930 | * are included in the final tally. Note that the $query filters this |
||
931 | * set further (i.e. the item must be in the list AND match the $query). |
||
932 | * Leaving the value to NULL means all matching items at the current |
||
933 | * menu are included in the count. |
||
934 | * |
||
935 | * @return int The number of items, or -1 on failure (e.g. if the |
||
936 | * current menu does not have a "print" command or items to be counted). |
||
937 | */ |
||
938 | public function count(Query $query = null, $from = null) |
||
939 | { |
||
940 | $countRequest = new Request( |
||
941 | $this->menu . '/print count-only=""', |
||
942 | $query |
||
943 | ); |
||
944 | $countRequest->setArgument('from', $from); |
||
945 | $result = $this->client->sendSync($countRequest)->end() |
||
946 | ->getProperty('ret'); |
||
947 | |||
948 | if (null === $result) { |
||
949 | return -1; |
||
950 | } |
||
951 | if (Stream::isStream($result)) { |
||
952 | $result = stream_get_contents($result); |
||
953 | } |
||
954 | return (int)$result; |
||
955 | } |
||
956 | |||
957 | /** |
||
958 | * Gets all items in the current menu. |
||
959 | * |
||
960 | * Gets all items in the current menu, using a print request. |
||
961 | * |
||
962 | * @param array<string,string|resource>|array<int,string> $args Additional |
||
963 | * arguments to pass to the request. |
||
964 | * Each array key is the name of the argument, and each array value is |
||
965 | * the value of the argument to be passed. |
||
966 | * Arguments without a value (i.e. empty arguments) can also be |
||
967 | * specified using a numeric key, and the name of the argument as the |
||
968 | * array value. |
||
969 | * The "follow" and "follow-only" arguments are prohibited, |
||
970 | * as they would cause a synchronous request to run forever, without |
||
971 | * allowing the results to be observed. |
||
972 | * If you need to use those arguments, use {@link static::newRequest()}, |
||
973 | * and pass the resulting {@link Request} to {@link Client::sendAsync()}. |
||
974 | * The "count-only" argument is also prohibited, as results from it |
||
975 | * would not be consumable. Use {@link static::count()} for that. |
||
976 | * @param Query|null $query A query to |
||
977 | * filter items by. |
||
978 | * NULL to get all items. |
||
979 | * |
||
980 | * @return ResponseCollection A response collection with all |
||
981 | * {@link Response::TYPE_DATA} responses. The collection will be empty |
||
982 | * when there are no matching items. |
||
983 | * |
||
984 | * @throws NotSupportedException If $args contains prohibited arguments |
||
985 | * ("follow", "follow-only" or "count-only"). |
||
986 | * |
||
987 | * @throws RouterErrorException When there's an error upon attempting to |
||
988 | * call the "print" command on the specified menu (e.g. if there's no |
||
989 | * "print" command at the menu to begin with). |
||
990 | */ |
||
991 | public function getAll(array $args = array(), Query $query = null) |
||
992 | { |
||
993 | $printRequest = new Request($this->menu . '/print', $query); |
||
994 | foreach ($args as $name => $value) { |
||
995 | if (is_int($name)) { |
||
996 | $printRequest->setArgument($value); |
||
997 | } else { |
||
998 | $printRequest->setArgument($name, $value); |
||
999 | } |
||
1000 | } |
||
1001 | |||
1002 | foreach (array('follow', 'follow-only', 'count-only') as $arg) { |
||
1003 | if ($printRequest->getArgument($arg) !== null) { |
||
1004 | throw new NotSupportedException( |
||
1005 | "The argument '{$arg}' was specified, but is prohibited", |
||
1006 | NotSupportedException::CODE_ARG_PROHIBITED, |
||
1007 | null, |
||
1008 | $arg |
||
1009 | ); |
||
1010 | } |
||
1011 | } |
||
1012 | $responses = $this->client->sendSync($printRequest); |
||
1013 | |||
1014 | if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) { |
||
1015 | throw new RouterErrorException( |
||
1016 | 'Error when reading items', |
||
1017 | RouterErrorException::CODE_GETALL_ERROR, |
||
1018 | null, |
||
1019 | $responses |
||
1020 | ); |
||
1021 | } |
||
1022 | return $responses->getAllOfType(Response::TYPE_DATA); |
||
1023 | } |
||
1024 | |||
1025 | /** |
||
1026 | * Puts a file on RouterOS's file system. |
||
1027 | * |
||
1028 | * Puts a file on RouterOS's file system, regardless of the current menu. |
||
1029 | * Note that this is a **VERY VERY VERY** time consuming method - it takes a |
||
1030 | * minimum of a little over 4 seconds, most of which are in sleep. It waits |
||
1031 | * 2 seconds after a file is first created (required to actually start |
||
1032 | * writing to the file), and another 2 seconds after its contents is written |
||
1033 | * (performed in order to verify success afterwards). |
||
1034 | * Similarly for removal (when $data is NULL) - there are two seconds in |
||
1035 | * sleep, used to verify the file was really deleted. |
||
1036 | * |
||
1037 | * If you want an efficient way of transferring files, use (T)FTP. |
||
1038 | * If you want an efficient way of removing files, use |
||
1039 | * {@link static::setMenu()} to move to the "/file" menu, and call |
||
1040 | * {@link static::remove()} without performing verification afterwards. |
||
1041 | * |
||
1042 | * @param string $filename The filename to write data in. |
||
1043 | * @param string|resource|null $data The data the file is going to have |
||
1044 | * as a string or a seekable stream. |
||
1045 | * Setting the value to NULL removes a file of this name. |
||
1046 | * If a seekable stream is provided, it is sent from its current |
||
1047 | * position to its end, and the pointer is seeked back to its current |
||
1048 | * position after sending. |
||
1049 | * Non seekable streams, as well as all other types, are casted to a |
||
1050 | * string. |
||
1051 | * @param bool $overwrite Whether to overwrite the file if |
||
1052 | * it exists. |
||
1053 | * |
||
1054 | * @return bool TRUE on success, FALSE on failure. |
||
1055 | */ |
||
1056 | public function filePutContents($filename, $data, $overwrite = false) |
||
1057 | { |
||
1058 | $printRequest = new Request( |
||
1059 | '/file/print .proplist=""', |
||
1060 | Query::where('name', $filename) |
||
1061 | ); |
||
1062 | $fileExists = count($this->client->sendSync($printRequest)) > 1; |
||
1063 | |||
1064 | if (null === $data) { |
||
1065 | if (!$fileExists) { |
||
1066 | return false; |
||
1067 | } |
||
1068 | $removeRequest = new Request('/file/remove'); |
||
1069 | $this->client->sendSync( |
||
1070 | $removeRequest->setArgument('numbers', $filename) |
||
1071 | ); |
||
1072 | //Required for RouterOS to REALLY remove the file. |
||
1073 | sleep(2); |
||
1074 | return !(count($this->client->sendSync($printRequest)) > 1); |
||
1075 | } |
||
1076 | |||
1077 | if (!$overwrite && $fileExists) { |
||
1078 | return false; |
||
1079 | } |
||
1080 | $result = $this->client->sendSync( |
||
1081 | $printRequest->setArgument('file', $filename) |
||
1082 | ); |
||
1083 | if (count($result->getAllOfType(Response::TYPE_ERROR)) > 0) { |
||
1084 | return false; |
||
1085 | } |
||
1086 | //Required for RouterOS to write the initial file. |
||
1087 | sleep(2); |
||
1088 | $setRequest = new Request('/file/set contents=""'); |
||
1089 | $setRequest->setArgument('numbers', $filename); |
||
1090 | $this->client->sendSync($setRequest); |
||
1091 | $this->client->sendSync($setRequest->setArgument('contents', $data)); |
||
1092 | //Required for RouterOS to write the file's new contents. |
||
1093 | sleep(2); |
||
1094 | |||
1095 | $fileSize = $this->client->sendSync( |
||
1096 | $printRequest->setArgument('file', null) |
||
1097 | ->setArgument('.proplist', 'size') |
||
1098 | )->getProperty('size'); |
||
1099 | if (Stream::isStream($fileSize)) { |
||
1100 | $fileSize = stream_get_contents($fileSize); |
||
1101 | } |
||
1102 | if (Communicator::isSeekableStream($data)) { |
||
1103 | return Communicator::seekableStreamLength($data) == $fileSize; |
||
1104 | } else { |
||
1105 | return sprintf('%u', strlen((string)$data)) === $fileSize; |
||
1106 | } |
||
1107 | } |
||
1108 | |||
1109 | /** |
||
1110 | * Gets the contents of a specified file. |
||
1111 | * |
||
1112 | * @param string $filename The name of the file to get |
||
1113 | * the contents of. |
||
1114 | * @param string|null $tmpScriptName In order to get the file's contents, a |
||
1115 | * script is created at "/system script", the source of which is then |
||
1116 | * overwritten with the file's contents, then retrieved from there, |
||
1117 | * after which the script is removed. |
||
1118 | * If this argument is left NULL, a random string, |
||
1119 | * prefixed with the computer's name, is generated and used |
||
1120 | * as the script's name. |
||
1121 | * To eliminate any possibility of name clashes, |
||
1122 | * you can specify your own name instead. |
||
1123 | * |
||
1124 | * @return string|resource The contents of the file as a string or as |
||
1125 | * new PHP temp stream if the underlying |
||
1126 | * {@link Client::isStreamingResponses()} is set to TRUE. |
||
1127 | * |
||
1128 | * @throws RouterErrorException When there's an error with the temporary |
||
1129 | * script used to get the file, or if the file doesn't exist. |
||
1130 | */ |
||
1131 | public function fileGetContents($filename, $tmpScriptName = null) |
||
1132 | { |
||
1133 | try { |
||
1134 | $responses = $this->exec( |
||
1135 | ':error ("&" . [/file get $filename contents]);', |
||
1136 | array('filename' => $filename), |
||
1137 | null, |
||
1138 | $tmpScriptName |
||
1139 | ); |
||
1140 | throw new RouterErrorException( |
||
1141 | 'Unable to read file through script (no error returned)', |
||
1142 | RouterErrorException::CODE_SCRIPT_FILE_ERROR, |
||
1143 | null, |
||
1144 | $responses |
||
1145 | ); |
||
1146 | } catch (RouterErrorException $e) { |
||
1147 | if ($e->getCode() !== RouterErrorException::CODE_SCRIPT_RUN_ERROR) { |
||
1148 | throw $e; |
||
1149 | } |
||
1150 | $message = $e->getResponses()->getAllOfType(Response::TYPE_ERROR) |
||
1151 | ->getProperty('message'); |
||
1152 | if (Stream::isStream($message)) { |
||
1153 | $successToken = fread($message, 1/*strlen('&')*/); |
||
1154 | if ('&' === $successToken) { |
||
1155 | $messageCopy = fopen('php://temp', 'r+b'); |
||
1156 | stream_copy_to_stream($message, $messageCopy); |
||
1157 | rewind($messageCopy); |
||
1158 | fclose($message); |
||
1159 | return $messageCopy; |
||
1160 | } |
||
1161 | rewind($message); |
||
1162 | } elseif (strpos($message, '&') === 0) { |
||
1163 | return substr($message, 1/*strlen('&')*/); |
||
1164 | } |
||
1165 | throw $e; |
||
1166 | } |
||
1167 | } |
||
1168 | |||
1169 | /** |
||
1170 | * Performs an action on a bulk of items at the current menu. |
||
1171 | * |
||
1172 | * @param string $command What command to perform. |
||
1173 | * @param array $args Zero or more arguments can be specified, |
||
1174 | * each being a criteria. |
||
1175 | * If zero arguments are specified, matches all items. |
||
1176 | * See {@link static::find()} for a description of what criteria are |
||
1177 | * accepted. |
||
1178 | * |
||
1179 | * @return ResponseCollection Returns the response collection, allowing you |
||
1180 | * to inspect errors, if any. |
||
1181 | */ |
||
1182 | protected function doBulk($command, array $args = array()) |
||
1190 | } |
||
1191 | } |
||
1192 |