Completed
Push — develop ( 193d99...7ce9b0 )
by Vasil
03:00
created

Util::setMenu()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 2
Metric Value
c 2
b 1
f 2
dl 0
loc 19
rs 9.2
cc 4
eloc 15
nc 4
nop 1
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
 * Values at {@link Util::exec()} can be casted from this type.
25
 */
26
use DateTime;
27
28
/**
29
 * Values at {@link Util::exec()} can be casted from this type.
30
 */
31
use DateInterval;
32
33
/**
34
 * Used at {@link Util::getCurrentTime()} to get the proper time.
35
 */
36
use DateTimeZone;
37
38
/**
39
 * Implemented by this class.
40
 */
41
use Countable;
42
43
/**
44
 * Used to reliably write to streams at {@link Util::prepareScript()}.
45
 */
46
use PEAR2\Net\Transmitter\Stream;
47
48
/**
49
 * Used to catch a DateInterval exception at {@link Util::parseValue()}.
50
 */
51
use Exception as E;
52
53
/**
54
 * Utility class.
55
 *
56
 * Abstracts away frequently used functionality (particularly CRUD operations)
57
 * in convenient to use methods by wrapping around a connection.
58
 *
59
 * @category Net
60
 * @package  PEAR2_Net_RouterOS
61
 * @author   Vasil Rangelov <[email protected]>
62
 * @license  http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
63
 * @link     http://pear2.php.net/PEAR2_Net_RouterOS
64
 */
65
class Util implements Countable
66
{
67
    /**
68
     * @var Client The connection to wrap around.
69
     */
70
    protected $client;
71
72
    /**
73
     * @var string The current menu.
74
     */
75
    protected $menu = '/';
76
77
    /**
78
     * @var array<int,string>|null An array with the numbers of items in
79
     *     the current menu as keys, and the corresponding IDs as values.
80
     *     NULL when the cache needs regenerating.
81
     */
82
    protected $idCache = null;
83
84
    /**
85
     * Parses a value from a RouterOS scripting context.
86
     *
87
     * Turns a value from RouterOS into an equivalent PHP value, based on
88
     * determining the type in the same way RouterOS would determine it for a
89
     * literal.
90
     *
91
     * This method is intended to be the very opposite of
92
     * {@link static::escapeValue()}. That is, results from that method, if
93
     * given to this method, should produce equivalent results.
94
     * 
95
     * For better usefulness, in addition to "actual" RouterOS types, a pseudo
96
     * "date" type is also recognized, whenever the string is in the form
97
     * "M/j/Y".
98
     *
99
     * @param string $value The value to be parsed. Must be a literal of a
100
     *     value, e.g. what {@link static::escapeValue()} will give you.
101
     *
102
     * @return mixed Depending on RouterOS type detected:
103
     *     - "nil" or "nothing" - NULL.
104
     *     - "number" - int or double for large values.
105
     *     - "bool" - a boolean.
106
     *     - "time" - a {@link DateInterval} object.
107
     *     - "array" - an array, with the values processed recursively.
108
     *     - "str" - a string.
109
     *     - "date" (pseudo type) - a DateTime object with the specified date,
110
     *         at midnight UTC time.
111
     *     - Unrecognized type - treated as an unquoted string.
112
     */
113
    public static function parseValue($value)
114
    {
115
        $value = (string)$value;
116
117
        if (in_array($value, array('', 'nil'), true)) {
118
            return null;
119
        } elseif (in_array($value, array('true', 'false', 'yes', 'no'), true)) {
120
            return $value === 'true' || $value === 'yes';
121
        } elseif ($value === (string)($num = (int)$value)
122
            || $value === (string)($num = (double)$value)
123
        ) {
124
            return $num;
125
        } elseif (preg_match(
126
            '/^
127
                (?:(\d+)w)?
128
                (?:(\d+)d)?
129
                (?:(\d+)(?:\:|h))?
130
                (?|
131
                    (\d+)\:
132
                    (\d*(?:\.\d{1,9})?)
133
                |
134
                    (?:(\d+)m)?
135
                    (?:(\d+|\d*\.\d{1,9})s)?
136
                    (?:((?5))ms)?
137
                    (?:((?5))us)?
138
                    (?:((?5))ns)?
139
                )
140
            $/x',
141
            $value,
142
            $time
143
        )) {
144
            $days = isset($time[2]) ? (int)$time[2] : 0;
145
            if (isset($time[1])) {
146
                $days += 7 * (int)$time[1];
147
            }
148
            if (empty($time[3])) {
149
                $time[3] = 0;
150
            }
151
            if (empty($time[4])) {
152
                $time[4] = 0;
153
            }
154
            if (empty($time[5])) {
155
                $time[5] = 0;
156
            }
157
            
158
            $subsecondTime = 0.0;
159
            //@codeCoverageIgnoreStart
160
            // No PHP version currently supports sub-second DateIntervals,
161
            // meaning this section is untestable, since no version constraints
162
            // can be specified for test inputs.
163
            // All inputs currently use integer seconds only, making this
164
            // section unreachable during tests.
165
            // Nevertheless, this section exists right now, in order to provide
166
            // such support as soon as PHP has it.
167
            if (!empty($time[6])) {
168
                $subsecondTime += ((double)$time[6]) / 1000;
169
            }
170
            if (!empty($time[7])) {
171
                $subsecondTime += ((double)$time[7]) / 1000000;
172
            }
173
            if (!empty($time[8])) {
174
                $subsecondTime += ((double)$time[8]) / 1000000000;
175
            }
176
            //@codeCoverageIgnoreEnd
177
178
            $secondsSpec = $time[5] + $subsecondTime;
179
            try {
180
                return new DateInterval(
181
                    "P{$days}DT{$time[3]}H{$time[4]}M{$secondsSpec}S"
182
                );
183
                //@codeCoverageIgnoreStart
184
                // See previous ignored section's note.
185
                // 
186
                // This section is added for backwards compatibility with current
187
                // PHP versions, when in the future sub-second support is added.
188
                // In that event, the test inputs for older versions will be
189
                // expected to get a rounded up result of the sub-second data.
190
            } catch (E $e) {
191
                $secondsSpec = (int)round($secondsSpec);
192
                return new DateInterval(
193
                    "P{$days}DT{$time[3]}H{$time[4]}M{$secondsSpec}S"
194
                );
195
            }
196
            //@codeCoverageIgnoreEnd
197
        } elseif (preg_match(
198
            '#^
199
                (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)
200
                /
201
                (\d\d?)
202
                /
203
                (\d{4})
204
            $#uix',
205
            $value,
206
            $date
207
        )) {
208
            try {
209
                return DateTime::createFromFormat(
210
                    'M/j/Y',
211
                    ucfirst($date[1]) . '/' . (int)$date[2] . '/' . $date[3],
212
                    new DateTimeZone('UTC')
213
                );
214
            } catch (E $e) {
215
                return $value;
216
            }
217
        } elseif (('"' === $value[0]) && substr(strrev($value), 0, 1) === '"') {
218
            return str_replace(
219
                array('\"', '\\\\', "\\\n", "\\\r\n", "\\\r"),
220
                array('"', '\\'),
221
                substr($value, 1, -1)
222
            );
223
        } elseif ('{' === $value[0]) {
224
            $len = strlen($value);
225
            if ($value[$len - 1] === '}') {
226
                $value = substr($value, 1, -1);
227
                if ('' === $value) {
228
                    return array();
229
                }
230
                $parsedValue = preg_split(
231
                    '/
232
                        (\"(?:\\\\\\\\|\\\\"|[^"])*\")
233
                        |
234
                        (\{[^{}]*(?2)?\})
235
                        |
236
                        ([^;=]+)
237
                    /sx',
238
                    $value,
239
                    null,
240
                    PREG_SPLIT_DELIM_CAPTURE
241
                );
242
                $result = array();
243
                $newVal = null;
244
                $newKey = null;
245
                for ($i = 0, $l = count($parsedValue); $i < $l; ++$i) {
246
                    switch ($parsedValue[$i]) {
247
                    case '':
248
                        break;
249
                    case ';':
250
                        if (null === $newKey) {
251
                            $result[] = $newVal;
252
                        } else {
253
                            $result[$newKey] = $newVal;
254
                        }
255
                        $newKey = $newVal = null;
256
                        break;
257
                    case '=':
258
                        $newKey = static::parseValue($parsedValue[$i - 1]);
259
                        $newVal = static::parseValue($parsedValue[++$i]);
260
                        break;
261
                    default:
262
                        $newVal = static::parseValue($parsedValue[$i]);
263
                    }
264
                }
265
                if (null === $newKey) {
266
                    $result[] = $newVal;
267
                } else {
268
                    $result[$newKey] = $newVal;
269
                }
270
                return $result;
271
            }
272
        }
273
        return $value;
274
    }
275
276
    /**
277
     * Prepares a script.
278
     *
279
     * Prepares a script for eventual execution by prepending parameters as
280
     * variables to it.
281
     *
282
     * This is particularly useful when you're creating scripts that you don't
283
     * want to execute right now (as with {@link static::exec()}, but instead
284
     * you want to store it for later execution, perhaps by supplying it to
285
     * "/system scheduler".
286
     *
287
     * @param string|resource     $source The source of the script, as a string
288
     *     or stream. If a stream is provided, reading starts from the current
289
     *     position to the end of the stream, and the pointer stays at the end
290
     *     after reading is done.
291
     * @param array<string,mixed> $params An array of parameters to make
292
     *     available in the script as local variables.
293
     *     Variable names are array keys, and variable values are array values.
294
     *     Array values are automatically processed with
295
     *     {@link static::escapeValue()}. Streams are also supported, and are
296
     *     processed in chunks, each with
297
     *     {@link static::escapeString()}. Processing starts from the current
298
     *     position to the end of the stream, and the stream's pointer stays at
299
     *     the end after reading is done.
300
     *
301
     * @return resource A new PHP temporary stream with the script as contents,
302
     *     with the pointer back at the start.
303
     *
304
     * @see static::appendScript()
305
     */
306
    public static function prepareScript(
307
        $source,
308
        array $params = array()
309
    ) {
310
        $resultStream = fopen('php://temp', 'r+b');
311
        self::appendScript($resultStream, $source, $params);
312
        rewind($resultStream);
313
        return $resultStream;
314
    }
315
316
    /**
317
     * Appends a script.
318
     *
319
     * Appends a script to an existing stream.
320
     *
321
     * @param resource            $stream An existing stream to write the
322
     *     resulting script to.
323
     * @param string|resource     $source The source of the script, as a string
324
     *     or stream. If a stream is provided, reading starts from the current
325
     *     position to the end of the stream, and the pointer stays at the end
326
     *     after reading is done.
327
     * @param array<string,mixed> $params An array of parameters to make
328
     *     available in the script as local variables.
329
     *     Variable names are array keys, and variable values are array values.
330
     *     Array values are automatically processed with
331
     *     {@link static::escapeValue()}. Streams are also supported, and are
332
     *     processed in chunks, each with
333
     *     {@link static::escapeString()}. Processing starts from the current
334
     *     position to the end of the stream, and the stream's pointer stays at
335
     *     the end after reading is done.
336
     *
337
     * @return int The number of bytes written to $stream is returned,
338
     *     and the pointer remains where it was after the write
339
     *     (i.e. it is not seeked back, even if seeking is supported).
340
     */
341
    public static function appendScript(
342
        $stream,
343
        $source,
344
        array $params = array()
345
    ) {
346
        $writer = new Stream($stream, false);
347
        $bytes = 0;
348
349
        foreach ($params as $pname => $pvalue) {
350
            $pname = static::escapeString($pname);
351
            $bytes += $writer->send(":local \"{$pname}\" ");
352
            if (Stream::isStream($pvalue)) {
353
                $reader = new Stream($pvalue, false);
354
                $chunkSize = $reader->getChunk(Stream::DIRECTION_RECEIVE);
355
                $bytes += $writer->send('"');
356
                while ($reader->isAvailable() && $reader->isDataAwaiting()) {
357
                    $bytes += $writer->send(
358
                        static::escapeString(fread($pvalue, $chunkSize))
359
                    );
360
                }
361
                $bytes += $writer->send("\";\n");
362
            } else {
363
                $bytes += $writer->send(static::escapeValue($pvalue) . ";\n");
364
            }
365
        }
366
367
        $bytes += $writer->send($source);
368
        return $bytes;
369
    }
370
371
    /**
372
     * Escapes a value for a RouterOS scripting context.
373
     *
374
     * Turns any native PHP value into an equivalent whole value that can be
375
     * inserted as part of a RouterOS script.
376
     *
377
     * DateTime and DateInterval objects will be casted to RouterOS' "time"
378
     * type. A DateTime object will be converted to a time relative to the UNIX
379
     * epoch time. Note that if a DateInterval does not have the "days" property
380
     * ("a" in formatting), then its months and years will be ignored, because
381
     * they can't be unambiguously converted to a "time" value.
382
     *
383
     * Unrecognized types (i.e. resources and other objects) are casted to
384
     * strings.
385
     *
386
     * @param mixed $value The value to be escaped.
387
     *
388
     * @return string A string representation that can be directly inserted in a
389
     *     script as a whole value.
390
     */
391
    public static function escapeValue($value)
392
    {
393
        switch(gettype($value)) {
394
        case 'NULL':
395
            $value = '';
396
            break;
397
        case 'integer':
398
            $value = (string)$value;
399
            break;
400
        case 'boolean':
401
            $value = $value ? 'true' : 'false';
402
            break;
403
        case 'array':
404
            if (0 === count($value)) {
405
                $value = '({})';
406
                break;
407
            }
408
            $result = '';
409
            foreach ($value as $key => $val) {
410
                $result .= ';';
411
                if (!is_int($key)) {
412
                    $result .= static::escapeValue($key) . '=';
413
                }
414
                $result .= static::escapeValue($val);
415
            }
416
            $value = '{' . substr($result, 1) . '}';
417
            break;
418
        case 'object':
419
            if ($value instanceof DateTime) {
420
                $usec = $value->format('u');
421
                if ('000000' === $usec) {
422
                    unset($usec);
423
                }
424
                $unixEpoch = new DateTime('@0');
425
                $value = $unixEpoch->diff($value);
426
            }
427
            if ($value instanceof DateInterval) {
428
                if (false === $value->days || $value->days < 0) {
429
                    $value = $value->format('%r%dd%H:%I:%S');
430
                } else {
431
                    $value = $value->format('%r%ad%H:%I:%S');
432
                }
433
                if (strpos('.', $value) === false && isset($usec)) {
434
                    $value .= '.' . $usec;
435
                }
436
                break;
437
            }
438
            //break; intentionally omitted
439
        default:
440
            $value = '"' . static::escapeString((string)$value) . '"';
441
            break;
442
        }
443
        return $value;
444
    }
445
446
    /**
447
     * Escapes a string for a RouterOS scripting context.
448
     *
449
     * Escapes a string for a RouterOS scripting context. The value can then be
450
     * surrounded with quotes at a RouterOS script (or concatenated onto a
451
     * larger string first), and you can be sure there won't be any code
452
     * injections coming from it.
453
     *
454
     * @param string $value Value to be escaped.
455
     *
456
     * @return string The escaped value.
457
     */
458
    public static function escapeString($value)
459
    {
460
        return preg_replace_callback(
461
            '/[^\\_A-Za-z0-9]+/S',
462
            array(__CLASS__, '_escapeCharacters'),
463
            $value
464
        );
465
    }
466
467
    /**
468
     * Escapes a character for a RouterOS scripting context.
469
     *
470
     * Escapes a character for a RouterOS scripting context. Intended to only be
471
     * called for non-alphanumeric characters.
472
     *
473
     * @param string $chars The matches array, expected to contain exactly one
474
     *     member, in which is the whole string to be escaped.
475
     *
476
     * @return string The escaped characters.
477
     */
478
    private static function _escapeCharacters($chars)
479
    {
480
        $result = '';
481
        for ($i = 0, $l = strlen($chars[0]); $i < $l; ++$i) {
482
            $result .= '\\' . str_pad(
483
                strtoupper(dechex(ord($chars[0][$i]))),
484
                2,
485
                '0',
486
                STR_PAD_LEFT
487
            );
488
        }
489
        return $result;
490
    }
491
492
    /**
493
     * Creates a new Util instance.
494
     *
495
     * Wraps around a connection to provide convenience methods.
496
     *
497
     * @param Client $client The connection to wrap around.
498
     */
499
    public function __construct(Client $client)
500
    {
501
        $this->client = $client;
502
    }
503
504
    /**
505
     * Gets the current menu.
506
     *
507
     * @return string The absolute path to current menu, using API syntax.
508
     */
509
    public function getMenu()
510
    {
511
        return $this->menu;
512
    }
513
514
    /**
515
     * Sets the current menu.
516
     *
517
     * Sets the current menu.
518
     *
519
     * @param string $newMenu The menu to change to. Can be specified with API
520
     *     or CLI syntax and can be either absolute or relative. If relative,
521
     *     it's relative to the current menu, which by default is the root.
522
     *
523
     * @return $this The object itself. If an empty string is given for
524
     *     a new menu, no change is performed,
525
     *     but the ID cache is cleared anyway.
526
     *
527
     * @see static::clearIdCache()
528
     */
529
    public function setMenu($newMenu)
530
    {
531
        $newMenu = (string)$newMenu;
532
        if ('' !== $newMenu) {
533
            $menuRequest = new Request('/menu');
534
            if ('/' === $newMenu) {
535
                $this->menu = '/';
536
            } elseif ('/' === $newMenu[0]) {
537
                $this->menu = $menuRequest->setCommand($newMenu)->getCommand();
538
            } else {
539
                $this->menu = $menuRequest->setCommand(
540
                    '/' . str_replace('/', ' ', substr($this->menu, 1)) . ' ' .
541
                    str_replace('/', ' ', $newMenu)
542
                )->getCommand();
543
            }
544
        }
545
        $this->clearIdCache();
546
        return $this;
547
    }
548
549
    /**
550
     * Creates a Request object.
551
     *
552
     * Creates a {@link Request} object, with a command that's at the
553
     * current menu. The request can then be sent using {@link Client}.
554
     *
555
     * @param string      $command The command of the request, not including
556
     *     the menu. The request will have that command at the current menu.
557
     * @param array       $args    Arguments of the request.
558
     *     Each array key is the name of the argument, and each array value is
559
     *     the value of the argument to be passed.
560
     *     Arguments without a value (i.e. empty arguments) can also be
561
     *     specified using a numeric key, and the name of the argument as the
562
     *     array value.
563
     * @param Query|null  $query   The {@link Query} of the request.
564
     * @param string|null $tag     The tag of the request.
565
     *
566
     * @return Request The {@link Request} object.
567
     *
568
     * @throws NotSupportedException On an attempt to call a command in a
569
     *     different menu using API syntax.
570
     * @throws InvalidArgumentException On an attempt to call a command in a
571
     *     different menu using CLI syntax.
572
     */
573
    public function newRequest(
574
        $command,
575
        array $args = array(),
576
        Query $query = null,
577
        $tag = null
578
    ) {
579
        if (false !== strpos($command, '/')) {
580
            throw new NotSupportedException(
581
                'Command tried to go to a different menu',
582
                NotSupportedException::CODE_MENU_MISMATCH,
583
                null,
584
                $command
585
            );
586
        }
587
        $request = new Request('/menu', $query, $tag);
588
        $request->setCommand("{$this->menu}/{$command}");
589
        foreach ($args as $name => $value) {
590
            if (is_int($name)) {
591
                $request->setArgument($value);
592
            } else {
593
                $request->setArgument($name, $value);
594
            }
595
        }
596
        return $request;
597
    }
598
599
    /**
600
     * Executes a RouterOS script.
601
     *
602
     * Executes a RouterOS script, written as a string or a stream.
603
     * Note that in cases of errors, the line numbers will be off, because the
604
     * script is executed at the current menu as context, with the specified
605
     * variables pre declared. This is achieved by prepending 1+count($params)
606
     * lines before your actual script.
607
     *
608
     * @param string|resource     $source The source of the script, as a string
609
     *     or stream. If a stream is provided, reading starts from the current
610
     *     position to the end of the stream, and the pointer stays at the end
611
     *     after reading is done.
612
     * @param array<string,mixed> $params An array of parameters to make
613
     *     available in the script as local variables.
614
     *     Variable names are array keys, and variable values are array values.
615
     *     Array values are automatically processed with
616
     *     {@link static::escapeValue()}. Streams are also supported, and are
617
     *     processed in chunks, each processed with
618
     *     {@link static::escapeString()}. Processing starts from the current
619
     *     position to the end of the stream, and the stream's pointer is left
620
     *     untouched after the reading is done.
621
     *     Note that the script's (generated) name is always added as the
622
     *     variable "_", which will be inadvertently lost if you overwrite it
623
     *     from here.
624
     * @param string|null         $policy Allows you to specify a policy the
625
     *     script must follow. Has the same format as in terminal.
626
     *     If left NULL, the script has no restrictions beyond those imposed by
627
     *     the username.
628
     * @param string|null         $name   The script is executed after being
629
     *     saved in "/system script" and is removed after execution.
630
     *     If this argument is left NULL, a random string,
631
     *     prefixed with the computer's name, is generated and used
632
     *     as the script's name.
633
     *     To eliminate any possibility of name clashes,
634
     *     you can specify your own name instead.
635
     *
636
     * @return ResponseCollection Returns the response collection of the
637
     *     run, allowing you to inspect errors, if any.
638
     *     If the script was not added successfully before execution, the
639
     *     ResponseCollection from the add attempt is going to be returned.
640
     */
641
    public function exec(
642
        $source,
643
        array $params = array(),
644
        $policy = null,
645
        $name = null
646
    ) {
647
        return $this->_exec($source, $params, $policy, $name);
648
    }
649
650
    /**
651
     * Clears the ID cache.
652
     *
653
     * Normally, the ID cache improves performance when targeting items by a
654
     * number. If you're using both Util's methods and other means (e.g.
655
     * {@link Client} or {@link Util::exec()}) to add/move/remove items, the
656
     * cache may end up being out of date. By calling this method right before
657
     * targeting an item with a number, you can ensure number accuracy.
658
     *
659
     * Note that Util's {@link static::move()} and {@link static::remove()}
660
     * methods automatically clear the cache before returning, while
661
     * {@link static::add()} adds the new item's ID to the cache as the next
662
     * number. A change in the menu also clears the cache.
663
     *
664
     * Note also that the cache is being rebuilt unconditionally every time you
665
     * use {@link static::find()} with a callback.
666
     *
667
     * @return $this The Util object itself.
668
     */
669
    public function clearIdCache()
670
    {
671
        $this->idCache = null;
672
        return $this;
673
    }
674
675
    /**
676
     * Gets the current time on the router.
677
     * 
678
     * Gets the current time on the router, regardless of the current menu.
679
     * 
680
     * If your router uses a "manual" timezone, the resulting object will use
681
     * the "gmt-offset" as the timezone identifier.
682
     * 
683
     * @return DateTime The current time of the router, as a DateTime object.
684
     */
685
    public function getCurrentTime()
686
    {
687
        $clock = $this->client->sendSync(
688
            new Request('/system/clock/print')
689
        )->current();
690
        $datetime = ucfirst($clock->getProperty('date')) . ' ' .
691
            $clock->getProperty('time');
692
        if ('manual' === $clock->getProperty('time-zone-name')) {
693
            $result = DateTime::createFromFormat(
694
                'M/j/Y H:i:s P',
695
                $datetime . ' ' . $clock->getProperty('gmt-offset'),
696
                new DateTimeZone('UTC')
697
            );
698
        } else {
699
            $result = DateTime::createFromFormat(
700
                'M/j/Y H:i:s',
701
                $datetime,
702
                new DateTimeZone($clock->getProperty('time-zone-name'))
703
            );
704
        }
705
        return $result;
706
    }
707
708
    /**
709
     * Finds the IDs of items at the current menu.
710
     *
711
     * Finds the IDs of items based on specified criteria, and returns them as
712
     * a comma separated string, ready for insertion at a "numbers" argument.
713
     *
714
     * Accepts zero or more criteria as arguments. If zero arguments are
715
     * specified, returns all items' IDs. The value of each criteria can be a
716
     * number (just as in Winbox), a literal ID to be included, a {@link Query}
717
     * object, or a callback. If a callback is specified, it is called for each
718
     * item, with the item as an argument. If it returns a true value, the
719
     * item's ID is included in the result. Every other value is casted to a
720
     * string. A string is treated as a comma separated values of IDs, numbers
721
     * or callback names. Non-existent callback names are instead placed in the
722
     * result, which may be useful in menus that accept identifiers other than
723
     * IDs, but note that it can cause errors on other menus.
724
     *
725
     * @return string A comma separated list of all items matching the
726
     *     specified criteria.
727
     */
728
    public function find()
729
    {
730
        if (func_num_args() === 0) {
731
            if (null === $this->idCache) {
732
                $ret = $this->client->sendSync(
733
                    new Request($this->menu . '/find')
734
                )->getProperty('ret');
735
                if (null === $ret) {
736
                    $this->idCache = array();
737
                    return '';
738
                } elseif (!is_string($ret)) {
739
                    $ret = stream_get_contents($ret);
740
                }
741
742
                $idCache = str_replace(
743
                    ';',
744
                    ',',
745
                    $ret
746
                );
747
                $this->idCache = explode(',', $idCache);
748
                return $idCache;
749
            }
750
            return implode(',', $this->idCache);
751
        }
752
        $idList = '';
753
        foreach (func_get_args() as $criteria) {
754
            if ($criteria instanceof Query) {
755
                foreach ($this->client->sendSync(
756
                    new Request($this->menu . '/print .proplist=.id', $criteria)
757
                ) as $response) {
758
                    $idList .= $response->getProperty('.id') . ',';
759
                }
760
            } elseif (is_callable($criteria)) {
761
                $idCache = array();
762
                foreach ($this->client->sendSync(
763
                    new Request($this->menu . '/print')
764
                ) as $response) {
765
                    if ($criteria($response)) {
766
                        $idList .= $response->getProperty('.id') . ',';
767
                    }
768
                    $idCache[] = $response->getProperty('.id');
769
                }
770
                $this->idCache = $idCache;
771
            } else {
772
                $this->find();
773
                if (is_int($criteria)) {
774
                    if (isset($this->idCache[$criteria])) {
775
                        $idList = $this->idCache[$criteria] . ',';
776
                    }
777
                } else {
778
                    $criteria = (string)$criteria;
779
                    if ($criteria === (string)(int)$criteria) {
780
                        if (isset($this->idCache[(int)$criteria])) {
781
                            $idList .= $this->idCache[(int)$criteria] . ',';
782
                        }
783
                    } elseif (false === strpos($criteria, ',')) {
784
                        $idList .= $criteria . ',';
785
                    } else {
786
                        $criteriaArr = explode(',', $criteria);
787
                        for ($i = count($criteriaArr) - 1; $i >= 0; --$i) {
788
                            if ('' === $criteriaArr[$i]) {
789
                                unset($criteriaArr[$i]);
790
                            } elseif ('*' === $criteriaArr[$i][0]) {
791
                                $idList .= $criteriaArr[$i] . ',';
792
                                unset($criteriaArr[$i]);
793
                            }
794
                        }
795
                        if (!empty($criteriaArr)) {
796
                            $idList .= call_user_func_array(
797
                                array($this, 'find'),
798
                                $criteriaArr
799
                            ) . ',';
800
                        }
801
                    }
802
                }
803
            }
804
        }
805
        return rtrim($idList, ',');
806
    }
807
808
    /**
809
     * Gets a value of a specified item at the current menu.
810
     *
811
     * @param int|string|null $number    A number identifying the item you're
812
     *     targeting. Can also be an ID or (in some menus) name. For menus where
813
     *     there are no items (e.g. "/system identity"), you can specify NULL.
814
     * @param string          $valueName The name of the value you want to get.
815
     *
816
     * @return string|resource|null|false The value of the specified property as
817
     *     a string or as new PHP temp stream if the underlying
818
     *     {@link Client::isStreamingResponses()} is set to TRUE.
819
     *     If the property is not set, NULL will be returned. FALSE on failure
820
     *     (e.g. no such item, invalid property, etc.).
821
     */
822
    public function get($number, $valueName)
823
    {
824
        if (is_int($number) || ((string)$number === (string)(int)$number)) {
825
            $this->find();
826
            if (isset($this->idCache[(int)$number])) {
827
                $number = $this->idCache[(int)$number];
828
            } else {
829
                return false;
830
            }
831
        }
832
833
        //For new RouterOS versions
834
        $request = new Request($this->menu . '/get');
835
        $request->setArgument('number', $number);
836
        $request->setArgument('value-name', $valueName);
837
        $responses = $this->client->sendSync($request);
838
        if (Response::TYPE_ERROR === $responses->getType()) {
839
            return false;
840
        }
841
        $result = $responses->getProperty('ret');
842
        if (null !== $result) {
843
            return $result;
844
        }
845
846
        // The "get" of old RouterOS versions returns an empty !done response.
847
        // New versions return such only when the property is not set.
848
        // This is a backup for old versions' sake.
849
        $query = null;
850
        if (null !== $number) {
851
            $number = (string)$number;
852
            $query = Query::where('.id', $number)->orWhere('name', $number);
853
        }
854
        $responses = $this->getAll(
855
            array('.proplist' => $valueName, 'detail'),
856
            $query
857
        );
858
859
        if (0 === count($responses)) {
860
            // @codeCoverageIgnoreStart
861
            // New versions of RouterOS can't possibly reach this section.
862
            return false;
863
            // @codeCoverageIgnoreEnd
864
        }
865
        return $responses->getProperty($valueName);
866
    }
867
868
    /**
869
     * Enables all items at the current menu matching certain criteria.
870
     *
871
     * Zero or more arguments can be specified, each being a criteria.
872
     * If zero arguments are specified, enables all items.
873
     * See {@link static::find()} for a description of what criteria are
874
     * accepted.
875
     *
876
     * @return ResponseCollection returns the response collection, allowing you
877
     *     to inspect errors, if any.
878
     */
879
    public function enable()
880
    {
881
        return $this->doBulk('enable', func_get_args());
882
    }
883
884
    /**
885
     * Disables all items at the current menu matching certain criteria.
886
     *
887
     * Zero or more arguments can be specified, each being a criteria.
888
     * If zero arguments are specified, disables all items.
889
     * See {@link static::find()} for a description of what criteria are
890
     * accepted.
891
     *
892
     * @return ResponseCollection Returns the response collection, allowing you
893
     *     to inspect errors, if any.
894
     */
895
    public function disable()
896
    {
897
        return $this->doBulk('disable', func_get_args());
898
    }
899
900
    /**
901
     * Removes all items at the current menu matching certain criteria.
902
     *
903
     * Zero or more arguments can be specified, each being a criteria.
904
     * If zero arguments are specified, removes all items.
905
     * See {@link static::find()} for a description of what criteria are
906
     * accepted.
907
     *
908
     * @return ResponseCollection Returns the response collection, allowing you
909
     *     to inspect errors, if any.
910
     */
911
    public function remove()
912
    {
913
        $result = $this->doBulk('remove', func_get_args());
914
        $this->clearIdCache();
915
        return $result;
916
    }
917
918
    /**
919
     * Comments items.
920
     *
921
     * Sets new comments on all items at the current menu
922
     * which match certain criteria, using the "comment" command.
923
     *
924
     * Note that not all menus have a "comment" command. Most notably, those are
925
     * menus without items in them (e.g. "/system identity"), and menus with
926
     * fixed items (e.g. "/ip service").
927
     *
928
     * @param mixed           $numbers Targeted items. Can be any criteria
929
     *     accepted by {@link static::find()}.
930
     * @param string|resource $comment The new comment to set on the item as a
931
     *     string or a seekable stream.
932
     *     If a seekable stream is provided, it is sent from its current
933
     *     position to its end, and the pointer is seeked back to its current
934
     *     position after sending.
935
     *     Non seekable streams, as well as all other types, are casted to a
936
     *     string.
937
     *
938
     * @return ResponseCollection Returns the response collection, allowing you
939
     *     to inspect errors, if any.
940
     */
941
    public function comment($numbers, $comment)
942
    {
943
        $commentRequest = new Request($this->menu . '/comment');
944
        $commentRequest->setArgument('comment', $comment);
945
        $commentRequest->setArgument('numbers', $this->find($numbers));
946
        return $this->client->sendSync($commentRequest);
947
    }
948
949
    /**
950
     * Sets new values.
951
     *
952
     * Sets new values on certain properties on all items at the current menu
953
     * which match certain criteria.
954
     *
955
     * @param mixed                                           $numbers   Items
956
     *     to be modified.
957
     *     Can be any criteria accepted by {@link static::find()} or NULL
958
     *     in case the menu is one without items (e.g. "/system identity").
959
     * @param array<string,string|resource>|array<int,string> $newValues An
960
     *     array with the names of each property to set as an array key, and the
961
     *     new value as an array value.
962
     *     Flags (properties with a value "true" that is interpreted as
963
     *     equivalent of "yes" from CLI) can also be specified with a numeric
964
     *     index as the array key, and the name of the flag as the array value.
965
     *
966
     * @return ResponseCollection Returns the response collection, allowing you
967
     *     to inspect errors, if any.
968
     */
969
    public function set($numbers, array $newValues)
970
    {
971
        $setRequest = new Request($this->menu . '/set');
972
        foreach ($newValues as $name => $value) {
973
            if (is_int($name)) {
974
                $setRequest->setArgument($value, 'true');
0 ignored issues
show
Bug introduced by
It seems like $value defined by $value on line 972 can also be of type resource; however, PEAR2\Net\RouterOS\Request::setArgument() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
975
            } else {
976
                $setRequest->setArgument($name, $value);
977
            }
978
        }
979
        if (null !== $numbers) {
980
            $setRequest->setArgument('numbers', $this->find($numbers));
981
        }
982
        return $this->client->sendSync($setRequest);
983
    }
984
985
    /**
986
     * Alias of {@link static::set()}
987
     *
988
     * @param mixed                                           $numbers   Items
989
     *     to be modified.
990
     *     Can be any criteria accepted by {@link static::find()} or NULL
991
     *     in case the menu is one without items (e.g. "/system identity").
992
     * @param array<string,string|resource>|array<int,string> $newValues An
993
     *     array with the names of each property to set as an array key, and the
994
     *     new value as an array value.
995
     *
996
     * @return ResponseCollection Returns the response collection, allowing you
997
     *     to inspect errors, if any.
998
     */
999
    public function edit($numbers, array $newValues)
1000
    {
1001
        return $this->set($numbers, $newValues);
1002
    }
1003
1004
    /**
1005
     * Unsets a value of a specified item at the current menu.
1006
     *
1007
     * Equivalent of scripting's "unset" command. The "Value" part in the method
1008
     * name is added because "unset" is a language construct, and thus a
1009
     * reserved word.
1010
     *
1011
     * @param mixed  $numbers   Targeted items. Can be any criteria accepted
1012
     *     by {@link static::find()}.
1013
     * @param string $valueName The name of the value you want to unset.
1014
     *
1015
     * @return ResponseCollection Returns the response collection, allowing you
1016
     *     to inspect errors, if any.
1017
     */
1018
    public function unsetValue($numbers, $valueName)
1019
    {
1020
        $unsetRequest = new Request($this->menu . '/unset');
1021
        return $this->client->sendSync(
1022
            $unsetRequest->setArgument('numbers', $this->find($numbers))
1023
                ->setArgument('value-name', $valueName)
1024
        );
1025
    }
1026
1027
    /**
1028
     * Adds a new item at the current menu.
1029
     *
1030
     * @param array<string,string|resource>|array<int,string> $values Accepts
1031
     *     one or more items to add to the current menu.
1032
     *     The data about each item is specified as an array with the names of
1033
     *     each property as an array key, and the value as an array value.
1034
     *     Flags (properties with a value "true" that is interpreted as
1035
     *     equivalent of "yes" from CLI) can also be specified with a numeric
1036
     *     index as the array key, and the name of the flag as the array value.
1037
     * @param array<string,string|resource>|array<int,string> $...    Additional
1038
     *     items.
1039
     *
1040
     * @return string A comma separated list of the new items' IDs. If a
1041
     *     particular item was not added, this will be indicated by an empty
1042
     *     string in its spot on the list. e.g. "*1D,,*1E" means that
1043
     *     you supplied three items to be added, of which the second one was
1044
     *     not added for some reason.
1045
     */
1046
    public function add(array $values)
1047
    {
1048
        $addRequest = new Request($this->menu . '/add');
1049
        $idList = '';
1050
        foreach (func_get_args() as $values) {
1051
            $idList .= ',';
1052
            if (!is_array($values)) {
1053
                continue;
1054
            }
1055
            foreach ($values as $name => $value) {
1056
                if (is_int($name)) {
1057
                    $addRequest->setArgument($value, 'true');
1058
                } else {
1059
                    $addRequest->setArgument($name, $value);
1060
                }
1061
            }
1062
            $id = $this->client->sendSync($addRequest)->getProperty('ret');
1063
            if (null !== $this->idCache) {
1064
                $this->idCache[] = $id;
1065
            }
1066
            $idList .= $id;
1067
            $addRequest->removeAllArguments();
1068
        }
1069
        return substr($idList, 1);
1070
    }
1071
1072
    /**
1073
     * Moves items at the current menu before a certain other item.
1074
     *
1075
     * Moves items before a certain other item. Note that the "move"
1076
     * command is not available on all menus. As a rule of thumb, if the order
1077
     * of items in a menu is irrelevant to their interpretation, there won't
1078
     * be a move command on that menu. If in doubt, check from a terminal.
1079
     *
1080
     * @param mixed $numbers     Targeted items. Can be any criteria accepted
1081
     *     by {@link static::find()}.
1082
     * @param mixed $destination item before which the targeted items will be
1083
     *     moved to. Can be any criteria accepted by {@link static::find()}.
1084
     *     If multiple items match the criteria, the targeted items will move
1085
     *     above the first match.
1086
     *
1087
     * @return ResponseCollection Returns the response collection, allowing you
1088
     *     to inspect errors, if any.
1089
     */
1090
    public function move($numbers, $destination)
1091
    {
1092
        $moveRequest = new Request($this->menu . '/move');
1093
        $moveRequest->setArgument('numbers', $this->find($numbers));
1094
        $destination = $this->find($destination);
1095
        if (false !== strpos($destination, ',')) {
1096
            $destination = strstr($destination, ',', true);
1097
        }
1098
        $moveRequest->setArgument('destination', $destination);
1099
        $this->clearIdCache();
1100
        return $this->client->sendSync($moveRequest);
1101
    }
1102
1103
    /**
1104
     * Counts items at the current menu.
1105
     *
1106
     * Counts items at the current menu. This executes a dedicated command
1107
     * ("print" with a "count-only" argument) on RouterOS, which is why only
1108
     * queries are allowed as a criteria, in contrast with
1109
     * {@link static::find()}, where numbers and callbacks are allowed also.
1110
     *
1111
     * @param int        $mode  The counter mode.
1112
     *     Currently ignored, but present for compatibility with PHP 5.6+.
1113
     * @param Query|null $query A query to filter items by. Without it, all items
1114
     *     are included in the count.
1115
     *
1116
     * @return int The number of items, or -1 on failure (e.g. if the
1117
     *     current menu does not have a "print" command or items to be counted).
1118
     */
1119
    public function count($mode = COUNT_NORMAL, Query $query = null)
1120
    {
1121
        $result = $this->client->sendSync(
1122
            new Request($this->menu . '/print count-only=""', $query)
1123
        )->end()->getProperty('ret');
1124
1125
        if (null === $result) {
1126
            return -1;
1127
        }
1128
        if (Stream::isStream($result)) {
1129
            $result = stream_get_contents($result);
1130
        }
1131
        return (int)$result;
1132
    }
1133
1134
    /**
1135
     * Gets all items in the current menu.
1136
     *
1137
     * Gets all items in the current menu, using a print request.
1138
     *
1139
     * @param array<string,string|resource>|array<int,string> $args  Additional
1140
     *     arguments to pass to the request.
1141
     *     Each array key is the name of the argument, and each array value is
1142
     *     the value of the argument to be passed.
1143
     *     Arguments without a value (i.e. empty arguments) can also be
1144
     *     specified using a numeric key, and the name of the argument as the
1145
     *     array value.
1146
     *     The "follow" and "follow-only" arguments are prohibited,
1147
     *     as they would cause a synchronous request to run forever, without
1148
     *     allowing the results to be observed.
1149
     *     If you need to use those arguments, use {@link static::newRequest()},
1150
     *     and pass the resulting {@link Request} to {@link Client::sendAsync()}.
1151
     * @param Query|null                                      $query A query to
1152
     *     filter items by.
1153
     *     NULL to get all items.
1154
     *
1155
     * @return ResponseCollection|false A response collection with all
1156
     *     {@link Response::TYPE_DATA} responses. The collection will be empty
1157
     *     when there are no matching items. FALSE on failure.
1158
     *
1159
     * @throws NotSupportedException If $args contains prohibited arguments
1160
     *     ("follow" or "follow-only").
1161
     */
1162
    public function getAll(array $args = array(), Query $query = null)
1163
    {
1164
        $printRequest = new Request($this->menu . '/print', $query);
1165
        foreach ($args as $name => $value) {
1166
            if (is_int($name)) {
1167
                $printRequest->setArgument($value);
0 ignored issues
show
Bug introduced by
It seems like $value defined by $value on line 1165 can also be of type resource; however, PEAR2\Net\RouterOS\Request::setArgument() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1168
            } else {
1169
                $printRequest->setArgument($name, $value);
1170
            }
1171
        }
1172
        if ($printRequest->getArgument('follow') !== null
1173
            || $printRequest->getArgument('follow-only') !== null
1174
        ) {
1175
            throw new NotSupportedException(
1176
                'The "follow" and "follow-only" arguments are prohibited',
1177
                NotSupportedException::CODE_ARG_PROHIBITED
1178
            );
1179
        }
1180
        $responses = $this->client->sendSync($printRequest);
1181
1182
        if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
1183
            return false;
1184
        }
1185
        return $responses->getAllOfType(Response::TYPE_DATA);
1186
    }
1187
1188
    /**
1189
     * Puts a file on RouterOS's file system.
1190
     *
1191
     * Puts a file on RouterOS's file system, regardless of the current menu.
1192
     * Note that this is a **VERY VERY VERY** time consuming method - it takes a
1193
     * minimum of a little over 4 seconds, most of which are in sleep. It waits
1194
     * 2 seconds after a file is first created (required to actually start
1195
     * writing to the file), and another 2 seconds after its contents is written
1196
     * (performed in order to verify success afterwards).
1197
     * Similarly for removal (when $data is NULL) - there are two seconds in
1198
     * sleep, used to verify the file was really deleted.
1199
     *
1200
     * If you want an efficient way of transferring files, use (T)FTP.
1201
     * If you want an efficient way of removing files, use
1202
     * {@link static::setMenu()} to move to the "/file" menu, and call
1203
     * {@link static::remove()} without performing verification afterwards.
1204
     *
1205
     * @param string               $filename  The filename to write data in.
1206
     * @param string|resource|null $data      The data the file is going to have
1207
     *     as a string or a seekable stream.
1208
     *     Setting the value to NULL removes a file of this name.
1209
     *     If a seekable stream is provided, it is sent from its current
1210
     *     position to its end, and the pointer is seeked back to its current
1211
     *     position after sending.
1212
     *     Non seekable streams, as well as all other types, are casted to a
1213
     *     string.
1214
     * @param bool                 $overwrite Whether to overwrite the file if
1215
     *     it exists.
1216
     *
1217
     * @return bool TRUE on success, FALSE on failure.
1218
     */
1219
    public function filePutContents($filename, $data, $overwrite = false)
1220
    {
1221
        $printRequest = new Request(
1222
            '/file/print .proplist=""',
1223
            Query::where('name', $filename)
1224
        );
1225
        $fileExists = count($this->client->sendSync($printRequest)) > 1;
1226
1227
        if (null === $data) {
1228
            if (!$fileExists) {
1229
                return false;
1230
            }
1231
            $removeRequest = new Request('/file/remove');
1232
            $this->client->sendSync(
1233
                $removeRequest->setArgument('numbers', $filename)
1234
            );
1235
            //Required for RouterOS to REALLY remove the file.
1236
            sleep(2);
1237
            return !(count($this->client->sendSync($printRequest)) > 1);
1238
        }
1239
1240
        if (!$overwrite && $fileExists) {
1241
            return false;
1242
        }
1243
        $result = $this->client->sendSync(
1244
            $printRequest->setArgument('file', $filename)
1245
        );
1246
        if (count($result->getAllOfType(Response::TYPE_ERROR)) > 0) {
1247
            return false;
1248
        }
1249
        //Required for RouterOS to write the initial file.
1250
        sleep(2);
1251
        $setRequest = new Request('/file/set contents=""');
1252
        $setRequest->setArgument('numbers', $filename);
1253
        $this->client->sendSync($setRequest);
1254
        $this->client->sendSync($setRequest->setArgument('contents', $data));
1255
        //Required for RouterOS to write the file's new contents.
1256
        sleep(2);
1257
1258
        $fileSize = $this->client->sendSync(
1259
            $printRequest->setArgument('file', null)
1260
                ->setArgument('.proplist', 'size')
1261
        )->getProperty('size');
1262
        if (Stream::isStream($fileSize)) {
1263
            $fileSize = stream_get_contents($fileSize);
1264
        }
1265
        if (Communicator::isSeekableStream($data)) {
1266
            return Communicator::seekableStreamLength($data) == $fileSize;
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 1219 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...
1267
        } else {
1268
            return sprintf('%u', strlen((string)$data)) === $fileSize;
1269
        };
1270
    }
1271
1272
    /**
1273
     * Gets the contents of a specified file.
1274
     *
1275
     * @param string      $filename      The name of the file to get
1276
     *     the contents of.
1277
     * @param string|null $tmpScriptName In order to get the file's contents, a
1278
     *     script is created at "/system script", the source of which is then
1279
     *     overwritten with the file's contents, then retrieved from there,
1280
     *     after which the script is removed.
1281
     *     If this argument is left NULL, a random string,
1282
     *     prefixed with the computer's name, is generated and used
1283
     *     as the script's name.
1284
     *     To eliminate any possibility of name clashes,
1285
     *     you can specify your own name instead.
1286
     *
1287
     * @return string|resource|false The contents of the file as a string or as
1288
     *     new PHP temp stream if the underlying
1289
     *     {@link Client::isStreamingResponses()} is set to TRUE.
1290
     *     FALSE is returned if there is no such file.
1291
     */
1292
    public function fileGetContents($filename, $tmpScriptName = null)
1293
    {
1294
        $checkRequest = new Request(
1295
            '/file/print',
1296
            Query::where('name', $filename)
1297
        );
1298
        if (1 === count($this->client->sendSync($checkRequest))) {
1299
            return false;
1300
        }
1301
        $contents = $this->_exec(
1302
            '/system script set $"_" source=[/file get $filename contents]',
1303
            array('filename' => $filename),
1304
            null,
1305
            $tmpScriptName,
1306
            true
1307
        );
1308
        return $contents;
1309
    }
1310
1311
    /**
1312
     * Performs an action on a bulk of items at the current menu.
1313
     *
1314
     * @param string $what What action to perform.
1315
     * @param array  $args Zero or more arguments can be specified, each being
1316
     *     a criteria. If zero arguments are specified, removes all items.
1317
     *     See {@link static::find()} for a description of what criteria are
1318
     *     accepted.
1319
     *
1320
     * @return ResponseCollection Returns the response collection, allowing you
1321
     *     to inspect errors, if any.
1322
     */
1323
    protected function doBulk($what, array $args = array())
1324
    {
1325
        $bulkRequest = new Request($this->menu . '/' . $what);
1326
        $bulkRequest->setArgument(
1327
            'numbers',
1328
            call_user_func_array(array($this, 'find'), $args)
1329
        );
1330
        return $this->client->sendSync($bulkRequest);
1331
    }
1332
1333
    /**
1334
     * Executes a RouterOS script.
1335
     *
1336
     * Same as the public equivalent, with the addition of allowing you to get
1337
     * the contents of the script post execution, instead of removing it.
1338
     *
1339
     * @param string|resource     $source The source of the script, as a string
1340
     *     or stream. If a stream is provided, reading starts from the current
1341
     *     position to the end of the stream, and the pointer stays at the end
1342
     *     after reading is done.
1343
     * @param array<string,mixed> $params An array of parameters to make
1344
     *     available in the script as local variables.
1345
     *     Variable names are array keys, and variable values are array values.
1346
     *     Array values are automatically processed with
1347
     *     {@link static::escapeValue()}. Streams are also supported, and are
1348
     *     processed in chunks, each processed with
1349
     *     {@link static::escapeString()}. Processing starts from the current
1350
     *     position to the end of the stream, and the stream's pointer is left
1351
     *     untouched after the reading is done.
1352
     *     Note that the script's (generated) name is always added as the
1353
     *     variable "_", which will be inadvertently lost if you overwrite it
1354
     *     from here.
1355
     * @param string|null         $policy Allows you to specify a policy the
1356
     *     script must follow. Has the same format as in terminal.
1357
     *     If left NULL, the script has no restrictions beyond those imposed by
1358
     *     the username.
1359
     * @param string|null         $name   The script is executed after being
1360
     *     saved in "/system script" and is removed after execution.
1361
     *     If this argument is left NULL, a random string,
1362
     *     prefixed with the computer's name, is generated and used
1363
     *     as the script's name.
1364
     *     To eliminate any possibility of name clashes,
1365
     *     you can specify your own name instead.
1366
     * @param bool                $get    Whether to get the source
1367
     *     of the script.
1368
     *
1369
     * @return ResponseCollection|string Returns the response collection of the
1370
     *     run, allowing you to inspect errors, if any.
1371
     *     If the script was not added successfully before execution, the
1372
     *     ResponseCollection from the add attempt is going to be returned.
1373
     *     If $get is TRUE, returns the source of the script on success.
1374
     */
1375
    private function _exec(
1376
        $source,
1377
        array $params = array(),
1378
        $policy = null,
1379
        $name = null,
1380
        $get = false
1381
    ) {
1382
        $request = new Request('/system/script/add');
1383
        if (null === $name) {
1384
            $name = uniqid(gethostname(), true);
1385
        }
1386
        $request->setArgument('name', $name);
1387
        $request->setArgument('policy', $policy);
1388
1389
        $params += array('_' => $name);
1390
1391
        $finalSource = fopen('php://temp', 'r+b');
1392
        fwrite(
1393
            $finalSource,
1394
            '/' . str_replace('/', ' ', substr($this->menu, 1)). "\n"
1395
        );
1396
        static::appendScript($finalSource, $source, $params);
1397
        fwrite($finalSource, "\n");
1398
        rewind($finalSource);
1399
1400
        $request->setArgument('source', $finalSource);
1401
        $result = $this->client->sendSync($request);
1402
1403
        if (0 === count($result->getAllOfType(Response::TYPE_ERROR))) {
1404
            $request = new Request('/system/script/run');
1405
            $request->setArgument('number', $name);
1406
            $result = $this->client->sendSync($request);
1407
1408
            if ($get) {
1409
                $result = $this->client->sendSync(
1410
                    new Request(
1411
                        '/system/script/print .proplist="source"',
1412
                        Query::where('name', $name)
1413
                    )
1414
                )->getProperty('source');
1415
            }
1416
            $request = new Request('/system/script/remove');
1417
            $request->setArgument('numbers', $name);
1418
            $this->client->sendSync($request);
1419
        }
1420
1421
        return $result;
1422
    }
1423
}
1424