source: branches/feature-module-update/data/module/SOAP/Transport/HTTP.php @ 16957

Revision 16957, 20.7 KB checked in by naka, 16 years ago (diff)

PEAR::SOAPモジュールアップ

Line 
1<?php
2/**
3 * This file contains the code for a HTTP transport layer.
4 *
5 * PHP versions 4 and 5
6 *
7 * LICENSE: This source file is subject to version 2.02 of the PHP license,
8 * that is bundled with this package in the file LICENSE, and is available at
9 * through the world-wide-web at http://www.php.net/license/2_02.txt.  If you
10 * did not receive a copy of the PHP license and are unable to obtain it
11 * through the world-wide-web, please send a note to license@php.net so we can
12 * mail you a copy immediately.
13 *
14 * @category   Web Services
15 * @package    SOAP
16 * @author     Shane Caraveo <Shane@Caraveo.com>
17 * @author     Jan Schneider <jan@horde.org>
18 * @copyright  2003-2006 The PHP Group
19 * @license    http://www.php.net/license/2_02.txt  PHP License 2.02
20 * @link       http://pear.php.net/package/SOAP
21 */
22
23/**
24 * HTTP Transport class
25 *
26 * @package  SOAP
27 * @category Web_Services
28 */
29
30/**
31 * Needed Classes
32 */
33require_once 'SOAP/Transport.php';
34
35/**
36 *  HTTP Transport for SOAP
37 *
38 * @access public
39 * @package SOAP
40 * @author Shane Caraveo <shane@php.net>
41 * @author Jan Schneider <jan@horde.org>
42 */
43class SOAP_Transport_HTTP extends SOAP_Transport
44{
45    /**
46     * Basic Auth string.
47     *
48     * @var array
49     */
50    var $headers = array();
51
52    /**
53     * Cookies.
54     *
55     * @var array
56     */
57    var $cookies;
58
59    /**
60     * Connection timeout in seconds. 0 = none.
61     *
62     * @var integer
63     */
64    var $timeout = 4;
65
66    /**
67     * HTTP-Response Content-Type.
68     */
69    var $result_content_type;
70
71    var $result_headers = array();
72
73    var $result_cookies = array();
74
75    /**
76     * SOAP_Transport_HTTP Constructor
77     *
78     * @access public
79     *
80     * @param string $url       HTTP url to SOAP endpoint.
81     * @param string $encoding  Encoding to use.
82     */
83    function SOAP_Transport_HTTP($url, $encoding = SOAP_DEFAULT_ENCODING)
84    {
85        parent::SOAP_Base('HTTP');
86        $this->urlparts = @parse_url($url);
87        $this->url = $url;
88        $this->encoding = $encoding;
89    }
90
91    /**
92     * Sends and receives SOAP data.
93     *
94     * @access public
95     *
96     * @param string  Outgoing SOAP data.
97     * @param array   Options.
98     *
99     * @return string|SOAP_Fault
100     */
101    function send($msg, $options = array())
102    {
103        $this->fault = null;
104
105        if (!$this->_validateUrl()) {
106            return $this->fault;
107        }
108
109        if (isset($options['timeout'])) {
110            $this->timeout = (int)$options['timeout'];
111        }
112
113        if (strcasecmp($this->urlparts['scheme'], 'HTTP') == 0) {
114            return $this->_sendHTTP($msg, $options);
115        } elseif (strcasecmp($this->urlparts['scheme'], 'HTTPS') == 0) {
116            return $this->_sendHTTPS($msg, $options);
117        }
118
119        return $this->_raiseSoapFault('Invalid url scheme ' . $this->url);
120    }
121
122    /**
123     * Sets data for HTTP authentication, creates authorization header.
124     *
125     * @param string $username   Username.
126     * @param string $password   Response data, minus HTTP headers.
127     *
128     * @access public
129     */
130    function setCredentials($username, $password)
131    {
132        $this->headers['Authorization'] = 'Basic ' . base64_encode($username . ':' . $password);
133    }
134
135    /**
136     * Adds a cookie.
137     *
138     * @access public
139     * @param string $name  Cookie name.
140     * @param mixed $value  Cookie value.
141     */
142    function addCookie($name, $value)
143    {
144        $this->cookies[$name] = $value;
145    }
146
147    /**
148     * Generates the correct headers for the cookies.
149     *
150     * @access private
151     *
152     * @param array $options  Cookie options. If 'nocookies' is set and true
153     *                        the cookies from the last response are added
154     *                        automatically. 'cookies' is name-value-hash with
155     *                        a list of cookies to add.
156     *
157     * @return string  The cookie header value.
158     */
159    function _generateCookieHeader($options)
160    {
161        $this->cookies = array();
162
163        if (empty($options['nocookies']) &&
164            isset($this->result_cookies)) {
165            // Add the cookies we got from the last request.
166            foreach ($this->result_cookies as $cookie) {
167                if ($cookie['domain'] == $this->urlparts['host']) {
168                    $this->cookies[$cookie['name']] = $cookie['value'];
169                }
170            }
171        }
172
173        // Add cookies the user wants to set.
174        if (isset($options['cookies'])) {
175            foreach ($options['cookies'] as $cookie) {
176                if ($cookie['domain'] == $this->urlparts['host']) {
177                    $this->cookies[$cookie['name']] = $cookie['value'];
178                }
179            }
180        }
181
182        $cookies = '';
183        foreach ($this->cookies as $name => $value) {
184            if (!empty($cookies)) {
185                $cookies .= '; ';
186            }
187            $cookies .= urlencode($name) . '=' . urlencode($value);
188        }
189
190        return $cookies;
191    }
192
193    /**
194     * Validate url data passed to constructor.
195     *
196     * @access private
197     * @return boolean
198     */
199    function _validateUrl()
200    {
201        if (!is_array($this->urlparts) ) {
202            $this->_raiseSoapFault('Unable to parse URL ' . $this->url);
203            return false;
204        }
205        if (!isset($this->urlparts['host'])) {
206            $this->_raiseSoapFault('No host in URL ' . $this->url);
207            return false;
208        }
209        if (!isset($this->urlparts['port'])) {
210            if (strcasecmp($this->urlparts['scheme'], 'HTTP') == 0) {
211                $this->urlparts['port'] = 80;
212            } elseif (strcasecmp($this->urlparts['scheme'], 'HTTPS') == 0) {
213                $this->urlparts['port'] = 443;
214            }
215
216        }
217        if (isset($this->urlparts['user'])) {
218            $this->setCredentials(urldecode($this->urlparts['user']),
219                                  urldecode($this->urlparts['pass']));
220        }
221        if (!isset($this->urlparts['path']) || !$this->urlparts['path']) {
222            $this->urlparts['path'] = '/';
223        }
224
225        return true;
226    }
227
228    /**
229     * Finds out what the encoding is.
230     * Sets the object property accordingly.
231     *
232     * @access private
233     * @param array $headers  Headers.
234     */
235    function _parseEncoding($headers)
236    {
237        $h = stristr($headers, 'Content-Type');
238        preg_match_all('/^Content-Type:\s*(.*)$/im', $h, $ct, PREG_SET_ORDER);
239        $n = count($ct);
240        $ct = $ct[$n - 1];
241
242        // Strip the string of \r.
243        $this->result_content_type = str_replace("\r", '', $ct[1]);
244
245        if (preg_match('/(.*?)(?:;\s?charset=)(.*)/i',
246                       $this->result_content_type,
247                       $m)) {
248            $this->result_content_type = $m[1];
249            if (count($m) > 2) {
250                $enc = strtoupper(str_replace('"', '', $m[2]));
251                if (in_array($enc, $this->_encodings)) {
252                    $this->result_encoding = $enc;
253                }
254            }
255        }
256
257        // Deal with broken servers that don't set content type on faults.
258        if (!$this->result_content_type) {
259            $this->result_content_type = 'text/xml';
260        }
261    }
262
263    /**
264     * Parses the headers.
265     *
266     * @param array $headers  The headers.
267     */
268    function _parseHeaders($headers)
269    {
270        /* Largely borrowed from HTTP_Request. */
271        $this->result_headers = array();
272        $headers = split("\r?\n", $headers);
273        foreach ($headers as $value) {
274            if (strpos($value,':') === false) {
275                $this->result_headers[0] = $value;
276                continue;
277            }
278            list($name, $value) = split(':', $value);
279            $headername = strtolower($name);
280            $headervalue = trim($value);
281            $this->result_headers[$headername] = $headervalue;
282
283            if ($headername == 'set-cookie') {
284                // Parse a SetCookie header to fill _cookies array.
285                $cookie = array('expires' => null,
286                                'domain'  => $this->urlparts['host'],
287                                'path'    => null,
288                                'secure'  => false);
289
290                if (!strpos($headervalue, ';')) {
291                    // Only a name=value pair.
292                    list($cookie['name'], $cookie['value']) = array_map('trim', explode('=', $headervalue));
293                    $cookie['name']  = urldecode($cookie['name']);
294                    $cookie['value'] = urldecode($cookie['value']);
295
296                } else {
297                    // Some optional parameters are supplied.
298                    $elements = explode(';', $headervalue);
299                    list($cookie['name'], $cookie['value']) = array_map('trim', explode('=', $elements[0]));
300                    $cookie['name']  = urldecode($cookie['name']);
301                    $cookie['value'] = urldecode($cookie['value']);
302
303                    for ($i = 1; $i < count($elements);$i++) {
304                        list($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
305                        if ('secure' == $elName) {
306                            $cookie['secure'] = true;
307                        } elseif ('expires' == $elName) {
308                            $cookie['expires'] = str_replace('"', '', $elValue);
309                        } elseif ('path' == $elName OR 'domain' == $elName) {
310                            $cookie[$elName] = urldecode($elValue);
311                        } else {
312                            $cookie[$elName] = $elValue;
313                        }
314                    }
315                }
316                $this->result_cookies[] = $cookie;
317            }
318        }
319    }
320
321    /**
322     * Removes HTTP headers from response.
323     *
324     * @return boolean
325     * @access private
326     */
327    function _parseResponse()
328    {
329        if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s",
330                       $this->incoming_payload,
331                       $match)) {
332            $this->response = $match[2];
333            // Find the response error, some servers response with 500 for
334            // SOAP faults.
335            $this->_parseHeaders($match[1]);
336
337            list(, $code, $msg) = sscanf($this->result_headers[0], '%s %s %s');
338            unset($this->result_headers[0]);
339
340            switch($code) {
341                case 100: // Continue
342                    $this->incoming_payload = $match[2];
343                    return $this->_parseResponse();
344                case 400:
345                    $this->_raiseSoapFault("HTTP Response $code Bad Request");
346                    return false;
347                    break;
348                case 401:
349                    $this->_raiseSoapFault("HTTP Response $code Authentication Failed");
350                    return false;
351                    break;
352                case 403:
353                    $this->_raiseSoapFault("HTTP Response $code Forbidden");
354                    return false;
355                    break;
356                case 404:
357                    $this->_raiseSoapFault("HTTP Response $code Not Found");
358                    return false;
359                    break;
360                case 407:
361                    $this->_raiseSoapFault("HTTP Response $code Proxy Authentication Required");
362                    return false;
363                    break;
364                case 408:
365                    $this->_raiseSoapFault("HTTP Response $code Request Timeout");
366                    return false;
367                    break;
368                case 410:
369                    $this->_raiseSoapFault("HTTP Response $code Gone");
370                    return false;
371                    break;
372                default:
373                    if ($code >= 400 && $code < 500) {
374                        $this->_raiseSoapFault("HTTP Response $code Not Found, Server message: $msg");
375                        return false;
376                    }
377            }
378
379            $this->_parseEncoding($match[1]);
380
381            if ($this->result_content_type == 'application/dime') {
382                // XXX quick hack insertion of DIME
383                if (PEAR::isError($this->_decodeDIMEMessage($this->response, $this->headers, $this->attachments))) {
384                    // _decodeDIMEMessage already raised $this->fault
385                    return false;
386                }
387                $this->result_content_type = $this->headers['content-type'];
388            } elseif (stristr($this->result_content_type, 'multipart/related')) {
389                $this->response = $this->incoming_payload;
390                if (PEAR::isError($this->_decodeMimeMessage($this->response, $this->headers, $this->attachments))) {
391                    // _decodeMimeMessage already raised $this->fault
392                    return false;
393                }
394            } elseif ($this->result_content_type != 'text/xml') {
395                $this->_raiseSoapFault($this->response);
396                return false;
397            }
398            // if no content, return false
399            return strlen($this->response) > 0;
400        }
401        $this->_raiseSoapFault('Invalid HTTP Response');
402        return false;
403    }
404
405    /**
406     * Creates an HTTP request, including headers, for the outgoing request.
407     *
408     * @access private
409     *
410     * @param string $msg     Outgoing SOAP package.
411     * @param array $options  Options.
412     *
413     * @return string  Outgoing payload.
414     */
415    function _getRequest($msg, $options)
416    {
417        $this->headers = array();
418
419        $action = isset($options['soapaction']) ? $options['soapaction'] : '';
420        $fullpath = $this->urlparts['path'];
421        if (isset($this->urlparts['query'])) {
422            $fullpath .= '?' . $this->urlparts['query'];
423        }
424        if (isset($this->urlparts['fragment'])) {
425            $fullpath .= '#' . $this->urlparts['fragment'];
426        }
427
428        if (isset($options['proxy_host'])) {
429            $fullpath = 'http://' . $this->urlparts['host'] . ':' .
430                $this->urlparts['port'] . $fullpath;
431        }
432
433        if (isset($options['proxy_user'])) {
434            $this->headers['Proxy-Authorization'] = 'Basic ' .
435                base64_encode($options['proxy_user'] . ':' .
436                              $options['proxy_pass']);
437        }
438
439        if (isset($options['user'])) {
440            $this->setCredentials($options['user'], $options['pass']);
441        }
442
443        $this->headers['User-Agent'] = $this->_userAgent;
444        $this->headers['Host'] = $this->urlparts['host'];
445        $this->headers['Content-Type'] = "text/xml; charset=$this->encoding";
446        $this->headers['Content-Length'] = strlen($msg);
447        $this->headers['SOAPAction'] = '"' . $action . '"';
448        $this->headers['Connection'] = 'close';
449
450        if (isset($options['headers'])) {
451            $this->headers = array_merge($this->headers, $options['headers']);
452        }
453
454        $cookies = $this->_generateCookieHeader($options);
455        if ($cookies) {
456            $this->headers['Cookie'] = $cookies;
457        }
458
459        $headers = '';
460        foreach ($this->headers as $k => $v) {
461            $headers .= "$k: $v\r\n";
462        }
463        $this->outgoing_payload = "POST $fullpath HTTP/1.0\r\n" . $headers .
464            "\r\n" . $msg;
465
466        return $this->outgoing_payload;
467    }
468
469    /**
470     * Sends the outgoing HTTP request and reads and parses the response.
471     *
472     * @access private
473     *
474     * @param string $msg     Outgoing SOAP package.
475     * @param array $options  Options.
476     *
477     * @return string  Response data without HTTP headers.
478     */
479    function _sendHTTP($msg, $options)
480    {
481        $this->incoming_payload = '';
482        $this->_getRequest($msg, $options);
483        $host = $this->urlparts['host'];
484        $port = $this->urlparts['port'];
485        if (isset($options['proxy_host'])) {
486            $host = $options['proxy_host'];
487            $port = isset($options['proxy_port']) ? $options['proxy_port'] : 8080;
488        }
489        // Send.
490        if ($this->timeout > 0) {
491            $fp = @fsockopen($host, $port, $this->errno, $this->errmsg, $this->timeout);
492        } else {
493            $fp = @fsockopen($host, $port, $this->errno, $this->errmsg);
494        }
495        if (!$fp) {
496            return $this->_raiseSoapFault("Connect Error to $host:$port");
497        }
498        if ($this->timeout > 0) {
499            // some builds of PHP do not support this, silence the warning
500            @socket_set_timeout($fp, $this->timeout);
501        }
502        if (!fputs($fp, $this->outgoing_payload, strlen($this->outgoing_payload))) {
503            return $this->_raiseSoapFault("Error POSTing Data to $host");
504        }
505
506        // get reponse
507        // XXX time consumer
508        do {
509            $data = fread($fp, 4096);
510            $_tmp_status = socket_get_status($fp);
511            if ($_tmp_status['timed_out']) {
512                return $this->_raiseSoapFault("Timed out read from $host");
513            } else {
514                $this->incoming_payload .= $data;
515            }
516        } while (!$_tmp_status['eof']);
517
518        fclose($fp);
519
520        if (!$this->_parseResponse()) {
521            return $this->fault;
522        }
523        return $this->response;
524    }
525
526    /**
527     * Sends the outgoing HTTPS request and reads and parses the response.
528     *
529     * @access private
530     *
531     * @param string $msg     Outgoing SOAP package.
532     * @param array $options  Options.
533     *
534     * @return string  Response data without HTTP headers.
535     */
536    function _sendHTTPS($msg, $options)
537    {
538        /* Check if the required curl extension is installed. */
539        if (!extension_loaded('curl')) {
540            return $this->_raiseSoapFault('CURL Extension is required for HTTPS');
541        }
542
543        $ch = curl_init();
544
545        if (isset($options['proxy_host'])) {
546            $port = isset($options['proxy_port']) ? $options['proxy_port'] : 8080;
547            curl_setopt($ch, CURLOPT_PROXY,
548                        $options['proxy_host'] . ':' . $port);
549        }
550        if (isset($options['proxy_user'])) {
551            curl_setopt($ch, CURLOPT_PROXYUSERPWD,
552                        $options['proxy_user'] . ':' . $options['proxy_pass']);
553        }
554
555        if (isset($options['user'])) {
556            curl_setopt($ch, CURLOPT_USERPWD,
557                        $options['user'] . ':' . $options['pass']);
558        }
559
560        if (!isset($options['soapaction'])) {
561            $options['soapaction'] = '';
562        }
563        if (!isset($options['headers']['Content-Type'])) {
564           $options['headers']['Content-Type'] = 'text/xml';
565        }
566        curl_setopt($ch, CURLOPT_HTTPHEADER,
567                    array('Content-Type: ' . $options['headers']['Content-Type']
568                         . ';charset=' . $this->encoding,
569                          'SOAPAction: "' . $options['soapaction'] . '"'));
570        curl_setopt($ch, CURLOPT_USERAGENT,
571                    $this->_userAgent);
572
573        if ($this->timeout) {
574            curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
575        }
576
577        curl_setopt($ch, CURLOPT_POSTFIELDS, $msg);
578        curl_setopt($ch, CURLOPT_URL, $this->url);
579        curl_setopt($ch, CURLOPT_POST, 1);
580        curl_setopt($ch, CURLOPT_FAILONERROR, 0);
581        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
582        curl_setopt($ch, CURLOPT_HEADER, 1);
583        if (defined('CURLOPT_HTTP_VERSION')) {
584            curl_setopt($ch, CURLOPT_HTTP_VERSION, 1);
585        }
586        if (!ini_get('safe_mode') && !ini_get('open_basedir')) {
587            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
588        }
589        $cookies = $this->_generateCookieHeader($options);
590        if ($cookies) {
591            curl_setopt($ch, CURLOPT_COOKIE, $cookies);
592        }
593
594        if (isset($options['curl'])) {
595            foreach ($options['curl'] as $key => $val) {
596                curl_setopt($ch, $key, $val);
597            }
598        }
599
600        // Save the outgoing XML. This doesn't quite match _sendHTTP as CURL
601        // generates the headers, but having the XML is usually the most
602        // important part for tracing/debugging.
603        $this->outgoing_payload = $msg;
604
605        $this->incoming_payload = curl_exec($ch);
606        if (!$this->incoming_payload) {
607            $m = 'curl_exec error ' . curl_errno($ch) . ' ' . curl_error($ch);
608            curl_close($ch);
609            return $this->_raiseSoapFault($m);
610        }
611        curl_close($ch);
612
613        if (!$this->_parseResponse()) {
614            return $this->fault;
615        }
616
617        return $this->response;
618    }
619
620}
Note: See TracBrowser for help on using the repository browser.