diff --git a/src/future/http/BaseHTTPFuture.php b/src/future/http/BaseHTTPFuture.php index 5d0c0f80..14562ebe 100644 --- a/src/future/http/BaseHTTPFuture.php +++ b/src/future/http/BaseHTTPFuture.php @@ -1,447 +1,458 @@ resolve(); * * This is an abstract base class which defines the API that HTTP futures * conform to. Concrete implementations are available in @{class:HTTPFuture} * and @{class:HTTPSFuture}. All futures return a tuple * when resolved; status is an object of class @{class:HTTPFutureResponseStatus} * and may represent any of a wide variety of errors in the transport layer, * a support library, or the actual HTTP exchange. * * @task create Creating a New Request * @task config Configuring the Request * @task resolve Resolving the Request * @task internal Internals */ abstract class BaseHTTPFuture extends Future { private $method = 'GET'; private $timeout = 300.0; private $headers = array(); private $uri; private $data; private $expect; - + private $disableContentDecoding; /* -( Creating a New Request )--------------------------------------------- */ /** * Build a new future which will make an HTTP request to a given URI, with * some optional data payload. Since this class is abstract you can't actually * instantiate it; instead, build a new @{class:HTTPFuture} or * @{class:HTTPSFuture}. * * @param string Fully-qualified URI to send a request to. * @param mixed String or array to include in the request. Strings will be * transmitted raw; arrays will be encoded and sent as * 'application/x-www-form-urlencoded'. * @task create */ final public function __construct($uri, $data = array()) { $this->setURI((string)$uri); $this->setData($data); } /* -( Configuring the Request )-------------------------------------------- */ /** * Set a timeout for the service call. If the request hasn't resolved yet, * the future will resolve with a status that indicates the request timed * out. You can determine if a status is a timeout status by calling * isTimeout() on the status object. * * @param float Maximum timeout, in seconds. * @return this * @task config */ public function setTimeout($timeout) { $this->timeout = $timeout; return $this; } /** * Get the currently configured timeout. * * @return float Maximum number of seconds the request will execute for. * @task config */ public function getTimeout() { return $this->timeout; } /** * Select the HTTP method (e.g., "GET", "POST", "PUT") to use for the request. * By default, requests use "GET". * * @param string HTTP method name. * @return this * @task config */ final public function setMethod($method) { static $supported_methods = array( 'GET' => true, 'POST' => true, 'PUT' => true, 'DELETE' => true, ); if (empty($supported_methods[$method])) { throw new Exception( pht( "The HTTP method '%s' is not supported. Supported HTTP methods ". "are: %s.", $method, implode(', ', array_keys($supported_methods)))); } $this->method = $method; return $this; } /** * Get the HTTP method the request will use. * * @return string HTTP method name, like "GET". * @task config */ final public function getMethod() { return $this->method; } /** * Set the URI to send the request to. Note that this is also a constructor * parameter. * * @param string URI to send the request to. * @return this * @task config */ public function setURI($uri) { $this->uri = (string)$uri; return $this; } /** * Get the fully-qualified URI the request will be made to. * * @return string URI the request will be sent to. * @task config */ public function getURI() { return $this->uri; } /** * Provide data to send along with the request. Note that this is also a * constructor parameter; it may be more convenient to provide it there. Data * must be a string (in which case it will be sent raw) or an array (in which * case it will be encoded and sent as 'application/x-www-form-urlencoded'). * * @param mixed Data to send with the request. * @return this * @task config */ public function setData($data) { if (!is_string($data) && !is_array($data)) { throw new Exception(pht('Data parameter must be an array or string.')); } $this->data = $data; return $this; } /** * Get the data which will be sent with the request. * * @return mixed Data which will be sent. * @task config */ public function getData() { return $this->data; } /** * Add an HTTP header to the request. The same header name can be specified * more than once, which will cause multiple headers to be sent. * * @param string Header name, like "Accept-Language". * @param string Header value, like "en-us". * @return this * @task config */ public function addHeader($name, $value) { $this->headers[] = array($name, $value); return $this; } /** * Get headers which will be sent with the request. Optionally, you can * provide a filter, which will return only headers with that name. For * example: * * $all_headers = $future->getHeaders(); * $just_user_agent = $future->getHeaders('User-Agent'); * * In either case, an array with all (or all matching) headers is returned. * * @param string|null Optional filter, which selects only headers with that * name if provided. * @return array List of all (or all matching) headers. * @task config */ public function getHeaders($filter = null) { $filter = strtolower($filter); $result = array(); foreach ($this->headers as $header) { list($name, $value) = $header; if (!$filter || ($filter == strtolower($name))) { $result[] = $header; } } return $result; } /** * Set the status codes that are expected in the response. * If set, isError on the status object will return true for status codes * that are not in the input array. Otherwise, isError will be true for any * HTTP status code outside the 2xx range (notwithstanding other errors such * as connection or transport issues). * * @param array|null List of expected HTTP status codes. * * @return this * @task config */ public function setExpectStatus($status_codes) { $this->expect = $status_codes; return $this; } /** * Return list of expected status codes, or null if not set. * * @return array|null List of expected status codes. */ public function getExpectStatus() { return $this->expect; } /** * Add a HTTP basic authentication header to the request. * * @param string Username to authenticate with. * @param PhutilOpaqueEnvelope Password to authenticate with. * @return this * @task config */ public function setHTTPBasicAuthCredentials( $username, PhutilOpaqueEnvelope $password) { $password_plaintext = $password->openEnvelope(); $credentials = base64_encode($username.':'.$password_plaintext); return $this->addHeader('Authorization', 'Basic '.$credentials); } public function getHTTPRequestByteLength() { // NOTE: This isn't very accurate, but it's only used by the "--trace" // call profiler to help pick out huge requests. $data = $this->getData(); if (is_scalar($data)) { return strlen($data); } return strlen(phutil_build_http_querystring($data)); } + public function setDisableContentDecoding($disable_decoding) { + $this->disableContentDecoding = $disable_decoding; + return $this; + } + + public function getDisableContentDecoding() { + return $this->disableContentDecoding; + } + /* -( Resolving the Request )---------------------------------------------- */ /** * Exception-oriented @{method:resolve}. Throws if the status indicates an * error occurred. * * @return tuple HTTP request result tuple. * @task resolve */ final public function resolvex() { $result = $this->resolve(); list($status, $body, $headers) = $result; if ($status->isError()) { throw $status; } return array($body, $headers); } /* -( Internals )---------------------------------------------------------- */ /** * Parse a raw HTTP response into a tuple. * * @param string Raw HTTP response. * @return tuple Valid resolution tuple. * @task internal */ protected function parseRawHTTPResponse($raw_response) { $rex_base = "@^(?P.*?)\r?\n\r?\n(?P.*)$@s"; $rex_head = "@^HTTP/\S+ (?P\d+) ?(?P.*?)". "(?:\r?\n(?P.*))?$@s"; // We need to parse one or more header blocks in case we got any // "HTTP/1.X 100 Continue" nonsense back as part of the response. This // happens with HTTPS requests, at the least. $response = $raw_response; while (true) { $matches = null; if (!preg_match($rex_base, $response, $matches)) { return $this->buildMalformedResult($raw_response); } $head = $matches['head']; $body = $matches['body']; if (!preg_match($rex_head, $head, $matches)) { return $this->buildMalformedResult($raw_response); } $response_code = (int)$matches['code']; $response_status = strtolower($matches['status']); if ($response_code == 100) { // This is HTTP/1.X 100 Continue, so this whole chunk is moot. $response = $body; } else if (($response_code == 200) && ($response_status == 'connection established')) { // When tunneling through an HTTPS proxy, we get an initial header // block like "HTTP/1.X 200 Connection established", then newlines, // then the normal response. Drop this chunk. $response = $body; } else { $headers = $this->parseHeaders(idx($matches, 'headers')); break; } } - $content_encoding = null; - foreach ($headers as $header) { - list($name, $value) = $header; - $name = phutil_utf8_strtolower($name); - if (!strcasecmp($name, 'Content-Encoding')) { - $content_encoding = $value; - break; + if (!$this->getDisableContentDecoding()) { + $content_encoding = null; + foreach ($headers as $header) { + list($name, $value) = $header; + $name = phutil_utf8_strtolower($name); + if (!strcasecmp($name, 'Content-Encoding')) { + $content_encoding = $value; + break; + } } - } - switch ($content_encoding) { - case 'gzip': - $decoded_body = gzdecode($body); - if ($decoded_body === false) { - return $this->buildMalformedResult($raw_response); - } - $body = $decoded_body; - break; + switch ($content_encoding) { + case 'gzip': + $decoded_body = @gzdecode($body); + if ($decoded_body === false) { + return $this->buildMalformedResult($raw_response); + } + $body = $decoded_body; + break; + } } $status = new HTTPFutureHTTPResponseStatus( $response_code, $body, $headers, $this->expect); return array($status, $body, $headers); } /** * Parse an HTTP header block. * * @param string Raw HTTP headers. * @return list List of HTTP header tuples. * @task internal */ protected function parseHeaders($head_raw) { $rex_header = '@^(?P.*?):\s*(?P.*)$@'; $headers = array(); if (!$head_raw) { return $headers; } $headers_raw = preg_split("/\r?\n/", $head_raw); foreach ($headers_raw as $header) { $m = null; if (preg_match($rex_header, $header, $m)) { $headers[] = array($m['name'], $m['value']); } else { $headers[] = array($header, null); } } return $headers; } /** * Find value of the first header with given name. * * @param list List of headers from `resolve()`. * @param string Case insensitive header name. * @return string Value of the header or null if not found. * @task resolve */ public static function getHeader(array $headers, $search) { assert_instances_of($headers, 'array'); foreach ($headers as $header) { list($name, $value) = $header; if (strcasecmp($name, $search) == 0) { return $value; } } return null; } /** * Build a result tuple indicating a parse error resulting from a malformed * HTTP response. * * @return tuple Valid resolution tuple. * @task internal */ protected function buildMalformedResult($raw_response) { $body = null; $headers = array(); $status = new HTTPFutureParseResponseStatus( HTTPFutureParseResponseStatus::ERROR_MALFORMED_RESPONSE, $raw_response); return array($status, $body, $headers); } }