Completed
Push — develop ( 8c3dc1...869a75 )
by Vasil
02:51
created

Request::verify()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 1
Metric Value
c 1
b 1
f 1
dl 0
loc 20
rs 9.2
cc 4
eloc 14
nc 6
nop 1
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
     * @var string The command to be executed.
42
     */
43
    private $_command;
44
45
    /**
46
     * @var Query A query for the command.
47
     */
48
    private $_query;
49
50
    /**
51
     * Creates a request to send to RouterOS.
52
     *
53
     * @param string      $command The command to send.
54
     *     Can also contain arguments expressed in a shell-like syntax.
55
     * @param Query|null  $query   A query to associate with the request.
56
     * @param string|null $tag     The tag for the request.
57
     *
58
     * @see setCommand()
59
     * @see setArgument()
60
     * @see setTag()
61
     * @see setQuery()
62
     */
63
    public function __construct($command, Query $query = null, $tag = null)
64
    {
65
        if (false !== strpos($command, '=')
66
            && false !== ($spaceBeforeEquals = strrpos(
67
                strstr($command, '=', true),
68
                ' '
69
            ))
70
        ) {
71
            $this->parseArgumentString(substr($command, $spaceBeforeEquals));
72
            $command = rtrim(substr($command, 0, $spaceBeforeEquals));
73
        }
74
        $this->setCommand($command);
75
        $this->setQuery($query);
76
        $this->setTag($tag);
77
    }
78
79
    /**
80
     * A shorthand gateway.
81
     *
82
     * This is a magic PHP method that allows you to call the object as a
83
     * function. Depending on the argument given, one of the other functions in
84
     * the class is invoked and its returned value is returned by this function.
85
     *
86
     * @param Query|Communicator|string|null $arg A {@link Query} to associate
87
     *     the request with, a {@link Communicator} to send the request over,
88
     *     an argument to get the value of, or NULL to get the tag. If a
89
     *     second argument is provided, this becomes the name of the argument to
90
     *     set the value of, and the second argument is the value to set.
91
     *
92
     * @return string|resource|int|$this Whatever the long form
93
     *     function returns.
94
     */
95
    public function __invoke($arg = null)
96
    {
97
        if (func_num_args() > 1) {
98
            return $this->setArgument(func_get_arg(0), func_get_arg(1));
99
        }
100
        if ($arg instanceof Query) {
101
            return $this->setQuery($arg);
102
        }
103
        if ($arg instanceof Communicator) {
104
            return $this->send($arg);
105
        }
106
        return parent::__invoke($arg);
107
    }
108
109
    /**
110
     * Sets the command to send to RouterOS.
111
     *
112
     * Sets the command to send to RouterOS. The command can use the API or CLI
113
     * syntax of RouterOS, but either way, it must be absolute (begin  with a
114
     * "/") and without arguments.
115
     *
116
     * @param string $command The command to send.
117
     *
118
     * @return $this The request object.
119
     *
120
     * @see getCommand()
121
     * @see setArgument()
122
     */
123
    public function setCommand($command)
124
    {
125
        $command = (string) $command;
126
        if (strpos($command, '/') !== 0) {
127
            throw new InvalidArgumentException(
128
                'Commands must be absolute.',
129
                InvalidArgumentException::CODE_ABSOLUTE_REQUIRED
130
            );
131
        }
132
        if (substr_count($command, '/') === 1) {
133
            //Command line syntax convertion
134
            $cmdParts = preg_split('#[\s/]+#sm', $command);
135
            $cmdRes = array($cmdParts[0]);
136
            for ($i = 1, $n = count($cmdParts); $i < $n; $i++) {
137
                if ('..' === $cmdParts[$i]) {
138
                    $delIndex = count($cmdRes) - 1;
139
                    if ($delIndex < 1) {
140
                        throw new InvalidArgumentException(
141
                            'Unable to resolve command',
142
                            InvalidArgumentException::CODE_CMD_UNRESOLVABLE
143
                        );
144
                    }
145
                    unset($cmdRes[$delIndex]);
146
                    $cmdRes = array_values($cmdRes);
147
                } else {
148
                    $cmdRes[] = $cmdParts[$i];
149
                }
150
            }
151
            $command = implode('/', $cmdRes);
152
        }
153
        if (!preg_match('#^/\S+$#sm', $command)) {
154
            throw new InvalidArgumentException(
155
                'Invalid command supplied.',
156
                InvalidArgumentException::CODE_CMD_INVALID
157
            );
158
        }
159
        $this->_command = $command;
160
        return $this;
161
    }
162
163
    /**
164
     * Gets the command that will be send to RouterOS.
165
     *
166
     * Gets the command that will be send to RouterOS in its API syntax.
167
     *
168
     * @return string The command to send.
169
     *
170
     * @see setCommand()
171
     */
172
    public function getCommand()
173
    {
174
        return $this->_command;
175
    }
176
177
    /**
178
     * Sets the query to send with the command.
179
     *
180
     * @param Query|null $query The query to be set.
181
     *     Setting NULL will remove the  currently associated query.
182
     *
183
     * @return $this The request object.
184
     *
185
     * @see getQuery()
186
     */
187
    public function setQuery(Query $query = null)
188
    {
189
        $this->_query = $query;
190
        return $this;
191
    }
192
193
    /**
194
     * Gets the currently associated query
195
     *
196
     * @return Query|null The currently associated query.
197
     *
198
     * @see setQuery()
199
     */
200
    public function getQuery()
201
    {
202
        return $this->_query;
203
    }
204
205
    /**
206
     * Sets the tag to associate the request with.
207
     *
208
     * Sets the tag to associate the request with. Setting NULL erases the
209
     * currently set tag.
210
     *
211
     * @param string|null $tag The tag to set.
212
     *
213
     * @return $this The request object.
214
     *
215
     * @see getTag()
216
     */
217
    public function setTag($tag)
218
    {
219
        return parent::setTag($tag);
220
    }
221
222
    /**
223
     * Sets an argument for the request.
224
     *
225
     * @param string               $name  Name of the argument.
226
     * @param string|resource|null $value Value of the argument as a string or
227
     *     seekable stream.
228
     *     Setting the value to NULL removes an argument of this name.
229
     *     If a seekable stream is provided, it is sent from its current
230
     *     position to its end, and the pointer is seeked back to its current
231
     *     position after sending.
232
     *     Non seekable streams, as well as all other types, are casted to a
233
     *     string.
234
     *
235
     * @return $this The request object.
236
     *
237
     * @see getArgument()
238
     */
239
    public function setArgument($name, $value = '')
240
    {
241
        return parent::setAttribute($name, $value);
242
    }
243
244
    /**
245
     * Gets the value of an argument.
246
     *
247
     * @param string $name The name of the argument.
248
     *
249
     * @return string|resource|null The value of the specified argument.
250
     *     Returns NULL if such an argument is not set.
251
     *
252
     * @see setAttribute()
253
     */
254
    public function getArgument($name)
255
    {
256
        return parent::getAttribute($name);
257
    }
258
259
    /**
260
     * Removes all arguments from the request.
261
     *
262
     * @return $this The request object.
263
     */
264
    public function removeAllArguments()
265
    {
266
        return parent::removeAllAttributes();
267
    }
268
269
    /**
270
     * Sends a request over a communicator.
271
     *
272
     * @param Communicator  $com The communicator to send the request over.
273
     * @param Registry|null $reg An optional registry to sync the request with.
274
     *
275
     * @return int The number of bytes sent.
276
     *
277
     * @see Client::sendSync()
278
     * @see Client::sendAsync()
279
     */
280
    public function send(Communicator $com, Registry $reg = null)
281
    {
282
        if (null !== $reg
283
            && (null != $this->getTag() || !$reg->isTaglessModeOwner())
284
        ) {
285
            $originalTag = $this->getTag();
286
            $this->setTag($reg->getOwnershipTag() . $originalTag);
287
            $bytes = $this->send($com);
288
            $this->setTag($originalTag);
289
            return $bytes;
290
        }
291
        if ($com->getTransmitter()->isPersistent()) {
292
            $old = $com->getTransmitter()->lock(T\Stream::DIRECTION_SEND);
293
            $bytes = $this->_send($com);
294
            $com->getTransmitter()->lock($old, true);
295
            return $bytes;
296
        }
297
        return $this->_send($com);
298
    }
299
300
    /**
301
     * Sends a request over a communicator.
302
     *
303
     * The only difference with the non private equivalent is that this one does
304
     * not do locking.
305
     *
306
     * @param Communicator $com The communicator to send the request over.
307
     *
308
     * @return int The number of bytes sent.
309
     *
310
     * @see Client::sendSync()
311
     * @see Client::sendAsync()
312
     */
313
    private function _send(Communicator $com)
314
    {
315
        if (!$com->getTransmitter()->isAcceptingData()) {
316
            throw new SocketException(
317
                'Transmitter is invalid. Sending aborted.',
318
                SocketException::CODE_REQUEST_SEND_FAIL
319
            );
320
        }
321
        $bytes = 0;
322
        $bytes += $com->sendWord($this->getCommand());
323
        if (null !== ($tag = $this->getTag())) {
324
            $bytes += $com->sendWord('.tag=' . $tag);
325
        }
326
        foreach ($this->attributes as $name => $value) {
327
            $prefix = '=' . $name . '=';
328
            if (is_string($value)) {
329
                $bytes += $com->sendWord($prefix . $value);
330
            } else {
331
                $bytes += $com->sendWordFromStream($prefix, $value);
332
            }
333
        }
334
        $query = $this->getQuery();
335
        if ($query instanceof Query) {
336
            $bytes += $query->send($com);
337
        }
338
        $bytes += $com->sendWord('');
339
        return $bytes;
340
    }
341
342
    /**
343
     * Verifies the request.
344
     * 
345
     * Verifies the request against a communicator, i.e. whether the request
346
     * could successfully be sent (assuming the connection is still opened).
347
     * 
348
     * @param Communicator $com The Communicator to check against.
349
     * 
350
     * @return $this The request object itself.
351
     * 
352
     * @throws LengthException If the resulting length of an API word is not
353
     *     supported.
354
     */
355
    public function verify(Communicator $com)
356
    {
357
        $com::verifyLengthSupport(strlen($this->getCommand()));
358
        $com::verifyLengthSupport(strlen('.tag=' . (string)$this->getTag()));
359
        foreach ($this->attributes as $name => $value) {
360
            if (is_string($value)) {
361
                $com::verifyLengthSupport(strlen('=' . $name . '=' . $value));
362
            } else {
363
                $com::verifyLengthSupport(
364
                    strlen('=' . $name . '=') +
365
                    $com::seekableStreamLength($value)
366
                );
367
            }
368
        }
369
        $query = $this->getQuery();
370
        if ($query instanceof Query) {
371
            $query->verify($com);
372
        }
373
        return $this;
374
    }
375
376
    /**
377
     * Parses the arguments of a command.
378
     *
379
     * @param string $string The argument string to parse.
380
     *
381
     * @return void
382
     */
383
    protected function parseArgumentString($string)
384
    {
385
        /*
386
         * Grammar:
387
         *
388
         * <arguments> := (<<\s+>>, <argument>)*,
389
         * <argument> := <name>, <value>?
390
         * <name> := <<[^\=\s]+>>
391
         * <value> := "=", (<quoted string> | <unquoted string>)
392
         * <quotedString> := <<">>, <<([^"]|\\"|\\\\)*>>, <<">>
393
         * <unquotedString> := <<\S+>>
394
         */
395
396
        $token = '';
397
        $name = null;
398
        while ($string = substr($string, strlen($token))) {
399
            if (null === $name) {
400
                if (preg_match('/^\s+([^\s=]+)/sS', $string, $matches)) {
401
                    $token = $matches[0];
402
                    $name = $matches[1];
403
                } else {
404
                    throw new InvalidArgumentException(
405
                        "Parsing of argument name failed near '{$string}'",
406
                        InvalidArgumentException::CODE_NAME_UNPARSABLE
407
                    );
408
                }
409
            } elseif (preg_match('/^\s/s', $string, $matches)) {
410
                //Empty argument
411
                $token = '';
412
                $this->setArgument($name);
413
                $name = null;
414
            } elseif (preg_match(
415
                '/^="(([^\\\"]|\\\"|\\\\)*)"/sS',
416
                $string,
417
                $matches
418
            )) {
419
                $token = $matches[0];
420
                $this->setArgument(
421
                    $name,
422
                    str_replace(
423
                        array('\\"', '\\\\'),
424
                        array('"', '\\'),
425
                        $matches[1]
426
                    )
427
                );
428
                $name = null;
429
            } elseif (preg_match('/^=(\S+)/sS', $string, $matches)) {
430
                $token = $matches[0];
431
                $this->setArgument($name, $matches[1]);
432
                $name = null;
433
            } else {
434
                throw new InvalidArgumentException(
435
                    "Parsing of argument value failed near '{$string}'",
436
                    InvalidArgumentException::CODE_VALUE_UNPARSABLE
437
                );
438
            }
439
        }
440
441
        if (null !== $name && ('' !== ($name = trim($name)))) {
442
            $this->setArgument($name, '');
443
        }
444
445
    }
446
}
447