Completed
Push — develop ( a7dd5e...9cdb61 )
by Vasil
03:31
created

Util   D

Complexity

Total Complexity 107

Size/Duplication

Total Lines 1114
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 44
Bugs 21 Features 29
Metric Value
wmc 107
c 44
b 21
f 29
lcom 1
cbo 9
dl 0
loc 1114
rs 4.4102

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getMenu() 0 4 2
B setMenu() 0 31 5
B newRequest() 0 25 4
B exec() 0 81 5
A clearIdCache() 0 5 1
B getCurrentTime() 0 39 4
D find() 0 90 22
C get() 0 48 10
A enable() 0 13 2
A disable() 0 13 2
A remove() 0 14 2
A comment() 0 16 2
B set() 0 24 5
A edit() 0 6 2
A unsetValue() 0 17 2
C add() 0 40 8
B move() 0 23 4
A count() 0 14 3
B getAll() 0 33 6
C filePutContents() 0 52 8
B fileGetContents() 0 33 6
A doBulk() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like Util often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Util, and based on these observations, apply Extract Interface, too.

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