Encoder   F
last analyzed

Complexity

Total Complexity 74

Size/Duplication

Total Lines 371
Duplicated Lines 0 %

Test Coverage

Coverage 58.24%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 192
c 2
b 0
f 0
dl 0
loc 371
ccs 106
cts 182
cp 0.5824
rs 2.48
wmc 74

3 Methods

Rating   Name   Duplication   Size   Complexity  
F decodeXml() 0 93 19
D decode() 0 97 26
D encode() 0 99 29

How to fix   Complexity   

Complex Class

Complex classes like Encoder 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.

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 Encoder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpXmlRpc;
4
5
use PhpXmlRpc\Helper\XMLParser;
6
use PhpXmlRpc\Traits\LoggerAware;
7
use PhpXmlRpc\Traits\ParserAware;
8
9
/**
10
 * A helper class to easily convert between Value objects and php native values.
11
 *
12
 * @todo implement an interface
13
 * @todo add class constants for the options values
14
 */
15
class Encoder
16
{
17
    use LoggerAware;
18
    use ParserAware;
19
20
    /**
21
     * Takes an xml-rpc Value in object instance and translates it into native PHP types, recursively.
22
     * Works with xml-rpc Request objects as input, too.
23
     * Xmlrpc dateTime values will be converted to strings or DateTime objects depending on an $options parameter
24
     * Supports i8 and NIL xml-rpc values without the need for specific options.
25
     * Both xml-rpc arrays and structs are decoded into PHP arrays, with the exception described below:
26
     * Given proper options parameter, can rebuild generic php object instances (provided those have been encoded to
27
     * xml-rpc format using a corresponding option in php_xmlrpc_encode()).
28
     * PLEASE NOTE that rebuilding php objects involves calling their constructor function.
29
     * This means that the remote communication end can decide which php code will get executed on your server, leaving
30
     * the door possibly open to 'php-injection' style of attacks (provided you have some classes defined on your server
31 3
     * that might wreak havoc if instances are built outside an appropriate context).
32
     * Make sure you trust the remote server/client before enabling this!
33 3
     *
34 1
     * @author Dan Libby
35
     *
36 3
     * @param Value|Request $xmlrpcVal
37
     * @param array $options accepted elements:
38
     *                      - 'decode_php_objs': if set in the options array, xml-rpc structs can be decoded into php
39
     *                         objects, see the details above;
40
     *                      - 'dates_as_objects': when set xml-rpc dateTimes are decoded as php DateTime objects
41
     *                      - 'extension_api': reserved for usage by phpxmlrpc-polyfill
42
     * @return mixed
43
     *
44
     * Feature creep -- add an option to allow converting xml-rpc dateTime values to unix timestamps (integers)
45
     */
46
    public function decode($xmlrpcVal, $options = array())
47
    {
48
        switch ($xmlrpcVal->kindOf()) {
49
            case 'scalar':
50
                if (in_array('extension_api', $options)) {
51
                    $val = $xmlrpcVal->scalarVal();
0 ignored issues
show
Bug introduced by
The method scalarVal() does not exist on PhpXmlRpc\Request. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

51
                    /** @scrutinizer ignore-call */ 
52
                    $val = $xmlrpcVal->scalarVal();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
52
                    $typ = $xmlrpcVal->scalarTyp();
0 ignored issues
show
Bug introduced by
The method scalarTyp() does not exist on PhpXmlRpc\Request. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

52
                    /** @scrutinizer ignore-call */ 
53
                    $typ = $xmlrpcVal->scalarTyp();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
53
                    switch ($typ) {
54
                        case 'dateTime.iso8601':
55
                            $xmlrpcVal = array(
56
                                'xmlrpc_type' => 'datetime',
57
                                'scalar' => $val,
58
                                'timestamp' => \PhpXmlRpc\Helper\Date::iso8601Decode($val)
59
                            );
60
                            return (object)$xmlrpcVal;
61
                        case 'base64':
62
                            $xmlrpcVal = array(
63
                                'xmlrpc_type' => 'base64',
64
                                'scalar' => $val
65 279
                            );
66
                            return (object)$xmlrpcVal;
67 279
                        case 'string':
68 279
                            if (isset($options['extension_api_encoding'])) {
69 279
                                // if iconv is not available, we use mb_convert_encoding
70
                                if (function_exists('iconv')) {
71
                                    $dval = @iconv('UTF-8', $options['extension_api_encoding'], $val);
72
                                } elseif (function_exists('mb_convert_encoding')) {
73
                                    /// @todo check for discrepancies between the supported charset names
74
                                    $dval = @mb_convert_encoding($val, $options['extension_api_encoding'], 'UTF-8');
75
                                } else {
76
                                    $dval = false;
77
                                }
78
                                if ($dval !== false) {
79
                                    return $dval;
80
                                }
81
                            }
82
                            // break through voluntarily
83
                        default:
84
                            return $val;
85
                    }
86
                }
87
                if (in_array('dates_as_objects', $options) && $xmlrpcVal->scalarTyp() == 'dateTime.iso8601') {
88
                    // we return a Datetime object instead of a string; since now the constructor of xml-rpc value accepts
89
                    // safely string, int and DateTimeInterface, we cater to all 3 cases here
90
                    $out = $xmlrpcVal->scalarVal();
91
                    if (is_string($out)) {
92
                        $out = strtotime($out);
93
                        // NB: if the string does not convert into a timestamp, this will return false.
94
                        // We avoid logging an error here, as we presume it was already done when parsing the xml
95
                        /// @todo we could return null, to be more in line with what the XMLParser does...
96
                    }
97
                    if (is_int($out)) {
98
                        $result = new \DateTime();
99 279
                        $result->setTimestamp($out);
100
101
                        return $result;
102
                    } elseif (is_a($out, 'DateTimeInterface') || is_a($out, 'DateTime')) {
103
                        return $out;
104
                    }
105
                }
106
                return $xmlrpcVal->scalarVal();
107
108
            case 'array':
109
                $arr = array();
110
                foreach ($xmlrpcVal as $value) {
111
                    $arr[] = $this->decode($value, $options);
112
                }
113
                return $arr;
114
115 279
            case 'struct':
116
                // If user said so, try to rebuild php objects for specific struct vals.
117 277
                /// @todo should we raise a warning for class not found?
118 106
                // shall we check for proper subclass of xml-rpc value instead of presence of _php_class to detect
119 106
                // what we can do?
120 106
                if (in_array('decode_php_objs', $options) && $xmlrpcVal->_php_class != ''
0 ignored issues
show
Bug Best Practice introduced by
The property _php_class does not exist on PhpXmlRpc\Request. Since you implemented __get, consider adding a @property annotation.
Loading history...
121
                    && class_exists($xmlrpcVal->_php_class)
0 ignored issues
show
Bug introduced by
It seems like $xmlrpcVal->_php_class can also be of type null; however, parameter $class of class_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

121
                    && class_exists(/** @scrutinizer ignore-type */ $xmlrpcVal->_php_class)
Loading history...
122 106
                ) {
123
                    $obj = @new $xmlrpcVal->_php_class();
124 214
                    foreach ($xmlrpcVal as $key => $value) {
125
                        $obj->$key = $this->decode($value, $options);
126
                    }
127
                    return $obj;
128
                } else {
129 65
                    $arr = array();
130 43
                    foreach ($xmlrpcVal as $key => $value) {
131
                        $arr[$key] = $this->decode($value, $options);
132 22
                    }
133 22
                    return $arr;
134 22
                }
135
136 22
            case 'msg':
137
                $paramCount = $xmlrpcVal->getNumParams();
0 ignored issues
show
Bug introduced by
The method getNumParams() does not exist on PhpXmlRpc\Value. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

137
                /** @scrutinizer ignore-call */ 
138
                $paramCount = $xmlrpcVal->getNumParams();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
138 44
                $arr = array();
139 44
                for ($i = 0; $i < $paramCount; $i++) {
140 44
                    $arr[] = $this->decode($xmlrpcVal->getParam($i), $options);
0 ignored issues
show
Bug introduced by
The method getParam() does not exist on PhpXmlRpc\Value. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

140
                    $arr[] = $this->decode($xmlrpcVal->/** @scrutinizer ignore-call */ getParam($i), $options);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
141
                }
142 44
                return $arr;
143
144
            /// @todo throw on unsupported type
145 150
        }
146 150
    }
147 150
148 150
    /**
149 150
     * Takes native php types and encodes them into xml-rpc Value objects, recursively.
150
     * PHP strings, integers, floats and booleans have a straightforward encoding - note that integers will _not_ be
151 150
     * converted to xml-rpc <i8> elements, even if they exceed the 32-bit range.
152
     * PHP arrays will be encoded to either xml-rpc structs or arrays, depending on whether they are hashes
153
     * or plain 0..N integer indexed.
154
     * PHP objects will be encoded into xml-rpc structs, except if they implement DateTimeInterface, in which case they
155
     * will be encoded as dateTime values.
156
     * PhpXmlRpc\Value objects will not be double-encoded - which makes it possible to pass in a pre-created base64 Value
157
     * as part of a php array.
158
     * If given a proper $options parameter, php object instances will be encoded into 'special' xml-rpc values, that can
159
     * later be decoded into php object instances by calling php_xmlrpc_decode() with a corresponding option.
160
     * PHP resource and NULL variables will be converted into uninitialized Value objects (which will lead to invalid
161
     * xml-rpc when later serialized); to support encoding of the latter use the appropriate $options parameter.
162
     *
163
     * @author Dan Libby
164
     *
165
     * @param mixed $phpVal the value to be converted into an xml-rpc value object
166
     * @param array $options can include:
167
     *                       - 'encode_php_objs' when set, some out-of-band info will be added to the xml produced by
168
     *                         serializing the built Value, which can later be decoced by this library to rebuild an
169
     *                         instance of the same php object
170
     *                       - 'auto_dates': when set, any string which respects the xml-rpc datetime format will be converted to a dateTime Value
171
     *                       - 'null_extension': when set, php NULL values will be converted to an xml-rpc <NIL> (or <EX:NIL>) Value
172
     *                       - 'extension_api': reserved for usage by phpxmlrpc-polyfill
173
     * @return Value
174 263
     *
175
     * Feature creep -- could support more types via optional type argument (string => datetime support has been added,
176 263
     * ??? => base64 not yet). Also: allow auto-encoding of integers to i8 when too-big to fit into i4
177 263
     */
178 263
    public function encode($phpVal, $options = array())
179
    {
180 258
        $type = gettype($phpVal);
181 2
        switch ($type) {
182
            case 'string':
183 258
                if (in_array('auto_dates', $options) && preg_match(PhpXmlRpc::$xmlrpc_datetime_format, $phpVal)) {
184
                    $xmlrpcVal = new Value($phpVal, Value::$xmlrpcDateTime);
185 258
                } else {
186 116
                    $xmlrpcVal = new Value($phpVal, Value::$xmlrpcString);
187 111
                }
188 111
                break;
189 30
            case 'integer':
190 2
                $xmlrpcVal = new Value($phpVal, Value::$xmlrpcInt);
191 2
                break;
192
            case 'double':
193 30
                $xmlrpcVal = new Value($phpVal, Value::$xmlrpcDouble);
194 2
                break;
195 2
            case 'boolean':
196 30
                $xmlrpcVal = new Value($phpVal, Value::$xmlrpcBoolean);
197
                break;
198
            case 'array':
199
                // A shorter one-liner would be
200
                //     $tmp = array_diff(array_keys($phpVal), range(0, count($phpVal)-1));
201
                // but execution time skyrockets!
202 26
                $j = 0;
203 26
                $arr = array();
204 26
                $ko = false;
205 26
                foreach ($phpVal as $key => $val) {
206 26
                    $arr[$key] = $this->encode($val, $options);
207 26
                    if (!$ko && $key !== $j) {
208 24
                        $ko = true;
209
                    }
210 26
                    $j++;
211
                }
212 26
                if ($ko) {
213 24
                    $xmlrpcVal = new Value($arr, Value::$xmlrpcStruct);
214
                } else {
215 4
                    $xmlrpcVal = new Value($arr, Value::$xmlrpcArray);
216
                }
217 26
                break;
218 5
            case 'object':
219 3
                if (is_a($phpVal, 'PhpXmlRpc\Value')) {
220 1
                    $xmlrpcVal = $phpVal;
221 2
                // DateTimeInterface is not present in php 5.4...
222 1
                } elseif (is_a($phpVal, 'DateTimeInterface') || is_a($phpVal, 'DateTime')) {
223 1
                    $xmlrpcVal = new Value($phpVal->format('Ymd\TH:i:s'), Value::$xmlrpcDateTime);
224
                } elseif (in_array('extension_api', $options) && $phpVal instanceof \stdClass && isset($phpVal->xmlrpc_type)) {
225
                    // Handle the 'pre-converted' base64 and datetime values
226
                    if (isset($phpVal->scalar)) {
227
                        switch ($phpVal->xmlrpc_type) {
228
                            case 'base64':
229
                                $xmlrpcVal = new Value($phpVal->scalar, Value::$xmlrpcBase64);
230
                                break;
231
                            case 'datetime':
232
                                $xmlrpcVal = new Value($phpVal->scalar, Value::$xmlrpcDateTime);
233
                                break;
234
                            default:
235
                                $xmlrpcVal = new Value();
236
                        }
237
                    } else {
238
                        $xmlrpcVal = new Value();
239
                    }
240
241 1
                } else {
242 1
                    $arr = array();
243 1
                    foreach ($phpVal as $k => $v) {
244
                        $arr[$k] = $this->encode($v, $options);
245 1
                    }
246 1
                    $xmlrpcVal = new Value($arr, Value::$xmlrpcStruct);
247
                    if (in_array('encode_php_objs', $options)) {
248
                        // let's save original class name into xml-rpc value: it might be useful later on...
249 1
                        $xmlrpcVal->_php_class = get_class($phpVal);
250
                    }
251
                }
252 3
                break;
253 2
            case 'NULL':
254 2
                if (in_array('extension_api', $options)) {
255
                    $xmlrpcVal = new Value('', Value::$xmlrpcString);
256 2
                } elseif (in_array('null_extension', $options)) {
257
                    $xmlrpcVal = new Value('', Value::$xmlrpcNull);
258
                } else {
259 2
                    $xmlrpcVal = new Value();
260
                }
261 2
                break;
262
            case 'resource':
263
                if (in_array('extension_api', $options)) {
264
                    $xmlrpcVal = new Value((int)$phpVal, Value::$xmlrpcInt);
265
                } else {
266
                    $xmlrpcVal = new Value();
267
                }
268
                break;
269
            // catch "user function", "unknown type"
270
            default:
271
                // it has to return an empty object in case, not a boolean. (giancarlo pinerolo)
272
                $xmlrpcVal = new Value();
273
                break;
274
        }
275
276
        return $xmlrpcVal;
277 263
    }
278
279
    /**
280
     * Convert the xml representation of a method response, method request or single
281
     * xml-rpc value into the appropriate object (a.k.a. deserialize).
282
     *
283
     * @param string $xmlVal
284
     * @param array $options unused atm
285
     * @return Value|Request|Response|false false on error, or an instance of either Value, Request or Response
286
     *
287
     * @todo is this a good name/class for this method? It does something quite different from 'decode' after all
288
     *       (returning objects vs returns plain php values)... In fact, it belongs rather to a Parser class
289
     * @todo feature creep -- we should allow an option to return php native types instead of PhpXmlRpc objects instances
290
     * @todo feature creep -- allow source charset to be passed in as an option, in case the xml misses its declaration
291
     * @todo feature creep -- allow expected type (val/req/resp) to be passed in as an option
292 3
     */
293
    public function decodeXml($xmlVal, $options = array())
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

293
    public function decodeXml($xmlVal, /** @scrutinizer ignore-unused */ $options = array())

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

Loading history...
294
    {
295 3
        // 'guestimate' encoding
296 3
        $valEncoding = XMLParser::guessEncoding('', $xmlVal);
297
        if ($valEncoding != '') {
298
299
            // Since parsing will fail if
300
            // - charset is not specified in the xml declaration,
301
            // - the encoding is not UTF8 and
302
            // - there are non-ascii chars in the text,
303
            // we try to work round that...
304
            // The following code might be better for mb_string enabled installs, but makes the lib about 200% slower...
305 3
            //if (!is_valid_charset($valEncoding, array('UTF-8'))
306
            if (!in_array($valEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($xmlVal)) {
307
                if (function_exists('mb_convert_encoding')) {
308
                    $xmlVal = mb_convert_encoding($xmlVal, 'UTF-8', $valEncoding);
309
                } else {
310
                    if ($valEncoding == 'ISO-8859-1') {
311
                        $xmlVal = utf8_encode($xmlVal);
312
                    } else {
313
                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': invalid charset encoding of xml text: ' . $valEncoding);
314
                    }
315
                }
316
            }
317
        }
318
319 3
        // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8!
320
        /// @todo with php < 5.6, this does not work. We should add a manual conversion of the xml string to UTF8
321
        if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
322
            $parserOptions = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
323 3
        } else {
324
            $parserOptions = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
325
        }
326 3
327 3
        $xmlRpcParser = $this->getParser();
328 3
        $_xh = $xmlRpcParser->parse(
329 3
            $xmlVal,
330 3
            XMLParser::RETURN_XMLRPCVALS,
331
            XMLParser::ACCEPT_REQUEST | XMLParser::ACCEPT_RESPONSE | XMLParser::ACCEPT_VALUE | XMLParser::ACCEPT_FAULT,
332
            $parserOptions
333
        );
334 3
        // BC
335
        if (!is_array($_xh)) {
336
            $_xh = $xmlRpcParser->_xh;
337
        }
338
339
        if ($_xh['isf'] > 1) {
340
            // test that $_xh['value'] is an obj, too???
341
342 3
            $this->getLogger()->error('XML-RPC: ' . $_xh['isf_reason']);
343 3
344 3
            return false;
345 3
        }
346
347
        switch ($_xh['rt']) {
348
            case 'methodresponse':
349
                $v = $_xh['value'];
350
                if ($_xh['isf'] == 1) {
351
                    /** @var Value $vc */
352 3
                    $vc = $v['faultCode'];
353
                    /** @var Value $vs */
354 3
                    $vs = $v['faultString'];
355
                    $r = new Response(0, $vc->scalarVal(), $vs->scalarVal());
356 1
                } else {
357 1
                    $r = new Response($v);
358 1
                }
359 1
                return $r;
360
361 1
            case 'methodcall':
362
                $req = new Request($_xh['method']);
363 1
                for ($i = 0; $i < count($_xh['params']); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
364 1
                    $req->addParam($_xh['params'][$i]);
365
                }
366
                return $req;
367
368
            case 'value':
369
                return $_xh['value'];
370
371
            case 'fault':
372
                // EPI api emulation
373
                $v = $_xh['value'];
374
                // use a known error code
375
                /** @var Value $vc */
376
                $vc = isset($v['faultCode']) ? $v['faultCode']->scalarVal() : PhpXmlRpc::$xmlrpcerr['invalid_return'];
377
                /** @var Value $vs */
378
                $vs = isset($v['faultString']) ? $v['faultString']->scalarVal() : '';
379
                if (!is_int($vc) || $vc == 0) {
0 ignored issues
show
introduced by
The condition is_int($vc) is always false.
Loading history...
380
                    $vc = PhpXmlRpc::$xmlrpcerr['invalid_return'];
381
                }
382
                return new Response(0, $vc, $vs);
0 ignored issues
show
Bug introduced by
$vs of type PhpXmlRpc\Value is incompatible with the type string expected by parameter $fString of PhpXmlRpc\Response::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

382
                return new Response(0, $vc, /** @scrutinizer ignore-type */ $vs);
Loading history...
383
384
            default:
385
                return false;
386
        }
387
    }
388
}
389