Completed
Push — develop ( 869a75...517092 )
by Vasil
02:59
created

Script   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 413
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 55
c 1
b 0
f 0
lcom 1
cbo 1
dl 0
loc 413
rs 6.8

6 Methods

Rating   Name   Duplication   Size   Complexity  
F parseValue() 0 169 30
A prepare() 0 9 1
B append() 0 29 5
C escapeValue() 0 50 16
A escapeString() 0 8 1
A _escapeCharacters() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like Script often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Script, and based on these observations, apply Extract Interface, too.

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::prepareScript()}.
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
     * For better usefulness, in addition to "actual" RouterOS types, a pseudo
75
     * "date" type is also recognized, whenever the string is in the form
76
     * "M/j/Y".
77
     *
78
     * @param string $value The value to be parsed. Must be a literal of a
79
     *     value, e.g. what {@link static::escapeValue()} will give you.
80
     *
81
     * @return mixed Depending on RouterOS type detected:
82
     *     - "nil" or "nothing" - NULL.
83
     *     - "number" - int or double for large values.
84
     *     - "bool" - a boolean.
85
     *     - "time" - a {@link DateInterval} object.
86
     *     - "array" - an array, with the values processed recursively.
87
     *     - "str" - a string.
88
     *     - "date" (pseudo type) - a DateTime object with the specified date,
89
     *         at midnight UTC time.
90
     *     - Unrecognized type - treated as an unquoted string.
91
     */
92
    public static function parseValue($value)
93
    {
94
        $value = (string)$value;
95
96
        if (in_array($value, array('', 'nil'), true)) {
97
            return null;
98
        } elseif (in_array($value, array('true', 'false', 'yes', 'no'), true)) {
99
            return $value === 'true' || $value === 'yes';
100
        } elseif ($value === (string)($num = (int)$value)
101
            || $value === (string)($num = (double)$value)
102
        ) {
103
            return $num;
104
        } elseif (preg_match(
105
            '/^
106
                (?:(\d+)w)?
107
                (?:(\d+)d)?
108
                (?:(\d+)(?:\:|h))?
109
                (?|
110
                    (\d+)\:
111
                    (\d*(?:\.\d{1,9})?)
112
                |
113
                    (?:(\d+)m)?
114
                    (?:(\d+|\d*\.\d{1,9})s)?
115
                    (?:((?5))ms)?
116
                    (?:((?5))us)?
117
                    (?:((?5))ns)?
118
                )
119
            $/x',
120
            $value,
121
            $time
122
        )) {
123
            $days = isset($time[2]) ? (int)$time[2] : 0;
124
            if (isset($time[1])) {
125
                $days += 7 * (int)$time[1];
126
            }
127
            if (empty($time[3])) {
128
                $time[3] = 0;
129
            }
130
            if (empty($time[4])) {
131
                $time[4] = 0;
132
            }
133
            if (empty($time[5])) {
134
                $time[5] = 0;
135
            }
136
            
137
            $subsecondTime = 0.0;
138
            //@codeCoverageIgnoreStart
139
            // No PHP version currently supports sub-second DateIntervals,
140
            // meaning this section is untestable, since no version constraints
141
            // can be specified for test inputs.
142
            // All inputs currently use integer seconds only, making this
143
            // section unreachable during tests.
144
            // Nevertheless, this section exists right now, in order to provide
145
            // such support as soon as PHP has it.
146
            if (!empty($time[6])) {
147
                $subsecondTime += ((double)$time[6]) / 1000;
148
            }
149
            if (!empty($time[7])) {
150
                $subsecondTime += ((double)$time[7]) / 1000000;
151
            }
152
            if (!empty($time[8])) {
153
                $subsecondTime += ((double)$time[8]) / 1000000000;
154
            }
155
            //@codeCoverageIgnoreEnd
156
157
            $secondsSpec = $time[5] + $subsecondTime;
158
            try {
159
                return new DateInterval(
160
                    "P{$days}DT{$time[3]}H{$time[4]}M{$secondsSpec}S"
161
                );
162
                //@codeCoverageIgnoreStart
163
                // See previous ignored section's note.
164
                //
165
                // This section is added for backwards compatibility with current
166
                // PHP versions, when in the future sub-second support is added.
167
                // In that event, the test inputs for older versions will be
168
                // expected to get a rounded up result of the sub-second data.
169
            } catch (E $e) {
170
                $secondsSpec = (int)round($secondsSpec);
171
                return new DateInterval(
172
                    "P{$days}DT{$time[3]}H{$time[4]}M{$secondsSpec}S"
173
                );
174
            }
175
            //@codeCoverageIgnoreEnd
176
        } elseif (preg_match(
177
            '#^
178
                (?<mon>jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)
179
                /
180
                (?<day>\d\d?)
181
                /
182
                (?<year>\d{4})
183
                (?:
184
                    \s+(?<time>\d{2}\:\d{2}:\d{2})
185
                )?
186
            $#uix',
187
            $value,
188
            $date
189
        )) {
190
            if (!isset($date['time'])) {
191
                $date['time'] = '00:00:00';
192
            }
193
            try {
194
                return new DateTime(
195
                    $date['year'] .
196
                    '-' . ucfirst($date['mon']) .
197
                    "-{$date['day']} {$date['time']}",
198
                    new DateTimeZone('UTC')
199
                );
200
            } catch (E $e) {
201
                return $value;
202
            }
203
        } elseif (('"' === $value[0]) && substr(strrev($value), 0, 1) === '"') {
204
            return str_replace(
205
                array('\"', '\\\\', "\\\n", "\\\r\n", "\\\r"),
206
                array('"', '\\'),
207
                substr($value, 1, -1)
208
            );
209
        } elseif ('{' === $value[0]) {
210
            $len = strlen($value);
211
            if ($value[$len - 1] === '}') {
212
                $value = substr($value, 1, -1);
213
                if ('' === $value) {
214
                    return array();
215
                }
216
                $parsedValue = preg_split(
217
                    '/
218
                        (\"(?:\\\\\\\\|\\\\"|[^"])*\")
219
                        |
220
                        (\{[^{}]*(?2)?\})
221
                        |
222
                        ([^;=]+)
223
                    /sx',
224
                    $value,
225
                    null,
226
                    PREG_SPLIT_DELIM_CAPTURE
227
                );
228
                $result = array();
229
                $newVal = null;
230
                $newKey = null;
231
                for ($i = 0, $l = count($parsedValue); $i < $l; ++$i) {
232
                    switch ($parsedValue[$i]) {
233
                    case '':
234
                        break;
235
                    case ';':
236
                        if (null === $newKey) {
237
                            $result[] = $newVal;
238
                        } else {
239
                            $result[$newKey] = $newVal;
240
                        }
241
                        $newKey = $newVal = null;
242
                        break;
243
                    case '=':
244
                        $newKey = static::parseValue($parsedValue[$i - 1]);
245
                        $newVal = static::parseValue($parsedValue[++$i]);
246
                        break;
247
                    default:
248
                        $newVal = static::parseValue($parsedValue[$i]);
249
                    }
250
                }
251
                if (null === $newKey) {
252
                    $result[] = $newVal;
253
                } else {
254
                    $result[$newKey] = $newVal;
255
                }
256
                return $result;
257
            }
258
        }
259
        return $value;
260
    }
261
262
    /**
263
     * Prepares a script.
264
     *
265
     * Prepares a script for eventual execution by prepending parameters as
266
     * variables to it.
267
     *
268
     * This is particularly useful when you're creating scripts that you don't
269
     * want to execute right now (as with {@link static::exec()}, but instead
270
     * you want to store it for later execution, perhaps by supplying it to
271
     * "/system scheduler".
272
     *
273
     * @param string|resource     $source The source of the script, as a string
274
     *     or stream. If a stream is provided, reading starts from the current
275
     *     position to the end of the stream, and the pointer stays at the end
276
     *     after reading is done.
277
     * @param array<string,mixed> $params An array of parameters to make
278
     *     available in the script as local variables.
279
     *     Variable names are array keys, and variable values are array values.
280
     *     Array values are automatically processed with
281
     *     {@link static::escapeValue()}. Streams are also supported, and are
282
     *     processed in chunks, each with
283
     *     {@link static::escapeString()}. Processing starts from the current
284
     *     position to the end of the stream, and the stream's pointer stays at
285
     *     the end after reading is done.
286
     *
287
     * @return resource A new PHP temporary stream with the script as contents,
288
     *     with the pointer back at the start.
289
     *
290
     * @see static::appendScript()
291
     */
292
    public static function prepare(
293
        $source,
294
        array $params = array()
295
    ) {
296
        $resultStream = fopen('php://temp', 'r+b');
297
        static::append($resultStream, $source, $params);
298
        rewind($resultStream);
299
        return $resultStream;
300
    }
301
302
    /**
303
     * Appends a script.
304
     *
305
     * Appends a script to an existing stream.
306
     *
307
     * @param resource            $stream An existing stream to write the
308
     *     resulting script to.
309
     * @param string|resource     $source The source of the script, as a string
310
     *     or stream. If a stream is provided, reading starts from the current
311
     *     position to the end of the stream, and the pointer stays at the end
312
     *     after reading is done.
313
     * @param array<string,mixed> $params An array of parameters to make
314
     *     available in the script as local variables.
315
     *     Variable names are array keys, and variable values are array values.
316
     *     Array values are automatically processed with
317
     *     {@link static::escapeValue()}. Streams are also supported, and are
318
     *     processed in chunks, each with
319
     *     {@link static::escapeString()}. Processing starts from the current
320
     *     position to the end of the stream, and the stream's pointer stays at
321
     *     the end after reading is done.
322
     *
323
     * @return int The number of bytes written to $stream is returned,
324
     *     and the pointer remains where it was after the write
325
     *     (i.e. it is not seeked back, even if seeking is supported).
326
     */
327
    public static function append(
328
        $stream,
329
        $source,
330
        array $params = array()
331
    ) {
332
        $writer = new Stream($stream, false);
333
        $bytes = 0;
334
335
        foreach ($params as $pname => $pvalue) {
336
            $pname = static::escapeString($pname);
337
            $bytes += $writer->send(":local \"{$pname}\" ");
338
            if (Stream::isStream($pvalue)) {
339
                $reader = new Stream($pvalue, false);
340
                $chunkSize = $reader->getChunk(Stream::DIRECTION_RECEIVE);
341
                $bytes += $writer->send('"');
342
                while ($reader->isAvailable() && $reader->isDataAwaiting()) {
343
                    $bytes += $writer->send(
344
                        static::escapeString(fread($pvalue, $chunkSize))
345
                    );
346
                }
347
                $bytes += $writer->send("\";\n");
348
            } else {
349
                $bytes += $writer->send(static::escapeValue($pvalue) . ";\n");
350
            }
351
        }
352
353
        $bytes += $writer->send($source);
354
        return $bytes;
355
    }
356
357
    /**
358
     * Escapes a value for a RouterOS scripting context.
359
     *
360
     * Turns any native PHP value into an equivalent whole value that can be
361
     * inserted as part of a RouterOS script.
362
     *
363
     * DateInterval objects will be casted to RouterOS' "time" type.
364
     * 
365
     * DateTime objects will be casted to a string following the "M/d/Y H:i:s"
366
     * format. If the time is exactly midnight (including microseconds), and
367
     * the timezone is UTC, the string will include only the "M/d/Y" date.
368
     *
369
     * Unrecognized types (i.e. resources and other objects) are casted to
370
     * strings.
371
     *
372
     * @param mixed $value The value to be escaped.
373
     *
374
     * @return string A string representation that can be directly inserted in a
375
     *     script as a whole value.
376
     */
377
    public static function escapeValue($value)
378
    {
379
        switch(gettype($value)) {
380
        case 'NULL':
381
            $value = '';
382
            break;
383
        case 'integer':
384
            $value = (string)$value;
385
            break;
386
        case 'boolean':
387
            $value = $value ? 'true' : 'false';
388
            break;
389
        case 'array':
390
            if (0 === count($value)) {
391
                $value = '({})';
392
                break;
393
            }
394
            $result = '';
395
            foreach ($value as $key => $val) {
396
                $result .= ';';
397
                if (!is_int($key)) {
398
                    $result .= static::escapeValue($key) . '=';
399
                }
400
                $result .= static::escapeValue($val);
401
            }
402
            $value = '{' . substr($result, 1) . '}';
403
            break;
404
        case 'object':
405
            if ($value instanceof DateTime) {
406
                $usec = $value->format('u');
407
                $usec = '000000' === $usec ? '' : '.' . $usec;
408
                $value = '00:00:00.000000 UTC' === $value->format('H:i:s.u e')
409
                    ? $value->format('M/d/Y')
410
                    : $value->format('M/d/Y H:i:s') . $usec;
411
            }
412
            if ($value instanceof DateInterval) {
413
                if (false === $value->days || $value->days < 0) {
414
                    $value = $value->format('%r%dd%H:%I:%S');
415
                } else {
416
                    $value = $value->format('%r%ad%H:%I:%S');
417
                }
418
                break;
419
            }
420
            //break; intentionally omitted
421
        default:
422
            $value = '"' . static::escapeString((string)$value) . '"';
423
            break;
424
        }
425
        return $value;
426
    }
427
428
    /**
429
     * Escapes a string for a RouterOS scripting context.
430
     *
431
     * Escapes a string for a RouterOS scripting context. The value can then be
432
     * surrounded with quotes at a RouterOS script (or concatenated onto a
433
     * larger string first), and you can be sure there won't be any code
434
     * injections coming from it.
435
     *
436
     * @param string $value Value to be escaped.
437
     *
438
     * @return string The escaped value.
439
     */
440
    public static function escapeString($value)
441
    {
442
        return preg_replace_callback(
443
            '/[^\\_A-Za-z0-9]+/S',
444
            array(__CLASS__, '_escapeCharacters'),
445
            $value
446
        );
447
    }
448
449
    /**
450
     * Escapes a character for a RouterOS scripting context.
451
     *
452
     * Escapes a character for a RouterOS scripting context. Intended to only be
453
     * called for non-alphanumeric characters.
454
     *
455
     * @param string $chars The matches array, expected to contain exactly one
456
     *     member, in which is the whole string to be escaped.
457
     *
458
     * @return string The escaped characters.
459
     */
460
    private static function _escapeCharacters($chars)
461
    {
462
        $result = '';
463
        for ($i = 0, $l = strlen($chars[0]); $i < $l; ++$i) {
464
            $result .= '\\' . str_pad(
465
                strtoupper(dechex(ord($chars[0][$i]))),
466
                2,
467
                '0',
468
                STR_PAD_LEFT
469
            );
470
        }
471
        return $result;
472
    }
473
}
474