Completed
Push — develop ( 869a75...517092 )
by Vasil
02:59
created

Util::enable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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