initial sabre upgrade (needs lots of work - to wit: authentication, redo the browser interface, and rework event export/import)
This commit is contained in:
601
vendor/sabre/http/lib/Client.php
vendored
Normal file
601
vendor/sabre/http/lib/Client.php
vendored
Normal file
@@ -0,0 +1,601 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
use Sabre\Event\EventEmitter;
|
||||
use Sabre\Uri;
|
||||
|
||||
/**
|
||||
* A rudimentary HTTP client.
|
||||
*
|
||||
* This object wraps PHP's curl extension and provides an easy way to send it a
|
||||
* Request object, and return a Response object.
|
||||
*
|
||||
* This is by no means intended as the next best HTTP client, but it does the
|
||||
* job and provides a simple integration with the rest of sabre/http.
|
||||
*
|
||||
* This client emits the following events:
|
||||
* beforeRequest(RequestInterface $request)
|
||||
* afterRequest(RequestInterface $request, ResponseInterface $response)
|
||||
* error(RequestInterface $request, ResponseInterface $response, bool &$retry, int $retryCount)
|
||||
* exception(RequestInterface $request, ClientException $e, bool &$retry, int $retryCount)
|
||||
*
|
||||
* The beforeRequest event allows you to do some last minute changes to the
|
||||
* request before it's done, such as adding authentication headers.
|
||||
*
|
||||
* The afterRequest event will be emitted after the request is completed
|
||||
* succesfully.
|
||||
*
|
||||
* If a HTTP error is returned (status code higher than 399) the error event is
|
||||
* triggered. It's possible using this event to retry the request, by setting
|
||||
* retry to true.
|
||||
*
|
||||
* The amount of times a request has retried is passed as $retryCount, which
|
||||
* can be used to avoid retrying indefinitely. The first time the event is
|
||||
* called, this will be 0.
|
||||
*
|
||||
* It's also possible to intercept specific http errors, by subscribing to for
|
||||
* example 'error:401'.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Client extends EventEmitter {
|
||||
|
||||
/**
|
||||
* List of curl settings
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $curlSettings = [];
|
||||
|
||||
/**
|
||||
* Wether or not exceptions should be thrown when a HTTP error is returned.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $throwExceptions = false;
|
||||
|
||||
/**
|
||||
* The maximum number of times we'll follow a redirect.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $maxRedirects = 5;
|
||||
|
||||
/**
|
||||
* Initializes the client.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function __construct() {
|
||||
|
||||
$this->curlSettings = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_NOBODY => false,
|
||||
CURLOPT_USERAGENT => 'sabre-http/' . Version::VERSION . ' (http://sabre.io/)',
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to a HTTP server, and returns a response.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
function send(RequestInterface $request) {
|
||||
|
||||
$this->emit('beforeRequest', [$request]);
|
||||
|
||||
$retryCount = 0;
|
||||
$redirects = 0;
|
||||
|
||||
do {
|
||||
|
||||
$doRedirect = false;
|
||||
$retry = false;
|
||||
|
||||
try {
|
||||
|
||||
$response = $this->doRequest($request);
|
||||
|
||||
$code = (int)$response->getStatus();
|
||||
|
||||
// We are doing in-PHP redirects, because curl's
|
||||
// FOLLOW_LOCATION throws errors when PHP is configured with
|
||||
// open_basedir.
|
||||
//
|
||||
// https://github.com/fruux/sabre-http/issues/12
|
||||
if (in_array($code, [301, 302, 307, 308]) && $redirects < $this->maxRedirects) {
|
||||
|
||||
$oldLocation = $request->getUrl();
|
||||
|
||||
// Creating a new instance of the request object.
|
||||
$request = clone $request;
|
||||
|
||||
// Setting the new location
|
||||
$request->setUrl(Uri\resolve(
|
||||
$oldLocation,
|
||||
$response->getHeader('Location')
|
||||
));
|
||||
|
||||
$doRedirect = true;
|
||||
$redirects++;
|
||||
|
||||
}
|
||||
|
||||
// This was a HTTP error
|
||||
if ($code >= 400) {
|
||||
|
||||
$this->emit('error', [$request, $response, &$retry, $retryCount]);
|
||||
$this->emit('error:' . $code, [$request, $response, &$retry, $retryCount]);
|
||||
|
||||
}
|
||||
|
||||
} catch (ClientException $e) {
|
||||
|
||||
$this->emit('exception', [$request, $e, &$retry, $retryCount]);
|
||||
|
||||
// If retry was still set to false, it means no event handler
|
||||
// dealt with the problem. In this case we just re-throw the
|
||||
// exception.
|
||||
if (!$retry) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($retry) {
|
||||
$retryCount++;
|
||||
}
|
||||
|
||||
} while ($retry || $doRedirect);
|
||||
|
||||
$this->emit('afterRequest', [$request, $response]);
|
||||
|
||||
if ($this->throwExceptions && $code >= 400) {
|
||||
throw new ClientHttpException($response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a HTTP request asynchronously.
|
||||
*
|
||||
* Due to the nature of PHP, you must from time to time poll to see if any
|
||||
* new responses came in.
|
||||
*
|
||||
* After calling sendAsync, you must therefore occasionally call the poll()
|
||||
* method, or wait().
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param callable $success
|
||||
* @param callable $error
|
||||
* @return void
|
||||
*/
|
||||
function sendAsync(RequestInterface $request, callable $success = null, callable $error = null) {
|
||||
|
||||
$this->emit('beforeRequest', [$request]);
|
||||
$this->sendAsyncInternal($request, $success, $error);
|
||||
$this->poll();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method checks if any http requests have gotten results, and if so,
|
||||
* call the appropriate success or error handlers.
|
||||
*
|
||||
* This method will return true if there are still requests waiting to
|
||||
* return, and false if all the work is done.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function poll() {
|
||||
|
||||
// nothing to do?
|
||||
if (!$this->curlMultiMap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
do {
|
||||
$r = curl_multi_exec(
|
||||
$this->curlMultiHandle,
|
||||
$stillRunning
|
||||
);
|
||||
} while ($r === CURLM_CALL_MULTI_PERFORM);
|
||||
|
||||
do {
|
||||
|
||||
messageQueue:
|
||||
|
||||
$status = curl_multi_info_read(
|
||||
$this->curlMultiHandle,
|
||||
$messagesInQueue
|
||||
);
|
||||
|
||||
if ($status && $status['msg'] === CURLMSG_DONE) {
|
||||
|
||||
$resourceId = intval($status['handle']);
|
||||
list(
|
||||
$request,
|
||||
$successCallback,
|
||||
$errorCallback,
|
||||
$retryCount,
|
||||
) = $this->curlMultiMap[$resourceId];
|
||||
unset($this->curlMultiMap[$resourceId]);
|
||||
$curlResult = $this->parseCurlResult(curl_multi_getcontent($status['handle']), $status['handle']);
|
||||
$retry = false;
|
||||
|
||||
if ($curlResult['status'] === self::STATUS_CURLERROR) {
|
||||
|
||||
$e = new ClientException($curlResult['curl_errmsg'], $curlResult['curl_errno']);
|
||||
$this->emit('exception', [$request, $e, &$retry, $retryCount]);
|
||||
|
||||
if ($retry) {
|
||||
$retryCount++;
|
||||
$this->sendAsyncInternal($request, $successCallback, $errorCallback, $retryCount);
|
||||
goto messageQueue;
|
||||
}
|
||||
|
||||
$curlResult['request'] = $request;
|
||||
|
||||
if ($errorCallback) {
|
||||
$errorCallback($curlResult);
|
||||
}
|
||||
|
||||
} elseif ($curlResult['status'] === self::STATUS_HTTPERROR) {
|
||||
|
||||
$this->emit('error', [$request, $curlResult['response'], &$retry, $retryCount]);
|
||||
$this->emit('error:' . $curlResult['http_code'], [$request, $curlResult['response'], &$retry, $retryCount]);
|
||||
|
||||
if ($retry) {
|
||||
|
||||
$retryCount++;
|
||||
$this->sendAsyncInternal($request, $successCallback, $errorCallback, $retryCount);
|
||||
goto messageQueue;
|
||||
|
||||
}
|
||||
|
||||
$curlResult['request'] = $request;
|
||||
|
||||
if ($errorCallback) {
|
||||
$errorCallback($curlResult);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
$this->emit('afterRequest', [$request, $curlResult['response']]);
|
||||
|
||||
if ($successCallback) {
|
||||
$successCallback($curlResult['response']);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} while ($messagesInQueue > 0);
|
||||
|
||||
return count($this->curlMultiMap) > 0;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes every HTTP request in the queue, and waits till they are all
|
||||
* completed.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wait() {
|
||||
|
||||
do {
|
||||
curl_multi_select($this->curlMultiHandle);
|
||||
$stillRunning = $this->poll();
|
||||
} while ($stillRunning);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is set to true, the Client will automatically throw exceptions
|
||||
* upon HTTP errors.
|
||||
*
|
||||
* This means that if a response came back with a status code greater than
|
||||
* or equal to 400, we will throw a ClientHttpException.
|
||||
*
|
||||
* This only works for the send() method. Throwing exceptions for
|
||||
* sendAsync() is not supported.
|
||||
*
|
||||
* @param bool $throwExceptions
|
||||
* @return void
|
||||
*/
|
||||
function setThrowExceptions($throwExceptions) {
|
||||
|
||||
$this->throwExceptions = $throwExceptions;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a CURL setting.
|
||||
*
|
||||
* These settings will be included in every HTTP request.
|
||||
*
|
||||
* @param int $name
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
function addCurlSetting($name, $value) {
|
||||
|
||||
$this->curlSettings[$name] = $value;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is responsible for performing a single request.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function doRequest(RequestInterface $request) {
|
||||
|
||||
$settings = $this->createCurlSettingsArray($request);
|
||||
|
||||
if (!$this->curlHandle) {
|
||||
$this->curlHandle = curl_init();
|
||||
}
|
||||
|
||||
curl_setopt_array($this->curlHandle, $settings);
|
||||
$response = $this->curlExec($this->curlHandle);
|
||||
$response = $this->parseCurlResult($response, $this->curlHandle);
|
||||
|
||||
if ($response['status'] === self::STATUS_CURLERROR) {
|
||||
throw new ClientException($response['curl_errmsg'], $response['curl_errno']);
|
||||
}
|
||||
|
||||
return $response['response'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached curl handle.
|
||||
*
|
||||
* By keeping this resource around for the lifetime of this object, things
|
||||
* like persistent connections are possible.
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
private $curlHandle;
|
||||
|
||||
/**
|
||||
* Handler for curl_multi requests.
|
||||
*
|
||||
* The first time sendAsync is used, this will be created.
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
private $curlMultiHandle;
|
||||
|
||||
/**
|
||||
* Has a list of curl handles, as well as their associated success and
|
||||
* error callbacks.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $curlMultiMap = [];
|
||||
|
||||
/**
|
||||
* Turns a RequestInterface object into an array with settings that can be
|
||||
* fed to curl_setopt
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @return array
|
||||
*/
|
||||
protected function createCurlSettingsArray(RequestInterface $request) {
|
||||
|
||||
$settings = $this->curlSettings;
|
||||
|
||||
switch ($request->getMethod()) {
|
||||
case 'HEAD' :
|
||||
$settings[CURLOPT_NOBODY] = true;
|
||||
$settings[CURLOPT_CUSTOMREQUEST] = 'HEAD';
|
||||
$settings[CURLOPT_POSTFIELDS] = '';
|
||||
$settings[CURLOPT_PUT] = false;
|
||||
break;
|
||||
case 'GET' :
|
||||
$settings[CURLOPT_CUSTOMREQUEST] = 'GET';
|
||||
$settings[CURLOPT_POSTFIELDS] = '';
|
||||
$settings[CURLOPT_PUT] = false;
|
||||
break;
|
||||
default :
|
||||
$body = $request->getBody();
|
||||
if (is_resource($body)) {
|
||||
// This needs to be set to PUT, regardless of the actual
|
||||
// method used. Without it, INFILE will be ignored for some
|
||||
// reason.
|
||||
$settings[CURLOPT_PUT] = true;
|
||||
$settings[CURLOPT_INFILE] = $request->getBody();
|
||||
} else {
|
||||
// For security we cast this to a string. If somehow an array could
|
||||
// be passed here, it would be possible for an attacker to use @ to
|
||||
// post local files.
|
||||
$settings[CURLOPT_POSTFIELDS] = (string)$body;
|
||||
}
|
||||
$settings[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
$nHeaders = [];
|
||||
foreach ($request->getHeaders() as $key => $values) {
|
||||
|
||||
foreach ($values as $value) {
|
||||
$nHeaders[] = $key . ': ' . $value;
|
||||
}
|
||||
|
||||
}
|
||||
$settings[CURLOPT_HTTPHEADER] = $nHeaders;
|
||||
$settings[CURLOPT_URL] = $request->getUrl();
|
||||
// FIXME: CURLOPT_PROTOCOLS is currently unsupported by HHVM
|
||||
if (defined('CURLOPT_PROTOCOLS')) {
|
||||
$settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
}
|
||||
// FIXME: CURLOPT_REDIR_PROTOCOLS is currently unsupported by HHVM
|
||||
if (defined('CURLOPT_REDIR_PROTOCOLS')) {
|
||||
$settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
}
|
||||
|
||||
return $settings;
|
||||
|
||||
}
|
||||
|
||||
const STATUS_SUCCESS = 0;
|
||||
const STATUS_CURLERROR = 1;
|
||||
const STATUS_HTTPERROR = 2;
|
||||
|
||||
/**
|
||||
* Parses the result of a curl call in a format that's a bit more
|
||||
* convenient to work with.
|
||||
*
|
||||
* The method returns an array with the following elements:
|
||||
* * status - one of the 3 STATUS constants.
|
||||
* * curl_errno - A curl error number. Only set if status is
|
||||
* STATUS_CURLERROR.
|
||||
* * curl_errmsg - A current error message. Only set if status is
|
||||
* STATUS_CURLERROR.
|
||||
* * response - Response object. Only set if status is STATUS_SUCCESS, or
|
||||
* STATUS_HTTPERROR.
|
||||
* * http_code - HTTP status code, as an int. Only set if Only set if
|
||||
* status is STATUS_SUCCESS, or STATUS_HTTPERROR
|
||||
*
|
||||
* @param string $response
|
||||
* @param resource $curlHandle
|
||||
* @return Response
|
||||
*/
|
||||
protected function parseCurlResult($response, $curlHandle) {
|
||||
|
||||
list(
|
||||
$curlInfo,
|
||||
$curlErrNo,
|
||||
$curlErrMsg
|
||||
) = $this->curlStuff($curlHandle);
|
||||
|
||||
if ($curlErrNo) {
|
||||
return [
|
||||
'status' => self::STATUS_CURLERROR,
|
||||
'curl_errno' => $curlErrNo,
|
||||
'curl_errmsg' => $curlErrMsg,
|
||||
];
|
||||
}
|
||||
|
||||
$headerBlob = substr($response, 0, $curlInfo['header_size']);
|
||||
// In the case of 204 No Content, strlen($response) == $curlInfo['header_size].
|
||||
// This will cause substr($response, $curlInfo['header_size']) return FALSE instead of NULL
|
||||
// An exception will be thrown when calling getBodyAsString then
|
||||
$responseBody = substr($response, $curlInfo['header_size']) ?: null;
|
||||
|
||||
unset($response);
|
||||
|
||||
// In the case of 100 Continue, or redirects we'll have multiple lists
|
||||
// of headers for each separate HTTP response. We can easily split this
|
||||
// because they are separated by \r\n\r\n
|
||||
$headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n"));
|
||||
|
||||
// We only care about the last set of headers
|
||||
$headerBlob = $headerBlob[count($headerBlob) - 1];
|
||||
|
||||
// Splitting headers
|
||||
$headerBlob = explode("\r\n", $headerBlob);
|
||||
|
||||
$response = new Response();
|
||||
$response->setStatus($curlInfo['http_code']);
|
||||
|
||||
foreach ($headerBlob as $header) {
|
||||
$parts = explode(':', $header, 2);
|
||||
if (count($parts) == 2) {
|
||||
$response->addHeader(trim($parts[0]), trim($parts[1]));
|
||||
}
|
||||
}
|
||||
|
||||
$response->setBody($responseBody);
|
||||
|
||||
$httpCode = intval($response->getStatus());
|
||||
|
||||
return [
|
||||
'status' => $httpCode >= 400 ? self::STATUS_HTTPERROR : self::STATUS_SUCCESS,
|
||||
'response' => $response,
|
||||
'http_code' => $httpCode,
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an asynchronous HTTP request.
|
||||
*
|
||||
* We keep this in a separate method, so we can call it without triggering
|
||||
* the beforeRequest event and don't do the poll().
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param callable $success
|
||||
* @param callable $error
|
||||
* @param int $retryCount
|
||||
*/
|
||||
protected function sendAsyncInternal(RequestInterface $request, callable $success, callable $error, $retryCount = 0) {
|
||||
|
||||
if (!$this->curlMultiHandle) {
|
||||
$this->curlMultiHandle = curl_multi_init();
|
||||
}
|
||||
$curl = curl_init();
|
||||
curl_setopt_array(
|
||||
$curl,
|
||||
$this->createCurlSettingsArray($request)
|
||||
);
|
||||
curl_multi_add_handle($this->curlMultiHandle, $curl);
|
||||
$this->curlMultiMap[intval($curl)] = [
|
||||
$request,
|
||||
$success,
|
||||
$error,
|
||||
$retryCount
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
|
||||
/**
|
||||
* Calls curl_exec
|
||||
*
|
||||
* This method exists so it can easily be overridden and mocked.
|
||||
*
|
||||
* @param resource $curlHandle
|
||||
* @return string
|
||||
*/
|
||||
protected function curlExec($curlHandle) {
|
||||
|
||||
return curl_exec($curlHandle);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a bunch of information about a curl request.
|
||||
*
|
||||
* This method exists so it can easily be overridden and mocked.
|
||||
*
|
||||
* @param resource $curlHandle
|
||||
* @return array
|
||||
*/
|
||||
protected function curlStuff($curlHandle) {
|
||||
|
||||
return [
|
||||
curl_getinfo($curlHandle),
|
||||
curl_errno($curlHandle),
|
||||
curl_error($curlHandle),
|
||||
];
|
||||
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user