Completed
Push — develop ( b1eecf...f938b5 )
by Vasil
03:05
created

Util::clearIdCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 2
Metric Value
c 3
b 1
f 2
dl 0
loc 5
rs 9.4286
cc 1
eloc 3
nc 1
nop 0
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
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');
1 ignored issue
show
Bug introduced by
It seems like $value defined by $value on line 1000 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...
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);
1 ignored issue
show
Bug introduced by
It seems like $value defined by $value on line 1194 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...
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;
1 ignored issue
show
Bug introduced by
It seems like $data defined by parameter $data on line 1251 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...
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
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
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