Passed
Push — develop ( ead434...0a8e77 )
by Vasil
03:53
created

Request::__debugInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 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
 * Refers to transmitter direction constants.
25
 */
26
use PEAR2\Net\Transmitter as T;
27
28
/**
29
 * Represents a RouterOS request.
30
 *
31
 * @category Net
32
 * @package  PEAR2_Net_RouterOS
33
 * @author   Vasil Rangelov <[email protected]>
34
 * @license  http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
35
 * @link     http://pear2.php.net/PEAR2_Net_RouterOS
36
 */
37
class Request extends Message
38
{
39
40
    /**
41
     * The command to be executed.
42
     *
43
     * @var string
44
     */
45
    private $_command;
46
47
    /**
48
     * A query for the command.
49
     *
50
     * @var Query|null
51
     */
52
    private $_query;
53
54
    /**
55
     * Creates a request to send to RouterOS.
56
     *
57
     * @param string      $command The command to send.
58
     *     Can also contain arguments expressed in a shell-like syntax.
59
     * @param Query|null  $query   A query to associate with the request.
60
     * @param string|null $tag     The tag for the request.
61
     *
62
     * @see setCommand()
63
     * @see setArgument()
64
     * @see setTag()
65
     * @see setQuery()
66
     */
67
    public function __construct($command, Query $query = null, $tag = null)
68
    {
69
        if (false !== strpos($command, '=')
70
            && false !== ($spaceBeforeEquals = strrpos(
71
                strstr($command, '=', true),
72
                ' '
73
            ))
74
        ) {
75
            $this->parseArgumentString(substr($command, $spaceBeforeEquals));
76
            $command = rtrim(substr($command, 0, $spaceBeforeEquals));
77
        }
78
        $this->setCommand($command);
79
        $this->setQuery($query);
80
        $this->setTag($tag);
81
    }
82
83
    /**
84
     * A shorthand gateway.
85
     *
86
     * This is a magic PHP method that allows you to call the object as a
87
     * function. Depending on the argument given, one of the other functions in
88
     * the class is invoked and its returned value is returned by this function.
89
     *
90
     * @param Query|Communicator|string|null $arg A {@link Query} to associate
91
     *     the request with, a {@link Communicator} to send the request over,
92
     *     an argument to get the value of, or NULL to get the tag. If a
93
     *     second argument is provided, this becomes the name of the argument to
94
     *     set the value of, and the second argument is the value to set.
95
     *
96
     * @return string|resource|int|$this Whatever the long form
97
     *     function returns.
98
     */
99
    public function __invoke($arg = null)
100
    {
101
        if (func_num_args() > 1) {
102
            return $this->setArgument(func_get_arg(0), func_get_arg(1));
103
        }
104
        if ($arg instanceof Query) {
105
            return $this->setQuery($arg);
106
        }
107
        if ($arg instanceof Communicator) {
108
            return $this->send($arg);
109
        }
110
        return parent::__invoke($arg);
111
    }
112
113
    /**
114
     * Sets the command to send to RouterOS.
115
     *
116
     * Sets the command to send to RouterOS. The command can use the API or CLI
117
     * syntax of RouterOS, but either way, it must be absolute (begin  with a
118
     * "/") and without arguments.
119
     *
120
     * @param string $command The command to send.
121
     *
122
     * @return $this The request object.
123
     *
124
     * @see getCommand()
125
     * @see setArgument()
126
     */
127
    public function setCommand($command)
128
    {
129
        $command = (string) $command;
130
        if (strpos($command, '/') !== 0) {
131
            throw new InvalidArgumentException(
132
                'Commands must be absolute.',
133
                InvalidArgumentException::CODE_ABSOLUTE_REQUIRED
134
            );
135
        }
136
        if (substr_count($command, '/') === 1) {
137
            //Command line syntax convertion
138
            $cmdParts = preg_split('#[\s/]+#sm', $command);
139
            $cmdRes = array($cmdParts[0]);
140
            for ($i = 1, $n = count($cmdParts); $i < $n; $i++) {
0 ignored issues
show
Bug introduced by
It seems like $cmdParts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

140
            for ($i = 1, $n = count(/** @scrutinizer ignore-type */ $cmdParts); $i < $n; $i++) {
Loading history...
141
                if ('..' === $cmdParts[$i]) {
142
                    $delIndex = count($cmdRes) - 1;
143
                    if ($delIndex < 1) {
144
                        throw new InvalidArgumentException(
145
                            'Unable to resolve command',
146
                            InvalidArgumentException::CODE_CMD_UNRESOLVABLE
147
                        );
148
                    }
149
                    unset($cmdRes[$delIndex]);
150
                    $cmdRes = array_values($cmdRes);
151
                } else {
152
                    $cmdRes[] = $cmdParts[$i];
153
                }
154
            }
155
            $command = implode('/', $cmdRes);
156
        }
157
        if (!preg_match('#^/\S+$#sm', $command)) {
158
            throw new InvalidArgumentException(
159
                'Invalid command supplied.',
160
                InvalidArgumentException::CODE_CMD_INVALID
161
            );
162
        }
163
        $this->_command = $command;
164
        return $this;
165
    }
166
167
    /**
168
     * Gets the command that will be send to RouterOS.
169
     *
170
     * Gets the command that will be send to RouterOS in its API syntax.
171
     *
172
     * @return string The command to send.
173
     *
174
     * @see setCommand()
175
     */
176
    public function getCommand()
177
    {
178
        return $this->_command;
179
    }
180
181
    /**
182
     * Sets the query to send with the command.
183
     *
184
     * @param Query|null $query The query to be set.
185
     *     Setting NULL will remove the  currently associated query.
186
     *
187
     * @return $this The request object.
188
     *
189
     * @see getQuery()
190
     */
191
    public function setQuery(Query $query = null)
192
    {
193
        $this->_query = $query;
194
        return $this;
195
    }
196
197
    /**
198
     * Gets the currently associated query
199
     *
200
     * @return Query|null The currently associated query.
201
     *
202
     * @see setQuery()
203
     */
204
    public function getQuery()
205
    {
206
        return $this->_query;
207
    }
208
209
    /**
210
     * Sets the tag to associate the request with.
211
     *
212
     * Sets the tag to associate the request with. Setting NULL erases the
213
     * currently set tag.
214
     *
215
     * @param string|null $tag The tag to set.
216
     *
217
     * @return $this The request object.
218
     *
219
     * @see getTag()
220
     */
221
    public function setTag($tag)
222
    {
223
        return parent::setTag($tag);
224
    }
225
226
    /**
227
     * Sets an argument for the request.
228
     *
229
     * @param string               $name  Name of the argument.
230
     * @param string|resource|null $value Value of the argument as a string or
231
     *     seekable stream.
232
     *     Setting the value to NULL removes an argument of this name.
233
     *     If a seekable stream is provided, it is sent from its current
234
     *     position to its end, and the pointer is seeked back to its current
235
     *     position after sending.
236
     *     Non seekable streams, as well as all other types, are casted to a
237
     *     string.
238
     *
239
     * @return $this The request object.
240
     *
241
     * @see getArgument()
242
     */
243
    public function setArgument($name, $value = '')
244
    {
245
        return parent::setAttribute($name, $value);
246
    }
247
248
    /**
249
     * Gets the value of an argument.
250
     *
251
     * @param string $name The name of the argument.
252
     *
253
     * @return string|resource|null The value of the specified argument.
254
     *     Returns NULL if such an argument is not set.
255
     *
256
     * @see setAttribute()
257
     */
258
    public function getArgument($name)
259
    {
260
        return parent::getAttribute($name);
261
    }
262
263
    /**
264
     * Removes all arguments from the request.
265
     *
266
     * @return $this The request object.
267
     */
268
    public function removeAllArguments()
269
    {
270
        return parent::removeAllAttributes();
271
    }
272
273
    /**
274
     * Get actionable debug info.
275
     *
276
     * This is a magic method available to PHP 5.6 and above, due to which
277
     * output of var_dump() will be more actionable.
278
     *
279
     * You can still call it in earlier versions to get the object as a plain array.
280
     *
281
     * @return array The info, as an associative array.
282
     */
283
    public function __debugInfo()
284
    {
285
        return parent::__debugInfo() + array(
286
            'command' => $this->_command,
287
            'query' => $this->_query
288
        );
289
    }
290
291
    /**
292
     * Sends a request over a communicator.
293
     *
294
     * @param Communicator  $com The communicator to send the request over.
295
     * @param Registry|null $reg An optional registry to sync the request with.
296
     *
297
     * @return int The number of bytes sent.
298
     *
299
     * @see Client::sendSync()
300
     * @see Client::sendAsync()
301
     */
302
    public function send(Communicator $com, Registry $reg = null)
303
    {
304
        if (null !== $reg
305
            && (null != $this->getTag() || !$reg->isTaglessModeOwner())
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $this->getTag() of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
306
        ) {
307
            $originalTag = $this->getTag();
308
            $this->setTag($reg->getOwnershipTag() . $originalTag);
309
            $bytes = $this->send($com);
310
            $this->setTag($originalTag);
311
            return $bytes;
312
        }
313
        if ($com->getTransmitter()->isPersistent()) {
314
            $old = $com->getTransmitter()->lock(T\Stream::DIRECTION_SEND);
315
            $bytes = $this->_send($com);
316
            $com->getTransmitter()->lock($old, true);
317
            return $bytes;
318
        }
319
        return $this->_send($com);
320
    }
321
322
    /**
323
     * Sends a request over a communicator.
324
     *
325
     * The only difference with the non private equivalent is that this one does
326
     * not do locking.
327
     *
328
     * @param Communicator $com The communicator to send the request over.
329
     *
330
     * @return int The number of bytes sent.
331
     *
332
     * @see Client::sendSync()
333
     * @see Client::sendAsync()
334
     */
335
    private function _send(Communicator $com)
336
    {
337
        if (!$com->getTransmitter()->isAcceptingData()) {
338
            throw new SocketException(
339
                'Transmitter is invalid. Sending aborted.',
340
                SocketException::CODE_REQUEST_SEND_FAIL
341
            );
342
        }
343
        $bytes = 0;
344
        $bytes += $com->sendWord($this->getCommand());
345
        if (null !== ($tag = $this->getTag())) {
346
            $bytes += $com->sendWord('.tag=' . $tag);
347
        }
348
        foreach ($this->attributes as $name => $value) {
349
            $prefix = '=' . $name . '=';
350
            $bytes += $com->sendWord($prefix, $value);
351
        }
352
        $query = $this->getQuery();
353
        if ($query instanceof Query) {
354
            $bytes += $query->send($com);
355
        }
356
        $bytes += $com->sendWord('');
357
        return $bytes;
358
    }
359
360
    /**
361
     * Verifies the request.
362
     *
363
     * Verifies the request against a communicator, i.e. whether the request
364
     * could successfully be sent (assuming the connection is still opened).
365
     *
366
     * @param Communicator $com The Communicator to check against.
367
     *
368
     * @return $this The request object itself.
369
     *
370
     * @throws LengthException If the resulting length of an API word is not
371
     *     supported.
372
     */
373
    public function verify(Communicator $com)
374
    {
375
        $com::verifyLengthSupport(strlen($this->getCommand()));
376
        $com::verifyLengthSupport(strlen('.tag=' . (string)$this->getTag()));
377
        foreach ($this->attributes as $name => $value) {
378
            if (is_string($value)) {
379
                $com::verifyLengthSupport(strlen('=' . $name . '=' . $value));
380
            } else {
381
                $com::verifyLengthSupport(
382
                    strlen('=' . $name . '=') +
0 ignored issues
show
Bug introduced by
strlen('=' . $name . '='...bleStreamLength($value) of type double is incompatible with the type integer expected by parameter $length of PEAR2\Net\RouterOS\Commu...::verifyLengthSupport(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

382
                    /** @scrutinizer ignore-type */ strlen('=' . $name . '=') +
Loading history...
383
                    $com::seekableStreamLength($value)
384
                );
385
            }
386
        }
387
        $query = $this->getQuery();
388
        if ($query instanceof Query) {
389
            $query->verify($com);
390
        }
391
        return $this;
392
    }
393
394
    /**
395
     * Parses the arguments of a command.
396
     *
397
     * @param string $string The argument string to parse.
398
     *
399
     * @return void
400
     */
401
    protected function parseArgumentString($string)
402
    {
403
        /*
404
         * Grammar:
405
         *
406
         * <arguments> := (<<\s+>>, <argument>)*,
407
         * <argument> := <name>, <value>?
408
         * <name> := <<[^\=\s]+>>
409
         * <value> := "=", (<quoted string> | <unquoted string>)
410
         * <quotedString> := <<">>, <<([^"]|\\"|\\\\)*>>, <<">>
411
         * <unquotedString> := <<\S+>>
412
         */
413
414
        $token = '';
415
        $name = null;
416
        while ($string = substr($string, strlen($token))) {
417
            if (null === $name) {
418
                if (preg_match('/^\s+([^\s=]+)/sS', $string, $matches)) {
419
                    $token = $matches[0];
420
                    $name = $matches[1];
421
                } else {
422
                    throw new InvalidArgumentException(
423
                        "Parsing of argument name failed near '{$string}'",
424
                        InvalidArgumentException::CODE_NAME_UNPARSABLE
425
                    );
426
                }
427
            } elseif (preg_match('/^\s/s', $string, $matches)) {
428
                //Empty argument
429
                $token = '';
430
                $this->setArgument($name);
0 ignored issues
show
Bug introduced by
$name of type void is incompatible with the type string expected by parameter $name of PEAR2\Net\RouterOS\Request::setArgument(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

430
                $this->setArgument(/** @scrutinizer ignore-type */ $name);
Loading history...
431
                $name = null;
432
            } elseif (preg_match(
433
                '/^="(([^\\\"]|\\\"|\\\\)*)"/sS',
434
                $string,
435
                $matches
436
            )
437
            ) {
438
                $token = $matches[0];
439
                $this->setArgument(
440
                    $name,
441
                    str_replace(
442
                        array('\\"', '\\\\'),
443
                        array('"', '\\'),
444
                        $matches[1]
445
                    )
446
                );
447
                $name = null;
448
            } elseif (preg_match('/^=(\S+)/sS', $string, $matches)) {
449
                $token = $matches[0];
450
                $this->setArgument($name, $matches[1]);
451
                $name = null;
452
            } else {
453
                throw new InvalidArgumentException(
454
                    "Parsing of argument value failed near '{$string}'",
455
                    InvalidArgumentException::CODE_VALUE_UNPARSABLE
456
                );
457
            }
458
        }
459
460
        if (null !== $name && ('' !== ($name = trim($name)))) {
461
            $this->setArgument($name, '');
462
        }
463
    }
464
}
465