Completed
Push — develop ( 24c2e3...6aa4d1 )
by Vasil
03:08
created

Util::find()   C

Complexity

Conditions 16
Paths 14

Size

Total Lines 74
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 74
rs 5.3586
c 0
b 0
f 0
cc 16
eloc 58
nc 14
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * ~~summary~~
5
 *
6
 * ~~description~~
7
 *
8
 * PHP version 5
9
 *
10
 * @category  Net
11
 * @package   PEAR2_Net_RouterOS
12
 * @author    Vasil Rangelov <[email protected]>
13
 * @copyright 2011 Vasil Rangelov
14
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
15
 * @version   GIT: $Id$
16
 * @link      http://pear2.php.net/PEAR2_Net_RouterOS
17
 */
18
/**
19
 * The namespace declaration.
20
 */
21
namespace PEAR2\Net\RouterOS;
22
23
/**
24
 * Returned from {@link Util::getCurrentTime()}.
25
 */
26
use DateTime;
27
28
/**
29
 * Used at {@link Util::getCurrentTime()} to get the proper time.
30
 */
31
use DateTimeZone;
32
33
/**
34
 * Implemented by this class.
35
 */
36
use Countable;
37
38
/**
39
 * Used to detect streams in various methods of this class.
40
 */
41
use PEAR2\Net\Transmitter\Stream;
42
43
/**
44
 * Used to catch a DateTime exception at {@link Util::getCurrentTime()}.
45
 */
46
use Exception as E;
47
48
/**
49
 * Utility class.
50
 *
51
 * Abstracts away frequently used functionality (particularly CRUD operations)
52
 * in convenient to use methods by wrapping around a connection.
53
 *
54
 * @category Net
55
 * @package  PEAR2_Net_RouterOS
56
 * @author   Vasil Rangelov <[email protected]>
57
 * @license  http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
58
 * @link     http://pear2.php.net/PEAR2_Net_RouterOS
59
 */
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('/,{2,}/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)
895
    {
896
        $moveRequest = new Request($this->menu . '/move');
897
        $moveRequest->setArgument('numbers', $this->find($numbers));
898
        if (null !== $destination) {
899
            $destination = $this->find($destination);
900
            if (false !== strpos($destination, ',')) {
901
                $destination = strstr($destination, ',', true);
902
            }
903
            $moveRequest->setArgument('destination', $destination);
904
        }
905
        $this->clearIdCache();
906
        $responses = $this->client->sendSync($moveRequest);
907
        if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
908
            throw new RouterErrorException(
909
                'Error when moving items',
910
                RouterErrorException::CODE_MOVE_ERROR,
911
                null,
912
                $responses
913
            );
914
        }
915
        return $responses;
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())
1183
    {
1184
        $bulkRequest = new Request("{$this->menu}/{$command}");
1185
        $bulkRequest->setArgument(
1186
            'numbers',
1187
            call_user_func_array(array($this, 'find'), $args)
1188
        );
1189
        return $this->client->sendSync($bulkRequest);
1190
    }
1191
}
1192