Passed
Push — master ( a94b2b...8614a7 )
by Gaetano
08:04
created

XMLParser::parse()   F

Complexity

Conditions 17
Paths 1233

Size

Total Lines 119
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 63
CRAP Score 17.1903

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 17
eloc 73
c 1
b 0
f 0
nc 1233
nop 4
dl 0
loc 119
ccs 63
cts 69
cp 0.913
crap 17.1903
rs 1.0499

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

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

700
    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...
701
    {
702 82
        // skip processing if xml fault already detected
703
        if ($this->_xh['isf'] >= 2) {
704 82
            return;
705
        }
706
707
        // "lookforvalue == 3" means that we've found an entire value and should discard any further character data
708
        if ($this->_xh['lv'] != 3) {
709
            $this->_xh['ac'] .= $data;
710
        }
711
    }
712 82
713 82
    /**
714
     * xml parser handler function for 'other stuff', ie. not char data or element start/end tag.
715 78
     * In fact it only gets called on unknown entities...
716
     * @internal
717
     *
718 5
     * @param $parser
719
     * @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...
720
     * @return void
721
     */
722
    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

722
    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...
723
    {
724
        // skip processing if xml fault already detected
725
        if ($this->_xh['isf'] >= 2) {
726
            return;
727
        }
728
729
        if (substr($data, 0, 1) == '&' && substr($data, -1, 1) == ';') {
730
            $this->_xh['ac'] .= $data;
731
        }
732
    }
733
734
    /**
735
     * xml charset encoding guessing helper function.
736
     * Tries to determine the charset encoding of an XML chunk received over HTTP.
737
     * NB: according to the spec (RFC 3023), if text/xml content-type is received over HTTP without a content-type,
738
     * we SHOULD assume it is strictly US-ASCII. But we try to be more tolerant of non-conforming (legacy?) clients/servers,
739
     * which will be most probably using UTF-8 anyway...
740
     * In order of importance checks:
741
     * 1. http headers
742
     * 2. BOM
743
     * 3. XML declaration
744
     * 4. guesses using mb_detect_encoding()
745
     *
746
     * @param string $httpHeader the http Content-type header
747
     * @param string $xmlChunk xml content buffer
748
     * @param string $encodingPrefs comma separated list of character encodings to be used as default (when mb extension is enabled).
749
     *                              This can also be set globally using PhpXmlRpc::$xmlrpc_detectencodings
750
     * @return string the encoding determined. Null if it can't be determined and mbstring is enabled,
751
     *                PhpXmlRpc::$xmlrpc_defencoding if it can't be determined and mbstring is not enabled
752
     *
753
     * @todo explore usage of mb_http_input(): does it detect http headers + post data? if so, use it instead of hand-detection!!!
754
     */
755
    public static function guessEncoding($httpHeader = '', $xmlChunk = '', $encodingPrefs = null)
756
    {
757
        // discussion: see http://www.yale.edu/pclt/encoding/
758
        // 1 - test if encoding is specified in HTTP HEADERS
759
760
        // Details:
761
        // LWS:           (\13\10)?( |\t)+
762
        // token:         (any char but excluded stuff)+
763
        // quoted string: " (any char but double quotes and control chars)* "
764
        // header:        Content-type = ...; charset=value(; ...)*
765
        //   where value is of type token, no LWS allowed between 'charset' and value
766
        // Note: we do not check for invalid chars in VALUE:
767
        //   this had better be done using pure ereg as below
768
        // Note 2: we might be removing whitespace/tabs that ought to be left in if
769
        //   the received charset is a quoted string. But nobody uses such charset names...
770
771
        /// @todo this test will pass if ANY header has charset specification, not only Content-Type. Fix it?
772
        $matches = array();
773
        if (preg_match('/;\s*charset\s*=([^;]+)/i', $httpHeader, $matches)) {
774
            return strtoupper(trim($matches[1], " \t\""));
775
        }
776
777
        // 2 - scan the first bytes of the data for a UTF-16 (or other) BOM pattern
778
        //     (source: http://www.w3.org/TR/2000/REC-xml-20001006)
779
        //     NOTE: actually, according to the spec, even if we find the BOM and determine
780
        //     an encoding, we should check if there is an encoding specified
781
        //     in the xml declaration, and verify if they match.
782
        /// @todo implement check as described above?
783
        /// @todo implement check for first bytes of string even without a BOM? (It sure looks harder than for cases WITH a BOM)
784
        if (preg_match('/^(\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\x00\x00\xFF\xFE|\xFE\xFF\x00\x00)/', $xmlChunk)) {
785
            return 'UCS-4';
786
        } elseif (preg_match('/^(\xFE\xFF|\xFF\xFE)/', $xmlChunk)) {
787
            return 'UTF-16';
788
        } elseif (preg_match('/^(\xEF\xBB\xBF)/', $xmlChunk)) {
789
            return 'UTF-8';
790
        }
791
792
        // 3 - test if encoding is specified in the xml declaration
793
        /// @todo this regexp will fail if $xmlChunk uses UTF-32/UCS-4, and most likely UTF-16/UCS-2 as well. In that
794
        ///       case we leave the guesswork up to mbstring - which seems to be able to detect it, starting with php 5.6.
795
        ///       For lower versions, we could attempt usage of mb_ereg...
796
        // Details:
797
        // SPACE:         (#x20 | #x9 | #xD | #xA)+ === [ \x9\xD\xA]+
798
        // EQ:            SPACE?=SPACE? === [ \x9\xD\xA]*=[ \x9\xD\xA]*
799
        if (preg_match('/^<\?xml\s+version\s*=\s*' . "((?:\"[a-zA-Z0-9_.:-]+\")|(?:'[a-zA-Z0-9_.:-]+'))" .
800
            '\s+encoding\s*=\s*' . "((?:\"[A-Za-z][A-Za-z0-9._-]*\")|(?:'[A-Za-z][A-Za-z0-9._-]*'))/",
801
            $xmlChunk, $matches)) {
802
            return strtoupper(substr($matches[2], 1, -1));
803
        }
804
805
        // 4 - if mbstring is available, let it do the guesswork
806
        if (function_exists('mb_detect_encoding')) {
807
            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...
808
                $encodingPrefs = PhpXmlRpc::$xmlrpc_detectencodings;
809
            }
810
            if ($encodingPrefs) {
811
                $enc = mb_detect_encoding($xmlChunk, $encodingPrefs);
812
            } else {
813
                $enc = mb_detect_encoding($xmlChunk);
814
            }
815
            // NB: mb_detect likes to call it ascii, xml parser likes to call it US_ASCII...
816
            // IANA also likes better US-ASCII, so go with it
817
            if ($enc == 'ASCII') {
818
                $enc = 'US-' . $enc;
819
            }
820
821
            return $enc;
822
        } else {
823
            // no encoding specified: as per HTTP1.1 assume it is iso-8859-1?
824
            // Both RFC 2616 (HTTP 1.1) and 1945 (HTTP 1.0) clearly state that for text/xxx content types
825
            // this should be the standard. And we should be getting text/xml as request and response.
826
            // BUT we have to be backward compatible with the lib, which always used UTF-8 as default...
827
            return PhpXmlRpc::$xmlrpc_defencoding;
828
        }
829
    }
830
831
    /**
832
     * Helper function: checks if an xml chunk has a charset declaration (BOM or in the xml declaration).
833
     *
834
     * @param string $xmlChunk
835
     * @return bool
836
     */
837
    public static function hasEncoding($xmlChunk)
838
    {
839
        // scan the first bytes of the data for a UTF-16 (or other) BOM pattern
840
        //     (source: http://www.w3.org/TR/2000/REC-xml-20001006)
841
        if (preg_match('/^(\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\x00\x00\xFF\xFE|\xFE\xFF\x00\x00)/', $xmlChunk)) {
842
            return true;
843
        } elseif (preg_match('/^(\xFE\xFF|\xFF\xFE)/', $xmlChunk)) {
844
            return true;
845
        } elseif (preg_match('/^(\xEF\xBB\xBF)/', $xmlChunk)) {
846
            return true;
847
        }
848
849
        // test if encoding is specified in the xml declaration
850
        // Details:
851
        // SPACE:         (#x20 | #x9 | #xD | #xA)+ === [ \x9\xD\xA]+
852
        // EQ:            SPACE?=SPACE? === [ \x9\xD\xA]*=[ \x9\xD\xA]*
853
        if (preg_match('/^<\?xml\s+version\s*=\s*' . "((?:\"[a-zA-Z0-9_.:-]+\")|(?:'[a-zA-Z0-9_.:-]+'))" .
854
            '\s+encoding\s*=\s*' . "((?:\"[A-Za-z][A-Za-z0-9._-]*\")|(?:'[A-Za-z][A-Za-z0-9._-]*'))/",
855
            $xmlChunk, $matches)) {
856
            return true;
857
        }
858
859
        return false;
860
    }
861
862
    // BC layer
863
864
    public function __set($name, $value)
865
    {
866
        //trigger_error('setting property Response::' . $name . ' is deprecated', E_USER_DEPRECATED);
867
868
        switch ($name) {
869
            case 'accept':
870
                $this->current_parsing_options['accept'] = $value;
871
                break;
872
            default:
873
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
874
                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
875
        }
876
    }
877
878
    public function __isset($name)
879
    {
880
        //trigger_error('checking property Response::' . $name . ' is deprecated', E_USER_DEPRECATED);
881
882
        switch ($name) {
883
            case 'accept':
884
                return isset($this->current_parsing_options['accept']);
885
            default:
886
                return false;
887
        }
888
    }
889
890
    public function __unset($name)
891
    {
892
        switch ($name) {
893
            case 'accept':
894
                unset($this->current_parsing_options['accept']);
895
                break;
896
            default:
897
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
898
                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
899
        }
900
    }
901
}
902