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