Completed
Push — develop ( a562e1...e6f3f4 )
by Vasil
07:47
created

Script::prepare()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 1
eloc 7
nc 1
nop 2
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
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
75
     *     of a value, e.g. what {@link static::escapeValue()} will give you.
76
     * @param DateTimeZone|null $timezone The timezone which any resulting
77
     *     DateTime object (either the main value, or values within an array)
78
     *     will use. Defaults to UTC.
79
     *
80
     * @return mixed Depending on RouterOS type detected:
81
     *     - "nil" (the string "[]") or "nothing" (empty string) - NULL.
82
     *     - "number" - int or double for large values.
83
     *     - "bool" - a boolean.
84
     *     - "array" - an array, with the keys and values processed recursively.
85
     *     - "time" - a {@link DateInterval} object.
86
     *     - "date" (pseudo type; string in the form "M/j/Y") - a DateTime
87
     *         object with the specified date, at midnight.
88
     *     - "datetime" (pseudo type; string in the form "M/j/Y H:i:s") - a
89
     *         DateTime object with the specified date and time.
90
     *     - "str" (a quoted string) - a string, with the contents escaped.
91
     *     - Unrecognized type - casted to a string, unmodified.
92
     */
93
    public static function parseValue($value, DateTimeZone $timezone = null)
0 ignored issues
show
Unused Code introduced by
The parameter $timezone is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
94
    {
95
        $value = static::parseValueToSimple($value);
96
        if (!is_string($value)) {
97
            return $value;
98
        }
99
        
100
        $value = static::parseValueToArray($value);
101
        if (!is_string($value)) {
102
            return $value;
103
        }
104
105
        $value = static::parseValueToDateInterval($value);
106
        if (!is_string($value)) {
107
            return $value;
108
        }
109
110
        $value = static::parseValueToDateTime($value);
111
        if (!is_string($value)) {
112
            return $value;
113
        }
114
115
        return static::parseValueToString($value);
116
    }
117
    
118
    public static function parseValueToString($value) {
0 ignored issues
show
Coding Style introduced by
Missing function doc comment
Loading history...
Coding Style introduced by
Opening brace should be on a new line
Loading history...
119
        $value = (string)$value;
120
        if ('"' === $value[0] && '"' === $value[strlen($value) - 1]) {
121
            return str_replace(
122
                array('\"', '\\\\', "\\\n", "\\\r\n", "\\\r"),
123
                array('"', '\\'),
124
                substr($value, 1, -1)
125
            );
126
        }
127
        return $value;
128
    }
129
130
    /**
131
     * Parses a RouterOS value into a PHP simple type.
132
     *
133
     * Parses a RouterOS value into a PHP simple type. "Simple" types being
134
     * scalar types, plus NULL.
135
     *
136
     * @param string $value The value to be parsed. Must be a literal of a
137
     *     value, e.g. what {@link static::escapeValue()} will give you.
138
     *
139
     * @return string|bool|int|double|null Depending on RouterOS type detected:
140
     *     - "nil" (the string "[]") or "nothing" (empty string) - NULL.
141
     *     - "number" - int or double for large values.
142
     *     - "bool" - a boolean.
143
     *     - Unrecognized type - casted to a string, unmodified.
144
     */
145
    public static function parseValueToSimple($value)
146
    {
147
        $value = (string)$value;
148
149
        if (in_array($value, array('', '[]'), true)) {
150
            return null;
151
        } elseif (in_array($value, array('true', 'false', 'yes', 'no'), true)) {
152
            return $value === 'true' || $value === 'yes';
153
        } elseif ($value === (string)($num = (int)$value)
154
            || $value === (string)($num = (double)$value)
155
        ) {
156
            return $num;
157
        }
158
        return $value;
159
    }
160
161
    /**
162
     * Parses a RouterOS value into a PHP DateTime object
163
     *
164
     * Parses a RouterOS value into a PHP DateTime object.
165
     *
166
     * @param string       $value The value to be parsed. Must be a literal of a
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
167
     *     value, e.g. what {@link static::escapeValue()} will give you.
168
     * @param DateTimeZone $timezone The timezone which the resulting DateTime
0 ignored issues
show
Documentation introduced by
Should the type for parameter $timezone not be null|DateTimeZone?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

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