commit vendor

This commit is contained in:
2025-11-11 14:49:30 +01:00
parent f33121a308
commit 6d03080c00
2436 changed files with 483781 additions and 0 deletions

220
vendor/sabre/http/lib/Auth/AWS.php vendored Normal file
View File

@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP\Auth;
use Sabre\HTTP;
/**
* HTTP AWS Authentication handler.
*
* Use this class to leverage amazon's AWS authentication header
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class AWS extends AbstractAuth
{
/**
* The signature supplied by the HTTP client.
*
* @var string
*/
private $signature = null;
/**
* The accesskey supplied by the HTTP client.
*
* @var string
*/
private $accessKey = null;
/**
* An error code, if any.
*
* This value will be filled with one of the ERR_* constants
*
* @var int
*/
public $errorCode = 0;
const ERR_NOAWSHEADER = 1;
const ERR_MD5CHECKSUMWRONG = 2;
const ERR_INVALIDDATEFORMAT = 3;
const ERR_REQUESTTIMESKEWED = 4;
const ERR_INVALIDSIGNATURE = 5;
/**
* Gathers all information from the headers.
*
* This method needs to be called prior to anything else.
*/
public function init(): bool
{
$authHeader = $this->request->getHeader('Authorization');
if (null === $authHeader) {
$this->errorCode = self::ERR_NOAWSHEADER;
return false;
}
$authHeader = explode(' ', $authHeader);
if ('AWS' !== $authHeader[0] || !isset($authHeader[1])) {
$this->errorCode = self::ERR_NOAWSHEADER;
return false;
}
list($this->accessKey, $this->signature) = explode(':', $authHeader[1]);
return true;
}
/**
* Returns the username for the request.
*/
public function getAccessKey(): string
{
return $this->accessKey;
}
/**
* Validates the signature based on the secretKey.
*/
public function validate(string $secretKey): bool
{
$contentMD5 = $this->request->getHeader('Content-MD5');
if ($contentMD5) {
// We need to validate the integrity of the request
$body = $this->request->getBody();
$this->request->setBody($body);
if ($contentMD5 !== base64_encode(md5((string) $body, true))) {
// content-md5 header did not match md5 signature of body
$this->errorCode = self::ERR_MD5CHECKSUMWRONG;
return false;
}
}
if (!$requestDate = $this->request->getHeader('x-amz-date')) {
$requestDate = $this->request->getHeader('Date');
}
if (!$this->validateRFC2616Date((string) $requestDate)) {
return false;
}
$amzHeaders = $this->getAmzHeaders();
$signature = base64_encode(
$this->hmacsha1($secretKey,
$this->request->getMethod()."\n".
$contentMD5."\n".
$this->request->getHeader('Content-type')."\n".
$requestDate."\n".
$amzHeaders.
$this->request->getUrl()
)
);
if ($this->signature !== $signature) {
$this->errorCode = self::ERR_INVALIDSIGNATURE;
return false;
}
return true;
}
/**
* Returns an HTTP 401 header, forcing login.
*
* This should be called when username and password are incorrect, or not supplied at all
*/
public function requireLogin()
{
$this->response->addHeader('WWW-Authenticate', 'AWS');
$this->response->setStatus(401);
}
/**
* Makes sure the supplied value is a valid RFC2616 date.
*
* If we would just use strtotime to get a valid timestamp, we have no way of checking if a
* user just supplied the word 'now' for the date header.
*
* This function also makes sure the Date header is within 15 minutes of the operating
* system date, to prevent replay attacks.
*/
protected function validateRFC2616Date(string $dateHeader): bool
{
$date = HTTP\parseDate($dateHeader);
// Unknown format
if (!$date) {
$this->errorCode = self::ERR_INVALIDDATEFORMAT;
return false;
}
$min = new \DateTime('-15 minutes');
$max = new \DateTime('+15 minutes');
// We allow 15 minutes around the current date/time
if ($date > $max || $date < $min) {
$this->errorCode = self::ERR_REQUESTTIMESKEWED;
return false;
}
return true;
}
/**
* Returns a list of AMZ headers.
*/
protected function getAmzHeaders(): string
{
$amzHeaders = [];
$headers = $this->request->getHeaders();
foreach ($headers as $headerName => $headerValue) {
if (0 === strpos(strtolower($headerName), 'x-amz-')) {
$amzHeaders[strtolower($headerName)] = str_replace(["\r\n"], [' '], $headerValue[0])."\n";
}
}
ksort($amzHeaders);
$headerStr = '';
foreach ($amzHeaders as $h => $v) {
$headerStr .= $h.':'.$v;
}
return $headerStr;
}
/**
* Generates an HMAC-SHA1 signature.
*/
private function hmacsha1(string $key, string $message): string
{
if (function_exists('hash_hmac')) {
return hash_hmac('sha1', $message, $key, true);
}
$blocksize = 64;
if (strlen($key) > $blocksize) {
$key = pack('H*', sha1($key));
}
$key = str_pad($key, $blocksize, chr(0x00));
$ipad = str_repeat(chr(0x36), $blocksize);
$opad = str_repeat(chr(0x5c), $blocksize);
$hmac = pack('H*', sha1(($key ^ $opad).pack('H*', sha1(($key ^ $ipad).$message))));
return $hmac;
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP\Auth;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* HTTP Authentication base class.
*
* This class provides some common functionality for the various base classes.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
abstract class AbstractAuth
{
/**
* Authentication realm.
*
* @var string
*/
protected $realm;
/**
* Request object.
*
* @var RequestInterface
*/
protected $request;
/**
* Response object.
*
* @var ResponseInterface
*/
protected $response;
/**
* Creates the object.
*/
public function __construct(string $realm = 'SabreTooth', RequestInterface $request, ResponseInterface $response)
{
$this->realm = $realm;
$this->request = $request;
$this->response = $response;
}
/**
* This method sends the needed HTTP header and statuscode (401) to force
* the user to login.
*/
abstract public function requireLogin();
/**
* Returns the HTTP realm.
*/
public function getRealm(): string
{
return $this->realm;
}
}

60
vendor/sabre/http/lib/Auth/Basic.php vendored Normal file
View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP\Auth;
/**
* HTTP Basic authentication utility.
*
* This class helps you setup basic auth. The process is fairly simple:
*
* 1. Instantiate the class.
* 2. Call getCredentials (this will return null or a user/pass pair)
* 3. If you didn't get valid credentials, call 'requireLogin'
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Basic extends AbstractAuth
{
/**
* This method returns a numeric array with a username and password as the
* only elements.
*
* If no credentials were found, this method returns null.
*
* @return array|null
*/
public function getCredentials()
{
$auth = $this->request->getHeader('Authorization');
if (!$auth) {
return null;
}
if ('basic ' !== strtolower(substr($auth, 0, 6))) {
return null;
}
$credentials = explode(':', base64_decode(substr($auth, 6)), 2);
if (2 !== count($credentials)) {
return null;
}
return $credentials;
}
/**
* This method sends the needed HTTP header and statuscode (401) to force
* the user to login.
*/
public function requireLogin()
{
$this->response->addHeader('WWW-Authenticate', 'Basic realm="'.$this->realm.'", charset="UTF-8"');
$this->response->setStatus(401);
}
}

53
vendor/sabre/http/lib/Auth/Bearer.php vendored Normal file
View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP\Auth;
/**
* HTTP Bearer authentication utility.
*
* This class helps you setup bearer auth. The process is fairly simple:
*
* 1. Instantiate the class.
* 2. Call getToken (this will return null or a token as string)
* 3. If you didn't get a valid token, call 'requireLogin'
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author François Kooman (fkooman@tuxed.net)
* @license http://sabre.io/license/ Modified BSD License
*/
class Bearer extends AbstractAuth
{
/**
* This method returns a string with an access token.
*
* If no token was found, this method returns null.
*
* @return string|null
*/
public function getToken()
{
$auth = $this->request->getHeader('Authorization');
if (!$auth) {
return null;
}
if ('bearer ' !== strtolower(substr($auth, 0, 7))) {
return null;
}
return substr($auth, 7);
}
/**
* This method sends the needed HTTP header and statuscode (401) to force
* authentication.
*/
public function requireLogin()
{
$this->response->addHeader('WWW-Authenticate', 'Bearer realm="'.$this->realm.'"');
$this->response->setStatus(401);
}
}

210
vendor/sabre/http/lib/Auth/Digest.php vendored Normal file
View File

@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP\Auth;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* HTTP Digest Authentication handler.
*
* Use this class for easy http digest authentication.
* Instructions:
*
* 1. Create the object
* 2. Call the setRealm() method with the realm you plan to use
* 3. Call the init method function.
* 4. Call the getUserName() function. This function may return null if no
* authentication information was supplied. Based on the username you
* should check your internal database for either the associated password,
* or the so-called A1 hash of the digest.
* 5. Call either validatePassword() or validateA1(). This will return true
* or false.
* 6. To make sure an authentication prompt is displayed, call the
* requireLogin() method.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Digest extends AbstractAuth
{
/**
* These constants are used in setQOP();.
*/
const QOP_AUTH = 1;
const QOP_AUTHINT = 2;
protected $nonce;
protected $opaque;
protected $digestParts;
protected $A1;
protected $qop = self::QOP_AUTH;
/**
* Initializes the object.
*/
public function __construct(string $realm = 'SabreTooth', RequestInterface $request, ResponseInterface $response)
{
$this->nonce = uniqid();
$this->opaque = md5($realm);
parent::__construct($realm, $request, $response);
}
/**
* Gathers all information from the headers.
*
* This method needs to be called prior to anything else.
*/
public function init()
{
$digest = $this->getDigest();
$this->digestParts = $this->parseDigest((string) $digest);
}
/**
* Sets the quality of protection value.
*
* Possible values are:
* Sabre\HTTP\DigestAuth::QOP_AUTH
* Sabre\HTTP\DigestAuth::QOP_AUTHINT
*
* Multiple values can be specified using logical OR.
*
* QOP_AUTHINT ensures integrity of the request body, but this is not
* supported by most HTTP clients. QOP_AUTHINT also requires the entire
* request body to be md5'ed, which can put strains on CPU and memory.
*/
public function setQOP(int $qop)
{
$this->qop = $qop;
}
/**
* Validates the user.
*
* The A1 parameter should be md5($username . ':' . $realm . ':' . $password);
*/
public function validateA1(string $A1): bool
{
$this->A1 = $A1;
return $this->validate();
}
/**
* Validates authentication through a password. The actual password must be provided here.
* It is strongly recommended not store the password in plain-text and use validateA1 instead.
*/
public function validatePassword(string $password): bool
{
$this->A1 = md5($this->digestParts['username'].':'.$this->realm.':'.$password);
return $this->validate();
}
/**
* Returns the username for the request.
* Returns null if there were none.
*
* @return string|null
*/
public function getUsername()
{
return $this->digestParts['username'] ?? null;
}
/**
* Validates the digest challenge.
*/
protected function validate(): bool
{
if (!is_array($this->digestParts)) {
return false;
}
$A2 = $this->request->getMethod().':'.$this->digestParts['uri'];
if ('auth-int' === $this->digestParts['qop']) {
// Making sure we support this qop value
if (!($this->qop & self::QOP_AUTHINT)) {
return false;
}
// We need to add an md5 of the entire request body to the A2 part of the hash
$body = $this->request->getBody($asString = true);
$this->request->setBody($body);
$A2 .= ':'.md5($body);
} elseif (!($this->qop & self::QOP_AUTH)) {
return false;
}
$A2 = md5($A2);
$validResponse = md5("{$this->A1}:{$this->digestParts['nonce']}:{$this->digestParts['nc']}:{$this->digestParts['cnonce']}:{$this->digestParts['qop']}:{$A2}");
return $this->digestParts['response'] === $validResponse;
}
/**
* Returns an HTTP 401 header, forcing login.
*
* This should be called when username and password are incorrect, or not supplied at all
*/
public function requireLogin()
{
$qop = '';
switch ($this->qop) {
case self::QOP_AUTH:
$qop = 'auth';
break;
case self::QOP_AUTHINT:
$qop = 'auth-int';
break;
case self::QOP_AUTH | self::QOP_AUTHINT:
$qop = 'auth,auth-int';
break;
}
$this->response->addHeader('WWW-Authenticate', 'Digest realm="'.$this->realm.'",qop="'.$qop.'",nonce="'.$this->nonce.'",opaque="'.$this->opaque.'"');
$this->response->setStatus(401);
}
/**
* This method returns the full digest string.
*
* It should be compatibile with mod_php format and other webservers.
*
* If the header could not be found, null will be returned
*
* @return mixed
*/
public function getDigest()
{
return $this->request->getHeader('Authorization');
}
/**
* Parses the different pieces of the digest string into an array.
*
* This method returns false if an incomplete digest was supplied
*
* @return bool|array
*/
protected function parseDigest(string $digest)
{
// protect against missing data
$needed_parts = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1];
$data = [];
preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $digest, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
$data[$m[1]] = $m[2] ?: $m[3];
unset($needed_parts[$m[1]]);
}
return $needed_parts ? false : $data;
}
}

614
vendor/sabre/http/lib/Client.php vendored Normal file
View File

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

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
/**
* This exception may be emitted by the HTTP\Client class, in case there was a
* problem emitting the request.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ClientException extends \Exception
{
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
/**
* This exception represents a HTTP error coming from the Client.
*
* By default the Client will not emit these, this has to be explicitly enabled
* with the setThrowExceptions method.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ClientHttpException extends \Exception implements HttpException
{
/**
* Response object.
*
* @var ResponseInterface
*/
protected $response;
/**
* Constructor.
*/
public function __construct(ResponseInterface $response)
{
$this->response = $response;
parent::__construct($response->getStatusText(), $response->getStatus());
}
/**
* The http status code for the error.
*/
public function getHttpStatus(): int
{
return $this->response->getStatus();
}
/**
* Returns the full response object.
*/
public function getResponse(): ResponseInterface
{
return $this->response;
}
}

31
vendor/sabre/http/lib/HttpException.php vendored Normal file
View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
/**
* An exception representing a HTTP error.
*
* This can be used as a generic exception in your application, if you'd like
* to map HTTP errors to exceptions.
*
* If you'd like to use this, create a new exception class, extending Exception
* and implementing this interface.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface HttpException
{
/**
* The http status code for the error.
*
* This may either be just the number, or a number and a human-readable
* message, separated by a space.
*
* @return string|null
*/
public function getHttpStatus();
}

291
vendor/sabre/http/lib/Message.php vendored Normal file
View File

@ -0,0 +1,291 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
/**
* This is the abstract base class for both the Request and Response objects.
*
* This object contains a few simple methods that are shared by both.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
abstract class Message implements MessageInterface
{
/**
* Request body.
*
* This should be a stream resource, string or a callback writing the body to php://output
*
* @var resource|string|callable
*/
protected $body;
/**
* Contains the list of HTTP headers.
*
* @var array
*/
protected $headers = [];
/**
* HTTP message version (1.0, 1.1 or 2.0).
*
* @var string
*/
protected $httpVersion = '1.1';
/**
* Returns the body as a readable stream resource.
*
* Note that the stream may not be rewindable, and therefore may only be
* read once.
*
* @return resource
*/
public function getBodyAsStream()
{
$body = $this->getBody();
if (is_callable($this->body)) {
$body = $this->getBodyAsString();
}
if (is_string($body) || null === $body) {
$stream = fopen('php://temp', 'r+');
fwrite($stream, (string) $body);
rewind($stream);
return $stream;
}
return $body;
}
/**
* Returns the body as a string.
*
* Note that because the underlying data may be based on a stream, this
* method could only work correctly the first time.
*/
public function getBodyAsString(): string
{
$body = $this->getBody();
if (is_string($body)) {
return $body;
}
if (null === $body) {
return '';
}
if (is_callable($body)) {
ob_start();
$body();
return ob_get_clean();
}
/**
* @var string|int|null
*/
$contentLength = $this->getHeader('Content-Length');
if (is_int($contentLength) || ctype_digit($contentLength)) {
return stream_get_contents($body, (int) $contentLength);
}
return stream_get_contents($body);
}
/**
* Returns the message body, as it's internal representation.
*
* This could be either a string, a stream or a callback writing the body to php://output.
*
* @return resource|string|callable
*/
public function getBody()
{
return $this->body;
}
/**
* Replaces the body resource with a new stream, string or a callback writing the body to php://output.
*
* @param resource|string|callable $body
*/
public function setBody($body)
{
$this->body = $body;
}
/**
* Returns all the HTTP headers as an array.
*
* Every header is returned as an array, with one or more values.
*/
public function getHeaders(): array
{
$result = [];
foreach ($this->headers as $headerInfo) {
$result[$headerInfo[0]] = $headerInfo[1];
}
return $result;
}
/**
* Will return true or false, depending on if a HTTP header exists.
*/
public function hasHeader(string $name): bool
{
return isset($this->headers[strtolower($name)]);
}
/**
* Returns a specific HTTP header, based on it's name.
*
* The name must be treated as case-insensitive.
* If the header does not exist, this method must return null.
*
* If a header appeared more than once in a HTTP request, this method will
* concatenate all the values with a comma.
*
* Note that this not make sense for all headers. Some, such as
* `Set-Cookie` cannot be logically combined with a comma. In those cases
* you *should* use getHeaderAsArray().
*
* @return string|null
*/
public function getHeader(string $name)
{
$name = strtolower($name);
if (isset($this->headers[$name])) {
return implode(',', $this->headers[$name][1]);
}
return null;
}
/**
* Returns a HTTP header as an array.
*
* For every time the HTTP header appeared in the request or response, an
* item will appear in the array.
*
* If the header did not exists, this method will return an empty array.
*
* @return string[]
*/
public function getHeaderAsArray(string $name): array
{
$name = strtolower($name);
if (isset($this->headers[$name])) {
return $this->headers[$name][1];
}
return [];
}
/**
* Updates a HTTP header.
*
* The case-sensitivity of the name value must be retained as-is.
*
* If the header already existed, it will be overwritten.
*
* @param string|string[] $value
*/
public function setHeader(string $name, $value)
{
$this->headers[strtolower($name)] = [$name, (array) $value];
}
/**
* Sets a new set of HTTP headers.
*
* The headers array should contain headernames for keys, and their value
* should be specified as either a string or an array.
*
* Any header that already existed will be overwritten.
*/
public function setHeaders(array $headers)
{
foreach ($headers as $name => $value) {
$this->setHeader($name, $value);
}
}
/**
* Adds a HTTP header.
*
* This method will not overwrite any existing HTTP header, but instead add
* another value. Individual values can be retrieved with
* getHeadersAsArray.
*
* @param string|string[] $value
*/
public function addHeader(string $name, $value)
{
$lName = strtolower($name);
if (isset($this->headers[$lName])) {
$this->headers[$lName][1] = array_merge(
$this->headers[$lName][1],
(array) $value
);
} else {
$this->headers[$lName] = [
$name,
(array) $value,
];
}
}
/**
* Adds a new set of HTTP headers.
*
* Any existing headers will not be overwritten.
*/
public function addHeaders(array $headers)
{
foreach ($headers as $name => $value) {
$this->addHeader($name, $value);
}
}
/**
* Removes a HTTP header.
*
* The specified header name must be treated as case-insensitive.
* This method should return true if the header was successfully deleted,
* and false if the header did not exist.
*/
public function removeHeader(string $name): bool
{
$name = strtolower($name);
if (!isset($this->headers[$name])) {
return false;
}
unset($this->headers[$name]);
return true;
}
/**
* Sets the HTTP version.
*
* Should be 1.0, 1.1 or 2.0.
*/
public function setHttpVersion(string $version)
{
$this->httpVersion = $version;
}
/**
* Returns the HTTP version.
*/
public function getHttpVersion(): string
{
return $this->httpVersion;
}
}

View File

@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
/**
* This trait contains a bunch of methods, shared by both the RequestDecorator
* and the ResponseDecorator.
*
* Didn't seem needed to create a full class for this, so we're just
* implementing it as a trait.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
trait MessageDecoratorTrait
{
/**
* The inner request object.
*
* All method calls will be forwarded here.
*
* @var MessageInterface
*/
protected $inner;
/**
* Returns the body as a readable stream resource.
*
* Note that the stream may not be rewindable, and therefore may only be
* read once.
*
* @return resource
*/
public function getBodyAsStream()
{
return $this->inner->getBodyAsStream();
}
/**
* Returns the body as a string.
*
* Note that because the underlying data may be based on a stream, this
* method could only work correctly the first time.
*/
public function getBodyAsString(): string
{
return $this->inner->getBodyAsString();
}
/**
* Returns the message body, as it's internal representation.
*
* This could be either a string or a stream.
*
* @return resource|string
*/
public function getBody()
{
return $this->inner->getBody();
}
/**
* Updates the body resource with a new stream.
*
* @param resource|string|callable $body
*/
public function setBody($body)
{
$this->inner->setBody($body);
}
/**
* Returns all the HTTP headers as an array.
*
* Every header is returned as an array, with one or more values.
*/
public function getHeaders(): array
{
return $this->inner->getHeaders();
}
/**
* Will return true or false, depending on if a HTTP header exists.
*/
public function hasHeader(string $name): bool
{
return $this->inner->hasHeader($name);
}
/**
* Returns a specific HTTP header, based on it's name.
*
* The name must be treated as case-insensitive.
* If the header does not exist, this method must return null.
*
* If a header appeared more than once in a HTTP request, this method will
* concatenate all the values with a comma.
*
* Note that this not make sense for all headers. Some, such as
* `Set-Cookie` cannot be logically combined with a comma. In those cases
* you *should* use getHeaderAsArray().
*
* @return string|null
*/
public function getHeader(string $name)
{
return $this->inner->getHeader($name);
}
/**
* Returns a HTTP header as an array.
*
* For every time the HTTP header appeared in the request or response, an
* item will appear in the array.
*
* If the header did not exists, this method will return an empty array.
*/
public function getHeaderAsArray(string $name): array
{
return $this->inner->getHeaderAsArray($name);
}
/**
* Updates a HTTP header.
*
* The case-sensitivity of the name value must be retained as-is.
*
* If the header already existed, it will be overwritten.
*
* @param string|string[] $value
*/
public function setHeader(string $name, $value)
{
$this->inner->setHeader($name, $value);
}
/**
* Sets a new set of HTTP headers.
*
* The headers array should contain headernames for keys, and their value
* should be specified as either a string or an array.
*
* Any header that already existed will be overwritten.
*/
public function setHeaders(array $headers)
{
$this->inner->setHeaders($headers);
}
/**
* Adds a HTTP header.
*
* This method will not overwrite any existing HTTP header, but instead add
* another value. Individual values can be retrieved with
* getHeadersAsArray.
*
* @param string|string[] $value
*/
public function addHeader(string $name, $value)
{
$this->inner->addHeader($name, $value);
}
/**
* Adds a new set of HTTP headers.
*
* Any existing headers will not be overwritten.
*/
public function addHeaders(array $headers)
{
$this->inner->addHeaders($headers);
}
/**
* Removes a HTTP header.
*
* The specified header name must be treated as case-insensitive.
* This method should return true if the header was successfully deleted,
* and false if the header did not exist.
*/
public function removeHeader(string $name): bool
{
return $this->inner->removeHeader($name);
}
/**
* Sets the HTTP version.
*
* Should be 1.0, 1.1 or 2.0.
*/
public function setHttpVersion(string $version)
{
$this->inner->setHttpVersion($version);
}
/**
* Returns the HTTP version.
*/
public function getHttpVersion(): string
{
return $this->inner->getHttpVersion();
}
}

View File

@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
/**
* The MessageInterface is the base interface that's used by both
* the RequestInterface and ResponseInterface.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface MessageInterface
{
/**
* Returns the body as a readable stream resource.
*
* Note that the stream may not be rewindable, and therefore may only be
* read once.
*
* @return resource
*/
public function getBodyAsStream();
/**
* Returns the body as a string.
*
* Note that because the underlying data may be based on a stream, this
* method could only work correctly the first time.
*/
public function getBodyAsString(): string;
/**
* Returns the message body, as it's internal representation.
*
* This could be either a string, a stream or a callback writing the body to php://output
*
* @return resource|string|callable
*/
public function getBody();
/**
* Updates the body resource with a new stream.
*
* @param resource|string|callable $body
*/
public function setBody($body);
/**
* Returns all the HTTP headers as an array.
*
* Every header is returned as an array, with one or more values.
*/
public function getHeaders(): array;
/**
* Will return true or false, depending on if a HTTP header exists.
*/
public function hasHeader(string $name): bool;
/**
* Returns a specific HTTP header, based on it's name.
*
* The name must be treated as case-insensitive.
* If the header does not exist, this method must return null.
*
* If a header appeared more than once in a HTTP request, this method will
* concatenate all the values with a comma.
*
* Note that this not make sense for all headers. Some, such as
* `Set-Cookie` cannot be logically combined with a comma. In those cases
* you *should* use getHeaderAsArray().
*
* @return string|null
*/
public function getHeader(string $name);
/**
* Returns a HTTP header as an array.
*
* For every time the HTTP header appeared in the request or response, an
* item will appear in the array.
*
* If the header did not exists, this method will return an empty array.
*
* @return string[]
*/
public function getHeaderAsArray(string $name): array;
/**
* Updates a HTTP header.
*
* The case-sensitity of the name value must be retained as-is.
*
* If the header already existed, it will be overwritten.
*
* @param string|string[] $value
*/
public function setHeader(string $name, $value);
/**
* Sets a new set of HTTP headers.
*
* The headers array should contain headernames for keys, and their value
* should be specified as either a string or an array.
*
* Any header that already existed will be overwritten.
*/
public function setHeaders(array $headers);
/**
* Adds a HTTP header.
*
* This method will not overwrite any existing HTTP header, but instead add
* another value. Individual values can be retrieved with
* getHeadersAsArray.
*
* @param string|string[] $value
*/
public function addHeader(string $name, $value);
/**
* Adds a new set of HTTP headers.
*
* Any existing headers will not be overwritten.
*/
public function addHeaders(array $headers);
/**
* Removes a HTTP header.
*
* The specified header name must be treated as case-insenstive.
* This method should return true if the header was successfully deleted,
* and false if the header did not exist.
*/
public function removeHeader(string $name): bool;
/**
* Sets the HTTP version.
*
* Should be 1.0, 1.1 or 2.0.
*/
public function setHttpVersion(string $version);
/**
* Returns the HTTP version.
*/
public function getHttpVersion(): string;
}

267
vendor/sabre/http/lib/Request.php vendored Normal file
View File

@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
use LogicException;
use Sabre\Uri;
/**
* The Request class represents a single HTTP request.
*
* You can either simply construct the object from scratch, or if you need
* access to the current HTTP request, use Sapi::getRequest.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Request extends Message implements RequestInterface
{
/**
* HTTP Method.
*
* @var string
*/
protected $method;
/**
* Request Url.
*
* @var string
*/
protected $url;
/**
* Creates the request object.
*
* @param resource|callable|string $body
*/
public function __construct(string $method, string $url, array $headers = [], $body = null)
{
$this->setMethod($method);
$this->setUrl($url);
$this->setHeaders($headers);
$this->setBody($body);
}
/**
* Returns the current HTTP method.
*/
public function getMethod(): string
{
return $this->method;
}
/**
* Sets the HTTP method.
*/
public function setMethod(string $method)
{
$this->method = $method;
}
/**
* Returns the request url.
*/
public function getUrl(): string
{
return $this->url;
}
/**
* Sets the request url.
*/
public function setUrl(string $url)
{
$this->url = $url;
}
/**
* Returns the list of query parameters.
*
* This is equivalent to PHP's $_GET superglobal.
*/
public function getQueryParameters(): array
{
$url = $this->getUrl();
if (false === ($index = strpos($url, '?'))) {
return [];
}
parse_str(substr($url, $index + 1), $queryParams);
return $queryParams;
}
protected $absoluteUrl;
/**
* Sets the absolute url.
*/
public function setAbsoluteUrl(string $url)
{
$this->absoluteUrl = $url;
}
/**
* Returns the absolute url.
*/
public function getAbsoluteUrl(): string
{
if (!$this->absoluteUrl) {
// Guessing we're a http endpoint.
$this->absoluteUrl = 'http://'.
($this->getHeader('Host') ?? 'localhost').
$this->getUrl();
}
return $this->absoluteUrl;
}
/**
* Base url.
*
* @var string
*/
protected $baseUrl = '/';
/**
* Sets a base url.
*
* This url is used for relative path calculations.
*/
public function setBaseUrl(string $url)
{
$this->baseUrl = $url;
}
/**
* Returns the current base url.
*/
public function getBaseUrl(): string
{
return $this->baseUrl;
}
/**
* Returns the relative path.
*
* This is being calculated using the base url. This path will not start
* with a slash, so it will always return something like
* 'example/path.html'.
*
* If the full path is equal to the base url, this method will return an
* empty string.
*
* This method will also urldecode the path, and if the url was incoded as
* ISO-8859-1, it will convert it to UTF-8.
*
* If the path is outside of the base url, a LogicException will be thrown.
*/
public function getPath(): string
{
// Removing duplicated slashes.
$uri = str_replace('//', '/', $this->getUrl());
$uri = Uri\normalize($uri);
$baseUri = Uri\normalize($this->getBaseUrl());
if (0 === strpos($uri, $baseUri)) {
// We're not interested in the query part (everything after the ?).
list($uri) = explode('?', $uri);
return trim(decodePath(substr($uri, strlen($baseUri))), '/');
}
if ($uri.'/' === $baseUri) {
return '';
}
// A special case, if the baseUri was accessed without a trailing
// slash, we'll accept it as well.
throw new \LogicException('Requested uri ('.$this->getUrl().') is out of base uri ('.$this->getBaseUrl().')');
}
/**
* Equivalent of PHP's $_POST.
*
* @var array
*/
protected $postData = [];
/**
* Sets the post data.
*
* This is equivalent to PHP's $_POST superglobal.
*
* This would not have been needed, if POST data was accessible as
* php://input, but unfortunately we need to special case it.
*/
public function setPostData(array $postData)
{
$this->postData = $postData;
}
/**
* Returns the POST data.
*
* This is equivalent to PHP's $_POST superglobal.
*/
public function getPostData(): array
{
return $this->postData;
}
/**
* An array containing the raw _SERVER array.
*
* @var array
*/
protected $rawServerData;
/**
* Returns an item from the _SERVER array.
*
* If the value does not exist in the array, null is returned.
*
* @return string|null
*/
public function getRawServerValue(string $valueName)
{
return $this->rawServerData[$valueName] ?? null;
}
/**
* Sets the _SERVER array.
*/
public function setRawServerData(array $data)
{
$this->rawServerData = $data;
}
/**
* Serializes the request object as a string.
*
* This is useful for debugging purposes.
*/
public function __toString(): string
{
$out = $this->getMethod().' '.$this->getUrl().' HTTP/'.$this->getHttpVersion()."\r\n";
foreach ($this->getHeaders() as $key => $value) {
foreach ($value as $v) {
if ('Authorization' === $key) {
list($v) = explode(' ', $v, 2);
$v .= ' REDACTED';
}
$out .= $key.': '.$v."\r\n";
}
}
$out .= "\r\n";
$out .= $this->getBodyAsString();
return $out;
}
}

View File

@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
/**
* Request Decorator.
*
* This helper class allows you to easily create decorators for the Request
* object.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class RequestDecorator implements RequestInterface
{
use MessageDecoratorTrait;
/**
* Constructor.
*/
public function __construct(RequestInterface $inner)
{
$this->inner = $inner;
}
/**
* Returns the current HTTP method.
*/
public function getMethod(): string
{
return $this->inner->getMethod();
}
/**
* Sets the HTTP method.
*/
public function setMethod(string $method)
{
$this->inner->setMethod($method);
}
/**
* Returns the request url.
*/
public function getUrl(): string
{
return $this->inner->getUrl();
}
/**
* Sets the request url.
*/
public function setUrl(string $url)
{
$this->inner->setUrl($url);
}
/**
* Returns the absolute url.
*/
public function getAbsoluteUrl(): string
{
return $this->inner->getAbsoluteUrl();
}
/**
* Sets the absolute url.
*/
public function setAbsoluteUrl(string $url)
{
$this->inner->setAbsoluteUrl($url);
}
/**
* Returns the current base url.
*/
public function getBaseUrl(): string
{
return $this->inner->getBaseUrl();
}
/**
* Sets a base url.
*
* This url is used for relative path calculations.
*
* The base url should default to /
*/
public function setBaseUrl(string $url)
{
$this->inner->setBaseUrl($url);
}
/**
* Returns the relative path.
*
* This is being calculated using the base url. This path will not start
* with a slash, so it will always return something like
* 'example/path.html'.
*
* If the full path is equal to the base url, this method will return an
* empty string.
*
* This method will also urldecode the path, and if the url was incoded as
* ISO-8859-1, it will convert it to UTF-8.
*
* If the path is outside of the base url, a LogicException will be thrown.
*/
public function getPath(): string
{
return $this->inner->getPath();
}
/**
* Returns the list of query parameters.
*
* This is equivalent to PHP's $_GET superglobal.
*/
public function getQueryParameters(): array
{
return $this->inner->getQueryParameters();
}
/**
* Returns the POST data.
*
* This is equivalent to PHP's $_POST superglobal.
*/
public function getPostData(): array
{
return $this->inner->getPostData();
}
/**
* Sets the post data.
*
* This is equivalent to PHP's $_POST superglobal.
*
* This would not have been needed, if POST data was accessible as
* php://input, but unfortunately we need to special case it.
*/
public function setPostData(array $postData)
{
$this->inner->setPostData($postData);
}
/**
* Returns an item from the _SERVER array.
*
* If the value does not exist in the array, null is returned.
*
* @return string|null
*/
public function getRawServerValue(string $valueName)
{
return $this->inner->getRawServerValue($valueName);
}
/**
* Sets the _SERVER array.
*/
public function setRawServerData(array $data)
{
$this->inner->setRawServerData($data);
}
/**
* Serializes the request object as a string.
*
* This is useful for debugging purposes.
*/
public function __toString(): string
{
return $this->inner->__toString();
}
}

View File

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
/**
* The RequestInterface represents a HTTP request.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface RequestInterface extends MessageInterface
{
/**
* Returns the current HTTP method.
*/
public function getMethod(): string;
/**
* Sets the HTTP method.
*/
public function setMethod(string $method);
/**
* Returns the request url.
*/
public function getUrl(): string;
/**
* Sets the request url.
*/
public function setUrl(string $url);
/**
* Returns the absolute url.
*/
public function getAbsoluteUrl(): string;
/**
* Sets the absolute url.
*/
public function setAbsoluteUrl(string $url);
/**
* Returns the current base url.
*/
public function getBaseUrl(): string;
/**
* Sets a base url.
*
* This url is used for relative path calculations.
*
* The base url should default to /
*/
public function setBaseUrl(string $url);
/**
* Returns the relative path.
*
* This is being calculated using the base url. This path will not start
* with a slash, so it will always return something like
* 'example/path.html'.
*
* If the full path is equal to the base url, this method will return an
* empty string.
*
* This method will also urldecode the path, and if the url was incoded as
* ISO-8859-1, it will convert it to UTF-8.
*
* If the path is outside of the base url, a LogicException will be thrown.
*/
public function getPath(): string;
/**
* Returns the list of query parameters.
*
* This is equivalent to PHP's $_GET superglobal.
*/
public function getQueryParameters(): array;
/**
* Returns the POST data.
*
* This is equivalent to PHP's $_POST superglobal.
*/
public function getPostData(): array;
/**
* Sets the post data.
*
* This is equivalent to PHP's $_POST superglobal.
*
* This would not have been needed, if POST data was accessible as
* php://input, but unfortunately we need to special case it.
*/
public function setPostData(array $postData);
/**
* Returns an item from the _SERVER array.
*
* If the value does not exist in the array, null is returned.
*
* @return string|null
*/
public function getRawServerValue(string $valueName);
/**
* Sets the _SERVER array.
*/
public function setRawServerData(array $data);
}

188
vendor/sabre/http/lib/Response.php vendored Normal file
View File

@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
/**
* This class represents a single HTTP response.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Response extends Message implements ResponseInterface
{
/**
* This is the list of currently registered HTTP status codes.
*
* @var array
*/
public static $statusCodes = [
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authorative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status', // RFC 4918
208 => 'Already Reported', // RFC 5842
226 => 'IM Used', // RFC 3229
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a teapot', // RFC 2324
421 => 'Misdirected Request', // RFC7540 (HTTP/2)
422 => 'Unprocessable Entity', // RFC 4918
423 => 'Locked', // RFC 4918
424 => 'Failed Dependency', // RFC 4918
426 => 'Upgrade Required',
428 => 'Precondition Required', // RFC 6585
429 => 'Too Many Requests', // RFC 6585
431 => 'Request Header Fields Too Large', // RFC 6585
451 => 'Unavailable For Legal Reasons', // draft-tbray-http-legally-restricted-status
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version not supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage', // RFC 4918
508 => 'Loop Detected', // RFC 5842
509 => 'Bandwidth Limit Exceeded', // non-standard
510 => 'Not extended',
511 => 'Network Authentication Required', // RFC 6585
];
/**
* HTTP status code.
*
* @var int
*/
protected $status;
/**
* HTTP status text.
*
* @var string
*/
protected $statusText;
/**
* Creates the response object.
*
* @param string|int $status
* @param array $headers
* @param resource $body
*/
public function __construct($status = 500, array $headers = null, $body = null)
{
if (null !== $status) {
$this->setStatus($status);
}
if (null !== $headers) {
$this->setHeaders($headers);
}
if (null !== $body) {
$this->setBody($body);
}
}
/**
* Returns the current HTTP status code.
*/
public function getStatus(): int
{
return $this->status;
}
/**
* Returns the human-readable status string.
*
* In the case of a 200, this may for example be 'OK'.
*/
public function getStatusText(): string
{
return $this->statusText;
}
/**
* Sets the HTTP status code.
*
* This can be either the full HTTP status code with human readable string,
* for example: "403 I can't let you do that, Dave".
*
* Or just the code, in which case the appropriate default message will be
* added.
*
* @param string|int $status
*
* @throws \InvalidArgumentException
*/
public function setStatus($status)
{
if (ctype_digit($status) || is_int($status)) {
$statusCode = $status;
$statusText = self::$statusCodes[$status] ?? 'Unknown';
} else {
list(
$statusCode,
$statusText
) = explode(' ', $status, 2);
$statusCode = (int) $statusCode;
}
if ($statusCode < 100 || $statusCode > 999) {
throw new \InvalidArgumentException('The HTTP status code must be exactly 3 digits');
}
$this->status = $statusCode;
$this->statusText = $statusText;
}
/**
* Serializes the response object as a string.
*
* This is useful for debugging purposes.
*/
public function __toString(): string
{
$str = 'HTTP/'.$this->httpVersion.' '.$this->getStatus().' '.$this->getStatusText()."\r\n";
foreach ($this->getHeaders() as $key => $value) {
foreach ($value as $v) {
$str .= $key.': '.$v."\r\n";
}
}
$str .= "\r\n";
$str .= $this->getBodyAsString();
return $str;
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
/**
* Response Decorator.
*
* This helper class allows you to easily create decorators for the Response
* object.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ResponseDecorator implements ResponseInterface
{
use MessageDecoratorTrait;
/**
* Constructor.
*/
public function __construct(ResponseInterface $inner)
{
$this->inner = $inner;
}
/**
* Returns the current HTTP status code.
*/
public function getStatus(): int
{
return $this->inner->getStatus();
}
/**
* Returns the human-readable status string.
*
* In the case of a 200, this may for example be 'OK'.
*/
public function getStatusText(): string
{
return $this->inner->getStatusText();
}
/**
* Sets the HTTP status code.
*
* This can be either the full HTTP status code with human readable string,
* for example: "403 I can't let you do that, Dave".
*
* Or just the code, in which case the appropriate default message will be
* added.
*
* @param string|int $status
*/
public function setStatus($status)
{
$this->inner->setStatus($status);
}
/**
* Serializes the request object as a string.
*
* This is useful for debugging purposes.
*/
public function __toString(): string
{
return $this->inner->__toString();
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
/**
* This interface represents a HTTP response.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface ResponseInterface extends MessageInterface
{
/**
* Returns the current HTTP status code.
*/
public function getStatus(): int;
/**
* Returns the human-readable status string.
*
* In the case of a 200, this may for example be 'OK'.
*/
public function getStatusText(): string;
/**
* Sets the HTTP status code.
*
* This can be either the full HTTP status code with human readable string,
* for example: "403 I can't let you do that, Dave".
*
* Or just the code, in which case the appropriate default message will be
* added.
*
* @param string|int $status
*
* @throws \InvalidArgumentException
*/
public function setStatus($status);
}

243
vendor/sabre/http/lib/Sapi.php vendored Normal file
View File

@ -0,0 +1,243 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
use InvalidArgumentException;
/**
* PHP SAPI.
*
* This object is responsible for:
* 1. Constructing a Request object based on the current HTTP request sent to
* the PHP process.
* 2. Sending the Response object back to the client.
*
* It could be said that this class provides a mapping between the Request and
* Response objects, and php's:
*
* * $_SERVER
* * $_POST
* * $_FILES
* * php://input
* * echo()
* * header()
* * php://output
*
* You can choose to either call all these methods statically, but you can also
* instantiate this as an object to allow for polymorhpism.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Sapi
{
/**
* This static method will create a new Request object, based on the
* current PHP request.
*/
public static function getRequest(): Request
{
$serverArr = $_SERVER;
if ('cli' === PHP_SAPI) {
// If we're running off the CLI, we're going to set some default
// settings.
$serverArr['REQUEST_URI'] = $_SERVER['REQUEST_URI'] ?? '/';
$serverArr['REQUEST_METHOD'] = $_SERVER['REQUEST_METHOD'] ?? 'CLI';
}
$r = self::createFromServerArray($serverArr);
$r->setBody(fopen('php://input', 'r'));
$r->setPostData($_POST);
return $r;
}
/**
* Sends the HTTP response back to a HTTP client.
*
* This calls php's header() function and streams the body to php://output.
*/
public static function sendResponse(ResponseInterface $response)
{
header('HTTP/'.$response->getHttpVersion().' '.$response->getStatus().' '.$response->getStatusText());
foreach ($response->getHeaders() as $key => $value) {
foreach ($value as $k => $v) {
if (0 === $k) {
header($key.': '.$v);
} else {
header($key.': '.$v, false);
}
}
}
$body = $response->getBody();
if (null === $body) {
return;
}
if (is_callable($body)) {
$body();
return;
}
$contentLength = $response->getHeader('Content-Length');
if (null !== $contentLength) {
$output = fopen('php://output', 'wb');
if (is_resource($body) && 'stream' == get_resource_type($body)) {
if (PHP_INT_SIZE > 4) {
// use the dedicated function on 64 Bit systems
// a workaround to make PHP more possible to use mmap based copy, see https://github.com/sabre-io/http/pull/119
$left = (int) $contentLength;
// copy with 4MiB chunks
$chunk_size = 4 * 1024 * 1024;
stream_set_chunk_size($output, $chunk_size);
// If this is a partial response, flush the beginning bytes until the first position that is a multiple of the page size.
$contentRange = $response->getHeader('Content-Range');
// Matching "Content-Range: bytes 1234-5678/7890"
if (null !== $contentRange && preg_match('/^bytes\s([0-9]+)-([0-9]+)\//i', $contentRange, $matches)) {
// 4kB should be the default page size on most architectures
$pageSize = 4096;
$offset = (int) $matches[1];
$delta = ($offset % $pageSize) > 0 ? ($pageSize - $offset % $pageSize) : 0;
if ($delta > 0) {
$left -= stream_copy_to_stream($body, $output, min($delta, $left));
}
}
while ($left > 0) {
$copied = stream_copy_to_stream($body, $output, min($left, $chunk_size));
// stream_copy_to_stream($src, $dest, $maxLength) must return the number of bytes copied or false in case of failure
// But when the $maxLength is greater than the total number of bytes remaining in the stream,
// It returns the negative number of bytes copied
// So break the loop in such cases.
if ($copied <= 0) {
break;
}
$left -= $copied;
}
} else {
// workaround for 32 Bit systems to avoid stream_copy_to_stream
while (!feof($body)) {
fwrite($output, fread($body, 8192));
}
}
} else {
fwrite($output, $body, (int) $contentLength);
}
} else {
file_put_contents('php://output', $body);
}
if (is_resource($body)) {
fclose($body);
}
}
/**
* This static method will create a new Request object, based on a PHP
* $_SERVER array.
*
* REQUEST_URI and REQUEST_METHOD are required.
*/
public static function createFromServerArray(array $serverArray): Request
{
$headers = [];
$method = null;
$url = null;
$httpVersion = '1.1';
$protocol = 'http';
$hostName = 'localhost';
foreach ($serverArray as $key => $value) {
switch ($key) {
case 'SERVER_PROTOCOL':
if ('HTTP/1.0' === $value) {
$httpVersion = '1.0';
} elseif ('HTTP/2.0' === $value) {
$httpVersion = '2.0';
}
break;
case 'REQUEST_METHOD':
$method = $value;
break;
case 'REQUEST_URI':
$url = $value;
break;
// These sometimes show up without a HTTP_ prefix
case 'CONTENT_TYPE':
$headers['Content-Type'] = $value;
break;
case 'CONTENT_LENGTH':
$headers['Content-Length'] = $value;
break;
// mod_php on apache will put credentials in these variables.
// (fast)cgi does not usually do this, however.
case 'PHP_AUTH_USER':
if (isset($serverArray['PHP_AUTH_PW'])) {
$headers['Authorization'] = 'Basic '.base64_encode($value.':'.$serverArray['PHP_AUTH_PW']);
}
break;
// Similarly, mod_php may also screw around with digest auth.
case 'PHP_AUTH_DIGEST':
$headers['Authorization'] = 'Digest '.$value;
break;
// Apache may prefix the HTTP_AUTHORIZATION header with
// REDIRECT_, if mod_rewrite was used.
case 'REDIRECT_HTTP_AUTHORIZATION':
$headers['Authorization'] = $value;
break;
case 'HTTP_HOST':
$hostName = $value;
$headers['Host'] = $value;
break;
case 'HTTPS':
if (!empty($value) && 'off' !== $value) {
$protocol = 'https';
}
break;
default:
if ('HTTP_' === substr($key, 0, 5)) {
// It's a HTTP header
// Normalizing it to be prettier
$header = strtolower(substr($key, 5));
// Transforming dashes into spaces, and uppercasing
// every first letter.
$header = ucwords(str_replace('_', ' ', $header));
// Turning spaces into dashes.
$header = str_replace(' ', '-', $header);
$headers[$header] = $value;
}
break;
}
}
if (null === $url) {
throw new InvalidArgumentException('The _SERVER array must have a REQUEST_URI key');
}
if (null === $method) {
throw new InvalidArgumentException('The _SERVER array must have a REQUEST_METHOD key');
}
$r = new Request($method, $url, $headers);
$r->setHttpVersion($httpVersion);
$r->setRawServerData($serverArray);
$r->setAbsoluteUrl($protocol.'://'.$hostName.$url);
return $r;
}
}

20
vendor/sabre/http/lib/Version.php vendored Normal file
View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
/**
* This class contains the version number for the HTTP package.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Version
{
/**
* Full version number.
*/
const VERSION = '5.1.0';
}

415
vendor/sabre/http/lib/functions.php vendored Normal file
View File

@ -0,0 +1,415 @@
<?php
declare(strict_types=1);
namespace Sabre\HTTP;
use DateTime;
use InvalidArgumentException;
/**
* A collection of useful helpers for parsing or generating various HTTP
* headers.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
/**
* Parses a HTTP date-string.
*
* This method returns false if the date is invalid.
*
* The following formats are supported:
* Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate
* Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format
* Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
*
* See:
* http://tools.ietf.org/html/rfc7231#section-7.1.1.1
*
* @return bool|DateTime
*/
function parseDate(string $dateString)
{
// Only the format is checked, valid ranges are checked by strtotime below
$month = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)';
$weekday = '(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)';
$wkday = '(Mon|Tue|Wed|Thu|Fri|Sat|Sun)';
$time = '([0-1]\d|2[0-3])(\:[0-5]\d){2}';
$date3 = $month.' ([12]\d|3[01]| [1-9])';
$date2 = '(0[1-9]|[12]\d|3[01])\-'.$month.'\-\d{2}';
// 4-digit year cannot begin with 0 - unix timestamp begins in 1970
$date1 = '(0[1-9]|[12]\d|3[01]) '.$month.' [1-9]\d{3}';
// ANSI C's asctime() format
// 4-digit year cannot begin with 0 - unix timestamp begins in 1970
$asctime_date = $wkday.' '.$date3.' '.$time.' [1-9]\d{3}';
// RFC 850, obsoleted by RFC 1036
$rfc850_date = $weekday.', '.$date2.' '.$time.' GMT';
// RFC 822, updated by RFC 1123
$rfc1123_date = $wkday.', '.$date1.' '.$time.' GMT';
// allowed date formats by RFC 2616
$HTTP_date = "($rfc1123_date|$rfc850_date|$asctime_date)";
// allow for space around the string and strip it
$dateString = trim($dateString, ' ');
if (!preg_match('/^'.$HTTP_date.'$/', $dateString)) {
return false;
}
// append implicit GMT timezone to ANSI C time format
if (false === strpos($dateString, ' GMT')) {
$dateString .= ' GMT';
}
try {
return new DateTime($dateString, new \DateTimeZone('UTC'));
} catch (\Exception $e) {
return false;
}
}
/**
* Transforms a DateTime object to a valid HTTP/1.1 Date header value.
*/
function toDate(DateTime $dateTime): string
{
// We need to clone it, as we don't want to affect the existing
// DateTime.
$dateTime = clone $dateTime;
$dateTime->setTimezone(new \DateTimeZone('GMT'));
return $dateTime->format('D, d M Y H:i:s \G\M\T');
}
/**
* This function can be used to aid with content negotiation.
*
* It takes 2 arguments, the $acceptHeaderValue, which usually comes from
* an Accept header, and $availableOptions, which contains an array of
* items that the server can support.
*
* The result of this function will be the 'best possible option'. If no
* best possible option could be found, null is returned.
*
* When it's null you can according to the spec either return a default, or
* you can choose to emit 406 Not Acceptable.
*
* The method also accepts sending 'null' for the $acceptHeaderValue,
* implying that no accept header was sent.
*
* @param string|null $acceptHeaderValue
*
* @return string|null
*/
function negotiateContentType($acceptHeaderValue, array $availableOptions)
{
if (!$acceptHeaderValue) {
// Grabbing the first in the list.
return reset($availableOptions);
}
$proposals = array_map(
'Sabre\HTTP\parseMimeType',
explode(',', $acceptHeaderValue)
);
// Ensuring array keys are reset.
$availableOptions = array_values($availableOptions);
$options = array_map(
'Sabre\HTTP\parseMimeType',
$availableOptions
);
$lastQuality = 0;
$lastSpecificity = 0;
$lastOptionIndex = 0;
$lastChoice = null;
foreach ($proposals as $proposal) {
// Ignoring broken values.
if (null === $proposal) {
continue;
}
// If the quality is lower we don't have to bother comparing.
if ($proposal['quality'] < $lastQuality) {
continue;
}
foreach ($options as $optionIndex => $option) {
if ('*' !== $proposal['type'] && $proposal['type'] !== $option['type']) {
// no match on type.
continue;
}
if ('*' !== $proposal['subType'] && $proposal['subType'] !== $option['subType']) {
// no match on subtype.
continue;
}
// Any parameters appearing on the options must appear on
// proposals.
foreach ($option['parameters'] as $paramName => $paramValue) {
if (!array_key_exists($paramName, $proposal['parameters'])) {
continue 2;
}
if ($paramValue !== $proposal['parameters'][$paramName]) {
continue 2;
}
}
// If we got here, we have a match on parameters, type and
// subtype. We need to calculate a score for how specific the
// match was.
$specificity =
('*' !== $proposal['type'] ? 20 : 0) +
('*' !== $proposal['subType'] ? 10 : 0) +
count($option['parameters']);
// Does this entry win?
if (
($proposal['quality'] > $lastQuality) ||
($proposal['quality'] === $lastQuality && $specificity > $lastSpecificity) ||
($proposal['quality'] === $lastQuality && $specificity === $lastSpecificity && $optionIndex < $lastOptionIndex)
) {
$lastQuality = $proposal['quality'];
$lastSpecificity = $specificity;
$lastOptionIndex = $optionIndex;
$lastChoice = $availableOptions[$optionIndex];
}
}
}
return $lastChoice;
}
/**
* Parses the Prefer header, as defined in RFC7240.
*
* Input can be given as a single header value (string) or multiple headers
* (array of string).
*
* This method will return a key->value array with the various Prefer
* parameters.
*
* Prefer: return=minimal will result in:
*
* [ 'return' => 'minimal' ]
*
* Prefer: foo, wait=10 will result in:
*
* [ 'foo' => true, 'wait' => '10']
*
* This method also supports the formats from older drafts of RFC7240, and
* it will automatically map them to the new values, as the older values
* are still pretty common.
*
* Parameters are currently discarded. There's no known prefer value that
* uses them.
*
* @param string|string[] $input
*/
function parsePrefer($input): array
{
$token = '[!#$%&\'*+\-.^_`~A-Za-z0-9]+';
// Work in progress
$word = '(?: [a-zA-Z0-9]+ | "[a-zA-Z0-9]*" )';
$regex = <<<REGEX
/
^
(?<name> $token) # Prefer property name
\s* # Optional space
(?: = \s* # Prefer property value
(?<value> $word)
)?
(?: \s* ; (?: .*))? # Prefer parameters (ignored)
$
/x
REGEX;
$output = [];
foreach (getHeaderValues($input) as $value) {
if (!preg_match($regex, $value, $matches)) {
// Ignore
continue;
}
// Mapping old values to their new counterparts
switch ($matches['name']) {
case 'return-asynch':
$output['respond-async'] = true;
break;
case 'return-representation':
$output['return'] = 'representation';
break;
case 'return-minimal':
$output['return'] = 'minimal';
break;
case 'strict':
$output['handling'] = 'strict';
break;
case 'lenient':
$output['handling'] = 'lenient';
break;
default:
if (isset($matches['value'])) {
$value = trim($matches['value'], '"');
} else {
$value = true;
}
$output[strtolower($matches['name'])] = empty($value) ? true : $value;
break;
}
}
return $output;
}
/**
* This method splits up headers into all their individual values.
*
* A HTTP header may have more than one header, such as this:
* Cache-Control: private, no-store
*
* Header values are always split with a comma.
*
* You can pass either a string, or an array. The resulting value is always
* an array with each spliced value.
*
* If the second headers argument is set, this value will simply be merged
* in. This makes it quicker to merge an old list of values with a new set.
*
* @param string|string[] $values
* @param string|string[] $values2
*/
function getHeaderValues($values, $values2 = null): array
{
$values = (array) $values;
if ($values2) {
$values = array_merge($values, (array) $values2);
}
$result = [];
foreach ($values as $l1) {
foreach (explode(',', $l1) as $l2) {
$result[] = trim($l2);
}
}
return $result;
}
/**
* Parses a mime-type and splits it into:.
*
* 1. type
* 2. subtype
* 3. quality
* 4. parameters
*/
function parseMimeType(string $str): array
{
$parameters = [];
// If no q= parameter appears, then quality = 1.
$quality = 1;
$parts = explode(';', $str);
// The first part is the mime-type.
$mimeType = trim(array_shift($parts));
if ('*' === $mimeType) {
$mimeType = '*/*';
}
$mimeType = explode('/', $mimeType);
if (2 !== count($mimeType)) {
// Illegal value
var_dump($mimeType);
die();
throw new InvalidArgumentException('Not a valid mime-type: '.$str);
}
list($type, $subType) = $mimeType;
foreach ($parts as $part) {
$part = trim($part);
if (strpos($part, '=')) {
list($partName, $partValue) =
explode('=', $part, 2);
} else {
$partName = $part;
$partValue = null;
}
// The quality parameter, if it appears, also marks the end of
// the parameter list. Anything after the q= counts as an
// 'accept extension' and could introduce new semantics in
// content-negotation.
if ('q' !== $partName) {
$parameters[$partName] = $part;
} else {
$quality = (float) $partValue;
break; // Stop parsing parts
}
}
return [
'type' => $type,
'subType' => $subType,
'quality' => $quality,
'parameters' => $parameters,
];
}
/**
* Encodes the path of a url.
*
* slashes (/) are treated as path-separators.
*/
function encodePath(string $path): string
{
return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\)\/:@])/', function ($match) {
return '%'.sprintf('%02x', ord($match[0]));
}, $path);
}
/**
* Encodes a 1 segment of a path.
*
* Slashes are considered part of the name, and are encoded as %2f
*/
function encodePathSegment(string $pathSegment): string
{
return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\):@])/', function ($match) {
return '%'.sprintf('%02x', ord($match[0]));
}, $pathSegment);
}
/**
* Decodes a url-encoded path.
*/
function decodePath(string $path): string
{
return decodePathSegment($path);
}
/**
* Decodes a url-encoded path segment.
*/
function decodePathSegment(string $path): string
{
$path = rawurldecode($path);
$encoding = mb_detect_encoding($path, ['UTF-8', 'ISO-8859-1']);
switch ($encoding) {
case 'ISO-8859-1':
$path = utf8_encode($path);
}
return $path;
}