Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • dmj/selbstzeugnisse-hab
  • goermar/selbstzeugnisse-hab
2 results
Show changes
Showing
with 3517 additions and 0 deletions
<?php
namespace GuzzleHttp\Exception;
/**
* Exception when a server error is encountered (5xx codes)
*/
class ServerException extends BadResponseException
{
}
<?php
namespace GuzzleHttp\Exception;
class TooManyRedirectsException extends RequestException
{
}
<?php
namespace GuzzleHttp\Exception;
class TransferException extends \RuntimeException implements GuzzleException
{
}
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\LazyOpenStream;
use GuzzleHttp\TransferStats;
use Psr\Http\Message\RequestInterface;
/**
* Creates curl resources from a request
*/
class CurlFactory implements CurlFactoryInterface
{
const CURL_VERSION_STR = 'curl_version';
const LOW_CURL_VERSION_NUMBER = '7.21.2';
/** @var array */
private $handles = [];
/** @var int Total number of idle handles to keep in cache */
private $maxHandles;
/**
* @param int $maxHandles Maximum number of idle handles.
*/
public function __construct($maxHandles)
{
$this->maxHandles = $maxHandles;
}
public function create(RequestInterface $request, array $options)
{
if (isset($options['curl']['body_as_string'])) {
$options['_body_as_string'] = $options['curl']['body_as_string'];
unset($options['curl']['body_as_string']);
}
$easy = new EasyHandle;
$easy->request = $request;
$easy->options = $options;
$conf = $this->getDefaultConf($easy);
$this->applyMethod($easy, $conf);
$this->applyHandlerOptions($easy, $conf);
$this->applyHeaders($easy, $conf);
unset($conf['_headers']);
// Add handler options from the request configuration options
if (isset($options['curl'])) {
$conf = array_replace($conf, $options['curl']);
}
$conf[CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
$easy->handle = $this->handles
? array_pop($this->handles)
: curl_init();
curl_setopt_array($easy->handle, $conf);
return $easy;
}
public function release(EasyHandle $easy)
{
$resource = $easy->handle;
unset($easy->handle);
if (count($this->handles) >= $this->maxHandles) {
curl_close($resource);
} else {
// Remove all callback functions as they can hold onto references
// and are not cleaned up by curl_reset. Using curl_setopt_array
// does not work for some reason, so removing each one
// individually.
curl_setopt($resource, CURLOPT_HEADERFUNCTION, null);
curl_setopt($resource, CURLOPT_READFUNCTION, null);
curl_setopt($resource, CURLOPT_WRITEFUNCTION, null);
curl_setopt($resource, CURLOPT_PROGRESSFUNCTION, null);
curl_reset($resource);
$this->handles[] = $resource;
}
}
/**
* Completes a cURL transaction, either returning a response promise or a
* rejected promise.
*
* @param callable $handler
* @param EasyHandle $easy
* @param CurlFactoryInterface $factory Dictates how the handle is released
*
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public static function finish(
callable $handler,
EasyHandle $easy,
CurlFactoryInterface $factory
) {
if (isset($easy->options['on_stats'])) {
self::invokeStats($easy);
}
if (!$easy->response || $easy->errno) {
return self::finishError($handler, $easy, $factory);
}
// Return the response if it is present and there is no error.
$factory->release($easy);
// Rewind the body of the response if possible.
$body = $easy->response->getBody();
if ($body->isSeekable()) {
$body->rewind();
}
return new FulfilledPromise($easy->response);
}
private static function invokeStats(EasyHandle $easy)
{
$curlStats = curl_getinfo($easy->handle);
$curlStats['appconnect_time'] = curl_getinfo($easy->handle, CURLINFO_APPCONNECT_TIME);
$stats = new TransferStats(
$easy->request,
$easy->response,
$curlStats['total_time'],
$easy->errno,
$curlStats
);
call_user_func($easy->options['on_stats'], $stats);
}
private static function finishError(
callable $handler,
EasyHandle $easy,
CurlFactoryInterface $factory
) {
// Get error information and release the handle to the factory.
$ctx = [
'errno' => $easy->errno,
'error' => curl_error($easy->handle),
'appconnect_time' => curl_getinfo($easy->handle, CURLINFO_APPCONNECT_TIME),
] + curl_getinfo($easy->handle);
$ctx[self::CURL_VERSION_STR] = curl_version()['version'];
$factory->release($easy);
// Retry when nothing is present or when curl failed to rewind.
if (empty($easy->options['_err_message'])
&& (!$easy->errno || $easy->errno == 65)
) {
return self::retryFailedRewind($handler, $easy, $ctx);
}
return self::createRejection($easy, $ctx);
}
private static function createRejection(EasyHandle $easy, array $ctx)
{
static $connectionErrors = [
CURLE_OPERATION_TIMEOUTED => true,
CURLE_COULDNT_RESOLVE_HOST => true,
CURLE_COULDNT_CONNECT => true,
CURLE_SSL_CONNECT_ERROR => true,
CURLE_GOT_NOTHING => true,
];
// If an exception was encountered during the onHeaders event, then
// return a rejected promise that wraps that exception.
if ($easy->onHeadersException) {
return \GuzzleHttp\Promise\rejection_for(
new RequestException(
'An error was encountered during the on_headers event',
$easy->request,
$easy->response,
$easy->onHeadersException,
$ctx
)
);
}
if (version_compare($ctx[self::CURL_VERSION_STR], self::LOW_CURL_VERSION_NUMBER)) {
$message = sprintf(
'cURL error %s: %s (%s)',
$ctx['errno'],
$ctx['error'],
'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'
);
} else {
$message = sprintf(
'cURL error %s: %s (%s) for %s',
$ctx['errno'],
$ctx['error'],
'see https://curl.haxx.se/libcurl/c/libcurl-errors.html',
$easy->request->getUri()
);
}
// Create a connection exception if it was a specific error code.
$error = isset($connectionErrors[$easy->errno])
? new ConnectException($message, $easy->request, null, $ctx)
: new RequestException($message, $easy->request, $easy->response, null, $ctx);
return \GuzzleHttp\Promise\rejection_for($error);
}
private function getDefaultConf(EasyHandle $easy)
{
$conf = [
'_headers' => $easy->request->getHeaders(),
CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(),
CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''),
CURLOPT_RETURNTRANSFER => false,
CURLOPT_HEADER => false,
CURLOPT_CONNECTTIMEOUT => 150,
];
if (defined('CURLOPT_PROTOCOLS')) {
$conf[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
}
$version = $easy->request->getProtocolVersion();
if ($version == 1.1) {
$conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
} elseif ($version == 2.0) {
$conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
} else {
$conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
}
return $conf;
}
private function applyMethod(EasyHandle $easy, array &$conf)
{
$body = $easy->request->getBody();
$size = $body->getSize();
if ($size === null || $size > 0) {
$this->applyBody($easy->request, $easy->options, $conf);
return;
}
$method = $easy->request->getMethod();
if ($method === 'PUT' || $method === 'POST') {
// See http://tools.ietf.org/html/rfc7230#section-3.3.2
if (!$easy->request->hasHeader('Content-Length')) {
$conf[CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
}
} elseif ($method === 'HEAD') {
$conf[CURLOPT_NOBODY] = true;
unset(
$conf[CURLOPT_WRITEFUNCTION],
$conf[CURLOPT_READFUNCTION],
$conf[CURLOPT_FILE],
$conf[CURLOPT_INFILE]
);
}
}
private function applyBody(RequestInterface $request, array $options, array &$conf)
{
$size = $request->hasHeader('Content-Length')
? (int) $request->getHeaderLine('Content-Length')
: null;
// Send the body as a string if the size is less than 1MB OR if the
// [curl][body_as_string] request value is set.
if (($size !== null && $size < 1000000) ||
!empty($options['_body_as_string'])
) {
$conf[CURLOPT_POSTFIELDS] = (string) $request->getBody();
// Don't duplicate the Content-Length header
$this->removeHeader('Content-Length', $conf);
$this->removeHeader('Transfer-Encoding', $conf);
} else {
$conf[CURLOPT_UPLOAD] = true;
if ($size !== null) {
$conf[CURLOPT_INFILESIZE] = $size;
$this->removeHeader('Content-Length', $conf);
}
$body = $request->getBody();
if ($body->isSeekable()) {
$body->rewind();
}
$conf[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
return $body->read($length);
};
}
// If the Expect header is not present, prevent curl from adding it
if (!$request->hasHeader('Expect')) {
$conf[CURLOPT_HTTPHEADER][] = 'Expect:';
}
// cURL sometimes adds a content-type by default. Prevent this.
if (!$request->hasHeader('Content-Type')) {
$conf[CURLOPT_HTTPHEADER][] = 'Content-Type:';
}
}
private function applyHeaders(EasyHandle $easy, array &$conf)
{
foreach ($conf['_headers'] as $name => $values) {
foreach ($values as $value) {
$value = (string) $value;
if ($value === '') {
// cURL requires a special format for empty headers.
// See https://github.com/guzzle/guzzle/issues/1882 for more details.
$conf[CURLOPT_HTTPHEADER][] = "$name;";
} else {
$conf[CURLOPT_HTTPHEADER][] = "$name: $value";
}
}
}
// Remove the Accept header if one was not set
if (!$easy->request->hasHeader('Accept')) {
$conf[CURLOPT_HTTPHEADER][] = 'Accept:';
}
}
/**
* Remove a header from the options array.
*
* @param string $name Case-insensitive header to remove
* @param array $options Array of options to modify
*/
private function removeHeader($name, array &$options)
{
foreach (array_keys($options['_headers']) as $key) {
if (!strcasecmp($key, $name)) {
unset($options['_headers'][$key]);
return;
}
}
}
private function applyHandlerOptions(EasyHandle $easy, array &$conf)
{
$options = $easy->options;
if (isset($options['verify'])) {
if ($options['verify'] === false) {
unset($conf[CURLOPT_CAINFO]);
$conf[CURLOPT_SSL_VERIFYHOST] = 0;
$conf[CURLOPT_SSL_VERIFYPEER] = false;
} else {
$conf[CURLOPT_SSL_VERIFYHOST] = 2;
$conf[CURLOPT_SSL_VERIFYPEER] = true;
if (is_string($options['verify'])) {
// Throw an error if the file/folder/link path is not valid or doesn't exist.
if (!file_exists($options['verify'])) {
throw new \InvalidArgumentException(
"SSL CA bundle not found: {$options['verify']}"
);
}
// If it's a directory or a link to a directory use CURLOPT_CAPATH.
// If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
if (is_dir($options['verify']) ||
(is_link($options['verify']) && is_dir(readlink($options['verify'])))) {
$conf[CURLOPT_CAPATH] = $options['verify'];
} else {
$conf[CURLOPT_CAINFO] = $options['verify'];
}
}
}
}
if (!empty($options['decode_content'])) {
$accept = $easy->request->getHeaderLine('Accept-Encoding');
if ($accept) {
$conf[CURLOPT_ENCODING] = $accept;
} else {
$conf[CURLOPT_ENCODING] = '';
// Don't let curl send the header over the wire
$conf[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
}
}
if (isset($options['sink'])) {
$sink = $options['sink'];
if (!is_string($sink)) {
$sink = \GuzzleHttp\Psr7\stream_for($sink);
} elseif (!is_dir(dirname($sink))) {
// Ensure that the directory exists before failing in curl.
throw new \RuntimeException(sprintf(
'Directory %s does not exist for sink value of %s',
dirname($sink),
$sink
));
} else {
$sink = new LazyOpenStream($sink, 'w+');
}
$easy->sink = $sink;
$conf[CURLOPT_WRITEFUNCTION] = function ($ch, $write) use ($sink) {
return $sink->write($write);
};
} else {
// Use a default temp stream if no sink was set.
$conf[CURLOPT_FILE] = fopen('php://temp', 'w+');
$easy->sink = Psr7\stream_for($conf[CURLOPT_FILE]);
}
$timeoutRequiresNoSignal = false;
if (isset($options['timeout'])) {
$timeoutRequiresNoSignal |= $options['timeout'] < 1;
$conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
}
// CURL default value is CURL_IPRESOLVE_WHATEVER
if (isset($options['force_ip_resolve'])) {
if ('v4' === $options['force_ip_resolve']) {
$conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
} elseif ('v6' === $options['force_ip_resolve']) {
$conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6;
}
}
if (isset($options['connect_timeout'])) {
$timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
$conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
}
if ($timeoutRequiresNoSignal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
$conf[CURLOPT_NOSIGNAL] = true;
}
if (isset($options['proxy'])) {
if (!is_array($options['proxy'])) {
$conf[CURLOPT_PROXY] = $options['proxy'];
} else {
$scheme = $easy->request->getUri()->getScheme();
if (isset($options['proxy'][$scheme])) {
$host = $easy->request->getUri()->getHost();
if (!isset($options['proxy']['no']) ||
!\GuzzleHttp\is_host_in_noproxy($host, $options['proxy']['no'])
) {
$conf[CURLOPT_PROXY] = $options['proxy'][$scheme];
}
}
}
}
if (isset($options['cert'])) {
$cert = $options['cert'];
if (is_array($cert)) {
$conf[CURLOPT_SSLCERTPASSWD] = $cert[1];
$cert = $cert[0];
}
if (!file_exists($cert)) {
throw new \InvalidArgumentException(
"SSL certificate not found: {$cert}"
);
}
$conf[CURLOPT_SSLCERT] = $cert;
}
if (isset($options['ssl_key'])) {
if (is_array($options['ssl_key'])) {
if (count($options['ssl_key']) === 2) {
list($sslKey, $conf[CURLOPT_SSLKEYPASSWD]) = $options['ssl_key'];
} else {
list($sslKey) = $options['ssl_key'];
}
}
$sslKey = isset($sslKey) ? $sslKey: $options['ssl_key'];
if (!file_exists($sslKey)) {
throw new \InvalidArgumentException(
"SSL private key not found: {$sslKey}"
);
}
$conf[CURLOPT_SSLKEY] = $sslKey;
}
if (isset($options['progress'])) {
$progress = $options['progress'];
if (!is_callable($progress)) {
throw new \InvalidArgumentException(
'progress client option must be callable'
);
}
$conf[CURLOPT_NOPROGRESS] = false;
$conf[CURLOPT_PROGRESSFUNCTION] = function () use ($progress) {
$args = func_get_args();
// PHP 5.5 pushed the handle onto the start of the args
if (is_resource($args[0])) {
array_shift($args);
}
call_user_func_array($progress, $args);
};
}
if (!empty($options['debug'])) {
$conf[CURLOPT_STDERR] = \GuzzleHttp\debug_resource($options['debug']);
$conf[CURLOPT_VERBOSE] = true;
}
}
/**
* This function ensures that a response was set on a transaction. If one
* was not set, then the request is retried if possible. This error
* typically means you are sending a payload, curl encountered a
* "Connection died, retrying a fresh connect" error, tried to rewind the
* stream, and then encountered a "necessary data rewind wasn't possible"
* error, causing the request to be sent through curl_multi_info_read()
* without an error status.
*/
private static function retryFailedRewind(
callable $handler,
EasyHandle $easy,
array $ctx
) {
try {
// Only rewind if the body has been read from.
$body = $easy->request->getBody();
if ($body->tell() > 0) {
$body->rewind();
}
} catch (\RuntimeException $e) {
$ctx['error'] = 'The connection unexpectedly failed without '
. 'providing an error. The request would have been retried, '
. 'but attempting to rewind the request body failed. '
. 'Exception: ' . $e;
return self::createRejection($easy, $ctx);
}
// Retry no more than 3 times before giving up.
if (!isset($easy->options['_curl_retries'])) {
$easy->options['_curl_retries'] = 1;
} elseif ($easy->options['_curl_retries'] == 2) {
$ctx['error'] = 'The cURL request was retried 3 times '
. 'and did not succeed. The most likely reason for the failure '
. 'is that cURL was unable to rewind the body of the request '
. 'and subsequent retries resulted in the same error. Turn on '
. 'the debug option to see what went wrong. See '
. 'https://bugs.php.net/bug.php?id=47204 for more information.';
return self::createRejection($easy, $ctx);
} else {
$easy->options['_curl_retries']++;
}
return $handler($easy->request, $easy->options);
}
private function createHeaderFn(EasyHandle $easy)
{
if (isset($easy->options['on_headers'])) {
$onHeaders = $easy->options['on_headers'];
if (!is_callable($onHeaders)) {
throw new \InvalidArgumentException('on_headers must be callable');
}
} else {
$onHeaders = null;
}
return function ($ch, $h) use (
$onHeaders,
$easy,
&$startingResponse
) {
$value = trim($h);
if ($value === '') {
$startingResponse = true;
$easy->createResponse();
if ($onHeaders !== null) {
try {
$onHeaders($easy->response);
} catch (\Exception $e) {
// Associate the exception with the handle and trigger
// a curl header write error by returning 0.
$easy->onHeadersException = $e;
return -1;
}
}
} elseif ($startingResponse) {
$startingResponse = false;
$easy->headers = [$value];
} else {
$easy->headers[] = $value;
}
return strlen($h);
};
}
}
<?php
namespace GuzzleHttp\Handler;
use Psr\Http\Message\RequestInterface;
interface CurlFactoryInterface
{
/**
* Creates a cURL handle resource.
*
* @param RequestInterface $request Request
* @param array $options Transfer options
*
* @return EasyHandle
* @throws \RuntimeException when an option cannot be applied
*/
public function create(RequestInterface $request, array $options);
/**
* Release an easy handle, allowing it to be reused or closed.
*
* This function must call unset on the easy handle's "handle" property.
*
* @param EasyHandle $easy
*/
public function release(EasyHandle $easy);
}
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Psr7;
use Psr\Http\Message\RequestInterface;
/**
* HTTP handler that uses cURL easy handles as a transport layer.
*
* When using the CurlHandler, custom curl options can be specified as an
* associative array of curl option constants mapping to values in the
* **curl** key of the "client" key of the request.
*/
class CurlHandler
{
/** @var CurlFactoryInterface */
private $factory;
/**
* Accepts an associative array of options:
*
* - factory: Optional curl factory used to create cURL handles.
*
* @param array $options Array of options to use with the handler
*/
public function __construct(array $options = [])
{
$this->factory = isset($options['handle_factory'])
? $options['handle_factory']
: new CurlFactory(3);
}
public function __invoke(RequestInterface $request, array $options)
{
if (isset($options['delay'])) {
usleep($options['delay'] * 1000);
}
$easy = $this->factory->create($request, $options);
curl_exec($easy->handle);
$easy->errno = curl_errno($easy->handle);
return CurlFactory::finish($this, $easy, $this->factory);
}
}
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Promise as P;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface;
/**
* Returns an asynchronous response using curl_multi_* functions.
*
* When using the CurlMultiHandler, custom curl options can be specified as an
* associative array of curl option constants mapping to values in the
* **curl** key of the provided request options.
*
* @property resource $_mh Internal use only. Lazy loaded multi-handle.
*/
class CurlMultiHandler
{
/** @var CurlFactoryInterface */
private $factory;
private $selectTimeout;
private $active;
private $handles = [];
private $delays = [];
private $options = [];
/**
* This handler accepts the following options:
*
* - handle_factory: An optional factory used to create curl handles
* - select_timeout: Optional timeout (in seconds) to block before timing
* out while selecting curl handles. Defaults to 1 second.
* - options: An associative array of CURLMOPT_* options and
* corresponding values for curl_multi_setopt()
*
* @param array $options
*/
public function __construct(array $options = [])
{
$this->factory = isset($options['handle_factory'])
? $options['handle_factory'] : new CurlFactory(50);
if (isset($options['select_timeout'])) {
$this->selectTimeout = $options['select_timeout'];
} elseif ($selectTimeout = getenv('GUZZLE_CURL_SELECT_TIMEOUT')) {
$this->selectTimeout = $selectTimeout;
} else {
$this->selectTimeout = 1;
}
$this->options = isset($options['options']) ? $options['options'] : [];
}
public function __get($name)
{
if ($name === '_mh') {
$this->_mh = curl_multi_init();
foreach ($this->options as $option => $value) {
// A warning is raised in case of a wrong option.
curl_multi_setopt($this->_mh, $option, $value);
}
// Further calls to _mh will return the value directly, without entering the
// __get() method at all.
return $this->_mh;
}
throw new \BadMethodCallException();
}
public function __destruct()
{
if (isset($this->_mh)) {
curl_multi_close($this->_mh);
unset($this->_mh);
}
}
public function __invoke(RequestInterface $request, array $options)
{
$easy = $this->factory->create($request, $options);
$id = (int) $easy->handle;
$promise = new Promise(
[$this, 'execute'],
function () use ($id) {
return $this->cancel($id);
}
);
$this->addRequest(['easy' => $easy, 'deferred' => $promise]);
return $promise;
}
/**
* Ticks the curl event loop.
*/
public function tick()
{
// Add any delayed handles if needed.
if ($this->delays) {
$currentTime = Utils::currentTime();
foreach ($this->delays as $id => $delay) {
if ($currentTime >= $delay) {
unset($this->delays[$id]);
curl_multi_add_handle(
$this->_mh,
$this->handles[$id]['easy']->handle
);
}
}
}
// Step through the task queue which may add additional requests.
P\queue()->run();
if ($this->active &&
curl_multi_select($this->_mh, $this->selectTimeout) === -1
) {
// Perform a usleep if a select returns -1.
// See: https://bugs.php.net/bug.php?id=61141
usleep(250);
}
while (curl_multi_exec($this->_mh, $this->active) === CURLM_CALL_MULTI_PERFORM);
$this->processMessages();
}
/**
* Runs until all outstanding connections have completed.
*/
public function execute()
{
$queue = P\queue();
while ($this->handles || !$queue->isEmpty()) {
// If there are no transfers, then sleep for the next delay
if (!$this->active && $this->delays) {
usleep($this->timeToNext());
}
$this->tick();
}
}
private function addRequest(array $entry)
{
$easy = $entry['easy'];
$id = (int) $easy->handle;
$this->handles[$id] = $entry;
if (empty($easy->options['delay'])) {
curl_multi_add_handle($this->_mh, $easy->handle);
} else {
$this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000);
}
}
/**
* Cancels a handle from sending and removes references to it.
*
* @param int $id Handle ID to cancel and remove.
*
* @return bool True on success, false on failure.
*/
private function cancel($id)
{
// Cannot cancel if it has been processed.
if (!isset($this->handles[$id])) {
return false;
}
$handle = $this->handles[$id]['easy']->handle;
unset($this->delays[$id], $this->handles[$id]);
curl_multi_remove_handle($this->_mh, $handle);
curl_close($handle);
return true;
}
private function processMessages()
{
while ($done = curl_multi_info_read($this->_mh)) {
$id = (int) $done['handle'];
curl_multi_remove_handle($this->_mh, $done['handle']);
if (!isset($this->handles[$id])) {
// Probably was cancelled.
continue;
}
$entry = $this->handles[$id];
unset($this->handles[$id], $this->delays[$id]);
$entry['easy']->errno = $done['result'];
$entry['deferred']->resolve(
CurlFactory::finish(
$this,
$entry['easy'],
$this->factory
)
);
}
}
private function timeToNext()
{
$currentTime = Utils::currentTime();
$nextTime = PHP_INT_MAX;
foreach ($this->delays as $time) {
if ($time < $nextTime) {
$nextTime = $time;
}
}
return max(0, $nextTime - $currentTime) * 1000000;
}
}
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* Represents a cURL easy handle and the data it populates.
*
* @internal
*/
final class EasyHandle
{
/** @var resource cURL resource */
public $handle;
/** @var StreamInterface Where data is being written */
public $sink;
/** @var array Received HTTP headers so far */
public $headers = [];
/** @var ResponseInterface Received response (if any) */
public $response;
/** @var RequestInterface Request being sent */
public $request;
/** @var array Request options */
public $options = [];
/** @var int cURL error number (if any) */
public $errno = 0;
/** @var \Exception Exception during on_headers (if any) */
public $onHeadersException;
/**
* Attach a response to the easy handle based on the received headers.
*
* @throws \RuntimeException if no headers have been received.
*/
public function createResponse()
{
if (empty($this->headers)) {
throw new \RuntimeException('No headers have been received');
}
// HTTP-version SP status-code SP reason-phrase
$startLine = explode(' ', array_shift($this->headers), 3);
$headers = \GuzzleHttp\headers_from_lines($this->headers);
$normalizedKeys = \GuzzleHttp\normalize_header_keys($headers);
if (!empty($this->options['decode_content'])
&& isset($normalizedKeys['content-encoding'])
) {
$headers['x-encoded-content-encoding']
= $headers[$normalizedKeys['content-encoding']];
unset($headers[$normalizedKeys['content-encoding']]);
if (isset($normalizedKeys['content-length'])) {
$headers['x-encoded-content-length']
= $headers[$normalizedKeys['content-length']];
$bodyLength = (int) $this->sink->getSize();
if ($bodyLength) {
$headers[$normalizedKeys['content-length']] = $bodyLength;
} else {
unset($headers[$normalizedKeys['content-length']]);
}
}
}
// Attach a response to the easy handle with the parsed headers.
$this->response = new Response(
$startLine[1],
$headers,
$this->sink,
substr($startLine[0], 5),
isset($startLine[2]) ? (string) $startLine[2] : null
);
}
public function __get($name)
{
$msg = $name === 'handle'
? 'The EasyHandle has been released'
: 'Invalid property: ' . $name;
throw new \BadMethodCallException($msg);
}
}
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\TransferStats;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Handler that returns responses or throw exceptions from a queue.
*/
class MockHandler implements \Countable
{
private $queue = [];
private $lastRequest;
private $lastOptions;
private $onFulfilled;
private $onRejected;
/**
* Creates a new MockHandler that uses the default handler stack list of
* middlewares.
*
* @param array $queue Array of responses, callables, or exceptions.
* @param callable $onFulfilled Callback to invoke when the return value is fulfilled.
* @param callable $onRejected Callback to invoke when the return value is rejected.
*
* @return HandlerStack
*/
public static function createWithMiddleware(
array $queue = null,
callable $onFulfilled = null,
callable $onRejected = null
) {
return HandlerStack::create(new self($queue, $onFulfilled, $onRejected));
}
/**
* The passed in value must be an array of
* {@see Psr7\Http\Message\ResponseInterface} objects, Exceptions,
* callables, or Promises.
*
* @param array $queue
* @param callable $onFulfilled Callback to invoke when the return value is fulfilled.
* @param callable $onRejected Callback to invoke when the return value is rejected.
*/
public function __construct(
array $queue = null,
callable $onFulfilled = null,
callable $onRejected = null
) {
$this->onFulfilled = $onFulfilled;
$this->onRejected = $onRejected;
if ($queue) {
call_user_func_array([$this, 'append'], $queue);
}
}
public function __invoke(RequestInterface $request, array $options)
{
if (!$this->queue) {
throw new \OutOfBoundsException('Mock queue is empty');
}
if (isset($options['delay']) && is_numeric($options['delay'])) {
usleep($options['delay'] * 1000);
}
$this->lastRequest = $request;
$this->lastOptions = $options;
$response = array_shift($this->queue);
if (isset($options['on_headers'])) {
if (!is_callable($options['on_headers'])) {
throw new \InvalidArgumentException('on_headers must be callable');
}
try {
$options['on_headers']($response);
} catch (\Exception $e) {
$msg = 'An error was encountered during the on_headers event';
$response = new RequestException($msg, $request, $response, $e);
}
}
if (is_callable($response)) {
$response = call_user_func($response, $request, $options);
}
$response = $response instanceof \Exception
? \GuzzleHttp\Promise\rejection_for($response)
: \GuzzleHttp\Promise\promise_for($response);
return $response->then(
function ($value) use ($request, $options) {
$this->invokeStats($request, $options, $value);
if ($this->onFulfilled) {
call_user_func($this->onFulfilled, $value);
}
if (isset($options['sink'])) {
$contents = (string) $value->getBody();
$sink = $options['sink'];
if (is_resource($sink)) {
fwrite($sink, $contents);
} elseif (is_string($sink)) {
file_put_contents($sink, $contents);
} elseif ($sink instanceof \Psr\Http\Message\StreamInterface) {
$sink->write($contents);
}
}
return $value;
},
function ($reason) use ($request, $options) {
$this->invokeStats($request, $options, null, $reason);
if ($this->onRejected) {
call_user_func($this->onRejected, $reason);
}
return \GuzzleHttp\Promise\rejection_for($reason);
}
);
}
/**
* Adds one or more variadic requests, exceptions, callables, or promises
* to the queue.
*/
public function append()
{
foreach (func_get_args() as $value) {
if ($value instanceof ResponseInterface
|| $value instanceof \Exception
|| $value instanceof PromiseInterface
|| is_callable($value)
) {
$this->queue[] = $value;
} else {
throw new \InvalidArgumentException('Expected a response or '
. 'exception. Found ' . \GuzzleHttp\describe_type($value));
}
}
}
/**
* Get the last received request.
*
* @return RequestInterface
*/
public function getLastRequest()
{
return $this->lastRequest;
}
/**
* Get the last received request options.
*
* @return array
*/
public function getLastOptions()
{
return $this->lastOptions;
}
/**
* Returns the number of remaining items in the queue.
*
* @return int
*/
public function count()
{
return count($this->queue);
}
public function reset()
{
$this->queue = [];
}
private function invokeStats(
RequestInterface $request,
array $options,
ResponseInterface $response = null,
$reason = null
) {
if (isset($options['on_stats'])) {
$transferTime = isset($options['transfer_time']) ? $options['transfer_time'] : 0;
$stats = new TransferStats($request, $response, $transferTime, $reason);
call_user_func($options['on_stats'], $stats);
}
}
}
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\RequestOptions;
use Psr\Http\Message\RequestInterface;
/**
* Provides basic proxies for handlers.
*/
class Proxy
{
/**
* Sends synchronous requests to a specific handler while sending all other
* requests to another handler.
*
* @param callable $default Handler used for normal responses
* @param callable $sync Handler used for synchronous responses.
*
* @return callable Returns the composed handler.
*/
public static function wrapSync(
callable $default,
callable $sync
) {
return function (RequestInterface $request, array $options) use ($default, $sync) {
return empty($options[RequestOptions::SYNCHRONOUS])
? $default($request, $options)
: $sync($request, $options);
};
}
/**
* Sends streaming requests to a streaming compatible handler while sending
* all other requests to a default handler.
*
* This, for example, could be useful for taking advantage of the
* performance benefits of curl while still supporting true streaming
* through the StreamHandler.
*
* @param callable $default Handler used for non-streaming responses
* @param callable $streaming Handler used for streaming responses
*
* @return callable Returns the composed handler.
*/
public static function wrapStreaming(
callable $default,
callable $streaming
) {
return function (RequestInterface $request, array $options) use ($default, $streaming) {
return empty($options['stream'])
? $default($request, $options)
: $streaming($request, $options);
};
}
}
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7;
use GuzzleHttp\TransferStats;
use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* HTTP handler that uses PHP's HTTP stream wrapper.
*/
class StreamHandler
{
private $lastHeaders = [];
/**
* Sends an HTTP request.
*
* @param RequestInterface $request Request to send.
* @param array $options Request transfer options.
*
* @return PromiseInterface
*/
public function __invoke(RequestInterface $request, array $options)
{
// Sleep if there is a delay specified.
if (isset($options['delay'])) {
usleep($options['delay'] * 1000);
}
$startTime = isset($options['on_stats']) ? Utils::currentTime() : null;
try {
// Does not support the expect header.
$request = $request->withoutHeader('Expect');
// Append a content-length header if body size is zero to match
// cURL's behavior.
if (0 === $request->getBody()->getSize()) {
$request = $request->withHeader('Content-Length', '0');
}
return $this->createResponse(
$request,
$options,
$this->createStream($request, $options),
$startTime
);
} catch (\InvalidArgumentException $e) {
throw $e;
} catch (\Exception $e) {
// Determine if the error was a networking error.
$message = $e->getMessage();
// This list can probably get more comprehensive.
if (strpos($message, 'getaddrinfo') // DNS lookup failed
|| strpos($message, 'Connection refused')
|| strpos($message, "couldn't connect to host") // error on HHVM
|| strpos($message, "connection attempt failed")
) {
$e = new ConnectException($e->getMessage(), $request, $e);
}
$e = RequestException::wrapException($request, $e);
$this->invokeStats($options, $request, $startTime, null, $e);
return \GuzzleHttp\Promise\rejection_for($e);
}
}
private function invokeStats(
array $options,
RequestInterface $request,
$startTime,
ResponseInterface $response = null,
$error = null
) {
if (isset($options['on_stats'])) {
$stats = new TransferStats(
$request,
$response,
Utils::currentTime() - $startTime,
$error,
[]
);
call_user_func($options['on_stats'], $stats);
}
}
private function createResponse(
RequestInterface $request,
array $options,
$stream,
$startTime
) {
$hdrs = $this->lastHeaders;
$this->lastHeaders = [];
$parts = explode(' ', array_shift($hdrs), 3);
$ver = explode('/', $parts[0])[1];
$status = $parts[1];
$reason = isset($parts[2]) ? $parts[2] : null;
$headers = \GuzzleHttp\headers_from_lines($hdrs);
list($stream, $headers) = $this->checkDecode($options, $headers, $stream);
$stream = Psr7\stream_for($stream);
$sink = $stream;
if (strcasecmp('HEAD', $request->getMethod())) {
$sink = $this->createSink($stream, $options);
}
$response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
if (isset($options['on_headers'])) {
try {
$options['on_headers']($response);
} catch (\Exception $e) {
$msg = 'An error was encountered during the on_headers event';
$ex = new RequestException($msg, $request, $response, $e);
return \GuzzleHttp\Promise\rejection_for($ex);
}
}
// Do not drain when the request is a HEAD request because they have
// no body.
if ($sink !== $stream) {
$this->drain(
$stream,
$sink,
$response->getHeaderLine('Content-Length')
);
}
$this->invokeStats($options, $request, $startTime, $response, null);
return new FulfilledPromise($response);
}
private function createSink(StreamInterface $stream, array $options)
{
if (!empty($options['stream'])) {
return $stream;
}
$sink = isset($options['sink'])
? $options['sink']
: fopen('php://temp', 'r+');
return is_string($sink)
? new Psr7\LazyOpenStream($sink, 'w+')
: Psr7\stream_for($sink);
}
private function checkDecode(array $options, array $headers, $stream)
{
// Automatically decode responses when instructed.
if (!empty($options['decode_content'])) {
$normalizedKeys = \GuzzleHttp\normalize_header_keys($headers);
if (isset($normalizedKeys['content-encoding'])) {
$encoding = $headers[$normalizedKeys['content-encoding']];
if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') {
$stream = new Psr7\InflateStream(
Psr7\stream_for($stream)
);
$headers['x-encoded-content-encoding']
= $headers[$normalizedKeys['content-encoding']];
// Remove content-encoding header
unset($headers[$normalizedKeys['content-encoding']]);
// Fix content-length header
if (isset($normalizedKeys['content-length'])) {
$headers['x-encoded-content-length']
= $headers[$normalizedKeys['content-length']];
$length = (int) $stream->getSize();
if ($length === 0) {
unset($headers[$normalizedKeys['content-length']]);
} else {
$headers[$normalizedKeys['content-length']] = [$length];
}
}
}
}
}
return [$stream, $headers];
}
/**
* Drains the source stream into the "sink" client option.
*
* @param StreamInterface $source
* @param StreamInterface $sink
* @param string $contentLength Header specifying the amount of
* data to read.
*
* @return StreamInterface
* @throws \RuntimeException when the sink option is invalid.
*/
private function drain(
StreamInterface $source,
StreamInterface $sink,
$contentLength
) {
// If a content-length header is provided, then stop reading once
// that number of bytes has been read. This can prevent infinitely
// reading from a stream when dealing with servers that do not honor
// Connection: Close headers.
Psr7\copy_to_stream(
$source,
$sink,
(strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1
);
$sink->seek(0);
$source->close();
return $sink;
}
/**
* Create a resource and check to ensure it was created successfully
*
* @param callable $callback Callable that returns stream resource
*
* @return resource
* @throws \RuntimeException on error
*/
private function createResource(callable $callback)
{
$errors = null;
set_error_handler(function ($_, $msg, $file, $line) use (&$errors) {
$errors[] = [
'message' => $msg,
'file' => $file,
'line' => $line
];
return true;
});
$resource = $callback();
restore_error_handler();
if (!$resource) {
$message = 'Error creating resource: ';
foreach ($errors as $err) {
foreach ($err as $key => $value) {
$message .= "[$key] $value" . PHP_EOL;
}
}
throw new \RuntimeException(trim($message));
}
return $resource;
}
private function createStream(RequestInterface $request, array $options)
{
static $methods;
if (!$methods) {
$methods = array_flip(get_class_methods(__CLASS__));
}
// HTTP/1.1 streams using the PHP stream wrapper require a
// Connection: close header
if ($request->getProtocolVersion() == '1.1'
&& !$request->hasHeader('Connection')
) {
$request = $request->withHeader('Connection', 'close');
}
// Ensure SSL is verified by default
if (!isset($options['verify'])) {
$options['verify'] = true;
}
$params = [];
$context = $this->getDefaultContext($request);
if (isset($options['on_headers']) && !is_callable($options['on_headers'])) {
throw new \InvalidArgumentException('on_headers must be callable');
}
if (!empty($options)) {
foreach ($options as $key => $value) {
$method = "add_{$key}";
if (isset($methods[$method])) {
$this->{$method}($request, $context, $value, $params);
}
}
}
if (isset($options['stream_context'])) {
if (!is_array($options['stream_context'])) {
throw new \InvalidArgumentException('stream_context must be an array');
}
$context = array_replace_recursive(
$context,
$options['stream_context']
);
}
// Microsoft NTLM authentication only supported with curl handler
if (isset($options['auth'])
&& is_array($options['auth'])
&& isset($options['auth'][2])
&& 'ntlm' == $options['auth'][2]
) {
throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');
}
$uri = $this->resolveHost($request, $options);
$context = $this->createResource(
function () use ($context, $params) {
return stream_context_create($context, $params);
}
);
return $this->createResource(
function () use ($uri, &$http_response_header, $context, $options) {
$resource = fopen((string) $uri, 'r', null, $context);
$this->lastHeaders = $http_response_header;
if (isset($options['read_timeout'])) {
$readTimeout = $options['read_timeout'];
$sec = (int) $readTimeout;
$usec = ($readTimeout - $sec) * 100000;
stream_set_timeout($resource, $sec, $usec);
}
return $resource;
}
);
}
private function resolveHost(RequestInterface $request, array $options)
{
$uri = $request->getUri();
if (isset($options['force_ip_resolve']) && !filter_var($uri->getHost(), FILTER_VALIDATE_IP)) {
if ('v4' === $options['force_ip_resolve']) {
$records = dns_get_record($uri->getHost(), DNS_A);
if (!isset($records[0]['ip'])) {
throw new ConnectException(
sprintf(
"Could not resolve IPv4 address for host '%s'",
$uri->getHost()
),
$request
);
}
$uri = $uri->withHost($records[0]['ip']);
} elseif ('v6' === $options['force_ip_resolve']) {
$records = dns_get_record($uri->getHost(), DNS_AAAA);
if (!isset($records[0]['ipv6'])) {
throw new ConnectException(
sprintf(
"Could not resolve IPv6 address for host '%s'",
$uri->getHost()
),
$request
);
}
$uri = $uri->withHost('[' . $records[0]['ipv6'] . ']');
}
}
return $uri;
}
private function getDefaultContext(RequestInterface $request)
{
$headers = '';
foreach ($request->getHeaders() as $name => $value) {
foreach ($value as $val) {
$headers .= "$name: $val\r\n";
}
}
$context = [
'http' => [
'method' => $request->getMethod(),
'header' => $headers,
'protocol_version' => $request->getProtocolVersion(),
'ignore_errors' => true,
'follow_location' => 0,
],
];
$body = (string) $request->getBody();
if (!empty($body)) {
$context['http']['content'] = $body;
// Prevent the HTTP handler from adding a Content-Type header.
if (!$request->hasHeader('Content-Type')) {
$context['http']['header'] .= "Content-Type:\r\n";
}
}
$context['http']['header'] = rtrim($context['http']['header']);
return $context;
}
private function add_proxy(RequestInterface $request, &$options, $value, &$params)
{
if (!is_array($value)) {
$options['http']['proxy'] = $value;
} else {
$scheme = $request->getUri()->getScheme();
if (isset($value[$scheme])) {
if (!isset($value['no'])
|| !\GuzzleHttp\is_host_in_noproxy(
$request->getUri()->getHost(),
$value['no']
)
) {
$options['http']['proxy'] = $value[$scheme];
}
}
}
}
private function add_timeout(RequestInterface $request, &$options, $value, &$params)
{
if ($value > 0) {
$options['http']['timeout'] = $value;
}
}
private function add_verify(RequestInterface $request, &$options, $value, &$params)
{
if ($value === true) {
// PHP 5.6 or greater will find the system cert by default. When
// < 5.6, use the Guzzle bundled cacert.
if (PHP_VERSION_ID < 50600) {
$options['ssl']['cafile'] = \GuzzleHttp\default_ca_bundle();
}
} elseif (is_string($value)) {
$options['ssl']['cafile'] = $value;
if (!file_exists($value)) {
throw new \RuntimeException("SSL CA bundle not found: $value");
}
} elseif ($value === false) {
$options['ssl']['verify_peer'] = false;
$options['ssl']['verify_peer_name'] = false;
return;
} else {
throw new \InvalidArgumentException('Invalid verify request option');
}
$options['ssl']['verify_peer'] = true;
$options['ssl']['verify_peer_name'] = true;
$options['ssl']['allow_self_signed'] = false;
}
private function add_cert(RequestInterface $request, &$options, $value, &$params)
{
if (is_array($value)) {
$options['ssl']['passphrase'] = $value[1];
$value = $value[0];
}
if (!file_exists($value)) {
throw new \RuntimeException("SSL certificate not found: {$value}");
}
$options['ssl']['local_cert'] = $value;
}
private function add_progress(RequestInterface $request, &$options, $value, &$params)
{
$this->addNotification(
$params,
function ($code, $a, $b, $c, $transferred, $total) use ($value) {
if ($code == STREAM_NOTIFY_PROGRESS) {
$value($total, $transferred, null, null);
}
}
);
}
private function add_debug(RequestInterface $request, &$options, $value, &$params)
{
if ($value === false) {
return;
}
static $map = [
STREAM_NOTIFY_CONNECT => 'CONNECT',
STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',
STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT',
STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS',
STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS',
STREAM_NOTIFY_REDIRECTED => 'REDIRECTED',
STREAM_NOTIFY_PROGRESS => 'PROGRESS',
STREAM_NOTIFY_FAILURE => 'FAILURE',
STREAM_NOTIFY_COMPLETED => 'COMPLETED',
STREAM_NOTIFY_RESOLVE => 'RESOLVE',
];
static $args = ['severity', 'message', 'message_code',
'bytes_transferred', 'bytes_max'];
$value = \GuzzleHttp\debug_resource($value);
$ident = $request->getMethod() . ' ' . $request->getUri()->withFragment('');
$this->addNotification(
$params,
function () use ($ident, $value, $map, $args) {
$passed = func_get_args();
$code = array_shift($passed);
fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
foreach (array_filter($passed) as $i => $v) {
fwrite($value, $args[$i] . ': "' . $v . '" ');
}
fwrite($value, "\n");
}
);
}
private function addNotification(array &$params, callable $notify)
{
// Wrap the existing function if needed.
if (!isset($params['notification'])) {
$params['notification'] = $notify;
} else {
$params['notification'] = $this->callArray([
$params['notification'],
$notify
]);
}
}
private function callArray(array $functions)
{
return function () use ($functions) {
$args = func_get_args();
foreach ($functions as $fn) {
call_user_func_array($fn, $args);
}
};
}
}
<?php
namespace GuzzleHttp;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Creates a composed Guzzle handler function by stacking middlewares on top of
* an HTTP handler function.
*/
class HandlerStack
{
/** @var callable|null */
private $handler;
/** @var array */
private $stack = [];
/** @var callable|null */
private $cached;
/**
* Creates a default handler stack that can be used by clients.
*
* The returned handler will wrap the provided handler or use the most
* appropriate default handler for your system. The returned HandlerStack has
* support for cookies, redirects, HTTP error exceptions, and preparing a body
* before sending.
*
* The returned handler stack can be passed to a client in the "handler"
* option.
*
* @param callable $handler HTTP handler function to use with the stack. If no
* handler is provided, the best handler for your
* system will be utilized.
*
* @return HandlerStack
*/
public static function create(callable $handler = null)
{
$stack = new self($handler ?: choose_handler());
$stack->push(Middleware::httpErrors(), 'http_errors');
$stack->push(Middleware::redirect(), 'allow_redirects');
$stack->push(Middleware::cookies(), 'cookies');
$stack->push(Middleware::prepareBody(), 'prepare_body');
return $stack;
}
/**
* @param callable $handler Underlying HTTP handler.
*/
public function __construct(callable $handler = null)
{
$this->handler = $handler;
}
/**
* Invokes the handler stack as a composed handler
*
* @param RequestInterface $request
* @param array $options
*
* @return ResponseInterface|PromiseInterface
*/
public function __invoke(RequestInterface $request, array $options)
{
$handler = $this->resolve();
return $handler($request, $options);
}
/**
* Dumps a string representation of the stack.
*
* @return string
*/
public function __toString()
{
$depth = 0;
$stack = [];
if ($this->handler) {
$stack[] = "0) Handler: " . $this->debugCallable($this->handler);
}
$result = '';
foreach (array_reverse($this->stack) as $tuple) {
$depth++;
$str = "{$depth}) Name: '{$tuple[1]}', ";
$str .= "Function: " . $this->debugCallable($tuple[0]);
$result = "> {$str}\n{$result}";
$stack[] = $str;
}
foreach (array_keys($stack) as $k) {
$result .= "< {$stack[$k]}\n";
}
return $result;
}
/**
* Set the HTTP handler that actually returns a promise.
*
* @param callable $handler Accepts a request and array of options and
* returns a Promise.
*/
public function setHandler(callable $handler)
{
$this->handler = $handler;
$this->cached = null;
}
/**
* Returns true if the builder has a handler.
*
* @return bool
*/
public function hasHandler()
{
return (bool) $this->handler;
}
/**
* Unshift a middleware to the bottom of the stack.
*
* @param callable $middleware Middleware function
* @param string $name Name to register for this middleware.
*/
public function unshift(callable $middleware, $name = null)
{
array_unshift($this->stack, [$middleware, $name]);
$this->cached = null;
}
/**
* Push a middleware to the top of the stack.
*
* @param callable $middleware Middleware function
* @param string $name Name to register for this middleware.
*/
public function push(callable $middleware, $name = '')
{
$this->stack[] = [$middleware, $name];
$this->cached = null;
}
/**
* Add a middleware before another middleware by name.
*
* @param string $findName Middleware to find
* @param callable $middleware Middleware function
* @param string $withName Name to register for this middleware.
*/
public function before($findName, callable $middleware, $withName = '')
{
$this->splice($findName, $withName, $middleware, true);
}
/**
* Add a middleware after another middleware by name.
*
* @param string $findName Middleware to find
* @param callable $middleware Middleware function
* @param string $withName Name to register for this middleware.
*/
public function after($findName, callable $middleware, $withName = '')
{
$this->splice($findName, $withName, $middleware, false);
}
/**
* Remove a middleware by instance or name from the stack.
*
* @param callable|string $remove Middleware to remove by instance or name.
*/
public function remove($remove)
{
$this->cached = null;
$idx = is_callable($remove) ? 0 : 1;
$this->stack = array_values(array_filter(
$this->stack,
function ($tuple) use ($idx, $remove) {
return $tuple[$idx] !== $remove;
}
));
}
/**
* Compose the middleware and handler into a single callable function.
*
* @return callable
*/
public function resolve()
{
if (!$this->cached) {
if (!($prev = $this->handler)) {
throw new \LogicException('No handler has been specified');
}
foreach (array_reverse($this->stack) as $fn) {
$prev = $fn[0]($prev);
}
$this->cached = $prev;
}
return $this->cached;
}
/**
* @param string $name
* @return int
*/
private function findByName($name)
{
foreach ($this->stack as $k => $v) {
if ($v[1] === $name) {
return $k;
}
}
throw new \InvalidArgumentException("Middleware not found: $name");
}
/**
* Splices a function into the middleware list at a specific position.
*
* @param string $findName
* @param string $withName
* @param callable $middleware
* @param bool $before
*/
private function splice($findName, $withName, callable $middleware, $before)
{
$this->cached = null;
$idx = $this->findByName($findName);
$tuple = [$middleware, $withName];
if ($before) {
if ($idx === 0) {
array_unshift($this->stack, $tuple);
} else {
$replacement = [$tuple, $this->stack[$idx]];
array_splice($this->stack, $idx, 1, $replacement);
}
} elseif ($idx === count($this->stack) - 1) {
$this->stack[] = $tuple;
} else {
$replacement = [$this->stack[$idx], $tuple];
array_splice($this->stack, $idx, 1, $replacement);
}
}
/**
* Provides a debug string for a given callable.
*
* @param array|callable $fn Function to write as a string.
*
* @return string
*/
private function debugCallable($fn)
{
if (is_string($fn)) {
return "callable({$fn})";
}
if (is_array($fn)) {
return is_string($fn[0])
? "callable({$fn[0]}::{$fn[1]})"
: "callable(['" . get_class($fn[0]) . "', '{$fn[1]}'])";
}
return 'callable(' . spl_object_hash($fn) . ')';
}
}
<?php
namespace GuzzleHttp;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Formats log messages using variable substitutions for requests, responses,
* and other transactional data.
*
* The following variable substitutions are supported:
*
* - {request}: Full HTTP request message
* - {response}: Full HTTP response message
* - {ts}: ISO 8601 date in GMT
* - {date_iso_8601} ISO 8601 date in GMT
* - {date_common_log} Apache common log date using the configured timezone.
* - {host}: Host of the request
* - {method}: Method of the request
* - {uri}: URI of the request
* - {version}: Protocol version
* - {target}: Request target of the request (path + query + fragment)
* - {hostname}: Hostname of the machine that sent the request
* - {code}: Status code of the response (if available)
* - {phrase}: Reason phrase of the response (if available)
* - {error}: Any error messages (if available)
* - {req_header_*}: Replace `*` with the lowercased name of a request header to add to the message
* - {res_header_*}: Replace `*` with the lowercased name of a response header to add to the message
* - {req_headers}: Request headers
* - {res_headers}: Response headers
* - {req_body}: Request body
* - {res_body}: Response body
*/
class MessageFormatter
{
/**
* Apache Common Log Format.
* @link http://httpd.apache.org/docs/2.4/logs.html#common
* @var string
*/
const CLF = "{hostname} {req_header_User-Agent} - [{date_common_log}] \"{method} {target} HTTP/{version}\" {code} {res_header_Content-Length}";
const DEBUG = ">>>>>>>>\n{request}\n<<<<<<<<\n{response}\n--------\n{error}";
const SHORT = '[{ts}] "{method} {target} HTTP/{version}" {code}';
/** @var string Template used to format log messages */
private $template;
/**
* @param string $template Log message template
*/
public function __construct($template = self::CLF)
{
$this->template = $template ?: self::CLF;
}
/**
* Returns a formatted message string.
*
* @param RequestInterface $request Request that was sent
* @param ResponseInterface $response Response that was received
* @param \Exception $error Exception that was received
*
* @return string
*/
public function format(
RequestInterface $request,
ResponseInterface $response = null,
\Exception $error = null
) {
$cache = [];
return preg_replace_callback(
'/{\s*([A-Za-z_\-\.0-9]+)\s*}/',
function (array $matches) use ($request, $response, $error, &$cache) {
if (isset($cache[$matches[1]])) {
return $cache[$matches[1]];
}
$result = '';
switch ($matches[1]) {
case 'request':
$result = Psr7\str($request);
break;
case 'response':
$result = $response ? Psr7\str($response) : '';
break;
case 'req_headers':
$result = trim($request->getMethod()
. ' ' . $request->getRequestTarget())
. ' HTTP/' . $request->getProtocolVersion() . "\r\n"
. $this->headers($request);
break;
case 'res_headers':
$result = $response ?
sprintf(
'HTTP/%s %d %s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
) . "\r\n" . $this->headers($response)
: 'NULL';
break;
case 'req_body':
$result = $request->getBody();
break;
case 'res_body':
$result = $response ? $response->getBody() : 'NULL';
break;
case 'ts':
case 'date_iso_8601':
$result = gmdate('c');
break;
case 'date_common_log':
$result = date('d/M/Y:H:i:s O');
break;
case 'method':
$result = $request->getMethod();
break;
case 'version':
$result = $request->getProtocolVersion();
break;
case 'uri':
case 'url':
$result = $request->getUri();
break;
case 'target':
$result = $request->getRequestTarget();
break;
case 'req_version':
$result = $request->getProtocolVersion();
break;
case 'res_version':
$result = $response
? $response->getProtocolVersion()
: 'NULL';
break;
case 'host':
$result = $request->getHeaderLine('Host');
break;
case 'hostname':
$result = gethostname();
break;
case 'code':
$result = $response ? $response->getStatusCode() : 'NULL';
break;
case 'phrase':
$result = $response ? $response->getReasonPhrase() : 'NULL';
break;
case 'error':
$result = $error ? $error->getMessage() : 'NULL';
break;
default:
// handle prefixed dynamic headers
if (strpos($matches[1], 'req_header_') === 0) {
$result = $request->getHeaderLine(substr($matches[1], 11));
} elseif (strpos($matches[1], 'res_header_') === 0) {
$result = $response
? $response->getHeaderLine(substr($matches[1], 11))
: 'NULL';
}
}
$cache[$matches[1]] = $result;
return $result;
},
$this->template
);
}
/**
* Get headers from message as string
*
* @return string
*/
private function headers(MessageInterface $message)
{
$result = '';
foreach ($message->getHeaders() as $name => $values) {
$result .= $name . ': ' . implode(', ', $values) . "\r\n";
}
return trim($result);
}
}
<?php
namespace GuzzleHttp;
use GuzzleHttp\Cookie\CookieJarInterface;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Psr7;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
/**
* Functions used to create and wrap handlers with handler middleware.
*/
final class Middleware
{
/**
* Middleware that adds cookies to requests.
*
* The options array must be set to a CookieJarInterface in order to use
* cookies. This is typically handled for you by a client.
*
* @return callable Returns a function that accepts the next handler.
*/
public static function cookies()
{
return function (callable $handler) {
return function ($request, array $options) use ($handler) {
if (empty($options['cookies'])) {
return $handler($request, $options);
} elseif (!($options['cookies'] instanceof CookieJarInterface)) {
throw new \InvalidArgumentException('cookies must be an instance of GuzzleHttp\Cookie\CookieJarInterface');
}
$cookieJar = $options['cookies'];
$request = $cookieJar->withCookieHeader($request);
return $handler($request, $options)
->then(
function ($response) use ($cookieJar, $request) {
$cookieJar->extractCookies($request, $response);
return $response;
}
);
};
};
}
/**
* Middleware that throws exceptions for 4xx or 5xx responses when the
* "http_error" request option is set to true.
*
* @return callable Returns a function that accepts the next handler.
*/
public static function httpErrors()
{
return function (callable $handler) {
return function ($request, array $options) use ($handler) {
if (empty($options['http_errors'])) {
return $handler($request, $options);
}
return $handler($request, $options)->then(
function (ResponseInterface $response) use ($request) {
$code = $response->getStatusCode();
if ($code < 400) {
return $response;
}
throw RequestException::create($request, $response);
}
);
};
};
}
/**
* Middleware that pushes history data to an ArrayAccess container.
*
* @param array|\ArrayAccess $container Container to hold the history (by reference).
*
* @return callable Returns a function that accepts the next handler.
* @throws \InvalidArgumentException if container is not an array or ArrayAccess.
*/
public static function history(&$container)
{
if (!is_array($container) && !$container instanceof \ArrayAccess) {
throw new \InvalidArgumentException('history container must be an array or object implementing ArrayAccess');
}
return function (callable $handler) use (&$container) {
return function ($request, array $options) use ($handler, &$container) {
return $handler($request, $options)->then(
function ($value) use ($request, &$container, $options) {
$container[] = [
'request' => $request,
'response' => $value,
'error' => null,
'options' => $options
];
return $value;
},
function ($reason) use ($request, &$container, $options) {
$container[] = [
'request' => $request,
'response' => null,
'error' => $reason,
'options' => $options
];
return \GuzzleHttp\Promise\rejection_for($reason);
}
);
};
};
}
/**
* Middleware that invokes a callback before and after sending a request.
*
* The provided listener cannot modify or alter the response. It simply
* "taps" into the chain to be notified before returning the promise. The
* before listener accepts a request and options array, and the after
* listener accepts a request, options array, and response promise.
*
* @param callable $before Function to invoke before forwarding the request.
* @param callable $after Function invoked after forwarding.
*
* @return callable Returns a function that accepts the next handler.
*/
public static function tap(callable $before = null, callable $after = null)
{
return function (callable $handler) use ($before, $after) {
return function ($request, array $options) use ($handler, $before, $after) {
if ($before) {
$before($request, $options);
}
$response = $handler($request, $options);
if ($after) {
$after($request, $options, $response);
}
return $response;
};
};
}
/**
* Middleware that handles request redirects.
*
* @return callable Returns a function that accepts the next handler.
*/
public static function redirect()
{
return function (callable $handler) {
return new RedirectMiddleware($handler);
};
}
/**
* Middleware that retries requests based on the boolean result of
* invoking the provided "decider" function.
*
* If no delay function is provided, a simple implementation of exponential
* backoff will be utilized.
*
* @param callable $decider Function that accepts the number of retries,
* a request, [response], and [exception] and
* returns true if the request is to be retried.
* @param callable $delay Function that accepts the number of retries and
* returns the number of milliseconds to delay.
*
* @return callable Returns a function that accepts the next handler.
*/
public static function retry(callable $decider, callable $delay = null)
{
return function (callable $handler) use ($decider, $delay) {
return new RetryMiddleware($decider, $handler, $delay);
};
}
/**
* Middleware that logs requests, responses, and errors using a message
* formatter.
*
* @param LoggerInterface $logger Logs messages.
* @param MessageFormatter $formatter Formatter used to create message strings.
* @param string $logLevel Level at which to log requests.
*
* @return callable Returns a function that accepts the next handler.
*/
public static function log(LoggerInterface $logger, MessageFormatter $formatter, $logLevel = 'info' /* \Psr\Log\LogLevel::INFO */)
{
return function (callable $handler) use ($logger, $formatter, $logLevel) {
return function ($request, array $options) use ($handler, $logger, $formatter, $logLevel) {
return $handler($request, $options)->then(
function ($response) use ($logger, $request, $formatter, $logLevel) {
$message = $formatter->format($request, $response);
$logger->log($logLevel, $message);
return $response;
},
function ($reason) use ($logger, $request, $formatter) {
$response = $reason instanceof RequestException
? $reason->getResponse()
: null;
$message = $formatter->format($request, $response, $reason);
$logger->notice($message);
return \GuzzleHttp\Promise\rejection_for($reason);
}
);
};
};
}
/**
* This middleware adds a default content-type if possible, a default
* content-length or transfer-encoding header, and the expect header.
*
* @return callable
*/
public static function prepareBody()
{
return function (callable $handler) {
return new PrepareBodyMiddleware($handler);
};
}
/**
* Middleware that applies a map function to the request before passing to
* the next handler.
*
* @param callable $fn Function that accepts a RequestInterface and returns
* a RequestInterface.
* @return callable
*/
public static function mapRequest(callable $fn)
{
return function (callable $handler) use ($fn) {
return function ($request, array $options) use ($handler, $fn) {
return $handler($fn($request), $options);
};
};
}
/**
* Middleware that applies a map function to the resolved promise's
* response.
*
* @param callable $fn Function that accepts a ResponseInterface and
* returns a ResponseInterface.
* @return callable
*/
public static function mapResponse(callable $fn)
{
return function (callable $handler) use ($fn) {
return function ($request, array $options) use ($handler, $fn) {
return $handler($request, $options)->then($fn);
};
};
}
}
<?php
namespace GuzzleHttp;
use GuzzleHttp\Promise\EachPromise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\PromisorInterface;
use Psr\Http\Message\RequestInterface;
/**
* Sends an iterator of requests concurrently using a capped pool size.
*
* The pool will read from an iterator until it is cancelled or until the
* iterator is consumed. When a request is yielded, the request is sent after
* applying the "request_options" request options (if provided in the ctor).
*
* When a function is yielded by the iterator, the function is provided the
* "request_options" array that should be merged on top of any existing
* options, and the function MUST then return a wait-able promise.
*/
class Pool implements PromisorInterface
{
/** @var EachPromise */
private $each;
/**
* @param ClientInterface $client Client used to send the requests.
* @param array|\Iterator $requests Requests or functions that return
* requests to send concurrently.
* @param array $config Associative array of options
* - concurrency: (int) Maximum number of requests to send concurrently
* - options: Array of request options to apply to each request.
* - fulfilled: (callable) Function to invoke when a request completes.
* - rejected: (callable) Function to invoke when a request is rejected.
*/
public function __construct(
ClientInterface $client,
$requests,
array $config = []
) {
// Backwards compatibility.
if (isset($config['pool_size'])) {
$config['concurrency'] = $config['pool_size'];
} elseif (!isset($config['concurrency'])) {
$config['concurrency'] = 25;
}
if (isset($config['options'])) {
$opts = $config['options'];
unset($config['options']);
} else {
$opts = [];
}
$iterable = \GuzzleHttp\Promise\iter_for($requests);
$requests = function () use ($iterable, $client, $opts) {
foreach ($iterable as $key => $rfn) {
if ($rfn instanceof RequestInterface) {
yield $key => $client->sendAsync($rfn, $opts);
} elseif (is_callable($rfn)) {
yield $key => $rfn($opts);
} else {
throw new \InvalidArgumentException('Each value yielded by '
. 'the iterator must be a Psr7\Http\Message\RequestInterface '
. 'or a callable that returns a promise that fulfills '
. 'with a Psr7\Message\Http\ResponseInterface object.');
}
}
};
$this->each = new EachPromise($requests(), $config);
}
/**
* Get promise
*
* @return PromiseInterface
*/
public function promise()
{
return $this->each->promise();
}
/**
* Sends multiple requests concurrently and returns an array of responses
* and exceptions that uses the same ordering as the provided requests.
*
* IMPORTANT: This method keeps every request and response in memory, and
* as such, is NOT recommended when sending a large number or an
* indeterminate number of requests concurrently.
*
* @param ClientInterface $client Client used to send the requests
* @param array|\Iterator $requests Requests to send concurrently.
* @param array $options Passes through the options available in
* {@see GuzzleHttp\Pool::__construct}
*
* @return array Returns an array containing the response or an exception
* in the same order that the requests were sent.
* @throws \InvalidArgumentException if the event format is incorrect.
*/
public static function batch(
ClientInterface $client,
$requests,
array $options = []
) {
$res = [];
self::cmpCallback($options, 'fulfilled', $res);
self::cmpCallback($options, 'rejected', $res);
$pool = new static($client, $requests, $options);
$pool->promise()->wait();
ksort($res);
return $res;
}
/**
* Execute callback(s)
*
* @return void
*/
private static function cmpCallback(array &$options, $name, array &$results)
{
if (!isset($options[$name])) {
$options[$name] = function ($v, $k) use (&$results) {
$results[$k] = $v;
};
} else {
$currentFn = $options[$name];
$options[$name] = function ($v, $k) use (&$results, $currentFn) {
$currentFn($v, $k);
$results[$k] = $v;
};
}
}
}
<?php
namespace GuzzleHttp;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7;
use Psr\Http\Message\RequestInterface;
/**
* Prepares requests that contain a body, adding the Content-Length,
* Content-Type, and Expect headers.
*/
class PrepareBodyMiddleware
{
/** @var callable */
private $nextHandler;
/**
* @param callable $nextHandler Next handler to invoke.
*/
public function __construct(callable $nextHandler)
{
$this->nextHandler = $nextHandler;
}
/**
* @param RequestInterface $request
* @param array $options
*
* @return PromiseInterface
*/
public function __invoke(RequestInterface $request, array $options)
{
$fn = $this->nextHandler;
// Don't do anything if the request has no body.
if ($request->getBody()->getSize() === 0) {
return $fn($request, $options);
}
$modify = [];
// Add a default content-type if possible.
if (!$request->hasHeader('Content-Type')) {
if ($uri = $request->getBody()->getMetadata('uri')) {
if ($type = Psr7\mimetype_from_filename($uri)) {
$modify['set_headers']['Content-Type'] = $type;
}
}
}
// Add a default content-length or transfer-encoding header.
if (!$request->hasHeader('Content-Length')
&& !$request->hasHeader('Transfer-Encoding')
) {
$size = $request->getBody()->getSize();
if ($size !== null) {
$modify['set_headers']['Content-Length'] = $size;
} else {
$modify['set_headers']['Transfer-Encoding'] = 'chunked';
}
}
// Add the expect header if needed.
$this->addExpectHeader($request, $options, $modify);
return $fn(Psr7\modify_request($request, $modify), $options);
}
/**
* Add expect header
*
* @return void
*/
private function addExpectHeader(
RequestInterface $request,
array $options,
array &$modify
) {
// Determine if the Expect header should be used
if ($request->hasHeader('Expect')) {
return;
}
$expect = isset($options['expect']) ? $options['expect'] : null;
// Return if disabled or if you're not using HTTP/1.1 or HTTP/2.0
if ($expect === false || $request->getProtocolVersion() < 1.1) {
return;
}
// The expect header is unconditionally enabled
if ($expect === true) {
$modify['set_headers']['Expect'] = '100-Continue';
return;
}
// By default, send the expect header when the payload is > 1mb
if ($expect === null) {
$expect = 1048576;
}
// Always add if the body cannot be rewound, the size cannot be
// determined, or the size is greater than the cutoff threshold
$body = $request->getBody();
$size = $body->getSize();
if ($size === null || $size >= (int) $expect || !$body->isSeekable()) {
$modify['set_headers']['Expect'] = '100-Continue';
}
}
}
<?php
namespace GuzzleHttp;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Exception\TooManyRedirectsException;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
/**
* Request redirect middleware.
*
* Apply this middleware like other middleware using
* {@see \GuzzleHttp\Middleware::redirect()}.
*/
class RedirectMiddleware
{
const HISTORY_HEADER = 'X-Guzzle-Redirect-History';
const STATUS_HISTORY_HEADER = 'X-Guzzle-Redirect-Status-History';
public static $defaultSettings = [
'max' => 5,
'protocols' => ['http', 'https'],
'strict' => false,
'referer' => false,
'track_redirects' => false,
];
/** @var callable */
private $nextHandler;
/**
* @param callable $nextHandler Next handler to invoke.
*/
public function __construct(callable $nextHandler)
{
$this->nextHandler = $nextHandler;
}
/**
* @param RequestInterface $request
* @param array $options
*
* @return PromiseInterface
*/
public function __invoke(RequestInterface $request, array $options)
{
$fn = $this->nextHandler;
if (empty($options['allow_redirects'])) {
return $fn($request, $options);
}
if ($options['allow_redirects'] === true) {
$options['allow_redirects'] = self::$defaultSettings;
} elseif (!is_array($options['allow_redirects'])) {
throw new \InvalidArgumentException('allow_redirects must be true, false, or array');
} else {
// Merge the default settings with the provided settings
$options['allow_redirects'] += self::$defaultSettings;
}
if (empty($options['allow_redirects']['max'])) {
return $fn($request, $options);
}
return $fn($request, $options)
->then(function (ResponseInterface $response) use ($request, $options) {
return $this->checkRedirect($request, $options, $response);
});
}
/**
* @param RequestInterface $request
* @param array $options
* @param ResponseInterface $response
*
* @return ResponseInterface|PromiseInterface
*/
public function checkRedirect(
RequestInterface $request,
array $options,
ResponseInterface $response
) {
if (substr($response->getStatusCode(), 0, 1) != '3'
|| !$response->hasHeader('Location')
) {
return $response;
}
$this->guardMax($request, $options);
$nextRequest = $this->modifyRequest($request, $options, $response);
if (isset($options['allow_redirects']['on_redirect'])) {
call_user_func(
$options['allow_redirects']['on_redirect'],
$request,
$response,
$nextRequest->getUri()
);
}
/** @var PromiseInterface|ResponseInterface $promise */
$promise = $this($nextRequest, $options);
// Add headers to be able to track history of redirects.
if (!empty($options['allow_redirects']['track_redirects'])) {
return $this->withTracking(
$promise,
(string) $nextRequest->getUri(),
$response->getStatusCode()
);
}
return $promise;
}
/**
* Enable tracking on promise.
*
* @return PromiseInterface
*/
private function withTracking(PromiseInterface $promise, $uri, $statusCode)
{
return $promise->then(
function (ResponseInterface $response) use ($uri, $statusCode) {
// Note that we are pushing to the front of the list as this
// would be an earlier response than what is currently present
// in the history header.
$historyHeader = $response->getHeader(self::HISTORY_HEADER);
$statusHeader = $response->getHeader(self::STATUS_HISTORY_HEADER);
array_unshift($historyHeader, $uri);
array_unshift($statusHeader, $statusCode);
return $response->withHeader(self::HISTORY_HEADER, $historyHeader)
->withHeader(self::STATUS_HISTORY_HEADER, $statusHeader);
}
);
}
/**
* Check for too many redirects
*
* @return void
*
* @throws TooManyRedirectsException Too many redirects.
*/
private function guardMax(RequestInterface $request, array &$options)
{
$current = isset($options['__redirect_count'])
? $options['__redirect_count']
: 0;
$options['__redirect_count'] = $current + 1;
$max = $options['allow_redirects']['max'];
if ($options['__redirect_count'] > $max) {
throw new TooManyRedirectsException(
"Will not follow more than {$max} redirects",
$request
);
}
}
/**
* @param RequestInterface $request
* @param array $options
* @param ResponseInterface $response
*
* @return RequestInterface
*/
public function modifyRequest(
RequestInterface $request,
array $options,
ResponseInterface $response
) {
// Request modifications to apply.
$modify = [];
$protocols = $options['allow_redirects']['protocols'];
// Use a GET request if this is an entity enclosing request and we are
// not forcing RFC compliance, but rather emulating what all browsers
// would do.
$statusCode = $response->getStatusCode();
if ($statusCode == 303 ||
($statusCode <= 302 && !$options['allow_redirects']['strict'])
) {
$modify['method'] = 'GET';
$modify['body'] = '';
}
$uri = $this->redirectUri($request, $response, $protocols);
if (isset($options['idn_conversion']) && ($options['idn_conversion'] !== false)) {
$idnOptions = ($options['idn_conversion'] === true) ? IDNA_DEFAULT : $options['idn_conversion'];
$uri = Utils::idnUriConvert($uri, $idnOptions);
}
$modify['uri'] = $uri;
Psr7\rewind_body($request);
// Add the Referer header if it is told to do so and only
// add the header if we are not redirecting from https to http.
if ($options['allow_redirects']['referer']
&& $modify['uri']->getScheme() === $request->getUri()->getScheme()
) {
$uri = $request->getUri()->withUserInfo('');
$modify['set_headers']['Referer'] = (string) $uri;
} else {
$modify['remove_headers'][] = 'Referer';
}
// Remove Authorization header if host is different.
if ($request->getUri()->getHost() !== $modify['uri']->getHost()) {
$modify['remove_headers'][] = 'Authorization';
}
return Psr7\modify_request($request, $modify);
}
/**
* Set the appropriate URL on the request based on the location header
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @param array $protocols
*
* @return UriInterface
*/
private function redirectUri(
RequestInterface $request,
ResponseInterface $response,
array $protocols
) {
$location = Psr7\UriResolver::resolve(
$request->getUri(),
new Psr7\Uri($response->getHeaderLine('Location'))
);
// Ensure that the redirect URI is allowed based on the protocols.
if (!in_array($location->getScheme(), $protocols)) {
throw new BadResponseException(
sprintf(
'Redirect URI, %s, does not use one of the allowed redirect protocols: %s',
$location,
implode(', ', $protocols)
),
$request,
$response
);
}
return $location;
}
}
<?php
namespace GuzzleHttp;
/**
* This class contains a list of built-in Guzzle request options.
*
* More documentation for each option can be found at http://guzzlephp.org/.
*
* @link http://docs.guzzlephp.org/en/v6/request-options.html
*/
final class RequestOptions
{
/**
* allow_redirects: (bool|array) Controls redirect behavior. Pass false
* to disable redirects, pass true to enable redirects, pass an
* associative to provide custom redirect settings. Defaults to "false".
* This option only works if your handler has the RedirectMiddleware. When
* passing an associative array, you can provide the following key value
* pairs:
*
* - max: (int, default=5) maximum number of allowed redirects.
* - strict: (bool, default=false) Set to true to use strict redirects
* meaning redirect POST requests with POST requests vs. doing what most
* browsers do which is redirect POST requests with GET requests
* - referer: (bool, default=false) Set to true to enable the Referer
* header.
* - protocols: (array, default=['http', 'https']) Allowed redirect
* protocols.
* - on_redirect: (callable) PHP callable that is invoked when a redirect
* is encountered. The callable is invoked with the request, the redirect
* response that was received, and the effective URI. Any return value
* from the on_redirect function is ignored.
*/
const ALLOW_REDIRECTS = 'allow_redirects';
/**
* auth: (array) Pass an array of HTTP authentication parameters to use
* with the request. The array must contain the username in index [0],
* the password in index [1], and you can optionally provide a built-in
* authentication type in index [2]. Pass null to disable authentication
* for a request.
*/
const AUTH = 'auth';
/**
* body: (resource|string|null|int|float|StreamInterface|callable|\Iterator)
* Body to send in the request.
*/
const BODY = 'body';
/**
* cert: (string|array) Set to a string to specify the path to a file
* containing a PEM formatted SSL client side certificate. If a password
* is required, then set cert to an array containing the path to the PEM
* file in the first array element followed by the certificate password
* in the second array element.
*/
const CERT = 'cert';
/**
* cookies: (bool|GuzzleHttp\Cookie\CookieJarInterface, default=false)
* Specifies whether or not cookies are used in a request or what cookie
* jar to use or what cookies to send. This option only works if your
* handler has the `cookie` middleware. Valid values are `false` and
* an instance of {@see GuzzleHttp\Cookie\CookieJarInterface}.
*/
const COOKIES = 'cookies';
/**
* connect_timeout: (float, default=0) Float describing the number of
* seconds to wait while trying to connect to a server. Use 0 to wait
* indefinitely (the default behavior).
*/
const CONNECT_TIMEOUT = 'connect_timeout';
/**
* debug: (bool|resource) Set to true or set to a PHP stream returned by
* fopen() enable debug output with the HTTP handler used to send a
* request.
*/
const DEBUG = 'debug';
/**
* decode_content: (bool, default=true) Specify whether or not
* Content-Encoding responses (gzip, deflate, etc.) are automatically
* decoded.
*/
const DECODE_CONTENT = 'decode_content';
/**
* delay: (int) The amount of time to delay before sending in milliseconds.
*/
const DELAY = 'delay';
/**
* expect: (bool|integer) Controls the behavior of the
* "Expect: 100-Continue" header.
*
* Set to `true` to enable the "Expect: 100-Continue" header for all
* requests that sends a body. Set to `false` to disable the
* "Expect: 100-Continue" header for all requests. Set to a number so that
* the size of the payload must be greater than the number in order to send
* the Expect header. Setting to a number will send the Expect header for
* all requests in which the size of the payload cannot be determined or
* where the body is not rewindable.
*
* By default, Guzzle will add the "Expect: 100-Continue" header when the
* size of the body of a request is greater than 1 MB and a request is
* using HTTP/1.1.
*/
const EXPECT = 'expect';
/**
* form_params: (array) Associative array of form field names to values
* where each value is a string or array of strings. Sets the Content-Type
* header to application/x-www-form-urlencoded when no Content-Type header
* is already present.
*/
const FORM_PARAMS = 'form_params';
/**
* headers: (array) Associative array of HTTP headers. Each value MUST be
* a string or array of strings.
*/
const HEADERS = 'headers';
/**
* http_errors: (bool, default=true) Set to false to disable exceptions
* when a non- successful HTTP response is received. By default,
* exceptions will be thrown for 4xx and 5xx responses. This option only
* works if your handler has the `httpErrors` middleware.
*/
const HTTP_ERRORS = 'http_errors';
/**
* idn: (bool|int, default=true) A combination of IDNA_* constants for
* idn_to_ascii() PHP's function (see "options" parameter). Set to false to
* disable IDN support completely, or to true to use the default
* configuration (IDNA_DEFAULT constant).
*/
const IDN_CONVERSION = 'idn_conversion';
/**
* json: (mixed) Adds JSON data to a request. The provided value is JSON
* encoded and a Content-Type header of application/json will be added to
* the request if no Content-Type header is already present.
*/
const JSON = 'json';
/**
* multipart: (array) Array of associative arrays, each containing a
* required "name" key mapping to the form field, name, a required
* "contents" key mapping to a StreamInterface|resource|string, an
* optional "headers" associative array of custom headers, and an
* optional "filename" key mapping to a string to send as the filename in
* the part. If no "filename" key is present, then no "filename" attribute
* will be added to the part.
*/
const MULTIPART = 'multipart';
/**
* on_headers: (callable) A callable that is invoked when the HTTP headers
* of the response have been received but the body has not yet begun to
* download.
*/
const ON_HEADERS = 'on_headers';
/**
* on_stats: (callable) allows you to get access to transfer statistics of
* a request and access the lower level transfer details of the handler
* associated with your client. ``on_stats`` is a callable that is invoked
* when a handler has finished sending a request. The callback is invoked
* with transfer statistics about the request, the response received, or
* the error encountered. Included in the data is the total amount of time
* taken to send the request.
*/
const ON_STATS = 'on_stats';
/**
* progress: (callable) Defines a function to invoke when transfer
* progress is made. The function accepts the following positional
* arguments: the total number of bytes expected to be downloaded, the
* number of bytes downloaded so far, the number of bytes expected to be
* uploaded, the number of bytes uploaded so far.
*/
const PROGRESS = 'progress';
/**
* proxy: (string|array) Pass a string to specify an HTTP proxy, or an
* array to specify different proxies for different protocols (where the
* key is the protocol and the value is a proxy string).
*/
const PROXY = 'proxy';
/**
* query: (array|string) Associative array of query string values to add
* to the request. This option uses PHP's http_build_query() to create
* the string representation. Pass a string value if you need more
* control than what this method provides
*/
const QUERY = 'query';
/**
* sink: (resource|string|StreamInterface) Where the data of the
* response is written to. Defaults to a PHP temp stream. Providing a
* string will write data to a file by the given name.
*/
const SINK = 'sink';
/**
* synchronous: (bool) Set to true to inform HTTP handlers that you intend
* on waiting on the response. This can be useful for optimizations. Note
* that a promise is still returned if you are using one of the async
* client methods.
*/
const SYNCHRONOUS = 'synchronous';
/**
* ssl_key: (array|string) Specify the path to a file containing a private
* SSL key in PEM format. If a password is required, then set to an array
* containing the path to the SSL key in the first array element followed
* by the password required for the certificate in the second element.
*/
const SSL_KEY = 'ssl_key';
/**
* stream: Set to true to attempt to stream a response rather than
* download it all up-front.
*/
const STREAM = 'stream';
/**
* verify: (bool|string, default=true) Describes the SSL certificate
* verification behavior of a request. Set to true to enable SSL
* certificate verification using the system CA bundle when available
* (the default). Set to false to disable certificate verification (this
* is insecure!). Set to a string to provide the path to a CA bundle on
* disk to enable verification using a custom certificate.
*/
const VERIFY = 'verify';
/**
* timeout: (float, default=0) Float describing the timeout of the
* request in seconds. Use 0 to wait indefinitely (the default behavior).
*/
const TIMEOUT = 'timeout';
/**
* read_timeout: (float, default=default_socket_timeout ini setting) Float describing
* the body read timeout, for stream requests.
*/
const READ_TIMEOUT = 'read_timeout';
/**
* version: (float) Specifies the HTTP protocol version to attempt to use.
*/
const VERSION = 'version';
/**
* force_ip_resolve: (bool) Force client to use only ipv4 or ipv6 protocol
*/
const FORCE_IP_RESOLVE = 'force_ip_resolve';
}
<?php
namespace GuzzleHttp;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Psr7;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Middleware that retries requests based on the boolean result of
* invoking the provided "decider" function.
*/
class RetryMiddleware
{
/** @var callable */
private $nextHandler;
/** @var callable */
private $decider;
/** @var callable */
private $delay;
/**
* @param callable $decider Function that accepts the number of retries,
* a request, [response], and [exception] and
* returns true if the request is to be
* retried.
* @param callable $nextHandler Next handler to invoke.
* @param callable $delay Function that accepts the number of retries
* and [response] and returns the number of
* milliseconds to delay.
*/
public function __construct(
callable $decider,
callable $nextHandler,
callable $delay = null
) {
$this->decider = $decider;
$this->nextHandler = $nextHandler;
$this->delay = $delay ?: __CLASS__ . '::exponentialDelay';
}
/**
* Default exponential backoff delay function.
*
* @param int $retries
*
* @return int milliseconds.
*/
public static function exponentialDelay($retries)
{
return (int) pow(2, $retries - 1) * 1000;
}
/**
* @param RequestInterface $request
* @param array $options
*
* @return PromiseInterface
*/
public function __invoke(RequestInterface $request, array $options)
{
if (!isset($options['retries'])) {
$options['retries'] = 0;
}
$fn = $this->nextHandler;
return $fn($request, $options)
->then(
$this->onFulfilled($request, $options),
$this->onRejected($request, $options)
);
}
/**
* Execute fulfilled closure
*
* @return mixed
*/
private function onFulfilled(RequestInterface $req, array $options)
{
return function ($value) use ($req, $options) {
if (!call_user_func(
$this->decider,
$options['retries'],
$req,
$value,
null
)) {
return $value;
}
return $this->doRetry($req, $options, $value);
};
}
/**
* Execute rejected closure
*
* @return callable
*/
private function onRejected(RequestInterface $req, array $options)
{
return function ($reason) use ($req, $options) {
if (!call_user_func(
$this->decider,
$options['retries'],
$req,
null,
$reason
)) {
return \GuzzleHttp\Promise\rejection_for($reason);
}
return $this->doRetry($req, $options);
};
}
/**
* @return self
*/
private function doRetry(RequestInterface $request, array $options, ResponseInterface $response = null)
{
$options['delay'] = call_user_func($this->delay, ++$options['retries'], $response);
return $this($request, $options);
}
}
<?php
namespace GuzzleHttp;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
/**
* Represents data at the point after it was transferred either successfully
* or after a network error.
*/
final class TransferStats
{
private $request;
private $response;
private $transferTime;
private $handlerStats;
private $handlerErrorData;
/**
* @param RequestInterface $request Request that was sent.
* @param ResponseInterface|null $response Response received (if any)
* @param float|null $transferTime Total handler transfer time.
* @param mixed $handlerErrorData Handler error data.
* @param array $handlerStats Handler specific stats.
*/
public function __construct(
RequestInterface $request,
ResponseInterface $response = null,
$transferTime = null,
$handlerErrorData = null,
$handlerStats = []
) {
$this->request = $request;
$this->response = $response;
$this->transferTime = $transferTime;
$this->handlerErrorData = $handlerErrorData;
$this->handlerStats = $handlerStats;
}
/**
* @return RequestInterface
*/
public function getRequest()
{
return $this->request;
}
/**
* Returns the response that was received (if any).
*
* @return ResponseInterface|null
*/
public function getResponse()
{
return $this->response;
}
/**
* Returns true if a response was received.
*
* @return bool
*/
public function hasResponse()
{
return $this->response !== null;
}
/**
* Gets handler specific error data.
*
* This might be an exception, a integer representing an error code, or
* anything else. Relying on this value assumes that you know what handler
* you are using.
*
* @return mixed
*/
public function getHandlerErrorData()
{
return $this->handlerErrorData;
}
/**
* Get the effective URI the request was sent to.
*
* @return UriInterface
*/
public function getEffectiveUri()
{
return $this->request->getUri();
}
/**
* Get the estimated time the request was being transferred by the handler.
*
* @return float|null Time in seconds.
*/
public function getTransferTime()
{
return $this->transferTime;
}
/**
* Gets an array of all of the handler specific transfer data.
*
* @return array
*/
public function getHandlerStats()
{
return $this->handlerStats;
}
/**
* Get a specific handler statistic from the handler by name.
*
* @param string $stat Handler specific transfer stat to retrieve.
*
* @return mixed|null
*/
public function getHandlerStat($stat)
{
return isset($this->handlerStats[$stat])
? $this->handlerStats[$stat]
: null;
}
}