Passed
Push — master ( 327b5e...7cdbcc )
by Gaetano
05:53
created

XMLParser::parse()   F

Complexity

Conditions 15
Paths 706

Size

Total Lines 107
Code Lines 72

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 54
CRAP Score 15.225

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 15
eloc 72
c 1
b 0
f 0
nc 706
nop 4
dl 0
loc 107
ccs 54
cts 60
cp 0.9
crap 15.225
rs 2.1583

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
namespace PhpXmlRpc\Helper;
4
5
use PhpXmlRpc\PhpXmlRpc;
6
use PhpXmlRpc\Value;
7
8
/**
9
 * Deals with parsing the XML.
10
 * @see http://xmlrpc.com/spec.md
11
 *
12
 * @todo implement an interface to allow for alternative implementations
13
 *       - make access to $_xh protected, return more high-level data structures
14
 *       - move $this->accept, $this->callbacks to an internal-use parsing-options config, along with the private
15
 *         parts of $_xh
16
 *       - add parseRequest, parseResponse, parseValue methods
17
 * @todo if iconv() or mb_string() are available, we could allow to convert the received xml to a custom charset encoding
18
 *       while parsing, which is faster than doing it later by going over the rebuilt data structure
19
 * @todo allow to parse data from a stream, to avoid having to copy first the whole xml to memory
20
 */
21
class XMLParser
22
{
23
    const RETURN_XMLRPCVALS = 'xmlrpcvals';
24
    const RETURN_EPIVALS = 'epivals';
25
    const RETURN_PHP = 'phpvals';
26
27
    const ACCEPT_REQUEST = 1;
28
    const ACCEPT_RESPONSE = 2;
29
    const ACCEPT_VALUE = 4;
30
    const ACCEPT_FAULT = 8;
31
32
    protected static $logger;
33
34
    /**
35
     * @var array
36
     * Used to store state during parsing and to pass parsing results to callers.
37
     * Quick explanation of components:
38
     *  private:
39
     *    ac - used to accumulate values
40
     *    stack - array with genealogy of xml elements names, used to validate nesting of xmlrpc elements
41
     *    valuestack - array used for parsing arrays and structs
42
     *    lv - used to indicate "looking for a value": implements the logic to allow values with no types to be strings
43
     *         (values: 0=not looking, 1=looking, 3=found)
44
     *  public:
45
     *    isf - used to indicate an xml-rpc response fault (1), invalid xml-rpc fault (2), xml parsing fault (3) or
46
     *          bad parameters passed to the parsing call (4)
47
     *    isf_reason - used for storing xml-rpc response fault string
48
     *    value - used to store the value in responses
49
     *    method - used to store method name in requests
50
     *    params - used to store parameters in requests
51
     *    pt - used to store the type of each received parameter. Useful if parameters are automatically decoded to php values
52
     *    rt - 'methodcall', 'methodresponse', 'value' or 'fault' (the last one used only in EPI emulation mode)
53
     */
54
    public $_xh = array(
55
        'ac' => '',
56
        'stack' => array(),
57
        'valuestack' => array(),
58
        'isf' => 0,
59
        'isf_reason' => '',
60
        'value' => null,
61
        'method' => false,
62
        'params' => array(),
63
        'pt' => array(),
64
        'rt' => '',
65
    );
66
67
    /**
68
     * @var array[]
69
     * @internal
70
     */
71
    public $xmlrpc_valid_parents = array(
72
        'VALUE' => array('MEMBER', 'DATA', 'PARAM', 'FAULT'),
73
        'BOOLEAN' => array('VALUE'),
74
        'I4' => array('VALUE'),
75
        'I8' => array('VALUE'),
76
        'EX:I8' => array('VALUE'),
77
        'INT' => array('VALUE'),
78
        'STRING' => array('VALUE'),
79
        'DOUBLE' => array('VALUE'),
80
        'DATETIME.ISO8601' => array('VALUE'),
81
        'BASE64' => array('VALUE'),
82
        'MEMBER' => array('STRUCT'),
83
        'NAME' => array('MEMBER'),
84
        'DATA' => array('ARRAY'),
85
        'ARRAY' => array('VALUE'),
86
        'STRUCT' => array('VALUE'),
87
        'PARAM' => array('PARAMS'),
88
        'METHODNAME' => array('METHODCALL'),
89
        'PARAMS' => array('METHODCALL', 'METHODRESPONSE'),
90
        'FAULT' => array('METHODRESPONSE'),
91 572
        'NIL' => array('VALUE'), // only used when extension activated
92
        'EX:NIL' => array('VALUE'), // only used when extension activated
93 572
    );
94 572
95
    /** @var int[] $parsing_options */
96
    protected $parsing_options = array();
97
    /** @var int $accept self::ACCEPT_REQUEST | self::ACCEPT_RESPONSE by default */
98
    protected $accept = 3;
99
    /** @var int $maxChunkLength 4 MB by default. Any value below 10MB should be good */
100
    protected $maxChunkLength = 4194304;
101
    /** @var \Callable[] */
102 712
    protected $callbacks = array();
103
104 712
    public function getLogger()
105
    {
106
        if (self::$logger === null) {
107
            self::$logger = Logger::instance();
108
        }
109
        return self::$logger;
110
    }
111
112
    /**
113
     * @param $logger
114
     * @return void
115
     */
116
    public static function setLogger($logger)
117 712
    {
118
        self::$logger = $logger;
119
    }
120 712
121 2
    /**
122 2
     * @param int[] $options passed to the xml parser
123 2
     */
124
    public function __construct(array $options = array())
125
    {
126 710
        $this->parsing_options = $options;
127
    }
128 710
129
    /**
130
     * @param string $data
131 710
     * @param string $returnType self::RETURN_XMLRPCVALS, self::RETURN_PHP, self::RETURN_EPIVALS
132 709
     * @param int $accept a bit-combination of self::ACCEPT_REQUEST, self::ACCEPT_RESPONSE, self::ACCEPT_VALUE
133
     * @param array $options integer-key options are passed to the xml parser, in addition to the options received in
134
     *                       the constructor. String-key options are used independently
135 710
     * @return void
136
     */
137 710
    public function parse($data, $returnType = self::RETURN_XMLRPCVALS, $accept = 3, $options = array())
138
    {
139
        $this->_xh = array(
140 710
            'ac' => '',
141 27
            'stack' => array(),
142 27
            'valuestack' => array(),
143 708
            'isf' => 0,
144
            'isf_reason' => '',
145
            'value' => null,
146
            'method' => false, // so we can check later if we got a methodname or not
147 708
            'params' => array(),
148
            'pt' => array(),
149
            'rt' => '',
150 710
        );
151 710
152
        $len = strlen($data);
153 710
154
        // we test for empty documents here to save on resource allocation and simply the chunked-parsing loop below
155
        if ($len == 0) {
156 710
            $this->_xh['isf'] = 3;
157 710
            $this->_xh['isf_reason'] = 'XML error 5: empty document';
158
            return;
159 710
        }
160 3
161 3
        $prevAccept = $this->accept;
162 3
        $this->accept = $accept;
163
164 3
        $this->callbacks = array();
165 3
        foreach ($options as $key => $val) {
166 3
            if (is_string($key)) {
167
                switch($key) {
168
                    case 'methodname_callback':
169
                        if (!is_callable($val)) {
170 710
                            $this->_xh['isf'] = 4;
171 710
                            $this->_xh['isf_reason'] = "Callback passed as 'methodname_callback' is not callable";
172
                            return;
173
                        } else {
174
                            $this->callbacks['methodname'] = $val;
175
                        }
176
                        break;
177
                    default:
178
                        $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ": unsupported option: $key");
179
                }
180
                unset($options[$key]);
181 710
            }
182
        }
183
184 710
        // NB: we use '' instead of null to force charset detection from the xml declaration
185
        $parser = xml_parser_create('');
186
187 710
        foreach ($this->parsing_options as $key => $val) {
188
            xml_parser_set_option($parser, $key, $val);
189
        }
190
        foreach ($options as $key => $val) {
191
            xml_parser_set_option($parser, $key, $val);
192 710
        }
193 710
        // always set this, in case someone tries to disable it via options...
194
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 1);
195
196
        xml_set_object($parser, $this);
197 710
198 708
        switch ($returnType) {
199 3
            case self::RETURN_PHP:
200 703
                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
201 710
                break;
202
            case self::RETURN_EPIVALS:
203 2
                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_epi');
204 2
                break;
205
            /// @todo log a warning on unsupported return type
206 703
            case XMLParser::RETURN_XMLRPCVALS:
207
            default:
208
                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
209
        }
210 710
211 710
        xml_set_character_data_handler($parser, 'xmlrpc_cd');
212 2
        xml_set_default_handler($parser, 'xmlrpc_dh');
213 2
214
        try {
215 2
            // @see ticket #70 - we have to parse big xml docs in chunks to avoid errors
216
            for ($offset = 0; $offset < $len; $offset += $this->maxChunkLength) {
217
                $chunk = substr($data, $offset, $this->maxChunkLength);
218
                // error handling: xml not well formed
219 710
                if (!xml_parse($parser, $chunk, $offset + $this->maxChunkLength >= $len)) {
220
                    $errCode = xml_get_error_code($parser);
221 710
                    $errStr = sprintf('XML error %s: %s at line %d, column %d', $errCode, xml_error_string($errCode),
222
                        xml_get_current_line_number($parser), xml_get_current_column_number($parser));
223 708
224 708
                    $this->_xh['isf'] = 3;
225 708
                    $this->_xh['isf_reason'] = $errStr;
226 708
                    break;
227 708
                }
228 710
                // no need to parse further if we already have a fatal error
229 710
                if ($this->_xh['isf'] >= 2) {
230 1
                    break;
231
                }
232
            }
233
        } catch (\Exception $e) {
234
            xml_parser_free($parser);
235
            $this->callbacks = array();
236
            $this->accept = $prevAccept;
237
            /// @todo should we set $this->_xh['isf'] and $this->_xh['isf_reason'] ?
238 710
            throw $e;
239 710
        }
240 710
241 710
        xml_parser_free($parser);
242 710
        $this->callbacks = array();
243 710
        $this->accept = $prevAccept;
244 710
    }
245 685
246
    /**
247 1
     * xml parser handler function for opening element tags.
248 1
     * @internal
249
     *
250 1
     * @param resource $parser
251
     * @param string $name
252 685
     * @param $attrs
253 685
     * @param bool $acceptSingleVals DEPRECATED use the $accept parameter instead
254 710
     * @return void
255 710
     *
256 400
     * @todo optimization: throw when setting $this->_xh['isf'] > 1, to completely avoid further xml parsing
257
     */
258 1
    public function xmlrpc_se($parser, $name, $attrs, $acceptSingleVals = false)
0 ignored issues
show
Unused Code introduced by
The parameter $parser 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

258
    public function xmlrpc_se(/** @scrutinizer ignore-unused */ $parser, $name, $attrs, $acceptSingleVals = false)

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...
259 1
    {
260
        // if invalid xmlrpc already detected, skip all processing
261 1
        if ($this->_xh['isf'] >= 2) {
262
            return;
263
        }
264 399
265 399
        // check for correct element nesting
266 399
        if (count($this->_xh['stack']) == 0) {
267
            // top level element can only be of 2 types
268
            /// @todo optimization creep: save this check into a bool variable, instead of using count() every time:
269 399
            ///       there is only a single top level element in xml anyway
270 22
            // BC
271
            if ($acceptSingleVals === false) {
272 399
                $accept = $this->accept;
273 399
            } else {
274 399
                $accept = self::ACCEPT_REQUEST | self::ACCEPT_RESPONSE | self::ACCEPT_VALUE;
275 710
            }
276 239
            if (($name == 'METHODCALL' && ($accept & self::ACCEPT_REQUEST)) ||
277
                ($name == 'METHODRESPONSE' && ($accept & self::ACCEPT_RESPONSE)) ||
278 1
                ($name == 'VALUE' && ($accept & self::ACCEPT_VALUE)) ||
279 1
                ($name == 'FAULT' && ($accept & self::ACCEPT_FAULT))) {
280
                $this->_xh['rt'] = strtolower($name);
281 1
            } else {
282
                $this->_xh['isf'] = 2;
283 710
                $this->_xh['isf_reason'] = 'missing top level xmlrpc element. Found: ' . $name;
284 710
285 710
                return;
286
            }
287 710
        } else {
288 710
            // not top level element: see if parent is OK
289 710
            $parent = end($this->_xh['stack']);
290
            if (!array_key_exists($name, $this->xmlrpc_valid_parents) || !in_array($parent, $this->xmlrpc_valid_parents[$name])) {
291 637
                $this->_xh['isf'] = 2;
292 637
                $this->_xh['isf_reason'] = "xmlrpc element $name cannot be child of $parent";
293 710
294 109
                return;
295 109
            }
296 710
        }
297
298 289
        switch ($name) {
299
            // optimize for speed switch cases: most common cases first
300
            case 'VALUE':
301 688
                /// @todo we could check for 2 VALUE elements inside a MEMBER or PARAM element
302
                $this->_xh['vt'] = 'value'; // indicator: no value found yet
303 710
                $this->_xh['ac'] = '';
304 710
                $this->_xh['lv'] = 1;
305 23
                $this->_xh['php_class'] = null;
306 23
                break;
307 23
308 23
            case 'I8':
309
            case 'EX:I8':
310
                if (PHP_INT_SIZE === 4) {
311
                    // INVALID ELEMENT: RAISE ISF so that it is later recognized!!!
312
                    $this->_xh['isf'] = 2;
313
                    $this->_xh['isf_reason'] = "Received i8 element but php is compiled in 32 bit mode";
314
315 23
                    return;
316 23
                }
317
                // fall through voluntarily
318
319
            case 'I4':
320
            case 'INT':
321
            case 'STRING':
322 1
            case 'BOOLEAN':
323 1
            case 'DOUBLE':
324 1
            case 'DATETIME.ISO8601':
325
            case 'BASE64':
326
                if ($this->_xh['vt'] != 'value') {
327
                    // two data elements inside a value: an error occurred!
328 710
                    $this->_xh['isf'] = 2;
329
                    $this->_xh['isf_reason'] = "$name element following a {$this->_xh['vt']} element inside a single value";
330
331 710
                    return;
332 710
                }
333
                $this->_xh['ac'] = ''; // reset the accumulator
334
                break;
335 710
336
            case 'STRUCT':
337
            case 'ARRAY':
338
                if ($this->_xh['vt'] != 'value') {
339
                    // two data elements inside a value: an error occurred!
340
                    $this->_xh['isf'] = 2;
341
                    $this->_xh['isf_reason'] = "$name element following a {$this->_xh['vt']} element inside a single value";
342
343
                    return;
344
                }
345
                // create an empty array to hold child values, and push it onto appropriate stack
346
                $curVal = array();
347
                $curVal['values'] = array();
348
                $curVal['type'] = $name;
349
                // check for out-of-band information to rebuild php objs
350
                // and in case it is found, save it
351
                if (@isset($attrs['PHP_CLASS'])) {
352
                    $curVal['php_class'] = $attrs['PHP_CLASS'];
353
                }
354
                $this->_xh['valuestack'][] = $curVal;
355
                $this->_xh['vt'] = 'data'; // be prepared for a data element next
356
                break;
357 710
358
            case 'DATA':
359 710
                if ($this->_xh['vt'] != 'data') {
360
                    // two data elements inside a value: an error occurred!
361
                    $this->_xh['isf'] = 2;
362
                    $this->_xh['isf_reason'] = "found two data elements inside an array element";
363
364 709
                    return;
365
                }
366 709
367 709
            case 'METHODCALL':
368
            case 'METHODRESPONSE':
369 707
            case 'PARAMS':
370 30
                // valid elements that add little to processing
371 30
                break;
372
373
            case 'METHODNAME':
374 707
            case 'NAME':
375
                /// @todo we could check for 2 NAME elements inside a MEMBER element
376 705
                $this->_xh['ac'] = '';
377
                break;
378
379 705
            case 'FAULT':
380 22
                $this->_xh['isf'] = 1;
381
                break;
382 705
383 27
            case 'MEMBER':
384
                // set member name to null, in case we do not find in the xml later on
385
                $this->_xh['valuestack'][count($this->_xh['valuestack']) - 1]['name'] = '';
386
                //$this->_xh['ac']='';
387
                // Drop trough intentionally
388
389
            case 'PARAM':
390
                // clear value type, so we can check later if no value has been passed for this param/member
391
                $this->_xh['vt'] = null;
392
                break;
393
394
            case 'NIL':
395
            case 'EX:NIL':
396
                if (PhpXmlRpc::$xmlrpc_null_extension) {
397
                    if ($this->_xh['vt'] != 'value') {
398
                        // two data elements inside a value: an error occurred!
399
                        $this->_xh['isf'] = 2;
400
                        $this->_xh['isf_reason'] = "$name element following a {$this->_xh['vt']} element inside a single value";
401
402
                        return;
403
                    }
404
                    // reset the accumulator - q: is this necessary at all here?
405
                    $this->_xh['ac'] = '';
406 707
                    break;
407 707
                }
408 239
                // if here, we do not support the <NIL/> extension, so
409
                // drop through intentionally
410 707
411 709
            default:
412 709
                // INVALID ELEMENT: RAISE ISF so that it is later recognized
413 709
                /// @todo feature creep = allow a callback instead
414 709
                $this->_xh['isf'] = 2;
415 709
                $this->_xh['isf_reason'] = "found not-xmlrpc xml element $name";
416 709
                break;
417 708
        }
418 708
419 708
        // Save current element name to stack, to validate nesting
420 685
        $this->_xh['stack'][] = $name;
421
422
        /// @todo optimization creep: move this inside the big switch() above
423 685
        if ($name != 'VALUE') {
424 594
            $this->_xh['lv'] = 0;
425 477
        }
426 7
    }
427
428
    /**
429 7
     * xml parser handler function for opening element tags.
430 7
     * Used in decoding xml chunks that might represent single xmlrpc values as well as requests, responses.
431 472
     * @deprecated
432
     * @param resource $parser
433 22
     * @param $name
434 451
     * @param $attrs
435
     * @return void
436
     */
437
    public function xmlrpc_se_any($parser, $name, $attrs)
438
    {
439
        $this->xmlrpc_se($parser, $name, $attrs, true);
440
    }
441 46
442 46
    /**
443
     * xml parser handler function for close element tags.
444
     * @internal
445 24
     *
446
     * @param resource $parser
447
     * @param string $name
448 46
     * @param int $rebuildXmlrpcvals >1 for rebuilding xmlrpcvals, 0 for rebuilding php values, -1 for xmlrpc-extension compatibility
449
     * @return void
450 408
     */
451
    public function xmlrpc_ee($parser, $name, $rebuildXmlrpcvals = 1)
452
    {
453
        if ($this->_xh['isf'] >= 2) {
454 25
            return;
455
456
        }
457
        // push this element name from stack
458
        // NB: if XML validates, correct opening/closing is guaranteed and we do not have to check for $name == $currElem.
459
        // we also checked for proper nesting at start of elements...
460 25
        $currElem = array_pop($this->_xh['stack']);
0 ignored issues
show
Unused Code introduced by
The assignment to $currElem is dead and can be removed.
Loading history...
461
462
        switch ($name) {
463
            case 'VALUE':
464
                // This if() detects if no scalar was inside <VALUE></VALUE>
465 387
                if ($this->_xh['vt'] == 'value') {
466
                    $this->_xh['value'] = $this->_xh['ac'];
467
                    $this->_xh['vt'] = Value::$xmlrpcString;
468
                }
469
470
                if ($rebuildXmlrpcvals > 0) {
471 387
                    // build the xmlrpc val out of the data received, and substitute it
472
                    $temp = new Value($this->_xh['value'], $this->_xh['vt']);
473
                    // in case we got info about underlying php class, save it in the object we're rebuilding
474 685
                    if (isset($this->_xh['php_class'])) {
475 685
                        $temp->_php_class = $this->_xh['php_class'];
476 708
                    }
477 289
                    $this->_xh['value'] = $temp;
478 289
                } elseif ($rebuildXmlrpcvals < 0) {
479 708
                    if ($this->_xh['vt'] == Value::$xmlrpcDateTime) {
480
                        $this->_xh['value'] = (object)array(
481
                            'xmlrpc_type' => 'datetime',
482 289
                            'scalar' => $this->_xh['value'],
483 268
                            'timestamp' => \PhpXmlRpc\Helper\Date::iso8601Decode($this->_xh['value'])
484 268
                        );
485
                    } elseif ($this->_xh['vt'] == Value::$xmlrpcBase64) {
486 22
                        $this->_xh['value'] = (object)array(
487
                            'xmlrpc_type' => 'base64',
488 289
                            'scalar' => $this->_xh['value']
489 708
                        );
490 239
                    }
491 239
                } else {
492 707
                    /// @todo this should handle php-serialized objects, since std deserializing is done
493 707
                    ///  by php_xmlrpc_decode, which we will not be calling...
494
                    //if (isset($this->_xh['php_class'])) {
495 398
                    //}
496 398
                }
497 398
498 398
                // check if we are inside an array or struct:
499 22
                // if value just built is inside an array, let's move it into array on the stack
500
                $vscount = count($this->_xh['valuestack']);
501 398
                if ($vscount && $this->_xh['valuestack'][$vscount - 1]['type'] == 'ARRAY') {
502 707
                    $this->_xh['valuestack'][$vscount - 1]['values'][] = $this->_xh['value'];
503
                }
504
                break;
505 684
506 684
            case 'BOOLEAN':
507 684
            case 'I4':
508
            case 'I8':
509
            case 'EX:I8':
510
            case 'INT':
511 684
            case 'STRING':
512 707
            case 'DOUBLE':
513 562
            case 'DATETIME.ISO8601':
514 562
            case 'BASE64':
515 706
                $this->_xh['vt'] = strtolower($name);
516 706
                /// @todo: optimization creep - remove the if/elseif cycle below
517 23
                /// since the case() in which we are already did that
518 23
                if ($name == 'STRING') {
519 23
                    $this->_xh['value'] = $this->_xh['ac'];
520 23
                } elseif ($name == 'DATETIME.ISO8601') {
521 23
                    if (!preg_match('/^[0-9]{8}T[0-9]{2}:[0-9]{2}:[0-9]{2}$/', $this->_xh['ac'])) {
522
                        $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': invalid value received in DATETIME: ' . $this->_xh['ac']);
523
                    }
524 706
                    $this->_xh['vt'] = Value::$xmlrpcDateTime;
525 706
                    $this->_xh['value'] = $this->_xh['ac'];
526 706
                } elseif ($name == 'BASE64') {
527 705
                    /// @todo check for failure of base64 decoding / catch warnings
528 706
                    $this->_xh['value'] = base64_decode($this->_xh['ac']);
529
                } elseif ($name == 'BOOLEAN') {
530
                    // special case here: we translate boolean 1 or 0 into PHP constants true or false.
531
                    // Strings 'true' and 'false' are accepted, even though the spec never mentions them (see eg.
532 705
                    // Blogger api docs)
533
                    // NB: this simple checks helps a lot sanitizing input, ie. no security problems around here
534
                    if ($this->_xh['ac'] == '1' || strcasecmp($this->_xh['ac'], 'true') == 0) {
535 710
                        $this->_xh['value'] = true;
536
                    } else {
537
                        // log if receiving something strange, even though we set the value to false anyway
538
                        if ($this->_xh['ac'] != '0' && strcasecmp($this->_xh['ac'], 'false') != 0) {
539
                            $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': invalid value received in BOOLEAN: ' . $this->_xh['ac']);
540
                        }
541
                        $this->_xh['value'] = false;
542
                    }
543 27
                } elseif ($name == 'DOUBLE') {
544
                    // we have a DOUBLE
545 27
                    // we must check that only 0123456789-.<space> are characters here
546 27
                    // NOTE: regexp could be much stricter than this...
547
                    if (!preg_match('/^[+-eE0123456789 \t.]+$/', $this->_xh['ac'])) {
548
                        /// @todo: find a better way of throwing an error than this!
549
                        $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': non numeric value received in DOUBLE: ' . $this->_xh['ac']);
550
                        $this->_xh['value'] = 'ERROR_NON_NUMERIC_FOUND';
551
                    } else {
552
                        // it's ok, add it on
553
                        $this->_xh['value'] = (double)$this->_xh['ac'];
554
                    }
555
                } else {
556
                    // we have an I4/I8/INT
557
                    // we must check that only 0123456789-<space> are characters here
558
                    if (!preg_match('/^[+-]?[0123456789 \t]+$/', $this->_xh['ac'])) {
559
                        /// @todo find a better way of throwing an error than this!
560
                        $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': non numeric value received in INT: ' . $this->_xh['ac']);
561
                        $this->_xh['value'] = 'ERROR_NON_NUMERIC_FOUND';
562
                    } else {
563
                        // it's ok, add it on
564
                        $this->_xh['value'] = (int)$this->_xh['ac'];
565 710
                    }
566
                }
567
                $this->_xh['lv'] = 3; // indicate we've found a value
568 710
                break;
569
570
            case 'NAME':
571 710
                $this->_xh['valuestack'][count($this->_xh['valuestack']) - 1]['name'] = $this->_xh['ac'];
572 710
                break;
573
574
            case 'MEMBER':
575 710
                // add to array in the stack the last element built, unless no VALUE was found
576
                if ($this->_xh['vt']) {
577
                    $vscount = count($this->_xh['valuestack']);
578
                    $this->_xh['valuestack'][$vscount - 1]['values'][$this->_xh['valuestack'][$vscount - 1]['name']] = $this->_xh['value'];
579
                } else {
580
                    $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': missing VALUE inside STRUCT in received xml');
581
                }
582
                break;
583
584 696
            case 'DATA':
585
                $this->_xh['vt'] = null; // reset this to check for 2 data elements in a row - even if they're empty
586
                break;
587 696
588 696
            case 'STRUCT':
589
            case 'ARRAY':
590
                // fetch out of stack array of values, and promote it to current value
591
                $currVal = array_pop($this->_xh['valuestack']);
592
                $this->_xh['value'] = $currVal['values'];
593
                $this->_xh['vt'] = strtolower($name);
594 696
                if (isset($currVal['php_class'])) {
595
                    $this->_xh['php_class'] = $currVal['php_class'];
596
                }
597
                break;
598
599
            case 'PARAM':
600
                // add to array of params the current value, unless no VALUE was found
601
                if ($this->_xh['vt']) {
602
                    $this->_xh['params'][] = $this->_xh['value'];
603
                    $this->_xh['pt'][] = $this->_xh['vt'];
604
                } else {
605
                    $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': missing VALUE inside PARAM in received xml');
606
                }
607
                break;
608
609
            case 'METHODNAME':
610
                /// @todo why do we strip leading whitespace in method names, but not trailing whitespace?
611
                $methodname = preg_replace('/^[\n\r\t ]+/', '', $this->_xh['ac']);
612
                $this->_xh['method'] = $methodname;
613
                // we allow the callback to f.e. give us back a mangled method name by manipulating $this
614
                if (isset($this->callbacks['methodname'])) {
615
                    call_user_func($this->callbacks['methodname'], $methodname, $this, $parser);
616
                }
617 711
                break;
618
619
            case 'NIL':
620
            case 'EX:NIL':
621
                if (PhpXmlRpc::$xmlrpc_null_extension) {
622
                    $this->_xh['vt'] = 'null';
623
                    $this->_xh['value'] = null;
624
                    $this->_xh['lv'] = 3;
625
                    break;
626
                }
627
628
            // drop through intentionally if nil extension not enabled
629
            case 'PARAMS':
630
            case 'FAULT':
631
            case 'METHODCALL':
632
            case 'METHORESPONSE':
633
                break;
634 711
635 711
            default:
636 695
                // End of INVALID ELEMENT
637
                // Should we add an assert here for unreachable code? When an invalid element is found in xmlrpc_se,
638
                //
639
                break;
640
        }
641
    }
642
643
    /**
644
     * Used in decoding xmlrpc requests/responses without rebuilding xmlrpc Values.
645
     * @internal
646 529
     *
647
     * @param resource $parser
648 529
     * @param string $name
649
     * @return void
650 529
     */
651
    public function xmlrpc_ee_fast($parser, $name)
652
    {
653
        $this->xmlrpc_ee($parser, $name, 0);
654
    }
655
656
    /**
657
     * Used in decoding xmlrpc requests/responses while building xmlrpc-extension Values (plain php for all but base64 and datetime).
658 529
     * @internal
659 529
     *
660
     * @param resource $parser
661 24
     * @param string $name
662
     * @return void
663
     */
664
    public function xmlrpc_ee_epi($parser, $name)
665 506
    {
666 506
        $this->xmlrpc_ee($parser, $name, -1);
667 4
    }
668
669 506
    /**
670 4
     * xml parser handler function for character data.
671
     * @internal
672 502
     *
673
     * @param resource $parser
674
     * @param string $data
675
     * @return void
676 506
     */
677 499
    public function xmlrpc_cd($parser, $data)
0 ignored issues
show
Unused Code introduced by
The parameter $parser 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

677
    public function xmlrpc_cd(/** @scrutinizer ignore-unused */ $parser, $data)

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...
678
    {
679
        // skip processing if xml fault already detected
680 506
        if ($this->_xh['isf'] >= 2) {
681
            return;
682
        }
683
684
        // "lookforvalue == 3" means that we've found an entire value and should discard any further character data
685
        if ($this->_xh['lv'] != 3) {
686
            $this->_xh['ac'] .= $data;
687
        }
688
    }
689
690
    /**
691
     * xml parser handler function for 'other stuff', ie. not char data or element start/end tag.
692
     * In fact it only gets called on unknown entities...
693
     * @internal
694
     *
695
     * @param $parser
696 82
     * @param string data
0 ignored issues
show
Bug introduced by
The type PhpXmlRpc\Helper\data was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
697
     * @return void
698
     */
699
    public function xmlrpc_dh($parser, $data)
0 ignored issues
show
Unused Code introduced by
The parameter $parser 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

699
    public function xmlrpc_dh(/** @scrutinizer ignore-unused */ $parser, $data)

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...
700 82
    {
701
        // skip processing if xml fault already detected
702 82
        if ($this->_xh['isf'] >= 2) {
703
            return;
704 82
        }
705
706
        if (substr($data, 0, 1) == '&' && substr($data, -1, 1) == ';') {
707
            $this->_xh['ac'] .= $data;
708
        }
709
    }
710
711
    /**
712 82
     * xml charset encoding guessing helper function.
713 82
     * Tries to determine the charset encoding of an XML chunk received over HTTP.
714
     * NB: according to the spec (RFC 3023), if text/xml content-type is received over HTTP without a content-type,
715 78
     * we SHOULD assume it is strictly US-ASCII. But we try to be more tolerant of non-conforming (legacy?) clients/servers,
716
     * which will be most probably using UTF-8 anyway...
717
     * In order of importance checks:
718 5
     * 1. http headers
719
     * 2. BOM
720
     * 3. XML declaration
721
     * 4. guesses using mb_detect_encoding()
722
     *
723
     * @param string $httpHeader the http Content-type header
724
     * @param string $xmlChunk xml content buffer
725
     * @param string $encodingPrefs comma separated list of character encodings to be used as default (when mb extension is enabled).
726
     *                              This can also be set globally using PhpXmlRpc::$xmlrpc_detectencodings
727
     * @return string the encoding determined. Null if it can't be determined and mbstring is enabled,
728
     *                PhpXmlRpc::$xmlrpc_defencoding if it can't be determined and mbstring is not enabled
729
     *
730
     * @todo explore usage of mb_http_input(): does it detect http headers + post data? if so, use it instead of hand-detection!!!
731
     */
732
    public static function guessEncoding($httpHeader = '', $xmlChunk = '', $encodingPrefs = null)
733
    {
734
        // discussion: see http://www.yale.edu/pclt/encoding/
735
        // 1 - test if encoding is specified in HTTP HEADERS
736
737
        // Details:
738
        // LWS:           (\13\10)?( |\t)+
739
        // token:         (any char but excluded stuff)+
740
        // quoted string: " (any char but double quotes and control chars)* "
741
        // header:        Content-type = ...; charset=value(; ...)*
742
        //   where value is of type token, no LWS allowed between 'charset' and value
743
        // Note: we do not check for invalid chars in VALUE:
744
        //   this had better be done using pure ereg as below
745
        // Note 2: we might be removing whitespace/tabs that ought to be left in if
746
        //   the received charset is a quoted string. But nobody uses such charset names...
747
748
        /// @todo this test will pass if ANY header has charset specification, not only Content-Type. Fix it?
749
        $matches = array();
750
        if (preg_match('/;\s*charset\s*=([^;]+)/i', $httpHeader, $matches)) {
751
            return strtoupper(trim($matches[1], " \t\""));
752
        }
753
754
        // 2 - scan the first bytes of the data for a UTF-16 (or other) BOM pattern
755
        //     (source: http://www.w3.org/TR/2000/REC-xml-20001006)
756
        //     NOTE: actually, according to the spec, even if we find the BOM and determine
757
        //     an encoding, we should check if there is an encoding specified
758
        //     in the xml declaration, and verify if they match.
759
        /// @todo implement check as described above?
760
        /// @todo implement check for first bytes of string even without a BOM? (It sure looks harder than for cases WITH a BOM)
761
        if (preg_match('/^(\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\x00\x00\xFF\xFE|\xFE\xFF\x00\x00)/', $xmlChunk)) {
762
            return 'UCS-4';
763
        } elseif (preg_match('/^(\xFE\xFF|\xFF\xFE)/', $xmlChunk)) {
764
            return 'UTF-16';
765
        } elseif (preg_match('/^(\xEF\xBB\xBF)/', $xmlChunk)) {
766
            return 'UTF-8';
767
        }
768
769
        // 3 - test if encoding is specified in the xml declaration
770
        /// @todo this regexp will fail if $xmlChunk uses UTF-32/UCS-4, and most likely UTF-16/UCS-2 as well. In that
771
        ///       case we leave the guesswork up to mbstring - which seems to be able to detect it, starting with php 5.6.
772
        ///       For lower versions, we could attempt usage of mb_ereg...
773
        // Details:
774
        // SPACE:         (#x20 | #x9 | #xD | #xA)+ === [ \x9\xD\xA]+
775
        // EQ:            SPACE?=SPACE? === [ \x9\xD\xA]*=[ \x9\xD\xA]*
776
        if (preg_match('/^<\?xml\s+version\s*=\s*' . "((?:\"[a-zA-Z0-9_.:-]+\")|(?:'[a-zA-Z0-9_.:-]+'))" .
777
            '\s+encoding\s*=\s*' . "((?:\"[A-Za-z][A-Za-z0-9._-]*\")|(?:'[A-Za-z][A-Za-z0-9._-]*'))/",
778
            $xmlChunk, $matches)) {
779
            return strtoupper(substr($matches[2], 1, -1));
780
        }
781
782
        // 4 - if mbstring is available, let it do the guesswork
783
        if (function_exists('mb_detect_encoding')) {
784
            if ($encodingPrefs == null && PhpXmlRpc::$xmlrpc_detectencodings != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $encodingPrefs of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
785
                $encodingPrefs = PhpXmlRpc::$xmlrpc_detectencodings;
786
            }
787
            if ($encodingPrefs) {
788
                $enc = mb_detect_encoding($xmlChunk, $encodingPrefs);
789
            } else {
790
                $enc = mb_detect_encoding($xmlChunk);
791
            }
792
            // NB: mb_detect likes to call it ascii, xml parser likes to call it US_ASCII...
793
            // IANA also likes better US-ASCII, so go with it
794
            if ($enc == 'ASCII') {
795
                $enc = 'US-' . $enc;
796
            }
797
798
            return $enc;
799
        } else {
800
            // no encoding specified: as per HTTP1.1 assume it is iso-8859-1?
801
            // Both RFC 2616 (HTTP 1.1) and 1945 (HTTP 1.0) clearly state that for text/xxx content types
802
            // this should be the standard. And we should be getting text/xml as request and response.
803
            // BUT we have to be backward compatible with the lib, which always used UTF-8 as default...
804
            return PhpXmlRpc::$xmlrpc_defencoding;
805
        }
806
    }
807
808
    /**
809
     * Helper function: checks if an xml chunk has a charset declaration (BOM or in the xml declaration).
810
     *
811
     * @param string $xmlChunk
812
     * @return bool
813
     */
814
    public static function hasEncoding($xmlChunk)
815
    {
816
        // scan the first bytes of the data for a UTF-16 (or other) BOM pattern
817
        //     (source: http://www.w3.org/TR/2000/REC-xml-20001006)
818
        if (preg_match('/^(\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\x00\x00\xFF\xFE|\xFE\xFF\x00\x00)/', $xmlChunk)) {
819
            return true;
820
        } elseif (preg_match('/^(\xFE\xFF|\xFF\xFE)/', $xmlChunk)) {
821
            return true;
822
        } elseif (preg_match('/^(\xEF\xBB\xBF)/', $xmlChunk)) {
823
            return true;
824
        }
825
826
        // test if encoding is specified in the xml declaration
827
        // Details:
828
        // SPACE:         (#x20 | #x9 | #xD | #xA)+ === [ \x9\xD\xA]+
829
        // EQ:            SPACE?=SPACE? === [ \x9\xD\xA]*=[ \x9\xD\xA]*
830
        if (preg_match('/^<\?xml\s+version\s*=\s*' . "((?:\"[a-zA-Z0-9_.:-]+\")|(?:'[a-zA-Z0-9_.:-]+'))" .
831
            '\s+encoding\s*=\s*' . "((?:\"[A-Za-z][A-Za-z0-9._-]*\")|(?:'[A-Za-z][A-Za-z0-9._-]*'))/",
832
            $xmlChunk, $matches)) {
833
            return true;
834
        }
835
836
        return false;
837
    }
838
}
839