Passed
Push — master ( 549088...8cdf44 )
by Gaetano
06:10
created

Client::setSSLVersion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

707
        $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...
708 375
        $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

708
        /** @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...
709 375
    {
710 73
        /// @todo log a warning if passed an unsupported method
711
712
        if ($port == 0) {
713 375
            $port = ($method === 'https') ? 443 : 80;
714 375
        }
715 32
716
        // Only create the payload if it was not created previously
717
        if (empty($req->payload)) {
718 32
            $req->serialize($this->request_charset_encoding);
719 32
        }
720 32
721 32
        $payload = $req->payload;
722 32
        // Deflate request body and set appropriate request headers
723
        $encodingHdr = '';
724
        if (function_exists('gzdeflate') && ($this->request_compression == 'gzip' || $this->request_compression == 'deflate')) {
725
            if ($this->request_compression == 'gzip') {
726 32
                $a = @gzencode($payload);
727
                if ($a) {
728
                    $payload = $a;
729 343
                    $encodingHdr = "Content-Encoding: gzip\r\n";
730 343
                }
731 343
            } else {
732 343
                $a = @gzcompress($payload);
733
                if ($a) {
734
                    $payload = $a;
735
                    $encodingHdr = "Content-Encoding: deflate\r\n";
736 375
                }
737 375
            }
738 309
        }
739 309
740 309
        // thanks to Grant Rauscher <[email protected]> for this
741
        $credentials = '';
742
        if ($username != '') {
743
            $credentials = 'Authorization: Basic ' . base64_encode($username . ':' . $password) . "\r\n";
744
            if ($authType != 1) {
745
                /// @todo make this a proper error, ie. return a failure
746
                $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth is supported with HTTP 1.0');
747
            }
748
        }
749
750
        $acceptedEncoding = '';
751
        if (is_array($this->accepted_compression) && count($this->accepted_compression)) {
752
            $acceptedEncoding = 'Accept-Encoding: ' . implode(', ', $this->accepted_compression) . "\r\n";
753 309
        }
754
755
        $proxyCredentials = '';
756 309
        if ($proxyHost) {
757
            if ($proxyPort == 0) {
758
                $proxyPort = 8080;
759
            }
760 375
            $connectServer = $proxyHost;
761 375
            $connectPort = $proxyPort;
762
            $transport = 'tcp';
763
            /// @todo check: should we not use https in some cases?
764
            $uri = 'http://' . $server . ':' . $port . $this->path;
765
            if ($proxyUsername != '') {
766 375
                if ($proxyAuthType != 1) {
767 375
                    /// @todo make this a proper error, ie. return a failure
768 375
                    $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth to proxy is supported with HTTP 1.0');
769 375
                }
770 375
                $proxyCredentials = 'Proxy-Authorization: Basic ' . base64_encode($proxyUsername . ':' . $proxyPassword) . "\r\n";
771 375
            }
772 375
        } else {
773 375
            $connectServer = $server;
774 375
            $connectPort = $port;
775 375
            $transport = ($method === 'https') ? 'tls' : 'tcp';
776 375
            $uri = $this->path;
777 375
        }
778
779 375
        // Cookie generation, as per rfc2965 (version 1 cookies) or netscape's rules (version 0 cookies)
780 2
        $cookieHeader = '';
781
        if (count($this->cookies)) {
782
            $version = '';
783 375
            foreach ($this->cookies as $name => $cookie) {
784 375
                if ($cookie['version']) {
785 32
                    $version = ' $Version="' . $cookie['version'] . '";';
786
                    $cookieHeader .= ' ' . $name . '="' . $cookie['value'] . '";';
787
                    if ($cookie['path']) {
788
                        $cookieHeader .= ' $Path="' . $cookie['path'] . '";';
789
                    }
790
                    if ($cookie['domain']) {
791 32
                        $cookieHeader .= ' $Domain="' . $cookie['domain'] . '";';
792
                    }
793
                    if ($cookie['port']) {
794 32
                        $cookieHeader .= ' $Port="' . $cookie['port'] . '";';
795
                    }
796
                } else {
797 32
                    $cookieHeader .= ' ' . $name . '=' . $cookie['value'] . ";";
798
                }
799
            }
800 32
            $cookieHeader = 'Cookie:' . $version . substr($cookieHeader, 0, -1) . "\r\n";
801 32
        }
802
803
        // omit port if default
804 375
        if (($port == 80 && in_array($method, array('http', 'http10'))) || ($port == 443 && $method == 'https')) {
805
            $port =  '';
806 375
        } else {
807 64
            $port = ':' . $port;
808
        }
809 311
810
        $op = 'POST ' . $uri . " HTTP/1.0\r\n" .
811
            'User-Agent: ' . $this->user_agent . "\r\n" .
812 375
            'Host: ' . $server . $port . "\r\n" .
813 375
            $credentials .
814
            $proxyCredentials .
815 375
            $acceptedEncoding .
816 375
            $encodingHdr .
817 375
            'Accept-Charset: ' . implode(',', $this->accepted_charset_encodings) . "\r\n" .
818 374
            $cookieHeader .
819 367
            'Content-Type: ' . $req->content_type . "\r\nContent-Length: " .
820
            strlen($payload) . "\r\n\r\n" .
821
            $payload;
822 1
823
        if ($this->debug > 1) {
824
            $this->getLogger()->debugMessage("---SENDING---\n$op\n---END---");
825
        }
826
827 1
        $contextOptions = array();
828 1
        if ($method == 'https') {
829
            if ($cert != '') {
830 1
                $contextOptions['ssl']['local_cert'] = $cert;
831
                if ($certPass != '') {
832
                    $contextOptions['ssl']['passphrase'] = $certPass;
833 374
                }
834
            }
835
            if ($caCert != '') {
836
                $contextOptions['ssl']['cafile'] = $caCert;
837
            }
838
            if ($caCertDir != '') {
839
                $contextOptions['ssl']['capath'] = $caCertDir;
840
            }
841
            if ($key != '') {
842
                $contextOptions['ssl']['local_pk'] = $key;
843 374
            }
844
            $contextOptions['ssl']['verify_peer'] = $this->verifypeer;
845
            $contextOptions['ssl']['verify_peer_name'] = $this->verifypeer;
846
        }
847 374
848 374
        $context = stream_context_create($contextOptions);
849 374
850
        if ($timeout <= 0) {
851 374
            $connectTimeout = ini_get('default_socket_timeout');
852
        } else {
853 374
            $connectTimeout = $timeout;
854
        }
855
856
        $this->errno = 0;
857
        $this->errstr = '';
858
859
        $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

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

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

1385
            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...
1386
                return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1387
                    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 1297. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1388
                    $result->httpResponse());
1389
            }
1390
            $numRets = $rets->count();
1391
            if ($numRets != count($reqs)) {
1392
                // wrong number of return values.
1393
                return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1394
                    PhpXmlRpc::$xmlrpcstr['multicall_error'] . ': incorrect number of responses', 'xmlrpcvals',
1395
                    $result->httpResponse());
1396
            }
1397
1398
            foreach ($rets as $i => $val) {
0 ignored issues
show
Bug introduced by
The expression $rets of type integer is not traversable.
Loading history...
1399
                switch ($val->kindOf()) {
1400
                    case 'array':
1401
                        if ($val->count() != 1) {
1402
                            return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1403
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has too many items",
1404
                                'phpvals', $result->httpResponse());
1405
                        }
1406
                        // Normal return value
1407
                        $response[] = new Response($val[0], 0, '', 'xmlrpcvals', $result->httpResponse());
1408
                        break;
1409
                    case 'struct':
1410
                        if ($val->count() != 2) {
1411
                            return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1412
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has too many items",
1413
                                'phpvals', $result->httpResponse());
1414
                        }
1415
                        /** @var Value $code */
1416
                        $code = $val['faultCode'];
1417
                        if ($code->kindOf() != 'scalar' || $code->scalartyp() != 'int') {
1418
                            return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1419
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no faultCode",
1420
                                'xmlrpcvals', $result->httpResponse());
1421
                        }
1422
                        /** @var Value $str */
1423
                        $str = $val['faultString'];
1424
                        if ($str->kindOf() != 'scalar' || $str->scalartyp() != 'string') {
1425
                            return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1426
                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no faultCode",
1427
                                'xmlrpcvals', $result->httpResponse());
1428
                        }
1429
                        $response[] = new Response(0, $code->scalarval(), $str->scalarval(), 'xmlrpcvals', $result->httpResponse());
1430
                        break;
1431
                    default:
1432
                        return new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
1433
                            PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i is not an array or struct",
1434
                            'xmlrpcvals', $result->httpResponse());
1435
                }
1436
            }
1437
        }
1438
1439
        return $response;
1440
    }
1441
}
1442