Issues (323)

src/Client.php (17 issues)

1
<?php
2
3
namespace PhpXmlRpc;
4
5
use PhpXmlRpc\Exception\ValueErrorException;
6
use PhpXmlRpc\Helper\XMLParser;
7
use PhpXmlRpc\Traits\CharsetEncoderAware;
8
use PhpXmlRpc\Traits\DeprecationLogger;
9
10
/**
11
 * Used to represent a client of an XML-RPC server.
12
 *
13
 * @property int $errno deprecated - public access left in purely for BC.
14
 * @property string $errstr deprecated - public access left in purely for BC.
15
 * @property string $method deprecated - public access left in purely for BC. Access via getUrl()/__construct()
16
 * @property string $server deprecated - public access left in purely for BC. Access via getUrl()/__construct()
17
 * @property int $port deprecated - public access left in purely for BC. Access via getUrl()/__construct()
18
 * @property string $path deprecated - public access left in purely for BC. Access via getUrl()/__construct()
19
 */
20
class Client
21
{
22
    use DeprecationLogger;
23
    //use CharsetEncoderAware;
24
25
    const USE_CURL_NEVER = 0;
26
    const USE_CURL_ALWAYS = 1;
27
    const USE_CURL_AUTO = 2;
28
29
    const OPT_ACCEPTED_CHARSET_ENCODINGS = 'accepted_charset_encodings';
30
    const OPT_ACCEPTED_COMPRESSION = 'accepted_compression';
31
    const OPT_AUTH_TYPE = 'authtype';
32
    const OPT_CA_CERT = 'cacert';
33
    const OPT_CA_CERT_DIR = 'cacertdir';
34
    const OPT_CERT = 'cert';
35
    const OPT_CERT_PASS = 'certpass';
36
    const OPT_COOKIES = 'cookies';
37
    const OPT_DEBUG = 'debug';
38
    const OPT_EXTRA_CURL_OPTS = 'extracurlopts';
39
    const OPT_EXTRA_SOCKET_OPTS = 'extrasockopts';
40
    const OPT_KEEPALIVE = 'keepalive';
41
    const OPT_KEY = 'key';
42
    const OPT_KEY_PASS = 'keypass';
43
    const OPT_NO_MULTICALL = 'no_multicall';
44
    const OPT_PASSWORD = 'password';
45
    const OPT_PROXY = 'proxy';
46
    const OPT_PROXY_AUTH_TYPE = 'proxy_authtype';
47
    const OPT_PROXY_PASS = 'proxy_pass';
48
    const OPT_PROXY_PORT = 'proxyport';
49
    const OPT_PROXY_USER = 'proxy_user';
50
    const OPT_REQUEST_CHARSET_ENCODING = 'request_charset_encoding';
51
    const OPT_REQUEST_COMPRESSION = 'request_compression';
52
    const OPT_RETURN_TYPE = 'return_type';
53
    const OPT_SSL_VERSION = 'sslversion';
54
    const OPT_TIMEOUT = 'timeout';
55
    const OPT_USERNAME = 'username';
56
    const OPT_USER_AGENT = 'user_agent';
57
    const OPT_USE_CURL = 'use_curl';
58
    const OPT_VERIFY_HOST = 'verifyhost';
59
    const OPT_VERIFY_PEER = 'verifypeer';
60
    const OPT_EXTRA_HEADERS = 'extra_headers';
61
62
    /** @var string */
63
    protected static $requestClass = '\\PhpXmlRpc\\Request';
64
    /** @var string */
65
    protected static $responseClass = '\\PhpXmlRpc\\Response';
66
67
    /**
68
     * @var int
69
     * @deprecated will be removed in the future
70
     */
71
    protected $errno;
72
    /**
73
     * @var string
74
     * @deprecated will be removed in the future
75
     */
76
    protected $errstr;
77
78
    /// @todo: do all the ones below need to be public?
79
80
    /**
81
     * @var string
82
     */
83
    protected $method = 'http';
84
    /**
85
     * @var string
86
     */
87
    protected $server;
88
    /**
89
     * @var int
90
     */
91
    protected $port = 0;
92
    /**
93
     * @var string
94
     */
95
    protected $path;
96
97
    /**
98
     * @var int
99
     */
100
    protected $debug = 0;
101
    /**
102
     * @var string
103
     */
104
    protected $username = '';
105
    /**
106
     * @var string
107
     */
108
    protected $password = '';
109
    /**
110
     * @var int
111
     */
112
    protected $authtype = 1;
113
    /**
114
     * @var string
115
     */
116
    protected $cert = '';
117
    /**
118
     * @var string
119
     */
120
    protected $certpass = '';
121
    /**
122 2
     * @var string
123
     */
124 2
    protected $cacert = '';
125 2
    /**
126
     * @var string
127 2
     */
128
    protected $cacertdir = '';
129
    /**
130
     * @var string
131
     */
132
    protected $key = '';
133
    /**
134
     * @var string
135
     */
136
    protected $keypass = '';
137
    /**
138
     * @var bool
139
     */
140
    protected $verifypeer = true;
141
    /**
142
     * @var int
143
     */
144
    protected $verifyhost = 2;
145
    /**
146
     * @var int Corresponds to CURL_SSLVERSION_DEFAULT. Other CURL_SSLVERSION_ values are supported when in curl mode,
147
     *          and in socket mode different values from 0 to 7, matching the corresponding curl value. Old php versions
148
     *          do not support all values, php 5.4 and 5.5 do not support any in fact.
149
     *          NB: please do not use any version lower than TLS 1.3 (value: 7) as they are considered insecure.
150
     */
151
    protected $sslversion = 0;
152 718
    /**
153
     * @var string
154
     */
155 718
    protected $proxy = '';
156 7
    /**
157 7
     * @var int
158 7
     */
159 7
    protected $proxyport = 0;
160
    /**
161
     * @var string
162 7
     */
163
    protected $proxy_user = '';
164
    /**
165 7
     * @var string
166
     */
167
    protected $proxy_pass = '';
168 7
    /**
169 7
     * @var int
170
     */
171 7
    protected $proxy_authtype = 1;
172
    /**
173
     * @var array
174 7
     */
175
    protected $cookies = array();
176
    /**
177
     * @var array
178 718
     */
179
    protected $extrasockopts = array();
180
    /**
181 718
     * @var array
182
     */
183 718
    protected $extracurlopts = array();
184 718
    /**
185 3
     * @var int
186
     */
187 718
    protected $timeout = 0;
188 7
    /**
189
     * @var int
190
     */
191
    protected $use_curl = self::USE_CURL_AUTO;
192 718
    /**
193
     * @var bool
194 711
     *
195
     * This determines whether the multicall() method will try to take advantage of the system.multicall xml-rpc method
196
     * to dispatch to the server an array of requests in a single http roundtrip or simply execute many consecutive http
197 718
     * calls. Defaults to FALSE, but it will be enabled automatically on the first failure of execution of
198
     * system.multicall.
199
     */
200
    protected $no_multicall = false;
201 718
    /**
202
     * @var array
203
     *
204 718
     * List of http compression methods accepted by the client for responses.
205
     * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib.
206
     *
207
     * NNB: you can set it to any non-empty array for HTTP11+ and HTTPS, since in those cases it will be up to CURL to
208
     * decide the compression methods it supports. You might check for the presence of 'zlib' in the output of
209
     * curl_version() to determine whether compression is supported or not
210
     */
211
    protected $accepted_compression = array();
212
    /**
213
     * @var string|null
214
     *
215
     * Name of compression scheme to be used for sending requests.
216
     * Either null, 'gzip' or 'deflate'.
217 718
     */
218 718
    protected $request_compression = '';
219
    /**
220
     * @var bool
221
     *
222
     * Whether to use persistent connections for http 1.1 and https. Value set at constructor time.
223
     */
224
    protected $keepalive = false;
225
    /**
226
     * @var string[]
227
     *
228
     * Charset encodings that can be decoded without problems by the client. Value set at constructor time
229
     */
230
    protected $accepted_charset_encodings = array();
231
    /**
232
     * @var string
233 714
     *
234
     * The charset encoding that will be used for serializing request sent by the client.
235 714
     * It defaults to NULL, which means using US-ASCII and encoding all characters outside the ASCII printable range
236 714
     * using their xml character entity representation (this has the benefit that line end characters will not be mangled
237
     * in the transfer, a CR-LF will be preserved as well as a singe LF).
238
     * Valid values are 'US-ASCII', 'UTF-8' and 'ISO-8859-1'.
239
     * For the fastest mode of operation, set your both your app internal encoding and this to UTF-8.
240
     */
241
    protected $request_charset_encoding = '';
242
    /**
243
     * @var string
244
     *
245
     * Decides the content of Response objects returned by calls to send() and multicall().
246
     * Valid values are 'xmlrpcvals', 'phpvals' or 'xml'.
247
     *
248
     * Determines whether the value returned inside a Response object as results of calls to the send() and multicall()
249
     * methods will be a Value object, a plain php value or a raw xml string.
250
     * Allowed values are 'xmlrpcvals' (the default), 'phpvals' and 'xml'.
251
     * To allow the user to differentiate between a correct and a faulty response, fault responses will be returned as
252 66
     * Response objects in any case.
253
     * Note that the 'phpvals' setting will yield faster execution times, but some of the information from the original
254 66
     * response will be lost. It will be e.g. impossible to tell whether a particular php string value was sent by the
255 66
     * server as an xml-rpc string or base64 value.
256 66
     */
257 66
    protected $return_type = XMLParser::RETURN_XMLRPCVALS;
258
    /**
259
     * @var string
260
     *
261
     * Sent to servers in http headers. Value set at constructor time.
262
     */
263
    protected $user_agent;
264
265
    /**
266
     * Additional headers to be included in the requests.
267
     *
268
     * @var string[]
269
     */
270
    protected $extra_headers = array();
271
272
    /**
273
     * CURL handle: used for keep-alive
274
     * @internal
275
     */
276
    public $xmlrpc_curl_handle = null;
277
278
    /**
279
     * @var array
280
     */
281
    protected static $options = array(
282
        self::OPT_ACCEPTED_CHARSET_ENCODINGS,
283
        self::OPT_ACCEPTED_COMPRESSION,
284
        self::OPT_AUTH_TYPE,
285
        self::OPT_CA_CERT,
286
        self::OPT_CA_CERT_DIR,
287
        self::OPT_CERT,
288
        self::OPT_CERT_PASS,
289
        self::OPT_COOKIES,
290
        self::OPT_DEBUG,
291
        self::OPT_EXTRA_CURL_OPTS,
292
        self::OPT_EXTRA_SOCKET_OPTS,
293
        self::OPT_KEEPALIVE,
294
        self::OPT_KEY,
295
        self::OPT_KEY_PASS,
296
        self::OPT_NO_MULTICALL,
297
        self::OPT_PASSWORD,
298
        self::OPT_PROXY,
299
        self::OPT_PROXY_AUTH_TYPE,
300
        self::OPT_PROXY_PASS,
301
        self::OPT_PROXY_USER,
302
        self::OPT_PROXY_PORT,
303
        self::OPT_REQUEST_CHARSET_ENCODING,
304
        self::OPT_REQUEST_COMPRESSION,
305
        self::OPT_RETURN_TYPE,
306
        self::OPT_SSL_VERSION,
307
        self::OPT_TIMEOUT,
308
        self::OPT_USE_CURL,
309
        self::OPT_USER_AGENT,
310
        self::OPT_USERNAME,
311
        self::OPT_VERIFY_HOST,
312
        self::OPT_VERIFY_PEER,
313
        self::OPT_EXTRA_HEADERS,
314
    );
315
316 132
    /**
317
     * @param string $path either the PATH part of the xml-rpc server URL, or complete server URL (in which case you
318 132
     *                     should use an empty string for all other parameters)
319 132
     *                     e.g. /xmlrpc/server.php
320
     *                     e.g. http://phpxmlrpc.sourceforge.net/server.php
321
     *                     e.g. https://james:[email protected]:444/xmlrpcserver?agent=007
322
     *                     e.g. h2://fast-and-secure-services.org/endpoint
323
     * @param string $server the server name / ip address
324
     * @param integer $port the port the server is listening on, when omitted defaults to 80 or 443 depending on
325
     *                      protocol used
326
     * @param string $method the http protocol variant: defaults to 'http'; 'https', 'http11', 'h2' and 'h2c' can
327
     *                       be used if CURL is installed. The value set here can be overridden in any call to $this->send().
328 132
     *                       Use 'h2' to make the lib attempt to use http/2 over a secure connection, and 'h2c'
329
     *                       for http/2 without tls. Note that 'h2c' will not use the h2c 'upgrade' method, and be
330 132
     *                       thus incompatible with any server/proxy not supporting http/2. This is because POST
331 132
     *                       request are not compatible with h2c upgrade.
332
     */
333
    public function __construct($path, $server = '', $port = '', $method = '')
334
    {
335
        // allow user to specify all params in $path
336
        if ($server == '' && $port == '' && $method == '') {
337
            $parts = parse_url($path);
338 132
            $server = $parts['host'];
339
            $path = isset($parts['path']) ? $parts['path'] : '';
340 132
            if (isset($parts['query'])) {
341 132
                $path .= '?' . $parts['query'];
342
            }
343
            if (isset($parts['fragment'])) {
344
                $path .= '#' . $parts['fragment'];
345
            }
346
            if (isset($parts['port'])) {
347
                $port = $parts['port'];
348
            }
349
            if (isset($parts['scheme'])) {
350
                $method = $parts['scheme'];
351
            }
352
            if (isset($parts['user'])) {
353
                $this->username = $parts['user'];
354
            }
355 99
            if (isset($parts['pass'])) {
356
                $this->password = $parts['pass'];
357 99
            }
358 99
        }
359 99
        if ($path == '' || $path[0] != '/') {
360 99
            $this->path = '/' . $path;
361 99
        } else {
362 99
            $this->path = $path;
363
        }
364
        $this->server = $server;
365
        if ($port != '') {
366
            $this->port = $port;
367
        }
368
        if ($method != '') {
369
            $this->method = $method;
370
        }
371
372
        // if ZLIB is enabled, let the client by default accept compressed responses
373
        if (function_exists('gzinflate') || (
374
                function_exists('curl_version') && (($info = curl_version()) &&
375
                    ((is_string($info) && strpos($info, 'zlib') !== null) || isset($info['libz_version'])))
376
            )
377
        ) {
378
            $this->accepted_compression = array('gzip', 'deflate');
379
        }
380
381
        // keepalives: enabled by default
382
        $this->keepalive = true;
383
384
        // by default the xml parser can support these 3 charset encodings
385
        $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII');
386
387
        // NB: this is disabled to avoid making all the requests sent huge... mbstring supports more than 80 charsets!
388
        //$ch = $this->getCharsetEncoder();
389
        //$this->accepted_charset_encodings = $ch->knownCharsets();
390
391
        // initialize user_agent string
392
        $this->user_agent = PhpXmlRpc::$xmlrpcName . ' ' . PhpXmlRpc::$xmlrpcVersion;
393
    }
394
395
    /**
396
     * @param string $name see all the OPT_ constants
397
     * @param mixed $value
398
     * @return $this
399
     * @throws ValueErrorException on unsupported option
400
     */
401
    public function setOption($name, $value)
402
    {
403
        if (in_array($name, static::$options)) {
404
            $this->$name = $value;
405
            return $this;
406
        }
407
408
        throw new ValueErrorException("Unsupported option '$name'");
409
    }
410
411
    /**
412
     * @param string $name see all the OPT_ constants
413
     * @return mixed
414
     * @throws ValueErrorException on unsupported option
415
     */
416
    public function getOption($name)
417
    {
418 580
        if (in_array($name, static::$options)) {
419
            return $this->$name;
420 580
        }
421 580
422
        throw new ValueErrorException("Unsupported option '$name'");
423
    }
424
425
    /**
426
     * Returns the complete list of Client options, with their value.
427 580
     * @return array
428
     */
429 580
    public function getOptions()
430
    {
431
        $values = array();
432
        foreach (static::$options as $opt) {
433
            $values[$opt] = $this->getOption($opt);
434
        }
435
        return $values;
436
    }
437
438
    /**
439
     * @param array $options key: any valid option (see all the OPT_ constants)
440
     * @return $this
441
     * @throws ValueErrorException on unsupported option
442
     */
443
    public function setOptions($options)
444
    {
445
        foreach ($options as $name => $value) {
446 66
            $this->setOption($name, $value);
447
        }
448 66
449 66
        return $this;
450
    }
451
452
    /**
453
     * Enable/disable the echoing to screen of the xml-rpc responses received. The default is not to output anything.
454
     *
455
     * The debugging information at level 1 includes the raw data returned from the XML-RPC server it was querying
456
     * (including bot HTTP headers and the full XML payload), and the PHP value the client attempts to create to
457
     * represent the value returned by the server.
458
     * At level 2, the complete payload of the xml-rpc request is also printed, before being sent to the server.
459
     * At level -1, the Response objects returned by send() calls will not carry information about the http response's
460
     * cookies, headers and body, which might save some memory
461
     *
462
     * This option can be very useful when debugging servers as it allows you to see exactly what the client sends and
463
     * the server returns. Never leave it enabled for production!
464
     *
465
     * @param integer $level values -1, 0, 1 and 2 are supported
466
     * @return $this
467
     */
468
    public function setDebug($level)
469
    {
470
        $this->debug = $level;
471
        return $this;
472
    }
473
474
    /**
475
     * Sets the username and password for authorizing the client to the server.
476
     *
477
     * With the default (HTTP) transport, this information is used for HTTP Basic authorization.
478
     * Note that username and password can also be set using the class constructor.
479
     * With HTTP 1.1 and HTTPS transport, NTLM and Digest authentication protocols are also supported. To enable them use
480
     * the constants CURLAUTH_DIGEST and CURLAUTH_NTLM as values for the auth type parameter.
481
     *
482
     * @param string $user username
483
     * @param string $password password
484
     * @param integer $authType auth type. See curl_setopt man page for supported auth types. Defaults to CURLAUTH_BASIC
485
     *                          (basic auth). Note that auth types NTLM and Digest will only work if the Curl php
486
     *                          extension is enabled.
487
     * @return $this
488
     */
489
    public function setCredentials($user, $password, $authType = 1)
490
    {
491
        $this->username = $user;
492
        $this->password = $password;
493
        $this->authtype = $authType;
494
        return $this;
495 697
    }
496
497
    /**
498
     * Set the optional certificate and passphrase used in SSL-enabled communication with a remote server.
499 697
     *
500 117
     * Note: to retrieve information about the client certificate on the server side, you will need to look into the
501
     * environment variables which are set up by the webserver. Different webservers will typically set up different
502
     * variables.
503 697
     *
504
     * @param string $cert the name of a file containing a PEM formatted certificate
505 66
     * @param string $certPass the password required to use it
506
     * @return $this
507 66
     */
508 697
    public function setCertificate($cert, $certPass = '')
509 28
    {
510 28
        $this->cert = $cert;
511 28
        $this->certpass = $certPass;
512
        return $this;
513
    }
514
515 697
    /**
516
     * Add a CA certificate to verify server with in SSL-enabled communication when SetSSLVerifypeer has been set to TRUE.
517
     *
518
     * See the php manual page about CURLOPT_CAINFO for more details.
519 697
     *
520 697
     * @param string $caCert certificate file name (or dir holding certificates)
521
     * @param bool $isDir set to true to indicate cacert is a dir. defaults to false
522 697
     * @return $this
523 322
     */
524 322
    public function setCaCertificate($caCert, $isDir = false)
525 322
    {
526 322
        if ($isDir) {
527
            $this->cacertdir = $caCert;
528 322
        } else {
529 322
            $this->cacert = $caCert;
530 322
        }
531 322
        return $this;
532 322
    }
533 322
534 322
    /**
535 322
     * Set attributes for SSL communication: private SSL key.
536 322
     *
537 322
     * NB: does not work in older php/curl installs.
538 322
     * Thanks to Daniel Convissor.
539 322
     *
540
     * @param string $key The name of a file containing a private SSL key
541 322
     * @param string $keyPass The secret password needed to use the private SSL key
542 322
     * @return $this
543 322
     */
544 322
    public function setKey($key, $keyPass)
545 322
    {
546
        $this->key = $key;
547
        $this->keypass = $keyPass;
548
        return $this;
549 375
    }
550 375
551 375
    /**
552 375
     * Set attributes for SSL communication: verify the remote host's SSL certificate, and cause the connection to fail
553
     * if the cert verification fails.
554 375
     *
555 375
     * By default, verification is enabled.
556 375
     * To specify custom SSL certificates to validate the server with, use the setCaCertificate method.
557 375
     *
558 375
     * @param bool $i enable/disable verification of peer certificate
559 375
     * @return $this
560 375
     * @deprecated use setOption
561 375
     */
562 375
    public function setSSLVerifyPeer($i)
563 375
    {
564 375
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
565 375
566
        $this->verifypeer = $i;
567 375
        return $this;
568 375
    }
569 375
570
    /**
571
     * Set attributes for SSL communication: verify the remote host's SSL certificate's common name (CN).
572
     *
573 697
     * Note that support for value 1 has been removed in cURL 7.28.1
574
     *
575
     * @param int $i Set to 1 to only the existence of a CN, not that it matches
576
     * @return $this
577
     * @deprecated use setOption
578
     */
579
    public function setSSLVerifyHost($i)
580
    {
581
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
582
583
        $this->verifyhost = $i;
584
        return $this;
585
    }
586
587
    /**
588
     * Set attributes for SSL communication: SSL version to use. Best left at 0 (default value): let PHP decide.
589
     *
590
     * @param int $i use CURL_SSLVERSION_ constants. When in socket mode, use the same values: 2 (SSLv2) to 7 (TLSv1.3),
591
     *               0 for auto (note that old php versions do not support all TLS versions).
592
     *               Note that, in curl mode, the actual ssl version in use might be higher than requested.
593
     *               NB: please do not use any version lower than TLS 1.3 as they are considered insecure.
594
     * @return $this
595
     * @deprecated use setOption
596
     */
597
    public function setSSLVersion($i)
598
    {
599
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
600
601
        $this->sslversion = $i;
602
        return $this;
603
    }
604
605
    /**
606
     * Set proxy info.
607
     *
608
     * NB: CURL versions before 7.11.10 cannot use a proxy to communicate with https servers.
609
     *
610
     * @param string $proxyHost
611
     * @param string $proxyPort Defaults to 8080 for HTTP and 443 for HTTPS
612
     * @param string $proxyUsername Leave blank if proxy has public access
613
     * @param string $proxyPassword Leave blank if proxy has public access
614
     * @param int $proxyAuthType defaults to CURLAUTH_BASIC (Basic authentication protocol); set to constant CURLAUTH_NTLM
615
     *                           to use NTLM auth with proxy (has effect only when the client uses the HTTP 1.1 protocol)
616
     * @return $this
617
     */
618
    public function setProxy($proxyHost, $proxyPort, $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1)
619
    {
620
        $this->proxy = $proxyHost;
621
        $this->proxyport = $proxyPort;
0 ignored issues
show
Documentation Bug introduced by
The property $proxyport was declared of type integer, but $proxyPort is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
622
        $this->proxy_user = $proxyUsername;
623
        $this->proxy_pass = $proxyPassword;
624
        $this->proxy_authtype = $proxyAuthType;
625
        return $this;
626
    }
627
628
    /**
629
     * Enables/disables reception of compressed xml-rpc responses.
630
     *
631
     * This requires the "zlib" extension to be enabled in your php install. If it is, by default xmlrpc_client
632
     * instances will enable reception of compressed content.
633
     * Note that enabling reception of compressed responses merely adds some standard http headers to xml-rpc requests.
634
     * It is up to the xml-rpc server to return compressed responses when receiving such requests.
635
     *
636
     * @param string $compMethod either 'gzip', 'deflate', 'any' or ''
637
     * @return $this
638
     */
639
    public function setAcceptedCompression($compMethod)
640
    {
641
        if ($compMethod == 'any') {
642
            $this->accepted_compression = array('gzip', 'deflate');
643
        } elseif ($compMethod == false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $compMethod of type string to the boolean false. If you are specifically checking for an empty string, consider using the more explicit === '' instead.
Loading history...
644
            $this->accepted_compression = array();
645
        } else {
646
            $this->accepted_compression = array($compMethod);
647
        }
648
        return $this;
649
    }
650
651
    /**
652
     * Enables/disables http compression of xml-rpc request.
653
     *
654
     * This requires the "zlib" extension to be enabled in your php install.
655
     * Take care when sending compressed requests: servers might not support them (and automatic fallback to
656
     * uncompressed requests is not yet implemented).
657
     *
658
     * @param string $compMethod either 'gzip', 'deflate' or ''
659
     * @return $this
660
     * @deprecated use setOption
661
     */
662
    public function setRequestCompression($compMethod)
663
    {
664 375
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
665
666
        $this->request_compression = $compMethod;
667
        return $this;
668
    }
669
670
    /**
671 375
     * Adds a cookie to list of cookies that will be sent to server with every further request (useful e.g. for keeping
672 373
     * session info outside the xml-rpc payload).
673
     *
674
     * NB: by default all cookies set via this method are sent to the server, regardless of path/domain/port. Taking
675
     * advantage of those values is left to the single developer.
676 375
     *
677 346
     * @param string $name nb: will not be escaped in the request's http headers. Take care not to use CTL chars or
678
     *                     separators!
679
     * @param string $value
680 375
     * @param string $path
681
     * @param string $domain
682 375
     * @param int $port do not use! Cookies are not separated by port
683 375
     * @return $this
684 64
     *
685 32
     * @todo check correctness of urlencoding cookie value (copied from php way of doing it, but php is generally sending
686 32
     *       response not requests. We do the opposite...)
687 32
     * @todo strip invalid chars from cookie name? As per RFC 6265, we should follow RFC 2616, Section 2.2
688 32
     * @todo drop/rename $port parameter. Cookies are not isolated by port!
689
     * @todo feature-creep allow storing 'expires', 'secure', 'httponly' and 'samesite' cookie attributes (we could do
690
     *       as php, and allow $path to be an array of attributes...)
691 32
     */
692 32
    public function setCookie($name, $value = '', $path = '', $domain = '', $port = null)
693 32
    {
694 32
        $this->cookies[$name]['value'] = rawurlencode($value);
695
        if ($path || $domain || $port) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $port of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
696
            $this->cookies[$name]['path'] = $path;
697
            $this->cookies[$name]['domain'] = $domain;
698
            $this->cookies[$name]['port'] = $port;
699
        }
700 375
        return $this;
701 375
    }
702 32
703 32
    /**
704
     * Directly set cURL options, for extra flexibility (when in cURL mode).
705
     *
706
     * It allows e.g. to bind client to a specific IP interface / address.
707
     *
708 375
     * @param array $options
709 375
     * @return $this
710 73
     * @deprecated use setOption
711
     */
712
    public function setCurlOptions($options)
713 375
    {
714 375
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
715 32
716
        $this->extracurlopts = $options;
717
        return $this;
718 32
    }
719 32
720 32
    /**
721 32
     * @param int $useCurlMode self::USE_CURL_ALWAYS, self::USE_CURL_AUTO or self::USE_CURL_NEVER
722 32
     *                         In 'auto' mode, curl is picked up based on features used, such as fe. NTLM auth, or https
723
     * @return $this
724
     * @deprecated use setOption
725
     */
726 32
    public function setUseCurl($useCurlMode)
727
    {
728
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
729 343
730 343
        $this->use_curl = $useCurlMode;
731 343
        return $this;
732 343
    }
733
734
    /**
735
     * Set user-agent string that will be used by this client instance in http headers sent to the server.
736 375
     *
737 375
     * The default user agent string includes the name of this library and the version number.
738 309
     *
739 309
     * @param string $agentString
740 309
     * @return $this
741
     * @deprecated use setOption
742
     */
743
    public function setUserAgent($agentString)
744
    {
745
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
746
747
        $this->user_agent = $agentString;
748
        return $this;
749
    }
750
751
    /**
752
     * @param null|int $component allowed values: PHP_URL_SCHEME, PHP_URL_HOST, PHP_URL_PORT, PHP_URL_PATH
753 309
     * @return string|int Notes: the path component will include query string and fragment; NULL is a valid value for port
754
     *                    (in which case the default port for http/https will be used); the url scheme component will
755
     *                    reflect the `$method` used in the constructor, so it might not be http or https
756 309
     * @throws ValueErrorException on unsupported component
757
     */
758
    public function getUrl($component = null)
759
    {
760 375
        if (is_int($component) || ($component !== null && ctype_digit($component))) {
761 375
            switch ($component) {
762
                case PHP_URL_SCHEME:
763
                    return $this->method;
764
                case PHP_URL_HOST:
765
                    return $this->server;
766 375
                case PHP_URL_PORT:
767 375
                    return $this->port;
768 375
                case  PHP_URL_PATH:
769 375
                    return $this->path;
770 375
                case '':
771 375
772 375
                default:
773 375
                    throw new ValueErrorException("Unsupported component '$component'");
774 375
            }
775 375
        }
776 375
777 375
        $url = $this->method . '://' . $this->server;
778
        if ($this->port == 0 || ($this->port == 80 && in_array($this->method, array('http', 'http10', 'http11',  'http11_only', 'h2c'))) ||
779 375
            ($this->port == 443 && in_array($this->method, array('https', 'h2')))) {
780 2
            return $url . $this->path;
781
        } else {
782
            return $url . ':' . $this->port . $this->path;
783 375
        }
784 375
    }
785 32
786
    /**
787
     * Send an xml-rpc request to the server.
788
     *
789
     * @param Request|Request[]|string $req The Request object, or an array of requests for using multicall, or the
790
     *                                      complete xml representation of a request.
791 32
     *                                      When sending an array of Request objects, the client will try to make use of
792
     *                                      a single 'system.multicall' xml-rpc method call to forward to the server all
793
     *                                      the requests in a single HTTP round trip, unless $this->no_multicall has
794 32
     *                                      been previously set to TRUE (see the multicall method below), in which case
795
     *                                      many consecutive xml-rpc requests will be sent. The method will return an
796
     *                                      array of Response objects in both cases.
797 32
     *                                      The third variant allows to build by hand (or any other means) a complete
798
     *                                      xml-rpc request message, and send it to the server. $req should be a string
799
     *                                      containing the complete xml representation of the request. It is e.g. useful
800 32
     *                                      when, for maximal speed of execution, the request is serialized into a
801 32
     *                                      string using the native php xml-rpc functions (see http://www.php.net/xmlrpc)
802
     * @param integer $timeout deprecated. Connection timeout, in seconds, If unspecified, the timeout set with setOption
803
     *                         will be used. If that is 0, a platform specific timeout will apply.
804 375
     *                         This timeout value is passed to fsockopen(). It is also used for detecting server
805
     *                         timeouts during communication (i.e. if the server does not send anything to the client
806 375
     *                         for $timeout seconds, the connection will be closed). When in CURL mode, this is the
807 64
     *                         CURL timeout.
808
     *                         NB: in both CURL and Socket modes, some conditions might lead to the client not
809 311
     *                        respecting the given timeout. Eg. if the network is not connected
810
     * @param string $method deprecated. Use the same value in the constructor instead.
811
     *                       Valid values are 'http', 'http11', 'https', 'h2' and 'h2c'. If left empty,
812 375
     *                       the http protocol chosen during creation of the object will be used.
813 375
     *                       Use 'h2' to make the lib attempt to use http/2 over a secure connection, and 'h2c'
814
     *                       for http/2 without tls. Note that 'h2c' will not use the h2c 'upgrade' method, and be
815 375
     *                       thus incompatible with any server/proxy not supporting http/2. This is because POST
816 375
     *                       request are not compatible with h2c upgrade.
817 375
     *                       You can also use 'http11_only' to force usage of curl with http 1.1 (no http2)
818 374
     * @return Response|Response[] Note that the client will always return a Response object, even if the call fails
819 367
     *
820
     * @todo allow throwing exceptions instead of returning responses in case of failed calls and/or Fault responses
821
     * @todo refactor: we now support many options besides connection timeout and http version to use. Why only privilege those?
822 1
     */
823
    public function send($req, $timeout = 0, $method = '')
824
    {
825
        if ($method !== '' || $timeout !== 0) {
826
            $this->logDeprecation("Using non-default values for arguments 'method' and 'timeout' when calling method " . __METHOD__ . ' is deprecated');
827 1
        }
828 1
829
        // if user does not specify http protocol, use native method of this client
830 1
        // (i.e. method set during call to constructor)
831
        if ($method == '') {
832
            $method = $this->method;
833 374
        }
834
835
        if ($timeout == 0) {
836
            $timeout = $this->timeout;
837
        }
838
839
        if (is_array($req)) {
840
            // $req is an array of Requests
841
            /// @todo switch to the new syntax for multicall
842
            return $this->multicall($req, $timeout, $method);
843 374
        } elseif (is_string($req)) {
844
            $n = new static::$requestClass('');
845
            /// @todo we should somehow allow the caller to declare a custom contenttype too, esp. for the charset declaration
846
            $n->setPayload($req);
847 374
            $req = $n;
848 374
        }
849 374
850
        // where req is a Request
851 374
        $req->setDebug($this->debug);
852
853 374
        /// @todo move to its own function, to make it easier to change in subclasses
854
        /// @todo we could be smarter about this:
855
        ///       - not force usage of curl if it is not present
856
        ///       - not force usage of curl for https (minor BC)
857
        ///       - use the presence of curl_extra_opts or socket_extra_opts as a hint
858
        $useCurl = ($this->use_curl == self::USE_CURL_ALWAYS) || ($this->use_curl == self::USE_CURL_AUTO && (
859
            in_array($method, array('https', 'http11', 'http11_only', 'h2c', 'h2')) ||
860
            ($this->username != '' && $this->authtype != 1) ||
861
            ($this->proxy != '' && $this->proxy_user != '' && $this->proxy_authtype != 1)
862
            // uncomment the following if not forcing curl always for 'https'
863
            //|| ($this->sslversion == 7 && PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION == '7.3')
864
            //|| ($this->sslversion != 0 && PHP_MAJOR_VERSION < 6)
865
        ));
866
867
        // BC - we go through sendPayloadCURL/sendPayloadSocket in case some subclass reimplemented those
868
        if ($useCurl) {
869
            $r = $this->sendPayloadCURL(
0 ignored issues
show
Deprecated Code introduced by
The function PhpXmlRpc\Client::sendPayloadCURL() has been deprecated. ( Ignorable by Annotation )

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

869
            $r = /** @scrutinizer ignore-deprecated */ $this->sendPayloadCURL(
Loading history...
870
                $req,
871
                $this->server,
872
                $this->port,
873
                $timeout,
874
                $this->username,
875
                $this->password,
876
                $this->authtype,
877
                $this->cert,
878
                $this->certpass,
879
                $this->cacert,
880
                $this->cacertdir,
881
                $this->proxy,
882
                $this->proxyport,
883
                $this->proxy_user,
884
                $this->proxy_pass,
885
                $this->proxy_authtype,
886 322
                // BC - http11 was used to force enabling curl
887
                $method == 'http11' ? 'http' : ($method == 'http11_only' ? 'http11' : $method),
888
                $this->keepalive,
889
                $this->key,
890
                $this->keypass,
891 322
                $this->sslversion
892
            );
893
        } else {
894
            $r = $this->sendPayloadSocket(
0 ignored issues
show
Deprecated Code introduced by
The function PhpXmlRpc\Client::sendPayloadSocket() has been deprecated. ( Ignorable by Annotation )

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

894
            $r = /** @scrutinizer ignore-deprecated */ $this->sendPayloadSocket(
Loading history...
895 322
                $req,
896
                $this->server,
897 96
                $this->port,
898 96
                $timeout,
899
                $this->username,
900
                $this->password,
901
                $this->authtype,
902
                $this->cert,
903
                $this->certpass,
904 322
                $this->cacert,
905 322
                $this->cacertdir,
906
                $this->proxy,
907
                $this->proxyport,
908
                $this->proxy_user,
909
                $this->proxy_pass,
910 322
                $this->proxy_authtype,
911
                $method == 'http11_only' ? 'http11' : $method,
912
                $this->key,
913
                $this->keypass,
914
                $this->sslversion
915 322
            );
916
        }
917 322
918
        return $r;
919
    }
920
921
    /**
922
     * @param Request $req
923
     * @param string $method
924
     * @param string $server
925
     * @param int $port
926
     * @param string $path
927
     * @param array $opts
928
     * @return Response
929 322
     */
930
    protected function sendViaSocket($req, $method, $server, $port, $path, $opts)
931
    {
932 1
        /// @todo log a warning if passed an unsupported method
933 1
934 1
        // Only create the payload if it was not created previously
935 1
        /// @todo what if the request's payload was created with a different encoding?
936 1
        ///       Also, if we do not call serialize(), the request will not set its content-type to have the charset declared
937
        $payload = $req->getPayload();
938
        if (empty($payload)) {
939 322
            $payload = $req->serialize($opts['request_charset_encoding']);
940 160
        }
941
942 322
        // Deflate request body and set appropriate request headers
943
        $encodingHdr = '';
944 322
        if ($opts['request_compression'] == 'gzip' || $opts['request_compression'] == 'deflate') {
945
            if ($opts['request_compression'] == 'gzip' && function_exists('gzencode')) {
946
                $a = @gzencode($payload);
947
                if ($a) {
948
                    $payload = $a;
949
                    $encodingHdr = "Content-Encoding: gzip\r\n";
950 322
                } else {
951
                    $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': gzencode failure in compressing request');
952
                }
953 323
            } else if (function_exists('gzcompress')) {
954
                $a = @gzcompress($payload);
955
                if ($a) {
956
                    $payload = $a;
957
                    $encodingHdr = "Content-Encoding: deflate\r\n";
958 323
                } else {
959 322
                    $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': gzcompress failure in compressing request');
960 226
                }
961
            } else {
962 96
                $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': desired request compression method is unsupported by this PHP install');
963
            }
964
        } else {
965
            if ($opts['request_compression'] != '') {
966
                $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': desired request compression method is unsupported');
967 323
            }
968 302
        }
969
970
        // thanks to Grant Rauscher
971
        $credentials = '';
972 323
        if ($opts['username'] != '') {
973 323
            if ($opts['authtype'] != 1) {
974 64
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth is supported with HTTP 1.0');
975 32
                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unsupported_option'],
976 32
                    PhpXmlRpc::$xmlrpcerr['unsupported_option'] . ': only Basic auth is supported with HTTP 1.0');
977 32
            }
978 32
            $credentials = 'Authorization: Basic ' . base64_encode($opts['username'] . ':' . $opts['password']) . "\r\n";
979
        }
980
981 32
        $acceptedEncoding = '';
982 32
        if (is_array($opts['accepted_compression']) && count($opts['accepted_compression'])) {
983 32
            $acceptedEncoding = 'Accept-Encoding: ' . implode(', ', $opts['accepted_compression']) . "\r\n";
984 64
        }
985
986
        if ($port == 0) {
987
            $port = ($method === 'https') ? 443 : 80;
988 259
        }
989
990
        $proxyCredentials = '';
991 323
        if ($opts['proxy']) {
992 323
            if ($opts['proxyport'] == 0) {
993 64
                $opts['proxyport'] = 8080;
994
            }
995 259
            $connectServer = $opts['proxy'];
996 32
            $connectPort = $opts['proxyport'];
997
            $transport = 'tcp';
998
            $protocol = $method;
999 227
            if ($method === 'http10' || $method === 'http11') {
1000
                $protocol = 'http';
1001
            } elseif ($method === 'h2') {
1002 323
                $protocol = 'https';
1003 323
            } else if (strpos($protocol, ':') !== false) {
1004 322
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ": warning - attempted hacking attempt?. The protocol requested for the call is: '$protocol'");
1005
                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unsupported_option'], PhpXmlRpc::$xmlrpcerr['unsupported_option'] .
1006
                    " attempted hacking attempt?. The protocol requested for the call is: '$protocol'");
1007 56
            }
1008
            /// @todo this does not work atm (tested at least with an http proxy forwarding to an https server) - we
1009
            ///       should implement the CONNECT protocol
1010
            $uri = $protocol . '://' . $server . ':' . $port . $path;
1011 323
            if ($opts['proxy_user'] != '') {
1012
                if ($opts['proxy_authtype'] != 1) {
1013 323
                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth to proxy is supported with HTTP 1.0');
1014
                    return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unsupported_option'],
1015
                        PhpXmlRpc::$xmlrpcerr['unsupported_option'] . ': only Basic auth to proxy is supported with socket transport');
1016
                }
1017 323
                $proxyCredentials = 'Proxy-Authorization: Basic ' . base64_encode($opts['proxy_user'] . ':' .
1018
                    $opts['proxy_pass']) . "\r\n";
1019 323
            }
1020
        } else {
1021 323
            $connectServer = $server;
1022
            $connectPort = $port;
1023
            /// @todo should we add support for 'h2' method? If so, is it 'tls' or 'tcp' ?
1024 323
            $transport = ($method === 'https') ? 'tls' : 'tcp';
1025
            $uri = $path;
1026
        }
1027
1028 323
        // Cookie generation, as per RFC 6265
1029
        // NB: the following code does not honour 'expires', 'path' and 'domain' cookie attributes set to client obj by the user...
1030
        $cookieHeader = '';
1031 66
        if (count($opts['cookies'])) {
1032 64
            $version = '';
1033
            foreach ($opts['cookies'] as $name => $cookie) {
1034 2
                /// @todo should we sanitize the cookie value on behalf of the user? See setCookie comments
1035
                $cookieHeader .= ' ' . $name . '=' . $cookie['value'] . ";";
1036
            }
1037
            $cookieHeader = 'Cookie:' . $version . substr($cookieHeader, 0, -1) . "\r\n";
1038 323
        }
1039
1040 323
        $extraHeaders = '';
1041 161
        if (is_array($this->extra_headers) && $this->extra_headers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extra_headers of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1042
            $extraHeaders = implode("\r\n", $this->extra_headers) . "\r\n";
1043
        }
1044 323
1045 64
        // omit port if default
1046
        /// @todo add handling of http2, h2c in case they start being supported by fosckopen
1047
        if (($port == 80 && in_array($method, array('http', 'http10', 'http11'))) || ($port == 443 && $method == 'https')) {
1048
            $port = '';
1049
        } else {
1050 323
            $port = ':' . $port;
1051
        }
1052 323
1053
        $op = 'POST ' . $uri . " HTTP/1.0\r\n" .
1054 323
            'User-Agent: ' . $opts['user_agent'] . "\r\n" .
1055 272
            'Host: ' . $server . $port . "\r\n" .
1056
            $credentials .
1057
            $proxyCredentials .
1058 323
            $acceptedEncoding .
1059 323
            $encodingHdr .
1060 32
            'Accept-Charset: ' . implode(',', $opts['accepted_charset_encodings']) . "\r\n" .
1061 32
            $cookieHeader .
1062 291
            'Content-Type: ' . $req->getContentType() . "\r\n" .
1063
            $extraHeaders .
1064
            'Content-Length: ' . strlen($payload) . "\r\n\r\n" .
1065 291
            $payload;
1066 32
1067 32
        if ($opts['debug'] > 1) {
1068 259
            $this->getLogger()->debug("---SENDING---\n$op\n---END---");
1069 32
        }
1070 32
1071
        $contextOptions = array();
1072
        if ($method == 'https') {
1073 323
            if ($opts['cert'] != '') {
1074 32
                $contextOptions['ssl']['local_cert'] = $opts['cert'];
1075 32
                if ($opts['certpass'] != '') {
1076 32
                    $contextOptions['ssl']['passphrase'] = $opts['certpass'];
1077
                }
1078
            }
1079
            if ($opts['cacert'] != '') {
1080
                $contextOptions['ssl']['cafile'] = $opts['cacert'];
1081
            }
1082 323
            if ($opts['cacertdir'] != '') {
1083
                $contextOptions['ssl']['capath'] = $opts['cacertdir'];
1084 96
            }
1085
            if ($opts['key'] != '') {
1086
                $contextOptions['ssl']['local_pk'] = $opts['key'];
1087
            }
1088 96
            $contextOptions['ssl']['verify_peer'] = $opts['verifypeer'];
1089
            $contextOptions['ssl']['verify_peer_name'] = $opts['verifypeer'];
1090
1091
            if ($opts['sslversion'] != 0) {
1092 96
                /// @see https://www.php.net/manual/en/curl.constants.php,
1093
                ///      https://www.php.net/manual/en/function.stream-socket-enable-crypto.php
1094 96
                ///      https://www.php.net/manual/en/migration56.openssl.php,
1095
                ///      https://wiki.php.net/rfc/improved-tls-constants
1096
                switch($opts['sslversion']) {
1097 96
                    case 1: // TLSv1x
1098
                        if (version_compare(PHP_VERSION, '7.2.0', '>=')) {
1099
                            $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLS_CLIENT;
1100
                        } else {
1101 96
                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unsupported_option'],
1102
                                PhpXmlRpc::$xmlrpcerr['unsupported_option'] . ': TLS-any only is supported with PHP 7.2 or later');
1103
                        }
1104
                        break;
1105 96
                    case 2: // SSLv2
1106
                        $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_SSLv2_CLIENT;
1107
                        break;
1108
                    case 3: // SSLv3
1109
                        $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_SSLv3_CLIENT;
1110 96
                        break;
1111
                    case 4: // TLSv1.0 - not always available?
1112 96
                        $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT;
1113
                        break;
1114
                    case 5: // TLSv1.1 - not always available?
1115
                        $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
1116 323
                        break;
1117 64
                    case 6: // TLSv1.2 - not always available?
1118
                        $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
1119
                        break;
1120 64
                    case 7: // TLSv1.3 - not always available
1121 64
                        if (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT')) {
1122
                            $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT;
1123
                        } else {
1124
                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unsupported_option'],
1125
                                PhpXmlRpc::$xmlrpcerr['unsupported_option'] . ': TLS-1.3 only is supported with PHP 7.4 or later');
1126
                        }
1127
                        break;
1128
                    default:
1129
                        return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unsupported_option'],
1130
                            PhpXmlRpc::$xmlrpcerr['unsupported_option'] . ': Unsupported required TLS version');
1131
                }
1132
            }
1133 323
        }
1134 271
1135 271
        foreach ($opts['extrasockopts'] as $proto => $protoOpts) {
1136 271
            foreach ($protoOpts as $key => $val) {
1137
                $contextOptions[$proto][$key] = $val;
1138 271
            }
1139
        }
1140
1141 323
        $context = stream_context_create($contextOptions);
1142
1143
        if ($opts['timeout'] <= 0) {
1144
            $connectTimeout = ini_get('default_socket_timeout');
1145 323
        } else {
1146
            $connectTimeout = $opts['timeout'];
1147
        }
1148
1149 323
        $this->errno = 0;
1150
        $this->errstr = '';
1151
1152
        /// @todo using `error_get_last` does not give us very detailed messages for connections errors, eg. for ssl
1153
        ///       problems on php 5.6 we get 'Connect error: stream_socket_client(): unable to connect to tls://localhost:443 (Unknown error) (0)',
1154
        ///       versus the more detailed warnings 'PHP Warning:  stream_socket_client(): SSL operation failed with code 1. OpenSSL Error messages:
1155
        ///         error:0A0C0103:SSL routines::internal error in /home/docker/workspace/src/Client.php on line 1121
1156
        ///         PHP Warning:  stream_socket_client(): Failed to enable crypto in /home/docker/workspace/src/Client.php on line 1121
1157
        ///         PHP Warning:  stream_socket_client(): unable to connect to tls://localhost:443 (Unknown error) in /home/docker/workspace/src/Client.php on line 1121'
1158
        ///       This could be obviated by removing the `@` and capturing warnings via ob_start and co
1159
        $fp = @stream_socket_client("$transport://$connectServer:$connectPort", $this->errno, $this->errstr, $connectTimeout,
1160
            STREAM_CLIENT_CONNECT, $context);
1161
        if ($fp) {
0 ignored issues
show
$fp is of type false|resource, thus it always evaluated to false.
Loading history...
1162
            if ($opts['timeout'] > 0) {
1163
                stream_set_timeout($fp, $opts['timeout'], 0);
1164
            }
1165
        } else {
1166
            if ($this->errstr == '') {
1167
                $err = error_get_last();
1168
                $this->errstr = $err['message'];
1169
            }
1170
1171
            $this->errstr = 'Connect error: ' . $this->errstr;
1172
            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['http_error'], $this->errstr . ' (' . $this->errno . ')');
1173
        }
1174 66
1175
        /// @todo from here onwards, we can inject the results of stream_get_meta_data in the response. We could
1176 66
        ///       do that f.e. only in new debug level 3, or starting at v1
1177
1178
        if (!fputs($fp, $op, strlen($op))) {
1179 66
            fclose($fp);
1180 45
            $this->errstr = 'Write error';
1181 45
            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['http_error'], $this->errstr);
1182
        }
1183 45
1184
        $info = stream_get_meta_data($fp);
1185
        if ($info['timed_out']) {
1186
            fclose($fp);
1187
            $this->errstr = 'Write timeout';
1188
            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['http_error'], $this->errstr);
1189
        }
1190
1191
        // Close socket before parsing.
1192
        // It should yield slightly better execution times, and make easier recursive calls (e.g. to follow http redirects)
1193
        $ipd = '';
1194
        do {
1195
            // shall we check for $data === FALSE?
1196
            // as per the manual, it signals an error
1197
            $ipd .= fread($fp, 32768);
1198
1199
            $info = stream_get_meta_data($fp);
1200
            if ($info['timed_out']) {
1201 23
                fclose($fp);
1202
                $this->errstr = 'Read timeout';
1203
                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['http_error'], $this->errstr);
1204 23
            }
1205 23
1206
        } while (!feof($fp));
1207
        fclose($fp);
1208
1209 23
        return $req->parseResponse($ipd, false, $opts['return_type']);
1210 23
    }
1211
1212
    /**
1213
     * Contributed by Justin Miller
1214
     * Requires curl to be built into PHP
1215
     * NB: CURL versions before 7.11.10 cannot use proxy to talk to https servers!
1216
     *
1217
     * @param Request $req
1218
     * @param string $method
1219
     * @param string $server
1220
     * @param int $port
1221 23
     * @param string $path
1222
     * @param array $opts the keys/values match self::getOptions
1223
     * @return Response
1224
     *
1225
     * @todo the $path arg atm is ignored. What to do if it is != $this->path?
1226
     */
1227
    protected function sendViaCURL($req, $method, $server, $port, $path, $opts)
0 ignored issues
show
The parameter $path 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

1227
    protected function sendViaCURL($req, $method, $server, $port, /** @scrutinizer ignore-unused */ $path, $opts)

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...
1228
    {
1229
        if (!function_exists('curl_init')) {
1230
            $this->errstr = 'CURL unavailable on this install';
1231
            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['no_curl'], PhpXmlRpc::$xmlrpcstr['no_curl']);
1232
        }
1233
        if ($method == 'https' || $method == 'h2') {
1234
            // q: what about installs where we get back a string, but curl is linked to other ssl libs than openssl?
1235 45
            if (($info = curl_version()) &&
1236
                ((is_string($info) && strpos($info, 'OpenSSL') === null) || (is_array($info) && !isset($info['ssl_version'])))
0 ignored issues
show
The condition is_string($info) is always false.
Loading history...
1237
            ) {
1238 45
                $this->errstr = 'SSL unavailable on this install';
1239 45
                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['no_ssl'], PhpXmlRpc::$xmlrpcstr['no_ssl']);
1240 45
            }
1241 45
        }
1242 45
        if (($method == 'h2' && !defined('CURL_HTTP_VERSION_2_0')) ||
1243 45
            ($method == 'h2c' && !defined('CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE'))) {
1244 45
            $this->errstr = 'HTTP/2 unavailable on this install';
1245
            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['no_http2'], PhpXmlRpc::$xmlrpcstr['no_http2']);
1246 45
        }
1247 45
1248
        // BC - we go through prepareCurlHandle in case some subclass reimplemented it
1249 45
        $curl = $this->prepareCurlHandle($req, $server, $port, $opts['timeout'], $opts['username'], $opts['password'],
0 ignored issues
show
Deprecated Code introduced by
The function PhpXmlRpc\Client::prepareCurlHandle() has been deprecated. ( Ignorable by Annotation )

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

1249
        $curl = /** @scrutinizer ignore-deprecated */ $this->prepareCurlHandle($req, $server, $port, $opts['timeout'], $opts['username'], $opts['password'],
Loading history...
1250 45
            $opts['authtype'], $opts['cert'], $opts['certpass'], $opts['cacert'], $opts['cacertdir'], $opts['proxy'],
1251
            $opts['proxyport'], $opts['proxy_user'], $opts['proxy_pass'], $opts['proxy_authtype'], $method,
1252
            $opts['keepalive'], $opts['key'], $opts['keypass'], $opts['sslversion']);
1253 45
1254
        if (!$curl) {
1255 45
            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['curl_fail'], PhpXmlRpc::$xmlrpcstr['curl_fail'] .
1256
                ': error during curl initialization. Check php error log for details');
1257
        }
1258
1259
        $result = curl_exec($curl);
1260
1261 45
        if ($opts['debug'] > 1) {
1262
            $message = "---CURL INFO---\n";
1263 45
            foreach (curl_getinfo($curl) as $name => $val) {
1264
                if (is_array($val)) {
1265 45
                    $val = implode("\n", $val);
1266
                }
1267 22
                $message .= $name . ': ' . $val . "\n";
1268 22
            }
1269
            $message .= '---END---';
1270
            $this->getLogger()->debug($message);
1271 22
        }
1272 22
1273
        if (!$result) {
1274
            /// @todo we should use a better check here - what if we get back '' or '0'?
1275
1276 22
            $this->errstr = 'no response';
1277 22
            $resp = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['curl_fail'], PhpXmlRpc::$xmlrpcstr['curl_fail'] .
1278 22
                ': ' . curl_error($curl));
1279 22
            if (PHP_MAJOR_VERSION < 8) curl_close($curl);
1280
            if ($opts['keepalive']) {
1281
                $this->xmlrpc_curl_handle = null;
1282 22
            }
1283 22
        } else {
1284 22
            if (!$opts['keepalive']) {
1285
                if (PHP_MAJOR_VERSION < 8) curl_close($curl);
1286
            }
1287
            $resp = $req->parseResponse($result, true, $opts['return_type']);
0 ignored issues
show
It seems like $result can also be of type true; however, parameter $data of PhpXmlRpc\Request::parseResponse() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1287
            $resp = $req->parseResponse(/** @scrutinizer ignore-type */ $result, true, $opts['return_type']);
Loading history...
1288 22
            if ($opts['keepalive']) {
1289 22
                /// @todo if we got back a 302 or 308, we should not reuse the curl handle for later calls
1290 22
                if (is_object($resp) && $resp->faultCode() == PhpXmlRpc::$xmlrpcerr['http_error']) {
1291
                    if (PHP_MAJOR_VERSION < 8) curl_close($curl);
1292 22
                    $this->xmlrpc_curl_handle = null;
1293 22
                }
1294
            }
1295
        }
1296 22
1297 22
        return $resp;
1298
    }
1299
1300 22
    /**
1301 22
     * @param Request $req
1302
     * @param string $method
1303
     * @param string $server
1304
     * @param int $port
1305
     * @param string $path
1306
     * @param array $opts the keys/values match self::getOptions
1307 22
     * @return \CurlHandle|resource|false
1308
     *
1309
     * @todo allow this method to either throw or return a Response, so that we can pass back to caller more info on errors
1310
     */
1311 24
    protected function createCURLHandle($req, $method, $server, $port, $path, $opts)
1312 24
    {
1313
        if ($port == 0) {
1314
            if (in_array($method, array('http', 'http10', 'http11', 'h2c'))) {
1315 24
                $port = 80;
1316 24
            } else {
1317
                $port = 443;
1318
            }
1319
        }
1320 24
1321 24
        // Only create the payload if it was not created previously
1322 24
        $payload = $req->getPayload();
1323 24
        if (empty($payload)) {
1324 24
            $payload = $req->serialize($opts['request_charset_encoding']);
1325
        }
1326
1327
        // Deflate request body and set appropriate request headers
1328 24
        $encodingHdr = '';
1329 24
        if (($opts['request_compression'] == 'gzip' || $opts['request_compression'] == 'deflate')) {
1330 22
            if ($opts['request_compression'] == 'gzip' && function_exists('gzencode')) {
1331 22
                $a = @gzencode($payload);
1332
                if ($a) {
1333 22
                    $payload = $a;
1334
                    $encodingHdr = 'Content-Encoding: gzip';
1335
                } else {
1336 22
                    $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': gzencode failure in compressing request');
1337
                }
1338 22
            } else if (function_exists('gzcompress')) {
1339
                $a = @gzcompress($payload);
1340
                if ($a) {
1341 22
                    $payload = $a;
1342 22
                    $encodingHdr = 'Content-Encoding: deflate';
1343
                } else {
1344
                    $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': gzcompress failure in compressing request');
1345
                }
1346
            } else {
1347
                $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': desired request compression method is unsupported by this PHP install');
1348 24
            }
1349
        } else {
1350
            if ($opts['request_compression'] != '') {
1351
                $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': desired request compression method is unsupported');
1352
            }
1353
        }
1354
1355
        if (!$opts['keepalive'] || !$this->xmlrpc_curl_handle) {
1356
            if ($method == 'http11' || $method == 'http10' || $method == 'h2c') {
1357
                $protocol = 'http';
1358
            } else {
1359
                if ($method == 'h2') {
1360
                    $protocol = 'https';
1361
                } else {
1362
                    // http, https
1363
                    $protocol = $method;
1364
                    if (strpos($protocol, ':') !== false) {
1365
                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ": warning - attempted hacking attempt?. The curl protocol requested for the call is: '$protocol'");
1366
                        return false;
1367
                    }
1368
                }
1369
            }
1370
            $curl = curl_init($protocol . '://' . $server . ':' . $port . $path);
1371
            if (!$curl) {
1372
                return false;
1373
            }
1374
            if ($opts['keepalive']) {
1375
                $this->xmlrpc_curl_handle = $curl;
1376
            }
1377
        } else {
1378
            $curl = $this->xmlrpc_curl_handle;
1379
        }
1380
1381
        // results into variable
1382
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
1383
1384
        if ($opts['debug'] > 1) {
1385
            curl_setopt($curl, CURLOPT_VERBOSE, true);
1386
            /// @todo redirect curlopt_stderr to some stream which can be piped to the logger
1387
        }
1388
        curl_setopt($curl, CURLOPT_USERAGENT, $opts['user_agent']);
1389
        // required for XMLRPC: post the data
1390
        curl_setopt($curl, CURLOPT_POST, 1);
1391
        // the data
1392
        curl_setopt($curl, CURLOPT_POSTFIELDS, $payload);
1393
1394
        // return the header too
1395
        curl_setopt($curl, CURLOPT_HEADER, 1);
1396
1397
        // NB: if we set an empty string, CURL will add http header indicating
1398
        // ALL methods it is supporting. This is possibly a better option than letting the user tell what curl can / cannot do...
1399
        if (is_array($opts['accepted_compression']) && count($opts['accepted_compression'])) {
1400
            //curl_setopt($curl, CURLOPT_ENCODING, implode(',', $opts['accepted_compression']));
1401
            // empty string means 'any supported by CURL' (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
1402
            if (count($opts['accepted_compression']) == 1) {
1403
                curl_setopt($curl, CURLOPT_ENCODING, $opts['accepted_compression'][0]);
1404
            } else {
1405
                curl_setopt($curl, CURLOPT_ENCODING, '');
1406
            }
1407
        }
1408
        // extra headers
1409
        $headers = array('Content-Type: ' . $req->getContentType(), 'Accept-Charset: ' . implode(',', $opts['accepted_charset_encodings']));
1410
        // if no keepalive is wanted, let the server know it in advance
1411
        if (!$opts['keepalive']) {
1412
            $headers[] = 'Connection: close';
1413
        }
1414
        // request compression header
1415
        if ($encodingHdr) {
1416
            $headers[] = $encodingHdr;
1417
        }
1418
1419
        if (is_array($this->extra_headers) && $this->extra_headers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extra_headers of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1420
            $headers = array_merge($headers, $this->extra_headers);
1421
        }
1422
1423
        // Fix the HTTP/1.1 417 Expectation Failed Bug (curl by default adds a 'Expect: 100-continue' header when POST
1424
        // size exceeds 1025 bytes, apparently)
1425
        $headers[] = 'Expect:';
1426
1427
        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
1428
        // previous note: "timeout is borked" (on some old php/curl versions? It seems to work on 8.1. Maybe the issue
1429
        // has to do with dns resolution...)
1430
        if ($opts['timeout']) {
1431
            curl_setopt($curl, CURLOPT_TIMEOUT, $opts['timeout']);
1432
        }
1433
1434
        // nb: for 'http' and 'https' we leave it up to curl to decide
1435
        /// @see https://www.php.net/manual/en/curl.constants.php#constant.curl-http-version-1-0
1436
        /// @see https://curl.se/libcurl/c/CURLOPT_HTTP_VERSION.html
1437
        /// @todo add support for CURL_VERSION_HTTP3
1438
        switch ($method) {
1439
            case 'http10':
1440
                curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
1441
                break;
1442
            case 'http11':
1443
                curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
1444
                break;
1445
            case 'h2c':
1446
                if (defined('CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE')) {
1447
                    curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE);
1448
                } else {
1449
                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': warning. HTTP2 is not supported by the current PHP/curl install');
1450
                    if (PHP_MAJOR_VERSION < 8) curl_close($curl);
1451
                    return false;
1452
                }
1453
                break;
1454
            case 'h2':
1455
                curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
1456
                break;
1457
        }
1458
1459
        if ($opts['username'] && $opts['password']) {
1460
            curl_setopt($curl, CURLOPT_USERPWD, $opts['username'] . ':' . $opts['password']);
1461
            if (defined('CURLOPT_HTTPAUTH')) {
1462
                curl_setopt($curl, CURLOPT_HTTPAUTH, $opts['authtype']);
1463
            } elseif ($opts['authtype'] != 1) {
1464
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth is supported by the current PHP/curl install');
1465
                if (PHP_MAJOR_VERSION < 8) curl_close($curl);
1466
                return false;
1467
            }
1468
        }
1469
1470
        // note: h2c is http2 without the https. No need to have it in this IF
1471
        if ($method == 'https' || $method == 'h2') {
1472
            // set cert file
1473
            if ($opts['cert']) {
1474
                curl_setopt($curl, CURLOPT_SSLCERT, $opts['cert']);
1475
            }
1476
            // set cert password
1477
            if ($opts['certpass']) {
1478
                curl_setopt($curl, CURLOPT_SSLCERTPASSWD, $opts['certpass']);
1479
            }
1480
            // whether to verify remote host's cert
1481
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $opts['verifypeer']);
1482
            // set ca certificates file/dir
1483
            if ($opts['cacert']) {
1484
                curl_setopt($curl, CURLOPT_CAINFO, $opts['cacert']);
1485
            }
1486
            if ($opts['cacertdir']) {
1487
                curl_setopt($curl, CURLOPT_CAPATH, $opts['cacertdir']);
1488
            }
1489
            // set key file (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
1490
            if ($opts['key']) {
1491
                curl_setopt($curl, CURLOPT_SSLKEY, $opts['key']);
1492
            }
1493
            // set key password (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
1494
            if ($opts['keypass']) {
1495
                curl_setopt($curl, CURLOPT_SSLKEYPASSWD, $opts['keypass']);
1496
            }
1497
            // whether to verify cert's common name (CN); 0 for no, 1 to verify that it exists, and 2 to verify that
1498
            // it matches the hostname used
1499
            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $opts['verifyhost']);
1500
            // allow usage of different SSL versions
1501
            curl_setopt($curl, CURLOPT_SSLVERSION, $opts['sslversion']);
1502
        }
1503
1504
        // proxy info
1505
        if ($opts['proxy']) {
1506
            if ($opts['proxyport'] == 0) {
1507
                $opts['proxyport'] = 8080; // NB: even for HTTPS, local connection is on port 8080
1508
            }
1509
            curl_setopt($curl, CURLOPT_PROXY, $opts['proxy'] . ':' . $opts['proxyport']);
1510
            if ($opts['proxy_user']) {
1511
                curl_setopt($curl, CURLOPT_PROXYUSERPWD, $opts['proxy_user'] . ':' . $opts['proxy_pass']);
1512
                if (defined('CURLOPT_PROXYAUTH')) {
1513
                    curl_setopt($curl, CURLOPT_PROXYAUTH, $opts['proxy_authtype']);
1514
                } elseif ($opts['proxy_authtype'] != 1) {
1515
                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth to proxy is supported by the current PHP/curl install');
1516
                    if (PHP_MAJOR_VERSION < 8) curl_close($curl);
1517
                    return false;
1518
                }
1519
            }
1520
        }
1521
1522
        // NB: should we build cookie http headers by hand rather than let CURL do it?
1523
        // NB: the following code does not honour 'expires', 'path' and 'domain' cookie attributes set to client obj by the user...
1524
        if (count($opts['cookies'])) {
1525
            $cookieHeader = '';
1526
            foreach ($opts['cookies'] as $name => $cookie) {
1527
                $cookieHeader .= $name . '=' . $cookie['value'] . '; ';
1528
            }
1529
            curl_setopt($curl, CURLOPT_COOKIE, substr($cookieHeader, 0, -2));
1530
        }
1531
1532
        foreach ($opts['extracurlopts'] as $opt => $val) {
1533
            curl_setopt($curl, $opt, $val);
1534
        }
1535
1536
        if ($opts['debug'] > 1) {
1537
            $this->getLogger()->debug("---SENDING---\n$payload\n---END---");
1538
        }
1539
1540
        return $curl;
1541
    }
1542
1543
    /**
1544
     * Send an array of requests and return an array of responses.
1545
     *
1546
     * Unless $this->no_multicall has been set to true, it will try first to use one single xml-rpc call to server method
1547
     * system.multicall, and revert to sending many successive calls in case of failure.
1548
     * This failure is also stored in $this->no_multicall for subsequent calls.
1549
     * Unfortunately, there is no server error code universally used to denote the fact that multicall is unsupported,
1550
     * so there is no way to reliably distinguish between that and a temporary failure.
1551
     * If you are sure that server supports multicall and do not want to fallback to using many single calls, set the
1552
     * 2np parameter to FALSE.
1553
     *
1554
     * NB: trying to shoehorn extra functionality into existing syntax has resulted
1555
     * in pretty much convoluted code...
1556
     *
1557
     * @param Request[] $reqs an array of Request objects
1558
     * @param bool $noFallback When true, upon receiving an error during multicall, multiple single calls will not be
1559
     *                         attempted.
1560
     *                         Deprecated alternative, was: int - "connection timeout (in seconds). See the details in the
1561
     *                         docs for the send() method". Please use setOption instead to set a timeout
1562
     * @param string $method deprecated. Was: "the http protocol variant to be used. See the details in the docs for the send() method."
1563
     *                       Please use the constructor to set an http protocol variant.
1564
     * @param boolean $fallback deprecated. Was: "when true, upon receiving an error during multicall, multiple single
1565
     *                          calls will be attempted"
1566
     * @return Response[]
1567
     */
1568
    public function multicall($reqs, $timeout = 0, $method = '', $fallback = true)
1569
    {
1570
        // BC
1571
        if (is_bool($timeout) && $fallback === true) {
1572
            $fallback = !$timeout;
1573
            $timeout = 0;
1574
        }
1575
1576
        if ($method == '') {
1577
            $method = $this->method;
1578
        }
1579
1580
        if (!$this->no_multicall) {
1581
            $results = $this->_try_multicall($reqs, $timeout, $method);
1582
            /// @todo how to handle the case of $this->return_type = xml?
1583
            if (is_array($results)) {
1584
                // System.multicall succeeded
1585
                return $results;
1586
            } else {
1587
                // either system.multicall is unsupported by server, or the call failed for some other reason.
1588
                // Feature creep: is there a way to tell apart unsupported multicall from other faults?
1589
                if ($fallback) {
1590
                    // Don't try it next time...
1591
                    $this->no_multicall = true;
1592
                } else {
1593
                    $result = $results;
1594
                }
1595
            }
1596
        } else {
1597
            // override fallback, in case careless user tries to do two
1598
            // opposite things at the same time
1599
            $fallback = true;
1600
        }
1601
1602
        $results = array();
1603
        if ($fallback) {
1604
            // system.multicall is (probably) unsupported by server: emulate multicall via multiple requests
1605
            /// @todo use curl multi_ functions to make this quicker (see the implementation in the parallel.php demo)
1606
            foreach ($reqs as $req) {
1607
                $results[] = $this->send($req, $timeout, $method);
1608
            }
1609
        } else {
1610
            // user does NOT want to fallback on many single calls: since we should always return an array of responses,
1611
            // we return an array with the same error repeated n times
1612
            foreach ($reqs as $req) {
1613
                $results[] = $result;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
1614
            }
1615
        }
1616
1617
        return $results;
1618
    }
1619
1620
    /**
1621
     * Attempt to boxcar $reqs via system.multicall.
1622
     *
1623
     * @param Request[] $reqs
1624
     * @param int $timeout
1625
     * @param string $method
1626
     * @return Response[]|Response a single Response when the call returned a fault / does not conform to what we expect
1627
     *                             from a multicall response
1628
     */
1629
    protected function _try_multicall($reqs, $timeout, $method)
1630
    {
1631
        // Construct multicall request
1632
        $calls = array();
1633
        foreach ($reqs as $req) {
1634
            $call['methodName'] = new Value($req->method(), 'string');
1635
            $numParams = $req->getNumParams();
1636
            $params = array();
1637
            for ($i = 0; $i < $numParams; $i++) {
1638
                $params[$i] = $req->getParam($i);
1639
            }
1640
            $call['params'] = new Value($params, 'array');
1641
            $calls[] = new Value($call, 'struct');
1642
        }
1643
        $multiCall = new static::$requestClass('system.multicall');
1644
        $multiCall->addParam(new Value($calls, 'array'));
1645
1646
        // Attempt RPC call
1647
        $result = $this->send($multiCall, $timeout, $method);
1648
1649
        if ($result->faultCode() != 0) {
1650
            // call to system.multicall failed
1651
            return $result;
1652
        }
1653
1654
        // Unpack responses.
1655
        $rets = $result->value();
1656
        $response = array();
1657
1658
        if ($this->return_type == 'xml') {
1659
            for ($i = 0; $i < count($reqs); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

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

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

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
1660
                /// @todo can we do better? we set the complete xml into each response...
1661
                $response[] = new static::$responseClass($rets, 0, '', 'xml', $result->httpResponse());
1662
            }
1663
1664
        } elseif ($this->return_type == 'phpvals') {
1665
            if (!is_array($rets)) {
1666
                // bad return type from system.multicall
1667
                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1668
                    PhpXmlRpc::$xmlrpcstr['multicall_error'] . ': not an array', 'phpvals', $result->httpResponse());
1669
            }
1670
            $numRets = count($rets);
1671
            if ($numRets != count($reqs)) {
1672
                // wrong number of return values.
1673
                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1674
                    PhpXmlRpc::$xmlrpcstr['multicall_error'] . ': incorrect number of responses', 'phpvals',
1675
                    $result->httpResponse());
1676
            }
1677
1678
            for ($i = 0; $i < $numRets; $i++) {
1679
                $val = $rets[$i];
1680
                if (!is_array($val)) {
1681
                    return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1682
                        PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i is not an array or struct",
1683
                        'phpvals', $result->httpResponse());
1684
                }
1685
                switch (count($val)) {
1686
                    case 1:
1687
                        if (!isset($val[0])) {
1688
                            // Bad value
1689
                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1690
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has no value",
1691
                                'phpvals', $result->httpResponse());
1692
                        }
1693
                        // Normal return value
1694
                        $response[$i] = new static::$responseClass($val[0], 0, '', 'phpvals', $result->httpResponse());
1695
                        break;
1696
                    case 2:
1697
                        /// @todo remove usage of @: it is apparently quite slow
1698
                        $code = @$val['faultCode'];
1699
                        if (!is_int($code)) {
1700
                            /// @todo should we check that it is != 0?
1701
                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1702
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no faultCode",
1703
                                'phpvals', $result->httpResponse());
1704
                        }
1705
                        $str = @$val['faultString'];
1706
                        if (!is_string($str)) {
1707
                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1708
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no FaultString",
1709
                                'phpvals', $result->httpResponse());
1710
                        }
1711
                        $response[$i] = new static::$responseClass(0, $code, $str, 'phpvals', $result->httpResponse());
1712
                        break;
1713
                    default:
1714
                        return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1715
                            PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has too many items",
1716
                            'phpvals', $result->httpResponse());
1717
                }
1718
            }
1719
1720
        } else {
1721
            // return type == 'xmlrpcvals'
1722
            if ($rets->kindOf() != 'array') {
1723
                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1724
                    PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i is not an array", 'xmlrpcvals',
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $i seems to be defined by a foreach iteration on line 1633. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1725
                    $result->httpResponse());
1726
            }
1727
            $numRets = $rets->count();
1728
            if ($numRets != count($reqs)) {
1729
                // wrong number of return values.
1730
                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1731
                    PhpXmlRpc::$xmlrpcstr['multicall_error'] . ': incorrect number of responses', 'xmlrpcvals',
1732
                    $result->httpResponse());
1733
            }
1734
1735
            foreach ($rets as $i => $val) {
1736
                switch ($val->kindOf()) {
1737
                    case 'array':
1738
                        if ($val->count() != 1) {
1739
                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1740
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has too many items",
1741
                                'phpvals', $result->httpResponse());
1742
                        }
1743
                        // Normal return value
1744
                        $response[] = new static::$responseClass($val[0], 0, '', 'xmlrpcvals', $result->httpResponse());
1745
                        break;
1746
                    case 'struct':
1747
                        if ($val->count() != 2) {
1748
                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1749
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has too many items",
1750
                                'phpvals', $result->httpResponse());
1751
                        }
1752
                        /** @var Value $code */
1753
                        $code = @$val['faultCode'];
1754
                        if (!$code || $code->kindOf() != 'scalar' || $code->scalarTyp() != 'int') {
1755
                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1756
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no faultCode",
1757
                                'xmlrpcvals', $result->httpResponse());
1758
                        }
1759
                        /** @var Value $str */
1760
                        $str = @$val['faultString'];
1761
                        if (!$str || $str->kindOf() != 'scalar' || $str->scalarTyp() != 'string') {
1762
                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1763
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no faultString",
1764
                                'xmlrpcvals', $result->httpResponse());
1765
                        }
1766
                        $response[] = new static::$responseClass(0, $code->scalarVal(), $str->scalarVal(), 'xmlrpcvals', $result->httpResponse());
1767
                        break;
1768
                    default:
1769
                        return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1770
                            PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i is not an array or struct",
1771
                            'xmlrpcvals', $result->httpResponse());
1772
                }
1773
            }
1774
        }
1775
1776
        return $response;
1777
    }
1778
1779
    // *** BC layer ***
1780
1781
    /**
1782
     * NB: always goes via socket, never curl
1783
     *
1784
     * @deprecated
1785
     *
1786
     * @param Request $req
1787
     * @param string $server
1788
     * @param int $port
1789
     * @param int $timeout
1790
     * @param string $username
1791
     * @param string $password
1792
     * @param int $authType
1793
     * @param string $proxyHost
1794
     * @param int $proxyPort
1795
     * @param string $proxyUsername
1796
     * @param string $proxyPassword
1797
     * @param int $proxyAuthType
1798
     * @param string $method
1799
     * @return Response
1800
     */
1801
    protected function sendPayloadHTTP10($req, $server, $port, $timeout = 0, $username = '', $password = '',
1802
        $authType = 1, $proxyHost = '', $proxyPort = 0, $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1,
1803
        $method = 'http')
1804
    {
1805
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
1806
1807
        return $this->sendPayloadSocket($req, $server, $port, $timeout, $username, $password, $authType, null, null,
0 ignored issues
show
Deprecated Code introduced by
The function PhpXmlRpc\Client::sendPayloadSocket() has been deprecated. ( Ignorable by Annotation )

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

1807
        return /** @scrutinizer ignore-deprecated */ $this->sendPayloadSocket($req, $server, $port, $timeout, $username, $password, $authType, null, null,
Loading history...
1808
            null, null, $proxyHost, $proxyPort, $proxyUsername, $proxyPassword, $proxyAuthType, $method);
1809
    }
1810
1811
    /**
1812
     * NB: always goes via curl, never socket
1813
     *
1814
     * @deprecated
1815
     *
1816
     * @param Request $req
1817
     * @param string $server
1818
     * @param int $port
1819
     * @param int $timeout
1820
     * @param string $username
1821
     * @param string $password
1822
     * @param int $authType
1823
     * @param string $cert
1824
     * @param string $certPass
1825
     * @param string $caCert
1826
     * @param string $caCertDir
1827
     * @param string $proxyHost
1828
     * @param int $proxyPort
1829
     * @param string $proxyUsername
1830
     * @param string $proxyPassword
1831
     * @param int $proxyAuthType
1832
     * @param bool $keepAlive
1833
     * @param string $key
1834
     * @param string $keyPass
1835
     * @param int $sslVersion
1836
     * @return Response
1837
     */
1838
    protected function sendPayloadHTTPS($req, $server, $port, $timeout = 0, $username = '', $password = '',
1839
        $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0,
1840
        $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $keepAlive = false, $key = '', $keyPass = '',
1841
        $sslVersion = 0)
1842
    {
1843
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
1844
1845
        return $this->sendPayloadCURL($req, $server, $port, $timeout, $username,
0 ignored issues
show
Deprecated Code introduced by
The function PhpXmlRpc\Client::sendPayloadCURL() has been deprecated. ( Ignorable by Annotation )

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

1845
        return /** @scrutinizer ignore-deprecated */ $this->sendPayloadCURL($req, $server, $port, $timeout, $username,
Loading history...
1846
            $password, $authType, $cert, $certPass, $caCert, $caCertDir, $proxyHost, $proxyPort,
1847
            $proxyUsername, $proxyPassword, $proxyAuthType, 'https', $keepAlive, $key, $keyPass, $sslVersion);
1848
    }
1849
1850
    /**
1851
     * @deprecated
1852
     *
1853
     * @param Request $req
1854
     * @param string $server
1855
     * @param int $port
1856
     * @param int $timeout
1857
     * @param string $username
1858
     * @param string $password
1859
     * @param int $authType only value supported is 1
1860
     * @param string $cert
1861
     * @param string $certPass
1862
     * @param string $caCert
1863
     * @param string $caCertDir
1864
     * @param string $proxyHost
1865
     * @param int $proxyPort
1866
     * @param string $proxyUsername
1867
     * @param string $proxyPassword
1868
     * @param int $proxyAuthType only value supported is 1
1869
     * @param string $method 'http' (synonym for 'http10'), 'http10' or 'https'
1870
     * @param string $key
1871
     * @param string $keyPass @todo not implemented yet.
1872
     * @param int $sslVersion
1873
     * @return Response
1874
     */
1875
    protected function sendPayloadSocket($req, $server, $port, $timeout = 0, $username = '', $password = '',
1876
        $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0,
1877
        $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method = 'http', $key = '', $keyPass = '',
1878
        $sslVersion = 0)
1879
    {
1880
        $this->logDeprecationUnlessCalledBy('send');
1881
1882
        return $this->sendViaSocket($req, $method, $server, $port, $this->path, array(
1883
            'accepted_charset_encodings' => $this->accepted_charset_encodings,
1884
            'accepted_compression' => $this->accepted_compression,
1885
            'authtype' => $authType,
1886
            'cacert' => $caCert,
1887
            'cacertdir' => $caCertDir,
1888
            'cert' => $cert,
1889
            'certpass' => $certPass,
1890
            'cookies' => $this->cookies,
1891
            'debug' => $this->debug,
1892
            'extracurlopts' => $this->extracurlopts,
1893
            'extrasockopts' => $this->extrasockopts,
1894
            'keepalive' => $this->keepalive,
1895
            'key' => $key,
1896
            'keypass' => $keyPass,
1897
            'no_multicall' => $this->no_multicall,
1898
            'password' => $password,
1899
            'proxy' => $proxyHost,
1900
            'proxy_authtype' => $proxyAuthType,
1901
            'proxy_pass' => $proxyPassword,
1902
            'proxyport' => $proxyPort,
1903
            'proxy_user' => $proxyUsername,
1904
            'request_charset_encoding' => $this->request_charset_encoding,
1905
            'request_compression' => $this->request_compression,
1906
            'return_type' => $this->return_type,
1907
            'sslversion' => $sslVersion,
1908
            'timeout' => $timeout,
1909
            'username' => $username,
1910
            'user_agent' => $this->user_agent,
1911
            'use_curl' => $this->use_curl,
1912
            'verifyhost' => $this->verifyhost,
1913
            'verifypeer' => $this->verifypeer,
1914
        ));
1915
    }
1916
1917
    /**
1918
     * @deprecated
1919
     *
1920
     * @param Request $req
1921
     * @param string $server
1922
     * @param int $port
1923
     * @param int $timeout
1924
     * @param string $username
1925
     * @param string $password
1926
     * @param int $authType
1927
     * @param string $cert
1928
     * @param string $certPass
1929
     * @param string $caCert
1930
     * @param string $caCertDir
1931
     * @param string $proxyHost
1932
     * @param int $proxyPort
1933
     * @param string $proxyUsername
1934
     * @param string $proxyPassword
1935
     * @param int $proxyAuthType
1936
     * @param string $method 'http' (let curl decide), 'http10', 'http11', 'https', 'h2c' or 'h2'
1937
     * @param bool $keepAlive
1938
     * @param string $key
1939
     * @param string $keyPass
1940
     * @param int $sslVersion
1941
     * @return Response
1942
     */
1943
    protected function sendPayloadCURL($req, $server, $port, $timeout = 0, $username = '', $password = '',
1944
        $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0,
1945
        $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method = 'https', $keepAlive = false, $key = '',
1946
        $keyPass = '', $sslVersion = 0)
1947
    {
1948
        $this->logDeprecationUnlessCalledBy('send');
1949
1950
        return $this->sendViaCURL($req, $method, $server, $port, $this->path, array(
1951
            'accepted_charset_encodings' => $this->accepted_charset_encodings,
1952
            'accepted_compression' => $this->accepted_compression,
1953
            'authtype' => $authType,
1954
            'cacert' => $caCert,
1955
            'cacertdir' => $caCertDir,
1956
            'cert' => $cert,
1957
            'certpass' => $certPass,
1958
            'cookies' => $this->cookies,
1959
            'debug' => $this->debug,
1960
            'extracurlopts' => $this->extracurlopts,
1961
            'extrasockopts' => $this->extrasockopts,
1962
            'keepalive' => $keepAlive,
1963
            'key' => $key,
1964
            'keypass' => $keyPass,
1965
            'no_multicall' => $this->no_multicall,
1966
            'password' => $password,
1967
            'proxy' => $proxyHost,
1968
            'proxy_authtype' => $proxyAuthType,
1969
            'proxy_pass' => $proxyPassword,
1970
            'proxyport' => $proxyPort,
1971
            'proxy_user' => $proxyUsername,
1972
            'request_charset_encoding' => $this->request_charset_encoding,
1973
            'request_compression' => $this->request_compression,
1974
            'return_type' => $this->return_type,
1975
            'sslversion' => $sslVersion,
1976
            'timeout' => $timeout,
1977
            'username' => $username,
1978
            'user_agent' => $this->user_agent,
1979
            'use_curl' => $this->use_curl,
1980
            'verifyhost' => $this->verifyhost,
1981
            'verifypeer' => $this->verifypeer,
1982
        ));
1983
    }
1984
1985
    /**
1986
     * @deprecated
1987
     *
1988
     * @param $req
1989
     * @param $server
1990
     * @param $port
1991
     * @param $timeout
1992
     * @param $username
1993
     * @param $password
1994
     * @param $authType
1995
     * @param $cert
1996
     * @param $certPass
1997
     * @param $caCert
1998
     * @param $caCertDir
1999
     * @param $proxyHost
2000
     * @param $proxyPort
2001
     * @param $proxyUsername
2002
     * @param $proxyPassword
2003
     * @param $proxyAuthType
2004
     * @param $method
2005
     * @param $keepAlive
2006
     * @param $key
2007
     * @param $keyPass
2008
     * @param $sslVersion
2009
     * @return false|\CurlHandle|resource
2010
     */
2011
    protected function prepareCurlHandle($req, $server, $port, $timeout = 0, $username = '', $password = '',
2012
         $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0,
2013
         $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method = 'https', $keepAlive = false, $key = '',
2014
         $keyPass = '', $sslVersion = 0)
2015
    {
2016
        $this->logDeprecationUnlessCalledBy('sendViaCURL');
2017
2018
        return $this->createCURLHandle($req, $method, $server, $port, $this->path, array(
2019
            'accepted_charset_encodings' => $this->accepted_charset_encodings,
2020
            'accepted_compression' => $this->accepted_compression,
2021
            'authtype' => $authType,
2022
            'cacert' => $caCert,
2023
            'cacertdir' => $caCertDir,
2024
            'cert' => $cert,
2025
            'certpass' => $certPass,
2026
            'cookies' => $this->cookies,
2027
            'debug' => $this->debug,
2028
            'extracurlopts' => $this->extracurlopts,
2029
            'keepalive' => $keepAlive,
2030
            'key' => $key,
2031
            'keypass' => $keyPass,
2032
            'no_multicall' => $this->no_multicall,
2033
            'password' => $password,
2034
            'proxy' => $proxyHost,
2035
            'proxy_authtype' => $proxyAuthType,
2036
            'proxy_pass' => $proxyPassword,
2037
            'proxyport' => $proxyPort,
2038
            'proxy_user' => $proxyUsername,
2039
            'request_charset_encoding' => $this->request_charset_encoding,
2040
            'request_compression' => $this->request_compression,
2041
            'return_type' => $this->return_type,
2042
            'sslversion' => $sslVersion,
2043
            'timeout' => $timeout,
2044
            'username' => $username,
2045
            'user_agent' => $this->user_agent,
2046
            'use_curl' => $this->use_curl,
2047
            'verifyhost' => $this->verifyhost,
2048
            'verifypeer' => $this->verifypeer,
2049
        ));
2050
    }
2051
2052
    // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];`
2053
    public function &__get($name)
2054
    {
2055
        if (in_array($name, static::$options)) {
2056
            $this->logDeprecation('Getting property Client::' . $name . ' is deprecated');
2057
            return $this->$name;
2058
        }
2059
2060
        switch ($name) {
2061
            case 'errno':
2062
            case 'errstr':
2063
            case 'method':
2064
            case 'server':
2065
            case 'port':
2066
            case 'path':
2067
                $this->logDeprecation('Getting property Client::' . $name . ' is deprecated');
2068
                return $this->$name;
2069
            default:
2070
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
2071
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
2072
                trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
2073
                $result = null;
2074
                return $result;
2075
        }
2076
    }
2077
2078
    public function __set($name, $value)
2079
    {
2080
        if (in_array($name, static::$options)) {
2081
            $this->logDeprecation('Setting property Client::' . $name . ' is deprecated');
2082
            $this->$name = $value;
2083
            return;
2084
        }
2085
2086
        switch ($name) {
2087
            case 'errno':
2088
            case 'errstr':
2089
            case 'method':
2090
            case 'server':
2091
            case 'port':
2092
            case 'path':
2093
                $this->logDeprecation('Setting property Client::' . $name . ' is deprecated');
2094
                $this->$name = $value;
2095
                return;
2096
            default:
2097
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
2098
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
2099
                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
2100
        }
2101
    }
2102
2103
    public function __isset($name)
2104
    {
2105
        if (in_array($name, static::$options)) {
2106
            $this->logDeprecation('Checking property Client::' . $name . ' is deprecated');
2107
            return isset($this->$name);
2108
        }
2109
2110
        switch ($name) {
2111
            case 'errno':
2112
            case 'errstr':
2113
            case 'method':
2114
            case 'server':
2115
            case 'port':
2116
            case 'path':
2117
                $this->logDeprecation('Checking property Client::' . $name . ' is deprecated');
2118
                return isset($this->$name);
2119
            default:
2120
                return false;
2121
        }
2122
    }
2123
2124
    public function __unset($name)
2125
    {
2126
        if (in_array($name, static::$options)) {
2127
            $this->logDeprecation('Unsetting property Client::' . $name . ' is deprecated');
2128
            unset($this->$name);
2129
            return;
2130
        }
2131
2132
        switch ($name) {
2133
            case 'errno':
2134
            case 'errstr':
2135
            case 'method':
2136
            case 'server':
2137
            case 'port':
2138
            case 'path':
2139
                $this->logDeprecation('Unsetting property Client::' . $name . ' is deprecated');
2140
                unset($this->$name);
2141
                return;
2142
            default:
2143
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
2144
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
2145
                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
2146
        }
2147
    }
2148
}
2149