Initial Commit

This commit is contained in:
David Stone
2024-11-30 18:24:12 -07:00
commit e8f7955c1c
5432 changed files with 1397750 additions and 0 deletions

View File

@ -0,0 +1,468 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
use WP_Ultimo\Dependencies\Amp\Http\Client\Body\StringBody;
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
use WP_Ultimo\Dependencies\Amp\Http\Message;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\League\Uri;
use WP_Ultimo\Dependencies\Psr\Http\Message\UriInterface;
use function WP_Ultimo\Dependencies\Amp\call;
/**
* An HTTP request.
*/
final class Request extends Message
{
use ForbidSerialization;
public const DEFAULT_HEADER_SIZE_LIMIT = 2 * 8192;
public const DEFAULT_BODY_SIZE_LIMIT = 10485760;
/**
* @template TValue
*
* @param mixed $value
* @psalm-param TValue $value
*
* @return mixed
* @psalm-return TValue
*/
private static function clone($value)
{
if ($value === null || \is_scalar($value)) {
return $value;
}
// force deep cloning
return \unserialize(\serialize($value), ['allowed_classes' => \true]);
}
/** @var string[] */
private $protocolVersions = ['1.1', '2'];
/** @var string */
private $method;
/** @var UriInterface */
private $uri;
/** @var RequestBody */
private $body;
/** @var int */
private $tcpConnectTimeout = 10000;
/** @var int */
private $tlsHandshakeTimeout = 10000;
/** @var int */
private $transferTimeout = 10000;
/** @var int */
private $inactivityTimeout = 10000;
/** @var int */
private $bodySizeLimit = self::DEFAULT_BODY_SIZE_LIMIT;
/** @var int */
private $headerSizeLimit = self::DEFAULT_HEADER_SIZE_LIMIT;
/** @var callable|null */
private $onPush;
/** @var callable|null */
private $onUpgrade;
/** @var callable|null */
private $onInformationalResponse;
/** @var mixed[] */
private $attributes = [];
/** @var EventListener[] */
private $eventListeners = [];
/**
* Request constructor.
*
* @param string|UriInterface $uri
* @param string $method
* @param string $body
*/
public function __construct($uri, string $method = "GET", ?string $body = null)
{
$this->setUri($uri);
$this->setMethod($method);
$this->setBody($body);
}
public function addEventListener(EventListener $eventListener) : void
{
$this->eventListeners[] = $eventListener;
}
/**
* @return EventListener[]
*/
public function getEventListeners() : array
{
return $this->eventListeners;
}
/**
* Retrieve the requests's acceptable HTTP protocol versions.
*
* @return string[]
*/
public function getProtocolVersions() : array
{
return $this->protocolVersions;
}
/**
* Assign the requests's acceptable HTTP protocol versions.
*
* The HTTP client might choose any of these.
*
* @param string[] $versions
*/
public function setProtocolVersions(array $versions) : void
{
$versions = \array_unique($versions);
if (empty($versions)) {
/** @noinspection PhpUndefinedClassInspection */
throw new \Error("Empty array of protocol versions provided, must not be empty.");
}
foreach ($versions as $version) {
if (!\in_array($version, ["1.0", "1.1", "2"], \true)) {
/** @noinspection PhpUndefinedClassInspection */
throw new \Error("Invalid HTTP protocol version: " . $version);
}
}
$this->protocolVersions = $versions;
}
/**
* Retrieve the request's HTTP method verb.
*
* @return string
*/
public function getMethod() : string
{
return $this->method;
}
/**
* Specify the request's HTTP method verb.
*
* @param string $method
*/
public function setMethod(string $method) : void
{
$this->method = $method;
}
/**
* Retrieve the request's URI.
*
* @return UriInterface
*/
public function getUri() : UriInterface
{
return $this->uri;
}
/**
* Specify the request's HTTP URI.
*
* @param string|UriInterface $uri
*/
public function setUri($uri) : void
{
$this->uri = $uri instanceof UriInterface ? $uri : $this->createUriFromString($uri);
}
/**
* Assign a value for the specified header field by replacing any existing values for that field.
*
* @param string $name Header name.
* @param string|string[] $value Header value.
*/
public function setHeader(string $name, $value) : void
{
if (($name[0] ?? ":") === ":") {
throw new \Error("Header name cannot be empty or start with a colon (:)");
}
parent::setHeader($name, $value);
}
/**
* Assign a value for the specified header field by adding an additional header line.
*
* @param string $name Header name.
* @param string|string[] $value Header value.
*/
public function addHeader(string $name, $value) : void
{
if (($name[0] ?? ":") === ":") {
throw new \Error("Header name cannot be empty or start with a colon (:)");
}
parent::addHeader($name, $value);
}
public function setHeaders(array $headers) : void
{
/** @noinspection PhpUnhandledExceptionInspection */
parent::setHeaders($headers);
}
/**
* Remove the specified header field from the message.
*
* @param string $name Header name.
*/
public function removeHeader(string $name) : void
{
parent::removeHeader($name);
}
/**
* Retrieve the message entity body.
*/
public function getBody() : RequestBody
{
return $this->body;
}
/**
* Assign the message entity body.
*
* @param mixed $body
*/
public function setBody($body) : void
{
if ($body === null) {
$this->body = new StringBody("");
} elseif (\is_string($body)) {
$this->body = new StringBody($body);
} elseif (\is_scalar($body)) {
$this->body = new StringBody(\var_export($body, \true));
} elseif ($body instanceof RequestBody) {
$this->body = $body;
} else {
/** @noinspection PhpUndefinedClassInspection */
throw new \TypeError("Invalid body type: " . \gettype($body));
}
}
/**
* Registers a callback to the request that is invoked when the server pushes an additional resource.
* The callback is given two parameters: the Request generated from the pushed resource, and a promise for the
* Response containing the pushed resource. An HttpException, StreamException, or CancelledException can be thrown
* to refuse the push. If no callback is registered, pushes are automatically rejected.
*
* Interceptors can mostly use {@code interceptPush} instead.
*
* Example:
* function (Request $request, Promise $response): \Generator {
* $uri = $request->getUri(); // URI of pushed resource.
* $response = yield $promise; // Wait for resource to arrive.
* // Use Response object from resolved promise.
* }
*
* @param callable|null $onPush
*/
public function setPushHandler(?callable $onPush) : void
{
$this->onPush = $onPush;
}
/**
* Allows interceptors to modify also pushed responses.
*
* If no push callable has been set by the application, the interceptor won't be invoked. If you want to enable
* push in an interceptor without the application setting a push handler, you need to use {@code setPushHandler}.
*
* @param callable $interceptor Receives the response and might modify it or return a new instance.
*/
public function interceptPush(callable $interceptor) : void
{
if ($this->onPush === null) {
return;
}
$onPush = $this->onPush;
/** @psalm-suppress MissingClosureReturnType */
$this->onPush = static function (Request $request, Promise $response) use($onPush, $interceptor) {
$response = call(static function () use($response, $interceptor) : \Generator {
return (yield call($interceptor, (yield $response))) ?? $response;
});
return $onPush($request, $response);
};
}
/**
* @return callable|null
*/
public function getPushHandler() : ?callable
{
return $this->onPush;
}
/**
* Registers a callback invoked if a 101 response is returned to the request.
*
* @param callable|null $onUpgrade
*/
public function setUpgradeHandler(?callable $onUpgrade) : void
{
$this->onUpgrade = $onUpgrade;
}
/**
* @return callable|null
*/
public function getUpgradeHandler() : ?callable
{
return $this->onUpgrade;
}
/**
* Registers a callback invoked when a 1xx response is returned to the request (other than a 101).
*
* @param callable|null $onInformationalResponse
*/
public function setInformationalResponseHandler(?callable $onInformationalResponse) : void
{
$this->onInformationalResponse = $onInformationalResponse;
}
/**
* @return callable|null
*/
public function getInformationalResponseHandler() : ?callable
{
return $this->onInformationalResponse;
}
/**
* @return int Timeout in milliseconds for the TCP connection.
*/
public function getTcpConnectTimeout() : int
{
return $this->tcpConnectTimeout;
}
public function setTcpConnectTimeout(int $tcpConnectTimeout) : void
{
$this->tcpConnectTimeout = $tcpConnectTimeout;
}
/**
* @return int Timeout in milliseconds for the TLS handshake.
*/
public function getTlsHandshakeTimeout() : int
{
return $this->tlsHandshakeTimeout;
}
public function setTlsHandshakeTimeout(int $tlsHandshakeTimeout) : void
{
$this->tlsHandshakeTimeout = $tlsHandshakeTimeout;
}
/**
* @return int Timeout in milliseconds for the HTTP transfer (not counting TCP connect and TLS handshake)
*/
public function getTransferTimeout() : int
{
return $this->transferTimeout;
}
public function setTransferTimeout(int $transferTimeout) : void
{
$this->transferTimeout = $transferTimeout;
}
/**
* @return int Timeout in milliseconds since the last data was received before the request fails due to inactivity.
*/
public function getInactivityTimeout() : int
{
return $this->inactivityTimeout;
}
public function setInactivityTimeout(int $inactivityTimeout) : void
{
$this->inactivityTimeout = $inactivityTimeout;
}
public function getHeaderSizeLimit() : int
{
return $this->headerSizeLimit;
}
public function setHeaderSizeLimit(int $headerSizeLimit) : void
{
$this->headerSizeLimit = $headerSizeLimit;
}
public function getBodySizeLimit() : int
{
return $this->bodySizeLimit;
}
public function setBodySizeLimit(int $bodySizeLimit) : void
{
$this->bodySizeLimit = $bodySizeLimit;
}
/**
* Note: This method returns a deep clone of the request's attributes, so you can't modify the request attributes
* by modifying the returned value in any way.
*
* @return mixed[] An array of all request attributes in the request's local storage, indexed by name.
*/
public function getAttributes() : array
{
return self::clone($this->attributes);
}
/**
* Check whether a variable with the given name exists in the request's local storage.
*
* Each request has its own local storage to which applications and interceptors may read and write data.
* Other interceptors which are aware of this data can then access it without the server being tightly coupled to
* specific implementations.
*
* @param string $name Name of the attribute, should be namespaced with a vendor and package namespace like classes.
*
* @return bool
*/
public function hasAttribute(string $name) : bool
{
return \array_key_exists($name, $this->attributes);
}
/**
* Retrieve a variable from the request's local storage.
*
* Each request has its own local storage to which applications and interceptors may read and write data.
* Other interceptors which are aware of this data can then access it without the server being tightly coupled to
* specific implementations.
*
* Note: This method returns a deep clone of the request's attribute, so you can't modify the request attribute
* by modifying the returned value in any way.
*
* @param string $name Name of the attribute, should be namespaced with a vendor and package namespace like classes.
*
* @return mixed
*
* @throws MissingAttributeError If an attribute with the given name does not exist.
*/
public function getAttribute(string $name)
{
if (!$this->hasAttribute($name)) {
throw new MissingAttributeError("The requested attribute '{$name}' does not exist");
}
return self::clone($this->attributes[$name]);
}
/**
* Assign a variable to the request's local storage.
*
* Each request has its own local storage to which applications and interceptors may read and write data.
* Other interceptors which are aware of this data can then access it without the server being tightly coupled to
* specific implementations.
*
* Note: This method performs a deep clone of the value via serialization, so you can't modify the given value
* after setting it.
*
* **Example**
*
* ```php
* $request->setAttribute(Timing::class, $stopWatch);
* ```
*
* @param string $name Name of the attribute, should be namespaced with a vendor and package namespace like classes.
* @param mixed $value Value of the attribute, might be any serializable value.
*/
public function setAttribute(string $name, $value) : void
{
$this->attributes[$name] = self::clone($value);
}
/**
* Remove an attribute from the request's local storage.
*
* @param string $name Name of the attribute, should be namespaced with a vendor and package namespace like classes.
*
* @throws MissingAttributeError If an attribute with the given name does not exist.
*/
public function removeAttribute(string $name) : void
{
if (!$this->hasAttribute($name)) {
throw new MissingAttributeError("The requested attribute '{$name}' does not exist");
}
unset($this->attributes[$name]);
}
/**
* Remove all attributes from the request's local storage.
*/
public function removeAttributes() : void
{
$this->attributes = [];
}
public function isIdempotent() : bool
{
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
return \in_array($this->method, ['GET', 'HEAD', 'PUT', 'DELETE'], \true);
}
private function createUriFromString(string $uri) : UriInterface
{
return Uri\Http::createFromString($uri);
}
}