Completed
Push — develop ( 49c3fa...f053e8 )
by Vasil
03:26
created

Script::parseValue()   C

Complexity

Conditions 8
Paths 7

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 25
rs 5.3846
cc 8
eloc 17
nc 7
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
 * Values at {@link Script::escapeValue()} can be casted from this type.
25
 */
26
use DateTime;
27
28
/**
29
 * Values at {@link Script::escapeValue()} can be casted from this type.
30
 */
31
use DateInterval;
32
33
/**
34
 * Used at {@link Script::escapeValue()} to get the proper time.
35
 */
36
use DateTimeZone;
37
38
/**
39
 * Used to reliably write to streams at {@link Script::prepare()}.
40
 */
41
use PEAR2\Net\Transmitter\Stream;
42
43
/**
44
 * Used to catch DateTime and DateInterval exceptions at
45
 * {@link Script::parseValue()}.
46
 */
47
use Exception as E;
48
49
/**
50
 * Scripting class.
51
 *
52
 * Provides functionality related to parsing and composing RouterOS scripts and
53
 * values.
54
 *
55
 * @category Net
56
 * @package  PEAR2_Net_RouterOS
57
 * @author   Vasil Rangelov <[email protected]>
58
 * @license  http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
59
 * @link     http://pear2.php.net/PEAR2_Net_RouterOS
60
 */
61
class Script
62
{
63
    /**
64
     * Parses a value from a RouterOS scripting context.
65
     *
66
     * Turns a value from RouterOS into an equivalent PHP value, based on
67
     * determining the type in the same way RouterOS would determine it for a
68
     * literal.
69
     *
70
     * This method is intended to be the very opposite of
71
     * {@link static::escapeValue()}. That is, results from that method, if
72
     * given to this method, should produce equivalent results.
73
     *
74
     * @param string $value The value to be parsed. Must be a literal of a
75
     *     value, e.g. what {@link static::escapeValue()} will give you.
76
     *
77
     * @return mixed Depending on RouterOS type detected:
78
     *     - "nil" (the string "[]") or "nothing" (empty string) - NULL.
79
     *     - "number" - int or double for large values.
80
     *     - "bool" - a boolean.
81
     *     - "array" - an array, with the keys and values processed recursively.
82
     *     - "str" - a string.
83
     *     - "time" - a {@link DateInterval} object.
84
     *     - "date" (pseudo type; string in the form "M/j/Y") - a DateTime
85
     *         object with the specified date, at midnight UTC time.
86
     *     - "datetime" (pseudo type; string in the form "M/j/Y H:i:s") - a
87
     *         DateTime object with the specified date and UTC time.
88
     *     - Unrecognized type - treated as an unquoted string.
89
     */
90
    public static function parseValue($value)
91
    {
92
        $value = static::parseValueToSimple($value);
93
        if (!is_string($value)) {
94
            return $value;
95
        } elseif ('{' === $value[0] && '}' === $value[strlen($value) - 1]) {
96
            $value = static::parseValueToArray($value);
97
            if (!is_string($value)) {
98
                return $value;
99
            }
100
        } elseif ('"' === $value[0] && '"' === $value[strlen($value) - 1]) {
101
            return str_replace(
102
                array('\"', '\\\\', "\\\n", "\\\r\n", "\\\r"),
103
                array('"', '\\'),
104
                substr($value, 1, -1)
105
            );
106
        }
107
108
        $value = static::parseValueToObject($value);
109
        if (!is_string($value)) {
110
            return $value;
111
        }
112
113
        return $value;
114
    }
115
116
    /**
117
     * Parses a RouterOS value into a PHP simple type.
118
     * 
119
     * Parses a RouterOS value into a PHP simple type. "Simple" types being
120
     * scalar types, plus NULL.
121
     * 
122
     * @param string $value The value to be parsed. Must be a literal of a
123
     *     value, e.g. what {@link static::escapeValue()} will give you.
124
     * 
125
     * @return string|bool|int|double|null Depending on RouterOS type detected:
126
     *     - "nil" (the string "[]") or "nothing" (empty string) - NULL.
127
     *     - "number" - int or double for large values.
128
     *     - "bool" - a boolean.
129
     *     - Unrecognized type - treated as an unquoted string.
130
     */
131
    public static function parseValueToSimple($value)
132
    {
133
        $value = (string)$value;
134
135
        if (in_array($value, array('', '[]'), true)) {
136
            return null;
137
        } elseif (in_array($value, array('true', 'false', 'yes', 'no'), true)) {
138
            return $value === 'true' || $value === 'yes';
139
        } elseif ($value === (string)($num = (int)$value)
140
            || $value === (string)($num = (double)$value)
141
        ) {
142
            return $num;
143
        }
144
        return $value;
145
    }
146
147
    /**
148
     * Parses a RouterOS value into a PHP object.
149
     * 
150
     * Parses a RouterOS value into a PHP object.
151
     * 
152
     * @param string $value The value to be parsed. Must be a literal of a
153
     *     value, e.g. what {@link static::escapeValue()} will give you.
154
     * 
155
     * @return string|DateInterval|DateTime Depending on RouterOS type detected:
156
     *     - "time" - a {@link DateInterval} object.
157
     *     - "date" (pseudo type; string in the form "M/j/Y") - a DateTime
158
     *         object with the specified date, at midnight UTC time.
159
     *     - "datetime" (pseudo type; string in the form "M/j/Y H:i:s") - a
160
     *         DateTime object with the specified date and UTC time.
161
     *     - Unrecognized type - treated as an unquoted string.
162
     */
163
    public static function parseValueToObject($value)
164
    {
165
        $value = (string)$value;
166
        if ('' === $value) {
167
            return $value;
168
        }
169
170
        if (preg_match(
171
            '/^
172
                (?:(\d+)w)?
173
                (?:(\d+)d)?
174
                (?:(\d+)(?:\:|h))?
175
                (?|
176
                    (\d+)\:
177
                    (\d*(?:\.\d{1,9})?)
178
                |
179
                    (?:(\d+)m)?
180
                    (?:(\d+|\d*\.\d{1,9})s)?
181
                    (?:((?5))ms)?
182
                    (?:((?5))us)?
183
                    (?:((?5))ns)?
184
                )
185
            $/x',
186
            $value,
187
            $time
188
        )) {
189
            $days = isset($time[2]) ? (int)$time[2] : 0;
190
            if (isset($time[1])) {
191
                $days += 7 * (int)$time[1];
192
            }
193
            if (empty($time[3])) {
194
                $time[3] = 0;
195
            }
196
            if (empty($time[4])) {
197
                $time[4] = 0;
198
            }
199
            if (empty($time[5])) {
200
                $time[5] = 0;
201
            }
202
            
203
            $subsecondTime = 0.0;
204
            //@codeCoverageIgnoreStart
205
            // No PHP version currently supports sub-second DateIntervals,
206
            // meaning this section is untestable, since no version constraints
207
            // can be specified for test inputs.
208
            // All inputs currently use integer seconds only, making this
209
            // section unreachable during tests.
210
            // Nevertheless, this section exists right now, in order to provide
211
            // such support as soon as PHP has it.
212
            if (!empty($time[6])) {
213
                $subsecondTime += ((double)$time[6]) / 1000;
214
            }
215
            if (!empty($time[7])) {
216
                $subsecondTime += ((double)$time[7]) / 1000000;
217
            }
218
            if (!empty($time[8])) {
219
                $subsecondTime += ((double)$time[8]) / 1000000000;
220
            }
221
            //@codeCoverageIgnoreEnd
222
223
            $secondsSpec = $time[5] + $subsecondTime;
224
            try {
225
                return new DateInterval(
226
                    "P{$days}DT{$time[3]}H{$time[4]}M{$secondsSpec}S"
227
                );
228
                //@codeCoverageIgnoreStart
229
                // See previous ignored section's note.
230
                //
231
                // This section is added for backwards compatibility with current
232
                // PHP versions, when in the future sub-second support is added.
233
                // In that event, the test inputs for older versions will be
234
                // expected to get a rounded up result of the sub-second data.
235
            } catch (E $e) {
236
                $secondsSpec = (int)round($secondsSpec);
237
                return new DateInterval(
238
                    "P{$days}DT{$time[3]}H{$time[4]}M{$secondsSpec}S"
239
                );
240
            }
241
            //@codeCoverageIgnoreEnd
242
        } elseif (preg_match(
243
            '#^
244
                (?<mon>jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)
245
                /
246
                (?<day>\d\d?)
247
                /
248
                (?<year>\d{4})
249
                (?:
250
                    \s+(?<time>\d{2}\:\d{2}:\d{2})
251
                )?
252
            $#uix',
253
            $value,
254
            $date
255
        )) {
256
            if (!isset($date['time'])) {
257
                $date['time'] = '00:00:00';
258
            }
259
            try {
260
                return new DateTime(
261
                    $date['year'] .
262
                    '-' . ucfirst($date['mon']) .
263
                    "-{$date['day']} {$date['time']}",
264
                    new DateTimeZone('UTC')
265
                );
266
            } catch (E $e) {
267
                return $value;
268
            }
269
        }
270
        return $value;
271
    }
272
273
    /**
274
     * Parses a RouterOS value into a PHP array.
275
     * 
276
     * Parses a RouterOS value into a PHP array.
277
     * 
278
     * @param string $value The value to be parsed. Must be a literal of a
279
     *     value, e.g. what {@link static::escapeValue()} will give you.
280
     * 
281
     * @return string|array Depending on RouterOS type detected:
282
     *     - "array" - an array, with the keys and values processed recursively.
283
     *     - Unrecognized type - treated as an unquoted string.
284
     */
285
    public static function parseValueToArray($value) {
0 ignored issues
show
Coding Style introduced by
Opening brace should be on a new line
Loading history...
286
        $value = (string)$value;
287
        if ('{' === $value[0] && '}' === $value[strlen($value) - 1]) {
288
            $value = substr($value, 1, -1);
289
            if ('' === $value) {
290
                return array();
291
            }
292
            $parsedValue = preg_split(
293
                '/
294
                    (\"(?:\\\\\\\\|\\\\"|[^"])*\")
295
                    |
296
                    (\{[^{}]*(?2)?\})
297
                    |
298
                    ([^;=]+)
299
                /sx',
300
                $value,
301
                null,
302
                PREG_SPLIT_DELIM_CAPTURE
303
            );
304
            $result = array();
305
            $newVal = null;
306
            $newKey = null;
307
            for ($i = 0, $l = count($parsedValue); $i < $l; ++$i) {
308
                switch ($parsedValue[$i]) {
309
                case '':
310
                    break;
311
                case ';':
312
                    if (null === $newKey) {
313
                        $result[] = $newVal;
314
                    } else {
315
                        $result[$newKey] = $newVal;
316
                    }
317
                    $newKey = $newVal = null;
318
                    break;
319
                case '=':
320
                    $newKey = static::parseValueToSimple($parsedValue[$i - 1]);
321
                    $newVal = static::parseValue($parsedValue[++$i]);
322
                    break;
323
                default:
324
                    $newVal = static::parseValue($parsedValue[$i]);
325
                }
326
            }
327
            if (null === $newKey) {
328
                $result[] = $newVal;
329
            } else {
330
                $result[$newKey] = $newVal;
331
            }
332
            return $result;
333
        }
334
        return $value;
335
    }
336
337
    /**
338
     * Prepares a script.
339
     *
340
     * Prepares a script for eventual execution by prepending parameters as
341
     * variables to it.
342
     *
343
     * This is particularly useful when you're creating scripts that you don't
344
     * want to execute right now (as with {@link static::exec()}, but instead
345
     * you want to store it for later execution, perhaps by supplying it to
346
     * "/system scheduler".
347
     *
348
     * @param string|resource     $source The source of the script, as a string
349
     *     or stream. If a stream is provided, reading starts from the current
350
     *     position to the end of the stream, and the pointer stays at the end
351
     *     after reading is done.
352
     * @param array<string,mixed> $params An array of parameters to make
353
     *     available in the script as local variables.
354
     *     Variable names are array keys, and variable values are array values.
355
     *     Array values are automatically processed with
356
     *     {@link static::escapeValue()}. Streams are also supported, and are
357
     *     processed in chunks, each with
358
     *     {@link static::escapeString()}. Processing starts from the current
359
     *     position to the end of the stream, and the stream's pointer stays at
360
     *     the end after reading is done.
361
     *
362
     * @return resource A new PHP temporary stream with the script as contents,
363
     *     with the pointer back at the start.
364
     *
365
     * @see static::append()
366
     */
367
    public static function prepare(
368
        $source,
369
        array $params = array()
370
    ) {
371
        $resultStream = fopen('php://temp', 'r+b');
372
        static::append($resultStream, $source, $params);
373
        rewind($resultStream);
374
        return $resultStream;
375
    }
376
377
    /**
378
     * Appends a script.
379
     *
380
     * Appends a script to an existing stream.
381
     *
382
     * @param resource            $stream An existing stream to write the
383
     *     resulting script to.
384
     * @param string|resource     $source The source of the script, as a string
385
     *     or stream. If a stream is provided, reading starts from the current
386
     *     position to the end of the stream, and the pointer stays at the end
387
     *     after reading is done.
388
     * @param array<string,mixed> $params An array of parameters to make
389
     *     available in the script as local variables.
390
     *     Variable names are array keys, and variable values are array values.
391
     *     Array values are automatically processed with
392
     *     {@link static::escapeValue()}. Streams are also supported, and are
393
     *     processed in chunks, each with
394
     *     {@link static::escapeString()}. Processing starts from the current
395
     *     position to the end of the stream, and the stream's pointer stays at
396
     *     the end after reading is done.
397
     *
398
     * @return int The number of bytes written to $stream is returned,
399
     *     and the pointer remains where it was after the write
400
     *     (i.e. it is not seeked back, even if seeking is supported).
401
     */
402
    public static function append(
403
        $stream,
404
        $source,
405
        array $params = array()
406
    ) {
407
        $writer = new Stream($stream, false);
408
        $bytes = 0;
409
410
        foreach ($params as $pname => $pvalue) {
411
            $pname = static::escapeString($pname);
412
            $bytes += $writer->send(":local \"{$pname}\" ");
413
            if (Stream::isStream($pvalue)) {
414
                $reader = new Stream($pvalue, false);
415
                $chunkSize = $reader->getChunk(Stream::DIRECTION_RECEIVE);
416
                $bytes += $writer->send('"');
417
                while ($reader->isAvailable() && $reader->isDataAwaiting()) {
418
                    $bytes += $writer->send(
419
                        static::escapeString(fread($pvalue, $chunkSize))
420
                    );
421
                }
422
                $bytes += $writer->send("\";\n");
423
            } else {
424
                $bytes += $writer->send(static::escapeValue($pvalue) . ";\n");
425
            }
426
        }
427
428
        $bytes += $writer->send($source);
429
        return $bytes;
430
    }
431
432
    /**
433
     * Escapes a value for a RouterOS scripting context.
434
     *
435
     * Turns any native PHP value into an equivalent whole value that can be
436
     * inserted as part of a RouterOS script.
437
     *
438
     * DateInterval objects will be casted to RouterOS' "time" type.
439
     * 
440
     * DateTime objects will be casted to a string following the "M/d/Y H:i:s"
441
     * format. If the time is exactly midnight (including microseconds), and
442
     * the timezone is UTC, the string will include only the "M/d/Y" date.
443
     *
444
     * Unrecognized types (i.e. resources and other objects) are casted to
445
     * strings.
446
     *
447
     * @param mixed $value The value to be escaped.
448
     *
449
     * @return string A string representation that can be directly inserted in a
450
     *     script as a whole value.
451
     */
452
    public static function escapeValue($value)
453
    {
454
        switch(gettype($value)) {
455
        case 'NULL':
456
            $value = '';
457
            break;
458
        case 'integer':
459
            $value = (string)$value;
460
            break;
461
        case 'boolean':
462
            $value = $value ? 'true' : 'false';
463
            break;
464
        case 'array':
465
            if (0 === count($value)) {
466
                $value = '({})';
467
                break;
468
            }
469
            $result = '';
470
            foreach ($value as $key => $val) {
471
                $result .= ';';
472
                if (!is_int($key)) {
473
                    $result .= static::escapeValue($key) . '=';
474
                }
475
                $result .= static::escapeValue($val);
476
            }
477
            $value = '{' . substr($result, 1) . '}';
478
            break;
479
        case 'object':
480
            if ($value instanceof DateTime) {
481
                $usec = $value->format('u');
482
                $usec = '000000' === $usec ? '' : '.' . $usec;
483
                $value = '00:00:00.000000 UTC' === $value->format('H:i:s.u e')
484
                    ? $value->format('M/d/Y')
485
                    : $value->format('M/d/Y H:i:s') . $usec;
486
            }
487
            if ($value instanceof DateInterval) {
488
                if (false === $value->days || $value->days < 0) {
489
                    $value = $value->format('%r%dd%H:%I:%S');
490
                } else {
491
                    $value = $value->format('%r%ad%H:%I:%S');
492
                }
493
                break;
494
            }
495
            //break; intentionally omitted
496
        default:
497
            $value = '"' . static::escapeString((string)$value) . '"';
498
            break;
499
        }
500
        return $value;
501
    }
502
503
    /**
504
     * Escapes a string for a RouterOS scripting context.
505
     *
506
     * Escapes a string for a RouterOS scripting context. The value can then be
507
     * surrounded with quotes at a RouterOS script (or concatenated onto a
508
     * larger string first), and you can be sure there won't be any code
509
     * injections coming from it.
510
     *
511
     * @param string $value Value to be escaped.
512
     *
513
     * @return string The escaped value.
514
     */
515
    public static function escapeString($value)
516
    {
517
        return preg_replace_callback(
518
            '/[^\\_A-Za-z0-9]+/S',
519
            array(__CLASS__, '_escapeCharacters'),
520
            $value
521
        );
522
    }
523
524
    /**
525
     * Escapes a character for a RouterOS scripting context.
526
     *
527
     * Escapes a character for a RouterOS scripting context. Intended to only be
528
     * called for non-alphanumeric characters.
529
     *
530
     * @param string $chars The matches array, expected to contain exactly one
531
     *     member, in which is the whole string to be escaped.
532
     *
533
     * @return string The escaped characters.
534
     */
535
    private static function _escapeCharacters($chars)
536
    {
537
        $result = '';
538
        for ($i = 0, $l = strlen($chars[0]); $i < $l; ++$i) {
539
            $result .= '\\' . str_pad(
540
                strtoupper(dechex(ord($chars[0][$i]))),
541
                2,
542
                '0',
543
                STR_PAD_LEFT
544
            );
545
        }
546
        return $result;
547
    }
548
}
549