Completed
Push — develop ( 3b058e...12ac2d )
by Vasil
06:55
created

Util   D

Complexity

Total Complexity 90

Size/Duplication

Total Lines 988
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 40
Bugs 19 Features 28
Metric Value
wmc 90
c 40
b 19
f 28
lcom 1
cbo 8
dl 0
loc 988
rs 4.4444

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getMenu() 0 4 1
B setMenu() 0 23 4
B newRequest() 0 25 4
A exec() 0 8 1
A clearIdCache() 0 5 1
B getCurrentTime() 0 39 4
D find() 0 86 22
C get() 0 45 8
A enable() 0 4 1
A disable() 0 4 1
A remove() 0 6 1
A comment() 0 7 1
A set() 0 15 4
A edit() 0 6 2
A unsetValue() 0 8 1
B add() 0 25 6
A move() 0 14 3
A count() 0 14 3
B getAll() 0 28 6
C filePutContents() 0 52 8
A fileGetContents() 0 18 2
A doBulk() 0 9 1
B _exec() 0 48 4

How to fix   Complexity   

Complex Class

Complex classes like Util often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Util, and based on these observations, apply Extract Interface, too.

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