httpclient.php 7.96 KB
Newer Older
1 2
<?php
/**
Evan Prodromou's avatar
Evan Prodromou committed
3
 * StatusNet, the distributed open-source microblogging tool
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 *
 * Utility for doing HTTP-related things
 *
 * PHP version 5
 *
 * LICENCE: This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  Action
Evan Prodromou's avatar
Evan Prodromou committed
23
 * @package   StatusNet
24
 * @author    Evan Prodromou <evan@status.net>
Evan Prodromou's avatar
Evan Prodromou committed
25
 * @copyright 2009 StatusNet, Inc.
26
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
Evan Prodromou's avatar
Evan Prodromou committed
27
 * @link      http://status.net/
28 29
 */

30
if (!defined('STATUSNET')) {
31 32 33
    exit(1);
}

34 35 36
require_once 'HTTP/Request2.php';
require_once 'HTTP/Request2/Response.php';

37
/**
Evan Prodromou's avatar
Evan Prodromou committed
38
 * Useful structure for HTTP responses
39 40 41 42 43
 *
 * We make HTTP calls in several places, and we have several different
 * ways of doing them. This class hides the specifics of what underlying
 * library (curl or PHP-HTTP or whatever) that's used.
 *
44 45 46
 * This extends the HTTP_Request2_Response class with methods to get info
 * about any followed redirects.
 *
47
 * @category HTTP
48 49 50 51 52
 * @package StatusNet
 * @author Evan Prodromou <evan@status.net>
 * @author Brion Vibber <brion@status.net>
 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link http://status.net/
53
 */
54
class HTTPResponse extends HTTP_Request2_Response
55
{
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
    function __construct(HTTP_Request2_Response $response, $url, $redirects=0)
    {
        foreach (get_object_vars($response) as $key => $val) {
            $this->$key = $val;
        }
        $this->url = strval($url);
        $this->redirectCount = intval($redirects);
    }

    /**
     * Get the count of redirects that have been followed, if any.
     * @return int
     */
    function getRedirectCount()
    {
        return $this->redirectCount;
    }

    /**
     * Gets the final target URL, after any redirects have been followed.
     * @return string URL
     */
    function getUrl()
    {
        return $this->url;
    }

    /**
84
     * Check if the response is OK, generally a 200 or other 2xx status code.
85 86 87 88
     * @return bool
     */
    function isOk()
    {
89 90
        $status = $this->getStatus();
        return ($status >= 200 && $status < 300);
91
    }
92 93
}

Evan Prodromou's avatar
Evan Prodromou committed
94 95 96 97 98 99 100
/**
 * Utility class for doing HTTP client stuff
 *
 * We make HTTP calls in several places, and we have several different
 * ways of doing them. This class hides the specifics of what underlying
 * library (curl or PHP-HTTP or whatever) that's used.
 *
101 102 103 104 105 106 107
 * This extends the PEAR HTTP_Request2 package:
 * - sends StatusNet-specific User-Agent header
 * - 'follow_redirects' config option, defaulting off
 * - 'max_redirs' config option, defaulting to 10
 * - extended response class adds getRedirectCount() and getUrl() methods
 * - get() and post() convenience functions return body content directly
 *
Evan Prodromou's avatar
Evan Prodromou committed
108
 * @category HTTP
Evan Prodromou's avatar
Evan Prodromou committed
109
 * @package  StatusNet
110
 * @author   Evan Prodromou <evan@status.net>
111
 * @author   Brion Vibber <brion@status.net>
Evan Prodromou's avatar
Evan Prodromou committed
112
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
Evan Prodromou's avatar
Evan Prodromou committed
113
 * @link     http://status.net/
Evan Prodromou's avatar
Evan Prodromou committed
114 115
 */

116
class HTTPClient extends HTTP_Request2
117
{
Evan Prodromou's avatar
Evan Prodromou committed
118

119
    function __construct($url=null, $method=self::METHOD_GET, $config=array())
120
    {
121 122 123 124
        $this->config['max_redirs'] = 10;
        $this->config['follow_redirects'] = true;
        parent::__construct($url, $method, $config);
        $this->setHeader('User-Agent', $this->userAgent());
125 126
    }

127 128 129 130 131
    /**
     * Convenience/back-compat instantiator
     * @return HTTPClient
     */
    public static function start()
132
    {
133
        return new HTTPClient();
134 135
    }

136 137 138 139 140 141 142
    /**
     * Convenience function to run a GET request.
     *
     * @return HTTPResponse
     * @throws HTTP_Request2_Exception
     */
    public function get($url, $headers=array())
143
    {
144
        return $this->doRequest($url, self::METHOD_GET, $headers);
145 146
    }

147 148 149 150 151 152 153
    /**
     * Convenience function to run a HEAD request.
     *
     * @return HTTPResponse
     * @throws HTTP_Request2_Exception
     */
    public function head($url, $headers=array())
154
    {
155
        return $this->doRequest($url, self::METHOD_HEAD, $headers);
156
    }
157

158 159 160 161 162 163 164 165 166 167
    /**
     * Convenience function to POST form data.
     *
     * @param string $url
     * @param array $headers optional associative array of HTTP headers
     * @param array $data optional associative array or blob of form data to submit
     * @return HTTPResponse
     * @throws HTTP_Request2_Exception
     */
    public function post($url, $headers=array(), $data=array())
168
    {
169 170 171 172
        if ($data) {
            $this->addPostParameter($data);
        }
        return $this->doRequest($url, self::METHOD_POST, $headers);
173 174
    }

175 176 177 178 179
    /**
     * @return HTTPResponse
     * @throws HTTP_Request2_Exception
     */
    protected function doRequest($url, $method, $headers)
180
    {
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
        $this->setUrl($url);
        $this->setMethod($method);
        if ($headers) {
            foreach ($headers as $header) {
                $this->setHeader($header);
            }
        }
        $response = $this->send();
        return $response;
    }
    
    protected function log($level, $detail) {
        $method = $this->getMethod();
        $url = $this->getUrl();
        common_log($level, __CLASS__ . ": HTTP $method $url - $detail");
196
    }
197

198 199 200 201 202 203
    /**
     * Pulls up StatusNet's customized user-agent string, so services
     * we hit can track down the responsible software.
     *
     * @return string
     */
204
    function userAgent()
205
    {
206
        return "StatusNet/".STATUSNET_VERSION." (".STATUSNET_CODENAME.")";
207
    }
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259

    /**
     * Actually performs the HTTP request and returns an HTTPResponse object
     * with response body and header info.
     *
     * Wraps around parent send() to add logging and redirection processing.
     *
     * @return HTTPResponse
     * @throw HTTP_Request2_Exception
     */
    public function send()
    {
        $maxRedirs = intval($this->config['max_redirs']);
        if (empty($this->config['follow_redirects'])) {
            $maxRedirs = 0;
        }
        $redirs = 0;
        do {
            try {
                $response = parent::send();
            } catch (HTTP_Request2_Exception $e) {
                $this->log(LOG_ERR, $e->getMessage());
                throw $e;
            }
            $code = $response->getStatus();
            if ($code >= 200 && $code < 300) {
                $reason = $response->getReasonPhrase();
                $this->log(LOG_INFO, "$code $reason");
            } elseif ($code >= 300 && $code < 400) {
                $url = $this->getUrl();
                $target = $response->getHeader('Location');
                
                if (++$redirs >= $maxRedirs) {
                    common_log(LOG_ERR, __CLASS__ . ": Too many redirects: skipping $code redirect from $url to $target");
                    break;
                }
                try {
                    $this->setUrl($target);
                    $this->setHeader('Referer', $url);
                    common_log(LOG_INFO, __CLASS__ . ": Following $code redirect from $url to $target");
                    continue;
                } catch (HTTP_Request2_Exception $e) {
                    common_log(LOG_ERR, __CLASS__ . ": Invalid $code redirect from $url to $target");
                }
            } else {
                $reason = $response->getReasonPhrase();
                $this->log(LOG_ERR, "$code $reason");
            }
            break;
        } while ($maxRedirs);
        return new HTTPResponse($response, $this->getUrl(), $redirs);
    }
260
}