Completed
Branch develop (f938b5)
by Vasil
03:23
created

Util::find()   D

Complexity

Conditions 22
Paths 26

Size

Total Lines 86
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 17
Bugs 8 Features 12
Metric Value
c 17
b 8
f 12
dl 0
loc 86
rs 4.7292
cc 22
eloc 67
nc 26
nop 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * ~~summary~~
5
 *
6
 * ~~description~~
7
 *
8
 * PHP version 5
9
 *
10
 * @category  Net
11
 * @package   PEAR2_Net_RouterOS
12
 * @author    Vasil Rangelov <[email protected]>
13
 * @copyright 2011 Vasil Rangelov
14
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
15
 * @version   GIT: $Id$
16
 * @link      http://pear2.php.net/PEAR2_Net_RouterOS
17
 */
18
/**
19
 * The namespace declaration.
20
 */
21
namespace PEAR2\Net\RouterOS;
22
23
/**
24
 * 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
                (?<mon>jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)
200
                /
201
                (?<day>\d\d?)
202
                /
203
                (?<year>\d{4})
204
                (?:
205
                    \s+(?<time>\d{2}\:\d{2}:\d{2})
206
                )?
207
            $#uix',
208
            $value,
209
            $date
210
        )) {
211
            if (!isset($date['time'])) {
212
                $date['time'] = '00:00:00';
213
            }
214
            try {
215
                return new DateTime(
216
                    $date['year'] .
217
                    '-' . ucfirst($date['mon']) .
218
                    "-{$date['day']} {$date['time']}",
219
                    new DateTimeZone('UTC')
220
                );
221
            } catch (E $e) {
222
                return $value;
223
            }
224
        } elseif (('"' === $value[0]) && substr(strrev($value), 0, 1) === '"') {
225
            return str_replace(
226
                array('\"', '\\\\', "\\\n", "\\\r\n", "\\\r"),
227
                array('"', '\\'),
228
                substr($value, 1, -1)
229
            );
230
        } elseif ('{' === $value[0]) {
231
            $len = strlen($value);
232
            if ($value[$len - 1] === '}') {
233
                $value = substr($value, 1, -1);
234
                if ('' === $value) {
235
                    return array();
236
                }
237
                $parsedValue = preg_split(
238
                    '/
239
                        (\"(?:\\\\\\\\|\\\\"|[^"])*\")
240
                        |
241
                        (\{[^{}]*(?2)?\})
242
                        |
243
                        ([^;=]+)
244
                    /sx',
245
                    $value,
246
                    null,
247
                    PREG_SPLIT_DELIM_CAPTURE
248
                );
249
                $result = array();
250
                $newVal = null;
251
                $newKey = null;
252
                for ($i = 0, $l = count($parsedValue); $i < $l; ++$i) {
253
                    switch ($parsedValue[$i]) {
254
                    case '':
255
                        break;
256
                    case ';':
257
                        if (null === $newKey) {
258
                            $result[] = $newVal;
259
                        } else {
260
                            $result[$newKey] = $newVal;
261
                        }
262
                        $newKey = $newVal = null;
263
                        break;
264
                    case '=':
265
                        $newKey = static::parseValue($parsedValue[$i - 1]);
266
                        $newVal = static::parseValue($parsedValue[++$i]);
267
                        break;
268
                    default:
269
                        $newVal = static::parseValue($parsedValue[$i]);
270
                    }
271
                }
272
                if (null === $newKey) {
273
                    $result[] = $newVal;
274
                } else {
275
                    $result[$newKey] = $newVal;
276
                }
277
                return $result;
278
            }
279
        }
280
        return $value;
281
    }
282
283
    /**
284
     * Prepares a script.
285
     *
286
     * Prepares a script for eventual execution by prepending parameters as
287
     * variables to it.
288
     *
289
     * This is particularly useful when you're creating scripts that you don't
290
     * want to execute right now (as with {@link static::exec()}, but instead
291
     * you want to store it for later execution, perhaps by supplying it to
292
     * "/system scheduler".
293
     *
294
     * @param string|resource     $source The source of the script, as a string
295
     *     or stream. If a stream is provided, reading starts from the current
296
     *     position to the end of the stream, and the pointer stays at the end
297
     *     after reading is done.
298
     * @param array<string,mixed> $params An array of parameters to make
299
     *     available in the script as local variables.
300
     *     Variable names are array keys, and variable values are array values.
301
     *     Array values are automatically processed with
302
     *     {@link static::escapeValue()}. Streams are also supported, and are
303
     *     processed in chunks, each with
304
     *     {@link static::escapeString()}. Processing starts from the current
305
     *     position to the end of the stream, and the stream's pointer stays at
306
     *     the end after reading is done.
307
     *
308
     * @return resource A new PHP temporary stream with the script as contents,
309
     *     with the pointer back at the start.
310
     *
311
     * @see static::appendScript()
312
     */
313
    public static function prepareScript(
314
        $source,
315
        array $params = array()
316
    ) {
317
        $resultStream = fopen('php://temp', 'r+b');
318
        self::appendScript($resultStream, $source, $params);
319
        rewind($resultStream);
320
        return $resultStream;
321
    }
322
323
    /**
324
     * Appends a script.
325
     *
326
     * Appends a script to an existing stream.
327
     *
328
     * @param resource            $stream An existing stream to write the
329
     *     resulting script to.
330
     * @param string|resource     $source The source of the script, as a string
331
     *     or stream. If a stream is provided, reading starts from the current
332
     *     position to the end of the stream, and the pointer stays at the end
333
     *     after reading is done.
334
     * @param array<string,mixed> $params An array of parameters to make
335
     *     available in the script as local variables.
336
     *     Variable names are array keys, and variable values are array values.
337
     *     Array values are automatically processed with
338
     *     {@link static::escapeValue()}. Streams are also supported, and are
339
     *     processed in chunks, each with
340
     *     {@link static::escapeString()}. Processing starts from the current
341
     *     position to the end of the stream, and the stream's pointer stays at
342
     *     the end after reading is done.
343
     *
344
     * @return int The number of bytes written to $stream is returned,
345
     *     and the pointer remains where it was after the write
346
     *     (i.e. it is not seeked back, even if seeking is supported).
347
     */
348
    public static function appendScript(
349
        $stream,
350
        $source,
351
        array $params = array()
352
    ) {
353
        $writer = new Stream($stream, false);
354
        $bytes = 0;
355
356
        foreach ($params as $pname => $pvalue) {
357
            $pname = static::escapeString($pname);
358
            $bytes += $writer->send(":local \"{$pname}\" ");
359
            if (Stream::isStream($pvalue)) {
360
                $reader = new Stream($pvalue, false);
361
                $chunkSize = $reader->getChunk(Stream::DIRECTION_RECEIVE);
362
                $bytes += $writer->send('"');
363
                while ($reader->isAvailable() && $reader->isDataAwaiting()) {
364
                    $bytes += $writer->send(
365
                        static::escapeString(fread($pvalue, $chunkSize))
366
                    );
367
                }
368
                $bytes += $writer->send("\";\n");
369
            } else {
370
                $bytes += $writer->send(static::escapeValue($pvalue) . ";\n");
371
            }
372
        }
373
374
        $bytes += $writer->send($source);
375
        return $bytes;
376
    }
377
378
    /**
379
     * Escapes a value for a RouterOS scripting context.
380
     *
381
     * Turns any native PHP value into an equivalent whole value that can be
382
     * inserted as part of a RouterOS script.
383
     *
384
     * DateInterval objects will be casted to RouterOS' "time" type.
385
     * 
386
     * DateTime objects will be casted to a string following the "M/d/Y H:i:s"
387
     * format. If the time is exactly midnight (including microseconds), and
388
     * the timezone is UTC, the string will include only the "M/d/Y" date.
389
     *
390
     * Unrecognized types (i.e. resources and other objects) are casted to
391
     * strings.
392
     *
393
     * @param mixed $value The value to be escaped.
394
     *
395
     * @return string A string representation that can be directly inserted in a
396
     *     script as a whole value.
397
     */
398
    public static function escapeValue($value)
399
    {
400
        switch(gettype($value)) {
401
        case 'NULL':
402
            $value = '';
403
            break;
404
        case 'integer':
405
            $value = (string)$value;
406
            break;
407
        case 'boolean':
408
            $value = $value ? 'true' : 'false';
409
            break;
410
        case 'array':
411
            if (0 === count($value)) {
412
                $value = '({})';
413
                break;
414
            }
415
            $result = '';
416
            foreach ($value as $key => $val) {
417
                $result .= ';';
418
                if (!is_int($key)) {
419
                    $result .= static::escapeValue($key) . '=';
420
                }
421
                $result .= static::escapeValue($val);
422
            }
423
            $value = '{' . substr($result, 1) . '}';
424
            break;
425
        case 'object':
426
            if ($value instanceof DateTime) {
427
                $usec = $value->format('u');
428
                $usec = '000000' === $usec ? '' : '.' . $usec;
429
                $value = '00:00:00.000000 UTC' === $value->format('H:i:s.u e')
430
                    ? $value->format('M/d/Y')
431
                    : $value->format('M/d/Y H:i:s') . $usec;
432
            }
433
            if ($value instanceof DateInterval) {
434
                if (false === $value->days || $value->days < 0) {
435
                    $value = $value->format('%r%dd%H:%I:%S');
436
                } else {
437
                    $value = $value->format('%r%ad%H:%I:%S');
438
                }
439
                break;
440
            }
441
            //break; intentionally omitted
442
        default:
443
            $value = '"' . static::escapeString((string)$value) . '"';
444
            break;
445
        }
446
        return $value;
447
    }
448
449
    /**
450
     * Escapes a string for a RouterOS scripting context.
451
     *
452
     * Escapes a string for a RouterOS scripting context. The value can then be
453
     * surrounded with quotes at a RouterOS script (or concatenated onto a
454
     * larger string first), and you can be sure there won't be any code
455
     * injections coming from it.
456
     *
457
     * @param string $value Value to be escaped.
458
     *
459
     * @return string The escaped value.
460
     */
461
    public static function escapeString($value)
462
    {
463
        return preg_replace_callback(
464
            '/[^\\_A-Za-z0-9]+/S',
465
            array(__CLASS__, '_escapeCharacters'),
466
            $value
467
        );
468
    }
469
470
    /**
471
     * Escapes a character for a RouterOS scripting context.
472
     *
473
     * Escapes a character for a RouterOS scripting context. Intended to only be
474
     * called for non-alphanumeric characters.
475
     *
476
     * @param string $chars The matches array, expected to contain exactly one
477
     *     member, in which is the whole string to be escaped.
478
     *
479
     * @return string The escaped characters.
480
     */
481
    private static function _escapeCharacters($chars)
482
    {
483
        $result = '';
484
        for ($i = 0, $l = strlen($chars[0]); $i < $l; ++$i) {
485
            $result .= '\\' . str_pad(
486
                strtoupper(dechex(ord($chars[0][$i]))),
487
                2,
488
                '0',
489
                STR_PAD_LEFT
490
            );
491
        }
492
        return $result;
493
    }
494
495
    /**
496
     * Creates a new Util instance.
497
     *
498
     * Wraps around a connection to provide convenience methods.
499
     *
500
     * @param Client $client The connection to wrap around.
501
     */
502
    public function __construct(Client $client)
503
    {
504
        $this->client = $client;
505
    }
506
507
    /**
508
     * Gets the current menu.
509
     *
510
     * @return string The absolute path to current menu, using API syntax.
511
     */
512
    public function getMenu()
513
    {
514
        return $this->menu;
515
    }
516
517
    /**
518
     * Sets the current menu.
519
     *
520
     * Sets the current menu.
521
     *
522
     * @param string $newMenu The menu to change to. Can be specified with API
523
     *     or CLI syntax and can be either absolute or relative. If relative,
524
     *     it's relative to the current menu, which by default is the root.
525
     *
526
     * @return $this The object itself. If an empty string is given for
527
     *     a new menu, no change is performed,
528
     *     but the ID cache is cleared anyway.
529
     *
530
     * @see static::clearIdCache()
531
     */
532
    public function setMenu($newMenu)
533
    {
534
        $newMenu = (string)$newMenu;
535
        if ('' !== $newMenu) {
536
            $menuRequest = new Request('/menu');
537
            if ('/' === $newMenu) {
538
                $this->menu = '/';
539
            } elseif ('/' === $newMenu[0]) {
540
                $this->menu = $menuRequest->setCommand($newMenu)->getCommand();
541
            } else {
542
                $this->menu = $menuRequest->setCommand(
543
                    '/' . str_replace('/', ' ', substr($this->menu, 1)) . ' ' .
544
                    str_replace('/', ' ', $newMenu)
545
                )->getCommand();
546
            }
547
        }
548
        $this->clearIdCache();
549
        return $this;
550
    }
551
552
    /**
553
     * Creates a Request object.
554
     *
555
     * Creates a {@link Request} object, with a command that's at the
556
     * current menu. The request can then be sent using {@link Client}.
557
     *
558
     * @param string      $command The command of the request, not including
559
     *     the menu. The request will have that command at the current menu.
560
     * @param array       $args    Arguments of the request.
561
     *     Each array key is the name of the argument, and each array value is
562
     *     the value of the argument to be passed.
563
     *     Arguments without a value (i.e. empty arguments) can also be
564
     *     specified using a numeric key, and the name of the argument as the
565
     *     array value.
566
     * @param Query|null  $query   The {@link Query} of the request.
567
     * @param string|null $tag     The tag of the request.
568
     *
569
     * @return Request The {@link Request} object.
570
     *
571
     * @throws NotSupportedException On an attempt to call a command in a
572
     *     different menu using API syntax.
573
     * @throws InvalidArgumentException On an attempt to call a command in a
574
     *     different menu using CLI syntax.
575
     */
576
    public function newRequest(
577
        $command,
578
        array $args = array(),
579
        Query $query = null,
580
        $tag = null
581
    ) {
582
        if (false !== strpos($command, '/')) {
583
            throw new NotSupportedException(
584
                'Command tried to go to a different menu',
585
                NotSupportedException::CODE_MENU_MISMATCH,
586
                null,
587
                $command
588
            );
589
        }
590
        $request = new Request('/menu', $query, $tag);
591
        $request->setCommand("{$this->menu}/{$command}");
592
        foreach ($args as $name => $value) {
593
            if (is_int($name)) {
594
                $request->setArgument($value);
595
            } else {
596
                $request->setArgument($name, $value);
597
            }
598
        }
599
        return $request;
600
    }
601
602
    /**
603
     * Executes a RouterOS script.
604
     *
605
     * Executes a RouterOS script, written as a string or a stream.
606
     * Note that in cases of errors, the line numbers will be off, because the
607
     * script is executed at the current menu as context, with the specified
608
     * variables pre declared. This is achieved by prepending 1+count($params)
609
     * lines before your actual script.
610
     *
611
     * @param string|resource     $source The source of the script, as a string
612
     *     or stream. If a stream is provided, reading starts from the current
613
     *     position to the end of the stream, and the pointer stays at the end
614
     *     after reading is done.
615
     * @param array<string,mixed> $params An array of parameters to make
616
     *     available in the script as local variables.
617
     *     Variable names are array keys, and variable values are array values.
618
     *     Array values are automatically processed with
619
     *     {@link static::escapeValue()}. Streams are also supported, and are
620
     *     processed in chunks, each processed with
621
     *     {@link static::escapeString()}. Processing starts from the current
622
     *     position to the end of the stream, and the stream's pointer is left
623
     *     untouched after the reading is done.
624
     *     Note that the script's (generated) name is always added as the
625
     *     variable "_", which will be inadvertently lost if you overwrite it
626
     *     from here.
627
     * @param string|null         $policy Allows you to specify a policy the
628
     *     script must follow. Has the same format as in terminal.
629
     *     If left NULL, the script has no restrictions beyond those imposed by
630
     *     the username.
631
     * @param string|null         $name   The script is executed after being
632
     *     saved in "/system script" and is removed after execution.
633
     *     If this argument is left NULL, a random string,
634
     *     prefixed with the computer's name, is generated and used
635
     *     as the script's name.
636
     *     To eliminate any possibility of name clashes,
637
     *     you can specify your own name instead.
638
     *
639
     * @return ResponseCollection Returns the response collection of the
0 ignored issues
show
Documentation introduced by
Should the return type not be ResponseCollection|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
640
     *     run, allowing you to inspect errors, if any.
641
     *     If the script was not added successfully before execution, the
642
     *     ResponseCollection from the add attempt is going to be returned.
643
     */
644
    public function exec(
645
        $source,
646
        array $params = array(),
647
        $policy = null,
648
        $name = null
649
    ) {
650
        return $this->_exec($source, $params, $policy, $name);
651
    }
652
653
    /**
654
     * Clears the ID cache.
655
     *
656
     * Normally, the ID cache improves performance when targeting items by a
657
     * number. If you're using both Util's methods and other means (e.g.
658
     * {@link Client} or {@link Util::exec()}) to add/move/remove items, the
659
     * cache may end up being out of date. By calling this method right before
660
     * targeting an item with a number, you can ensure number accuracy.
661
     *
662
     * Note that Util's {@link static::move()} and {@link static::remove()}
663
     * methods automatically clear the cache before returning, while
664
     * {@link static::add()} adds the new item's ID to the cache as the next
665
     * number. A change in the menu also clears the cache.
666
     *
667
     * Note also that the cache is being rebuilt unconditionally every time you
668
     * use {@link static::find()} with a callback.
669
     *
670
     * @return $this The Util object itself.
671
     */
672
    public function clearIdCache()
673
    {
674
        $this->idCache = null;
675
        return $this;
676
    }
677
678
    /**
679
     * Gets the current time on the router.
680
     * 
681
     * Gets the current time on the router, regardless of the current menu.
682
     * 
683
     * If the timezone is one known to both RouterOS and PHP, it will be used
684
     * as the timezone identifier. Otherwise (e.g. "manual"), the current GMT
685
     * offset will be used as a timezone, without any DST awareness.
686
     * 
687
     * @return DateTime The current time of the router, as a DateTime object.
688
     */
689
    public function getCurrentTime()
690
    {
691
        $clock = $this->client->sendSync(
692
            new Request(
693
                '/system/clock/print 
694
                .proplist=date,time,time-zone-name,gmt-offset'
695
            )
696
        )->current();
697
        $clockParts = array();
698
        foreach (array(
699
            'date',
700
            'time',
701
            'time-zone-name',
702
            'gmt-offset'
703
        ) as $clockPart) {
704
            $clockParts[$clockPart] = $clock->getProperty($clockPart);
705
            if (Stream::isStream($clockParts[$clockPart])) {
706
                $clockParts[$clockPart] = stream_get_contents(
707
                    $clockParts[$clockPart]
708
                );
709
            }
710
        }
711
        $datetime = ucfirst(strtolower($clockParts['date'])) . ' ' .
712
            $clockParts['time'];
713
        try {
714
            $result = DateTime::createFromFormat(
715
                'M/j/Y H:i:s',
716
                $datetime,
717
                new DateTimeZone($clockParts['time-zone-name'])
718
            );
719
        } catch (E $e) {
720
            $result = DateTime::createFromFormat(
721
                'M/j/Y H:i:s P',
722
                $datetime . ' ' . $clockParts['gmt-offset'],
723
                new DateTimeZone('UTC')
724
            );
725
        }
726
        return $result;
727
    }
728
729
    /**
730
     * Finds the IDs of items at the current menu.
731
     *
732
     * Finds the IDs of items based on specified criteria, and returns them as
733
     * a comma separated string, ready for insertion at a "numbers" argument.
734
     *
735
     * Accepts zero or more criteria as arguments. If zero arguments are
736
     * specified, returns all items' IDs. The value of each criteria can be a
737
     * number (just as in Winbox), a literal ID to be included, a {@link Query}
738
     * object, or a callback. If a callback is specified, it is called for each
739
     * item, with the item as an argument. If it returns a true value, the
740
     * item's ID is included in the result. Every other value is casted to a
741
     * string. A string is treated as a comma separated values of IDs, numbers
742
     * or callback names. Non-existent callback names are instead placed in the
743
     * result, which may be useful in menus that accept identifiers other than
744
     * IDs, but note that it can cause errors on other menus.
745
     *
746
     * @return string A comma separated list of all items matching the
747
     *     specified criteria.
748
     */
749
    public function find()
750
    {
751
        if (func_num_args() === 0) {
752
            if (null === $this->idCache) {
753
                $ret = $this->client->sendSync(
754
                    new Request($this->menu . '/find')
755
                )->getProperty('ret');
756
                if (null === $ret) {
757
                    $this->idCache = array();
758
                    return '';
759
                } elseif (!is_string($ret)) {
760
                    $ret = stream_get_contents($ret);
761
                }
762
763
                $idCache = str_replace(
764
                    ';',
765
                    ',',
766
                    $ret
767
                );
768
                $this->idCache = explode(',', $idCache);
769
                return $idCache;
770
            }
771
            return implode(',', $this->idCache);
772
        }
773
        $idList = '';
774
        foreach (func_get_args() as $criteria) {
775
            if ($criteria instanceof Query) {
776
                foreach ($this->client->sendSync(
777
                    new Request($this->menu . '/print .proplist=.id', $criteria)
778
                )->getAllOfType(Response::TYPE_DATA) as $response) {
779
                    $newId = $response->getProperty('.id');
780
                    $idList .= is_string($newId)
781
                        ? $newId . ','
782
                        : stream_get_contents($newId) . ',';
783
                }
784
            } elseif (is_callable($criteria)) {
785
                $idCache = array();
786
                foreach ($this->client->sendSync(
787
                    new Request($this->menu . '/print')
788
                )->getAllOfType(Response::TYPE_DATA) as $response) {
789
                    $newId = $response->getProperty('.id');
790
                    $newId = is_string($newId)
791
                        ? $newId
792
                        : stream_get_contents($newId);
793
                    if ($criteria($response)) {
794
                        $idList .= $newId . ',';
795
                    }
796
                    $idCache[] = $newId;
797
                }
798
                $this->idCache = $idCache;
799
            } else {
800
                $this->find();
801
                if (is_int($criteria)) {
802
                    if (isset($this->idCache[$criteria])) {
803
                        $idList = $this->idCache[$criteria] . ',';
804
                    }
805
                } else {
806
                    $criteria = (string)$criteria;
807
                    if ($criteria === (string)(int)$criteria) {
808
                        if (isset($this->idCache[(int)$criteria])) {
809
                            $idList .= $this->idCache[(int)$criteria] . ',';
810
                        }
811
                    } elseif (false === strpos($criteria, ',')) {
812
                        $idList .= $criteria . ',';
813
                    } else {
814
                        $criteriaArr = explode(',', $criteria);
815
                        for ($i = count($criteriaArr) - 1; $i >= 0; --$i) {
816
                            if ('' === $criteriaArr[$i]) {
817
                                unset($criteriaArr[$i]);
818
                            } elseif ('*' === $criteriaArr[$i][0]) {
819
                                $idList .= $criteriaArr[$i] . ',';
820
                                unset($criteriaArr[$i]);
821
                            }
822
                        }
823
                        if (!empty($criteriaArr)) {
824
                            $idList .= call_user_func_array(
825
                                array($this, 'find'),
826
                                $criteriaArr
827
                            ) . ',';
828
                        }
829
                    }
830
                }
831
            }
832
        }
833
        return rtrim($idList, ',');
834
    }
835
836
    /**
837
     * Gets a value of a specified item at the current menu.
838
     *
839
     * @param int|string|null $number    A number identifying the item you're
840
     *     targeting. Can also be an ID or (in some menus) name. For menus where
841
     *     there are no items (e.g. "/system identity"), you can specify NULL.
842
     * @param string          $valueName The name of the value you want to get.
843
     *
844
     * @return string|resource|null|false The value of the specified property as
845
     *     a string or as new PHP temp stream if the underlying
846
     *     {@link Client::isStreamingResponses()} is set to TRUE.
847
     *     If the property is not set, NULL will be returned. FALSE on failure
848
     *     (e.g. no such item, invalid property, etc.).
849
     */
850
    public function get($number, $valueName)
851
    {
852
        if (is_int($number) || ((string)$number === (string)(int)$number)) {
853
            $this->find();
854
            if (isset($this->idCache[(int)$number])) {
855
                $number = $this->idCache[(int)$number];
856
            } else {
857
                return false;
858
            }
859
        }
860
861
        //For new RouterOS versions
862
        $request = new Request($this->menu . '/get');
863
        $request->setArgument('number', $number);
864
        $request->setArgument('value-name', $valueName);
865
        $responses = $this->client->sendSync($request);
866
        if (Response::TYPE_ERROR === $responses->getType()) {
867
            return false;
868
        }
869
        $result = $responses->getProperty('ret');
870
        if (null !== $result) {
871
            return $result;
872
        }
873
874
        // The "get" of old RouterOS versions returns an empty !done response.
875
        // New versions return such only when the property is not set.
876
        // This is a backup for old versions' sake.
877
        $query = null;
878
        if (null !== $number) {
879
            $number = (string)$number;
880
            $query = Query::where('.id', $number)->orWhere('name', $number);
881
        }
882
        $responses = $this->getAll(
883
            array('.proplist' => $valueName, 'detail'),
884
            $query
885
        );
886
887
        if (0 === count($responses)) {
888
            // @codeCoverageIgnoreStart
889
            // New versions of RouterOS can't possibly reach this section.
890
            return false;
891
            // @codeCoverageIgnoreEnd
892
        }
893
        return $responses->getProperty($valueName);
894
    }
895
896
    /**
897
     * Enables all items at the current menu matching certain criteria.
898
     *
899
     * Zero or more arguments can be specified, each being a criteria.
900
     * If zero arguments are specified, enables all items.
901
     * See {@link static::find()} for a description of what criteria are
902
     * accepted.
903
     *
904
     * @return ResponseCollection returns the response collection, allowing you
905
     *     to inspect errors, if any.
906
     */
907
    public function enable()
908
    {
909
        return $this->doBulk('enable', func_get_args());
910
    }
911
912
    /**
913
     * Disables all items at the current menu matching certain criteria.
914
     *
915
     * Zero or more arguments can be specified, each being a criteria.
916
     * If zero arguments are specified, disables all items.
917
     * See {@link static::find()} for a description of what criteria are
918
     * accepted.
919
     *
920
     * @return ResponseCollection Returns the response collection, allowing you
921
     *     to inspect errors, if any.
922
     */
923
    public function disable()
924
    {
925
        return $this->doBulk('disable', func_get_args());
926
    }
927
928
    /**
929
     * Removes all items at the current menu matching certain criteria.
930
     *
931
     * Zero or more arguments can be specified, each being a criteria.
932
     * If zero arguments are specified, removes all items.
933
     * See {@link static::find()} for a description of what criteria are
934
     * accepted.
935
     *
936
     * @return ResponseCollection Returns the response collection, allowing you
937
     *     to inspect errors, if any.
938
     */
939
    public function remove()
940
    {
941
        $result = $this->doBulk('remove', func_get_args());
942
        $this->clearIdCache();
943
        return $result;
944
    }
945
946
    /**
947
     * Comments items.
948
     *
949
     * Sets new comments on all items at the current menu
950
     * which match certain criteria, using the "comment" command.
951
     *
952
     * Note that not all menus have a "comment" command. Most notably, those are
953
     * menus without items in them (e.g. "/system identity"), and menus with
954
     * fixed items (e.g. "/ip service").
955
     *
956
     * @param mixed           $numbers Targeted items. Can be any criteria
957
     *     accepted by {@link static::find()}.
958
     * @param string|resource $comment The new comment to set on the item as a
959
     *     string or a seekable stream.
960
     *     If a seekable stream is provided, it is sent from its current
961
     *     position to its end, and the pointer is seeked back to its current
962
     *     position after sending.
963
     *     Non seekable streams, as well as all other types, are casted to a
964
     *     string.
965
     *
966
     * @return ResponseCollection Returns the response collection, allowing you
967
     *     to inspect errors, if any.
968
     */
969
    public function comment($numbers, $comment)
970
    {
971
        $commentRequest = new Request($this->menu . '/comment');
972
        $commentRequest->setArgument('comment', $comment);
973
        $commentRequest->setArgument('numbers', $this->find($numbers));
974
        return $this->client->sendSync($commentRequest);
975
    }
976
977
    /**
978
     * Sets new values.
979
     *
980
     * Sets new values on certain properties on all items at the current menu
981
     * which match certain criteria.
982
     *
983
     * @param mixed                                           $numbers   Items
984
     *     to be modified.
985
     *     Can be any criteria accepted by {@link static::find()} or NULL
986
     *     in case the menu is one without items (e.g. "/system identity").
987
     * @param array<string,string|resource>|array<int,string> $newValues An
988
     *     array with the names of each property to set as an array key, and the
989
     *     new value as an array value.
990
     *     Flags (properties with a value "true" that is interpreted as
991
     *     equivalent of "yes" from CLI) can also be specified with a numeric
992
     *     index as the array key, and the name of the flag as the array value.
993
     *
994
     * @return ResponseCollection Returns the response collection, allowing you
995
     *     to inspect errors, if any.
996
     */
997
    public function set($numbers, array $newValues)
998
    {
999
        $setRequest = new Request($this->menu . '/set');
1000
        foreach ($newValues as $name => $value) {
1001
            if (is_int($name)) {
1002
                $setRequest->setArgument($value, 'true');
1003
            } else {
1004
                $setRequest->setArgument($name, $value);
1005
            }
1006
        }
1007
        if (null !== $numbers) {
1008
            $setRequest->setArgument('numbers', $this->find($numbers));
1009
        }
1010
        return $this->client->sendSync($setRequest);
1011
    }
1012
1013
    /**
1014
     * Alias of {@link static::set()}
1015
     *
1016
     * @param mixed                $numbers   Items to be modified.
1017
     *     Can be any criteria accepted by {@link static::find()} or NULL
1018
     *     in case the menu is one without items (e.g. "/system identity").
1019
     * @param string               $valueName Name of property to be modified.
1020
     * @param string|resource|null $newValue  The new value to set.
1021
     *     If set to NULL, the property is unset.
1022
     *
1023
     * @return ResponseCollection Returns the response collection, allowing you
1024
     *     to inspect errors, if any.
1025
     */
1026
    public function edit($numbers, $valueName, $newValue)
1027
    {
1028
        return null === $newValue
1029
            ? $this->unsetValue($numbers, $valueName)
1030
            : $this->set($numbers, array($valueName => $newValue));
1031
    }
1032
1033
    /**
1034
     * Unsets a value of a specified item at the current menu.
1035
     *
1036
     * Equivalent of scripting's "unset" command. The "Value" part in the method
1037
     * name is added because "unset" is a language construct, and thus a
1038
     * reserved word.
1039
     *
1040
     * @param mixed  $numbers   Targeted items. Can be any criteria accepted
1041
     *     by {@link static::find()}.
1042
     * @param string $valueName The name of the value you want to unset.
1043
     *
1044
     * @return ResponseCollection Returns the response collection, allowing you
1045
     *     to inspect errors, if any.
1046
     */
1047
    public function unsetValue($numbers, $valueName)
1048
    {
1049
        $unsetRequest = new Request($this->menu . '/unset');
1050
        return $this->client->sendSync(
1051
            $unsetRequest->setArgument('numbers', $this->find($numbers))
1052
                ->setArgument('value-name', $valueName)
1053
        );
1054
    }
1055
1056
    /**
1057
     * Adds a new item at the current menu.
1058
     *
1059
     * @param array<string,string|resource>|array<int,string> $values Accepts
1060
     *     one or more items to add to the current menu.
1061
     *     The data about each item is specified as an array with the names of
1062
     *     each property as an array key, and the value as an array value.
1063
     *     Flags (properties with a value "true" that is interpreted as
1064
     *     equivalent of "yes" from CLI) can also be specified with a numeric
1065
     *     index as the array key, and the name of the flag as the array value.
1066
     * @param array<string,string|resource>|array<int,string> $...    Additional
1067
     *     items.
1068
     *
1069
     * @return string A comma separated list of the new items' IDs. If a
1070
     *     particular item was not added, this will be indicated by an empty
1071
     *     string in its spot on the list. e.g. "*1D,,*1E" means that
1072
     *     you supplied three items to be added, of which the second one was
1073
     *     not added for some reason.
1074
     */
1075
    public function add(array $values)
1076
    {
1077
        $addRequest = new Request($this->menu . '/add');
1078
        $idList = '';
1079
        foreach (func_get_args() as $values) {
1080
            $idList .= ',';
1081
            if (!is_array($values)) {
1082
                continue;
1083
            }
1084
            foreach ($values as $name => $value) {
1085
                if (is_int($name)) {
1086
                    $addRequest->setArgument($value, 'true');
1087
                } else {
1088
                    $addRequest->setArgument($name, $value);
1089
                }
1090
            }
1091
            $id = $this->client->sendSync($addRequest)->getProperty('ret');
1092
            if (null !== $this->idCache) {
1093
                $this->idCache[] = $id;
1094
            }
1095
            $idList .= $id;
1096
            $addRequest->removeAllArguments();
1097
        }
1098
        return substr($idList, 1);
1099
    }
1100
1101
    /**
1102
     * Moves items at the current menu before a certain other item.
1103
     *
1104
     * Moves items before a certain other item. Note that the "move"
1105
     * command is not available on all menus. As a rule of thumb, if the order
1106
     * of items in a menu is irrelevant to their interpretation, there won't
1107
     * be a move command on that menu. If in doubt, check from a terminal.
1108
     *
1109
     * @param mixed $numbers     Targeted items. Can be any criteria accepted
1110
     *     by {@link static::find()}.
1111
     * @param mixed $destination item before which the targeted items will be
1112
     *     moved to. Can be any criteria accepted by {@link static::find()}.
1113
     *     If multiple items match the criteria, the targeted items will move
1114
     *     above the first match.
1115
     *
1116
     * @return ResponseCollection Returns the response collection, allowing you
1117
     *     to inspect errors, if any.
1118
     */
1119
    public function move($numbers, $destination)
1120
    {
1121
        $moveRequest = new Request($this->menu . '/move');
1122
        $moveRequest->setArgument('numbers', $this->find($numbers));
1123
        $destination = $this->find($destination);
1124
        if (false !== strpos($destination, ',')) {
1125
            $destination = strstr($destination, ',', true);
1126
        }
1127
        $moveRequest->setArgument('destination', $destination);
1128
        $this->clearIdCache();
1129
        return $this->client->sendSync($moveRequest);
1130
    }
1131
1132
    /**
1133
     * Counts items at the current menu.
1134
     *
1135
     * Counts items at the current menu. This executes a dedicated command
1136
     * ("print" with a "count-only" argument) on RouterOS, which is why only
1137
     * queries are allowed as a criteria, in contrast with
1138
     * {@link static::find()}, where numbers and callbacks are allowed also.
1139
     *
1140
     * @param int        $mode  The counter mode.
1141
     *     Currently ignored, but present for compatibility with PHP 5.6+.
1142
     * @param Query|null $query A query to filter items by. Without it, all items
1143
     *     are included in the count.
1144
     *
1145
     * @return int The number of items, or -1 on failure (e.g. if the
1146
     *     current menu does not have a "print" command or items to be counted).
1147
     */
1148
    public function count($mode = COUNT_NORMAL, Query $query = null)
1149
    {
1150
        $result = $this->client->sendSync(
1151
            new Request($this->menu . '/print count-only=""', $query)
1152
        )->end()->getProperty('ret');
1153
1154
        if (null === $result) {
1155
            return -1;
1156
        }
1157
        if (Stream::isStream($result)) {
1158
            $result = stream_get_contents($result);
1159
        }
1160
        return (int)$result;
1161
    }
1162
1163
    /**
1164
     * Gets all items in the current menu.
1165
     *
1166
     * Gets all items in the current menu, using a print request.
1167
     *
1168
     * @param array<string,string|resource>|array<int,string> $args  Additional
1169
     *     arguments to pass to the request.
1170
     *     Each array key is the name of the argument, and each array value is
1171
     *     the value of the argument to be passed.
1172
     *     Arguments without a value (i.e. empty arguments) can also be
1173
     *     specified using a numeric key, and the name of the argument as the
1174
     *     array value.
1175
     *     The "follow" and "follow-only" arguments are prohibited,
1176
     *     as they would cause a synchronous request to run forever, without
1177
     *     allowing the results to be observed.
1178
     *     If you need to use those arguments, use {@link static::newRequest()},
1179
     *     and pass the resulting {@link Request} to {@link Client::sendAsync()}.
1180
     * @param Query|null                                      $query A query to
1181
     *     filter items by.
1182
     *     NULL to get all items.
1183
     *
1184
     * @return ResponseCollection|false A response collection with all
1185
     *     {@link Response::TYPE_DATA} responses. The collection will be empty
1186
     *     when there are no matching items. FALSE on failure.
1187
     *
1188
     * @throws NotSupportedException If $args contains prohibited arguments
1189
     *     ("follow" or "follow-only").
1190
     */
1191
    public function getAll(array $args = array(), Query $query = null)
1192
    {
1193
        $printRequest = new Request($this->menu . '/print', $query);
1194
        foreach ($args as $name => $value) {
1195
            if (is_int($name)) {
1196
                $printRequest->setArgument($value);
1197
            } else {
1198
                $printRequest->setArgument($name, $value);
1199
            }
1200
        }
1201
        
1202
        foreach (array('follow', 'follow-only', 'count-only') as $arg) {
1203
            if ($printRequest->getArgument($arg) !== null) {
1204
                throw new NotSupportedException(
1205
                    "The argument '{$arg}' was specified, but is prohibited",
1206
                    NotSupportedException::CODE_ARG_PROHIBITED,
1207
                    null,
1208
                    $arg
1209
                );
1210
            }
1211
        }
1212
        $responses = $this->client->sendSync($printRequest);
1213
1214
        if (count($responses->getAllOfType(Response::TYPE_ERROR)) > 0) {
1215
            return false;
1216
        }
1217
        return $responses->getAllOfType(Response::TYPE_DATA);
1218
    }
1219
1220
    /**
1221
     * Puts a file on RouterOS's file system.
1222
     *
1223
     * Puts a file on RouterOS's file system, regardless of the current menu.
1224
     * Note that this is a **VERY VERY VERY** time consuming method - it takes a
1225
     * minimum of a little over 4 seconds, most of which are in sleep. It waits
1226
     * 2 seconds after a file is first created (required to actually start
1227
     * writing to the file), and another 2 seconds after its contents is written
1228
     * (performed in order to verify success afterwards).
1229
     * Similarly for removal (when $data is NULL) - there are two seconds in
1230
     * sleep, used to verify the file was really deleted.
1231
     *
1232
     * If you want an efficient way of transferring files, use (T)FTP.
1233
     * If you want an efficient way of removing files, use
1234
     * {@link static::setMenu()} to move to the "/file" menu, and call
1235
     * {@link static::remove()} without performing verification afterwards.
1236
     *
1237
     * @param string               $filename  The filename to write data in.
1238
     * @param string|resource|null $data      The data the file is going to have
1239
     *     as a string or a seekable stream.
1240
     *     Setting the value to NULL removes a file of this name.
1241
     *     If a seekable stream is provided, it is sent from its current
1242
     *     position to its end, and the pointer is seeked back to its current
1243
     *     position after sending.
1244
     *     Non seekable streams, as well as all other types, are casted to a
1245
     *     string.
1246
     * @param bool                 $overwrite Whether to overwrite the file if
1247
     *     it exists.
1248
     *
1249
     * @return bool TRUE on success, FALSE on failure.
1250
     */
1251
    public function filePutContents($filename, $data, $overwrite = false)
1252
    {
1253
        $printRequest = new Request(
1254
            '/file/print .proplist=""',
1255
            Query::where('name', $filename)
1256
        );
1257
        $fileExists = count($this->client->sendSync($printRequest)) > 1;
1258
1259
        if (null === $data) {
1260
            if (!$fileExists) {
1261
                return false;
1262
            }
1263
            $removeRequest = new Request('/file/remove');
1264
            $this->client->sendSync(
1265
                $removeRequest->setArgument('numbers', $filename)
1266
            );
1267
            //Required for RouterOS to REALLY remove the file.
1268
            sleep(2);
1269
            return !(count($this->client->sendSync($printRequest)) > 1);
1270
        }
1271
1272
        if (!$overwrite && $fileExists) {
1273
            return false;
1274
        }
1275
        $result = $this->client->sendSync(
1276
            $printRequest->setArgument('file', $filename)
1277
        );
1278
        if (count($result->getAllOfType(Response::TYPE_ERROR)) > 0) {
1279
            return false;
1280
        }
1281
        //Required for RouterOS to write the initial file.
1282
        sleep(2);
1283
        $setRequest = new Request('/file/set contents=""');
1284
        $setRequest->setArgument('numbers', $filename);
1285
        $this->client->sendSync($setRequest);
1286
        $this->client->sendSync($setRequest->setArgument('contents', $data));
1287
        //Required for RouterOS to write the file's new contents.
1288
        sleep(2);
1289
1290
        $fileSize = $this->client->sendSync(
1291
            $printRequest->setArgument('file', null)
1292
                ->setArgument('.proplist', 'size')
1293
        )->getProperty('size');
1294
        if (Stream::isStream($fileSize)) {
1295
            $fileSize = stream_get_contents($fileSize);
1296
        }
1297
        if (Communicator::isSeekableStream($data)) {
1298
            return Communicator::seekableStreamLength($data) == $fileSize;
1299
        } else {
1300
            return sprintf('%u', strlen((string)$data)) === $fileSize;
1301
        };
1302
    }
1303
1304
    /**
1305
     * Gets the contents of a specified file.
1306
     *
1307
     * @param string      $filename      The name of the file to get
1308
     *     the contents of.
1309
     * @param string|null $tmpScriptName In order to get the file's contents, a
1310
     *     script is created at "/system script", the source of which is then
1311
     *     overwritten with the file's contents, then retrieved from there,
1312
     *     after which the script is removed.
1313
     *     If this argument is left NULL, a random string,
1314
     *     prefixed with the computer's name, is generated and used
1315
     *     as the script's name.
1316
     *     To eliminate any possibility of name clashes,
1317
     *     you can specify your own name instead.
1318
     *
1319
     * @return string|resource|false The contents of the file as a string or as
0 ignored issues
show
Documentation introduced by
Should the return type not be false|string|resource|null|ResponseCollection?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1320
     *     new PHP temp stream if the underlying
1321
     *     {@link Client::isStreamingResponses()} is set to TRUE.
1322
     *     FALSE is returned if there is no such file.
1323
     */
1324
    public function fileGetContents($filename, $tmpScriptName = null)
1325
    {
1326
        $checkRequest = new Request(
1327
            '/file/print',
1328
            Query::where('name', $filename)
1329
        );
1330
        if (1 === count($this->client->sendSync($checkRequest))) {
1331
            return false;
1332
        }
1333
        $contents = $this->_exec(
1334
            '/system script set $"_" source=[/file get $filename contents]',
1335
            array('filename' => $filename),
1336
            null,
1337
            $tmpScriptName,
1338
            true
1339
        );
1340
        return $contents;
1341
    }
1342
1343
    /**
1344
     * Performs an action on a bulk of items at the current menu.
1345
     *
1346
     * @param string $what What action to perform.
1347
     * @param array  $args Zero or more arguments can be specified, each being
1348
     *     a criteria. If zero arguments are specified, removes all items.
1349
     *     See {@link static::find()} for a description of what criteria are
1350
     *     accepted.
1351
     *
1352
     * @return ResponseCollection Returns the response collection, allowing you
1353
     *     to inspect errors, if any.
1354
     */
1355
    protected function doBulk($what, array $args = array())
1356
    {
1357
        $bulkRequest = new Request($this->menu . '/' . $what);
1358
        $bulkRequest->setArgument(
1359
            'numbers',
1360
            call_user_func_array(array($this, 'find'), $args)
1361
        );
1362
        return $this->client->sendSync($bulkRequest);
1363
    }
1364
1365
    /**
1366
     * Executes a RouterOS script.
1367
     *
1368
     * Same as the public equivalent, with the addition of allowing you to get
1369
     * the contents of the script post execution, instead of removing it.
1370
     *
1371
     * @param string|resource     $source The source of the script, as a string
1372
     *     or stream. If a stream is provided, reading starts from the current
1373
     *     position to the end of the stream, and the pointer stays at the end
1374
     *     after reading is done.
1375
     * @param array<string,mixed> $params An array of parameters to make
1376
     *     available in the script as local variables.
1377
     *     Variable names are array keys, and variable values are array values.
1378
     *     Array values are automatically processed with
1379
     *     {@link static::escapeValue()}. Streams are also supported, and are
1380
     *     processed in chunks, each processed with
1381
     *     {@link static::escapeString()}. Processing starts from the current
1382
     *     position to the end of the stream, and the stream's pointer is left
1383
     *     untouched after the reading is done.
1384
     *     Note that the script's (generated) name is always added as the
1385
     *     variable "_", which will be inadvertently lost if you overwrite it
1386
     *     from here.
1387
     * @param string|null         $policy Allows you to specify a policy the
1388
     *     script must follow. Has the same format as in terminal.
1389
     *     If left NULL, the script has no restrictions beyond those imposed by
1390
     *     the username.
1391
     * @param string|null         $name   The script is executed after being
1392
     *     saved in "/system script" and is removed after execution.
1393
     *     If this argument is left NULL, a random string,
1394
     *     prefixed with the computer's name, is generated and used
1395
     *     as the script's name.
1396
     *     To eliminate any possibility of name clashes,
1397
     *     you can specify your own name instead.
1398
     * @param bool                $get    Whether to get the source
1399
     *     of the script.
1400
     *
1401
     * @return ResponseCollection|string Returns the response collection of the
0 ignored issues
show
Documentation introduced by
Should the return type not be string|resource|null|ResponseCollection?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1402
     *     run, allowing you to inspect errors, if any.
1403
     *     If the script was not added successfully before execution, the
1404
     *     ResponseCollection from the add attempt is going to be returned.
1405
     *     If $get is TRUE, returns the source of the script on success.
1406
     */
1407
    private function _exec(
1408
        $source,
1409
        array $params = array(),
1410
        $policy = null,
1411
        $name = null,
1412
        $get = false
1413
    ) {
1414
        $request = new Request('/system/script/add');
1415
        if (null === $name) {
1416
            $name = uniqid(gethostname(), true);
1417
        }
1418
        $request->setArgument('name', $name);
1419
        $request->setArgument('policy', $policy);
1420
1421
        $params += array('_' => $name);
1422
1423
        $finalSource = fopen('php://temp', 'r+b');
1424
        fwrite(
1425
            $finalSource,
1426
            '/' . str_replace('/', ' ', substr($this->menu, 1)). "\n"
1427
        );
1428
        static::appendScript($finalSource, $source, $params);
1429
        fwrite($finalSource, "\n");
1430
        rewind($finalSource);
1431
1432
        $request->setArgument('source', $finalSource);
1433
        $result = $this->client->sendSync($request);
1434
1435
        if (0 === count($result->getAllOfType(Response::TYPE_ERROR))) {
1436
            $request = new Request('/system/script/run');
1437
            $request->setArgument('number', $name);
1438
            $result = $this->client->sendSync($request);
1439
1440
            if ($get) {
1441
                $result = $this->client->sendSync(
1442
                    new Request(
1443
                        '/system/script/print .proplist="source"',
1444
                        Query::where('name', $name)
1445
                    )
1446
                )->getProperty('source');
1447
            }
1448
            $request = new Request('/system/script/remove');
1449
            $request->setArgument('numbers', $name);
1450
            $this->client->sendSync($request);
1451
        }
1452
1453
        return $result;
1454
    }
1455
}
1456