Script::parseValueToDateInterval()   F
last analyzed

Complexity

Conditions 12
Paths 513

Size

Total Lines 81
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 46
nc 513
nop 1
dl 0
loc 81
rs 3.4763
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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.
75
     *     Must be a literal of a value,
76
     *     e.g. what {@link static::escapeValue()} will give you.
77
     * @param DateTimeZone|null $timezone The timezone which any resulting
78
     *     DateTime object (either the main value, or values within an array)
79
     *     will use. Defaults to UTC.
80
     *
81
     * @return mixed Depending on RouterOS type detected:
82
     *     - "nil" (the string "[]") or "nothing" (empty string) - NULL.
83
     *     - "num" - int or double for large values.
84
     *     - "bool" - a boolean.
85
     *     - "array" - an array, with the keys and values processed recursively.
86
     *     - "time" - a {@link DateInterval} object.
87
     *     - "date" (pseudo type; string in the form "M/j/Y") - a DateTime
88
     *         object with the specified date, at midnight.
89
     *     - "datetime" (pseudo type; string in the form "M/j/Y H:i:s") - a
90
     *         DateTime object with the specified date and time.
91
     *     - "str" (a quoted string) - a string, with the contents escaped.
92
     *     - Unrecognized type - casted to a string, unmodified.
93
     */
94
    public static function parseValue($value, DateTimeZone $timezone = null)
95
    {
96
        $value = static::parseValueToSimple($value);
97
        if (!is_string($value)) {
98
            return $value;
99
        }
100
101
        try {
102
            return static::parseValueToArray($value, $timezone);
103
        } catch (ParserException $e) {
104
            try {
105
                return static::parseValueToDateInterval($value);
106
            } catch (ParserException $e) {
107
                try {
108
                    return static::parseValueToDateTime($value, $timezone);
109
                } catch (ParserException $e) {
110
                    return static::parseValueToString($value);
111
                }
112
            }
113
        }
114
    }
115
116
    /**
117
     * Parses a RouterOS value into a PHP string.
118
     *
119
     * @param string $value The value to be parsed.
120
     *     Must be a literal of a value,
121
     *     e.g. what {@link static::escapeValue()} will give you.
122
     *
123
     * @return string If a quoted string is provided, it would be parsed.
124
     *     Otherwise, the value is casted to a string, and returned unmodified.
125
     */
126
    public static function parseValueToString($value)
127
    {
128
        $value = (string)$value;
129
        if ('"' === $value[0] && '"' === $value[strlen($value) - 1]) {
130
            return str_replace(
131
                array('\"', '\\\\', "\\\n", "\\\r\n", "\\\r"),
132
                array('"', '\\'),
133
                substr($value, 1, -1)
134
            );
135
        }
136
        return $value;
137
    }
138
139
    /**
140
     * Parses a RouterOS value into a PHP simple type.
141
     *
142
     * Parses a RouterOS value into a PHP simple type. "Simple" types being
143
     * scalar types, plus NULL.
144
     *
145
     * @param string $value The value to be parsed. Must be a literal of a
146
     *     value, e.g. what {@link static::escapeValue()} will give you.
147
     *
148
     * @return string|bool|int|double|null Depending on RouterOS type detected:
149
     *     - "nil" (the string "[]") or "nothing" (empty string) - NULL.
150
     *     - "num" - int or double for large values.
151
     *     - "bool" - a boolean.
152
     *     - Unrecognized type - casted to a string, unmodified.
153
     */
154
    public static function parseValueToSimple($value)
155
    {
156
        $value = (string)$value;
157
158
        if (in_array($value, array('', '[]'), true)) {
159
            return null;
160
        } elseif (in_array($value, array('true', 'false', 'yes', 'no'), true)) {
161
            return $value === 'true' || $value === 'yes';
162
        } elseif ($value === (string)($num = (int)$value)
163
            || $value === (string)($num = (double)$value)
164
        ) {
165
            return $num;
166
        }
167
        return $value;
168
    }
169
170
    /**
171
     * Parses a RouterOS value into a PHP DateTime object
172
     *
173
     * Parses a RouterOS value into a PHP DateTime object.
174
     *
175
     * @param string            $value    The value to be parsed.
176
     *     Must be a literal of a value,
177
     *     e.g. what {@link static::escapeValue()} will give you.
178
     * @param DateTimeZone|null $timezone The timezone which the resulting
179
     *     DateTime object will use. Defaults to UTC.
180
     *
181
     * @return DateTime Depending on RouterOS type detected:
182
     *     - "date" (pseudo type; string in the form "M/j/Y") - a DateTime
183
     *         object with the specified date, at midnight UTC time (regardless
184
     *         of timezone provided).
185
     *     - "datetime" (pseudo type; string in the form "M/j/Y H:i:s") - a
186
     *         DateTime object with the specified date and time,
187
     *         with the specified timezone.
188
     *
189
     * @throws ParserException When the value is not of a recognized type.
190
     */
191
    public static function parseValueToDateTime(
192
        $value,
193
        DateTimeZone $timezone = null
194
    ) {
195
        $previous = null;
196
        $value = (string)$value;
197
        if ('' !== $value && preg_match(
198
            '#^
199
                (?<mon>jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)
200
                /
201
                (?<day>\d\d?)
202
                /
203
                (?<year>\d{4})
204
                (?:
205
                    \s+(?<time>\d{2}\:\d{2}:\d{2})
206
                )?
207
            $#uix',
208
            $value,
209
            $date
210
        )
211
        ) {
212
            if (!isset($date['time'])) {
213
                $date['time'] = '00:00:00';
214
                $timezone = new DateTimeZone('UTC');
215
            } elseif (null === $timezone) {
216
                $timezone = new DateTimeZone('UTC');
217
            }
218
            try {
219
                return new DateTime(
220
                    $date['year'] .
221
                    '-' . ucfirst($date['mon']) .
222
                    "-{$date['day']} {$date['time']}",
223
                    $timezone
224
                );
225
            } catch (E $e) {
226
                $previous = $e;
227
            }
228
        }
229
        throw new ParserException(
230
            'The supplied value can not be converted to a DateTime',
231
            ParserException::CODE_DATETIME,
232
            $previous
233
        );
234
    }
235
236
    /**
237
     * Parses a RouterOS value into a PHP DateInterval.
238
     *
239
     * Parses a RouterOS value into a PHP DateInterval.
240
     *
241
     * @param string $value The value to be parsed. Must be a literal of a
242
     *     value, e.g. what {@link static::escapeValue()} will give you.
243
     *
244
     * @return DateInterval The value as a DateInterval object.
245
     *
246
     * @throws ParserException When the value is not of a recognized type.
247
     */
248
    public static function parseValueToDateInterval($value)
249
    {
250
        $value = (string)$value;
251
        if ('' !== $value && preg_match(
252
            '/^
253
                (?:(\d+)w)?
254
                (?:(\d+)d)?
255
                (?:(\d+)(?:\:|h))?
256
                (?|
257
                    (\d+)\:
258
                    (\d*(?:\.\d{1,9})?)
259
                |
260
                    (?:(\d+)m)?
261
                    (?:(\d+|\d*\.\d{1,9})s)?
262
                    (?:((?5))ms)?
263
                    (?:((?5))us)?
264
                    (?:((?5))ns)?
265
                )
266
            $/x',
267
            $value,
268
            $time
269
        )
270
        ) {
271
            $days = isset($time[2]) ? (int)$time[2] : 0;
272
            if (isset($time[1])) {
273
                $days += 7 * (int)$time[1];
274
            }
275
            if (empty($time[3])) {
276
                $time[3] = 0;
277
            }
278
            if (empty($time[4])) {
279
                $time[4] = 0;
280
            }
281
            if (empty($time[5])) {
282
                $time[5] = 0;
283
            }
284
285
            $subsecondTime = 0.0;
286
            //@codeCoverageIgnoreStart
287
            // No PHP version currently supports sub-second DateIntervals,
288
            // meaning this section is untestable, since no version constraints
289
            // can be specified for test inputs.
290
            // All inputs currently use integer seconds only, making this
291
            // section unreachable during tests.
292
            // Nevertheless, this section exists right now, in order to provide
293
            // such support as soon as PHP has it.
294
            if (!empty($time[6])) {
295
                $subsecondTime += ((double)$time[6]) / 1000;
296
            }
297
            if (!empty($time[7])) {
298
                $subsecondTime += ((double)$time[7]) / 1000000;
299
            }
300
            if (!empty($time[8])) {
301
                $subsecondTime += ((double)$time[8]) / 1000000000;
302
            }
303
            //@codeCoverageIgnoreEnd
304
305
            $secondsSpec = $time[5] + $subsecondTime;
306
            try {
307
                return new DateInterval(
308
                    "P{$days}DT{$time[3]}H{$time[4]}M{$secondsSpec}S"
309
                );
310
                //@codeCoverageIgnoreStart
311
                // See previous ignored section's note.
312
                //
313
                // This section is added for backwards compatibility with
314
                // current PHP versions, when in the future sub-second support
315
                // is added.
316
                // In that event, the test inputs for older versions will be
317
                // expected to get a rounded up result of the sub-second data.
318
            } catch (E $e) {
319
                $secondsSpec = (int)round($secondsSpec);
320
                return new DateInterval(
321
                    "P{$days}DT{$time[3]}H{$time[4]}M{$secondsSpec}S"
322
                );
323
            }
324
            //@codeCoverageIgnoreEnd
325
        }
326
        throw new ParserException(
327
            'The supplied value can not be converted to DateInterval',
328
            ParserException::CODE_DATEINTERVAL
329
        );
330
    }
331
332
    /**
333
     * Parses a RouterOS value into a PHP array.
334
     *
335
     * Parses a RouterOS value into a PHP array.
336
     *
337
     * @param string            $value    The value to be parsed.
338
     *     Must be a literal of a value,
339
     *     e.g. what {@link static::escapeValue()} will give you.
340
     * @param DateTimeZone|null $timezone The timezone which any resulting
341
     *     DateTime object within the array will use. Defaults to UTC.
342
     *
343
     * @return array An array, with the keys and values processed recursively,
344
     *         the keys with {@link static::parseValueToSimple()},
345
     *         and the values with {@link static::parseValue()}.
346
     *
347
     * @throws ParserException When the value is not of a recognized type.
348
     */
349
    public static function parseValueToArray(
350
        $value,
351
        DateTimeZone $timezone = null
352
    ) {
353
        $value = (string)$value;
354
        if ('{' === $value[0] && '}' === $value[strlen($value) - 1]) {
355
            $value = substr($value, 1, -1);
356
            if ('' === $value) {
357
                return array();
358
            }
359
            $parsedValue = preg_split(
360
                '/
361
                    (\"(?:\\\\\\\\|\\\\"|[^"])*\")
362
                    |
363
                    (\{[^{}]*(?2)?\})
364
                    |
365
                    ([^;=]+)
366
                /sx',
367
                $value,
368
                null,
369
                PREG_SPLIT_DELIM_CAPTURE
370
            );
371
            $result = array();
372
            $newVal = null;
373
            $newKey = null;
374
            for ($i = 0, $l = count($parsedValue); $i < $l; ++$i) {
375
                switch ($parsedValue[$i]) {
376
                case '':
377
                    break;
378
                case ';':
379
                    if (null === $newKey) {
380
                        $result[] = $newVal;
381
                    } else {
382
                        $result[$newKey] = $newVal;
383
                    }
384
                    $newKey = $newVal = null;
385
                    break;
386
                case '=':
387
                    $newKey = static::parseValueToSimple($parsedValue[$i - 1]);
388
                    $newVal = static::parseValue($parsedValue[++$i], $timezone);
389
                    break;
390
                default:
391
                    $newVal = static::parseValue($parsedValue[$i], $timezone);
392
                }
393
            }
394
            if (null === $newKey) {
0 ignored issues
show
introduced by
The condition null === $newKey is always true.
Loading history...
395
                $result[] = $newVal;
396
            } else {
397
                $result[$newKey] = $newVal;
398
            }
399
            return $result;
400
        }
401
        throw new ParserException(
402
            'The supplied value can not be converted to an array',
403
            ParserException::CODE_ARRAY
404
        );
405
    }
406
407
    /**
408
     * Prepares a script.
409
     *
410
     * Prepares a script for eventual execution by prepending parameters as
411
     * variables to it.
412
     *
413
     * This is particularly useful when you're creating scripts that you don't
414
     * want to execute right now (as with {@link Util::exec()}, but instead
415
     * you want to store it for later execution, perhaps by supplying it to
416
     * "/system scheduler".
417
     *
418
     * @param string|resource         $source The source of the script,
419
     *     as a string or stream. If a stream is provided, reading starts from
420
     *     the current position to the end of the stream, and the pointer stays
421
     *     at the end after reading is done.
422
     * @param array<string|int,mixed> $params An array of parameters to make
423
     *     available in the script as local variables.
424
     *     Variable names are array keys, and variable values are array values.
425
     *     Array values are automatically processed with
426
     *     {@link static::escapeValue()}. Streams are also supported, and are
427
     *     processed in chunks, each with
428
     *     {@link static::escapeString()} with all bytes being escaped.
429
     *     Processing starts from the current position to the end of the stream,
430
     *     and the stream's pointer is left untouched after the reading is done.
431
     *     Variables with a value of type "nothing" can be declared with a
432
     *     numeric array key and the variable name as the array value
433
     *     (that is casted to a string).
434
     *
435
     * @return resource A new PHP temporary stream with the script as contents,
436
     *     with the pointer back at the start.
437
     *
438
     * @see static::append()
439
     */
440
    public static function prepare(
441
        $source,
442
        array $params = array()
443
    ) {
444
        $resultStream = fopen('php://temp', 'r+b');
445
        static::append($resultStream, $source, $params);
446
        rewind($resultStream);
447
        return $resultStream;
448
    }
449
450
    /**
451
     * Appends a script.
452
     *
453
     * Appends a script to an existing stream.
454
     *
455
     * @param resource                $stream An existing stream to write the
456
     *     resulting script to.
457
     * @param string|resource         $source The source of the script,
458
     *     as a string or stream. If a stream is provided, reading starts from
459
     *     the current position to the end of the stream, and the pointer stays
460
     *     at the end after reading is done.
461
     * @param array<string|int,mixed> $params An array of parameters to make
462
     *     available in the script as local variables.
463
     *     Variable names are array keys, and variable values are array values.
464
     *     Array values are automatically processed with
465
     *     {@link static::escapeValue()}. Streams are also supported, and are
466
     *     processed in chunks, each with
467
     *     {@link static::escapeString()} with all bytes being escaped.
468
     *     Processing starts from the current position to the end of the stream,
469
     *     and the stream's pointer is left untouched after the reading is done.
470
     *     Variables with a value of type "nothing" can be declared with a
471
     *     numeric array key and the variable name as the array value
472
     *     (that is casted to a string).
473
     *
474
     * @return int The number of bytes written to $stream is returned,
475
     *     and the pointer remains where it was after the write
476
     *     (i.e. it is not seeked back, even if seeking is supported).
477
     */
478
    public static function append(
479
        $stream,
480
        $source,
481
        array $params = array()
482
    ) {
483
        $writer = new Stream($stream, false);
484
        $bytes = 0;
485
486
        foreach ($params as $pname => $pvalue) {
487
            if (is_int($pname)) {
488
                $pvalue = static::escapeString((string)$pvalue);
489
                $bytes += $writer->send(":local \"{$pvalue}\";\n");
490
                continue;
491
            }
492
            $pname = static::escapeString($pname);
493
            $bytes += $writer->send(":local \"{$pname}\" ");
494
            if (Stream::isStream($pvalue)) {
495
                $reader = new Stream($pvalue, false);
496
                $chunkSize = $reader->getChunk(Stream::DIRECTION_RECEIVE);
497
                $bytes += $writer->send('"');
498
                while ($reader->isAvailable() && $reader->isDataAwaiting()) {
499
                    $bytes += $writer->send(
500
                        static::escapeString(fread($pvalue, $chunkSize), true)
501
                    );
502
                }
503
                $bytes += $writer->send("\";\n");
504
            } else {
505
                $bytes += $writer->send(static::escapeValue($pvalue) . ";\n");
506
            }
507
        }
508
509
        $bytes += $writer->send($source);
510
        return $bytes;
511
    }
512
513
    /**
514
     * Escapes a value for a RouterOS scripting context.
515
     *
516
     * Turns any native PHP value into an equivalent whole value that can be
517
     * inserted as part of a RouterOS script.
518
     *
519
     * DateInterval objects will be casted to RouterOS' "time" type.
520
     *
521
     * DateTime objects will be casted to a string following the "M/d/Y H:i:s"
522
     * format. If the time is exactly midnight (including microseconds), and
523
     * the timezone is UTC, the string will include only the "M/d/Y" date.
524
     *
525
     * Unrecognized types (i.e. resources and other objects) are casted to
526
     * strings, and those strings are then escaped.
527
     *
528
     * @param mixed $value The value to be escaped.
529
     *
530
     * @return string A string representation that can be directly inserted in a
531
     *     script as a whole value.
532
     */
533
    public static function escapeValue($value)
534
    {
535
        switch (gettype($value)) {
536
        case 'NULL':
537
            $value = '[]';
538
            break;
539
        case 'integer':
540
            $value = (string)$value;
541
            break;
542
        case 'boolean':
543
            $value = $value ? 'true' : 'false';
544
            break;
545
        case 'array':
546
            if (0 === count($value)) {
547
                $value = '({})';
548
                break;
549
            }
550
            $result = '';
551
            foreach ($value as $key => $val) {
552
                $result .= ';';
553
                if (!is_int($key)) {
554
                    $result .= static::escapeValue($key) . '=';
555
                }
556
                $result .= static::escapeValue($val);
557
            }
558
            $value = '{' . substr($result, 1) . '}';
559
            break;
560
            /** @noinspection PhpMissingBreakStatementInspection */
561
        case 'object':
562
            if ($value instanceof DateTime) {
563
                $usec = $value->format('u');
564
                $usec = '000000' === $usec ? '' : '.' . $usec;
565
                $value = '00:00:00.000000 UTC' === $value->format('H:i:s.u e')
566
                    ? $value->format('M/d/Y')
567
                    : $value->format('M/d/Y H:i:s') . $usec;
568
            }
569
            if ($value instanceof DateInterval) {
570
                if (false === $value->days || $value->days < 0) {
571
                    $value = $value->format('%r%dd%H:%I:%S');
572
                } else {
573
                    $value = $value->format('%r%ad%H:%I:%S');
574
                }
575
                break;
576
            }
577
            //break; intentionally omitted
578
        default:
579
            $value = '"' . static::escapeString((string)$value) . '"';
580
            break;
581
        }
582
        return $value;
583
    }
584
585
    /**
586
     * Escapes a string for a RouterOS scripting context.
587
     *
588
     * Escapes a string for a RouterOS scripting context. The value can then be
589
     * surrounded with quotes at a RouterOS script (or concatenated onto a
590
     * larger string first), and you can be sure there won't be any code
591
     * injections coming from it.
592
     *
593
     * By default, for the sake of brevity of the output, ASCII alphanumeric
594
     * characters and underscores are left untouched. And for the sake of
595
     * character conversion, bytes above 0x7F are also left untouched.
596
     *
597
     * @param string $value Value to be escaped.
598
     * @param bool   $full  Whether to escape all bytes in the string, including
599
     *     ASCII alphanumeric characters, underscores and bytes above 0x7F.
600
     *
601
     * @return string The escaped value.
602
     *
603
     * @internal Why leave ONLY those ASCII characters and not also others?
604
     *     Because those can't in any way be mistaken for language constructs,
605
     *     unlike many other "safe inside strings, but not outside" ASCII
606
     *     characters, like ",", ".", "+", "-", "~", etc.
607
     */
608
    public static function escapeString($value, $full = false)
609
    {
610
        if ($full) {
611
            return self::_escapeCharacters(array($value));
612
        }
613
        return preg_replace_callback(
614
            '/[^\\_A-Za-z0-9\\x80-\\xFF]+/S',
615
            array(__CLASS__, '_escapeCharacters'),
616
            $value
617
        );
618
    }
619
620
    /**
621
     * Escapes a character for a RouterOS scripting context.
622
     *
623
     * Escapes a character for a RouterOS scripting context.
624
     * Intended to only be called by {@link self::escapeString()} for the
625
     * matching strings.
626
     *
627
     * @param array $chars The matches array, expected to contain exactly one
628
     *     member, in which is the whole string to be escaped.
629
     *
630
     * @return string The escaped characters.
631
     */
632
    private static function _escapeCharacters(array $chars)
633
    {
634
        $result = '';
635
        for ($i = 0, $l = strlen($chars[0]); $i < $l; ++$i) {
636
            $result .= '\\' . str_pad(
637
                strtoupper(dechex(ord($chars[0][$i]))),
638
                2,
639
                '0',
640
                STR_PAD_LEFT
641
            );
642
        }
643
        return $result;
644
    }
645
}
646