Util::get()   B
last analyzed

Complexity

Conditions 10
Paths 23

Size

Total Lines 57
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 10
eloc 36
c 5
b 1
f 0
nc 23
nop 2
dl 0
loc 57
rs 7.6666

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 (strpos($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
                }
440
441
                if (!is_string($ret)) {
442
                    $ret = stream_get_contents($ret);
443
                }
444
445
                $idCache = str_replace(
446
                    ';',
447
                    ',',
448
                    strtolower($ret)
449
                );
450
                $this->idCache = explode(',', $idCache);
451
                return $idCache;
452
            }
453
            return implode(',', $this->idCache);
454
        }
455
        $idList = '';
456
        foreach (func_get_args() as $criteria) {
457
            if ($criteria instanceof Query) {
458
                /** @var Response $response */
459
                foreach ($this->client->sendSync(
460
                    new Request($this->menu . '/print .proplist=.id', $criteria)
461
                )->getAllOfType(Response::TYPE_DATA) as $response) {
462
                    $newId = $response->getProperty('.id');
463
                    $newId = strtolower(
464
                        is_string($newId)
465
                        ? $newId
466
                        : stream_get_contents($newId)
467
                    );
468
                    $idList .= $newId . ',';
469
                }
470
            } elseif (is_callable($criteria)) {
471
                $idCache = array();
472
                /** @var Response $response */
473
                foreach ($this->client->sendSync(
474
                    new Request($this->menu . '/print')
475
                )->getAllOfType(Response::TYPE_DATA) as $response) {
476
                    $newId = $response->getProperty('.id');
477
                    $newId = strtolower(
478
                        is_string($newId)
479
                        ? $newId
480
                        : stream_get_contents($newId)
481
                    );
482
                    if ($criteria($response)) {
483
                        $idList .= $newId . ',';
484
                    }
485
                    $idCache[] = $newId;
486
                }
487
                $this->idCache = $idCache;
488
            } elseif (is_int($criteria)) {
489
                $this->find();
490
                if (isset($this->idCache[$criteria])) {
491
                    $idList .= $this->idCache[$criteria] . ',';
492
                }
493
            } else {
494
                $criteria = (string)$criteria;
495
                if (false === strpos($criteria, ',')) {
496
                    $idList .= $criteria . ',';
497
                } else {
498
                    $idList .= trim(
499
                        preg_replace('/,+/S', ',', $criteria),
500
                        ','
501
                    ) . ',';
502
                }
503
            }
504
        }
505
        return rtrim($idList, ',');
506
    }
507
508
    /**
509
     * Gets a value of a specified item at the current menu.
510
     *
511
     * @param int|string|null|Query $number    A number identifying the target
512
     *     item. Can also be an ID or (in some menus) name. For menus where
513
     *     there are no items (e.g. "/system identity"), you can specify NULL.
514
     *     You can also specify a {@link Query}, in which case the first match
515
     *     will be considered the target item.
516
     * @param string|resource|null  $valueName The name of the value to get.
517
     *     If omitted, or set to NULL, gets all properties of the target item.
518
     *
519
     * @return string|resource|null|array The value of the specified
520
     *     property as a string or as new PHP temp stream if the underlying
521
     *     {@link Client::isStreamingResponses()} is set to TRUE.
522
     *     If the property is not set, NULL will be returned.
523
     *     If $valueName is NULL, returns all properties as an array, where
524
     *     the result is parsed with {@link Script::parseValueToArray()}.
525
     *
526
     * @throws RouterErrorException When the router returns an error response
527
     *     (e.g. no such item, invalid property, etc.).
528
     *     This exception is also thrown for a query that has no match or
529
     *     if $numbers is an int not found in the ID cache, which most often
530
     *     happens if there is no such item in the first place.
531
     */
532
    public function get($number, $valueName = null)
533
    {
534
        if ($number instanceof Query) {
535
            /** @noinspection CallableParameterUseCaseInTypeContextInspection */
536
            $number = explode(',', $this->find($number));
537
            $number = $number[0];
538
            if ('' === $number) {
539
                throw new RouterErrorException(
540
                    'Query did not match an item',
541
                    RouterErrorException::CODE_GET_LOOKUP_ERROR
542
                );
543
            }
544
        } elseif (is_int($number)) {
545
            $this->find();
546
            if (isset($this->idCache[$number])) {
547
                $number = $this->idCache[$number];
548
            } else {
549
                throw new RouterErrorException(
550
                    <<< 'EOT'
551
Unable to resolve number from ID cache (no such item maybe)
552
EOT
553
                    ,
554
                    RouterErrorException::CODE_CACHE_ERROR
555
                );
556
            }
557
        }
558
559
        $request = new Request($this->menu . '/get');
560
        $request->setArgument('number', $number);
561
        $request->setArgument('value-name', $valueName);
562
        $responses = $this->client->sendSync($request);
563
        if (Response::TYPE_ERROR === $responses->getType()) {
564
            throw new RouterErrorException(
565
                'Error getting property',
566
                RouterErrorException::CODE_GET_ERROR,
567
                null,
568
                $responses
569
            );
570
        }
571
572
        $result = $responses->getProperty('ret');
573
        if (Stream::isStream($result)) {
574
            $result = stream_get_contents($result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type string; however, parameter $stream of stream_get_contents() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

574
            $result = stream_get_contents(/** @scrutinizer ignore-type */ $result);
Loading history...
575
        }
576
        if (null === $valueName) {
577
            // @codeCoverageIgnoreStart
578
            //Some earlier RouterOS versions use "," instead of ";" as separator
579
            //Newer versions can't possibly enter this condition
580
            if (false === strpos($result, ';')
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type null and resource; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

580
            if (false === strpos(/** @scrutinizer ignore-type */ $result, ';')
Loading history...
581
                && preg_match('/^([^=,]+\=[^=,]*)(?:\,(?1))+$/', $result)
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type null and resource; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

581
                && preg_match('/^([^=,]+\=[^=,]*)(?:\,(?1))+$/', /** @scrutinizer ignore-type */ $result)
Loading history...
582
            ) {
583
                $result = str_replace(',', ';', $result);
584
            }
585
            // @codeCoverageIgnoreEnd
586
            return Script::parseValueToArray('{' . $result . '}');
587
        }
588
        return $result;
589
    }
590
591
    /**
592
     * Enables all items at the current menu matching certain criteria.
593
     *
594
     * Zero or more arguments can be specified, each being a criteria.
595
     * If zero arguments are specified, enables all items.
596
     * See {@link static::find()} for a description of what criteria are
597
     * accepted.
598
     *
599
     * @return ResponseCollection Returns the response collection, allowing you
600
     *     to inspect the output. Current RouterOS versions don't return
601
     *     anything useful, but if future ones do, you can read it right away.
602
     *
603
     * @throws RouterErrorException When the router returns one or more errors.
604
     */
605
    public function enable()
606
    {
607
        $responses = $this->doBulk('enable', func_get_args());
608
        if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
609
            throw new RouterErrorException(
610
                'Error when enabling items',
611
                RouterErrorException::CODE_ENABLE_ERROR,
612
                null,
613
                $responses
614
            );
615
        }
616
        return $responses;
617
    }
618
619
    /**
620
     * Disables all items at the current menu matching certain criteria.
621
     *
622
     * Zero or more arguments can be specified, each being a criteria.
623
     * If zero arguments are specified, disables all items.
624
     * See {@link static::find()} for a description of what criteria are
625
     * accepted.
626
     *
627
     * @return ResponseCollection Returns the response collection, allowing you
628
     *     to inspect the output. Current RouterOS versions don't return
629
     *     anything useful, but if future ones do, you can read it right away.
630
     *
631
     * @throws RouterErrorException When the router returns one or more errors.
632
     */
633
    public function disable()
634
    {
635
        $responses = $this->doBulk('disable', func_get_args());
636
        if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
637
            throw new RouterErrorException(
638
                'Error when disabling items',
639
                RouterErrorException::CODE_DISABLE_ERROR,
640
                null,
641
                $responses
642
            );
643
        }
644
        return $responses;
645
    }
646
647
    /**
648
     * Removes all items at the current menu matching certain criteria.
649
     *
650
     * Zero or more arguments can be specified, each being a criteria.
651
     * If zero arguments are specified, removes all items.
652
     * See {@link static::find()} for a description of what criteria are
653
     * accepted.
654
     *
655
     * @return ResponseCollection Returns the response collection, allowing you
656
     *     to inspect the output. Current RouterOS versions don't return
657
     *     anything useful, but if future ones do, you can read it right away.
658
     *
659
     * @throws RouterErrorException When the router returns one or more errors.
660
     */
661
    public function remove()
662
    {
663
        $responses = $this->doBulk('remove', func_get_args());
664
        $this->clearIdCache();
665
        if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
666
            throw new RouterErrorException(
667
                'Error when removing items',
668
                RouterErrorException::CODE_REMOVE_ERROR,
669
                null,
670
                $responses
671
            );
672
        }
673
        return $responses;
674
    }
675
676
    /**
677
     * Comments items.
678
     *
679
     * Sets new comments on all items at the current menu
680
     * which match certain criteria, using the "comment" command.
681
     *
682
     * Note that not all menus have a "comment" command. Most notably, those are
683
     * menus without items in them (e.g. "/system identity"), and menus with
684
     * fixed items (e.g. "/ip service").
685
     *
686
     * @param mixed           $numbers Targeted items. Can be any criteria
687
     *     accepted by {@link static::find()}.
688
     * @param string|resource $comment The new comment to set on the item as a
689
     *     string or a seekable stream.
690
     *     If a seekable stream is provided, it is sent from its current
691
     *     position to its end, and the pointer is seeked back to its current
692
     *     position after sending.
693
     *     Non seekable streams, as well as all other types, are casted to a
694
     *     string.
695
     *
696
     * @return ResponseCollection Returns the response collection, allowing you
697
     *     to inspect the output. Current RouterOS versions don't return
698
     *     anything useful, but if future ones do, you can read it right away.
699
     *
700
     * @throws RouterErrorException When the router returns one or more errors.
701
     */
702
    public function comment($numbers, $comment)
703
    {
704
        $commentRequest = new Request($this->menu . '/comment');
705
        $commentRequest->setArgument('comment', $comment);
706
        $commentRequest->setArgument('numbers', $this->find($numbers));
707
        $responses = $this->client->sendSync($commentRequest);
708
        if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
709
            throw new RouterErrorException(
710
                'Error when commenting items',
711
                RouterErrorException::CODE_COMMENT_ERROR,
712
                null,
713
                $responses
714
            );
715
        }
716
        return $responses;
717
    }
718
719
    /**
720
     * Sets new values.
721
     *
722
     * Sets new values on certain properties on all items at the current menu
723
     * which match certain criteria.
724
     *
725
     * @param mixed                                           $numbers   Items
726
     *     to be modified.
727
     *     Can be any criteria accepted by {@link static::find()} or NULL
728
     *     in case the menu is one without items (e.g. "/system identity").
729
     * @param array<string,string|resource>|array<int,string> $newValues An
730
     *     array with the names of each property to set as an array key, and the
731
     *     new value as an array value.
732
     *     Flags (properties with a value "true" that is interpreted as
733
     *     equivalent of "yes" from CLI) can also be specified with a numeric
734
     *     index as the array key, and the name of the flag as the array value.
735
     *
736
     * @return ResponseCollection Returns the response collection, allowing you
737
     *     to inspect the output. Current RouterOS versions don't return
738
     *     anything useful, but if future ones do, you can read it right away.
739
     *
740
     * @throws RouterErrorException When the router returns one or more errors.
741
     */
742
    public function set($numbers, array $newValues)
743
    {
744
        $setRequest = new Request($this->menu . '/set');
745
        foreach ($newValues as $name => $value) {
746
            if (is_int($name)) {
747
                $setRequest->setArgument($value, 'true');
748
            } else {
749
                $setRequest->setArgument($name, $value);
750
            }
751
        }
752
        if (null !== $numbers) {
753
            $setRequest->setArgument('numbers', $this->find($numbers));
754
        }
755
        $responses = $this->client->sendSync($setRequest);
756
        if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
757
            throw new RouterErrorException(
758
                'Error when setting items',
759
                RouterErrorException::CODE_SET_ERROR,
760
                null,
761
                $responses
762
            );
763
        }
764
        return $responses;
765
    }
766
767
    /**
768
     * Sets or unsets a value.
769
     *
770
     * Sets or unsets a value of a single property on all items at the current
771
     * menu which match certain criteria.
772
     *
773
     * @param mixed                $numbers   Items to be modified.
774
     *     Can be any criteria accepted by {@link static::find()} or NULL
775
     *     in case the menu is one without items (e.g. "/system identity").
776
     * @param string               $valueName Name of property to be modified.
777
     * @param string|resource|null $newValue  The new value to set.
778
     *     If set to NULL, the property is unset.
779
     *
780
     * @return ResponseCollection Returns the response collection, allowing you
781
     *     to inspect the output. Current RouterOS versions don't return
782
     *     anything useful, but if future ones do, you can read it right away.
783
     *
784
     * @throws RouterErrorException When the router returns one or more errors.
785
     */
786
    public function edit($numbers, $valueName, $newValue)
787
    {
788
        return null === $newValue
789
            ? $this->unsetValue($numbers, $valueName)
790
            : $this->set($numbers, array($valueName => $newValue));
791
    }
792
793
    /**
794
     * Unsets a value of a specified item at the current menu.
795
     *
796
     * Equivalent of scripting's "unset" command. The "Value" part in the method
797
     * name is added because "unset" is a language construct, and thus a
798
     * reserved word.
799
     *
800
     * @param mixed  $numbers   Targeted items. Can be any criteria accepted
801
     *     by {@link static::find()}.
802
     * @param string $valueName The name of the value you want to unset.
803
     *
804
     * @return ResponseCollection Returns the response collection, allowing you
805
     *     to inspect the output. Current RouterOS versions don't return
806
     *     anything useful, but if future ones do, you can read it right away.
807
     *
808
     * @throws RouterErrorException When the router returns one or more errors.
809
     */
810
    public function unsetValue($numbers, $valueName)
811
    {
812
        $unsetRequest = new Request($this->menu . '/unset');
813
        $responses = $this->client->sendSync(
814
            $unsetRequest->setArgument('numbers', $this->find($numbers))
815
                ->setArgument('value-name', $valueName)
816
        );
817
        if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
818
            throw new RouterErrorException(
819
                'Error when unsetting value of items',
820
                RouterErrorException::CODE_UNSET_ERROR,
821
                null,
822
                $responses
823
            );
824
        }
825
        return $responses;
826
    }
827
828
    /**
829
     * Adds a new item at the current menu.
830
     *
831
     * @param array<string,string|resource>|array<int,string> $values     Accept
832
     *     one or more items to add to the current menu.
833
     *     The data about each item is specified as an array with the names of
834
     *     each property as an array key, and the value as an array value.
835
     *     Flags (properties with a value "true" that is interpreted as
836
     *     equivalent of "yes" from CLI) can also be specified with a numeric
837
     *     index as the array key, and the name of the flag as the array value.
838
     * @param array<string,string|resource>|array<int,string> $values,... More
839
     *     items.
840
     *
841
     * @return string A comma separated list of the new items' IDs.
842
     *
843
     * @throws RouterErrorException When one or more items were not succesfully
844
     *     added. Note that the response collection will include all replies of
845
     *     all add commands, including the successful ones, in order.
846
     */
847
    public function add(array $values)
848
    {
849
        $addRequest = new Request($this->menu . '/add');
850
        $hasErrors = false;
851
        $results = array();
852
        foreach (func_get_args() as $values) {
853
            if (!is_array($values)) {
854
                continue;
855
            }
856
            foreach ($values as $name => $value) {
857
                if (is_int($name)) {
858
                    $addRequest->setArgument($value, 'true');
859
                } else {
860
                    $addRequest->setArgument($name, $value);
861
                }
862
            }
863
            $result = $this->client->sendSync($addRequest);
864
            if (count($result->getAllOfType(Response::TYPE_ERROR)) > 0) {
865
                $hasErrors = true;
866
            }
867
            $results = array_merge($results, $result->toArray());
868
            $addRequest->removeAllArguments();
869
        }
870
871
        $this->clearIdCache();
872
        if ($hasErrors) {
873
            throw new RouterErrorException(
874
                'Router returned error when adding items',
875
                RouterErrorException::CODE_ADD_ERROR,
876
                null,
877
                new ResponseCollection($results)
878
            );
879
        }
880
        $results = new ResponseCollection($results);
881
        $idList = '';
882
        foreach ($results->getAllOfType(Response::TYPE_FINAL) as $final) {
883
            $idList .= ',' . strtolower($final->getProperty('ret'));
0 ignored issues
show
Bug introduced by
It seems like $final->getProperty('ret') can also be of type null and resource; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

883
            $idList .= ',' . strtolower(/** @scrutinizer ignore-type */ $final->getProperty('ret'));
Loading history...
884
        }
885
        return substr($idList, 1);
886
    }
887
888
    /**
889
     * Moves items at the current menu before a certain other item.
890
     *
891
     * Moves items before a certain other item. Note that the "move"
892
     * command is not available on all menus. As a rule of thumb, if the order
893
     * of items in a menu is irrelevant to their interpretation, there won't
894
     * be a move command on that menu. If in doubt, check from a terminal.
895
     *
896
     * @param mixed $numbers     Targeted items. Can be any criteria accepted
897
     *     by {@link static::find()}.
898
     * @param mixed $destination Item before which the targeted items will be
899
     *     moved to. Can be any criteria accepted by {@link static::find()}.
900
     *     If multiple items match the criteria, the targeted items will move
901
     *     above the first match.
902
     *     If NULL is given (or this argument is omitted), the targeted items
903
     *     will be moved to the bottom of the menu.
904
     *
905
     * @return ResponseCollection Returns the response collection, allowing you
906
     *     to inspect the output. Current RouterOS versions don't return
907
     *     anything useful, but if future ones do, you can read it right away.
908
     *
909
     * @throws RouterErrorException When the router returns one or more errors.
910
     */
911
    public function move($numbers, $destination = null)
912
    {
913
        $moveRequest = new Request($this->menu . '/move');
914
        $moveRequest->setArgument('numbers', $this->find($numbers));
915
        if (null !== $destination) {
916
            $destination = $this->find($destination);
917
            if (false !== strpos($destination, ',')) {
918
                $destination = strstr($destination, ',', true);
919
            }
920
            $moveRequest->setArgument('destination', $destination);
921
        }
922
        $this->clearIdCache();
923
        $responses = $this->client->sendSync($moveRequest);
924
        if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
925
            throw new RouterErrorException(
926
                'Error when moving items',
927
                RouterErrorException::CODE_MOVE_ERROR,
928
                null,
929
                $responses
930
            );
931
        }
932
        return $responses;
933
    }
934
935
    /**
936
     * Counts items at the current menu.
937
     *
938
     * Counts items at the current menu. This executes a dedicated command
939
     * ("print" with a "count-only" argument) on RouterOS, which is why only
940
     * queries are allowed as a criteria, in contrast with
941
     * {@link static::find()}, where numbers and callbacks are allowed also.
942
     *
943
     * @param Query|null           $query A query to filter items by.
944
     *     Without it, all items are included in the count.
945
     * @param string|resource|null $from  A comma separated list of item IDs.
946
     *     Any items in the set that still exist at the time of couting
947
     *     are included in the final tally. Note that the $query filters this
948
     *     set further (i.e. the item must be in the list AND match the $query).
949
     *     Leaving the value to NULL means all matching items at the current
950
     *     menu are included in the count.
951
     *
952
     * @return int The number of items, or -1 on failure (e.g. if the
953
     *     current menu does not have a "print" command or items to be counted).
954
     */
955
    #[\ReturnTypeWillChange]
956
    public function count(Query $query = null, $from = null)
957
    {
958
        $countRequest = new Request(
959
            $this->menu . '/print count-only=""',
960
            $query
961
        );
962
        $countRequest->setArgument('from', $from);
963
        $result = $this->client->sendSync($countRequest)->end()
964
            ->getProperty('ret');
965
966
        if (null === $result) {
967
            return -1;
968
        }
969
        if (Stream::isStream($result)) {
970
            $result = stream_get_contents($result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type string; however, parameter $stream of stream_get_contents() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

970
            $result = stream_get_contents(/** @scrutinizer ignore-type */ $result);
Loading history...
971
        }
972
        return (int)$result;
973
    }
974
975
    /**
976
     * Gets all items in the current menu.
977
     *
978
     * Gets all items in the current menu, using a print request.
979
     *
980
     * @param array<string,string|resource>|array<int,string> $args  Additional
981
     *     arguments to pass to the request.
982
     *     Each array key is the name of the argument, and each array value is
983
     *     the value of the argument to be passed.
984
     *     Arguments without a value (i.e. empty arguments) can also be
985
     *     specified using a numeric key, and the name of the argument as the
986
     *     array value.
987
     *     The "follow" and "follow-only" arguments are prohibited,
988
     *     as they would cause a synchronous request to run forever, without
989
     *     allowing the results to be observed.
990
     *     If you need to use those arguments, use
991
     *     {@link static::newRequest()}, and pass the resulting {@link Request}
992
     *     to {@link Client::sendAsync()}.
993
     *     The "count-only" argument is also prohibited, as results from it
994
     *     would not be consumable. Use {@link static::count()} for that.
995
     * @param Query|null                                      $query A query to
996
     *     filter items by.
997
     *     NULL to get all items.
998
     *
999
     * @return ResponseCollection A response collection with all
1000
     *     {@link Response::TYPE_DATA} responses. The collection will be empty
1001
     *     when there are no matching items.
1002
     *
1003
     * @throws NotSupportedException If $args contains prohibited arguments
1004
     *     ("follow", "follow-only" or "count-only").
1005
     *
1006
     * @throws RouterErrorException When there's an error upon attempting to
1007
     *     call the "print" command on the specified menu (e.g. if there's no
1008
     *     "print" command at the menu to begin with).
1009
     */
1010
    public function getAll(array $args = array(), Query $query = null)
1011
    {
1012
        $printRequest = new Request($this->menu . '/print', $query);
1013
        foreach ($args as $name => $value) {
1014
            if (is_int($name)) {
1015
                $printRequest->setArgument($value);
1016
            } else {
1017
                $printRequest->setArgument($name, $value);
1018
            }
1019
        }
1020
1021
        foreach (array('follow', 'follow-only', 'count-only') as $arg) {
1022
            if ($printRequest->getArgument($arg) !== null) {
1023
                throw new NotSupportedException(
1024
                    "The argument '{$arg}' was specified, but is prohibited",
1025
                    NotSupportedException::CODE_ARG_PROHIBITED,
1026
                    null,
1027
                    $arg
1028
                );
1029
            }
1030
        }
1031
        $responses = $this->client->sendSync($printRequest);
1032
1033
        if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
1034
            throw new RouterErrorException(
1035
                'Error when reading items',
1036
                RouterErrorException::CODE_GETALL_ERROR,
1037
                null,
1038
                $responses
1039
            );
1040
        }
1041
        return $responses->getAllOfType(Response::TYPE_DATA);
1042
    }
1043
1044
    /**
1045
     * Puts a file on RouterOS's file system.
1046
     *
1047
     * Puts a file on RouterOS's file system, regardless of the current menu.
1048
     * Note that this is a **VERY VERY VERY** time consuming method - it takes a
1049
     * minimum of a little over 4 seconds, most of which are in sleep. It waits
1050
     * 2 seconds after a file is first created (required to actually start
1051
     * writing to the file), and another 2 seconds after its contents is written
1052
     * (performed in order to verify success afterwards).
1053
     * Similarly for removal (when $data is NULL) - there are two seconds in
1054
     * sleep, used to verify the file was really deleted.
1055
     *
1056
     * If you want an efficient way of transferring files, use (T)FTP.
1057
     * If you want an efficient way of removing files, use
1058
     * {@link static::setMenu()} to move to the "/file" menu, and call
1059
     * {@link static::remove()} without performing verification afterwards.
1060
     *
1061
     * @param string               $filename  The filename to write data in.
1062
     * @param string|resource|null $data      The data the file is going to have
1063
     *     as a string or a seekable stream.
1064
     *     Setting the value to NULL removes a file of this name.
1065
     *     If a seekable stream is provided, it is sent from its current
1066
     *     position to its end, and the pointer is seeked back to its current
1067
     *     position after sending.
1068
     *     Non seekable streams, as well as all other types, are casted to a
1069
     *     string.
1070
     * @param bool                 $overwrite Whether to overwrite the file if
1071
     *     it exists.
1072
     *
1073
     * @return bool TRUE on success, FALSE on failure.
1074
     */
1075
    public function filePutContents($filename, $data, $overwrite = false)
1076
    {
1077
        $printRequest = new Request(
1078
            '/file/print .proplist=""',
1079
            Query::where('name', $filename)
1080
        );
1081
        $fileExists = count($this->client->sendSync($printRequest)) > 1;
1082
1083
        if (null === $data) {
1084
            if (!$fileExists) {
1085
                return false;
1086
            }
1087
            $removeRequest = new Request('/file/remove');
1088
            $this->client->sendSync(
1089
                $removeRequest->setArgument('numbers', $filename)
1090
            );
1091
            //Required for RouterOS to REALLY remove the file.
1092
            sleep(2);
1093
            return !(count($this->client->sendSync($printRequest)) > 1);
1094
        }
1095
1096
        if (!$overwrite && $fileExists) {
1097
            return false;
1098
        }
1099
        $result = $this->client->sendSync(
1100
            $printRequest->setArgument('file', $filename)
1101
        );
1102
        if (count($result->getAllOfType(Response::TYPE_ERROR)) > 0) {
1103
            return false;
1104
        }
1105
        //Required for RouterOS to write the initial file.
1106
        sleep(2);
1107
        $setRequest = new Request('/file/set contents=""');
1108
        $setRequest->setArgument('numbers', $filename);
1109
        $this->client->sendSync($setRequest);
1110
        $this->client->sendSync($setRequest->setArgument('contents', $data));
1111
        //Required for RouterOS to write the file's new contents.
1112
        sleep(2);
1113
1114
        $fileSize = $this->client->sendSync(
1115
            $printRequest->setArgument('file', null)
1116
                ->setArgument('.proplist', 'size')
1117
        )->getProperty('size');
1118
        if (Stream::isStream($fileSize)) {
1119
            $fileSize = stream_get_contents($fileSize);
0 ignored issues
show
Bug introduced by
It seems like $fileSize can also be of type string; however, parameter $stream of stream_get_contents() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1119
            $fileSize = stream_get_contents(/** @scrutinizer ignore-type */ $fileSize);
Loading history...
1120
        }
1121
        if (Communicator::isSeekableStream($data)) {
1122
            return (double)$fileSize === Communicator::seekableStreamLength(
1123
                /** @scrutinizer ignore-type */ $data
1124
            );
1125
        }
1126
1127
        return sprintf('%u', strlen((string)$data)) === $fileSize;
1128
    }
1129
1130
    /**
1131
     * Gets the contents of a specified file.
1132
     *
1133
     * @param string      $filename      The name of the file to get
1134
     *     the contents of.
1135
     * @param string|null $tmpScriptName In order to get the file's contents, a
1136
     *     script is created at "/system script", the source of which is then
1137
     *     overwritten with the file's contents, then retrieved from there,
1138
     *     after which the script is removed.
1139
     *     If this argument is left NULL, a random string,
1140
     *     prefixed with the computer's name, is generated and used
1141
     *     as the script's name.
1142
     *     To eliminate any possibility of name clashes,
1143
     *     you can specify your own name instead.
1144
     *
1145
     * @return string|resource The contents of the file as a string or as
1146
     *     new PHP temp stream if the underlying
1147
     *     {@link Client::isStreamingResponses()} is set to TRUE.
1148
     *
1149
     * @throws RouterErrorException When there's an error with the temporary
1150
     *     script used to get the file, or if the file doesn't exist.
1151
     */
1152
    public function fileGetContents($filename, $tmpScriptName = null)
1153
    {
1154
        try {
1155
            $responses = $this->exec(
1156
                ':error ("&" . [/file get $filename contents]);',
1157
                array('filename' => $filename),
1158
                null,
1159
                $tmpScriptName
1160
            );
1161
            throw new RouterErrorException(
1162
                'Unable to read file through script (no error returned)',
1163
                RouterErrorException::CODE_SCRIPT_FILE_ERROR,
1164
                null,
1165
                $responses
1166
            );
1167
        } catch (RouterErrorException $e) {
1168
            if ($e->getCode() !== RouterErrorException::CODE_SCRIPT_RUN_ERROR
1169
                || null === $e->getResponses()
1170
            ) {
1171
                throw $e;
1172
            }
1173
            $message = $e->getResponses()->getAllOfType(Response::TYPE_ERROR)
1174
                ->getProperty('message');
1175
            if (Stream::isStream($message)) {
1176
                $successToken = fread($message, 1/*strlen('&')*/);
0 ignored issues
show
Bug introduced by
It seems like $message can also be of type string; however, parameter $stream of fread() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1176
                $successToken = fread(/** @scrutinizer ignore-type */ $message, 1/*strlen('&')*/);
Loading history...
1177
                if ('&' === $successToken) {
1178
                    $messageCopy = fopen('php://temp', 'r+b');
1179
                    stream_copy_to_stream($message, $messageCopy);
0 ignored issues
show
Bug introduced by
It seems like $message can also be of type string; however, parameter $from of stream_copy_to_stream() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1179
                    stream_copy_to_stream(/** @scrutinizer ignore-type */ $message, $messageCopy);
Loading history...
1180
                    rewind($messageCopy);
1181
                    fclose($message);
0 ignored issues
show
Bug introduced by
It seems like $message can also be of type string; however, parameter $stream of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1181
                    fclose(/** @scrutinizer ignore-type */ $message);
Loading history...
1182
                    return $messageCopy;
1183
                }
1184
                rewind($message);
0 ignored issues
show
Bug introduced by
It seems like $message can also be of type string; however, parameter $stream of rewind() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1184
                rewind(/** @scrutinizer ignore-type */ $message);
Loading history...
1185
            } elseif (strpos($message, '&') === 0) {
0 ignored issues
show
Bug introduced by
It seems like $message can also be of type null and resource; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1185
            } elseif (strpos(/** @scrutinizer ignore-type */ $message, '&') === 0) {
Loading history...
1186
                return substr($message, 1/*strlen('&')*/);
0 ignored issues
show
Bug introduced by
It seems like $message can also be of type null and resource; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1186
                return substr(/** @scrutinizer ignore-type */ $message, 1/*strlen('&')*/);
Loading history...
1187
            }
1188
            throw $e;
1189
        }
1190
    }
1191
1192
    /**
1193
     * Performs an action on a bulk of items at the current menu.
1194
     *
1195
     * @param string $command What command to perform.
1196
     * @param array  $args    Zero or more arguments can be specified,
1197
     *     each being a criteria.
1198
     *     If zero arguments are specified, matches all items.
1199
     *     See {@link static::find()} for a description of what criteria are
1200
     *     accepted.
1201
     *
1202
     * @return ResponseCollection Returns the response collection, allowing you
1203
     *     to inspect errors, if any.
1204
     */
1205
    protected function doBulk($command, array $args = array())
1206
    {
1207
        $bulkRequest = new Request("{$this->menu}/{$command}");
1208
        $bulkRequest->setArgument(
1209
            'numbers',
1210
            call_user_func_array(array($this, 'find'), $args)
1211
        );
1212
        return $this->client->sendSync($bulkRequest);
1213
    }
1214
}
1215