Actualización

This commit is contained in:
Xes
2025-04-10 12:24:57 +02:00
parent 8969cc929d
commit 45420b6f0d
39760 changed files with 4303286 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
<?php
namespace Http\Message;
use Psr\Http\Message\RequestInterface;
/**
* Add authentication information to a PSR-7 Request.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
interface Authentication
{
/**
* Alter the request to add the authentication credentials.
*
* To do that, the implementation might use pre-stored credentials or do
* separate HTTP requests to obtain a valid token.
*
* @param RequestInterface $request The request without authentication information
*
* @return RequestInterface The request with added authentication information
*/
public function authenticate(RequestInterface $request);
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request using Basic Auth based on credentials in the URI.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class AutoBasicAuth implements Authentication
{
/**
* Whether user info should be removed from the URI.
*
* @var bool
*/
private $shouldRemoveUserInfo;
/**
* @param bool|true $shouldRremoveUserInfo
*/
public function __construct($shouldRremoveUserInfo = true)
{
$this->shouldRemoveUserInfo = (bool) $shouldRremoveUserInfo;
}
public function authenticate(RequestInterface $request)
{
$uri = $request->getUri();
$userInfo = $uri->getUserInfo();
if (!empty($userInfo)) {
if ($this->shouldRemoveUserInfo) {
$request = $request->withUri($uri->withUserInfo(''));
}
$request = $request->withHeader('Authorization', sprintf('Basic %s', base64_encode($userInfo)));
}
return $request;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request using Basic Auth.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class BasicAuth implements Authentication
{
/**
* @var string
*/
private $username;
/**
* @var string
*/
private $password;
/**
* @param string $username
* @param string $password
*/
public function __construct($username, $password)
{
$this->username = $username;
$this->password = $password;
}
public function authenticate(RequestInterface $request)
{
$header = sprintf('Basic %s', base64_encode(sprintf('%s:%s', $this->username, $this->password)));
return $request->withHeader('Authorization', $header);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request using a token.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class Bearer implements Authentication
{
/**
* @var string
*/
private $token;
/**
* @param string $token
*/
public function __construct($token)
{
$this->token = $token;
}
public function authenticate(RequestInterface $request)
{
$header = sprintf('Bearer %s', $this->token);
return $request->withHeader('Authorization', $header);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request with a multiple authentication methods.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class Chain implements Authentication
{
/**
* @var Authentication[]
*/
private $authenticationChain = [];
/**
* @param Authentication[] $authenticationChain
*/
public function __construct(array $authenticationChain = [])
{
foreach ($authenticationChain as $authentication) {
if (!$authentication instanceof Authentication) {
throw new \InvalidArgumentException(
'Members of the authentication chain must be of type Http\Message\Authentication'
);
}
}
$this->authenticationChain = $authenticationChain;
}
public function authenticate(RequestInterface $request)
{
foreach ($this->authenticationChain as $authentication) {
$request = $authentication->authenticate($request);
}
return $request;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
class Header implements Authentication
{
/**
* @var string
*/
private $name;
/**
* @var string|string[]
*/
private $value;
/**
* @param string|string[] $value
*/
public function __construct(string $name, $value)
{
$this->name = $name;
$this->value = $value;
}
public function authenticate(RequestInterface $request)
{
return $request->withHeader($this->name, $this->value);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Http\Message\RequestMatcher\CallbackRequestMatcher;
use Psr\Http\Message\RequestInterface;
@trigger_error('The '.__NAMESPACE__.'\Matching class is deprecated since version 1.2 and will be removed in 2.0. Use Http\Message\Authentication\RequestConditional instead.', E_USER_DEPRECATED);
/**
* Authenticate a PSR-7 Request if the request is matching.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @deprecated since since version 1.2, and will be removed in 2.0. Use {@link RequestConditional} instead.
*/
final class Matching implements Authentication
{
/**
* @var Authentication
*/
private $authentication;
/**
* @var CallbackRequestMatcher
*/
private $matcher;
public function __construct(Authentication $authentication, ?callable $matcher = null)
{
if (is_null($matcher)) {
$matcher = function () {
return true;
};
}
$this->authentication = $authentication;
$this->matcher = new CallbackRequestMatcher($matcher);
}
public function authenticate(RequestInterface $request)
{
if ($this->matcher->matches($request)) {
return $this->authentication->authenticate($request);
}
return $request;
}
/**
* Creates a matching authentication for an URL.
*
* @param string $url
*
* @return self
*/
public static function createUrlMatcher(Authentication $authentication, $url)
{
$matcher = function (RequestInterface $request) use ($url) {
return preg_match($url, $request->getRequestTarget());
};
return new static($authentication, $matcher);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request by adding parameters to its query.
*
* Note: Although in some cases it can be useful, we do not recommend using query parameters for authentication.
* Credentials in the URL is generally unsafe as they are not encrypted, anyone can see them.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class QueryParam implements Authentication
{
/**
* @var array
*/
private $params = [];
public function __construct(array $params)
{
$this->params = $params;
}
public function authenticate(RequestInterface $request)
{
$uri = $request->getUri();
$query = $uri->getQuery();
$params = [];
parse_str($query, $params);
$params = array_merge($params, $this->params);
$query = http_build_query($params, '', '&');
$uri = $uri->withQuery($query);
return $request->withUri($uri);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Http\Message\RequestMatcher;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request if the request is matching the given request matcher.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class RequestConditional implements Authentication
{
/**
* @var RequestMatcher
*/
private $requestMatcher;
/**
* @var Authentication
*/
private $authentication;
public function __construct(RequestMatcher $requestMatcher, Authentication $authentication)
{
$this->requestMatcher = $requestMatcher;
$this->authentication = $authentication;
}
public function authenticate(RequestInterface $request)
{
if ($this->requestMatcher->matches($request)) {
return $this->authentication->authenticate($request);
}
return $request;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request using WSSE.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class Wsse implements Authentication
{
/**
* @var string
*/
private $username;
/**
* @var string
*/
private $password;
/**
* @var string
*/
private $hashAlgorithm;
/**
* @param string $username
* @param string $password
* @param string $hashAlgorithm To use a better hashing algorithm than the weak sha1, pass the algorithm to use, e.g. "sha512"
*/
public function __construct($username, $password, $hashAlgorithm = 'sha1')
{
$this->username = $username;
$this->password = $password;
if (false === in_array($hashAlgorithm, hash_algos())) {
throw new \InvalidArgumentException(sprintf('Unaccepted hashing algorithm: %s', $hashAlgorithm));
}
$this->hashAlgorithm = $hashAlgorithm;
}
public function authenticate(RequestInterface $request)
{
$nonce = substr(md5(uniqid(uniqid().'_', true)), 0, 16);
$created = date('c');
$digest = base64_encode(hash($this->hashAlgorithm, base64_decode($nonce).$created.$this->password, true));
$wsse = sprintf(
'UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"',
$this->username,
$digest,
$nonce,
$created
);
return $request
->withHeader('Authorization', 'WSSE profile="UsernameToken"')
->withHeader('X-WSSE', $wsse)
;
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace Http\Message\Builder;
use Psr\Http\Message\ResponseInterface;
/**
* Fills response object with values.
*/
class ResponseBuilder
{
/**
* The response to be built.
*
* @var ResponseInterface
*/
protected $response;
/**
* Create builder for the given response.
*/
public function __construct(ResponseInterface $response)
{
$this->response = $response;
}
/**
* Return response.
*
* @return ResponseInterface
*/
public function getResponse()
{
return $this->response;
}
/**
* Add headers represented by an array of header lines.
*
* @param string[] $headers response headers as array of header lines
*
* @return $this
*
* @throws \UnexpectedValueException for invalid header values
* @throws \InvalidArgumentException for invalid status code arguments
*/
public function setHeadersFromArray(array $headers)
{
$status = array_shift($headers);
$this->setStatus($status);
foreach ($headers as $headerLine) {
$headerLine = trim($headerLine);
if ('' === $headerLine) {
continue;
}
$this->addHeader($headerLine);
}
return $this;
}
/**
* Add headers represented by a single string.
*
* @param string $headers response headers as single string
*
* @return $this
*
* @throws \InvalidArgumentException if $headers is not a string on object with __toString()
* @throws \UnexpectedValueException for invalid header values
*/
public function setHeadersFromString($headers)
{
if (!(is_string($headers)
|| (is_object($headers) && method_exists($headers, '__toString')))
) {
throw new \InvalidArgumentException(
sprintf(
'%s expects parameter 1 to be a string, %s given',
__METHOD__,
is_object($headers) ? get_class($headers) : gettype($headers)
)
);
}
$this->setHeadersFromArray(explode("\r\n", $headers));
return $this;
}
/**
* Set response status from a status string.
*
* @param string $statusLine response status as a string
*
* @return $this
*
* @throws \InvalidArgumentException for invalid status line
*/
public function setStatus($statusLine)
{
$parts = explode(' ', $statusLine, 3);
if (count($parts) < 2 || 0 !== strpos(strtolower($parts[0]), 'http/')) {
throw new \InvalidArgumentException(
sprintf('"%s" is not a valid HTTP status line', $statusLine)
);
}
$reasonPhrase = count($parts) > 2 ? $parts[2] : '';
$this->response = $this->response
->withStatus((int) $parts[1], $reasonPhrase)
->withProtocolVersion(substr($parts[0], 5));
return $this;
}
/**
* Add header represented by a string.
*
* @param string $headerLine response header as a string
*
* @return $this
*
* @throws \InvalidArgumentException for invalid header names or values
*/
public function addHeader($headerLine)
{
$parts = explode(':', $headerLine, 2);
if (2 !== count($parts)) {
throw new \InvalidArgumentException(
sprintf('"%s" is not a valid HTTP header line', $headerLine)
);
}
$name = trim($parts[0]);
$value = trim($parts[1]);
if ($this->response->hasHeader($name)) {
$this->response = $this->response->withAddedHeader($name, $value);
} else {
$this->response = $this->response->withHeader($name, $value);
}
return $this;
}
}

524
vendor/php-http/message/src/Cookie.php vendored Normal file
View File

@@ -0,0 +1,524 @@
<?php
namespace Http\Message;
/**
* Cookie Value Object.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @see http://tools.ietf.org/search/rfc6265
*/
final class Cookie
{
/**
* @var string
*/
private $name;
/**
* @var string|null
*/
private $value;
/**
* @var int|null
*/
private $maxAge;
/**
* @var string|null
*/
private $domain;
/**
* @var string
*/
private $path;
/**
* @var bool
*/
private $secure;
/**
* @var bool
*/
private $httpOnly;
/**
* Expires attribute is HTTP 1.0 only and should be avoided.
*
* @var \DateTime|null
*/
private $expires;
/**
* @param string $name
* @param string|null $value
* @param int|null $maxAge
* @param string|null $domain
* @param string|null $path
* @param bool $secure
* @param bool $httpOnly
* @param \DateTime|null $expires Expires attribute is HTTP 1.0 only and should be avoided.
*
* @throws \InvalidArgumentException if name, value or max age is not valid
*/
public function __construct(
$name,
$value = null,
$maxAge = null,
$domain = null,
$path = null,
$secure = false,
$httpOnly = false,
?\DateTime $expires = null
) {
$this->validateName($name);
$this->validateValue($value);
$this->validateMaxAge($maxAge);
$this->name = $name;
$this->value = $value;
$this->maxAge = $maxAge;
$this->expires = $expires;
$this->domain = $this->normalizeDomain($domain);
$this->path = $this->normalizePath($path);
$this->secure = (bool) $secure;
$this->httpOnly = (bool) $httpOnly;
}
/**
* Creates a new cookie without any attribute validation.
*
* @param string $name
* @param string|null $value
* @param int $maxAge
* @param string|null $domain
* @param string|null $path
* @param bool $secure
* @param bool $httpOnly
* @param \DateTime|null $expires Expires attribute is HTTP 1.0 only and should be avoided.
*/
public static function createWithoutValidation(
$name,
$value = null,
$maxAge = null,
$domain = null,
$path = null,
$secure = false,
$httpOnly = false,
?\DateTime $expires = null
) {
$cookie = new self('name', null, null, $domain, $path, $secure, $httpOnly, $expires);
$cookie->name = $name;
$cookie->value = $value;
$cookie->maxAge = $maxAge;
return $cookie;
}
/**
* Returns the name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Returns the value.
*
* @return string|null
*/
public function getValue()
{
return $this->value;
}
/**
* Checks if there is a value.
*
* @return bool
*/
public function hasValue()
{
return isset($this->value);
}
/**
* Sets the value.
*
* @param string|null $value
*
* @return Cookie
*/
public function withValue($value)
{
$this->validateValue($value);
$new = clone $this;
$new->value = $value;
return $new;
}
/**
* Returns the max age.
*
* @return int|null
*/
public function getMaxAge()
{
return $this->maxAge;
}
/**
* Checks if there is a max age.
*
* @return bool
*/
public function hasMaxAge()
{
return isset($this->maxAge);
}
/**
* Sets the max age.
*
* @param int|null $maxAge
*
* @return Cookie
*/
public function withMaxAge($maxAge)
{
$this->validateMaxAge($maxAge);
$new = clone $this;
$new->maxAge = $maxAge;
return $new;
}
/**
* Returns the expiration time.
*
* @return \DateTime|null
*/
public function getExpires()
{
return $this->expires;
}
/**
* Checks if there is an expiration time.
*
* @return bool
*/
public function hasExpires()
{
return isset($this->expires);
}
/**
* Sets the expires.
*
* @return Cookie
*/
public function withExpires(?\DateTime $expires = null)
{
$new = clone $this;
$new->expires = $expires;
return $new;
}
/**
* Checks if the cookie is expired.
*
* @return bool
*/
public function isExpired()
{
return isset($this->expires) and $this->expires < new \DateTime();
}
/**
* Returns the domain.
*
* @return string|null
*/
public function getDomain()
{
return $this->domain;
}
/**
* Checks if there is a domain.
*
* @return bool
*/
public function hasDomain()
{
return isset($this->domain);
}
/**
* Sets the domain.
*
* @param string|null $domain
*
* @return Cookie
*/
public function withDomain($domain)
{
$new = clone $this;
$new->domain = $this->normalizeDomain($domain);
return $new;
}
/**
* Checks whether this cookie is meant for this domain.
*
* @see http://tools.ietf.org/html/rfc6265#section-5.1.3
*
* @param string $domain
*
* @return bool
*/
public function matchDomain($domain)
{
// Domain is not set or exact match
if (!$this->hasDomain() || 0 === strcasecmp($domain, $this->domain)) {
return true;
}
// Domain is not an IP address
if (filter_var($domain, FILTER_VALIDATE_IP)) {
return false;
}
return (bool) preg_match(sprintf('/\b%s$/i', preg_quote($this->domain)), $domain);
}
/**
* Returns the path.
*
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* Sets the path.
*
* @param string|null $path
*
* @return Cookie
*/
public function withPath($path)
{
$new = clone $this;
$new->path = $this->normalizePath($path);
return $new;
}
/**
* Checks whether this cookie is meant for this path.
*
* @see http://tools.ietf.org/html/rfc6265#section-5.1.4
*
* @param string $path
*
* @return bool
*/
public function matchPath($path)
{
return $this->path === $path || (0 === strpos($path, rtrim($this->path, '/').'/'));
}
/**
* Checks whether this cookie may only be sent over HTTPS.
*
* @return bool
*/
public function isSecure()
{
return $this->secure;
}
/**
* Sets whether this cookie should only be sent over HTTPS.
*
* @param bool $secure
*
* @return Cookie
*/
public function withSecure($secure)
{
$new = clone $this;
$new->secure = (bool) $secure;
return $new;
}
/**
* Check whether this cookie may not be accessed through Javascript.
*
* @return bool
*/
public function isHttpOnly()
{
return $this->httpOnly;
}
/**
* Sets whether this cookie may not be accessed through Javascript.
*
* @param bool $httpOnly
*
* @return Cookie
*/
public function withHttpOnly($httpOnly)
{
$new = clone $this;
$new->httpOnly = (bool) $httpOnly;
return $new;
}
/**
* Checks if this cookie represents the same cookie as $cookie.
*
* This does not compare the values, only name, domain and path.
*
* @return bool
*/
public function match(self $cookie)
{
return $this->name === $cookie->name && $this->domain === $cookie->domain and $this->path === $cookie->path;
}
/**
* Validates cookie attributes.
*
* @return bool
*/
public function isValid()
{
try {
$this->validateName($this->name);
$this->validateValue($this->value);
$this->validateMaxAge($this->maxAge);
} catch (\InvalidArgumentException $e) {
return false;
}
return true;
}
/**
* Validates the name attribute.
*
* @see http://tools.ietf.org/search/rfc2616#section-2.2
*
* @param string $name
*
* @throws \InvalidArgumentException if the name is empty or contains invalid characters
*/
private function validateName($name)
{
if (strlen($name) < 1) {
throw new \InvalidArgumentException('The name cannot be empty');
}
// Name attribute is a token as per spec in RFC 2616
if (preg_match('/[\x00-\x20\x22\x28-\x29\x2C\x2F\x3A-\x40\x5B-\x5D\x7B\x7D\x7F]/', $name)) {
throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name));
}
}
/**
* Validates a value.
*
* @see http://tools.ietf.org/html/rfc6265#section-4.1.1
*
* @param string|null $value
*
* @throws \InvalidArgumentException if the value contains invalid characters
*/
private function validateValue($value)
{
if (isset($value)) {
if (preg_match('/[^\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/', $value)) {
throw new \InvalidArgumentException(sprintf('The cookie value "%s" contains invalid characters.', $value));
}
}
}
/**
* Validates a Max-Age attribute.
*
* @param int|null $maxAge
*
* @throws \InvalidArgumentException if the Max-Age is not an empty or integer value
*/
private function validateMaxAge($maxAge)
{
if (isset($maxAge)) {
if (!is_int($maxAge)) {
throw new \InvalidArgumentException('Max-Age must be integer');
}
}
}
/**
* Remove the leading '.' and lowercase the domain as per spec in RFC 6265.
*
* @see http://tools.ietf.org/html/rfc6265#section-4.1.2.3
* @see http://tools.ietf.org/html/rfc6265#section-5.1.3
* @see http://tools.ietf.org/html/rfc6265#section-5.2.3
*
* @param string|null $domain
*
* @return string
*/
private function normalizeDomain($domain)
{
if (isset($domain)) {
$domain = ltrim(strtolower($domain), '.');
}
return $domain;
}
/**
* Processes path as per spec in RFC 6265.
*
* @see http://tools.ietf.org/html/rfc6265#section-5.1.4
* @see http://tools.ietf.org/html/rfc6265#section-5.2.4
*
* @param string|null $path
*
* @return string
*/
private function normalizePath($path)
{
if (null !== $path) {
$path = rtrim($path, '/');
}
if (empty($path) or '/' !== substr($path, 0, 1)) {
$path = '/';
}
return $path;
}
}

View File

@@ -0,0 +1,206 @@
<?php
namespace Http\Message;
/**
* Cookie Jar holds a set of Cookies.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class CookieJar implements \Countable, \IteratorAggregate
{
/**
* @var \SplObjectStorage<Cookie, mixed>
*/
private $cookies;
public function __construct()
{
$this->cookies = new \SplObjectStorage();
}
/**
* Checks if there is a cookie.
*
* @return bool
*/
public function hasCookie(Cookie $cookie)
{
return $this->cookies->contains($cookie);
}
/**
* Adds a cookie.
*/
public function addCookie(Cookie $cookie)
{
if (!$this->hasCookie($cookie)) {
$cookies = $this->getMatchingCookies($cookie);
foreach ($cookies as $matchingCookie) {
if ($cookie->getValue() !== $matchingCookie->getValue() || $cookie->getMaxAge() > $matchingCookie->getMaxAge()) {
$this->removeCookie($matchingCookie);
continue;
}
}
if ($cookie->hasValue()) {
$this->cookies->attach($cookie);
}
}
}
/**
* Removes a cookie.
*/
public function removeCookie(Cookie $cookie)
{
$this->cookies->detach($cookie);
}
/**
* Returns the cookies.
*
* @return Cookie[]
*/
public function getCookies()
{
$match = function ($matchCookie) {
return true;
};
return $this->findMatchingCookies($match);
}
/**
* Returns all matching cookies.
*
* @return Cookie[]
*/
public function getMatchingCookies(Cookie $cookie)
{
$match = function ($matchCookie) use ($cookie) {
return $matchCookie->match($cookie);
};
return $this->findMatchingCookies($match);
}
/**
* Finds matching cookies based on a callable.
*
* @return Cookie[]
*/
private function findMatchingCookies(callable $match)
{
$cookies = [];
foreach ($this->cookies as $cookie) {
if ($match($cookie)) {
$cookies[] = $cookie;
}
}
return $cookies;
}
/**
* Checks if there are cookies.
*
* @return bool
*/
public function hasCookies()
{
return $this->cookies->count() > 0;
}
/**
* Sets the cookies and removes any previous one.
*
* @param Cookie[] $cookies
*/
public function setCookies(array $cookies)
{
$this->clear();
$this->addCookies($cookies);
}
/**
* Adds some cookies.
*
* @param Cookie[] $cookies
*/
public function addCookies(array $cookies)
{
foreach ($cookies as $cookie) {
$this->addCookie($cookie);
}
}
/**
* Removes some cookies.
*
* @param Cookie[] $cookies
*/
public function removeCookies(array $cookies)
{
foreach ($cookies as $cookie) {
$this->removeCookie($cookie);
}
}
/**
* Removes cookies which match the given parameters.
*
* Null means that parameter should not be matched
*
* @param string|null $name
* @param string|null $domain
* @param string|null $path
*/
public function removeMatchingCookies($name = null, $domain = null, $path = null)
{
$match = function ($cookie) use ($name, $domain, $path) {
$match = true;
if (isset($name)) {
$match = $match && ($cookie->getName() === $name);
}
if (isset($domain)) {
$match = $match && $cookie->matchDomain($domain);
}
if (isset($path)) {
$match = $match && $cookie->matchPath($path);
}
return $match;
};
$cookies = $this->findMatchingCookies($match);
$this->removeCookies($cookies);
}
/**
* Removes all cookies.
*/
public function clear()
{
$this->cookies = new \SplObjectStorage();
}
#[\ReturnTypeWillChange]
public function count()
{
return $this->cookies->count();
}
#[\ReturnTypeWillChange]
public function getIterator()
{
return clone $this->cookies;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Http\Message;
use Http\Message\Exception\UnexpectedValueException;
final class CookieUtil
{
/**
* Handles dates as defined by RFC 2616 section 3.3.1, and also some other
* non-standard, but common formats.
*
* @var array
*/
private static $dateFormats = [
'D, d M y H:i:s T',
'D, d M Y H:i:s T',
'D, d-M-y H:i:s T',
'D, d-M-Y H:i:s T',
'D, d-m-y H:i:s T',
'D, d-m-Y H:i:s T',
'D M j G:i:s Y',
'D M d H:i:s Y T',
];
/**
* @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/BrowserKit/Cookie.php
*
* @param string $dateValue
*
* @return \DateTime
*
* @throws UnexpectedValueException if we cannot parse the cookie date string
*/
public static function parseDate($dateValue)
{
foreach (self::$dateFormats as $dateFormat) {
if (false !== $date = \DateTime::createFromFormat($dateFormat, $dateValue, new \DateTimeZone('GMT'))) {
return $date;
}
}
// attempt a fallback for unusual formatting
if (false !== $date = date_create($dateValue, new \DateTimeZone('GMT'))) {
return $date;
}
throw new UnexpectedValueException(sprintf(
'Unparseable cookie date string "%s"',
$dateValue
));
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Http\Message\Decorator;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
/**
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait MessageDecorator
{
/**
* @var MessageInterface
*/
private $message;
/**
* Returns the decorated message.
*
* Since the underlying Message is immutable as well
* exposing it is not an issue, because it's state cannot be altered
*/
public function getMessage(): MessageInterface
{
return $this->message;
}
public function getProtocolVersion(): string
{
return $this->message->getProtocolVersion();
}
public function withProtocolVersion(string $version): MessageInterface
{
$new = clone $this;
$new->message = $this->message->withProtocolVersion($version);
return $new;
}
public function getHeaders(): array
{
return $this->message->getHeaders();
}
public function hasHeader(string $header): bool
{
return $this->message->hasHeader($header);
}
public function getHeader(string $header): array
{
return $this->message->getHeader($header);
}
public function getHeaderLine(string $header): string
{
return $this->message->getHeaderLine($header);
}
public function withHeader(string $header, $value): MessageInterface
{
$new = clone $this;
$new->message = $this->message->withHeader($header, $value);
return $new;
}
public function withAddedHeader(string $header, $value): MessageInterface
{
$new = clone $this;
$new->message = $this->message->withAddedHeader($header, $value);
return $new;
}
public function withoutHeader(string $header): MessageInterface
{
$new = clone $this;
$new->message = $this->message->withoutHeader($header);
return $new;
}
public function getBody(): StreamInterface
{
return $this->message->getBody();
}
public function withBody(StreamInterface $body): MessageInterface
{
$new = clone $this;
$new->message = $this->message->withBody($body);
return $new;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Http\Message\Decorator;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
/**
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait RequestDecorator
{
use MessageDecorator {
getMessage as getRequest;
}
/**
* Exchanges the underlying request with another.
*/
public function withRequest(RequestInterface $request): RequestInterface
{
$new = clone $this;
$new->message = $request;
return $new;
}
public function getRequestTarget(): string
{
return $this->message->getRequestTarget();
}
public function withRequestTarget(string $requestTarget): RequestInterface
{
$new = clone $this;
$new->message = $this->message->withRequestTarget($requestTarget);
return $new;
}
public function getMethod(): string
{
return $this->message->getMethod();
}
public function withMethod(string $method): RequestInterface
{
$new = clone $this;
$new->message = $this->message->withMethod($method);
return $new;
}
public function getUri(): UriInterface
{
return $this->message->getUri();
}
public function withUri(UriInterface $uri, bool $preserveHost = false): RequestInterface
{
$new = clone $this;
$new->message = $this->message->withUri($uri, $preserveHost);
return $new;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Http\Message\Decorator;
use Psr\Http\Message\ResponseInterface;
/**
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait ResponseDecorator
{
use MessageDecorator {
getMessage as getResponse;
}
/**
* Exchanges the underlying response with another.
*/
public function withResponse(ResponseInterface $response): ResponseInterface
{
$new = clone $this;
$new->message = $response;
return $new;
}
public function getStatusCode(): int
{
return $this->message->getStatusCode();
}
public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface
{
$new = clone $this;
$new->message = $this->message->withStatus($code, $reasonPhrase);
return $new;
}
public function getReasonPhrase(): string
{
return $this->message->getReasonPhrase();
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Http\Message\Decorator;
use Psr\Http\Message\StreamInterface;
/**
* Decorates a stream.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait StreamDecorator
{
/**
* @var StreamInterface
*/
protected $stream;
public function __toString(): string
{
return $this->stream->__toString();
}
public function close(): void
{
$this->stream->close();
}
public function detach()
{
return $this->stream->detach();
}
public function getSize(): ?int
{
return $this->stream->getSize();
}
public function tell(): int
{
return $this->stream->tell();
}
public function eof(): bool
{
return $this->stream->eof();
}
public function isSeekable(): bool
{
return $this->stream->isSeekable();
}
public function seek(int $offset, int $whence = SEEK_SET): void
{
$this->stream->seek($offset, $whence);
}
public function rewind(): void
{
$this->stream->rewind();
}
public function isWritable(): bool
{
return $this->stream->isWritable();
}
public function write(string $string): int
{
return $this->stream->write($string);
}
public function isReadable(): bool
{
return $this->stream->isReadable();
}
public function read(int $length): string
{
return $this->stream->read($length);
}
public function getContents(): string
{
return $this->stream->getContents();
}
public function getMetadata(?string $key = null)
{
return $this->stream->getMetadata($key);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Http\Message\Encoding;
/**
* Transform a regular stream into a chunked one.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class ChunkStream extends FilteredStream
{
protected function readFilter(): string
{
return 'chunk';
}
protected function writeFilter(): string
{
return 'dechunk';
}
protected function fill(): void
{
parent::fill();
if ($this->stream->eof()) {
$this->buffer .= "0\r\n\r\n";
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Psr\Http\Message\StreamInterface;
/**
* Stream compress (RFC 1950).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class CompressStream extends FilteredStream
{
/**
* @param int $level
*/
public function __construct(StreamInterface $stream, $level = -1)
{
if (!extension_loaded('zlib')) {
throw new \RuntimeException('The zlib extension must be enabled to use this stream');
}
parent::__construct($stream, ['window' => 15, 'level' => $level]);
// @deprecated will be removed in 2.0
$this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 15]);
}
protected function readFilter(): string
{
return 'zlib.deflate';
}
protected function writeFilter(): string
{
return 'zlib.inflate';
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Http\Message\Encoding;
/**
* Decorate a stream which is chunked.
*
* Allow to decode a chunked stream
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class DechunkStream extends FilteredStream
{
protected function readFilter(): string
{
return 'dechunk';
}
protected function writeFilter(): string
{
return 'chunk';
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Psr\Http\Message\StreamInterface;
/**
* Stream decompress (RFC 1950).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class DecompressStream extends FilteredStream
{
/**
* @param int $level
*/
public function __construct(StreamInterface $stream, $level = -1)
{
if (!extension_loaded('zlib')) {
throw new \RuntimeException('The zlib extension must be enabled to use this stream');
}
parent::__construct($stream, ['window' => 15]);
// @deprecated will be removed in 2.0
$this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 15, 'level' => $level]);
}
protected function readFilter(): string
{
return 'zlib.inflate';
}
protected function writeFilter(): string
{
return 'zlib.deflate';
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Psr\Http\Message\StreamInterface;
/**
* Stream deflate (RFC 1951).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class DeflateStream extends FilteredStream
{
/**
* @param int $level
*/
public function __construct(StreamInterface $stream, $level = -1)
{
parent::__construct($stream, ['window' => -15, 'level' => $level]);
// @deprecated will be removed in 2.0
$this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => -15]);
}
protected function readFilter(): string
{
return 'zlib.deflate';
}
protected function writeFilter(): string
{
return 'zlib.inflate';
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Http\Message\Encoding\Filter;
/**
* Userland implementation of the chunk stream filter.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class Chunk extends \php_user_filter
{
public function filter($in, $out, &$consumed, $closing): int
{
while ($bucket = stream_bucket_make_writeable($in)) {
$lenbucket = stream_bucket_new($this->stream, dechex($bucket->datalen)."\r\n");
stream_bucket_append($out, $lenbucket);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
$lenbucket = stream_bucket_new($this->stream, "\r\n");
stream_bucket_append($out, $lenbucket);
}
return PSFS_PASS_ON;
}
}

View File

@@ -0,0 +1,214 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Http\Message\Decorator\StreamDecorator;
use Psr\Http\Message\StreamInterface;
/**
* A filtered stream has a filter for filtering output and a filter for filtering input made to a underlying stream.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
abstract class FilteredStream implements StreamInterface
{
use StreamDecorator {
rewind as private doRewind;
seek as private doSeek;
}
public const BUFFER_SIZE = 8192;
/**
* @var callable
*/
protected $readFilterCallback;
/**
* @var resource
*
* @deprecated since version 1.5, will be removed in 2.0
*/
protected $readFilter;
/**
* @var callable
*
* @deprecated since version 1.5, will be removed in 2.0
*/
protected $writeFilterCallback;
/**
* @var resource
*
* @deprecated since version 1.5, will be removed in 2.0
*/
protected $writeFilter;
/**
* Internal buffer.
*
* @var string
*/
protected $buffer = '';
/**
* @param mixed|null $readFilterOptions
* @param mixed|null $writeFilterOptions deprecated since 1.5, will be removed in 2.0
*/
public function __construct(StreamInterface $stream, $readFilterOptions = null, $writeFilterOptions = null)
{
if (null !== $readFilterOptions) {
$this->readFilterCallback = Filter\fun($this->readFilter(), $readFilterOptions);
} else {
$this->readFilterCallback = Filter\fun($this->readFilter());
}
if (null !== $writeFilterOptions) {
$this->writeFilterCallback = Filter\fun($this->writeFilter(), $writeFilterOptions);
@trigger_error('The $writeFilterOptions argument is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED);
} else {
$this->writeFilterCallback = Filter\fun($this->writeFilter());
}
$this->stream = $stream;
}
public function read(int $length): string
{
if (strlen($this->buffer) >= $length) {
$read = substr($this->buffer, 0, $length);
$this->buffer = substr($this->buffer, $length);
return $read;
}
if ($this->stream->eof()) {
$buffer = $this->buffer;
$this->buffer = '';
return $buffer;
}
$read = $this->buffer;
$this->buffer = '';
$this->fill();
return $read.$this->read($length - strlen($read));
}
public function eof(): bool
{
return $this->stream->eof() && '' === $this->buffer;
}
/**
* Buffer is filled by reading underlying stream.
*
* Callback is reading once more even if the stream is ended.
* This allow to get last data in the PHP buffer otherwise this
* bug is present : https://bugs.php.net/bug.php?id=48725
*/
protected function fill(): void
{
$readFilterCallback = $this->readFilterCallback;
$this->buffer .= $readFilterCallback($this->stream->read(self::BUFFER_SIZE));
if ($this->stream->eof()) {
$this->buffer .= $readFilterCallback();
}
}
public function getContents(): string
{
$buffer = '';
while (!$this->eof()) {
$buf = $this->read(self::BUFFER_SIZE);
// Using a loose equality here to match on '' and false.
if (null == $buf) {
break;
}
$buffer .= $buf;
}
return $buffer;
}
/**
* Always returns null because we can't tell the size of a stream when we filter.
*/
public function getSize(): ?int
{
return null;
}
public function __toString(): string
{
return $this->getContents();
}
/**
* Filtered streams are not seekable.
*
* We would need to buffer and process everything to allow seeking.
*/
public function isSeekable(): bool
{
return false;
}
/**
* Filtered streams are not seekable and can thus not be rewound.
*/
public function rewind(): void
{
@trigger_error('Filtered streams are not seekable. This method will start raising an exception in the next major version', E_USER_DEPRECATED);
$this->doRewind();
}
/**
* Filtered streams are not seekable.
*/
public function seek(int $offset, int $whence = SEEK_SET): void
{
@trigger_error('Filtered streams are not seekable. This method will start raising an exception in the next major version', E_USER_DEPRECATED);
$this->doSeek($offset, $whence);
}
/**
* Returns the read filter name.
*
* @deprecated since version 1.5, will be removed in 2.0
*/
public function getReadFilter(): string
{
@trigger_error('The '.__CLASS__.'::'.__METHOD__.' method is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED);
return $this->readFilter();
}
/**
* Returns the write filter name.
*/
abstract protected function readFilter(): string;
/**
* Returns the write filter name.
*
* @deprecated since version 1.5, will be removed in 2.0
*/
public function getWriteFilter(): string
{
@trigger_error('The '.__CLASS__.'::'.__METHOD__.' method is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED);
return $this->writeFilter();
}
/**
* Returns the write filter name.
*/
abstract protected function writeFilter(): string;
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Psr\Http\Message\StreamInterface;
/**
* Stream for decoding from gzip format (RFC 1952).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class GzipDecodeStream extends FilteredStream
{
/**
* @param int $level
*/
public function __construct(StreamInterface $stream, $level = -1)
{
if (!extension_loaded('zlib')) {
throw new \RuntimeException('The zlib extension must be enabled to use this stream');
}
parent::__construct($stream, ['window' => 31]);
// @deprecated will be removed in 2.0
$this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 31, 'level' => $level]);
}
protected function readFilter(): string
{
return 'zlib.inflate';
}
protected function writeFilter(): string
{
return 'zlib.deflate';
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Psr\Http\Message\StreamInterface;
/**
* Stream for encoding to gzip format (RFC 1952).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class GzipEncodeStream extends FilteredStream
{
/**
* @param int $level
*/
public function __construct(StreamInterface $stream, $level = -1)
{
if (!extension_loaded('zlib')) {
throw new \RuntimeException('The zlib extension must be enabled to use this stream');
}
parent::__construct($stream, ['window' => 31, 'level' => $level]);
// @deprecated will be removed in 2.0
$this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 31]);
}
protected function readFilter(): string
{
return 'zlib.deflate';
}
protected function writeFilter(): string
{
return 'zlib.inflate';
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Psr\Http\Message\StreamInterface;
/**
* Stream inflate (RFC 1951).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class InflateStream extends FilteredStream
{
/**
* @param int $level
*/
public function __construct(StreamInterface $stream, $level = -1)
{
if (!extension_loaded('zlib')) {
throw new \RuntimeException('The zlib extension must be enabled to use this stream');
}
parent::__construct($stream, ['window' => -15]);
// @deprecated will be removed in 2.0
$this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => -15, 'level' => $level]);
}
protected function readFilter(): string
{
return 'zlib.inflate';
}
protected function writeFilter(): string
{
return 'zlib.deflate';
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Http\Message;
/**
* An interface implemented by all HTTP message related exceptions.
*/
interface Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Http\Message\Exception;
use Http\Message\Exception;
final class UnexpectedValueException extends \UnexpectedValueException implements Exception
{
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Http\Message;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Formats a request and/or a response as a string.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* The formatResponseForRequest method will be added to this interface in the next major version, replacing the formatRequest method.
* Meanwhile, callers SHOULD check the formatter for the existence of formatResponseForRequest and call that if available.
*
* @method string formatResponseForRequest(ResponseInterface $response, RequestInterface $request) Formats a response in context of its request.
*/
interface Formatter
{
/**
* Formats a request.
*
* @return string
*/
public function formatRequest(RequestInterface $request);
/**
* @deprecated since 1.13, use formatResponseForRequest() instead
*
* Formats a response.
*
* @return string
*/
public function formatResponse(ResponseInterface $response);
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Http\Message\Formatter;
use Http\Message\Formatter;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* A formatter that prints a cURL command for HTTP requests.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class CurlCommandFormatter implements Formatter
{
public function formatRequest(RequestInterface $request)
{
$command = sprintf('curl %s', escapeshellarg((string) $request->getUri()->withFragment('')));
if ('1.0' === $request->getProtocolVersion()) {
$command .= ' --http1.0';
} elseif ('2.0' === $request->getProtocolVersion()) {
$command .= ' --http2';
}
$method = strtoupper($request->getMethod());
if ('HEAD' === $method) {
$command .= ' --head';
} elseif ('GET' !== $method) {
$command .= ' --request '.$method;
}
$command .= $this->getHeadersAsCommandOptions($request);
$body = $request->getBody();
if ($body->getSize() > 0) {
// escapeshellarg argument max length on Windows, but longer body in curl command would be impractical anyways
if ($body->getSize() > 8192) {
$data = '[too long stream omitted]';
} elseif ($body->isSeekable()) {
$data = $body->__toString();
$body->rewind();
// all non-printable ASCII characters and <DEL> except for \t, \r, \n
if (preg_match('/([\x00-\x09\x0C\x0E-\x1F\x7F])/', $data)) {
$data = '[binary stream omitted]';
}
} else {
$data = '[non-seekable stream omitted]';
}
$escapedData = @escapeshellarg($data);
if (empty($escapedData)) {
$escapedData = 'We couldn\'t not escape the data properly';
}
$command .= sprintf(' --data %s', $escapedData);
}
return $command;
}
public function formatResponse(ResponseInterface $response)
{
return '';
}
/**
* Formats a response in context of its request.
*
* @return string
*/
public function formatResponseForRequest(ResponseInterface $response, RequestInterface $request)
{
return $this->formatResponse($response);
}
/**
* @return string
*/
private function getHeadersAsCommandOptions(RequestInterface $request)
{
$command = '';
foreach ($request->getHeaders() as $name => $values) {
if ('host' === strtolower($name) && $values[0] === $request->getUri()->getHost()) {
continue;
}
if ('user-agent' === strtolower($name)) {
$command .= sprintf(' -A %s', escapeshellarg($values[0]));
continue;
}
$command .= sprintf(' -H %s', escapeshellarg($name.': '.$request->getHeaderLine($name)));
}
return $command;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Http\Message\Formatter;
use Http\Message\Formatter;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* A formatter that prints the complete HTTP message.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class FullHttpMessageFormatter implements Formatter
{
/**
* The maximum length of the body.
*
* @var int|null
*/
private $maxBodyLength;
/**
* @var string
*/
private $binaryDetectionRegex;
/**
* @param int|null $maxBodyLength
* @param string $binaryDetectionRegex By default, this is all non-printable ASCII characters and <DEL> except for \t, \r, \n
*/
public function __construct($maxBodyLength = 1000, string $binaryDetectionRegex = '/([\x00-\x09\x0C\x0E-\x1F\x7F])/')
{
$this->maxBodyLength = $maxBodyLength;
$this->binaryDetectionRegex = $binaryDetectionRegex;
}
public function formatRequest(RequestInterface $request)
{
$message = sprintf(
"%s %s HTTP/%s\n",
$request->getMethod(),
$request->getRequestTarget(),
$request->getProtocolVersion()
);
foreach ($request->getHeaders() as $name => $values) {
$message .= $name.': '.implode(', ', $values)."\n";
}
return $this->addBody($request, $message);
}
public function formatResponse(ResponseInterface $response)
{
$message = sprintf(
"HTTP/%s %s %s\n",
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
foreach ($response->getHeaders() as $name => $values) {
$message .= $name.': '.implode(', ', $values)."\n";
}
return $this->addBody($response, $message);
}
/**
* Formats a response in context of its request.
*
* @return string
*/
public function formatResponseForRequest(ResponseInterface $response, RequestInterface $request)
{
return $this->formatResponse($response);
}
/**
* Add the message body if the stream is seekable.
*
* @param string $message
*
* @return string
*/
private function addBody(MessageInterface $request, $message)
{
$message .= "\n";
$stream = $request->getBody();
if (!$stream->isSeekable() || 0 === $this->maxBodyLength) {
// Do not read the stream
return $message;
}
$data = $stream->__toString();
$stream->rewind();
if (preg_match($this->binaryDetectionRegex, $data)) {
return $message.'[binary stream omitted]';
}
if (null === $this->maxBodyLength) {
return $message.$data;
}
return $message.mb_substr($data, 0, $this->maxBodyLength);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Http\Message\Formatter;
use Http\Message\Formatter;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Normalize a request or a response into a string or an array.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class SimpleFormatter implements Formatter
{
public function formatRequest(RequestInterface $request)
{
return sprintf(
'%s %s %s',
$request->getMethod(),
$request->getUri()->__toString(),
$request->getProtocolVersion()
);
}
public function formatResponse(ResponseInterface $response)
{
return sprintf(
'%s %s %s',
$response->getStatusCode(),
$response->getReasonPhrase(),
$response->getProtocolVersion()
);
}
/**
* Formats a response in context of its request.
*
* @return string
*/
public function formatResponseForRequest(ResponseInterface $response, RequestInterface $request)
{
return $this->formatResponse($response);
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Http\Message\MessageFactory;
use Http\Message\MessageFactory;
use Http\Message\StreamFactory\DiactorosStreamFactory;
use Laminas\Diactoros\Request as LaminasRequest;
use Laminas\Diactoros\Response as LaminasResponse;
use Zend\Diactoros\Request as ZendRequest;
use Zend\Diactoros\Response as ZendResponse;
if (!interface_exists(MessageFactory::class)) {
throw new \LogicException('You cannot use "Http\Message\MessageFactory\DiactorosMessageFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead');
}
/**
* Creates Diactoros messages.
*
* @author GeLo <geloen.eric@gmail.com>
*
* @deprecated This will be removed in php-http/message2.0. Consider using the official Diactoros PSR-17 factory
*/
final class DiactorosMessageFactory implements MessageFactory
{
/**
* @var DiactorosStreamFactory
*/
private $streamFactory;
public function __construct()
{
$this->streamFactory = new DiactorosStreamFactory();
}
public function createRequest(
$method,
$uri,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
) {
if (class_exists(LaminasRequest::class)) {
return (new LaminasRequest(
$uri,
$method,
$this->streamFactory->createStream($body),
$headers
))->withProtocolVersion($protocolVersion);
}
return (new ZendRequest(
$uri,
$method,
$this->streamFactory->createStream($body),
$headers
))->withProtocolVersion($protocolVersion);
}
public function createResponse(
$statusCode = 200,
$reasonPhrase = null,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
) {
if (class_exists(LaminasResponse::class)) {
return (new LaminasResponse(
$this->streamFactory->createStream($body),
$statusCode,
$headers
))->withProtocolVersion($protocolVersion);
}
return (new ZendResponse(
$this->streamFactory->createStream($body),
$statusCode,
$headers
))->withProtocolVersion($protocolVersion);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Http\Message\MessageFactory;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Http\Message\MessageFactory;
if (!interface_exists(MessageFactory::class)) {
throw new \LogicException('You cannot use "Http\Message\MessageFactory\GuzzleMessageFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead');
}
/**
* Creates Guzzle messages.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @deprecated This will be removed in php-http/message2.0. Consider using the official Guzzle PSR-17 factory
*/
final class GuzzleMessageFactory implements MessageFactory
{
public function createRequest(
$method,
$uri,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
) {
return new Request(
$method,
$uri,
$headers,
$body,
$protocolVersion
);
}
public function createResponse(
$statusCode = 200,
$reasonPhrase = null,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
) {
return new Response(
$statusCode,
$headers,
$body,
$protocolVersion,
$reasonPhrase
);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Http\Message\MessageFactory;
use Http\Message\MessageFactory;
use Http\Message\StreamFactory\SlimStreamFactory;
use Http\Message\UriFactory\SlimUriFactory;
use Slim\Http\Headers;
use Slim\Http\Request;
use Slim\Http\Response;
if (!interface_exists(MessageFactory::class)) {
throw new \LogicException('You cannot use "Http\Message\MessageFactory\SlimMessageFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead');
}
/**
* Creates Slim 3 messages.
*
* @author Mika Tuupola <tuupola@appelsiini.net>
*
* @deprecated This will be removed in php-http/message2.0. Consider using the official Slim PSR-17 factory
*/
final class SlimMessageFactory implements MessageFactory
{
/**
* @var SlimStreamFactory
*/
private $streamFactory;
/**
* @var SlimUriFactory
*/
private $uriFactory;
public function __construct()
{
$this->streamFactory = new SlimStreamFactory();
$this->uriFactory = new SlimUriFactory();
}
public function createRequest(
$method,
$uri,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
) {
return (new Request(
$method,
$this->uriFactory->createUri($uri),
new Headers($headers),
[],
[],
$this->streamFactory->createStream($body),
[]
))->withProtocolVersion($protocolVersion);
}
public function createResponse(
$statusCode = 200,
$reasonPhrase = null,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
) {
return (new Response(
$statusCode,
new Headers($headers),
$this->streamFactory->createStream($body)
))->withProtocolVersion($protocolVersion);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Http\Message;
use Psr\Http\Message\RequestInterface;
/**
* Match a request.
*
* PSR-7 equivalent of Symfony's RequestMatcher
*
* @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/RequestMatcherInterface.php
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
interface RequestMatcher
{
/**
* Decides whether the rule(s) implemented by the strategy matches the supplied request.
*
* @param RequestInterface $request The PSR7 request to check for a match
*
* @return bool true if the request matches, false otherwise
*/
public function matches(RequestInterface $request);
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Http\Message\RequestMatcher;
use Http\Message\RequestMatcher;
use Psr\Http\Message\RequestInterface;
/**
* Match a request with a callback.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class CallbackRequestMatcher implements RequestMatcher
{
/**
* @var callable
*/
private $callback;
public function __construct(callable $callback)
{
$this->callback = $callback;
}
public function matches(RequestInterface $request)
{
return (bool) call_user_func($this->callback, $request);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Http\Message\RequestMatcher;
use Http\Message\RequestMatcher;
use Psr\Http\Message\RequestInterface;
@trigger_error('The '.__NAMESPACE__.'\RegexRequestMatcher class is deprecated since version 1.2 and will be removed in 2.0. Use Http\Message\RequestMatcher\RequestMatcher instead.', E_USER_DEPRECATED);
/**
* Match a request with a regex on the uri.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*
* @deprecated since version 1.2 and will be removed in 2.0. Use {@link RequestMatcher} instead.
*/
final class RegexRequestMatcher implements RequestMatcher
{
/**
* Matching regex.
*
* @var string
*/
private $regex;
/**
* @param string $regex
*/
public function __construct($regex)
{
$this->regex = $regex;
}
public function matches(RequestInterface $request)
{
return (bool) preg_match($this->regex, (string) $request->getUri());
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Http\Message\RequestMatcher;
use Http\Message\RequestMatcher as RequestMatcherInterface;
use Psr\Http\Message\RequestInterface;
/**
* A port of the Symfony RequestMatcher for PSR-7.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class RequestMatcher implements RequestMatcherInterface
{
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $host;
/**
* @var array
*/
private $methods = [];
/**
* @var string[]
*/
private $schemes = [];
/**
* The regular expressions used for path or host must be specified without delimiter.
* You do not need to escape the forward slash / to match it.
*
* @param string|null $path Regular expression for the path
* @param string|null $host Regular expression for the hostname
* @param string|string[]|null $methods Method or list of methods to match
* @param string|string[]|null $schemes Scheme or list of schemes to match (e.g. http or https)
*/
public function __construct($path = null, $host = null, $methods = [], $schemes = [])
{
$this->path = $path;
$this->host = $host;
$this->methods = array_map('strtoupper', (array) $methods);
$this->schemes = array_map('strtolower', (array) $schemes);
}
/**
* @api
*/
public function matches(RequestInterface $request)
{
if ($this->schemes && !in_array($request->getUri()->getScheme(), $this->schemes)) {
return false;
}
if ($this->methods && !in_array($request->getMethod(), $this->methods)) {
return false;
}
if (null !== $this->path && !preg_match('{'.$this->path.'}', rawurldecode($request->getUri()->getPath()))) {
return false;
}
if (null !== $this->host && !preg_match('{'.$this->host.'}i', $request->getUri()->getHost())) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,237 @@
<?php
namespace Http\Message\Stream;
use Psr\Http\Message\StreamInterface;
/**
* Decorator to make any stream seekable.
*
* Internally it buffers an existing StreamInterface into a php://temp resource (or memory). By default it will use
* 2 megabytes of memory before writing to a temporary disk file.
*
* Due to this, very large stream can suffer performance issue (i/o slowdown).
*/
class BufferedStream implements StreamInterface
{
/** @var resource The buffered resource used to seek previous data */
private $resource;
/** @var int|null size of the stream if available */
private $size;
/** @var StreamInterface The underlying stream decorated by this class */
private $stream;
/** @var int How many bytes were written */
private $written = 0;
/**
* @param StreamInterface $stream Decorated stream
* @param bool $useFileBuffer Whether to use a file buffer (write to a file, if data exceed a certain size)
* by default, set this to false to only use memory
* @param int $memoryBuffer In conjunction with using file buffer, limit (in bytes) from which it begins to buffer
* the data in a file
*/
public function __construct(StreamInterface $stream, $useFileBuffer = true, $memoryBuffer = 2097152)
{
$this->stream = $stream;
$this->size = $stream->getSize();
if ($useFileBuffer) {
$this->resource = fopen('php://temp/maxmemory:'.$memoryBuffer, 'rw+');
} else {
$this->resource = fopen('php://memory', 'rw+');
}
if (false === $this->resource) {
throw new \RuntimeException('Cannot create a resource over temp or memory implementation');
}
}
public function __toString(): string
{
try {
$this->rewind();
return $this->getContents();
} catch (\Throwable $throwable) {
return '';
}
}
public function close(): void
{
if (null === $this->resource) {
throw new \RuntimeException('Cannot close on a detached stream');
}
$this->stream->close();
fclose($this->resource);
}
public function detach()
{
if (null === $this->resource) {
return null;
}
// Force reading the remaining data of the stream
$this->getContents();
$resource = $this->resource;
$this->stream->close();
$this->stream = null;
$this->resource = null;
return $resource;
}
public function getSize(): ?int
{
if (null === $this->resource) {
return null;
}
if (null === $this->size && $this->stream->eof()) {
return $this->written;
}
return $this->size;
}
public function tell(): int
{
if (null === $this->resource) {
throw new \RuntimeException('Cannot tell on a detached stream');
}
$tell = ftell($this->resource);
if (false === $tell) {
throw new \RuntimeException('ftell failed');
}
return $tell;
}
public function eof(): bool
{
if (null === $this->resource) {
throw new \RuntimeException('Cannot call eof on a detached stream');
}
// We are at the end only when both our resource and underlying stream are at eof
return $this->stream->eof() && (ftell($this->resource) === $this->written);
}
public function isSeekable(): bool
{
return null !== $this->resource;
}
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (null === $this->resource) {
throw new \RuntimeException('Cannot seek on a detached stream');
}
fseek($this->resource, $offset, $whence);
}
public function rewind(): void
{
if (null === $this->resource) {
throw new \RuntimeException('Cannot rewind on a detached stream');
}
rewind($this->resource);
}
public function isWritable(): bool
{
return false;
}
public function write(string $string): int
{
throw new \RuntimeException('Cannot write on this stream');
}
public function isReadable(): bool
{
return null !== $this->resource;
}
public function read(int $length): string
{
if (null === $this->resource) {
throw new \RuntimeException('Cannot read on a detached stream');
}
if ($length < 0) {
throw new \InvalidArgumentException('Can not read a negative amount of bytes');
}
if (0 === $length) {
return '';
}
$read = '';
// First read from the resource
if (ftell($this->resource) !== $this->written) {
$read = fread($this->resource, $length);
}
if (false === $read) {
throw new \RuntimeException('Failed to read from resource');
}
$bytesRead = strlen($read);
if ($bytesRead < $length) {
$streamRead = $this->stream->read($length - $bytesRead);
// Write on the underlying stream what we read
$this->written += fwrite($this->resource, $streamRead);
$read .= $streamRead;
}
return $read;
}
public function getContents(): string
{
if (null === $this->resource) {
throw new \RuntimeException('Cannot read on a detached stream');
}
$read = '';
while (!$this->eof()) {
$read .= $this->read(8192);
}
return $read;
}
public function getMetadata(?string $key = null)
{
if (null === $this->resource) {
if (null === $key) {
return [];
}
return null;
}
$metadata = stream_get_meta_data($this->resource);
if (null === $key) {
return $metadata;
}
if (!array_key_exists($key, $metadata)) {
return null;
}
return $metadata[$key];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Http\Message\StreamFactory;
use Http\Message\StreamFactory;
use Laminas\Diactoros\Stream as LaminasStream;
use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Stream as ZendStream;
if (!interface_exists(StreamFactory::class)) {
throw new \LogicException('You cannot use "Http\Message\MessageFactory\DiactorosStreamFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead');
}
/**
* Creates Diactoros streams.
*
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*
* @deprecated This will be removed in php-http/message2.0. Consider using the official Diactoros PSR-17 factory
*/
final class DiactorosStreamFactory implements StreamFactory
{
public function createStream($body = null)
{
if ($body instanceof StreamInterface) {
return $body;
}
if (is_resource($body)) {
if (class_exists(LaminasStream::class)) {
return new LaminasStream($body);
}
return new ZendStream($body);
}
if (class_exists(LaminasStream::class)) {
$stream = new LaminasStream('php://memory', 'rw');
} else {
$stream = new ZendStream('php://memory', 'rw');
}
if (null !== $body && '' !== $body) {
$stream->write((string) $body);
}
return $stream;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Http\Message\StreamFactory;
use GuzzleHttp\Psr7\Utils;
use Http\Message\StreamFactory;
if (!interface_exists(StreamFactory::class)) {
throw new \LogicException('You cannot use "Http\Message\MessageFactory\GuzzleStreamFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead');
}
/**
* Creates Guzzle streams.
*
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*
* @deprecated This will be removed in php-http/message2.0. Consider using the official Guzzle PSR-17 factory
*/
final class GuzzleStreamFactory implements StreamFactory
{
public function createStream($body = null)
{
if (class_exists(Utils::class)) {
return Utils::streamFor($body);
}
// legacy support for guzzle/psr7 1.*
return \GuzzleHttp\Psr7\stream_for($body);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Http\Message\StreamFactory;
use Http\Message\StreamFactory;
use Psr\Http\Message\StreamInterface;
use Slim\Http\Stream;
if (!interface_exists(StreamFactory::class)) {
throw new \LogicException('You cannot use "Http\Message\MessageFactory\SlimStreamFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead');
}
/**
* Creates Slim 3 streams.
*
* @author Mika Tuupola <tuupola@appelsiini.net>
*
* @deprecated This will be removed in php-http/message2.0. Consider using the official Slim PSR-17 factory
*/
final class SlimStreamFactory implements StreamFactory
{
public function createStream($body = null)
{
if ($body instanceof StreamInterface) {
return $body;
}
if (is_resource($body)) {
return new Stream($body);
}
$resource = fopen('php://memory', 'r+');
$stream = new Stream($resource);
if (null !== $body && '' !== $body) {
$stream->write((string) $body);
}
return $stream;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Http\Message\UriFactory;
use Http\Message\UriFactory;
use Laminas\Diactoros\Uri as LaminasUri;
use Psr\Http\Message\UriInterface;
use Zend\Diactoros\Uri as ZendUri;
if (!interface_exists(UriFactory::class)) {
throw new \LogicException('You cannot use "Http\Message\MessageFactory\DiactorosUriFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead');
}
/**
* Creates Diactoros URI.
*
* @author David de Boer <david@ddeboer.nl>
*
* @deprecated This will be removed in php-http/message2.0. Consider using the official Diactoros PSR-17 factory
*/
final class DiactorosUriFactory implements UriFactory
{
public function createUri($uri)
{
if ($uri instanceof UriInterface) {
return $uri;
} elseif (is_string($uri)) {
if (class_exists(LaminasUri::class)) {
return new LaminasUri($uri);
}
return new ZendUri($uri);
}
throw new \InvalidArgumentException('URI must be a string or UriInterface');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Http\Message\UriFactory;
use GuzzleHttp\Psr7\Utils;
use Http\Message\UriFactory;
use function GuzzleHttp\Psr7\uri_for;
if (!interface_exists(UriFactory::class)) {
throw new \LogicException('You cannot use "Http\Message\MessageFactory\GuzzleUriFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead');
}
/**
* Creates Guzzle URI.
*
* @author David de Boer <david@ddeboer.nl>
*
* @deprecated This will be removed in php-http/message2.0. Consider using the official Guzzle PSR-17 factory
*/
final class GuzzleUriFactory implements UriFactory
{
public function createUri($uri)
{
if (class_exists(Utils::class)) {
return Utils::uriFor($uri);
}
return uri_for($uri);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Http\Message\UriFactory;
use Http\Message\UriFactory;
use Psr\Http\Message\UriInterface;
use Slim\Http\Uri;
if (!interface_exists(UriFactory::class)) {
throw new \LogicException('You cannot use "Http\Message\MessageFactory\SlimUriFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead');
}
/**
* Creates Slim 3 URI.
*
* @author Mika Tuupola <tuupola@appelsiini.net>
*
* @deprecated This will be removed in php-http/message2.0. Consider using the official Slim PSR-17 factory
*/
final class SlimUriFactory implements UriFactory
{
public function createUri($uri)
{
if ($uri instanceof UriInterface) {
return $uri;
}
if (is_string($uri)) {
return Uri::createFromString($uri);
}
throw new \InvalidArgumentException('URI must be a string or UriInterface');
}
}

View File

@@ -0,0 +1,6 @@
<?php
// Register chunk filter if not found
if (!array_key_exists('chunk', stream_get_filters())) {
stream_filter_register('chunk', 'Http\Message\Encoding\Filter\Chunk');
}