Completed
Push — develop ( 5ed1fa...10f0a8 )
by Vasil
06:12
created

Util::_exec()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 48
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1024
        } else {
1025
            return sprintf('%u', strlen((string)$data)) === $fileSize;
1026
        }
1027
    }
1028
1029
    /**
1030
     * Gets the contents of a specified file.
1031
     *
1032
     * @param string      $filename      The name of the file to get
1033
     *     the contents of.
1034
     * @param string|null $tmpScriptName In order to get the file's contents, a
1035
     *     script is created at "/system script", the source of which is then
1036
     *     overwritten with the file's contents, then retrieved from there,
1037
     *     after which the script is removed.
1038
     *     If this argument is left NULL, a random string,
1039
     *     prefixed with the computer's name, is generated and used
1040
     *     as the script's name.
1041
     *     To eliminate any possibility of name clashes,
1042
     *     you can specify your own name instead.
1043
     *
1044
     * @return string|resource|false The contents of the file as a string or as
1045
     *     new PHP temp stream if the underlying
1046
     *     {@link Client::isStreamingResponses()} is set to TRUE.
1047
     *     FALSE is returned if there is no such file.
1048
     *
1049
     * @throws RouterErrorException When there's an error with the temporary
1050
     *     script used to get the file.
1051
     */
1052
    public function fileGetContents($filename, $tmpScriptName = null)
1053
    {
1054
        $checkRequest = new Request(
1055
            '/file/print .proplist=""',
1056
            Query::where('name', $filename)
1057
        );
1058
        if (1 === count($this->client->sendSync($checkRequest))) {
1059
            return false;
1060
        }
1061
        return $this->exec(
1062
            '/system script set $"_" source=[/file get $filename contents]',
1063
            array('filename' => $filename),
1064
            null,
1065
            $tmpScriptName
1066
        );
1067
    }
1068
1069
    /**
1070
     * Performs an action on a bulk of items at the current menu.
1071
     *
1072
     * @param string $what What action to perform.
1073
     * @param array  $args Zero or more arguments can be specified, each being
1074
     *     a criteria. If zero arguments are specified, removes all items.
1075
     *     See {@link static::find()} for a description of what criteria are
1076
     *     accepted.
1077
     *
1078
     * @return ResponseCollection Returns the response collection, allowing you
1079
     *     to inspect errors, if any.
1080
     */
1081
    protected function doBulk($what, array $args = array())
1082
    {
1083
        $bulkRequest = new Request($this->menu . '/' . $what);
1084
        $bulkRequest->setArgument(
1085
            'numbers',
1086
            call_user_func_array(array($this, 'find'), $args)
1087
        );
1088
        return $this->client->sendSync($bulkRequest);
1089
    }
1090
}
1091