Passed
Push — master ( 8cdf44...424db6 )
by Gaetano
08:49
created

Client::__construct()   F

Complexity

Conditions 21
Paths 2064

Size

Total Lines 66
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 21.1308

Importance

Changes 0
Metric Value
cc 21
eloc 32
nc 2064
nop 4
dl 0
loc 66
ccs 14
cts 15
cp 0.9333
crap 21.1308
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpXmlRpc;
4
5
use PhpXmlRpc\Helper\Logger;
6
use PhpXmlRpc\Helper\XMLParser;
7
8
/**
9
 * Used to represent a client of an XML-RPC server.
10
 *
11
 * @todo add a setTimeout method, in the vein of other options which can be set to the Client
12
 */
13
class Client
14
{
15
    const USE_CURL_NEVER = 0;
16
    const USE_CURL_ALWAYS = 1;
17
    const USE_CURL_AUTO = 2;
18
19
    protected static $logger;
20
21
    /// @todo: do these need to be public?
22
    public $method = 'http';
23
    public $server;
24
    public $port = 0;
25
    public $path;
26
27
    public $errno;
28
    public $errstr;
29
    public $debug = 0;
30
31
    public $username = '';
32
    public $password = '';
33
    public $authtype = 1;
34
35
    public $cert = '';
36
    public $certpass = '';
37
    public $cacert = '';
38
    public $cacertdir = '';
39
    public $key = '';
40
    public $keypass = '';
41
    public $verifypeer = true;
42
    public $verifyhost = 2;
43
    public $sslversion = 0; // corresponds to CURL_SSLVERSION_DEFAULT
44
45
    public $proxy = '';
46
    public $proxyport = 0;
47
    public $proxy_user = '';
48
    public $proxy_pass = '';
49
    public $proxy_authtype = 1;
50
51
    public $cookies = array();
52
    public $extracurlopts = array();
53
54
    /**
55
     * @var int
56
     */
57
    public $timeout = 0;
58
59
    /**
60
     * @var int
61
     */
62
    public $use_curl = self::USE_CURL_AUTO;
63
64
    /**
65
     * @var bool
66
     *
67
     * This determines whether the multicall() method will try to take advantage of the system.multicall xmlrpc method
68
     * to dispatch to the server an array of requests in a single http roundtrip or simply execute many consecutive http
69
     * calls. Defaults to FALSE, but it will be enabled automatically on the first failure of execution of
70
     * system.multicall.
71
     */
72
    public $no_multicall = false;
73
74
    /**
75
     * @var array
76
     *
77
     * List of http compression methods accepted by the client for responses.
78
     * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib.
79
     *
80
     * 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
81
     * decide the compression methods it supports. You might check for the presence of 'zlib' in the output of
82
     * curl_version() to determine whether compression is supported or not
83
     */
84
    public $accepted_compression = array();
85
86
    /**
87
     * @var string|null
88
     *
89
     * Name of compression scheme to be used for sending requests.
90
     * Either null, 'gzip' or 'deflate'.
91
     */
92
    public $request_compression = '';
93
94
    /**
95
     * CURL handle: used for keep-alive
96
     * @internal
97
     */
98
    public $xmlrpc_curl_handle = null;
99
100
    /**
101
     * @var bool
102
     *
103
     * Whether to use persistent connections for http 1.1 and https. Value set at constructor time
104
     */
105
    public $keepalive = false;
106
107
    /**
108
     * @var string[]
109
     *
110
     * Charset encodings that can be decoded without problems by the client. Value set at constructor time
111
     */
112
    public $accepted_charset_encodings = array();
113
114
    /**
115
     * @var string
116
     *
117
     * The charset encoding that will be used for serializing request sent by the client.
118
     * It defaults to NULL, which means using US-ASCII and encoding all characters outside the ASCII printable range
119
     * using their xml character entity representation (this has the benefit that line end characters will not be mangled
120
     * in the transfer, a CR-LF will be preserved as well as a singe LF).
121
     * Valid values are 'US-ASCII', 'UTF-8' and 'ISO-8859-1'.
122 2
     * For the fastest mode of operation, set your both your app internal encoding and this to UTF-8.
123
     */
124 2
    public $request_charset_encoding = '';
125 2
126
    /**
127 2
     * @var string
128
     *
129
     * Decides the content of Response objects returned by calls to send() and multicall().
130
     * Valid values are 'xmlrpcvals', 'phpvals' or 'xml'.
131
     *
132
     * Determines whether the value returned inside a Response object as results of calls to the send() and multicall()
133
     * methods will be a Value object, a plain php value or a raw xml string.
134
     * Allowed values are 'xmlrpcvals' (the default), 'phpvals' and 'xml'.
135
     * To allow the user to differentiate between a correct and a faulty response, fault responses will be returned as
136
     * Response objects in any case.
137
     * Note that the 'phpvals' setting will yield faster execution times, but some of the information from the original
138
     * response will be lost. It will be e.g. impossible to tell whether a particular php string value was sent by the
139
     * server as an xmlrpc string or base64 value.
140
     */
141
    public $return_type = XMLParser::RETURN_XMLRPCVALS;
142
143
    /**
144
     * @var string
145
     *
146
     * Sent to servers in http headers. Value set at constructor time.
147
     */
148
    public $user_agent;
149
150
    public function getLogger()
151
    {
152 718
        if (self::$logger === null) {
153
            self::$logger = Logger::instance();
154
        }
155 718
        return self::$logger;
156 7
    }
157 7
158 7
    /**
159 7
     * @param $logger
160
     * @return void
161
     */
162 7
    public static function setLogger($logger)
163
    {
164
        self::$logger = $logger;
165 7
    }
166
167
    /**
168 7
     * @param string $path either the PATH part of the xmlrpc server URL, or complete server URL (in which case you
169 7
     *                     should use and empty string for all other parameters)
170
     *                     e.g. /xmlrpc/server.php
171 7
     *                     e.g. http://phpxmlrpc.sourceforge.net/server.php
172
     *                     e.g. https://james:[email protected]:444/xmlrpcserver?agent=007
173
     *                     e.g. h2://fast-and-secure-services.org/endpoint
174 7
     * @param string $server the server name / ip address
175
     * @param integer $port the port the server is listening on, when omitted defaults to 80 or 443 depending on
176
     *                      protocol used
177
     * @param string $method the http protocol variant: defaults to 'http'; 'https', 'http11', 'h2' and 'h2c' can
178 718
     *                       be used if CURL is installed. The value set here can be overridden in any call to $this->send().
179
     *                       Use 'h2' to make the lib attempt to use http/2 over a secure connection, and 'h2c'
180
     *                       for http/2 without tls. Note that 'h2c' will not use the h2c 'upgrade' method, and be
181 718
     *                       thus incompatible with any server/proxy not supporting http/2. This is because POST
182
     *                       request are not compatible with h2c upgrade.
183 718
     */
184 718
    public function __construct($path, $server = '', $port = '', $method = '')
185 3
    {
186
        // allow user to specify all params in $path
187 718
        if ($server == '' && $port == '' && $method == '') {
188 7
            $parts = parse_url($path);
189
            $server = $parts['host'];
190
            $path = isset($parts['path']) ? $parts['path'] : '';
191
            if (isset($parts['query'])) {
192 718
                $path .= '?' . $parts['query'];
193
            }
194 711
            if (isset($parts['fragment'])) {
195
                $path .= '#' . $parts['fragment'];
196
            }
197 718
            if (isset($parts['port'])) {
198
                $port = $parts['port'];
199
            }
200
            if (isset($parts['scheme'])) {
201 718
                $method = $parts['scheme'];
202
            }
203
            if (isset($parts['user'])) {
204 718
                $this->username = $parts['user'];
205
            }
206
            if (isset($parts['pass'])) {
207
                $this->password = $parts['pass'];
208
            }
209
        }
210
        if ($path == '' || $path[0] != '/') {
211
            $this->path = '/' . $path;
212
        } else {
213
            $this->path = $path;
214
        }
215
        $this->server = $server;
216
        if ($port != '') {
217 718
            $this->port = $port;
218 718
        }
219
        if ($method != '') {
220
            $this->method = $method;
221
        }
222
223
        // if ZLIB is enabled, let the client by default accept compressed responses
224
        if (function_exists('gzinflate') || (
225
                function_exists('curl_version') && (($info = curl_version()) &&
226
                    ((is_string($info) && strpos($info, 'zlib') !== null) || isset($info['libz_version'])))
227
            )
228
        ) {
229
            $this->accepted_compression = array('gzip', 'deflate');
230
        }
231
232
        // keepalives: enabled by default
233 714
        $this->keepalive = true;
234
235 714
        // by default the xml parser can support these 3 charset encodings
236 714
        $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII');
237
238
        // Add all charsets which mbstring can handle, but remove junk not found in IANA registry at
239
        // http://www.iana.org/assignments/character-sets/character-sets.xhtml
240
        // NB: this is disabled to avoid making all the requests sent huge... mbstring supports more than 80 charsets!
241
        /*if (function_exists('mb_list_encodings')) {
242
243
            $encodings = array_diff(mb_list_encodings(), array('pass', 'auto', 'wchar', 'BASE64', 'UUENCODE', 'ASCII',
244
                'HTML-ENTITIES', 'Quoted-Printable', '7bit','8bit', 'byte2be', 'byte2le', 'byte4be', 'byte4le'));
245
            $this->accepted_charset_encodings = array_unique(array_merge($this->accepted_charset_encodings, $encodings));
246
        }*/
247
248
        // initialize user_agent string
249
        $this->user_agent = PhpXmlRpc::$xmlrpcName . ' ' . PhpXmlRpc::$xmlrpcVersion;
250
    }
251
252 66
    /**
253
     * Enable/disable the echoing to screen of the xmlrpc responses received. The default is not to output anything.
254 66
     *
255 66
     * The debugging information at level 1 includes the raw data returned from the XML-RPC server it was querying
256 66
     * (including bot HTTP headers and the full XML payload), and the PHP value the client attempts to create to
257 66
     * represent the value returned by the server.
258
     * At level2, the complete payload of the xmlrpc request is also printed, before being sent to the server.
259
     *
260
     * This option can be very useful when debugging servers as it allows you to see exactly what the client sends and
261
     * the server returns. Never leave it enabled for production!
262
     *
263
     * @param integer $level values 0, 1 and 2 are supported (2 = echo sent msg too, before received response)
264
     * @return $this
265
     */
266
    public function setDebug($level)
267
    {
268
        $this->debug = $level;
269
        return $this;
270
    }
271
272
    /**
273
     * Sets the username and password for authorizing the client to the server.
274
     *
275
     * With the default (HTTP) transport, this information is used for HTTP Basic authorization.
276
     * Note that username and password can also be set using the class constructor.
277
     * With HTTP 1.1 and HTTPS transport, NTLM and Digest authentication protocols are also supported. To enable them use
278
     * the constants CURLAUTH_DIGEST and CURLAUTH_NTLM as values for the auth type parameter.
279
     *
280
     * @param string $user username
281
     * @param string $password password
282
     * @param integer $authType auth type. See curl_setopt man page for supported auth types. Defaults to CURLAUTH_BASIC
283
     *                          (basic auth). Note that auth types NTLM and Digest will only work if the Curl php
284
     *                          extension is enabled.
285
     * @return $this
286
     */
287
    public function setCredentials($user, $password, $authType = 1)
288
    {
289
        $this->username = $user;
290
        $this->password = $password;
291
        $this->authtype = $authType;
292
        return $this;
293
    }
294
295
    /**
296
     * Set the optional certificate and passphrase used in SSL-enabled communication with a remote server.
297
     *
298
     * Note: to retrieve information about the client certificate on the server side, you will need to look into the
299
     * environment variables which are set up by the webserver. Different webservers will typically set up different
300
     * variables.
301
     *
302
     * @param string $cert the name of a file containing a PEM formatted certificate
303
     * @param string $certPass the password required to use it
304
     * @return $this
305
     */
306
    public function setCertificate($cert, $certPass = '')
307
    {
308
        $this->cert = $cert;
309
        $this->certpass = $certPass;
310
        return $this;
311
    }
312
313
    /**
314
     * Add a CA certificate to verify server with in SSL-enabled communication when SetSSLVerifypeer has been set to TRUE.
315
     *
316 132
     * See the php manual page about CURLOPT_CAINFO for more details.
317
     *
318 132
     * @param string $caCert certificate file name (or dir holding certificates)
319 132
     * @param bool $isDir set to true to indicate cacert is a dir. defaults to false
320
     * @return $this
321
     */
322
    public function setCaCertificate($caCert, $isDir = false)
323
    {
324
        if ($isDir) {
325
            $this->cacertdir = $caCert;
326
        } else {
327
            $this->cacert = $caCert;
328 132
        }
329
        return $this;
330 132
    }
331 132
332
    /**
333
     * Set attributes for SSL communication: private SSL key.
334
     *
335
     * NB: does not work in older php/curl installs.
336
     * Thanks to Daniel Convissor.
337
     *
338 132
     * @param string $key The name of a file containing a private SSL key
339
     * @param string $keyPass The secret password needed to use the private SSL key
340 132
     * @return $this
341 132
     */
342
    public function setKey($key, $keyPass)
343
    {
344
        $this->key = $key;
345
        $this->keypass = $keyPass;
346
        return $this;
347
    }
348
349
    /**
350
     * Set attributes for SSL communication: verify the remote host's SSL certificate, and cause the connection to fail
351
     * if the cert verification fails.
352
     *
353
     * By default, verification is enabled.
354
     * To specify custom SSL certificates to validate the server with, use the setCaCertificate method.
355 99
     *
356
     * @param bool $i enable/disable verification of peer certificate
357 99
     * @return $this
358 99
     */
359 99
    public function setSSLVerifyPeer($i)
360 99
    {
361 99
        $this->verifypeer = $i;
362 99
        return $this;
363
    }
364
365
    /**
366
     * Set attributes for SSL communication: verify the remote host's SSL certificate's common name (CN).
367
     *
368
     * Note that support for value 1 has been removed in cURL 7.28.1
369
     *
370
     * @param int $i Set to 1 to only the existence of a CN, not that it matches
371
     * @return $this
372
     */
373
    public function setSSLVerifyHost($i)
374
    {
375
        $this->verifyhost = $i;
376
        return $this;
377
    }
378
379
    /**
380
     * Set attributes for SSL communication: SSL version to use. Best left at 0 (default value): let cURL decide
381
     *
382
     * @param int $i
383
     * @return $this
384
     */
385
    public function setSSLVersion($i)
386
    {
387
        $this->sslversion = $i;
388
        return $this;
389
    }
390
391
    /**
392
     * Set proxy info.
393
     *
394
     * NB: CURL versions before 7.11.10 cannot use a proxy to communicate with https servers.
395
     *
396
     * @param string $proxyHost
397
     * @param string $proxyPort Defaults to 8080 for HTTP and 443 for HTTPS
398
     * @param string $proxyUsername Leave blank if proxy has public access
399
     * @param string $proxyPassword Leave blank if proxy has public access
400
     * @param int $proxyAuthType defaults to CURLAUTH_BASIC (Basic authentication protocol); set to constant CURLAUTH_NTLM
401
     *                           to use NTLM auth with proxy (has effect only when the client uses the HTTP 1.1 protocol)
402
     * @return $this
403
     */
404
    public function setProxy($proxyHost, $proxyPort, $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1)
405
    {
406
        $this->proxy = $proxyHost;
407
        $this->proxyport = $proxyPort;
408
        $this->proxy_user = $proxyUsername;
409
        $this->proxy_pass = $proxyPassword;
410
        $this->proxy_authtype = $proxyAuthType;
411
        return $this;
412
    }
413
414
    /**
415
     * Enables/disables reception of compressed xmlrpc responses.
416
     *
417
     * This requires the "zlib" extension to be enabled in your php install. If it is, by default xmlrpc_client
418 580
     * instances will enable reception of compressed content.
419
     * Note that enabling reception of compressed responses merely adds some standard http headers to xmlrpc requests.
420 580
     * It is up to the xmlrpc server to return compressed responses when receiving such requests.
421 580
     *
422
     * @param string $compMethod either 'gzip', 'deflate', 'any' or ''
423
     * @return $this
424
     */
425
    public function setAcceptedCompression($compMethod)
426
    {
427 580
        if ($compMethod == 'any') {
428
            $this->accepted_compression = array('gzip', 'deflate');
429 580
        } 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...
430
            $this->accepted_compression = array();
431
        } else {
432
            $this->accepted_compression = array($compMethod);
433
        }
434
        return $this;
435
    }
436
437
    /**
438
     * Enables/disables http compression of xmlrpc request.
439
     *
440
     * This requires the "zlib" extension to be enabled in your php install.
441
     * Take care when sending compressed requests: servers might not support them (and automatic fallback to
442
     * uncompressed requests is not yet implemented).
443
     *
444
     * @param string $compMethod either 'gzip', 'deflate' or ''
445
     * @return $this
446 66
     */
447
    public function setRequestCompression($compMethod)
448 66
    {
449 66
        $this->request_compression = $compMethod;
450
        return $this;
451
    }
452
453
    /**
454
     * Adds a cookie to list of cookies that will be sent to server with every further request (useful e.g. for keeping
455
     * session info outside of the xml-rpc payload).
456
     *
457
     * NB: By default cookies are sent using the 'original/netscape' format, which is also the same as the RFC 2965;
458
     * setting any param but name and value will turn the cookie into a 'version 1' cookie (i.e. RFC 2109 cookie) that
459
     * might not be fully supported by the server. Note that RFC 2109 has currently 'historic' status...
460
     *
461
     * @param string $name nb: will not be escaped in the request's http headers. Take care not to use CTL chars or
462
     *                     separators!
463
     * @param string $value
464
     * @param string $path leave this empty unless the xml-rpc server only accepts RFC 2109 cookies
465
     * @param string $domain leave this empty unless the xml-rpc server only accepts RFC 2109 cookies
466
     * @param int $port leave this empty unless the xml-rpc server only accepts RFC 2109 cookies
467
     * @return $this
468
     *
469
     * @todo check correctness of urlencoding cookie value (copied from php way of doing it, but php is generally sending
470
     *       response not requests. We do the opposite...)
471
     * @todo strip invalid chars from cookie name? As per RFC6265, we should follow RFC2616, Section 2.2
472
     */
473
    public function setCookie($name, $value = '', $path = '', $domain = '', $port = null)
474
    {
475
        $this->cookies[$name]['value'] = rawurlencode($value);
476
        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...
477
            $this->cookies[$name]['path'] = $path;
478
            $this->cookies[$name]['domain'] = $domain;
479
            $this->cookies[$name]['port'] = $port;
480
            $this->cookies[$name]['version'] = 1;
481
        } else {
482
            $this->cookies[$name]['version'] = 0;
483
        }
484
        return $this;
485
    }
486
487
    /**
488
     * Directly set cURL options, for extra flexibility (when in cURL mode).
489
     *
490
     * It allows eg. to bind client to a specific IP interface / address.
491
     *
492
     * @param array $options
493
     * @return $this
494
     */
495 697
    public function setCurlOptions($options)
496
    {
497
        $this->extracurlopts = $options;
498
        return $this;
499 697
    }
500 117
501
    /**
502
     * @param int $useCurlMode self::USE_CURL_ALWAYS, self::USE_CURL_AUTO or self::USE_CURL_NEVER
503 697
     * @return $this
504
     */
505 66
    public function setUseCurl($useCurlMode)
506
    {
507 66
        $this->use_curl = $useCurlMode;
508 697
        return $this;
509 28
    }
510 28
511 28
512
    /**
513
     * Set user-agent string that will be used by this client instance in http headers sent to the server.
514
     *
515 697
     * The default user agent string includes the name of this library and the version number.
516
     *
517
     * @param string $agentString
518
     * @return $this
519 697
     */
520 697
    public function setUserAgent($agentString)
521
    {
522 697
        $this->user_agent = $agentString;
523 322
        return $this;
524 322
    }
525 322
526 322
    /**
527
     * @param int $timeout
528 322
     * @return $this
529 322
     */
530 322
    public function setTimeout($timeout)
531 322
    {
532 322
        $this->timeout = $timeout;
533 322
        return $this;
534 322
    }
535 322
536 322
    /**
537 322
     * Send an xmlrpc request to the server.
538 322
     *
539 322
     * @param Request|Request[]|string $req The Request object, or an array of requests for using multicall, or the
540
     *                                      complete xml representation of a request.
541 322
     *                                      When sending an array of Request objects, the client will try to make use of
542 322
     *                                      a single 'system.multicall' xml-rpc method call to forward to the server all
543 322
     *                                      the requests in a single HTTP round trip, unless $this->no_multicall has
544 322
     *                                      been previously set to TRUE (see the multicall method below), in which case
545 322
     *                                      many consecutive xmlrpc requests will be sent. The method will return an
546
     *                                      array of Response objects in both cases.
547
     *                                      The third variant allows to build by hand (or any other means) a complete
548
     *                                      xmlrpc request message, and send it to the server. $req should be a string
549 375
     *                                      containing the complete xml representation of the request. It is e.g. useful
550 375
     *                                      when, for maximal speed of execution, the request is serialized into a
551 375
     *                                      string using the native php xmlrpc functions (see http://www.php.net/xmlrpc)
552 375
     * @param integer $timeout deprecated. Connection timeout, in seconds, If unspecified, the timeout set with setTimeout
553
     *                         will be used. If that is 0, a platform specific timeout will apply.
554 375
     *                         This timeout value is passed to fsockopen(). It is also used for detecting server
555 375
     *                         timeouts during communication (i.e. if the server does not send anything to the client
556 375
     *                         for $timeout seconds, the connection will be closed).
557 375
     * @param string $method deprecated. Use the same value in the constructor instead.
558 375
     *                       Valid values are 'http', 'http11', 'https', 'h2' and 'h2c'. If left empty,
559 375
     *                       the http protocol chosen during creation of the object will be used.
560 375
     *                       Use 'h2' to make the lib attempt to use http/2 over a secure connection, and 'h2c'
561 375
     *                       for http/2 without tls. Note that 'h2c' will not use the h2c 'upgrade' method, and be
562 375
     *                       thus incompatible with any server/proxy not supporting http/2. This is because POST
563 375
     *                       request are not compatible with h2c upgrade.
564 375
     * @return Response|Response[] Note that the client will always return a Response object, even if the call fails
565 375
     *
566
     * @todo allow throwing exceptions instead of returning responses in case of failed calls and/or Fault responses
567 375
     * @todo refactor: we now support many options besides connection timeout and http version to use. Why only privilege those?
568 375
     */
569 375
    public function send($req, $timeout = 0, $method = '')
570
    {
571
        // if user does not specify http protocol, use native method of this client
572
        // (i.e. method set during call to constructor)
573 697
        if ($method == '') {
574
            $method = $this->method;
575
        }
576
577
        if ($timeout == 0) {
578
            $timeout = $this->timeout;
579
        }
580
581
        if (is_array($req)) {
582
            // $req is an array of Requests
583
            $r = $this->multicall($req, $timeout, $method);
584
585
            return $r;
586
        } elseif (is_string($req)) {
587
            $n = new Request('');
588
            $n->payload = $req;
589
            $req = $n;
590
        }
591
592
        // where req is a Request
593
        $req->setDebug($this->debug);
594
595
        /// @todo we could be smarter about this and force usage of curl in scenarios where it is both available and
596
        ///       needed, such as digest or ntlm auth. Do not attempt to use it for https if not present
597
        $useCurl = ($this->use_curl == self::USE_CURL_ALWAYS) || ($this->use_curl == self::USE_CURL_AUTO &&
598
            (in_array($method, array('https', 'http11', 'h2c', 'h2'))));
599
600
        if ($useCurl) {
601
            $r = $this->sendPayloadCURL(
602
                $req,
603
                $this->server,
604
                $this->port,
605
                $timeout,
606
                $this->username,
607
                $this->password,
608
                $this->authtype,
609
                $this->cert,
610
                $this->certpass,
611
                $this->cacert,
612
                $this->cacertdir,
613
                $this->proxy,
614
                $this->proxyport,
615
                $this->proxy_user,
616
                $this->proxy_pass,
617
                $this->proxy_authtype,
618
                // bc
619
                $method == 'http11' ? 'http' : $method,
620
                $this->keepalive,
621
                $this->key,
622
                $this->keypass,
623
                $this->sslversion
624
            );
625
        } else {
626
            $r = $this->sendPayloadSocket(
627
                $req,
628
                $this->server,
629
                $this->port,
630
                $timeout,
631
                $this->username,
632
                $this->password,
633
                $this->authtype,
634
                $this->cert,
635
                $this->certpass,
636
                $this->cacert,
637
                $this->cacertdir,
638
                $this->proxy,
639
                $this->proxyport,
640
                $this->proxy_user,
641
                $this->proxy_pass,
642
                $this->proxy_authtype,
643
                $method,
644
                $this->key,
645
                $this->keypass,
646
                $this->sslversion
647
            );
648
        }
649
650
        return $r;
651
    }
652
653
    /**
654
     * @deprecated
655
     * @param Request $req
656
     * @param string $server
657
     * @param int $port
658
     * @param int $timeout
659
     * @param string $username
660
     * @param string $password
661
     * @param int $authType
662
     * @param string $proxyHost
663
     * @param int $proxyPort
664 375
     * @param string $proxyUsername
665
     * @param string $proxyPassword
666
     * @param int $proxyAuthType
667
     * @param string $method
668
     * @return Response
669
     */
670
    protected function sendPayloadHTTP10($req, $server, $port, $timeout = 0, $username = '', $password = '',
671 375
        $authType = 1, $proxyHost = '', $proxyPort = 0, $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1,
672 373
        $method='http')
673
    {
674
        //trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
675
676 375
        return $this->sendPayloadSocket($req, $server, $port, $timeout, $username, $password, $authType, null, null,
677 346
            null, null, $proxyHost, $proxyPort, $proxyUsername, $proxyPassword, $proxyAuthType, $method);
678
    }
679
680 375
    /**
681
     * @deprecated
682 375
     * @param Request $req
683 375
     * @param string $server
684 64
     * @param int $port
685 32
     * @param int $timeout
686 32
     * @param string $username
687 32
     * @param string $password
688 32
     * @param int $authType
689
     * @param string $cert
690
     * @param string $certPass
691 32
     * @param string $caCert
692 32
     * @param string $caCertDir
693 32
     * @param string $proxyHost
694 32
     * @param int $proxyPort
695
     * @param string $proxyUsername
696
     * @param string $proxyPassword
697
     * @param int $proxyAuthType
698
     * @param bool $keepAlive
699
     * @param string $key
700 375
     * @param string $keyPass
701 375
     * @param int $sslVersion
702 32
     * @return Response
703 32
     */
704
    protected function sendPayloadHTTPS($req, $server, $port, $timeout = 0, $username = '',  $password = '',
705
        $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0,
706
        $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $keepAlive = false, $key = '', $keyPass = '',
707
        $sslVersion = 0)
708 375
    {
709 375
        //trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
710 73
711
        return $this->sendPayloadCURL($req, $server, $port, $timeout, $username,
712
            $password, $authType, $cert, $certPass, $caCert, $caCertDir, $proxyHost, $proxyPort,
713 375
            $proxyUsername, $proxyPassword, $proxyAuthType, 'https', $keepAlive, $key, $keyPass, $sslVersion);
714 375
    }
715 32
716
    /**
717
     * @param Request $req
718 32
     * @param string $server
719 32
     * @param int $port
720 32
     * @param int $timeout
721 32
     * @param string $username
722 32
     * @param string $password
723
     * @param int $authType only value supported is 1
724
     * @param string $cert
725
     * @param string $certPass
726 32
     * @param string $caCert
727
     * @param string $caCertDir
728
     * @param string $proxyHost
729 343
     * @param int $proxyPort
730 343
     * @param string $proxyUsername
731 343
     * @param string $proxyPassword
732 343
     * @param int $proxyAuthType only value supported is 1
733
     * @param string $method 'http' (synonym for 'http10'), 'http10' or 'https'
734
     * @param string $key
735
     * @param string $keyPass @todo not implemented yet.
736 375
     * @param int $sslVersion @todo not implemented yet. See http://php.net/manual/en/migration56.openssl.php
737 375
     * @return Response
738 309
     *
739 309
     * @todo refactor: we get many options for the call passed in, but some we use from $this. We should clean that up
740 309
     */
741
    protected function sendPayloadSocket($req, $server, $port, $timeout = 0, $username = '', $password = '',
742
        $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0,
743
        $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method='http', $key = '', $keyPass = '',
0 ignored issues
show
Unused Code introduced by
The parameter $keyPass 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

743
        $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method='http', $key = '', /** @scrutinizer ignore-unused */ $keyPass = '',

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...
744
        $sslVersion = 0)
0 ignored issues
show
Unused Code introduced by
The parameter $sslVersion 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

744
        /** @scrutinizer ignore-unused */ $sslVersion = 0)

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...
745
    {
746
        /// @todo log a warning if passed an unsupported method
747
748
        if ($port == 0) {
749
            $port = ($method === 'https') ? 443 : 80;
750
        }
751
752
        // Only create the payload if it was not created previously
753 309
        if (empty($req->payload)) {
754
            $req->serialize($this->request_charset_encoding);
755
        }
756 309
757
        $payload = $req->payload;
758
        // Deflate request body and set appropriate request headers
759
        $encodingHdr = '';
760 375
        if (function_exists('gzdeflate') && ($this->request_compression == 'gzip' || $this->request_compression == 'deflate')) {
761 375
            if ($this->request_compression == 'gzip') {
762
                $a = @gzencode($payload);
763
                if ($a) {
764
                    $payload = $a;
765
                    $encodingHdr = "Content-Encoding: gzip\r\n";
766 375
                }
767 375
            } else {
768 375
                $a = @gzcompress($payload);
769 375
                if ($a) {
770 375
                    $payload = $a;
771 375
                    $encodingHdr = "Content-Encoding: deflate\r\n";
772 375
                }
773 375
            }
774 375
        }
775 375
776 375
        // thanks to Grant Rauscher <[email protected]> for this
777 375
        $credentials = '';
778
        if ($username != '') {
779 375
            $credentials = 'Authorization: Basic ' . base64_encode($username . ':' . $password) . "\r\n";
780 2
            if ($authType != 1) {
781
                /// @todo make this a proper error, ie. return a failure
782
                $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth is supported with HTTP 1.0');
783 375
            }
784 375
        }
785 32
786
        $acceptedEncoding = '';
787
        if (is_array($this->accepted_compression) && count($this->accepted_compression)) {
788
            $acceptedEncoding = 'Accept-Encoding: ' . implode(', ', $this->accepted_compression) . "\r\n";
789
        }
790
791 32
        $proxyCredentials = '';
792
        if ($proxyHost) {
793
            if ($proxyPort == 0) {
794 32
                $proxyPort = 8080;
795
            }
796
            $connectServer = $proxyHost;
797 32
            $connectPort = $proxyPort;
798
            $transport = 'tcp';
799
            /// @todo check: should we not use https in some cases?
800 32
            $uri = 'http://' . $server . ':' . $port . $this->path;
801 32
            if ($proxyUsername != '') {
802
                if ($proxyAuthType != 1) {
803
                    /// @todo make this a proper error, ie. return a failure
804 375
                    $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth to proxy is supported with HTTP 1.0');
805
                }
806 375
                $proxyCredentials = 'Proxy-Authorization: Basic ' . base64_encode($proxyUsername . ':' . $proxyPassword) . "\r\n";
807 64
            }
808
        } else {
809 311
            $connectServer = $server;
810
            $connectPort = $port;
811
            $transport = ($method === 'https') ? 'tls' : 'tcp';
812 375
            $uri = $this->path;
813 375
        }
814
815 375
        // Cookie generation, as per rfc2965 (version 1 cookies) or netscape's rules (version 0 cookies)
816 375
        $cookieHeader = '';
817 375
        if (count($this->cookies)) {
818 374
            $version = '';
819 367
            foreach ($this->cookies as $name => $cookie) {
820
                if ($cookie['version']) {
821
                    $version = ' $Version="' . $cookie['version'] . '";';
822 1
                    $cookieHeader .= ' ' . $name . '="' . $cookie['value'] . '";';
823
                    if ($cookie['path']) {
824
                        $cookieHeader .= ' $Path="' . $cookie['path'] . '";';
825
                    }
826
                    if ($cookie['domain']) {
827 1
                        $cookieHeader .= ' $Domain="' . $cookie['domain'] . '";';
828 1
                    }
829
                    if ($cookie['port']) {
830 1
                        $cookieHeader .= ' $Port="' . $cookie['port'] . '";';
831
                    }
832
                } else {
833 374
                    $cookieHeader .= ' ' . $name . '=' . $cookie['value'] . ";";
834
                }
835
            }
836
            $cookieHeader = 'Cookie:' . $version . substr($cookieHeader, 0, -1) . "\r\n";
837
        }
838
839
        // omit port if default
840
        if (($port == 80 && in_array($method, array('http', 'http10'))) || ($port == 443 && $method == 'https')) {
841
            $port =  '';
842
        } else {
843 374
            $port = ':' . $port;
844
        }
845
846
        $op = 'POST ' . $uri . " HTTP/1.0\r\n" .
847 374
            'User-Agent: ' . $this->user_agent . "\r\n" .
848 374
            'Host: ' . $server . $port . "\r\n" .
849 374
            $credentials .
850
            $proxyCredentials .
851 374
            $acceptedEncoding .
852
            $encodingHdr .
853 374
            'Accept-Charset: ' . implode(',', $this->accepted_charset_encodings) . "\r\n" .
854
            $cookieHeader .
855
            'Content-Type: ' . $req->content_type . "\r\nContent-Length: " .
856
            strlen($payload) . "\r\n\r\n" .
857
            $payload;
858
859
        if ($this->debug > 1) {
860
            $this->getLogger()->debugMessage("---SENDING---\n$op\n---END---");
861
        }
862
863
        $contextOptions = array();
864
        if ($method == 'https') {
865
            if ($cert != '') {
866
                $contextOptions['ssl']['local_cert'] = $cert;
867
                if ($certPass != '') {
868
                    $contextOptions['ssl']['passphrase'] = $certPass;
869
                }
870
            }
871
            if ($caCert != '') {
872
                $contextOptions['ssl']['cafile'] = $caCert;
873
            }
874
            if ($caCertDir != '') {
875
                $contextOptions['ssl']['capath'] = $caCertDir;
876
            }
877
            if ($key != '') {
878
                $contextOptions['ssl']['local_pk'] = $key;
879
            }
880
            $contextOptions['ssl']['verify_peer'] = $this->verifypeer;
881
            $contextOptions['ssl']['verify_peer_name'] = $this->verifypeer;
882
        }
883
884
        $context = stream_context_create($contextOptions);
885
886 322
        if ($timeout <= 0) {
887
            $connectTimeout = ini_get('default_socket_timeout');
888
        } else {
889
            $connectTimeout = $timeout;
890
        }
891 322
892
        $this->errno = 0;
893
        $this->errstr = '';
894
895 322
        $fp = @stream_socket_client("$transport://$connectServer:$connectPort", $this->errno, $this->errstr, $connectTimeout,
0 ignored issues
show
Bug introduced by
It seems like $connectTimeout can also be of type string; however, parameter $timeout of stream_socket_client() does only seem to accept double|null, 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

895
        $fp = @stream_socket_client("$transport://$connectServer:$connectPort", $this->errno, $this->errstr, /** @scrutinizer ignore-type */ $connectTimeout,
Loading history...
896
            STREAM_CLIENT_CONNECT, $context);
897 96
        if ($fp) {
0 ignored issues
show
introduced by
$fp is of type false|resource, thus it always evaluated to false.
Loading history...
898 96
            if ($timeout > 0) {
899
                stream_set_timeout($fp, $timeout, 0);
900
            }
901
        } else {
902
            if ($this->errstr == '') {
903
                $err = error_get_last();
904 322
                $this->errstr = $err['message'];
905 322
            }
906
907
            $this->errstr = 'Connect error: ' . $this->errstr;
908
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['http_error'], $this->errstr . ' (' . $this->errno . ')');
909
910 322
            return $r;
911
        }
912
913
        if (!fputs($fp, $op, strlen($op))) {
914
            fclose($fp);
915 322
            $this->errstr = 'Write error';
916
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['http_error'], $this->errstr);
917 322
918
            return $r;
919
        }
920
921
        // Close socket before parsing.
922
        // It should yield slightly better execution times, and make easier recursive calls (e.g. to follow http redirects)
923
        $ipd = '';
924
        do {
925
            // shall we check for $data === FALSE?
926
            // as per the manual, it signals an error
927
            $ipd .= fread($fp, 32768);
928
        } while (!feof($fp));
929 322
        fclose($fp);
930
931
        $r = $req->parseResponse($ipd, false, $this->return_type);
932 1
933 1
        return $r;
934 1
    }
935 1
936 1
    /**
937
     * Contributed by Justin Miller <[email protected]>
938
     * Requires curl to be built into PHP
939 322
     * NB: CURL versions before 7.11.10 cannot use proxy to talk to https servers!
940 160
     *
941
     * @param Request $req
942 322
     * @param string $server
943
     * @param int $port
944 322
     * @param int $timeout
945
     * @param string $username
946
     * @param string $password
947
     * @param int $authType
948
     * @param string $cert
949
     * @param string $certPass
950 322
     * @param string $caCert
951
     * @param string $caCertDir
952
     * @param string $proxyHost
953 323
     * @param int $proxyPort
954
     * @param string $proxyUsername
955
     * @param string $proxyPassword
956
     * @param int $proxyAuthType
957
     * @param string $method 'http' (let curl decide), 'http10', 'http11', 'https', 'h2c' or 'h2'
958 323
     * @param bool $keepAlive
959 322
     * @param string $key
960 226
     * @param string $keyPass
961
     * @param int $sslVersion
962 96
     * @return Response
963
     *
964
     * @todo refactor: we get many options for the call passed in, but some we use from $this. We should clean that up
965
     */
966
    protected function sendPayloadCURL($req, $server, $port, $timeout = 0, $username = '', $password = '',
967 323
        $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0,
968 302
        $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method = 'https', $keepAlive = false, $key = '',
969
        $keyPass = '', $sslVersion = 0)
970
    {
971
        if (!function_exists('curl_init')) {
972 323
            $this->errstr = 'CURL unavailable on this install';
973 323
            return new Response(0, PhpXmlRpc::$xmlrpcerr['no_curl'], PhpXmlRpc::$xmlrpcstr['no_curl']);
974 64
        }
975 32
        if ($method == 'https' || $method == 'h2') {
976 32
            // q: what about installs where we get back a string, but curl is linked to other ssl libs than openssl?
977 32
            if (($info = curl_version()) &&
978 32
                ((is_string($info) && strpos($info, 'OpenSSL') === null) || (is_array($info) && !isset($info['ssl_version'])))
0 ignored issues
show
introduced by
The condition is_string($info) is always false.
Loading history...
979
            ) {
980
                $this->errstr = 'SSL unavailable on this install';
981 32
                return new Response(0, PhpXmlRpc::$xmlrpcerr['no_ssl'], PhpXmlRpc::$xmlrpcstr['no_ssl']);
982 32
            }
983 32
        }
984 64
        if (($method == 'h2' && !defined('CURL_HTTP_VERSION_2_0')) ||
985
            ($method == 'h2c' && !defined('CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE'))) {
986
            $this->errstr = 'HTTP/2 unavailable on this install';
987
            return new Response(0, PhpXmlRpc::$xmlrpcerr['no_http2'], PhpXmlRpc::$xmlrpcstr['no_http2']);
988 259
        }
989
990
        $curl = $this->prepareCurlHandle($req, $server, $port, $timeout, $username, $password,
991 323
            $authType, $cert, $certPass, $caCert, $caCertDir, $proxyHost, $proxyPort,
992 323
            $proxyUsername, $proxyPassword, $proxyAuthType, $method, $keepAlive, $key,
993 64
            $keyPass, $sslVersion);
994
995 259
        if (!$curl) {
996 32
            return new Response(0, PhpXmlRpc::$xmlrpcerr['curl_fail'], PhpXmlRpc::$xmlrpcstr['curl_fail'] . ': error during curl initialization. Check php error log for details');
997
        }
998
999 227
        $result = curl_exec($curl);
1000
1001
        if ($this->debug > 1) {
1002 323
            $message = "---CURL INFO---\n";
1003 323
            foreach (curl_getinfo($curl) as $name => $val) {
1004 322
                if (is_array($val)) {
1005
                    $val = implode("\n", $val);
1006
                }
1007 56
                $message .= $name . ': ' . $val . "\n";
1008
            }
1009
            $message .= '---END---';
1010
            $this->getLogger()->debugMessage($message);
1011 323
        }
1012
1013 323
        if (!$result) {
1014
            /// @todo we should use a better check here - what if we get back '' or '0'?
1015
1016
            $this->errstr = 'no response';
1017 323
            $resp = new Response(0, PhpXmlRpc::$xmlrpcerr['curl_fail'], PhpXmlRpc::$xmlrpcstr['curl_fail'] . ': ' . curl_error($curl));
1018
            curl_close($curl);
1019 323
            if ($keepAlive) {
1020
                $this->xmlrpc_curl_handle = null;
1021 323
            }
1022
        } else {
1023
            if (!$keepAlive) {
1024 323
                curl_close($curl);
1025
            }
1026
            $resp = $req->parseResponse($result, true, $this->return_type);
0 ignored issues
show
Bug introduced by
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

1026
            $resp = $req->parseResponse(/** @scrutinizer ignore-type */ $result, true, $this->return_type);
Loading history...
1027
            if ($keepAlive) {
1028 323
                /// @todo if we got back a 302 or 308, we should not reuse the curl handle for later calls
1029
                if ($resp->faultCode() == PhpXmlRpc::$xmlrpcerr['http_error']) {
1030
                    curl_close($curl);
1031 66
                    $this->xmlrpc_curl_handle = null;
1032 64
                }
1033
            }
1034 2
        }
1035
1036
        return $resp;
1037
    }
1038 323
1039
    protected function prepareCurlHandle($req, $server, $port, $timeout = 0, $username = '', $password = '',
1040 323
         $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0,
1041 161
         $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method = 'https', $keepAlive = false, $key = '',
1042
         $keyPass = '', $sslVersion = 0)
1043
    {
1044 323
        if ($port == 0) {
1045 64
            if (in_array($method, array('http', 'http10', 'http11', 'h2c'))) {
1046
                $port = 80;
1047
            } else {
1048
                $port = 443;
1049
            }
1050 323
        }
1051
1052 323
        // Only create the payload if it was not created previously
1053
        if (empty($req->payload)) {
1054 323
            $req->serialize($this->request_charset_encoding);
1055 272
        }
1056
1057
        // Deflate request body and set appropriate request headers
1058 323
        $payload = $req->payload;
1059 323
        $encodingHdr = '';
1060 32
        /// @todo test for existence of proper function, in case of polyfills
1061 32
        if (function_exists('gzdeflate') && ($this->request_compression == 'gzip' || $this->request_compression == 'deflate')) {
1062 291
            if ($this->request_compression == 'gzip') {
1063
                $a = @gzencode($payload);
1064
                if ($a) {
1065 291
                    $payload = $a;
1066 32
                    $encodingHdr = 'Content-Encoding: gzip';
1067 32
                }
1068 259
            } else {
1069 32
                $a = @gzcompress($payload);
1070 32
                if ($a) {
1071
                    $payload = $a;
1072
                    $encodingHdr = 'Content-Encoding: deflate';
1073 323
                }
1074 32
            }
1075 32
        }
1076 32
1077
        if (!$keepAlive || !$this->xmlrpc_curl_handle) {
1078
            if ($method == 'http11' || $method == 'http10' || $method == 'h2c') {
1079
                $protocol = 'http';
1080
            } else {
1081
                if ($method == 'h2') {
1082 323
                    $protocol = 'https';
1083
                } else {
1084 96
                    // http, https
1085
                    $protocol = $method;
1086
                    if (strpos($protocol, ':') !== false) {
1087
                        $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ": warning - attempted hacking attempt?. The curl protocol requested for the call is: '$protocol'");
1088 96
                        return false;
1089
                    }
1090
                }
1091
            }
1092 96
            $curl = curl_init($protocol . '://' . $server . ':' . $port . $this->path);
1093
            if (!$curl) {
1094 96
                return false;
1095
            }
1096
            if ($keepAlive) {
1097 96
                $this->xmlrpc_curl_handle = $curl;
1098
            }
1099
        } else {
1100
            $curl = $this->xmlrpc_curl_handle;
1101 96
        }
1102
1103
        // results into variable
1104
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
1105 96
1106
        if ($this->debug > 1) {
1107
            curl_setopt($curl, CURLOPT_VERBOSE, true);
1108
            /// @todo allow callers to redirect curlopt_stderr to some stream which can be buffered
1109
        }
1110 96
        curl_setopt($curl, CURLOPT_USERAGENT, $this->user_agent);
1111
        // required for XMLRPC: post the data
1112 96
        curl_setopt($curl, CURLOPT_POST, 1);
1113
        // the data
1114
        curl_setopt($curl, CURLOPT_POSTFIELDS, $payload);
1115
1116 323
        // return the header too
1117 64
        curl_setopt($curl, CURLOPT_HEADER, 1);
1118
1119
        // NB: if we set an empty string, CURL will add http header indicating
1120 64
        // ALL methods it is supporting. This is possibly a better option than letting the user tell what curl can / cannot do...
1121 64
        if (is_array($this->accepted_compression) && count($this->accepted_compression)) {
1122
            //curl_setopt($curl, CURLOPT_ENCODING, implode(',', $this->accepted_compression));
1123
            // empty string means 'any supported by CURL' (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
1124
            if (count($this->accepted_compression) == 1) {
1125
                curl_setopt($curl, CURLOPT_ENCODING, $this->accepted_compression[0]);
1126
            } else {
1127
                curl_setopt($curl, CURLOPT_ENCODING, '');
1128
            }
1129
        }
1130
        // extra headers
1131
        $headers = array('Content-Type: ' . $req->content_type, 'Accept-Charset: ' . implode(',', $this->accepted_charset_encodings));
1132
        // if no keepalive is wanted, let the server know it in advance
1133 323
        if (!$keepAlive) {
1134 271
            $headers[] = 'Connection: close';
1135 271
        }
1136 271
        // request compression header
1137
        if ($encodingHdr) {
1138 271
            $headers[] = $encodingHdr;
1139
        }
1140
1141 323
        // Fix the HTTP/1.1 417 Expectation Failed Bug (curl by default adds a 'Expect: 100-continue' header when POST
1142
        // size exceeds 1025 bytes, apparently)
1143
        $headers[] = 'Expect:';
1144
1145 323
        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
1146
        // timeout is borked
1147
        if ($timeout) {
1148
            curl_setopt($curl, CURLOPT_TIMEOUT, $timeout == 1 ? 1 : $timeout - 1);
1149 323
        }
1150
1151
        switch ($method) {
1152
            case 'http10':
1153
                curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
1154
                break;
1155
            case 'http11':
1156
                curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
1157
                break;
1158
            case 'h2c':
1159
                if (defined('CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE')) {
1160
                    curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE);
1161
                } else {
1162
                    /// @todo make this a proper error, ie. return a failure
1163
                    $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': warning. HTTP2 is not supported by the current PHP/curl install');
1164
                }
1165
                break;
1166
            case 'h2':
1167
                curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
1168
                break;
1169
        }
1170
1171
        if ($username && $password) {
1172
            curl_setopt($curl, CURLOPT_USERPWD, $username . ':' . $password);
1173
            if (defined('CURLOPT_HTTPAUTH')) {
1174 66
                curl_setopt($curl, CURLOPT_HTTPAUTH, $authType);
1175
            } elseif ($authType != 1) {
1176 66
                /// @todo make this a proper error, ie. return a failure
1177
                $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth is supported by the current PHP/curl install');
1178
            }
1179 66
        }
1180 45
1181 45
        // note: h2c is http2 without the https. No need to have it in this IF
1182
        if ($method == 'https' || $method == 'h2') {
1183 45
            // set cert file
1184
            if ($cert) {
1185
                curl_setopt($curl, CURLOPT_SSLCERT, $cert);
1186
            }
1187
            // set cert password
1188
            if ($certPass) {
1189
                curl_setopt($curl, CURLOPT_SSLCERTPASSWD, $certPass);
1190
            }
1191
            // whether to verify remote host's cert
1192
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verifypeer);
1193
            // set ca certificates file/dir
1194
            if ($caCert) {
1195
                curl_setopt($curl, CURLOPT_CAINFO, $caCert);
1196
            }
1197
            if ($caCertDir) {
1198
                curl_setopt($curl, CURLOPT_CAPATH, $caCertDir);
1199
            }
1200
            // set key file (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
1201 23
            if ($key) {
1202
                curl_setopt($curl, CURLOPT_SSLKEY, $key);
1203
            }
1204 23
            // set key password (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
1205 23
            if ($keyPass) {
1206
                curl_setopt($curl, CURLOPT_SSLKEYPASSWD, $keyPass);
1207
            }
1208
            // whether to verify cert's common name (CN); 0 for no, 1 to verify that it exists, and 2 to verify that
1209 23
            // it matches the hostname used
1210 23
            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $this->verifyhost);
1211
            // allow usage of different SSL versions
1212
            curl_setopt($curl, CURLOPT_SSLVERSION, $sslVersion);
1213
        }
1214
1215
        // proxy info
1216
        if ($proxyHost) {
1217
            if ($proxyPort == 0) {
1218
                $proxyPort = 8080; // NB: even for HTTPS, local connection is on port 8080
1219
            }
1220
            curl_setopt($curl, CURLOPT_PROXY, $proxyHost . ':' . $proxyPort);
1221 23
            if ($proxyUsername) {
1222
                curl_setopt($curl, CURLOPT_PROXYUSERPWD, $proxyUsername . ':' . $proxyPassword);
1223
                if (defined('CURLOPT_PROXYAUTH')) {
1224
                    curl_setopt($curl, CURLOPT_PROXYAUTH, $proxyAuthType);
1225
                } elseif ($proxyAuthType != 1) {
1226
                    /// @todo make this a proper error, ie. return a failure
1227
                    $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth to proxy is supported by the current PHP/curl install');
1228
                }
1229
            }
1230
        }
1231
1232
        // NB: should we build cookie http headers by hand rather than let CURL do it?
1233
        // the following code does not honour 'expires', 'path' and 'domain' cookie attributes set to client obj the the user...
1234
        if (count($this->cookies)) {
1235 45
            $cookieHeader = '';
1236
            foreach ($this->cookies as $name => $cookie) {
1237
                $cookieHeader .= $name . '=' . $cookie['value'] . '; ';
1238 45
            }
1239 45
            curl_setopt($curl, CURLOPT_COOKIE, substr($cookieHeader, 0, -2));
1240 45
        }
1241 45
1242 45
        foreach ($this->extracurlopts as $opt => $val) {
1243 45
            curl_setopt($curl, $opt, $val);
1244 45
        }
1245
1246 45
        if ($this->debug > 1) {
1247 45
            $this->getLogger()->debugMessage("---SENDING---\n$payload\n---END---");
1248
        }
1249 45
1250 45
        return $curl;
1251
    }
1252
1253 45
    /**
1254
     * Send an array of requests and return an array of responses.
1255 45
     *
1256
     * Unless $this->no_multicall has been set to true, it will try first to use one single xmlrpc call to server method
1257
     * system.multicall, and revert to sending many successive calls in case of failure.
1258
     * This failure is also stored in $this->no_multicall for subsequent calls.
1259
     * Unfortunately, there is no server error code universally used to denote the fact that multicall is unsupported,
1260
     * so there is no way to reliably distinguish between that and a temporary failure.
1261 45
     * If you are sure that server supports multicall and do not want to fallback to using many single calls, set the
1262
     * fourth parameter to FALSE.
1263 45
     *
1264
     * NB: trying to shoehorn extra functionality into existing syntax has resulted
1265 45
     * in pretty much convoluted code...
1266
     *
1267 22
     * @param Request[] $reqs an array of Request objects
1268 22
     * @param integer $timeout deprecated - connection timeout (in seconds). See the details in the docs for the send() method
1269
     * @param string $method deprecated - the http protocol variant to be used. See the details in the docs for the send() method
1270
     * @param boolean $fallback When true, upon receiving an error during multicall, multiple single calls will be
1271 22
     *                         attempted
1272 22
     * @return Response[]
1273
     */
1274
    public function multicall($reqs, $timeout = 0, $method = '', $fallback = true)
1275
    {
1276 22
        if ($method == '') {
1277 22
            $method = $this->method;
1278 22
        }
1279 22
1280
        if (!$this->no_multicall) {
1281
            $results = $this->_try_multicall($reqs, $timeout, $method);
1282 22
            /// @todo how to handle the case of $this->return_type = xml?
1283 22
            if (is_array($results)) {
1284 22
                // System.multicall succeeded
1285
                return $results;
1286
            } else {
1287
                // either system.multicall is unsupported by server, or the call failed for some other reason.
1288 22
                // Feature creep: is there a way to tell apart unsupported multicall from other faults?
1289 22
                if ($fallback) {
1290 22
                    // Don't try it next time...
1291
                    $this->no_multicall = true;
1292 22
                } else {
1293 22
                    $result = $results;
1294
                }
1295
            }
1296 22
        } else {
1297 22
            // override fallback, in case careless user tries to do two
1298
            // opposite things at the same time
1299
            $fallback = true;
1300 22
        }
1301 22
1302
        $results = array();
1303
        if ($fallback) {
1304
            // system.multicall is (probably) unsupported by server: emulate multicall via multiple requests
1305
            /// @todo use curl multi_ functions to make this quicker (see the implementation in the parallel.php demo)
1306
            foreach ($reqs as $req) {
1307 22
                $results[] = $this->send($req, $timeout, $method);
1308
            }
1309
        } else {
1310
            // user does NOT want to fallback on many single calls: since we should always return an array of responses,
1311 24
            // we return an array with the same error repeated n times
1312 24
            foreach ($reqs as $req) {
1313
                $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...
1314
            }
1315 24
        }
1316 24
1317
        return $results;
1318
    }
1319
1320 24
    /**
1321 24
     * Attempt to boxcar $reqs via system.multicall.
1322 24
     *
1323 24
     * @param Request[] $reqs
1324 24
     * @param int $timeout
1325
     * @param string $method
1326
     * @return Response[]|Response a single Response when the call returned a fault / does not conform to what we expect
1327
     *                             from a multicall response
1328 24
     */
1329 24
    private function _try_multicall($reqs, $timeout, $method)
1330 22
    {
1331 22
        // Construct multicall request
1332
        $calls = array();
1333 22
        foreach ($reqs as $req) {
1334
            $call['methodName'] = new Value($req->method(), 'string');
1335
            $numParams = $req->getNumParams();
1336 22
            $params = array();
1337
            for ($i = 0; $i < $numParams; $i++) {
1338 22
                $params[$i] = $req->getParam($i);
1339
            }
1340
            $call['params'] = new Value($params, 'array');
1341 22
            $calls[] = new Value($call, 'struct');
1342 22
        }
1343
        $multiCall = new Request('system.multicall');
1344
        $multiCall->addParam(new Value($calls, 'array'));
1345
1346
        // Attempt RPC call
1347
        $result = $this->send($multiCall, $timeout, $method);
1348 24
1349
        if ($result->faultCode() != 0) {
1350
            // call to system.multicall failed
1351
            return $result;
1352
        }
1353
1354
        // Unpack responses.
1355
        $rets = $result->value();
1356
        $response = array();
1357
1358
        if ($this->return_type == 'xml') {
1359
            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...
1360
                $response[] = new Response($rets, 0, '', 'xml', $result->httpResponse());
1361
            }
1362
1363
        } elseif ($this->return_type == 'phpvals') {
1364
            if (!is_array($rets)) {
0 ignored issues
show
introduced by
The condition is_array($rets) is always false.
Loading history...
1365
                // bad return type from system.multicall
1366
                return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1367
                    PhpXmlRpc::$xmlrpcstr['multicall_error'] . ': not an array', 'phpvals', $result->httpResponse());
1368
            }
1369
            $numRets = count($rets);
1370
            if ($numRets != count($reqs)) {
1371
                // wrong number of return values.
1372
                return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1373
                    PhpXmlRpc::$xmlrpcstr['multicall_error'] . ': incorrect number of responses', 'phpvals',
1374
                    $result->httpResponse());
1375
            }
1376
1377
            for ($i = 0; $i < $numRets; $i++) {
1378
                $val = $rets[$i];
1379
                if (!is_array($val)) {
1380
                    return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1381
                        PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i is not an array or struct",
1382
                        'phpvals', $result->httpResponse());
1383
                }
1384
                switch (count($val)) {
1385
                    case 1:
1386
                        if (!isset($val[0])) {
1387
                            // Bad value
1388
                            return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1389
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has no value",
1390
                                'phpvals', $result->httpResponse());
1391
                        }
1392
                        // Normal return value
1393
                        $response[$i] = new Response($val[0], 0, '', 'phpvals', $result->httpResponse());
1394
                        break;
1395
                    case 2:
1396
                        /// @todo remove usage of @: it is apparently quite slow
1397
                        $code = @$val['faultCode'];
1398
                        if (!is_int($code)) {
1399
                            /// @todo should we check that it is != 0?
1400
                            return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1401
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no faultCode",
1402
                                'phpvals', $result->httpResponse());
1403
                        }
1404
                        $str = @$val['faultString'];
1405
                        if (!is_string($str)) {
1406
                            return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1407
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no FaultString",
1408
                                'phpvals', $result->httpResponse());
1409
                        }
1410
                        $response[$i] = new Response(0, $code, $str, 'phpvals', $result->httpResponse());
1411
                        break;
1412
                    default:
1413
                        return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1414
                            PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has too many items",
1415
                            'phpvals', $result->httpResponse());
1416
                }
1417
            }
1418
1419
        } else {
1420
            // return type == 'xmlrpcvals'
1421
            if ($rets->kindOf() != 'array') {
0 ignored issues
show
Bug introduced by
The method kindOf() does not exist on integer. ( Ignorable by Annotation )

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

1421
            if ($rets->/** @scrutinizer ignore-call */ kindOf() != 'array') {

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

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

Loading history...
1422
                return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1423
                    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 1333. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1424
                    $result->httpResponse());
1425
            }
1426
            $numRets = $rets->count();
1427
            if ($numRets != count($reqs)) {
1428
                // wrong number of return values.
1429
                return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1430
                    PhpXmlRpc::$xmlrpcstr['multicall_error'] . ': incorrect number of responses', 'xmlrpcvals',
1431
                    $result->httpResponse());
1432
            }
1433
1434
            foreach ($rets as $i => $val) {
0 ignored issues
show
Bug introduced by
The expression $rets of type integer is not traversable.
Loading history...
1435
                switch ($val->kindOf()) {
1436
                    case 'array':
1437
                        if ($val->count() != 1) {
1438
                            return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1439
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has too many items",
1440
                                'phpvals', $result->httpResponse());
1441
                        }
1442
                        // Normal return value
1443
                        $response[] = new Response($val[0], 0, '', 'xmlrpcvals', $result->httpResponse());
1444
                        break;
1445
                    case 'struct':
1446
                        if ($val->count() != 2) {
1447
                            return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1448
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has too many items",
1449
                                'phpvals', $result->httpResponse());
1450
                        }
1451
                        /** @var Value $code */
1452
                        $code = $val['faultCode'];
1453
                        if ($code->kindOf() != 'scalar' || $code->scalartyp() != 'int') {
1454
                            return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1455
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no faultCode",
1456
                                'xmlrpcvals', $result->httpResponse());
1457
                        }
1458
                        /** @var Value $str */
1459
                        $str = $val['faultString'];
1460
                        if ($str->kindOf() != 'scalar' || $str->scalartyp() != 'string') {
1461
                            return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1462
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no faultCode",
1463
                                'xmlrpcvals', $result->httpResponse());
1464
                        }
1465
                        $response[] = new Response(0, $code->scalarval(), $str->scalarval(), 'xmlrpcvals', $result->httpResponse());
1466
                        break;
1467
                    default:
1468
                        return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1469
                            PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i is not an array or struct",
1470
                            'xmlrpcvals', $result->httpResponse());
1471
                }
1472
            }
1473
        }
1474
1475
        return $response;
1476
    }
1477
}
1478