XMLParser::xmlrpc_ee()   F
last analyzed

Complexity

Conditions 70
Paths 164

Size

Total Lines 287
Code Lines 177

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 59
CRAP Score 93.6513

Importance

Changes 7
Bugs 1 Features 2
Metric Value
cc 70
eloc 177
c 7
b 1
f 2
nc 164
nop 3
dl 0
loc 287
ccs 59
cts 71
cp 0.831
crap 93.6513
rs 2.9066

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\Traits\DeprecationLogger;
7
use PhpXmlRpc\Value;
8
9
/**
10
 * Deals with parsing the XML.
11
 * @see http://xmlrpc.com/spec.md
12
 *
13
 * @todo implement an interface to allow for alternative implementations
14
 *       - make access to $_xh protected, return more high-level data structures
15
 *       - move the private parts of $_xh to the internal-use parsing-options config
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 rename? This is an xml-rpc parser, not a generic xml parser...
20
 *
21
 * @property array $xmlrpc_valid_parents deprecated - public access left in purely for BC
22
 * @property int $accept deprecated - (protected) access left in purely for BC
23
 */
24
class XMLParser
25
{
26
    use DeprecationLogger;
27
28
    const RETURN_XMLRPCVALS = 'xmlrpcvals';
29
    const RETURN_EPIVALS = 'epivals';
30
    const RETURN_PHP = 'phpvals';
31
32
    const ACCEPT_REQUEST = 1;
33
    const ACCEPT_RESPONSE = 2;
34
    const ACCEPT_VALUE = 4;
35
    const ACCEPT_FAULT = 8;
36
37
    /**
38
     * @var int
39
     * The max length beyond which data will get truncated in error messages
40
     */
41
    protected $maxLogValueLength = 100;
42
43
    /**
44
     * @var array
45
     * Used to store state during parsing and to pass parsing results to callers.
46
     * Quick explanation of components:
47
     *  private:
48
     *    ac - used to accumulate values
49
     *    stack - array with genealogy of xml elements names, used to validate nesting of xml-rpc elements
50
     *    valuestack - array used for parsing arrays and structs
51
     *    lv - used to indicate "looking for a value": implements the logic to allow values with no types to be strings
52
     *         (values: 0=not looking, 1=looking, 3=found)
53
     *  public:
54
     *    isf - used to indicate an xml-rpc response fault (1), invalid xml-rpc fault (2), xml parsing fault (3)
55
     *    isf_reason - used for storing xml-rpc response fault string
56
     *    value - used to store the value in responses
57
     *    method - used to store method name in requests
58
     *    params - used to store parameters in requests
59
     *    pt - used to store the type of each received parameter. Useful if parameters are automatically decoded to php values
60
     *    rt - 'methodcall', 'methodresponse', 'value' or 'fault' (the last one used only in EPI emulation mode)
61
     */
62
    protected $_xh = array(
63
        'ac' => '',
64
        'stack' => array(),
65
        'valuestack' => array(),
66
        'lv' => 0,
67
        'isf' => 0,
68
        'isf_reason' => '',
69
        'value' => null,
70
        'method' => false,
71
        'params' => array(),
72
        'pt' => array(),
73
        'rt' => '',
74
    );
75
76
    /**
77
     * @var array[]
78
     */
79
    protected $xmlrpc_valid_parents = array(
80
        'VALUE' => array('MEMBER', 'DATA', 'PARAM', 'FAULT'),
81
        'BOOLEAN' => array('VALUE'),
82
        'I4' => array('VALUE'),
83
        'I8' => array('VALUE'),
84
        'EX:I8' => array('VALUE'),
85
        'INT' => array('VALUE'),
86
        'STRING' => array('VALUE'),
87
        'DOUBLE' => array('VALUE'),
88
        'DATETIME.ISO8601' => array('VALUE'),
89
        'BASE64' => array('VALUE'),
90
        'MEMBER' => array('STRUCT'),
91 572
        'NAME' => array('MEMBER'),
92
        'DATA' => array('ARRAY'),
93 572
        'ARRAY' => array('VALUE'),
94 572
        'STRUCT' => array('VALUE'),
95
        'PARAM' => array('PARAMS'),
96
        'METHODNAME' => array('METHODCALL'),
97
        'PARAMS' => array('METHODCALL', 'METHODRESPONSE'),
98
        'FAULT' => array('METHODRESPONSE'),
99
        'NIL' => array('VALUE'), // only used when extension activated
100
        'EX:NIL' => array('VALUE'), // only used when extension activated
101
    );
102 712
103
    /** @var array $parsing_options */
104 712
    protected $parsing_options = array();
105
106
    /** @var int $accept self::ACCEPT_REQUEST | self::ACCEPT_RESPONSE by default */
107
    //protected $accept = 3;
108
109
    /** @var int $maxChunkLength 4 MB by default. Any value below 10MB should be good */
110
    protected $maxChunkLength = 4194304;
111
    /** @var array
112
     * Used keys: accept, target_charset, methodname_callback, plus the ones set here.
113
     * We initialize it partially to help keep BC with subclasses which might have reimplemented `parse()` but not
114
     * the element handler methods
115
     */
116
    protected $current_parsing_options = array(
117 712
        'xmlrpc_null_extension' => false,
118
        'xmlrpc_return_datetimes' => false,
119
        'xmlrpc_reject_invalid_values' => false
120 712
    );
121 2
122 2
    /**
123 2
     * @param array $options integer keys: options passed to the inner xml parser
124
     *                       string keys:
125
     *                       - target_charset (string)
126 710
     *                       - methodname_callback (callable)
127
     *                       - xmlrpc_null_extension (bool)
128 710
     *                       - xmlrpc_return_datetimes (bool)
129
     *                       - xmlrpc_reject_invalid_values (bool)
130
     */
131 710
    public function __construct(array $options = array())
132 709
    {
133
        $this->parsing_options = $options;
134
    }
135 710
136
    /**
137 710
     * Parses an xml-rpc xml string. Results of the parsing are found in $this->['_xh'].
138
     * Logs to the error log any issues which do not cause the parsing to fail.
139
     *
140 710
     * @param string $data
141 27
     * @param string $returnType self::RETURN_XMLRPCVALS, self::RETURN_PHP, self::RETURN_EPIVALS
142 27
     * @param int $accept a bit-combination of self::ACCEPT_REQUEST, self::ACCEPT_RESPONSE, self::ACCEPT_VALUE
143 708
     * @param array $options integer-key options are passed to the xml parser, string-key options are used independently.
144
     *                       These options are added to options received in the constructor.
145
     *                       Note that if options xmlrpc_null_extension, xmlrpc_return_datetimes and xmlrpc_reject_invalid_values
146
     *                       are not set, the default settings from PhpXmlRpc\PhpXmlRpc are used
147 708
     * @return array see the definition of $this->_xh for the meaning of the results
148
     * @throws \Exception this can happen if a callback function is set and it does throw (i.e. we do not catch exceptions)
149
     *
150 710
     * @todo refactor? we could 1. return the parsed data structure, and 2. move $returnType and $accept into options
151 710
     * @todo feature-creep make it possible to pass in options overriding usage of PhpXmlRpc::$xmlrpc_XXX_format, so
152
     *       that parsing will be completely independent of global state. Note that it might incur a small perf hit...
153 710
     */
154
    public function parse($data, $returnType = self::RETURN_XMLRPCVALS, $accept = 3, $options = array())
155
    {
156 710
        $this->_xh = array(
157 710
            'ac' => '',
158
            'stack' => array(),
159 710
            'valuestack' => array(),
160 3
            'lv' => 0,
161 3
            'isf' => 0,
162 3
            'isf_reason' => '',
163
            'value' => null,
164 3
            'method' => false, // so we can check later if we got a methodname or not
165 3
            'params' => false, // so we can check later if we got a params tag or not
166 3
            'pt' => array(),
167
            'rt' => '',
168
        );
169
170 710
        $len = strlen($data);
171 710
172
        // we test for empty documents here to save on resource allocation and simplify the chunked-parsing loop below
173
        if ($len == 0) {
174
            $this->_xh['isf'] = 3;
175
            $this->_xh['isf_reason'] = 'XML error 5: empty document';
176
            return $this->_xh;
177
        }
178
179
        $this->current_parsing_options = array('accept' => $accept);
180
181 710
        $mergedOptions = $this->parsing_options;
182
        foreach ($options as $key => $val) {
183
            $mergedOptions[$key] = $val;
184 710
        }
185
186
        foreach ($mergedOptions as $key => $val) {
187 710
            // q: can php be built without ctype? should we use a regexp?
188
            if (is_string($key) && !ctype_digit($key)) {
189
                /// @todo on invalid options, throw/error-out instead of logging an error message?
190
                switch($key) {
191
                    case 'target_charset':
192 710
                        if (function_exists('mb_convert_encoding')) {
193 710
                            $this->current_parsing_options['target_charset'] = $val;
194
                        } else {
195
                            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ": 'target_charset' option is unsupported without mbstring");
196
                        }
197 710
                        break;
198 708
199 3
                    case 'methodname_callback':
200 703
                        if (is_callable($val)) {
201 710
                            $this->current_parsing_options['methodname_callback'] = $val;
202
                        } else {
203 2
                            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ": Callback passed as 'methodname_callback' is not callable");
204 2
                        }
205
                        break;
206 703
207
                    case 'xmlrpc_null_extension':
208
                    case 'xmlrpc_return_datetimes':
209
                    case 'xmlrpc_reject_invalid_values':
210 710
                        $this->current_parsing_options[$key] = $val;
211 710
                        break;
212 2
213 2
                    default:
214
                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ": unsupported option: $key");
215 2
                }
216
                unset($mergedOptions[$key]);
217
            }
218
        }
219 710
220
        if (!isset($this->current_parsing_options['xmlrpc_null_extension'])) {
221 710
            $this->current_parsing_options['xmlrpc_null_extension'] = PhpXmlRpc::$xmlrpc_null_extension;
222
        }
223 708
        if (!isset($this->current_parsing_options['xmlrpc_return_datetimes'])) {
224 708
            $this->current_parsing_options['xmlrpc_return_datetimes'] = PhpXmlRpc::$xmlrpc_return_datetimes;
225 708
        }
226 708
        if (!isset($this->current_parsing_options['xmlrpc_reject_invalid_values'])) {
227 708
            $this->current_parsing_options['xmlrpc_reject_invalid_values'] = PhpXmlRpc::$xmlrpc_reject_invalid_values;
228 710
        }
229 710
230 1
        // NB: we use '' instead of null to force charset detection from the xml declaration
231
        $parser = xml_parser_create('');
232
233
        foreach ($mergedOptions as $key => $val) {
234
            xml_parser_set_option($parser, $key, $val);
235
        }
236
237
        // always set this, in case someone tries to disable it via options...
238 710
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 1);
239 710
240 710
        xml_set_object($parser, $this);
241 710
242 710
        switch ($returnType) {
243 710
            case self::RETURN_PHP:
244 710
                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
245 685
                break;
246
            case self::RETURN_EPIVALS:
247 1
                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_epi');
248 1
                break;
249
            /// @todo log an error / throw / error-out on unsupported return type
250 1
            case XMLParser::RETURN_XMLRPCVALS:
251
            default:
252 685
                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
253 685
        }
254 710
255 710
        xml_set_character_data_handler($parser, 'xmlrpc_cd');
256 400
        xml_set_default_handler($parser, 'xmlrpc_dh');
257
258 1
        try {
259 1
            // @see ticket #70 - we have to parse big xml docs in chunks to avoid errors
260
            for ($offset = 0; $offset < $len; $offset += $this->maxChunkLength) {
261 1
                $chunk = substr($data, $offset, $this->maxChunkLength);
262
                // error handling: xml not well formed
263
                if (!@xml_parse($parser, $chunk, $offset + $this->maxChunkLength >= $len)) {
264 399
                    $errCode = xml_get_error_code($parser);
265 399
                    $errStr = sprintf('XML error %s: %s at line %d, column %d', $errCode, xml_error_string($errCode),
266 399
                        xml_get_current_line_number($parser), xml_get_current_column_number($parser));
267
                    $this->_xh['isf'] = 3;
268
                    $this->_xh['isf_reason'] = $errStr;
269 399
                }
270 22
                // no need to parse further if we already have a fatal error
271
                if ($this->_xh['isf'] >= 2) {
272 399
                    break;
273 399
                }
274 399
            }
275 710
        /// @todo bump minimum php version to 5.5 and use a finally clause instead of doing cleanup 3 times
276 239
        } catch (\Exception $e) {
277
            xml_parser_free($parser);
278 1
            $this->current_parsing_options = array();
279 1
            /// @todo should we set $this->_xh['isf'] and $this->_xh['isf_reason'] ?
280
            throw $e;
281 1
        } catch (\Error $e) {
282
            xml_parser_free($parser);
283 710
            $this->current_parsing_options = array();
284 710
                //$this->accept = $prevAccept;
285 710
                /// @todo should we set $this->_xh['isf'] and $this->_xh['isf_reason'] ?
286
            throw $e;
287 710
        }
288 710
289 710
        xml_parser_free($parser);
290
        $this->current_parsing_options = array();
291 637
292 637
        // BC
293 710
        if ($this->_xh['params'] === false) {
0 ignored issues
show
introduced by
The condition $this->_xh['params'] === false is always true.
Loading history...
294 109
            $this->_xh['params'] = array();
295 109
        }
296 710
297
        return $this->_xh;
298 289
    }
299
300
    /**
301 688
     * xml parser handler function for opening element tags.
302
     * @internal
303 710
     *
304 710
     * @param resource $parser
305 23
     * @param string $name
306 23
     * @param $attrs
307 23
     * @param bool $acceptSingleVals DEPRECATED use the $accept parameter instead
308 23
     * @return void
309
     *
310
     * @todo optimization creep: throw when setting $this->_xh['isf'] > 1, to completely avoid further xml parsing
311
     *       and remove the checking for $this->_xh['isf'] >= 2 everywhere
312
     */
313
    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

313
    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...
314
    {
315 23
        // if invalid xml-rpc already detected, skip all processing
316 23
        if ($this->_xh['isf'] >= 2) {
317
            return;
318
        }
319
320
        // check for correct element nesting
321
        if (count($this->_xh['stack']) == 0) {
322 1
            // top level element can only be of 2 types
323 1
            /// @todo optimization creep: save this check into a bool variable, instead of using count() every time:
324 1
            ///       there is only a single top level element in xml anyway
325
326
            // BC
327
            if ($acceptSingleVals === false) {
328 710
                $accept = $this->current_parsing_options['accept'];
329
            } else {
330
                $this->logDeprecation('Using argument $acceptSingleVals for method ' . __METHOD__ . ' is deprecated');
331 710
                $accept = self::ACCEPT_REQUEST | self::ACCEPT_RESPONSE | self::ACCEPT_VALUE;
332 710
            }
333
            if (($name == 'METHODCALL' && ($accept & self::ACCEPT_REQUEST)) ||
334
                ($name == 'METHODRESPONSE' && ($accept & self::ACCEPT_RESPONSE)) ||
335 710
                ($name == 'VALUE' && ($accept & self::ACCEPT_VALUE)) ||
336
                ($name == 'FAULT' && ($accept & self::ACCEPT_FAULT))) {
337
                $this->_xh['rt'] = strtolower($name);
338
            } else {
339
                $this->_xh['isf'] = 2;
340
                $this->_xh['isf_reason'] = 'missing top level xmlrpc element. Found: ' . $name;
341
342
                return;
343
            }
344
        } else {
345
            // not top level element: see if parent is OK
346
            $parent = end($this->_xh['stack']);
347
            if (!array_key_exists($name, $this->xmlrpc_valid_parents) || !in_array($parent, $this->xmlrpc_valid_parents[$name])) {
348
                $this->_xh['isf'] = 2;
349
                $this->_xh['isf_reason'] = "xmlrpc element $name cannot be child of $parent";
350
351
                return;
352
            }
353
        }
354
355
        switch ($name) {
356
            // optimize for speed switch cases: most common cases first
357 710
            case 'VALUE':
358
                /// @todo we could check for 2 VALUE elements inside a MEMBER or PARAM element
359 710
                $this->_xh['vt'] = 'value'; // indicator: no value found yet
360
                $this->_xh['ac'] = '';
361
                $this->_xh['lv'] = 1;
362
                $this->_xh['php_class'] = null;
363
                break;
364 709
365
            case 'I8':
366 709
            case 'EX:I8':
367 709
                if (PHP_INT_SIZE === 4) {
368
                    // INVALID ELEMENT: RAISE ISF so that it is later recognized!!!
369 707
                    $this->_xh['isf'] = 2;
370 30
                    $this->_xh['isf_reason'] = "Received i8 element but php is compiled in 32 bit mode";
371 30
372
                    return;
373
                }
374 707
                // fall through voluntarily
375
376 705
            case 'I4':
377
            case 'INT':
378
            case 'STRING':
379 705
            case 'BOOLEAN':
380 22
            case 'DOUBLE':
381
            case 'DATETIME.ISO8601':
382 705
            case 'BASE64':
383 27
                if ($this->_xh['vt'] != 'value') {
384
                    // two data elements inside a value: an error occurred!
385
                    $this->_xh['isf'] = 2;
386
                    $this->_xh['isf_reason'] = "$name element following a {$this->_xh['vt']} element inside a single value";
387
388
                    return;
389
                }
390
                $this->_xh['ac'] = ''; // reset the accumulator
391
                break;
392
393
            case 'STRUCT':
394
            case 'ARRAY':
395
                if ($this->_xh['vt'] != 'value') {
396
                    // two data elements inside a value: an error occurred!
397
                    $this->_xh['isf'] = 2;
398
                    $this->_xh['isf_reason'] = "$name element following a {$this->_xh['vt']} element inside a single value";
399
400
                    return;
401
                }
402
                // create an empty array to hold child values, and push it onto appropriate stack
403
                $curVal = array(
404
                    'values' => array(),
405
                    'type' => $name,
406 707
                );
407 707
                // check for out-of-band information to rebuild php objs and, in case it is found, save it
408 239
                if (@isset($attrs['PHP_CLASS'])) {
409
                    $curVal['php_class'] = $attrs['PHP_CLASS'];
410 707
                }
411 709
                $this->_xh['valuestack'][] = $curVal;
412 709
                $this->_xh['vt'] = 'data'; // be prepared for a data element next
413 709
                break;
414 709
415 709
            case 'DATA':
416 709
                if ($this->_xh['vt'] != 'data') {
417 708
                    // two data elements inside a value: an error occurred!
418 708
                    $this->_xh['isf'] = 2;
419 708
                    $this->_xh['isf_reason'] = "found two data elements inside an array element";
420 685
421
                    return;
422
                }
423 685
424 594
            case 'METHODCALL':
425 477
            case 'METHODRESPONSE':
426 7
                // valid elements that add little to processing
427
                break;
428
429 7
            case 'PARAMS':
430 7
                $this->_xh['params'] = array();
431 472
                break;
432
433 22
            case 'METHODNAME':
434 451
            case 'NAME':
435
                /// @todo we could check for 2 NAME elements inside a MEMBER element
436
                $this->_xh['ac'] = '';
437
                break;
438
439
            case 'FAULT':
440
                $this->_xh['isf'] = 1;
441 46
                break;
442 46
443
            case 'MEMBER':
444
                // set member name to null, in case we do not find in the xml later on
445 24
                $this->_xh['valuestack'][count($this->_xh['valuestack']) - 1]['name'] = null;
446
                //$this->_xh['ac']='';
447
                // Drop trough intentionally
448 46
449
            case 'PARAM':
450 408
                // clear value type, so we can check later if no value has been passed for this param/member
451
                $this->_xh['vt'] = null;
452
                break;
453
454 25
            case 'NIL':
455
            case 'EX:NIL':
456
                if ($this->current_parsing_options['xmlrpc_null_extension']) {
457
                    if ($this->_xh['vt'] != 'value') {
458
                        // two data elements inside a value: an error occurred!
459
                        $this->_xh['isf'] = 2;
460 25
                        $this->_xh['isf_reason'] = "$name element following a {$this->_xh['vt']} element inside a single value";
461
462
                        return;
463
                    }
464
                    // reset the accumulator - q: is this necessary at all here? we don't use it on _ee anyway for NILs
465 387
                    $this->_xh['ac'] = '';
466
467
                } else {
468
                    $this->_xh['isf'] = 2;
469
                    $this->_xh['isf_reason'] = 'Invalid NIL value received. Support for NIL can be enabled via \\PhpXmlRpc\\PhpXmlRpc::$xmlrpc_null_extension';
470
471 387
                    return;
472
                }
473
                break;
474 685
475 685
            default:
476 708
                // INVALID ELEMENT: RAISE ISF so that it is later recognized
477 289
                /// @todo feature creep = allow a callback instead
478 289
                $this->_xh['isf'] = 2;
479 708
                $this->_xh['isf_reason'] = "found not-xmlrpc xml element $name";
480
481
                return;
482 289
        }
483 268
484 268
        // Save current element name to stack, to validate nesting
485
        $this->_xh['stack'][] = $name;
486 22
487
        /// @todo optimization creep: move this inside the big switch() above
488 289
        if ($name != 'VALUE') {
489 708
            $this->_xh['lv'] = 0;
490 239
        }
491 239
    }
492 707
493 707
    /**
494
     * xml parser handler function for close element tags.
495 398
     * @internal
496 398
     *
497 398
     * @param resource $parser
498 398
     * @param string $name
499 22
     * @param int $rebuildXmlrpcvals >1 for rebuilding xmlrpcvals, 0 for rebuilding php values, -1 for xmlrpc-extension compatibility
500
     * @return void
501 398
     * @throws \Exception this can happen if a callback function is set and it does throw (i.e. we do not catch exceptions)
502 707
     *
503
     * @todo optimization creep: throw when setting $this->_xh['isf'] > 1, to completely avoid further xml parsing
504
     *       and remove the checking for $this->_xh['isf'] >= 2 everywhere
505 684
     */
506 684
    public function xmlrpc_ee($parser, $name, $rebuildXmlrpcvals = 1)
507 684
    {
508
        if ($this->_xh['isf'] >= 2) {
509
            return;
510
        }
511 684
512 707
        // push this element name from stack
513 562
        // NB: if XML validates, correct opening/closing is guaranteed and we do not have to check for $name == $currElem.
514 562
        // we also checked for proper nesting at start of elements...
515 706
        $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...
516 706
517 23
        switch ($name) {
518 23
            case 'VALUE':
519 23
                // If no scalar was inside <VALUE></VALUE>, it was a string value
520 23
                if ($this->_xh['vt'] == 'value') {
521 23
                    $this->_xh['value'] = $this->_xh['ac'];
522
                    $this->_xh['vt'] = Value::$xmlrpcString;
523
                }
524 706
525 706
                // in case there is charset conversion required, do it here, to catch both cases of string values
526 706
                if (isset($this->current_parsing_options['target_charset']) && $this->_xh['vt'] === Value::$xmlrpcString) {
527 705
                    $this->_xh['value'] = mb_convert_encoding($this->_xh['value'], $this->current_parsing_options['target_charset'], 'UTF-8');
528 706
                }
529
530
                if ($rebuildXmlrpcvals > 0) {
531
                    // build the xml-rpc val out of the data received, and substitute it
532 705
                    $temp = new Value($this->_xh['value'], $this->_xh['vt']);
533
                    // in case we got info about underlying php class, save it in the object we're rebuilding
534
                    if (isset($this->_xh['php_class'])) {
535 710
                        $temp->_php_class = $this->_xh['php_class'];
536
                    }
537
                    $this->_xh['value'] = $temp;
538
                } elseif ($rebuildXmlrpcvals < 0) {
539
                    if ($this->_xh['vt'] == Value::$xmlrpcDateTime) {
540
                        $this->_xh['value'] = (object)array(
541
                            'xmlrpc_type' => 'datetime',
542
                            'scalar' => $this->_xh['value'],
543 27
                            'timestamp' => \PhpXmlRpc\Helper\Date::iso8601Decode($this->_xh['value'])
544
                        );
545 27
                    } elseif ($this->_xh['vt'] == Value::$xmlrpcBase64) {
546 27
                        $this->_xh['value'] = (object)array(
547
                            'xmlrpc_type' => 'base64',
548
                            'scalar' => $this->_xh['value']
549
                        );
550
                    }
551
                } else {
552
                    /// @todo this should handle php-serialized objects, since std deserializing is done
553
                    ///       by php_xmlrpc_decode, which we will not be calling...
554
                    //if (isset($this->_xh['php_class'])) {
555
                    //}
556
                }
557
558
                // check if we are inside an array or struct:
559
                // if value just built is inside an array, let's move it into array on the stack
560
                $vscount = count($this->_xh['valuestack']);
561
                if ($vscount && $this->_xh['valuestack'][$vscount - 1]['type'] == 'ARRAY') {
562
                    $this->_xh['valuestack'][$vscount - 1]['values'][] = $this->_xh['value'];
563
                }
564
                break;
565 710
566
            case 'STRING':
567
                $this->_xh['vt'] = Value::$xmlrpcString;
568 710
                $this->_xh['lv'] = 3; // indicate we've found a value
569
                $this->_xh['value'] = $this->_xh['ac'];
570
                break;
571 710
572 710
            case 'BOOLEAN':
573
                $this->_xh['vt'] = Value::$xmlrpcBoolean;
574
                $this->_xh['lv'] = 3; // indicate we've found a value
575 710
                // We translate boolean 1 or 0 into PHP constants true or false. Strings 'true' and 'false' are accepted,
576
                // even though the spec never mentions them (see e.g. Blogger api docs)
577
                // NB: this simple checks helps a lot sanitizing input, i.e. no security problems around here
578
                // Note the non-strict type check: it will allow ' 1 '
579
                /// @todo feature-creep: use a flexible regexp, the same as we do with int, double and datetime.
580
                ///       Note that using a regexp would also make this test less sensitive to phpunit shenanigans, and
581
                ///       to changes in the way php compares strings (since 8.0, leading and trailing newlines are
582
                ///       accepted when deciding if a string numeric...)
583
                if ($this->_xh['ac'] == '1' || strcasecmp($this->_xh['ac'], 'true') === 0) {
584 696
                    $this->_xh['value'] = true;
585
                } else {
586
                    // log if receiving something strange, even though we set the value to false anyway
587 696
                    /// @todo to be consistent with the other types, we should return a value outside the good-value domain, e.g. NULL
588 696
                    if ($this->_xh['ac'] != '0' && strcasecmp($this->_xh['ac'], 'false') !== 0) {
589
                        if (!$this->handleParsingError('invalid data received in BOOLEAN value: ' .
590
                            $this->truncateValueForLog($this->_xh['ac']), __METHOD__)) {
591
                            return;
592
                        }
593
                    }
594 696
                    $this->_xh['value'] = false;
595
                }
596
                break;
597
598
            case 'EX:I8':
599
                $name = 'i8';
600
                // fall through voluntarily
601
            case 'I4':
602
            case 'I8':
603
            case 'INT':
604
                // NB: we build the Value object with the original xml element name found, except for ex:i8. The
605
                // `Value::scalarTyp()` function will do some normalization of the data
606
                $this->_xh['vt'] = strtolower($name);
607
                $this->_xh['lv'] = 3; // indicate we've found a value
608
                if (!preg_match(PhpXmlRpc::$xmlrpc_int_format, $this->_xh['ac'])) {
609
                    if (!$this->handleParsingError('non numeric data received in INT value: ' .
610
                        $this->truncateValueForLog($this->_xh['ac']), __METHOD__)) {
611
                        return;
612
                    }
613
                    /// @todo: find a better way of reporting an error value than this! Use NaN?
614
                    $this->_xh['value'] = 'ERROR_NON_NUMERIC_FOUND';
615
                } else {
616
                    // it's ok, add it on
617 711
                    $this->_xh['value'] = (int)$this->_xh['ac'];
618
                }
619
                break;
620
621
            case 'DOUBLE':
622
                $this->_xh['vt'] = Value::$xmlrpcDouble;
623
                $this->_xh['lv'] = 3; // indicate we've found a value
624
                if (!preg_match(PhpXmlRpc::$xmlrpc_double_format, $this->_xh['ac'])) {
625
                    if (!$this->handleParsingError('non numeric data received in DOUBLE value: ' .
626
                        $this->truncateValueForLog($this->_xh['ac']), __METHOD__)) {
627
                        return;
628
                    }
629
630
                    $this->_xh['value'] = 'ERROR_NON_NUMERIC_FOUND';
631
                } else {
632
                    // it's ok, add it on
633
                    $this->_xh['value'] = (double)$this->_xh['ac'];
634 711
                }
635 711
                break;
636 695
637
            case 'DATETIME.ISO8601':
638
                $this->_xh['vt'] = Value::$xmlrpcDateTime;
639
                $this->_xh['lv'] = 3; // indicate we've found a value
640
                if (!preg_match(PhpXmlRpc::$xmlrpc_datetime_format, $this->_xh['ac'])) {
641
                    if (!$this->handleParsingError('invalid data received in DATETIME value: ' .
642
                        $this->truncateValueForLog($this->_xh['ac']), __METHOD__)) {
643
                        return;
644
                    }
645
                }
646 529
                if ($this->current_parsing_options['xmlrpc_return_datetimes']) {
647
                    try {
648 529
                        $this->_xh['value'] = new \DateTime($this->_xh['ac']);
649
650 529
                    // the default regex used to validate the date string a few lines above should make this case impossible,
651
                    // but one never knows...
652
                    } catch(\Exception $e) {
653
                        // what to do? We can not guarantee that a valid date can be created. We return null...
654
                        if (!$this->handleParsingError('invalid data received in DATETIME value. Error ' .
655
                            $e->getMessage(), __METHOD__)) {
656
                            return;
657
                        }
658 529
                    }
659 529
                } else {
660
                    $this->_xh['value'] = $this->_xh['ac'];
661 24
                }
662
                break;
663
664
            case 'BASE64':
665 506
                $this->_xh['vt'] = Value::$xmlrpcBase64;
666 506
                $this->_xh['lv'] = 3; // indicate we've found a value
667 4
                if ($this->current_parsing_options['xmlrpc_reject_invalid_values']) {
668
                    $v = base64_decode($this->_xh['ac'], true);
669 506
                    if ($v === false) {
670 4
                        $this->_xh['isf'] = 2;
671
                        $this->_xh['isf_reason'] = 'Invalid data received in BASE64 value: '. $this->truncateValueForLog($this->_xh['ac']);
672 502
                        return;
673
                    }
674
                } else {
675
                    $v = base64_decode($this->_xh['ac']);
676 506
                    if ($v === '' && $this->_xh['ac'] !== '') {
677 499
                        // only the empty string should decode to the empty string
678
                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': invalid data received in BASE64 value: ' .
679
                            $this->truncateValueForLog($this->_xh['ac']));
680 506
                    }
681
                }
682
                $this->_xh['value'] = $v;
683
                break;
684
685
            case 'NAME':
686
                $this->_xh['valuestack'][count($this->_xh['valuestack']) - 1]['name'] = $this->_xh['ac'];
687
                break;
688
689
            case 'MEMBER':
690
                // add to array in the stack the last element built, unless no VALUE or no NAME were found
691
                if ($this->_xh['vt']) {
692
                    $vscount = count($this->_xh['valuestack']);
693
                    if ($this->_xh['valuestack'][$vscount - 1]['name'] === null) {
694
                        if (!$this->handleParsingError('missing NAME inside STRUCT in received xml', __METHOD__)) {
695
                            return;
696 82
                        }
697
                        $this->_xh['valuestack'][$vscount - 1]['name'] = '';
698
                    }
699
                    $this->_xh['valuestack'][$vscount - 1]['values'][$this->_xh['valuestack'][$vscount - 1]['name']] = $this->_xh['value'];
700 82
                } else {
701
                    if (!$this->handleParsingError('missing VALUE inside STRUCT in received xml', __METHOD__)) {
702 82
                        return;
703
                    }
704 82
                }
705
                break;
706
707
            case 'DATA':
708
                $this->_xh['vt'] = null; // reset this to check for 2 data elements in a row - even if they're empty
709
                break;
710
711
            case 'STRUCT':
712 82
            case 'ARRAY':
713 82
                // fetch out of stack array of values, and promote it to current value
714
                $currVal = array_pop($this->_xh['valuestack']);
715 78
                $this->_xh['value'] = $currVal['values'];
716
                $this->_xh['vt'] = strtolower($name);
717
                if (isset($currVal['php_class'])) {
718 5
                    $this->_xh['php_class'] = $currVal['php_class'];
719
                }
720
                break;
721
722
            case 'PARAM':
723
                // add to array of params the current value, unless no VALUE was found
724
                /// @todo should we also check if there were two VALUE inside the PARAM?
725
                if ($this->_xh['vt']) {
726
                    $this->_xh['params'][] = $this->_xh['value'];
727
                    $this->_xh['pt'][] = $this->_xh['vt'];
728
                } else {
729
                    if (!$this->handleParsingError('missing VALUE inside PARAM in received xml', __METHOD__)) {
730
                        return;
731
                    }
732
                }
733
                break;
734
735
            case 'METHODNAME':
736
                if (!preg_match(PhpXmlRpc::$xmlrpc_methodname_format, $this->_xh['ac'])) {
737
                    if (!$this->handleParsingError('invalid data received in METHODNAME: '.
738
                        $this->truncateValueForLog($this->_xh['ac']), __METHOD__)) {
739
                        return;
740
                    }
741
                }
742
                $methodName = trim($this->_xh['ac']);
743
                $this->_xh['method'] = $methodName;
744
                // we allow the callback to f.e. give us back a mangled method name by manipulating $this
745
                if (isset($this->current_parsing_options['methodname_callback'])) {
746
                    call_user_func($this->current_parsing_options['methodname_callback'], $methodName, $this, $parser);
747
                }
748
                break;
749
750
            case 'NIL':
751
            case 'EX:NIL':
752
                // NB: if NIL support is not enabled, parsing stops at element start. So this If is redundant
753
                //if ($this->current_parsing_options['xmlrpc_null_extension']) {
754
                    $this->_xh['vt'] = 'null';
755
                    $this->_xh['value'] = null;
756
                    $this->_xh['lv'] = 3;
757
                //}
758
                break;
759
760
            /// @todo add extra checking:
761
            ///       - FAULT should contain a single struct with the 2 expected members (check their name and type)
762
            case 'PARAMS':
763
            case 'FAULT':
764
                break;
765
766
            case 'METHODCALL':
767
                /// @todo should we allow to accept this case via a call to handleParsingError ?
768
                if ($this->_xh['method'] === false) {
769
                    $this->_xh['isf'] = 2;
770
                    $this->_xh['isf_reason'] = "missing METHODNAME element inside METHODCALL";
771
                }
772
                break;
773
774
            case 'METHODRESPONSE':
775
                /// @todo should we allow to accept these cases via a call to handleParsingError ?
776
                if ($this->_xh['isf'] != 1 && $this->_xh['params'] === false) {
777
                    $this->_xh['isf'] = 2;
778
                    $this->_xh['isf_reason'] = "missing both FAULT and PARAMS elements inside METHODRESPONSE";
779
                } elseif ($this->_xh['isf'] == 0 && count($this->_xh['params']) !== 1) {
780
                    $this->_xh['isf'] = 2;
781
                    $this->_xh['isf_reason'] = "PARAMS element inside METHODRESPONSE should have exactly 1 PARAM";
782
                } elseif ($this->_xh['isf'] == 1 && $this->_xh['params'] !== false) {
783
                    $this->_xh['isf'] = 2;
784
                    $this->_xh['isf_reason'] = "both FAULT and PARAMS elements found inside METHODRESPONSE";
785
                }
786
                break;
787
788
            default:
789
                // End of INVALID ELEMENT
790
                // Should we add an assert here for unreachable code? When an invalid element is found in xmlrpc_se,
791
                // $this->_xh['isf'] is set to 2...
792
                break;
793
        }
794
    }
795
796
    /**
797
     * Used in decoding xml-rpc requests/responses without rebuilding xml-rpc Values.
798
     * @internal
799
     *
800
     * @param resource $parser
801
     * @param string $name
802
     * @return void
803
     */
804
    public function xmlrpc_ee_fast($parser, $name)
805
    {
806
        $this->xmlrpc_ee($parser, $name, 0);
807
    }
808
809
    /**
810
     * Used in decoding xml-rpc requests/responses while building xmlrpc-extension Values (plain php for all but base64 and datetime).
811
     * @internal
812
     *
813
     * @param resource $parser
814
     * @param string $name
815
     * @return void
816
     */
817
    public function xmlrpc_ee_epi($parser, $name)
818
    {
819
        $this->xmlrpc_ee($parser, $name, -1);
820
    }
821
822
    /**
823
     * xml parser handler function for character data.
824
     * @internal
825
     *
826
     * @param resource $parser
827
     * @param string $data
828
     * @return void
829
     */
830
    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

830
    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...
831
    {
832
        // skip processing if xml fault already detected
833
        if ($this->_xh['isf'] >= 2) {
834
            return;
835
        }
836
837
        // "lookforvalue == 3" means that we've found an entire value and should discard any further character data
838
        if ($this->_xh['lv'] != 3) {
839
            $this->_xh['ac'] .= $data;
840
        }
841
    }
842
843
    /**
844
     * xml parser handler function for 'other stuff', i.e. not char data or element start/end tag.
845
     * In fact, it only gets called on unknown entities...
846
     * @internal
847
     *
848
     * @param $parser
849
     * @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...
850
     * @return void
851
     */
852
    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

852
    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...
853
    {
854
        // skip processing if xml fault already detected
855
        if ($this->_xh['isf'] >= 2) {
856
            return;
857
        }
858
859
        if (substr($data, 0, 1) == '&' && substr($data, -1, 1) == ';') {
860
            $this->_xh['ac'] .= $data;
861
        }
862
    }
863
864
    /**
865
     * xml charset encoding guessing helper function.
866
     * Tries to determine the charset encoding of an XML chunk received over HTTP.
867
     *
868
     * NB: according to the spec (RFC 3023), if text/xml content-type is received over HTTP without a content-type,
869
     * we SHOULD assume it is strictly US-ASCII. But we try to be more tolerant of non-conforming (legacy?) clients/servers,
870
     * which will be most probably using UTF-8 anyway...
871
     * In order of importance checks:
872
     * 1. http headers
873
     * 2. BOM
874
     * 3. XML declaration
875
     * 4. guesses using mb_detect_encoding()
876
     *
877
     * @param string $httpHeader the http Content-type header
878
     * @param string $xmlChunk xml content buffer
879
     * @param string $encodingPrefs comma separated list of character encodings to be used as default (when mb extension is enabled).
880
     *                              This can also be set globally using PhpXmlRpc::$xmlrpc_detectencodings
881
     * @return string the encoding determined. Null if it can't be determined and mbstring is enabled,
882
     *                PhpXmlRpc::$xmlrpc_defencoding if it can't be determined and mbstring is not enabled
883
     *
884
     * @todo as of 2023, the relevant RFC for XML Media Types is now 7303, and for HTTP it is 9110. Check if the order of
885
     *       precedence implemented here is still correct
886
     * @todo explore usage of mb_http_input(): does it detect http headers + post data? if so, use it instead of hand-detection!!!
887
     * @todo feature-creep make it possible to pass in options overriding usage of PhpXmlRpc static variables, to make
888
     *       the method independent of global state
889
     */
890
    public static function guessEncoding($httpHeader = '', $xmlChunk = '', $encodingPrefs = null)
891
    {
892
        // discussion: see http://www.yale.edu/pclt/encoding/
893
        // 1 - test if encoding is specified in HTTP HEADERS
894
895
        // Details:
896
        // LWS:           (\13\10)?( |\t)+
897
        // token:         (any char but excluded stuff)+
898
        // quoted string: " (any char but double quotes and control chars)* "
899
        // header:        Content-type = ...; charset=value(; ...)*
900
        //   where value is of type token, no LWS allowed between 'charset' and value
901
        // Note: we do not check for invalid chars in VALUE:
902
        //   this had better be done using pure ereg as below
903
        // Note 2: we might be removing whitespace/tabs that ought to be left in if
904
        //   the received charset is a quoted string. But nobody uses such charset names...
905
906
        /// @todo this test will pass if ANY header has charset specification, not only Content-Type. Fix it?
907
        $matches = array();
908
        if (preg_match('/;\s*charset\s*=([^;]+)/i', $httpHeader, $matches)) {
909
            return strtoupper(trim($matches[1], " \t\""));
910
        }
911
912
        // 2 - scan the first bytes of the data for a UTF-16 (or other) BOM pattern
913
        //     (source: http://www.w3.org/TR/2000/REC-xml-20001006)
914
        //     NOTE: actually, according to the spec, even if we find the BOM and determine
915
        //     an encoding, we should check if there is an encoding specified
916
        //     in the xml declaration, and verify if they match.
917
        /// @todo implement check as described above?
918
        /// @todo implement check for first bytes of string even without a BOM? (It sure looks harder than for cases WITH a BOM)
919
        if (preg_match('/^(?:\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\x00\x00\xFF\xFE|\xFE\xFF\x00\x00)/', $xmlChunk)) {
920
            return 'UCS-4';
921
        } elseif (preg_match('/^(?:\xFE\xFF|\xFF\xFE)/', $xmlChunk)) {
922
            return 'UTF-16';
923
        } elseif (preg_match('/^(?:\xEF\xBB\xBF)/', $xmlChunk)) {
924
            return 'UTF-8';
925
        }
926
927
        // 3 - test if encoding is specified in the xml declaration
928
        /// @todo this regexp will fail if $xmlChunk uses UTF-32/UCS-4, and most likely UTF-16/UCS-2 as well. In that
929
        ///       case we leave the guesswork up to mbstring - which seems to be able to detect it, starting with php 5.6.
930
        ///       For lower versions, we could attempt usage of mb_ereg...
931
        // Details:
932
        // SPACE:         (#x20 | #x9 | #xD | #xA)+ === [ \x9\xD\xA]+
933
        // EQ:            SPACE?=SPACE? === [ \x9\xD\xA]*=[ \x9\xD\xA]*
934
        // We could be stricter on version number: VersionNum ::= '1.' [0-9]+
935
        if (preg_match('/^<\?xml\s+version\s*=\s*' . "((?:\"[a-zA-Z0-9_.:-]+\")|(?:'[a-zA-Z0-9_.:-]+'))" .
936
            '\s+encoding\s*=\s*' . "((?:\"[A-Za-z][A-Za-z0-9._-]*\")|(?:'[A-Za-z][A-Za-z0-9._-]*'))/",
937
            $xmlChunk, $matches)) {
938
            return strtoupper(substr($matches[2], 1, -1));
939
        }
940
941
        // 4 - if mbstring is available, let it do the guesswork
942
        if (function_exists('mb_detect_encoding')) {
943
            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...
944
                $encodingPrefs = PhpXmlRpc::$xmlrpc_detectencodings;
945
            }
946
            if ($encodingPrefs) {
947
                $enc = mb_detect_encoding($xmlChunk, $encodingPrefs);
948
            } else {
949
                $enc = mb_detect_encoding($xmlChunk);
950
            }
951
            // NB: mb_detect likes to call it ascii, xml parser likes to call it US_ASCII...
952
            // IANA also likes better US-ASCII, so go with it
953
            if ($enc == 'ASCII') {
954
                $enc = 'US-' . $enc;
955
            }
956
957
            return $enc;
958
        } else {
959
            // No encoding specified: assume it is iso-8859-1, as per HTTP1.1?
960
            // Both RFC 2616 (HTTP 1.1) and RFC 1945 (HTTP 1.0) clearly state that for text/xxx content types
961
            // this should be the standard. And we should be getting text/xml as request and response.
962
            // BUT we have to be backward compatible with the lib, which always used UTF-8 as default. Moreover,
963
            // RFC 7231, which obsoletes the two RFC mentioned above, has changed the rules. It says:
964
            // "The default charset of ISO-8859-1 for text media types has been removed; the default is now whatever
965
            // the media type definition says."
966
            return PhpXmlRpc::$xmlrpc_defencoding;
967
        }
968
    }
969
970
    /**
971
     * Helper function: checks if an xml chunk has a charset declaration (BOM or in the xml declaration).
972
     *
973
     * @param string $xmlChunk
974
     * @return bool
975
     *
976
     * @todo rename to hasEncodingDeclaration
977
     */
978
    public static function hasEncoding($xmlChunk)
979
    {
980
        // scan the first bytes of the data for a UTF-16 (or other) BOM pattern
981
        //     (source: http://www.w3.org/TR/2000/REC-xml-20001006)
982
        if (preg_match('/^(?:\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\x00\x00\xFF\xFE|\xFE\xFF\x00\x00)/', $xmlChunk)) {
983
            return true;
984
        } elseif (preg_match('/^(?:\xFE\xFF|\xFF\xFE)/', $xmlChunk)) {
985
            return true;
986
        } elseif (preg_match('/^(?:\xEF\xBB\xBF)/', $xmlChunk)) {
987
            return true;
988
        }
989
990
        // test if encoding is specified in the xml declaration
991
        // Details:
992
        // SPACE:         (#x20 | #x9 | #xD | #xA)+ === [ \x9\xD\xA]+
993
        // EQ:            SPACE?=SPACE? === [ \x9\xD\xA]*=[ \x9\xD\xA]*
994
        // We could be stricter on version number: VersionNum ::= '1.' [0-9]+
995
        if (preg_match('/^<\?xml\s+version\s*=\s*' . "((?:\"[a-zA-Z0-9_.:-]+\")|(?:'[a-zA-Z0-9_.:-]+'))" .
996
            '\s+encoding\s*=\s*' . "((?:\"[A-Za-z][A-Za-z0-9._-]*\")|(?:'[A-Za-z][A-Za-z0-9._-]*'))/",
997
            $xmlChunk)) {
998
            return true;
999
        }
1000
1001
        return false;
1002
    }
1003
1004
    /**
1005
     * @param string $message
1006
     * @param string $method method/file/line info
1007
     * @return bool false if the caller has to stop parsing
1008
     */
1009
    protected function handleParsingError($message, $method = '')
1010
    {
1011
        if ($this->current_parsing_options['xmlrpc_reject_invalid_values']) {
1012
            $this->_xh['isf'] = 2;
1013
            $this->_xh['isf_reason'] = ucfirst($message);
1014
            return false;
1015
        } else {
1016
            $this->getLogger()->error('XML-RPC: ' . ($method != '' ? $method . ': ' : '') . $message);
1017
            return true;
1018
        }
1019
    }
1020
1021
    /**
1022
     * Truncates unsafe data
1023
     * @param string $data
1024
     * @return string
1025
     */
1026
    protected function truncateValueForLog($data)
1027
    {
1028
        if (strlen($data) > $this->maxLogValueLength) {
1029
            return substr($data, 0, $this->maxLogValueLength - 3) . '...';
1030
        }
1031
1032
        return $data;
1033
    }
1034
1035
    // *** BC layer ***
1036
1037
    /**
1038
     * xml parser handler function for opening element tags.
1039
     * Used in decoding xml chunks that might represent single xml-rpc values as well as requests, responses.
1040
     * @deprecated
1041
     *
1042
     * @param resource $parser
1043
     * @param $name
1044
     * @param $attrs
1045
     * @return void
1046
     */
1047
    public function xmlrpc_se_any($parser, $name, $attrs)
1048
    {
1049
        // this will be spamming the log if this method is in use...
1050
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
1051
1052
        $this->xmlrpc_se($parser, $name, $attrs, true);
1053
    }
1054
1055
    public function &__get($name)
1056
    {
1057
        switch ($name) {
1058
            case '_xh':
1059
            case 'xmlrpc_valid_parents':
1060
                $this->logDeprecation('Getting property XMLParser::' . $name . ' is deprecated');
1061
                return $this->$name;
1062
            default:
1063
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
1064
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
1065
                trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
1066
                $result = null;
1067
                return $result;
1068
        }
1069
    }
1070
1071
    public function __set($name, $value)
1072
    {
1073
        switch ($name) {
1074
            // this should only ever be called by subclasses which overtook `parse()`
1075
            case 'accept':
1076
                $this->logDeprecation('Setting property XMLParser::' . $name . ' is deprecated');
1077
                $this->current_parsing_options['accept'] = $value;
1078
                break;
1079
            case '_xh':
1080
            case 'xmlrpc_valid_parents':
1081
                $this->logDeprecation('Setting property XMLParser::' . $name . ' is deprecated');
1082
                $this->$name = $value;
1083
                break;
1084
            default:
1085
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
1086
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
1087
                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
1088
        }
1089
    }
1090
1091
    public function __isset($name)
1092
    {
1093
        switch ($name) {
1094
            case 'accept':
1095
                $this->logDeprecation('Checking property XMLParser::' . $name . ' is deprecated');
1096
                return isset($this->current_parsing_options['accept']);
1097
            case '_xh':
1098
            case 'xmlrpc_valid_parents':
1099
                $this->logDeprecation('Checking property XMLParser::' . $name . ' is deprecated');
1100
                return isset($this->$name);
1101
            default:
1102
                return false;
1103
        }
1104
    }
1105
1106
    public function __unset($name)
1107
    {
1108
        switch ($name) {
1109
            // q: does this make sense at all?
1110
            case 'accept':
1111
                $this->logDeprecation('Unsetting property XMLParser::' . $name . ' is deprecated');
1112
                unset($this->current_parsing_options['accept']);
1113
                break;
1114
            case '_xh':
1115
            case 'xmlrpc_valid_parents':
1116
                $this->logDeprecation('Unsetting property XMLParser::' . $name . ' is deprecated');
1117
                unset($this->$name);
1118
                break;
1119
            default:
1120
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
1121
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
1122
                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
1123
        }
1124
    }
1125
}
1126