Allow the tests to use a version of SSL which should not conflict with curl/gnutls...
[plcapi.git] / src / Client.php
1 <?php
2
3 namespace PhpXmlRpc;
4
5 class Client
6 {
7     /// @todo: do these need to be public?
8     public $path;
9     public $server;
10     public $port = 0;
11     public $method = 'http';
12     public $errno;
13     public $errstr;
14     public $debug = 0;
15     public $username = '';
16     public $password = '';
17     public $authtype = 1;
18     public $cert = '';
19     public $certpass = '';
20     public $cacert = '';
21     public $cacertdir = '';
22     public $key = '';
23     public $keypass = '';
24     public $verifypeer = true;
25     public $verifyhost = 2;
26     public $sslversion = 0; // corresponds to CURL_SSLVERSION_DEFAULT
27     public $no_multicall = false;
28     public $proxy = '';
29     public $proxyport = 0;
30     public $proxy_user = '';
31     public $proxy_pass = '';
32     public $proxy_authtype = 1;
33     public $cookies = array();
34     public $extracurlopts = array();
35
36     /**
37      * List of http compression methods accepted by the client for responses.
38      * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib.
39      *
40      * NNB: you can set it to any non-empty array for HTTP11 and HTTPS, since
41      * in those cases it will be up to CURL to decide the compression methods
42      * it supports. You might check for the presence of 'zlib' in the output of
43      * curl_version() to determine wheter compression is supported or not
44      */
45     public $accepted_compression = array();
46     /**
47      * Name of compression scheme to be used for sending requests.
48      * Either null, gzip or deflate.
49      */
50     public $request_compression = '';
51     /**
52      * CURL handle: used for keep-alive connections (PHP 4.3.8 up, see:
53      * http://curl.haxx.se/docs/faq.html#7.3).
54      */
55     public $xmlrpc_curl_handle = null;
56     /// Whether to use persistent connections for http 1.1 and https
57     public $keepalive = false;
58     /// Charset encodings that can be decoded without problems by the client
59     public $accepted_charset_encodings = array();
60     /// Charset encoding to be used in serializing request. NULL = use ASCII
61     public $request_charset_encoding = '';
62     /**
63      * Decides the content of Response objects returned by calls to send()
64      * valid strings are 'xmlrpcvals', 'phpvals' or 'xml'.
65      */
66     public $return_type = 'xmlrpcvals';
67     /**
68      * Sent to servers in http headers.
69      */
70     public $user_agent;
71
72     /**
73      * @param string $path either the complete server URL or the PATH part of the xmlrc server URL, e.g. /xmlrpc/server.php
74      * @param string $server the server name / ip address
75      * @param integer $port the port the server is listening on, defaults to 80 or 443 depending on protocol used
76      * @param string $method the http protocol variant: defaults to 'http', 'https' and 'http11' can be used if CURL is installed
77      */
78     public function __construct($path, $server = '', $port = '', $method = '')
79     {
80         // allow user to specify all params in $path
81         if ($server == '' and $port == '' and $method == '') {
82             $parts = parse_url($path);
83             $server = $parts['host'];
84             $path = isset($parts['path']) ? $parts['path'] : '';
85             if (isset($parts['query'])) {
86                 $path .= '?' . $parts['query'];
87             }
88             if (isset($parts['fragment'])) {
89                 $path .= '#' . $parts['fragment'];
90             }
91             if (isset($parts['port'])) {
92                 $port = $parts['port'];
93             }
94             if (isset($parts['scheme'])) {
95                 $method = $parts['scheme'];
96             }
97             if (isset($parts['user'])) {
98                 $this->username = $parts['user'];
99             }
100             if (isset($parts['pass'])) {
101                 $this->password = $parts['pass'];
102             }
103         }
104         if ($path == '' || $path[0] != '/') {
105             $this->path = '/' . $path;
106         } else {
107             $this->path = $path;
108         }
109         $this->server = $server;
110         if ($port != '') {
111             $this->port = $port;
112         }
113         if ($method != '') {
114             $this->method = $method;
115         }
116
117         // if ZLIB is enabled, let the client by default accept compressed responses
118         if (function_exists('gzinflate') || (
119                 function_exists('curl_init') && (($info = curl_version()) &&
120                     ((is_string($info) && strpos($info, 'zlib') !== null) || isset($info['libz_version'])))
121             )
122         ) {
123             $this->accepted_compression = array('gzip', 'deflate');
124         }
125
126         // keepalives: enabled by default
127         $this->keepalive = true;
128
129         // by default the xml parser can support these 3 charset encodings
130         $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII');
131
132         // initialize user_agent string
133         $this->user_agent = PhpXmlRpc::$xmlrpcName . ' ' . PhpXmlRpc::$xmlrpcVersion;
134     }
135
136     /**
137      * Enables/disables the echoing to screen of the xmlrpc responses received.
138      *
139      * @param integer $in values 0, 1 and 2 are supported (2 = echo sent msg too, before received response)
140      */
141     public function setDebug($in)
142     {
143         $this->debug = $in;
144     }
145
146     /**
147      * Add some http BASIC AUTH credentials, used by the client to authenticate.
148      *
149      * @param string $u username
150      * @param string $p password
151      * @param integer $t auth type. See curl_setopt man page for supported auth types. Defaults to CURLAUTH_BASIC (basic auth)
152      */
153     public function setCredentials($u, $p, $t = 1)
154     {
155         $this->username = $u;
156         $this->password = $p;
157         $this->authtype = $t;
158     }
159
160     /**
161      * Add a client-side https certificate.
162      *
163      * @param string $cert
164      * @param string $certpass
165      */
166     public function setCertificate($cert, $certpass)
167     {
168         $this->cert = $cert;
169         $this->certpass = $certpass;
170     }
171
172     /**
173      * Add a CA certificate to verify server with (see man page about
174      * CURLOPT_CAINFO for more details).
175      *
176      * @param string $cacert certificate file name (or dir holding certificates)
177      * @param bool $is_dir set to true to indicate cacert is a dir. defaults to false
178      */
179     public function setCaCertificate($cacert, $is_dir = false)
180     {
181         if ($is_dir) {
182             $this->cacertdir = $cacert;
183         } else {
184             $this->cacert = $cacert;
185         }
186     }
187
188     /**
189      * Set attributes for SSL communication: private SSL key
190      * NB: does not work in older php/curl installs
191      * Thanks to Daniel Convissor.
192      *
193      * @param string $key The name of a file containing a private SSL key
194      * @param string $keypass The secret password needed to use the private SSL key
195      */
196     public function setKey($key, $keypass)
197     {
198         $this->key = $key;
199         $this->keypass = $keypass;
200     }
201
202     /**
203      * Set attributes for SSL communication: verify server certificate.
204      *
205      * @param bool $i enable/disable verification of peer certificate
206      */
207     public function setSSLVerifyPeer($i)
208     {
209         $this->verifypeer = $i;
210     }
211
212     /**
213      * Set attributes for SSL communication: verify match of server cert w. hostname.
214      *
215      * @param int $i
216      */
217     public function setSSLVerifyHost($i)
218     {
219         $this->verifyhost = $i;
220     }
221
222     /**
223      * Set attributes for SSL communication: SSL version to use. Best left at 0 (default value ): let cURL decide
224      *
225      * @param int $i
226      */
227     public function setSSLVersion($i)
228     {
229         $this->sslversion = $i;
230     }
231
232     /**
233      * Set proxy info.
234      *
235      * @param string $proxyhost
236      * @param string $proxyport Defaults to 8080 for HTTP and 443 for HTTPS
237      * @param string $proxyusername Leave blank if proxy has public access
238      * @param string $proxypassword Leave blank if proxy has public access
239      * @param int $proxyauthtype set to constant CURLAUTH_NTLM to use NTLM auth with proxy
240      */
241     public function setProxy($proxyhost, $proxyport, $proxyusername = '', $proxypassword = '', $proxyauthtype = 1)
242     {
243         $this->proxy = $proxyhost;
244         $this->proxyport = $proxyport;
245         $this->proxy_user = $proxyusername;
246         $this->proxy_pass = $proxypassword;
247         $this->proxy_authtype = $proxyauthtype;
248     }
249
250     /**
251      * Enables/disables reception of compressed xmlrpc responses.
252      * Note that enabling reception of compressed responses merely adds some standard
253      * http headers to xmlrpc requests. It is up to the xmlrpc server to return
254      * compressed responses when receiving such requests.
255      *
256      * @param string $compmethod either 'gzip', 'deflate', 'any' or ''
257      */
258     public function setAcceptedCompression($compmethod)
259     {
260         if ($compmethod == 'any') {
261             $this->accepted_compression = array('gzip', 'deflate');
262         } elseif ($compmethod == false) {
263             $this->accepted_compression = array();
264         } else {
265             $this->accepted_compression = array($compmethod);
266         }
267     }
268
269     /**
270      * Enables/disables http compression of xmlrpc request.
271      * Take care when sending compressed requests: servers might not support them
272      * (and automatic fallback to uncompressed requests is not yet implemented).
273      *
274      * @param string $compmethod either 'gzip', 'deflate' or ''
275      */
276     public function setRequestCompression($compmethod)
277     {
278         $this->request_compression = $compmethod;
279     }
280
281     /**
282      * Adds a cookie to list of cookies that will be sent to server.
283      * NB: setting any param but name and value will turn the cookie into a 'version 1' cookie:
284      * do not do it unless you know what you are doing.
285      *
286      * @param string $name
287      * @param string $value
288      * @param string $path
289      * @param string $domain
290      * @param int $port
291      *
292      * @todo check correctness of urlencoding cookie value (copied from php way of doing it...)
293      */
294     public function setCookie($name, $value = '', $path = '', $domain = '', $port = null)
295     {
296         $this->cookies[$name]['value'] = urlencode($value);
297         if ($path || $domain || $port) {
298             $this->cookies[$name]['path'] = $path;
299             $this->cookies[$name]['domain'] = $domain;
300             $this->cookies[$name]['port'] = $port;
301             $this->cookies[$name]['version'] = 1;
302         } else {
303             $this->cookies[$name]['version'] = 0;
304         }
305     }
306
307     /**
308      * Directly set cURL options, for extra flexibility
309      * It allows eg. to bind client to a specific IP interface / address.
310      *
311      * @param array $options
312      */
313     public function SetCurlOptions($options)
314     {
315         $this->extracurlopts = $options;
316     }
317
318     /**
319      * Set user-agent string that will be used by this client instance
320      * in http headers sent to the server.
321      */
322     public function SetUserAgent($agentstring)
323     {
324         $this->user_agent = $agentstring;
325     }
326
327     /**
328      * Send an xmlrpc request.
329      *
330      * @param mixed $msg The request object, or an array of requests for using multicall, or the complete xml representation of a request
331      * @param integer $timeout Connection timeout, in seconds, If unspecified, a platform specific timeout will apply
332      * @param string $method if left unspecified, the http protocol chosen during creation of the object will be used
333      *
334      * @return Response
335      */
336     public function & send($msg, $timeout = 0, $method = '')
337     {
338         // if user does not specify http protocol, use native method of this client
339         // (i.e. method set during call to constructor)
340         if ($method == '') {
341             $method = $this->method;
342         }
343
344         if (is_array($msg)) {
345             // $msg is an array of Requests
346             $r = $this->multicall($msg, $timeout, $method);
347
348             return $r;
349         } elseif (is_string($msg)) {
350             $n = new Request('');
351             $n->payload = $msg;
352             $msg = $n;
353         }
354
355         // where msg is a Request
356         $msg->debug = $this->debug;
357
358         if ($method == 'https') {
359             $r = $this->sendPayloadHTTPS(
360                 $msg,
361                 $this->server,
362                 $this->port,
363                 $timeout,
364                 $this->username,
365                 $this->password,
366                 $this->authtype,
367                 $this->cert,
368                 $this->certpass,
369                 $this->cacert,
370                 $this->cacertdir,
371                 $this->proxy,
372                 $this->proxyport,
373                 $this->proxy_user,
374                 $this->proxy_pass,
375                 $this->proxy_authtype,
376                 $this->keepalive,
377                 $this->key,
378                 $this->keypass,
379                 $this->sslversion
380             );
381         } elseif ($method == 'http11') {
382             $r = $this->sendPayloadCURL(
383                 $msg,
384                 $this->server,
385                 $this->port,
386                 $timeout,
387                 $this->username,
388                 $this->password,
389                 $this->authtype,
390                 null,
391                 null,
392                 null,
393                 null,
394                 $this->proxy,
395                 $this->proxyport,
396                 $this->proxy_user,
397                 $this->proxy_pass,
398                 $this->proxy_authtype,
399                 'http',
400                 $this->keepalive
401             );
402         } else {
403             $r = $this->sendPayloadHTTP10(
404                 $msg,
405                 $this->server,
406                 $this->port,
407                 $timeout,
408                 $this->username,
409                 $this->password,
410                 $this->authtype,
411                 $this->proxy,
412                 $this->proxyport,
413                 $this->proxy_user,
414                 $this->proxy_pass,
415                 $this->proxy_authtype
416             );
417         }
418
419         return $r;
420     }
421
422     private function sendPayloadHTTP10($msg, $server, $port, $timeout = 0,
423                                        $username = '', $password = '', $authtype = 1, $proxyhost = '',
424                                        $proxyport = 0, $proxyusername = '', $proxypassword = '', $proxyauthtype = 1)
425     {
426         if ($port == 0) {
427             $port = 80;
428         }
429
430         // Only create the payload if it was not created previously
431         if (empty($msg->payload)) {
432             $msg->createPayload($this->request_charset_encoding);
433         }
434
435         $payload = $msg->payload;
436         // Deflate request body and set appropriate request headers
437         if (function_exists('gzdeflate') && ($this->request_compression == 'gzip' || $this->request_compression == 'deflate')) {
438             if ($this->request_compression == 'gzip') {
439                 $a = @gzencode($payload);
440                 if ($a) {
441                     $payload = $a;
442                     $encoding_hdr = "Content-Encoding: gzip\r\n";
443                 }
444             } else {
445                 $a = @gzcompress($payload);
446                 if ($a) {
447                     $payload = $a;
448                     $encoding_hdr = "Content-Encoding: deflate\r\n";
449                 }
450             }
451         } else {
452             $encoding_hdr = '';
453         }
454
455         // thanks to Grant Rauscher <grant7@firstworld.net> for this
456         $credentials = '';
457         if ($username != '') {
458             $credentials = 'Authorization: Basic ' . base64_encode($username . ':' . $password) . "\r\n";
459             if ($authtype != 1) {
460                 error_log('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth is supported with HTTP 1.0');
461             }
462         }
463
464         $accepted_encoding = '';
465         if (is_array($this->accepted_compression) && count($this->accepted_compression)) {
466             $accepted_encoding = 'Accept-Encoding: ' . implode(', ', $this->accepted_compression) . "\r\n";
467         }
468
469         $proxy_credentials = '';
470         if ($proxyhost) {
471             if ($proxyport == 0) {
472                 $proxyport = 8080;
473             }
474             $connectserver = $proxyhost;
475             $connectport = $proxyport;
476             $uri = 'http://' . $server . ':' . $port . $this->path;
477             if ($proxyusername != '') {
478                 if ($proxyauthtype != 1) {
479                     error_log('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth to proxy is supported with HTTP 1.0');
480                 }
481                 $proxy_credentials = 'Proxy-Authorization: Basic ' . base64_encode($proxyusername . ':' . $proxypassword) . "\r\n";
482             }
483         } else {
484             $connectserver = $server;
485             $connectport = $port;
486             $uri = $this->path;
487         }
488
489         // Cookie generation, as per rfc2965 (version 1 cookies) or
490         // netscape's rules (version 0 cookies)
491         $cookieheader = '';
492         if (count($this->cookies)) {
493             $version = '';
494             foreach ($this->cookies as $name => $cookie) {
495                 if ($cookie['version']) {
496                     $version = ' $Version="' . $cookie['version'] . '";';
497                     $cookieheader .= ' ' . $name . '="' . $cookie['value'] . '";';
498                     if ($cookie['path']) {
499                         $cookieheader .= ' $Path="' . $cookie['path'] . '";';
500                     }
501                     if ($cookie['domain']) {
502                         $cookieheader .= ' $Domain="' . $cookie['domain'] . '";';
503                     }
504                     if ($cookie['port']) {
505                         $cookieheader .= ' $Port="' . $cookie['port'] . '";';
506                     }
507                 } else {
508                     $cookieheader .= ' ' . $name . '=' . $cookie['value'] . ";";
509                 }
510             }
511             $cookieheader = 'Cookie:' . $version . substr($cookieheader, 0, -1) . "\r\n";
512         }
513
514         // omit port if 80
515         $port = ($port == 80) ? '' : (':' . $port);
516
517         $op = 'POST ' . $uri . " HTTP/1.0\r\n" .
518             'User-Agent: ' . $this->user_agent . "\r\n" .
519             'Host: ' . $server . $port . "\r\n" .
520             $credentials .
521             $proxy_credentials .
522             $accepted_encoding .
523             $encoding_hdr .
524             'Accept-Charset: ' . implode(',', $this->accepted_charset_encodings) . "\r\n" .
525             $cookieheader .
526             'Content-Type: ' . $msg->content_type . "\r\nContent-Length: " .
527             strlen($payload) . "\r\n\r\n" .
528             $payload;
529
530         if ($this->debug > 1) {
531             $this->debugMessage("---SENDING---\n$op\n---END---");
532         }
533
534         if ($timeout > 0) {
535             $fp = @fsockopen($connectserver, $connectport, $this->errno, $this->errstr, $timeout);
536         } else {
537             $fp = @fsockopen($connectserver, $connectport, $this->errno, $this->errstr);
538         }
539         if ($fp) {
540             if ($timeout > 0 && function_exists('stream_set_timeout')) {
541                 stream_set_timeout($fp, $timeout);
542             }
543         } else {
544             $this->errstr = 'Connect error: ' . $this->errstr;
545             $r = new Response(0, PhpXmlRpc::$xmlrpcerr['http_error'], $this->errstr . ' (' . $this->errno . ')');
546
547             return $r;
548         }
549
550         if (!fputs($fp, $op, strlen($op))) {
551             fclose($fp);
552             $this->errstr = 'Write error';
553             $r = new Response(0, PhpXmlRpc::$xmlrpcerr['http_error'], $this->errstr);
554
555             return $r;
556         } else {
557             // reset errno and errstr on successful socket connection
558             $this->errstr = '';
559         }
560         // G. Giunta 2005/10/24: close socket before parsing.
561         // should yield slightly better execution times, and make easier recursive calls (e.g. to follow http redirects)
562         $ipd = '';
563         do {
564             // shall we check for $data === FALSE?
565             // as per the manual, it signals an error
566             $ipd .= fread($fp, 32768);
567         } while (!feof($fp));
568         fclose($fp);
569         $r = $msg->parseResponse($ipd, false, $this->return_type);
570
571         return $r;
572     }
573
574     private function sendPayloadHTTPS($msg, $server, $port, $timeout = 0, $username = '',
575                                       $password = '', $authtype = 1, $cert = '', $certpass = '', $cacert = '', $cacertdir = '',
576                                       $proxyhost = '', $proxyport = 0, $proxyusername = '', $proxypassword = '', $proxyauthtype = 1,
577                                       $keepalive = false, $key = '', $keypass = '', $sslversion = 0)
578     {
579         $r = $this->sendPayloadCURL($msg, $server, $port, $timeout, $username,
580             $password, $authtype, $cert, $certpass, $cacert, $cacertdir, $proxyhost, $proxyport,
581             $proxyusername, $proxypassword, $proxyauthtype, 'https', $keepalive, $key, $keypass, $sslversion);
582
583         return $r;
584     }
585
586     /**
587      * Contributed by Justin Miller <justin@voxel.net>
588      * Requires curl to be built into PHP
589      * NB: CURL versions before 7.11.10 cannot use proxy to talk to https servers!
590      */
591     private function sendPayloadCURL($msg, $server, $port, $timeout = 0, $username = '',
592                                      $password = '', $authtype = 1, $cert = '', $certpass = '', $cacert = '', $cacertdir = '',
593                                      $proxyhost = '', $proxyport = 0, $proxyusername = '', $proxypassword = '', $proxyauthtype = 1, $method = 'https',
594                                      $keepalive = false, $key = '', $keypass = '', $sslversion = 0)
595     {
596         if (!function_exists('curl_init')) {
597             $this->errstr = 'CURL unavailable on this install';
598             $r = new Response(0, PhpXmlRpc::$xmlrpcerr['no_curl'], PhpXmlRpc::$xmlrpcstr['no_curl']);
599
600             return $r;
601         }
602         if ($method == 'https') {
603             if (($info = curl_version()) &&
604                 ((is_string($info) && strpos($info, 'OpenSSL') === null) || (is_array($info) && !isset($info['ssl_version'])))
605             ) {
606                 $this->errstr = 'SSL unavailable on this install';
607                 $r = new Response(0, PhpXmlRpc::$xmlrpcerr['no_ssl'], PhpXmlRpc::$xmlrpcstr['no_ssl']);
608
609                 return $r;
610             }
611         }
612
613         if ($port == 0) {
614             if ($method == 'http') {
615                 $port = 80;
616             } else {
617                 $port = 443;
618             }
619         }
620
621         // Only create the payload if it was not created previously
622         if (empty($msg->payload)) {
623             $msg->createPayload($this->request_charset_encoding);
624         }
625
626         // Deflate request body and set appropriate request headers
627         $payload = $msg->payload;
628         if (function_exists('gzdeflate') && ($this->request_compression == 'gzip' || $this->request_compression == 'deflate')) {
629             if ($this->request_compression == 'gzip') {
630                 $a = @gzencode($payload);
631                 if ($a) {
632                     $payload = $a;
633                     $encoding_hdr = 'Content-Encoding: gzip';
634                 }
635             } else {
636                 $a = @gzcompress($payload);
637                 if ($a) {
638                     $payload = $a;
639                     $encoding_hdr = 'Content-Encoding: deflate';
640                 }
641             }
642         } else {
643             $encoding_hdr = '';
644         }
645
646         if ($this->debug > 1) {
647             $this->debugMessage("---SENDING---\n$payload\n---END---");
648             // let the client see this now in case http times out...
649             flush();
650         }
651
652         if (!$keepalive || !$this->xmlrpc_curl_handle) {
653             $curl = curl_init($method . '://' . $server . ':' . $port . $this->path);
654             if ($keepalive) {
655                 $this->xmlrpc_curl_handle = $curl;
656             }
657         } else {
658             $curl = $this->xmlrpc_curl_handle;
659         }
660
661         // results into variable
662         curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
663
664         if ($this->debug) {
665             curl_setopt($curl, CURLOPT_VERBOSE, 1);
666         }
667         curl_setopt($curl, CURLOPT_USERAGENT, $this->user_agent);
668         // required for XMLRPC: post the data
669         curl_setopt($curl, CURLOPT_POST, 1);
670         // the data
671         curl_setopt($curl, CURLOPT_POSTFIELDS, $payload);
672
673         // return the header too
674         curl_setopt($curl, CURLOPT_HEADER, 1);
675
676         // NB: if we set an empty string, CURL will add http header indicating
677         // ALL methods it is supporting. This is possibly a better option than
678         // letting the user tell what curl can / cannot do...
679         if (is_array($this->accepted_compression) && count($this->accepted_compression)) {
680             //curl_setopt($curl, CURLOPT_ENCODING, implode(',', $this->accepted_compression));
681             // empty string means 'any supported by CURL' (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
682             if (count($this->accepted_compression) == 1) {
683                 curl_setopt($curl, CURLOPT_ENCODING, $this->accepted_compression[0]);
684             } else {
685                 curl_setopt($curl, CURLOPT_ENCODING, '');
686             }
687         }
688         // extra headers
689         $headers = array('Content-Type: ' . $msg->content_type, 'Accept-Charset: ' . implode(',', $this->accepted_charset_encodings));
690         // if no keepalive is wanted, let the server know it in advance
691         if (!$keepalive) {
692             $headers[] = 'Connection: close';
693         }
694         // request compression header
695         if ($encoding_hdr) {
696             $headers[] = $encoding_hdr;
697         }
698
699         curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
700         // timeout is borked
701         if ($timeout) {
702             curl_setopt($curl, CURLOPT_TIMEOUT, $timeout == 1 ? 1 : $timeout - 1);
703         }
704
705         if ($username && $password) {
706             curl_setopt($curl, CURLOPT_USERPWD, $username . ':' . $password);
707             if (defined('CURLOPT_HTTPAUTH')) {
708                 curl_setopt($curl, CURLOPT_HTTPAUTH, $authtype);
709             } elseif ($authtype != 1) {
710                 error_log('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth is supported by the current PHP/curl install');
711             }
712         }
713
714         if ($method == 'https') {
715             // set cert file
716             if ($cert) {
717                 curl_setopt($curl, CURLOPT_SSLCERT, $cert);
718             }
719             // set cert password
720             if ($certpass) {
721                 curl_setopt($curl, CURLOPT_SSLCERTPASSWD, $certpass);
722             }
723             // whether to verify remote host's cert
724             curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verifypeer);
725             // set ca certificates file/dir
726             if ($cacert) {
727                 curl_setopt($curl, CURLOPT_CAINFO, $cacert);
728             }
729             if ($cacertdir) {
730                 curl_setopt($curl, CURLOPT_CAPATH, $cacertdir);
731             }
732             // set key file (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
733             if ($key) {
734                 curl_setopt($curl, CURLOPT_SSLKEY, $key);
735             }
736             // set key password (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
737             if ($keypass) {
738                 curl_setopt($curl, CURLOPT_SSLKEYPASSWD, $keypass);
739             }
740             // whether to verify cert's common name (CN); 0 for no, 1 to verify that it exists, and 2 to verify that it matches the hostname used
741             curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $this->verifyhost);
742             // allow usage of different SSL versions
743             curl_setopt($curl, CURLOPT_SSLVERSION, $sslversion);
744         }
745
746         // proxy info
747         if ($proxyhost) {
748             if ($proxyport == 0) {
749                 $proxyport = 8080; // NB: even for HTTPS, local connection is on port 8080
750             }
751             curl_setopt($curl, CURLOPT_PROXY, $proxyhost . ':' . $proxyport);
752             //curl_setopt($curl, CURLOPT_PROXYPORT,$proxyport);
753             if ($proxyusername) {
754                 curl_setopt($curl, CURLOPT_PROXYUSERPWD, $proxyusername . ':' . $proxypassword);
755                 if (defined('CURLOPT_PROXYAUTH')) {
756                     curl_setopt($curl, CURLOPT_PROXYAUTH, $proxyauthtype);
757                 } elseif ($proxyauthtype != 1) {
758                     error_log('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth to proxy is supported by the current PHP/curl install');
759                 }
760             }
761         }
762
763         // NB: should we build cookie http headers by hand rather than let CURL do it?
764         // the following code does not honour 'expires', 'path' and 'domain' cookie attributes
765         // set to client obj the the user...
766         if (count($this->cookies)) {
767             $cookieheader = '';
768             foreach ($this->cookies as $name => $cookie) {
769                 $cookieheader .= $name . '=' . $cookie['value'] . '; ';
770             }
771             curl_setopt($curl, CURLOPT_COOKIE, substr($cookieheader, 0, -2));
772         }
773
774         foreach ($this->extracurlopts as $opt => $val) {
775             curl_setopt($curl, $opt, $val);
776         }
777
778         $result = curl_exec($curl);
779
780         if ($this->debug > 1) {
781             $message = "---CURL INFO---\n";
782             foreach (curl_getinfo($curl) as $name => $val) {
783                 if (is_array($val)) {
784                     $val = implode("\n", $val);
785                 }
786                 $message .= $name . ': ' . $val . "\n";
787             }
788             $message .= "---END---";
789             $this->debugMessage($message);
790         }
791
792         if (!$result) {
793             /// @todo we should use a better check here - what if we get back '' or '0'?
794
795             $this->errstr = 'no response';
796             $resp = new Response(0, PhpXmlRpc::$xmlrpcerr['curl_fail'], PhpXmlRpc::$xmlrpcstr['curl_fail'] . ': ' . curl_error($curl));
797             curl_close($curl);
798             if ($keepalive) {
799                 $this->xmlrpc_curl_handle = null;
800             }
801         } else {
802             if (!$keepalive) {
803                 curl_close($curl);
804             }
805             $resp = $msg->parseResponse($result, true, $this->return_type);
806             // if we got back a 302, we can not reuse the curl handle for later calls
807             if ($resp->faultCode() == PhpXmlRpc::$xmlrpcerr['http_error'] && $keepalive) {
808                 curl_close($curl);
809                 $this->xmlrpc_curl_handle = null;
810             }
811         }
812
813         return $resp;
814     }
815
816     /**
817      * Send an array of requests and return an array of responses.
818      * Unless $this->no_multicall has been set to true, it will try first
819      * to use one single xmlrpc call to server method system.multicall, and
820      * revert to sending many successive calls in case of failure.
821      * This failure is also stored in $this->no_multicall for subsequent calls.
822      * Unfortunately, there is no server error code universally used to denote
823      * the fact that multicall is unsupported, so there is no way to reliably
824      * distinguish between that and a temporary failure.
825      * If you are sure that server supports multicall and do not want to
826      * fallback to using many single calls, set the fourth parameter to FALSE.
827      *
828      * NB: trying to shoehorn extra functionality into existing syntax has resulted
829      * in pretty much convoluted code...
830      *
831      * @param Request[] $msgs an array of Request objects
832      * @param integer $timeout connection timeout (in seconds)
833      * @param string $method the http protocol variant to be used
834      * @param boolean fallback When true, upon receiving an error during multicall, multiple single calls will be attempted
835      *
836      * @return array
837      */
838     public function multicall($msgs, $timeout = 0, $method = '', $fallback = true)
839     {
840         if ($method == '') {
841             $method = $this->method;
842         }
843         if (!$this->no_multicall) {
844             $results = $this->_try_multicall($msgs, $timeout, $method);
845             if (is_array($results)) {
846                 // System.multicall succeeded
847                 return $results;
848             } else {
849                 // either system.multicall is unsupported by server,
850                 // or call failed for some other reason.
851                 if ($fallback) {
852                     // Don't try it next time...
853                     $this->no_multicall = true;
854                 } else {
855                     if (is_a($results, '\PhpXmlRpc\Response')) {
856                         $result = $results;
857                     } else {
858                         $result = new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'], PhpXmlRpc::$xmlrpcstr['multicall_error']);
859                     }
860                 }
861             }
862         } else {
863             // override fallback, in case careless user tries to do two
864             // opposite things at the same time
865             $fallback = true;
866         }
867
868         $results = array();
869         if ($fallback) {
870             // system.multicall is (probably) unsupported by server:
871             // emulate multicall via multiple requests
872             foreach ($msgs as $msg) {
873                 $results[] = $this->send($msg, $timeout, $method);
874             }
875         } else {
876             // user does NOT want to fallback on many single calls:
877             // since we should always return an array of responses,
878             // return an array with the same error repeated n times
879             foreach ($msgs as $msg) {
880                 $results[] = $result;
881             }
882         }
883
884         return $results;
885     }
886
887     /**
888      * Attempt to boxcar $msgs via system.multicall.
889      * Returns either an array of xmlrpcreponses, an xmlrpc error response
890      * or false (when received response does not respect valid multicall syntax).
891      */
892     private function _try_multicall($msgs, $timeout, $method)
893     {
894         // Construct multicall request
895         $calls = array();
896         foreach ($msgs as $msg) {
897             $call['methodName'] = new Value($msg->method(), 'string');
898             $numParams = $msg->getNumParams();
899             $params = array();
900             for ($i = 0; $i < $numParams; $i++) {
901                 $params[$i] = $msg->getParam($i);
902             }
903             $call['params'] = new Value($params, 'array');
904             $calls[] = new Value($call, 'struct');
905         }
906         $multicall = new Request('system.multicall');
907         $multicall->addParam(new Value($calls, 'array'));
908
909         // Attempt RPC call
910         $result = $this->send($multicall, $timeout, $method);
911
912         if ($result->faultCode() != 0) {
913             // call to system.multicall failed
914             return $result;
915         }
916
917         // Unpack responses.
918         $rets = $result->value();
919
920         if ($this->return_type == 'xml') {
921             return $rets;
922         } elseif ($this->return_type == 'phpvals') {
923             ///@todo test this code branch...
924             $rets = $result->value();
925             if (!is_array($rets)) {
926                 return false;       // bad return type from system.multicall
927             }
928             $numRets = count($rets);
929             if ($numRets != count($msgs)) {
930                 return false;       // wrong number of return values.
931             }
932
933             $response = array();
934             for ($i = 0; $i < $numRets; $i++) {
935                 $val = $rets[$i];
936                 if (!is_array($val)) {
937                     return false;
938                 }
939                 switch (count($val)) {
940                     case 1:
941                         if (!isset($val[0])) {
942                             return false;       // Bad value
943                         }
944                         // Normal return value
945                         $response[$i] = new Response($val[0], 0, '', 'phpvals');
946                         break;
947                     case 2:
948                         /// @todo remove usage of @: it is apparently quite slow
949                         $code = @$val['faultCode'];
950                         if (!is_int($code)) {
951                             return false;
952                         }
953                         $str = @$val['faultString'];
954                         if (!is_string($str)) {
955                             return false;
956                         }
957                         $response[$i] = new Response(0, $code, $str);
958                         break;
959                     default:
960                         return false;
961                 }
962             }
963
964             return $response;
965         } else {
966             // return type == 'xmlrpcvals'
967
968             $rets = $result->value();
969             if ($rets->kindOf() != 'array') {
970                 return false;       // bad return type from system.multicall
971             }
972             $numRets = $rets->arraysize();
973             if ($numRets != count($msgs)) {
974                 return false;       // wrong number of return values.
975             }
976
977             $response = array();
978             for ($i = 0; $i < $numRets; $i++) {
979                 $val = $rets->arraymem($i);
980                 switch ($val->kindOf()) {
981                     case 'array':
982                         if ($val->arraysize() != 1) {
983                             return false;       // Bad value
984                         }
985                         // Normal return value
986                         $response[$i] = new Response($val->arraymem(0));
987                         break;
988                     case 'struct':
989                         $code = $val->structmem('faultCode');
990                         if ($code->kindOf() != 'scalar' || $code->scalartyp() != 'int') {
991                             return false;
992                         }
993                         $str = $val->structmem('faultString');
994                         if ($str->kindOf() != 'scalar' || $str->scalartyp() != 'string') {
995                             return false;
996                         }
997                         $response[$i] = new Response(0, $code->scalarval(), $str->scalarval());
998                         break;
999                     default:
1000                         return false;
1001                 }
1002             }
1003
1004             return $response;
1005         }
1006     }
1007
1008     /**
1009      * Echoes a debug message, taking care of escaping it when not in console mode
1010      *
1011      * @param string $message
1012      */
1013     protected function debugMessage($message)
1014     {
1015         if (PHP_SAPI != 'cli') {
1016             print "<PRE>\n".htmlentities($message)."\n</PRE>";
1017         }
1018         else {
1019             print "\n$message\n";
1020         }
1021         // let the client see this now in case http times out...
1022         flush();
1023     }
1024 }